【问题标题】:PyQt keep aspect ratio fixedPyQt 保持纵横比固定
【发布时间】:2019-06-29 09:12:38
【问题描述】:

我正在开发一个 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 中不再起作用了吗?我错过了什么吗?

【问题讨论】:

    标签: python python-3.x pyqt pyqt5 aspect-ratio


    【解决方案1】:

    如果我理解您的问题,您应该尝试在主窗口内使用布局。

    我这样做了:

    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_())
    

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

    【讨论】:

    • heightForWidth() 在这里没有做任何事情。它不会被调用。 sizePolicycode 也不做任何事情。
    • @EugeneGill 是的,我只是从 OP 代码中得到它。我会编辑答案
    • OP 希望小部件具有固定的纵横比。我在这里问过类似的问题:*.com/questions/66282813/…
    【解决方案2】:

    首先,由 Marc 回答并编码 in this questionheightForWidth 仅支持 QGraphicsLayout 的子类。

    二、如何在qt(或pyqt)is a question that have been asked for years中制作固定纵横比的窗口(或*widget)。但是,据我所知,没有标准的方法可以做到这一点,而且很难实现。总之,我的做法是使用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 的作者和编辑。如果没有他们对社区的贡献,就不可能得出这个答案。

    【讨论】: