首页 > 解决方案 > 当固定纵横比 QWidget 无法填满整个窗口时创建填充

问题描述

自定义小部件(类名 MyLabel,继承 QLabel)具有固定的纵横比 16:9。

当我调整窗口大小时,标签是左上对齐的,除非窗口恰好是 16:9,在这种情况下它会完美地填充窗口。

如何让标签居中?我已经查看了尺寸政策、对齐方式、使用空间项和拉伸,但我似乎无法让它按预期工作。

这是一个最小的可重现示例:

import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import QSize, Qt
from PyQt5.Qt import QVBoxLayout, QWidget

class MyLabel(QLabel):
    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setStyleSheet("background-color: lightgreen") # Just for visibility

    def resizeEvent(self, event):
        # Size of 16:9 and scale it to the new size maintaining aspect ratio.
        new_size = QSize(16, 9)
        new_size.scale(event.size(), Qt.KeepAspectRatio)
        self.resize(new_size)
    

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__(None)

        # Main widget and layout, and set as centralWidget
        self.main_layout = QVBoxLayout()
        self.main_widget = QWidget()
        self.main_widget.setLayout(self.main_layout)
        self.setCentralWidget(self.main_widget)

        # Add button to main_layout
        label = MyLabel("Hello World")
        self.main_layout.addWidget(label)

        self.show()

app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())

期望结果的例子:

在此处输入图像描述

在此处输入图像描述

实际结果示例:

在此处输入图像描述

在此处输入图像描述

标签: pythonpyqt5

解决方案


不幸的是,Qt 没有为需要固定纵横比的小部件提供直接的解决方案。

旧文档中有一些痕迹,但主要问题是:

  • 所有与小部件、布局和大小策略的纵横比(hasHeightForWidth()等)相关的功能都只考虑大小提示,因此如果小部件由布局手动调整大小,则没有可用的约束;
  • 正如文档报告moveEvent()的那样,在or中更改小部件的几何形状resizeEvent()可能会导致无限递归;
  • 在保持纵横比的同时(正确)控制尺寸增长或缩小是不可能的;

为了完整起见,这里是这个问题的部分解决方案,但请注意 QLabel 是一个非常特殊的小部件,它有一些与其文本表示相关的约束(最重要的是,具有富文本和/或自动换行)。

class MyLabel(QLabel):
    lastRect = None
    isResizing = False
    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setStyleSheet("background-color: lightgreen")
        self.setScaledContents(True)

    def restoreRatio(self, lastRect=None):
        if self.isResizing:
            return
        rect = QRect(QPoint(), 
            QSize(16, 9).scaled(self.size(), Qt.KeepAspectRatio))
        if not lastRect:
            lastRect = self.geometry()
        rect.moveCenter(lastRect.center())
        if rect != lastRect:
            self.isResizing = True
            self.setGeometry(rect)
            self.isResizing = False
        self.lastRect = None

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        if self.pixmap():
            return width * self.pixmap().height() / self.pixmap().width()
        return width * 9 / 16

    def sizeHint(self):
        if self.pixmap():
            return self.pixmap().size()
        return QSize(160, 90)

    def moveEvent(self, event):
        self.lastRect = self.geometry()

    def resizeEvent(self, event):
        self.restoreRatio(self.lastRect)

由于目的是显示图像,另一种可能性是自己手动绘制所有内容,您根本不需要 QLabel,您可以覆盖paintEventQWidget 的,但出于性能目的,它可能会稍微最好使用带有子 QLabel 的容器小部件:理论上这会使事情变得更快,因为所有计算都完全在 Qt 中完成:

class ParentedLabel(QWidget):
    def __init__(self, pixmap=None):
        super().__init__()
        self.child = QLabel(self, scaledContents=True)
        if pixmap:
            self.child.setPixmap(pixmap)

    def setPixmap(self, pixmap):
        self.child.setPixmap(pixmap)
        self.updateGeometry()

    def updateChild(self):
        if self.child.pixmap():
            r = self.child.pixmap().rect()
            size = self.child.pixmap().size().scaled(
                self.size(), Qt.KeepAspectRatio)
            r = QRect(QPoint(), size)
            r.moveCenter(self.rect().center())
            self.child.setGeometry(r)

    def hasHeightForWidth(self):
        return bool(self.child.pixmap())

    def heightForWidth(self, width):
        return width * self.child.pixmap().height() / self.child.pixmap().width()

    def sizeHint(self):
        if self.child.pixmap():
            return self.child.pixmap().size()
        return QSize(160, 90)

    def moveEvent(self, event):
        self.updateChild()

    def resizeEvent(self, event):
        self.updateChild()

最后,另一种可能性是使用 QGraphicsView,这可能是所有方法中更快的方法,但有一个小缺点:基于给定尺寸提示显示的图像可能会比原始图像略小(几个像素),与结果,由于调整大小,它看起来有点“失焦”。

class ViewLabel(QGraphicsView):
    def __init__(self, pixmap=None):
        super().__init__()
        self.setStyleSheet('ViewLabel { border: 0px solid none; }')
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scene = QGraphicsScene()
        self.setScene(scene)
        self.pixmapItem = QGraphicsPixmapItem(pixmap)
        self.pixmapItem.setTransformationMode(Qt.SmoothTransformation)
        scene.addItem(self.pixmapItem)

    def setPixmap(self, pixmap):
        self.pixmapItem.setPixmap(pixmap)
        self.updateGeometry()
        self.updateScene()

    def updateScene(self):
        self.fitInView(self.pixmapItem, Qt.KeepAspectRatio)

    def hasHeightForWidth(self):
        return not bool(self.pixmapItem.pixmap().isNull())

    def heightForWidth(self, width):
        return width * self.pixmapItem.pixmap().height() / self.pixmapItem.pixmap().width()

    def sizeHint(self):
        if not self.pixmapItem.pixmap().isNull():
            return self.pixmapItem.pixmap().size()
        return QSize(160, 90)

    def resizeEvent(self, event):
        self.updateScene()

推荐阅读