PyQt keep aspect ratio fixed
If I understood your question, you should try using a layout inside the main window.
I did this:
from PyQt5 import QtCore, QtGui, QtWidgetsclass 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_())
If you resize the window, the widgets inside it get resized.
First, answered by Marc and codeling in this question, heightForWidth
is only supported for QGraphicsLayout
's subclasses.
Second, how to make a fixed aspect ratio window (or top-level widget) in qt (or pyqt) is a question that have been asked for years. However, as far as I know, there is no standard way of doing so, and it is something surprisingly hard to achieve. In short, my way of doing this is use Qt.FramelessWindowHint
to create a frameless window without system move and resize function, and implement custom move and resize.
Explain important mechanism:
move:
- In
mousePressEvent
, keep the place where we last clicked on the widget(the draggable area). - In
mouseMoveEvent
, calculate the distance between the last clicked point and the current mouse location. Move the window according to this distance.
resize:
- Find the increase or decrease step size of width and height by dividing the minimum width and height of the window by their highest common factor.
- Use the step size to increase or decrease the window size to keep the aspect ratio.
A screenshot to show that it can resize according to the aspect ratio.
The following code should works with both PyQt5 and Pyside2.
from PyQt5.QtCore import Qt, QRect, QPoint, QEventfrom PyQt5.QtWidgets import (QLabel, QMainWindow, QApplication, QSizePolicy, QVBoxLayout, QWidget, QHBoxLayout, QPushButton)from enum import Enumclass 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 positionclass CursorPos(Enum): TOP = 1 BOTTOM = 2 RIGHT = 3 LEFT = 4 TOPRIGHT = 5 TOPLEFT = 6 BOTTOMRIGHT = 7 BOTTOMLEFT = 8 DEFAULT = 9if __name__ == "__main__": import sys app = QApplication(sys.argv) w = MainWindow() w.show() sys.exit(app.exec_())
Finally, I'd like to give special thanks to the authors and editors of this question, GLHF, DRPK, Elad Joseph, and SimoN SavioR. Without their contribution to the community, it wouldn't be possible to come up with this answer.