I would be very grateful for your help specifying the re-sizing behaviour of a Pyside6
QLabel
displaying a QMovie
such that the original aspect ratio of the movie (e.g. a .gif file) is maintained when the size of the parent QLabel
is changed.
These two questions have answers that detail the process for a QLabel
displaying a QPixmap
:
How to Autoresize QLabel pixmap keeping ratio without using classes?
Pyside6, How do I resize a QLabel without loosing the size aspect ratio? [duplicate]
Both make use of the QPixmap.scaled(w, h, aspectMode, mode)
method (link to docs), which has a parameter aspectMode
that can be set to Qt.KeepAspectRatio
.
Unlike QPixmap
objects, QMovie
objects do not have a .scaled()
method. They do have a QMovie.setScaledSize(size)
method (link to docs), which accepts a QSize
object as its only parameter.
My question therefore becomes how can I build a QSize
object with the same aspect ratio as my original .gif file, but scaled to be as big as possible within its parent QLabel
, and update this whenever the QLabel
is resized by the user?
Here is the custom class SclaedLabel
from this answer adapted as far as I can for scaling the QLabel.movie
rather than the QLabel.Pixmap
:
from PySide6.QtWidgets import QLabel
from PySide6.QtCore import Qt
class ScaledLabel(QLabel):
def __init__(self, *args, **kwargs):
super().__init__()
self._movie = self.movie()
def resizeEvent(self, event):
self.setMovie(self._movie)
def setMovie(self, movie):
if movie:
original_size = movie.scaledSize()
size_scaled = original_size.scale(self.size, Qt.KeepAspectRatio)
return QLabel.setMovie(self, self._movie.setScaledSize(size_scaled))
Unfortunately, movie.scaledSize()
is returning a QSize
of (-1, -1)
for my gif file, which causes size_scaled
to become None
.
I would therefore appreciate your help obtaining a QSize
object for my original movie (.gif file), and any suggestions on its implementation to obtain the desired QMovie KeepAspectRatio scaling behaviour.
By default, the QMovie class doesn't provide a valid scaledSize()
for GIF images, meaning that it will always return an invalid size (aka QSize(-1, -1)
) unless setScaledSize()
has been called explicitly with a valid size.
Calling setScaledSize()
alone would be inadequate, though, because it would also cause the QLabel to use that size as its minimum size, so the label may only become bigger and bigger, but never smaller.
Similarly to what normally is done with QLabel for fixed aspect ratio QPixmaps, the best approach is to both set a reasonable minimum size, and override the paint event ensuring that the content considers the minimum aspect ratio.
For optimization reasons (and to avoid delays in resizing, since the pixmap is cached and not updated until a new frame is shown), the painting is done in two ways: if the possible size doesn't match the current scaled size, we internally paint a scaled pixmap, otherwise we follow the default painting implementation by copying what QLabel does in the sources.
class ScaledLabel(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._movieSize = QSize()
self._minSize = QSize()
def minimumSizeHint(self):
if self._minSize.isValid():
return self._minSize
return super().minimumSizeHint()
def setMovie(self, movie):
if self.movie() == movie:
return
super().setMovie(movie)
if not isinstance(movie, QMovie) or not movie.isValid():
self._movieSize = QSize()
self._minSize = QSize()
self.updateGeometry()
return
cf = movie.currentFrameNumber()
state = movie.state()
movie.jumpToFrame(0)
rect = QRect()
for i in range(movie.frameCount()):
movie.jumpToNextFrame()
rect |= movie.frameRect()
width = rect.x() + rect.width()
height = rect.y() + rect.height()
self._movieSize = QSize(width, height)
if width == height and False:
if width < 4:
self._minSize = QSize(width, width)
else:
self._minSize = QSize(4, 4)
else:
minimum = min(width, height)
maximum = max(width, height)
ratio = maximum / minimum
base = min(4, minimum)
self._minSize = QSize(base, round(base * ratio))
if minimum == width:
self._minSize.transpose()
movie.jumpToFrame(cf)
if state == movie.MovieState.Running:
movie.setPaused(False)
self.updateGeometry()
def paintEvent(self, event):
movie = self.movie()
if not isinstance(movie, QMovie) or not movie.isValid():
super().paintEvent(event)
return
qp = QPainter(self)
self.drawFrame(qp)
cr = self.contentsRect()
margin = self.margin()
cr.adjust(margin, margin, -margin, -margin)
style = self.style()
alignment = style.visualAlignment(self.layoutDirection(), self.alignment())
maybeSize = self._movieSize.scaled(cr.size(), Qt.KeepAspectRatio)
if maybeSize != movie.scaledSize():
movie.setScaledSize(maybeSize)
style.drawItemPixmap(
qp, cr, alignment,
movie.currentPixmap().scaled(cr.size(), Qt.KeepAspectRatio)
)
else:
style.drawItemPixmap(
qp, cr, alignment,
movie.currentPixmap()
)