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

C++QtQt4Qlabel

C++ Problem Overview


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?

C++ Solutions


Solution 1 - C++

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

Solution 2 - C++

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)

Solution 3 - C++

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.

Solution 4 - C++

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

Solution 5 - C++

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)

Solution 6 - 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:

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

Solution 7 - C++

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.

Solution 8 - C++

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)

Solution 9 - C++

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.

Solution 10 - C++

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)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionmarvin2kView Question on Stackoverflow
Solution 1 - C++pnezisView Answer on Stackoverflow
Solution 2 - C++phyattView Answer on Stackoverflow
Solution 3 - C++TimmmmView Answer on Stackoverflow
Solution 4 - C++Alexander SchlüterView Answer on Stackoverflow
Solution 5 - C++kblstView Answer on Stackoverflow
Solution 6 - C++Jason CView Answer on Stackoverflow
Solution 7 - C++OmidView Answer on Stackoverflow
Solution 8 - C++Richard WhiteheadView Answer on Stackoverflow
Solution 9 - C++IvanView Answer on Stackoverflow
Solution 10 - C++iblancoView Answer on Stackoverflow