我正在try 创建一个小部件,其中包含4个显示文本的标签和一个显示图像的标签. 主布局是包含以下内容的QVBoxLayout:

  • 带有两个文本标签的QHBoxLayout
  • 图像的标签
  • 带有最后两个标签的QHBoxLayout

我为图像定制了QLabel,以便在保持初始长宽比的同时处理重新zoom .

我想要的是约束两个QHBoxLayout,这样当我调整Widget=>文本应该始终粘在图像角落时,它的宽度与显示在中心的图像相同

Expected behavior Actual behavior
expected actual

我try 了使用大小提示和大小策略,但没有任何成功.我怎么能这么做呢?

以下是我目前拥有的代码:

import sys

from PySide6.QtCore import Qt
from PySide6.QtGui import QImage, QPixmap, QResizeEvent
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget


class Thumbnail(QWidget):
    def __init__(self, image_preview: QPixmap, metadata_1: str, metadata_2: str, metadata_3: str, metadata_4: str):
        super().__init__()

        # infos settings
        metadata_1_label = QLabel()
        metadata_1_label.setText(metadata_1)
        metadata_1_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom)
        metadata_2_label = QLabel()
        metadata_2_label.setText(metadata_2)
        metadata_2_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignBottom)

        image = ScaledImageLabel()
        image.setPixmap(image_preview)
        image.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)

        metadata_3_label = QLabel()
        metadata_3_label.setText(metadata_3)
        metadata_3_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        metadata_4_label = QLabel()
        metadata_4_label.setText(metadata_4)
        metadata_4_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop)

        # layout organization
        top_metadata_layout = QHBoxLayout()
        top_metadata_layout.addWidget(metadata_1_label)
        top_metadata_layout.addWidget(metadata_2_label)

        bot_metadata_layout = QHBoxLayout()
        bot_metadata_layout.addWidget(metadata_3_label)
        bot_metadata_layout.addWidget(metadata_4_label)

        main_layout = QVBoxLayout()
        main_layout.addLayout(top_metadata_layout)
        main_layout.addWidget(image)
        main_layout.addLayout(bot_metadata_layout)
        main_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter)

        self.setLayout(main_layout)


class ScaledImageLabel(QLabel):
    def __init__(self):
        super().__init__()
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.setMinimumSize(100, 100)

    def resizeEvent(self, event: QResizeEvent):
        self.setPixmap(self._original_pixmap)
        return super().resizeEvent(event)

    def setPixmap(self, arg__1: QPixmap | QImage | str):
        self._original_pixmap = arg__1
        QLabel.setPixmap(self, self._original_pixmap.scaled(self.frameSize(), Qt.AspectRatioMode.KeepAspectRatio))


def main():
    app = QApplication(sys.argv)

    dummy_preview = QPixmap("tests\\data\\png_data\\IMG_6867.png")
    window = Thumbnail(dummy_preview, "Once", "Upon", "A", "Time")
    window.show()

    app.exec()


if __name__ == "__main__":
    sys.exit(main())

推荐答案

这很棘手,并且需要对布局问题有一些了解:根本没有标准的方法让复杂的微件始终遵循给定的纵横比,特别是如果需要该微件的大小来布局其他微件and为顶层窗口设置适当的大小.

在您的例子中,调整图像大小只是因为它有一个扩展策略,允许它占用所有可用空间:结果是显示图像的QLabel的大小总是等于或大于最终显示的图像.

如果将以下行添加到代码中,您将看到,只要窗口大小的一个维度大于图像纵横比所提供的维度,图像标签占用的空间总是远远超过图像所需的空间:

    window.setStyleSheet('QLabel { border: 1px solid red;}')

不幸的是,这与父布局无关,因为它不知道标签的visual个内容:其他小部件是根据其real大小放置的,这就是为什么标签被放置在水平图像边界之外的原因.请注意,如果您的布局将标签放置在图像的两侧,并且窗口高度远远大于其所需的比例,也会发生这种情况.

That is a commonly debated issue with widget layouts (not only for Qt): there is no absolute way to have widgets that have a constant aspect ratio, and each case requires different handling depending on the situation.
People often argue about this issue replying that web browsers are capable of such feature, but that's a misconception: browsers are, by nature, scrollable areas, and that aspect makes actually easy to resize widgets while keeping a possible aspect ratio: since the viewport is theoretically infinite, there's always available space to allow that. Windowed UIs don't have such luxury.

通常建议的解决方案是创建一个定制的QLayout子类,但这种"简单"的情况使这种情况变得不必要的复杂:因为对象的逻辑布局是已知的,所以可以通过在QWidget子类中手动布局这些对象来实现.

请注意,在下面的代码中,我完全go 掉了图像标签,因为它对于这个实现来说是不必要的,而且只会增加我们可以明显避免的复杂性.还需要注意的是,尽管在您的情况下不存在严格的问题(在扩展策略的间接"帮助"下),但是不鼓励在resizeEvent()中调用可能改变小部件(包括setPixmap())大小的函数,因为它可能导致部分递归(如果不是无限递归).如果删除setSizePolicy()行并try 立即调整窗口大小(例如,通过最大化它),您可能会注意到这个问题,这可能会显示图像调整大小的某种"进度",直到它达到最终大小.

此外,您的代码只有在ScaledImageLabel被显示或添加到已经活动的布局之前被实际调用setPixmap()时才能运行,但如果没有发生这种情况,就会导致崩溃,因为不存在_original_pixmap属性.在实现像resizeEvent()这样的事件处理程序时,如果它们依赖于可能在运行时创建的动态属性和属性,则必须非常小心.

现在的概念是考虑标签的大小作为最小大小的参考,并最终添加更多的页边距/间距.这对于两个方面是必要的:

  • 正确返回有效大小(最小值和提示);
  • 实际设置每个对象的所有几何图形:标签和图像;

The minimum size and size hints for the widget are based on the sizes of the labels, and eventually expanded, depending on the requested purpose: since you were using a layout, I considered the default layout margins and spacings of the current style (so that it will look like a "real" Qt layout), then I added the minimum size you set for the image label for the minimum overall size, and the actual image size for the regular hint.
For instance (and assuming that the pixmap is valid), if the image width with added layout borders is smaller than the "minimum" size, it will allow resizing to smaller dimensions, while the size hint will always try to respect the original image size, which is what Qt will try to use when the program is first shown. Be aware, though, that Qt always provides a size hint for the top level widget that is at most 2/3 of the screen in which it is being shown: a bigger size hint will always be bound to that computed size.

The geometry of all objects is finally computed in a similar way within the resizeEvent().
We get the minimum size required for all the labels, subtract it from the actual widget size to get the remaining space for the image, then we set the final geometry of the image and compute the new geometries of each label based on that: the geometry of the top left label is placed above the top left corner of the image, and so on.
The call to setMinimumSize() is required since we're not using an actual QLayout.

Finally, the paintEvent() will consider the pixmap rect set within resizeEvent() in order to properly paint the image at its correct geometry.
Note that, unlike your resizeEvent(), while the existence of that rectangle attribute is a "guessed" assumption, that's also a valid and educated guess, since widgets always receive a resizeEvent() before being shown and finally painted.

以下是使用512x512的源图像的结果,具体取决于窗口大小.

Original size(如程序启动时所示)

initial window

Size width greater than height

window resized as wider

Size height greater than width

window resized as taller

class Thumbnail(QWidget):
    def __init__(self, image_preview: QPixmap, metadata_1: str, metadata_2: str, metadata_3: str, metadata_4: str):
        super().__init__()
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

        self.tlLabel = QLabel(metadata_1, self)
        self.trLabel = QLabel(metadata_2, self)
        self.blLabel = QLabel(metadata_3, self)
        self.brLabel = QLabel(metadata_4, self)
        self.labels = (self.tlLabel, self.trLabel, self.blLabel, self.brLabel)

        self.pixmap = QPixmap(image_preview)

        self.setMinimumSize(self.minimumSizeHint())

    def changeEvent(self, event):
        super().changeEvent(event)
        if event.type() == event.Type.StyleChange:
            self.setMinimumSize(self.minimumSizeHint())
            self.updateGeometry()

    def labelHints(self):
        return (l.sizeHint() for l in self.labels)

    def styleLayoutData(self):
        style = self.style()
        return (
            style.pixelMetric(style.PixelMetric.PM_LayoutLeftMargin), 
            style.pixelMetric(style.PixelMetric.PM_LayoutHorizontalSpacing), 
            style.pixelMetric(style.PixelMetric.PM_LayoutRightMargin), 
            style.pixelMetric(style.PixelMetric.PM_LayoutTopMargin), 
            style.pixelMetric(style.PixelMetric.PM_LayoutVerticalSpacing), 
            style.pixelMetric(style.PixelMetric.PM_LayoutBottomMargin), 
        )

    def hintData(self):
        tl, tr, bl, br = self.labelHints()
        minWidth = max((tl + tr).width(), (bl + br).width())
        minHeight = max((tl + bl).height(), (tr + br).height())

        left, hs, right, top, vs, bottom = self.styleLayoutData()

        return (
            QSize(minWidth + hs, minHeight + vs * 2), 
            QSize(left + right, top + bottom)
        )

    def minimumSizeHint(self):
        hint, margins = self.hintData()
        if not self.pixmap.isNull():
            hint = self.pixmap.size().boundedTo(QSize(100, 100))
        else:
            hint += QSize(100, 100)
        return hint + margins

    def sizeHint(self):
        hint, margins = self.hintData()
        if not self.pixmap.isNull():
            pmSize = QSize(100, 100).expandedTo(self.pixmap.size() + margins)
            hint = QSize(
                max(hint.width(), pmSize.width()), 
                hint.height() + pmSize.height()
            )
        else:
            hint += QSize(100, 100) + margins
        return hint

    def resizeEvent(self, event):
        tl, tr, bl, br = self.labelHints()
        minWidth = max((tl + tr).width(), (bl + br).width())
        minHeight = max((tl + bl).height(), (tr + br).height())

        left, hs, right, top, vs, bottom = self.styleLayoutData()

        available = self.size()
        pmWidth = max(minWidth, available.width() - (left + right))
        pmHeight = available.height() - (top + bottom + minHeight + vs * 2)

        if self.pixmap.isNull():
            pmSize = QSize(100, 100).scaled(
                pmWidth, pmHeight, Qt.AspectRatioMode.KeepAspectRatio)
        else:
            pmSize = self.pixmap.size().scaled(
                pmWidth, pmHeight, Qt.AspectRatioMode.KeepAspectRatio)

        self.pmRect = QRect(0, 0, pmSize.width(), pmSize.height())
        self.pmRect.moveCenter(self.rect().center())

        tlGeo = QRect(QPoint(), tl)
        tlGeo.moveBottomLeft(QPoint(self.pmRect.x(), self.pmRect.y() - vs))
        self.tlLabel.setGeometry(tlGeo)
        trGeo = QRect(QPoint(), tr)
        trGeo.moveBottomRight(QPoint(self.pmRect.right(), self.pmRect.y() - vs))
        self.trLabel.setGeometry(trGeo)
        blGeo = QRect(QPoint(), bl)
        blGeo.moveTopLeft(QPoint(self.pmRect.x(), vs + self.pmRect.bottom()))
        self.blLabel.setGeometry(blGeo)
        brGeo = QRect(QPoint(), br)
        brGeo.moveTopRight(QPoint(self.pmRect.right(), vs + self.pmRect.bottom()))
        self.brLabel.setGeometry(brGeo)

    def paintEvent(self, event):
        qp = QPainter(self)
        if self.pixmap.isNull():
            qp.drawRect(self.pmRect)
            pixmap = QPixmap(':/qt-project.org/styles/commonstyle/images/file-16.png')
            if pixmap.isNull():
                return
            imgRect = pixmap.rect()
            imgRect.moveCenter(self.rect().center())
        else:
            pixmap = self.pixmap
            imgRect = self.pmRect
        qp.setRenderHint(qp.RenderHint.SmoothPixmapTransform)
        qp.drawPixmap(imgRect, pixmap)

如您所见,这相当复杂.

It can be made a bit simpler if you don't consider style margins, and if you completely get rid of the labels (by using QFontMetrics to get text size hints and QPainter to directly draw the text).
Still, the issue remains: respecting the aspect ratio for a windowed widget is never an easy matter.

Python相关问答推荐

如何从. text中进行pip安装跳过无法访问的库

当变量也可以是无或真时,判断是否为假

如何使用函数正确索引收件箱?

每个组每第n行就有Pandas

保留包含pandas pandras中文本的列

在Python中,什么表达相当于0x1.0p-53?

socket.gaierror:[Errno -2]名称或服务未知|Firebase x Raspberry Pi

如何观察cv2.erode()的中间过程?

按照行主要蛇扫描顺序对点列表进行排序

在使用Guouti包的Python中运行MPP模型时内存不足

比较两个二元组列表,NP.isin

. str.替换pandas.series的方法未按预期工作

Pandas 都是(),但有一个门槛

从numpy数组和参数创建收件箱

在Wayland上使用setCellWidget时,try 编辑QTable Widget中的单元格时,PyQt 6崩溃

Telethon加入私有频道

dask无groupby(ddf. agg([min,max])?''''

python中csv. Dictreader. fieldname的类型是什么?'

Python避免mypy在相互引用中从另一个类重定义类时失败

使用Openpyxl从Excel中的折线图更改图表样式