97

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?

isanae
  • 3,253
  • 1
  • 22
  • 47
marvin2k
  • 1,573
  • 2
  • 14
  • 18
  • By dynamically changing do you mean the pixel data or the dimensions? – r_ahlskog Nov 21 '11 at 12:48
  • 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... – marvin2k Nov 21 '11 at 15:11

10 Answers10

113

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
Haris
  • 13,645
  • 12
  • 90
  • 121
pnezis
  • 12,023
  • 2
  • 38
  • 38
  • 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... – marvin2k Nov 21 '11 at 15:16
  • 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. – pnezis Nov 21 '11 at 15:29
  • 1
    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. – marvin2k Nov 21 '11 at 15:53
  • You have to do in the resizeEvent of the widget where the label is. – pnezis Nov 21 '11 at 16:34
  • 24
    In order to be able to scale down as well, you need to add this call: `label->setMinimumSize(1, 1)` – Pieter-Jan Busschaert Jul 12 '13 at 13:21
  • Under certain circumstances you can use the `image` stylesheet element [instead](https://stackoverflow.com/a/70858333/616460). – Jason C Jan 26 '22 at 03:40
43

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)

Phlucious
  • 3,704
  • 28
  • 61
phyatt
  • 18,472
  • 5
  • 61
  • 80
  • 2
    Thanks, works great. I would also add ``QLabel::setPixmap(pix.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));`` to the ``setPixmap()`` method. – Hyndrix Dec 20 '14 at 07:18
  • 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. – phyatt Dec 21 '14 at 04:52
  • 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? – Julien M Jan 27 '15 at 10:58
  • Use layouts and stretch rules. – phyatt Jan 27 '15 at 11:00
  • Could you elaborate on recommended rules? I tried a lot of things in Designer but without success. Thanks in advance – Julien M Jan 27 '15 at 13:04
  • This is fantastic! I translated it to Python/PySide to solve a problem that had been bugging me for hours: https://gist.github.com/grayshirt/510f32bb63dac9505f0d. My version does have an issue where you can see the image visibly growing for a second or two if the widget is upsized very quickly. – grayshirt May 17 '15 at 23:36
  • I also can't come up with a suitable layout or suitable stretch rules for this. It starts with the picture's original resolution, and cannot shrink below that. My `AspectRatioPixmapLabel` is in a pair of nested `QVBoxLayout` and `QHBoxlayout`. – Christoph Aug 10 '15 at 11:12
  • Two problems: 1. if the source image is .svg it looks horrible if it is stretched far. 2. it is not centered but aligned to the left. – Mr. Developerdude Jan 06 '16 at 02:51
  • Great option! Thanks for contributing. For future users, I had to unset the [`scaledContents`](http://doc.qt.io/qt-4.8/qlabel.html#scaledContents-prop) property to get this to work properly. – Phlucious Jun 29 '16 at 16:13
  • Sounds good. Feel free to add another edit to the code to get that in the constructor. – phyatt Jun 29 '16 at 16:15
  • 6
    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. – Saul Dec 01 '16 at 22:33
  • Under certain circumstances you can avoid the subclassing and use the `image` stylesheet element on a vanilla QLabel [instead](https://stackoverflow.com/a/70858333/616460). – Jason C Jan 26 '22 at 03:41
23

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.

Timmmm
  • 88,195
  • 71
  • 364
  • 509
  • 4
    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. – thokra Jun 23 '17 at 14:39
  • 3
    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. – Cecil Curry Jan 19 '18 at 06:41
  • 1
    That's the way to go for HiResolution (a.k.a "retina") displays - it's much better than downscaling the QPixmap. – jvb Feb 19 '19 at 09:32
  • 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 Nov 10 '19 at 12:59
  • @ssokolow Yes that sounds better - feel free to edit the answer. – Timmmm Nov 10 '19 at 16:06
  • this worked really nice in pyside6 – Ryan Hope Jan 18 '23 at 19:10
8

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)
kblst
  • 493
  • 8
  • 12
7

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();
    }
}
Alexander Schlüter
  • 1,089
  • 1
  • 8
  • 7
  • 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](https://stackoverflow.com/users/999943/phyatt)'s [long-standing answer](https://stackoverflow.com/a/22618496/2809027). – Cecil Curry Jan 19 '18 at 08:00
6

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:

enter image description here Icon source: Designspace Team via Flaticon

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

Jason C
  • 38,729
  • 14
  • 126
  • 182
1

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

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.

Omid
  • 286
  • 1
  • 5
  • 20
  • 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. –  Jun 14 '21 at 18:35
0

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.

Ivan
  • 41
  • 4
0

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)
iblanco
  • 41
  • 1
  • 6