Python 当固定纵横比QWidget无法填充整个窗口时创建填充

Python 当固定纵横比QWidget无法填充整个窗口时创建填充,python,pyqt5,Python,Pyqt5,自定义小部件(类名MyLabel,继承QLabel)具有固定的纵横比16:9 当我调整窗口大小时,标签是左上对齐的,除非窗口正好是16:9,在这种情况下,标签会完全填充窗口 如何使标签居中?我已经研究了大小策略、对齐、使用spaceitems和拉伸,但我似乎无法让它按预期工作 以下是一个最小的可复制示例: import sys from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow from PyQt5.QtCore impor

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

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

如何使标签居中?我已经研究了大小策略、对齐、使用spaceitems和拉伸,但我似乎无法让它按预期工作

以下是一个最小的可复制示例:

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_())
预期结果的例子:

实际结果的例子:


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

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

  • 与小部件、布局和大小策略的纵横比(
    hasHeightForWidth()
    etc)相关的所有函数仅用于大小提示,因此,如果通过布局手动调整小部件的大小,则没有可用的约束
  • 因为在
    moveEvent()
    resizeEvent()
    中更改小部件的几何结构可能会导致无限递归
  • 在保持纵横比的同时,不可能(正确地)控制尺寸的增长或收缩
为了完整起见,这里提供了这个问题的部分解决方案,但请注意QLabel是一个非常特殊的小部件,它与文本表示(最重要的是,使用富文本和/或word wrap)有一些约束

由于其目的是显示图像,另一种可能是自己手动绘制所有内容,而您根本不需要QLabel,只需覆盖QWidget的
paintEvent
,但出于性能目的,使用带有子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()

要正确实现一个可调整大小的小部件来保持Qt上的纵横比并不容易(实际上,有时几乎是不可能的)。还要记住,在
resizeEvent()
中更改大小是非常困难的,因为它可能导致无限递归。问题是:您是否仅出于示例目的使用QLabel,而实际上您将使用自定义小部件,或者您绝对需要该类?或者你想展示一个保持纵横比的缩放图像?谢谢你对@musicamante的评论。在我的实际问题中,我使用opencv在一个确实继承了qlabel的小部件中显示视频。我愿意接受这里的任何其他建议。我有一个实现,我实际上对resizeevent进行了裁剪,这样整个主窗口都会保持纵横比,但这不是很好,在捕捉到侧面(顶部或底部)时会变得有点奇怪。Mmh。这可能会变得棘手,因为这取决于opencv实际如何使用QLabel。除了定位问题之外,您的实现工作正常吗?我的意思是,视频是否正确显示(位置和大小),或者是否被裁剪(可能在其左侧/底部)?事实上,我读取了一帧并将其放在pixmap中,所以没有问题。如果我们解决了这个问题中的例子,我们就可以开始了。惊人的答案,谢谢!
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()