PyQt保持宽高比固定

7
我正在制作一个PyQt5 GUI,到目前为止,我只有编写Python脚本的经验,没有深入研究创建用户界面。
GUI将需要在不同的屏幕上使用(可能还包括一些旧的4:3比例屏幕),并且需要在不同的尺寸下看起来漂亮。 现在,我为了让我的生活更轻松,采用了强制窗口固定宽高比的方法,并根据窗口大小调整不同元素的大小。
from PyQt5 import QtCore, QtGui, QtWidgets

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent= None):
        super().__init__(parent)
        self.form_widget = FormWidget(self)
        self.setCentralWidget(self.form_widget)
        self.resize(200, 400)
        self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        self.sizePolicy.setHeightForWidth(True)
        self.setSizePolicy(self.sizePolicy)

    def heightForWidth(self, width):
        return width * 2

class FormWidget(QtWidgets.QWidget):

    def __init__(self, parent):
        super().__init__(parent)

    def resizeEvent(self, event):
        f = self.font()
        temp = event.size().height()
        f.setPixelSize(temp / 16)
        self.setFont(f)

        return super().resizeEvent(event)


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)

    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

根据窗口大小调整元素的大小可以正常工作,但窗口纵横比却不保留。我从旧的PyQt4线程中复制了使用 heightForWidth 的方法。这种方法在PyQt5中不再适用吗?我是不是漏掉了什么?

2个回答

0
首先,Marc和codeling在这个问题中回答说,heightForWidth仅支持QGraphicsLayout的子类。
其次,在qt(或pyqt)中如何制作一个固定纵横比的窗口(或顶层小部件)是多年来一直被问到的问题。然而,据我所知,没有标准的方法来实现这一点,这是一件令人惊讶的难事。简而言之,我做到这一点的方法是使用Qt.FramelessWindowHint创建一个无系统移动和调整大小功能的无边框窗口,并实现自定义移动和调整大小。
重要机制的解释:
移动:
  1. mousePressEvent 中,保留我们上次单击小部件的位置(可拖动区域)。
  2. mouseMoveEvent 中,计算上次单击点与当前鼠标位置之间的距离。根据此距离移动窗口。

调整大小:

  1. 通过将窗口的最小宽度和高度除以它们的最大公因数来找到宽度和高度的增加或减少步长。
  2. 使用步长来增加或减少窗口大小以保持纵横比。

屏幕截图显示可以根据纵横比调整大小。

以下代码应适用于 PyQt5 和 Pyside2。

from PyQt5.QtCore import Qt, QRect, QPoint, QEvent
from PyQt5.QtWidgets import (QLabel, QMainWindow, QApplication, QSizePolicy,
                             QVBoxLayout, QWidget, QHBoxLayout, QPushButton)
from enum import Enum


class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.setWindowFlags(Qt.FramelessWindowHint)

        self.createCostumTitleBar()

        self.setContentsMargins(0, 0, 0, 0)

        self.central = QWidget()
        self.central.setStyleSheet("background-color: #f8ecdf")

        self.centralLayout = QVBoxLayout()
        self.central.setLayout(self.centralLayout)
        self.centralLayout.addWidget(
            self.costumsystemmenu, alignment=Qt.AlignTop)
        self.centralLayout.setContentsMargins(0, 0, 0, 0)

        self.setCentralWidget(self.central)

        # Set the minimum size to avoid window being resized too small.

        self.setMinimumSize(300, 400)
        self.minheight = self.minimumHeight()
        self.minwidth = self.minimumWidth()

        self.resize(300, 400)

        # make sure your minium size have the same aspect ratio as the step.
        self.stepY = 4
        self.stepX = 3

        # install the event filter on this window.
        self.installEventFilter(self)
        self.grabarea.installEventFilter(self)

        self.cursorpos = CursorPos.DEFAULT
        self.iswindowpress = False

    def createCostumTitleBar(self):
        self.costumsystemmenu = QWidget()
        self.costumsystemmenu.setStyleSheet("background-color: #ccc")
        self.costumsystemmenu.setContentsMargins(0, 0, 0, 0)
        self.costumsystemmenu.setMinimumHeight(30)

        self.grabarea = QLabel("")
        self.grabarea.setStyleSheet("background-color: #ccc")
        self.grabarea.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Preferred)

        titlebarlayout = QHBoxLayout()
        titlebarlayout.setContentsMargins(11, 11, 11, 11)
        titlebarlayout.setSpacing(0)

        self.closeButton = QPushButton("X")
        self.closeButton.setSizePolicy(
            QSizePolicy.Minimum, QSizePolicy.Preferred)
        self.closeButton.clicked.connect(self.close)

        self.costumsystemmenu.setLayout(titlebarlayout)
        titlebarlayout.addWidget(self.grabarea)
        titlebarlayout.addWidget(self.closeButton, alignment=Qt.AlignRight)

        self.istitlebarpress = False

    def eventFilter(self, object, event):
        # The eventFilter() function must return true if the event
        # should be filtered, (i.e. stopped); otherwise it must return false.
        # https://doc.qt.io/qt-5/qobject.html#eventFilter

        # check if the object is the mainwindow.
        if object == self:

            if event.type() == QEvent.HoverMove:
                if not self.iswindowpress:
                    self.setCursorShape(event)
                return True

            elif event.type() == QEvent.MouseButtonPress:
                self.iswindowpress = True
                # Get the position of the cursor and map to the global coordinate of the widget.
                self.globalpos = self.mapToGlobal(event.pos())
                self.origingeometry = self.geometry()

                return True

            elif event.type() == QEvent.MouseButtonRelease:
                self.iswindowpress = False
                return True

            elif event.type() == QEvent.MouseMove:
                if self.cursorpos != CursorPos.DEFAULT and self.iswindowpress:
                    self.resizing(self.globalpos, event,
                                  self.origingeometry, self.cursorpos)

                return True

            else:
                return False

        elif object == self.grabarea:
            if event.type() == QEvent.MouseButtonPress:
                if event.button() == Qt.LeftButton and self.iswindowpress == False:
                    self.oldpos = event.globalPos()
                    self.oldwindowpos = self.pos()
                    self.istitlebarpress = True

                return True
            elif event.type() == QEvent.MouseButtonRelease:
                self.istitlebarpress = False
                return True
            elif event.type() == QEvent.MouseMove:
                if (self.istitlebarpress):
                    distance = event.globalPos()-self.oldpos
                    newwindowpos = self.oldwindowpos + distance
                    self.move(newwindowpos)
                return True
            else:
                return False
        else:
            return False

    # Change the cursor shape when the cursor is over different part of the window.
    def setCursorShape(self, event, handlersize=11):
        rect = self.rect()
        topLeft = rect.topLeft()
        topRight = rect.topRight()
        bottomLeft = rect.bottomLeft()
        bottomRight = rect.bottomRight()

        # get the position of the cursor
        pos = event.pos()

        # make the resize handle include some space outside the window,
        # can avoid user move too fast and loss the handle.
        # top handle
        if pos in QRect(QPoint(topLeft.x()+handlersize, topLeft.y()-2*handlersize),
                        QPoint(topRight.x()-handlersize, topRight.y()+handlersize)):
            self.setCursor(Qt.SizeVerCursor)
            self.cursorpos = CursorPos.TOP

        # bottom handle
        elif pos in QRect(QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize),
                          QPoint(bottomRight.x()-handlersize, bottomRight.y()+2*handlersize)):
            self.setCursor(Qt.SizeVerCursor)
            self.cursorpos = CursorPos.BOTTOM

        # right handle
        elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()+handlersize),
                          QPoint(bottomRight.x()+2*handlersize, bottomRight.y()-handlersize)):
            self.setCursor(Qt.SizeHorCursor)
            self.cursorpos = CursorPos.RIGHT

        # left handle
        elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()+handlersize),
                          QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize)):
            self.setCursor(Qt.SizeHorCursor)
            self.cursorpos = CursorPos.LEFT

        # topRight handle
        elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()-2*handlersize),
                          QPoint(topRight.x()+2*handlersize, topRight.y()+handlersize)):
            self.setCursor(Qt.SizeBDiagCursor)
            self.cursorpos = CursorPos.TOPRIGHT

        # topLeft handle
        elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()-2*handlersize),
                          QPoint(topLeft.x()+handlersize, topLeft.y()+handlersize)):
            self.setCursor(Qt.SizeFDiagCursor)
            self.cursorpos = CursorPos.TOPLEFT

        # bottomRight handle
        elif pos in QRect(QPoint(bottomRight.x()-handlersize, bottomRight.y()-handlersize),
                          QPoint(bottomRight.x()+2*handlersize, bottomRight.y()+2*handlersize)):
            self.setCursor(Qt.SizeFDiagCursor)
            self.cursorpos = CursorPos.BOTTOMRIGHT

        # bottomLeft handle
        elif pos in QRect(QPoint(bottomLeft.x()-2*handlersize, bottomLeft.y()-handlersize),
                          QPoint(bottomLeft.x()+handlersize, bottomLeft.y()+2*handlersize)):
            self.setCursor(Qt.SizeBDiagCursor)
            self.cursorpos = CursorPos.BOTTOMLEFT

        # Default is the arrow cursor.
        else:
            self.setCursor(Qt.ArrowCursor)
            self.cursorpos = CursorPos.DEFAULT

    def resizing(self, originpos, event, geo, cursorpos):
        newpos = self.mapToGlobal(event.pos())

        # find the distance between new and old cursor position.
        dist = newpos - originpos

        # calculate the steps to grow or srink.
        if cursorpos in [CursorPos.TOP, CursorPos.BOTTOM,
                         CursorPos.TOPRIGHT,
                         CursorPos.BOTTOMLEFT, CursorPos.BOTTOMRIGHT]:
            steps = dist.y()//self.stepY
        elif cursorpos in [CursorPos.LEFT, CursorPos.TOPLEFT, CursorPos.RIGHT]:
            steps = dist.x()//self.stepX

        # if the distance moved is too stort, grow or srink by 1 step.
        if steps == 0:
            steps = -1 if dist.y() < 0 or dist.x() < 0 else 1

        oldwidth = geo.width()
        oldheight = geo.height()

        oldX = geo.x()
        oldY = geo.y()

        if cursorpos in [CursorPos.TOP, CursorPos.TOPRIGHT]:

            width = oldwidth - steps * self.stepX
            height = oldheight - steps * self.stepY

            newX = oldX
            newY = oldY + (steps * self.stepY)

            # check if the new size is within the size limit.
            if height >= self.minheight and width >= self.minwidth:
                self.setGeometry(newX, newY, width, height)

        elif cursorpos in [CursorPos.BOTTOM, CursorPos.RIGHT, CursorPos.BOTTOMRIGHT]:

            width = oldwidth + steps * self.stepX
            height = oldheight + steps * self.stepY

            self.resize(width, height)

        elif cursorpos in [CursorPos.LEFT, CursorPos.BOTTOMLEFT]:

            width = oldwidth - steps * self.stepX
            height = oldheight - steps * self.stepY

            newX = oldX + steps * self.stepX
            newY = oldY

            # check if the new size is within the size limit.
            if height >= self.minheight and width >= self.minwidth:
                self.setGeometry(newX, newY, width, height)

        elif cursorpos == CursorPos.TOPLEFT:

            width = oldwidth - steps * self.stepX
            height = oldheight - steps * self.stepY

            newX = oldX + steps * self.stepX
            newY = oldY + steps * self.stepY

            # check if the new size is within the size limit.
            if height >= self.minheight and width >= self.minwidth:
                self.setGeometry(newX, newY, width, height)

        else:
            pass

# cursor position
class CursorPos(Enum):
    TOP = 1
    BOTTOM = 2
    RIGHT = 3
    LEFT = 4
    TOPRIGHT = 5
    TOPLEFT = 6
    BOTTOMRIGHT = 7
    BOTTOMLEFT = 8
    DEFAULT = 9


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

最后,我想特别感谢this question的作者和编辑,GLHF、DRPK、Elad Joseph和SimoN SavioR。没有他们对社区的贡献,就不可能得出这个答案。

0
如果我理解你的问题,你应该尝试在主窗口内使用布局。
我做了这个:
from PyQt5 import QtCore, QtGui, QtWidgets

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent= None):
        super().__init__(parent)
        self.central_widget = QtWidgets.QWidget()
        self.central_layout = QtWidgets.QVBoxLayout()
        self.setCentralWidget(self.central_widget)
        self.central_widget.setLayout(self.central_layout)
        # Lets create some widgets inside
        self.label = QtWidgets.QLabel()
        self.list_view = QtWidgets.QListView()
        self.push_button = QtWidgets.QPushButton()
        self.label.setText('Hi, this is a label. And the next one is a List View :')
        self.push_button.setText('Push Button Here')
        # Lets add the widgets
        self.central_layout.addWidget(self.label)
        self.central_layout.addWidget(self.list_view)
        self.central_layout.addWidget(self.push_button)
      
if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

如果您调整窗口大小,其中的小部件也会被调整大小。

enter image description here

enter image description here


1
heightForWidth()在这里没有任何作用。它不会被调用。sizePolicy代码也没有任何作用。 - Eugene Gill
@EugeneGill 是的,我只是从原始代码中复制了那部分。我会编辑答案。 - Alejandro Condori
OP希望小部件具有固定的纵横比。我在这里提出了一个类似的问题:https://stackoverflow.com/questions/66282813/pyqt-pyside-maintain-grid-layout-widget-as-square - Eugene Gill

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接