Qt:调整包含QPixmap的QLabel的大小,同时保持其纵横比

99

我使用QLabel来向用户显示一个更大、动态变化的QPixmap内容。根据可用空间,将此标签缩小/放大会很好。屏幕尺寸并不总是像QPixmap那样大。

如何修改QLabel的QSizePolicy和sizeHint()以重新调整QPixmap大小,同时保持原始QPixmap的宽高比?

我无法修改QLabel的sizeHint(),将minimumSize()设置为零也没有帮助。在QLabel上设置hasScaledContents()允许其增长,但破坏了宽高比...

虽然对QLabel进行子类化有所帮助,但这个解决方案对于一个简单的问题添加了太多的代码...

有没有聪明的提示可以在进行子类化的情况下完成这个任务?


您所说的动态更改是指像素数据还是尺寸? - r_ahlskog
我的意思是在当前布局中QLabel的尺寸。QPixmap应该保持其大小、内容和尺寸。此外,如果调整大小(实际上是缩小)可以“自动地”填充可用空间,直到原始QPixmap的大小,那就太好了。所有这些都是通过子类化完成的... - marvin2k
10个回答

115

为了改变标签大小,您可以为标签选择适当的大小策略,例如扩展或最小扩展。

您可以通过在每次更改时保持其宽高比来缩放像素图:

QPixmap p; // load pixmap
// get label dimensions
int w = label->width();
int h = label->height();

// set a scaled pixmap to a w x h window keeping its aspect ratio 
label->setPixmap(p.scaled(w,h,Qt::KeepAspectRatio));

你应该在两个地方添加这段代码:

  • 当像素图更新时
  • 在包含标签的小部件的resizeEvent

据我所知,默认情况下不提供此功能。实现您想要的最优雅的方法是子类化 QLabel。否则,您可以在一个槽/函数中使用我的答案代码,每当像素图更改时都会调用它。 - pnezis
1
由于我希望QLabel能够根据用户调整QMainWindow和可用空间的大小自动扩展,因此我不能使用信号/槽解决方案--我无法通过这种方式建模一个“扩展”策略。 - marvin2k
你需要在包含标签的小部件的resizeEvent中完成此操作。 - pnezis
24
为了能够缩小尺寸,您需要添加以下调用: label->setMinimumSize(1, 1) - Pieter-Jan Busschaert
1
如果我想保持标签的纵横比,即使用户更改标签的大小,这并不是非常有用。 - Tomáš Zato
显示剩余2条评论

45

我已经完善了这个缺失的QLabel的子类。它很棒,而且工作正常。

aspectratiopixmaplabel.h

#ifndef ASPECTRATIOPIXMAPLABEL_H
#define ASPECTRATIOPIXMAPLABEL_H

#include <QLabel>
#include <QPixmap>
#include <QResizeEvent>

class AspectRatioPixmapLabel : public QLabel
{
    Q_OBJECT
public:
    explicit AspectRatioPixmapLabel(QWidget *parent = 0);
    virtual int heightForWidth( int width ) const;
    virtual QSize sizeHint() const;
    QPixmap scaledPixmap() const;
public slots:
    void setPixmap ( const QPixmap & );
    void resizeEvent(QResizeEvent *);
private:
    QPixmap pix;
};

#endif // ASPECTRATIOPIXMAPLABEL_H

aspectratiopixmaplabel.cpp

#include "aspectratiopixmaplabel.h"
//#include <QDebug>

AspectRatioPixmapLabel::AspectRatioPixmapLabel(QWidget *parent) :
    QLabel(parent)
{
    this->setMinimumSize(1,1);
    setScaledContents(false);
}

void AspectRatioPixmapLabel::setPixmap ( const QPixmap & p)
{
    pix = p;
    QLabel::setPixmap(scaledPixmap());
}

int AspectRatioPixmapLabel::heightForWidth( int width ) const
{
    return pix.isNull() ? this->height() : ((qreal)pix.height()*width)/pix.width();
}

QSize AspectRatioPixmapLabel::sizeHint() const
{
    int w = this->width();
    return QSize( w, heightForWidth(w) );
}

QPixmap AspectRatioPixmapLabel::scaledPixmap() const
{
    return pix.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

void AspectRatioPixmapLabel::resizeEvent(QResizeEvent * e)
{
    if(!pix.isNull())
        QLabel::setPixmap(scaledPixmap());
}

希望这能帮到你!(根据 @dmzl 的回答更新了 resizeEvent


2
谢谢,很好用。我还会在“setPixmap()”方法中添加“QLabel :: setPixmap(pix.scaled(this->size(), Qt :: KeepAspectRatio,Qt :: SmoothTransformation));”。 - Hyndrix
太棒了!我将它翻译成了Python/PySide,并解决了困扰我数小时的问题:https://gist.github.com/grayshirt/510f32bb63dac9505f0d。我的版本有一个问题,如果小部件快速调整大小,你会看到图像在一两秒钟内明显变大。 - grayshirt
我也无法为此设计出合适的布局或适当的拉伸规则。它始于图片的原始分辨率,不能缩小到以下。我的 AspectRatioPixmapLabel 位于一对嵌套的 QVBoxLayoutQHBoxlayout 中。 - Christoph
两个问题:1. 如果源图像是 .svg 格式,当被拉伸时它看起来很糟糕。2. 图片没有居中,而是靠左对齐。 - Mr. Developerdude
6
好的回答!对于需要在高 DPI 屏幕上工作的人,只需将 scaledPixmap() 更改为以下内容:auto scaled = pix.scaled(this->size() * devicePixelRatioF(), Qt::KeepAspectRatio, Qt::SmoothTransformation); scaled.setDevicePixelRatio(devicePixelRatioF()); return scaled; 这也适用于通常缩放的屏幕。 - Saul
显示剩余7条评论

23

我只使用 contentsMargin 来修复纵横比。

#pragma once

#include <QLabel>

class AspectRatioLabel : public QLabel
{
public:
    explicit AspectRatioLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
    ~AspectRatioLabel();

public slots:
    void setPixmap(const QPixmap& pm);

protected:
    void resizeEvent(QResizeEvent* event) override;

private:
    void updateMargins();

    int pixmapWidth = 0;
    int pixmapHeight = 0;
};
#include "AspectRatioLabel.h"

AspectRatioLabel::AspectRatioLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent, f)
{
}

AspectRatioLabel::~AspectRatioLabel()
{
}

void AspectRatioLabel::setPixmap(const QPixmap& pm)
{
    pixmapWidth = pm.width();
    pixmapHeight = pm.height();

    updateMargins();
    QLabel::setPixmap(pm);
}

void AspectRatioLabel::resizeEvent(QResizeEvent* event)
{
    updateMargins();
    QLabel::resizeEvent(event);
}

void AspectRatioLabel::updateMargins()
{
    if (pixmapWidth <= 0 || pixmapHeight <= 0)
        return;

    int w = this->width();
    int h = this->height();

    if (w <= 0 || h <= 0)
        return;

    if (w * pixmapHeight > h * pixmapWidth)
    {
        int m = (w - (pixmapWidth * h / pixmapHeight)) / 2;
        setContentsMargins(m, 0, m, 0);
    }
    else
    {
        int m = (h - (pixmapHeight * w / pixmapWidth)) / 2;
        setContentsMargins(0, m, 0, m);
    }
}

目前为止,对我来说完美地发挥作用。不客气。


4
刚刚用了这个,效果非常好!布局管理器的使用也很巧妙。应该被采纳为最佳答案,因为其他答案在某些情况下存在缺陷。 - thokra
3
尽管这个回答很有创意,但解决的是一个根本不同的问题:“对于一个标签尺寸已知且包含一个图像的标签,我们应该添加多少内部填充以保持该图像的纵横比?”其他所有回答都解决了原始问题:“为了保持图像的纵横比,我们应该将包含图像的标签调整到什么大小?”这个答案要求标签的大小在某种程度上预先确定(例如,使用固定大小策略),这在许多用例中是不可取甚至不可行的。 - Cecil Curry
1
这是针对HiResolution(又名“Retina”)显示器的正确方法 - 它比缩小QPixmap要好得多。 - jvb
也许我过于专注于使代码表达高级含义以维护可读性,但使用QSize而不是...Width...Height会更有意义吧?如果没有其他问题,这将使您的早期返回检查变为一个简单的QSize :: isEmpty调用。 QPixmapQWidget都有size方法来检索宽度和高度作为QSize - ssokolow
@ssokolow,是的,那听起来更好 - 请随意编辑答案。 - Timmmm
这在 PySide6 中非常好用。 - Ryan Hope

8

改编自Timmmm用于PYQT5

from PyQt5.QtGui import QPixmap
from PyQt5.QtGui import QResizeEvent
from PyQt5.QtWidgets import QLabel


class Label(QLabel):

    def __init__(self):
        super(Label, self).__init__()
        self.pixmap_width: int = 1
        self.pixmapHeight: int = 1

    def setPixmap(self, pm: QPixmap) -> None:
        self.pixmap_width = pm.width()
        self.pixmapHeight = pm.height()

        self.updateMargins()
        super(Label, self).setPixmap(pm)

    def resizeEvent(self, a0: QResizeEvent) -> None:
        self.updateMargins()
        super(Label, self).resizeEvent(a0)

    def updateMargins(self):
        if self.pixmap() is None:
            return
        pixmapWidth = self.pixmap().width()
        pixmapHeight = self.pixmap().height()
        if pixmapWidth <= 0 or pixmapHeight <= 0:
            return
        w, h = self.width(), self.height()
        if w <= 0 or h <= 0:
            return

        if w * pixmapHeight > h * pixmapWidth:
            m = int((w - (pixmapWidth * h / pixmapHeight)) / 2)
            self.setContentsMargins(m, 0, m, 0)
        else:
            m = int((h - (pixmapHeight * w / pixmapWidth)) / 2)
            self.setContentsMargins(0, m, 0, m)

对我有用。但我们不需要self.pixmap_width和self.pixmapHeight。谢谢。 - nfgf

7
我尝试使用phyatt的AspectRatioPixmapLabel类,但遇到了一些问题:
- 有时我的应用程序会进入调整大小事件的无限循环。我追溯到resizeEvent方法中QLabel :: setPixmap(...)的调用,因为QLabel实际上在setPixmap内部调用updateGeometry,这可能会触发调整大小事件...... - heightForWidth似乎被包含的小部件(在我的情况下是QScrollArea)忽略,直到我开始为标签设置大小策略,显式调用policy.setHeightForWidth(true) - 我希望标签永远不会超过原始像素图的大小 - QLabel的minimumSizeHint()实现对包含文本的标签进行了一些魔法,但总是将大小策略重置为默认值,所以我必须覆盖它
话虽如此,这是我的解决方案。我发现我可以只使用setScaledContents(true),让QLabel处理调整大小。 当然,这取决于包含小部件/布局遵守heightForWidth。
aspectratiopixmaplabel.h
#ifndef ASPECTRATIOPIXMAPLABEL_H
#define ASPECTRATIOPIXMAPLABEL_H

#include <QLabel>
#include <QPixmap>

class AspectRatioPixmapLabel : public QLabel
{
    Q_OBJECT
public:
    explicit AspectRatioPixmapLabel(const QPixmap &pixmap, QWidget *parent = 0);
    virtual int heightForWidth(int width) const;
    virtual bool hasHeightForWidth() { return true; }
    virtual QSize sizeHint() const { return pixmap()->size(); }
    virtual QSize minimumSizeHint() const { return QSize(0, 0); }
};

#endif // ASPECTRATIOPIXMAPLABEL_H

aspectratiopixmaplabel.cpp

#include "aspectratiopixmaplabel.h"

AspectRatioPixmapLabel::AspectRatioPixmapLabel(const QPixmap &pixmap, QWidget *parent) :
    QLabel(parent)
{
    QLabel::setPixmap(pixmap);
    setScaledContents(true);
    QSizePolicy policy(QSizePolicy::Maximum, QSizePolicy::Maximum);
    policy.setHeightForWidth(true);
    this->setSizePolicy(policy);
}

int AspectRatioPixmapLabel::heightForWidth(int width) const
{
    if (width > pixmap()->width()) {
        return pixmap()->height();
    } else {
        return ((qreal)pixmap()->height()*width)/pixmap()->width();
    }
}

虽然对于父小部件和/或包含此标签的布局尊重heightForWidth属性的边缘情况而言,这个答案是首选的,但是对于不尊重heightForWidth属性的一般情况而言,这个答案会失败。这很不幸,因为除此之外,这个答案比phyatt长期回答更可取。 - Cecil Curry

6
如果您的图像是资源或文件,您无需子类化任何内容; 只需在标签的样式表中设置image,它将按比例缩放以适应标签,并跟踪对标签进行的任何大小更改。您还可以选择使用image-position将图像移动到其中一个边缘。
这种方法不适用于动态更新的像素图(我的意思是,您可以随时设置不同的资源,但它们仍必须是资源),但如果您正在使用来自资源的像素图,则这是一种很好的方法。
样式表示例:
image: url(:/resource/path);
image-position: right center; /* optional: default is centered. */

在代码中(例如):
QString stylesheet = "image:url(%1);image-position:right center;";
existingLabel->setStyleSheet(stylesheet.arg(":/resource/path"));

或者你可以直接在设计师中设置样式属性:

enter image description here 图标来源:Designspace Team via Flaticon

需要注意的是它不会将图像放大,只能缩小,因此如果您希望它增长,请确保您的图像比您的尺寸范围更大(请注意,它支持SVG格式,这可以提高质量)。

标签的大小可以像往常一样进行控制:要么在样式表中使用大小元素,要么使用标准布局和大小策略。

有关详细信息,请参阅文档

自早期Qt以来,该样式就存在了(2007年左右添加了位置,但图像在那之前就已经存在了)。


1
我终于按照期望使它工作了。重要的是覆盖sizeHintresizeEvent,并设置最小尺寸和大小策略。 当控件与图像具有不同的纵横比时,setAlignment用于使图像在控件中水平或垂直居中。请保留HTML标签。
class ImageDisplayWidget(QLabel):
    def __init__(self, max_enlargement=2.0):
        super().__init__()
        self.max_enlargement = max_enlargement
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setAlignment(Qt.AlignCenter)
        self.setMinimumSize(1, 1)
        self.__image = None

    def setImage(self, image):
        self.__image = image
        self.resize(self.sizeHint())
        self.update()

    def sizeHint(self):
        if self.__image:
            return self.__image.size() * self.max_enlargement
        else:
            return QSize(1, 1)

    def resizeEvent(self, event):
        if self.__image:
            pixmap = QPixmap.fromImage(self.__image)
            scaled = pixmap.scaled(event.size(), Qt.KeepAspectRatio)
            self.setPixmap(scaled)
        super().resizeEvent(event)

0

Qt文档中有一个图像查看器示例,演示了如何在QLabel内处理调整大小的图像。基本思路是使用QScrollArea作为QLabel的容器,如果需要,使用label.setScaledContents(bool)scrollarea.setWidgetResizable(bool)来填充可用空间和/或确保QLabel内部可调整大小。 此外,为了在保持纵横比的同时调整QLabel的大小,请使用:

label.setPixmap(pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::FastTransformation));

widthheight 可以根据 scrollarea.width()scrollarea.height() 进行设置。 这样就不需要继承 QLabel。


该示例在自动调整大小时不会保持长宽比,它允许手动调整大小同时保持长宽比,并且可以自动调整大小但不保持长宽比,但不能同时实现两种功能。 - user4945014

0
这是将@phyatt的类移植到PySide2的端口。
除了移植之外,我还在resizeEvent中添加了一个额外的对齐方式,以使新调整大小的图像在可用空间中正确定位。
from typing import Union

from PySide2.QtCore import QSize, Qt
from PySide2.QtGui import QPixmap, QResizeEvent
from PySide2.QtWidgets import QLabel, QWidget

class QResizingPixmapLabel(QLabel):
    def __init__(self, parent: Union[QWidget, None] = ...):
        super().__init__(parent)
        self.setMinimumSize(1,1)
        self.setScaledContents(False)
        self._pixmap: Union[QPixmap, None] = None

    def heightForWidth(self, width:int) -> int:
        if self._pixmap is None:
            return self.height()
        else:
            return self._pixmap.height() * width / self._pixmap.width()

    def scaledPixmap(self) -> QPixmap:
        scaled = self._pixmap.scaled(
            self.size() * self.devicePixelRatioF(),
            Qt.KeepAspectRatio,
            Qt.SmoothTransformation
        )
        scaled.setDevicePixelRatio(self.devicePixelRatioF());
        return scaled;

    def setPixmap(self, pixmap: QPixmap) -> None:
        self._pixmap = pixmap
        super().setPixmap(pixmap)

    def sizeHint(self) -> QSize:
        width = self.width()
        return QSize(width, self.heightForWidth(width))

    def resizeEvent(self, event: QResizeEvent) -> None:
        if self._pixmap is not None:
            super().setPixmap(self.scaledPixmap())
            self.setAlignment(Qt.AlignCenter)

0

这里没有什么新的东西。

我混合了被接受的回复https://dev59.com/dGsy5IYBdhLWcg3w8Slc#8212120https://dev59.com/dGsy5IYBdhLWcg3w8Slc#43936590,它使用了setContentsMargins,但是我按照自己的方式编写了一些代码。

/**
 * @brief calcMargins Calculate the margins when a rectangle of one size is centred inside another
 * @param outside - the size of the surrounding rectanle
 * @param inside  - the size of the surrounded rectangle
 * @return the size of the four margins, as a QMargins
 */
QMargins calcMargins(QSize const outside, QSize const inside)
{
    int left = (outside.width()-inside.width())/2;
    int top  = (outside.height()-inside.height())/2;
    int right = outside.width()-(inside.width()+left);
    int bottom = outside.height()-(inside.height()+top);

    QMargins margins(left, top, right, bottom);
    return margins;
}

一个函数计算使一个矩形居中于另一个矩形所需的边距。这是一个非常通用的函数,可以用于许多事情,但我不知道具体是什么。
然后,通过添加几行代码,setContentsMargins 变得非常容易使用,而这些行代码很多人都会合并为一行。
QPixmap scaled = p.scaled(this->size(), Qt::KeepAspectRatio);
QMargins margins = calcMargins(this->size(), scaled.size());
this->setContentsMargins(margins);
setPixmap(scaled);

这可能会引起某人的兴趣...我需要处理mousePressEvent并知道自己在图像中的位置。

void MyClass::mousePressEvent(QMouseEvent *ev)
{
    QMargins margins = contentsMargins();

    QPoint labelCoordinateClickPos = ev->pos();
    QPoint pixmapCoordinateClickedPos = labelCoordinateClickPos - QPoint(margins.left(),margins.top());
    ... more stuff here
}

我的大图像是从摄像机中获取的,我通过将其除以像素图的宽度并乘以原始图像的宽度来获得相对坐标[0,1)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接