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

我使用 QLabel 向用户显示一个更大的、动态更改的 QPixmap 的内容。根据可用的空间,将这个标签做得更小或更大会更好。屏幕大小并不总是像 QPixmap 那样大。

如何修改 QLabel 的 QSizePolicysizeHint()来调整 QPixmap 的大小,同时保持原始 QPixmap 的高宽比?

我不能修改的 QLabel 的 sizeHint(),设置为零的 minimumSize()没有帮助。在 QLabel 上设置 hasScaledContents()允许增长,但打破了长宽比什么的..。

子类化 QLabel 确实有帮助,但是这个解决方案为一个简单的问题增加了太多的代码..。

有什么聪明的提示如何完成这个 没有子类?

131111 次浏览

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

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)

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

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.

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)

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.

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)

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.

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

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)