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

10

103

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?

Noisette answered 21/11, 2011 at 12:43 Comment(0)
M
116

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
Milford answered 21/11, 2011 at 12:53 Comment(6)
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...Noisette
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.Milford
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.Noisette
You have to do in the resizeEvent of the widget where the label is.Milford
In order to be able to scale down as well, you need to add this call: label->setMinimumSize(1, 1)Quadri
Under certain circumstances you can use the image stylesheet element instead.Achromat
H
48

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)

Huffy answered 24/3, 2014 at 19:11 Comment(12)
Thanks, works great. I would also add QLabel::setPixmap(pix.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); to the setPixmap() method.Dexter
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.Huffy
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?Wergild
Use layouts and stretch rules.Huffy
Could you elaborate on recommended rules? I tried a lot of things in Designer but without success. Thanks in advanceWergild
This is fantastic! I translated it to Python/PySide to solve a problem that had been bugging me for hours: 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.Dramshop
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.Overzealous
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.Lusatia
Great option! Thanks for contributing. For future users, I had to unset the scaledContents property to get this to work properly.Easternmost
Sounds good. Feel free to add another edit to the code to get that in the constructor.Huffy
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.Gaunt
Under certain circumstances you can avoid the subclassing and use the image stylesheet element on a vanilla QLabel instead.Achromat
G
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.

Georgetown answered 12/5, 2017 at 11:21 Comment(6)
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.Josefajosefina
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.Trifid
That's the way to go for HiResolution (a.k.a "retina") displays - it's much better than downscaling the QPixmap.Fetid
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.Neves
@Neves Yes that sounds better - feel free to edit the answer.Georgetown
this worked really nice in pyside6Loser
E
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)
Emie answered 12/6, 2020 at 22:24 Comment(1)
Worked for me. But we do not need self.pixmap_width and self.pixmapHeight. Thanks.Cards
A
8

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

Achromat answered 26/1, 2022 at 3:22 Comment(0)
C
6

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();
    }
}
Cony answered 30/12, 2016 at 22:22 Comment(1)
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.Trifid
C
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)
Cheops answered 1/11, 2021 at 11:49 Comment(0)
C
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.

Corded answered 2/9, 2020 at 14:21 Comment(1)
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.Isocyanide
K
0

Nothing new here really.

I mixed the accepted reply https://mcmap.net/q/209650/-qt-resizing-a-qlabel-containing-a-qpixmap-while-keeping-its-aspect-ratio and https://mcmap.net/q/209650/-qt-resizing-a-qlabel-containing-a-qpixmap-while-keeping-its-aspect-ratio 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.

Kraut answered 22/12, 2021 at 13:8 Comment(0)
G
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)
Gossipmonger answered 11/3, 2022 at 10:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.