这很棘手,并且需要对布局问题有一些了解:根本没有标准的方法让复杂的微件始终遵循给定的纵横比,特别是如果需要该微件的大小来布局其他微件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(如程序启动时所示)
Size width greater than height个
Size height greater than width个
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.