ChatGPT解决这个技术问题 Extra ChatGPT

Qt: resizing a QLabel containing a QPixmap while keeping its aspect ratio

I use a QLabel to display the content of a bigger, dynamically changing QPixmap to the user. It would be nice to make this label smaller/larger depending on the space available. The screen size is not always as big as the QPixmap.

How can I modify the QSizePolicy and sizeHint() of the QLabel to resize the QPixmap while keeping the aspect ratio of the original QPixmap?

I can't modify sizeHint() of the QLabel, setting the minimumSize() to zero does not help. Setting hasScaledContents() on the QLabel allows growing, but breaks the aspect ratio thingy...

Subclassing QLabel did help, but this solution adds too much code for just a simple problem...

Any smart hints how to accomplish this without subclassing?

By dynamically changing do you mean the pixel data or the dimensions?
I mean the dimensions of the QLabel in the current layout. The QPixmap should keep its size, content and dimension. Also, it would be nice of the resizing (shrinking in reality) happens "automagically" to fill up the available space -- up to the size of the original QPixmap. All this was done via subclassing...

H
Haris

In order to change the label size you can select an appropriate size policy for the label like expanding or minimum expanding.

You can scale the pixmap by keeping its aspect ratio every time it changes:

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));

There are two places where you should add this code:

When the pixmap is updated

In the resizeEvent of the widget that contains the label


hm yes, this was basically the core when I subclassed QLabel. But I thought this use case (showing Images with arbitrary size in Widgets of arbitrary size) would be common enough to have something like it implementable via existing code...
AFAIK this functionality is not provided by default. The most elegant way to achieve what you want is to subclass QLabel. Otherwise you can use the code of my answer in a slot/function which will be called every time the pixmap changes.
since I want the QLabel to automagically expand based on the users resizing of the QMainWindow and the available space, I can't use the signal/slot solution -- I can't model an expanding policy this way.
In order to be able to scale down as well, you need to add this call: label->setMinimumSize(1, 1)
This isn't very useful if I want to preserve the aspect ration even as user changes size of the label.
P
Phlucious

I have polished this missing subclass of QLabel. It is awesome and works well.

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());
}

Hope that helps! (Updated resizeEvent, per @dmzl's answer)


Thanks, works great. I would also add QLabel::setPixmap(pix.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); to the setPixmap() method.
You are right. I made the assumption that you want to store the highest quality version of the pixmap, and that you call setPixmap before resizing/anchoring the label. To reduce code duplication, I probably should put this->resize(width(), height()); at the tail end of the setPixmap function.
Thanks for sharing this. Would you have any suggestions on how I can set a "preferred" size to the QPixmap so that it does not take the maximum resolution on the first launch of the application?
Use layouts and stretch rules.
Great answer! For anyone needing to work on High DPI screens simply change scaledPixmap() to do: auto scaled = pix.scaled(this->size() * devicePixelRatioF(), Qt::KeepAspectRatio, Qt::SmoothTransformation); scaled.setDevicePixelRatio(devicePixelRatioF()); return scaled; This also works on normally scaled screens.
T
Timmmm

I just use contentsMargin to fix the aspect ratio.

#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);
    }
}

Works perfectly for me so far. You're welcome.


Just used this and it works like a charm! Also, pretty clever use of the layout manager. Should be the accepted answer since all the others have flaws in corner cases.
While non-intuitively clever, this answer solves a fundamentally different question: "How much internal padding should we add between a label whose size is already well-known and the pixmap contained in that label so as to preserve the aspect ratio of that pixmap?" Every other answer solves the original question: "To what size should we resize a label containing a pixmap so as to preserve the aspect ratio of that pixmap?" This answer requires the label's size to be predetermined somehow (e.g., with a fixed size policy), which is undesirable or even infeasible in many use cases.
That's the way to go for HiResolution (a.k.a "retina") displays - it's much better than downscaling the QPixmap.
Maybe I'm a little too focused on making code express high-level meaning for maintainability's sake but wouldn't it make more sense to use QSize instead of ...Width and ...Height? If nothing else, that'd make your early-return checks a simple QSize::isEmpty call. QPixmap and QWidget both have size methods to retrieve the width and height as a QSize.
@ssokolow Yes that sounds better - feel free to edit the answer.
k
kblst

Adapted from Timmmm to 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)

Worked for me. But we do not need self.pixmap_width and self.pixmapHeight. Thanks.
A
Alexander Schlüter

I tried using phyatt's AspectRatioPixmapLabel class, but experienced a few problems:

Sometimes my app entered an infinite loop of resize events. I traced this back to the call of QLabel::setPixmap(...) inside the resizeEvent method, because QLabel actually calls updateGeometry inside setPixmap, which may trigger resize events...

heightForWidth seemed to be ignored by the containing widget (a QScrollArea in my case) until I started setting a size policy for the label, explicitly calling policy.setHeightForWidth(true)

I want the label to never grow more than the original pixmap size

QLabel's implementation of minimumSizeHint() does some magic for labels containing text, but always resets the size policy to the default one, so I had to overwrite it

That said, here is my solution. I found that I could just use setScaledContents(true) and let QLabel handle the resizing. Of course, this depends on the containing widget / layout honoring the 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();
    }
}

While preferable for edge cases in which the parent widget and/or layout containing this label respect the heightForWidth property, this answer fails for the general case in which the parent widget and/or layout containing this label do not respect the heightForWidth property. Which is unfortunate, as this answer is otherwise preferable to phyatt's long-standing answer.
J
Jason C

If your image is a resource or a file you don't need to subclass anything; just set image in the label's stylesheet; and it will be scaled to fit the label while keeping its aspect ratio, and will track any size changes made to the label. You can optionally use image-position to move the image to one of the edges.

It doesn't fit the OP's case of a dynamically updated pixmap (I mean, you can set different resources whenever you want but they still have to be resources), but it's a good method if you're using pixmaps from resources.

Stylesheet example:

image: url(:/resource/path);
image-position: right center; /* optional: default is centered. */

In code (for example):

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

Or you can just set the stylesheet property right in Designer:

https://i.stack.imgur.com/4VHal.gif

The caveat is that it won't scale the image larger, only smaller, so make sure your image is bigger than your range of sizes if you want it to grow (note that it can support SVG, which can improve quality).

The label's size can be controlled as per usual: either use size elements in the stylesheet or use the standard layout and size policy strategies.

See the documentation for details.

This style has been present since early Qt (position was added in 4.3 circa 2007 but image was around before then).


O
Omid

The Qt documentations has an Image Viewer example which demonstrates handling resizing images inside a QLabel. The basic idea is to use QScrollArea as a container for the QLabel and if needed use label.setScaledContents(bool) and scrollarea.setWidgetResizable(bool) to fill available space and/or ensure QLabel inside is resizable. Additionally, to resize QLabel while honoring aspect ratio use:

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

The width and height can be set based on scrollarea.width() and scrollarea.height(). In this way there is no need to subclass QLabel.


That example does not maintain the aspect ratio while resizing automatically. It allows manual resizing while maintaining the aspect ratio, and can resize automatically without maintaining the aspect ratio, but not both at the same time.
R
Richard Whitehead

I finally got this to work as expected. It is essential to override sizeHint as well as resizeEvent, and to set the minimum size and the size policy. setAlignment is used to centre the image in the control either horizontally or vertically when the control is a different aspect ratio to the image.

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)

I
Ivan

Nothing new here really.

I mixed the accepted reply https://stackoverflow.com/a/8212120/11413792 and https://stackoverflow.com/a/43936590/11413792 which uses setContentsMargins, but just coded it a bit my own way.

/**
 * @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;
}

A function calculates the margins required to centre one rectangle inside another. Its a pretty generic function that could be used for lots of things though I have no idea what.

Then setContentsMargins becomes easy to use with a couple of extra lines which many people would combine into one.

QPixmap scaled = p.scaled(this->size(), Qt::KeepAspectRatio);
QMargins margins = calcMargins(this->size(), scaled.size());
this->setContentsMargins(margins);
setPixmap(scaled);

It may interest somebody ... I needed to handle mousePressEvent and to know where I am within the image.

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

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

My large image was from a camera and I obtained the relative coordinates [0, 1) by dividing by the width of the pixmap and then multiplied up by the width of the original image.


i
iblanco

This is the port of @phyatt's class to PySide2.

Apart from porting i added an additional aligment in the resizeEvent in order to make the newly resized image position properly in the available space.

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)

关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now