How to scale a QPixmap preserving aspect and centering the image? - python

I want to use an image (svg file) as background in an QMdiArea. I load the image as QPixmap and scale it in the resizeEvent method to the size of the QMdiArea using
self._background_scaled = self._background.scaled(self.size(), QtCore.Qt.KeepAspectRatio)
self.setBackground(self._background_scaled)
But that puts the image in the upper left corner and repeats it in either X or Y. I want the image to be centered.
How do I scale the QPixmap so it is resized preserving aspect ratio but then add borders to get the exact size?

The setBackground() method accepts a QBrush that is built based on the QPixmap you pass to it, but if a QBrush is built based on a QPixmap it will create the texture (repeating elements) and there is no way to change that behavior. So the solution is to override the paintEvent method and directly paint the QPixmap:
import sys
from PySide2 import QtCore, QtGui, QtWidgets
def create_pixmap(size):
pixmap = QtGui.QPixmap(size)
pixmap.fill(QtCore.Qt.red)
painter = QtGui.QPainter(pixmap)
painter.setBrush(QtCore.Qt.blue)
painter.drawEllipse(pixmap.rect())
return pixmap
class MdiArea(QtWidgets.QMdiArea):
def __init__(self, parent=None):
super().__init__(parent)
pixmap = QtGui.QPixmap(100, 100)
pixmap.fill(QtGui.QColor("transparent"))
self._background = pixmap
#property
def background(self):
return self._background
#background.setter
def background(self, background):
self._background = background
self.update()
def paintEvent(self, event):
super().paintEvent(event)
painter = QtGui.QPainter(self.viewport())
background_scaled = self.background.scaled(
self.size(), QtCore.Qt.KeepAspectRatio
)
background_scaled_rect = background_scaled.rect()
background_scaled_rect.moveCenter(self.rect().center())
painter.drawPixmap(background_scaled_rect.topLeft(), background_scaled)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mdiarea = MdiArea()
mdiarea.show()
mdiarea.background = create_pixmap(QtCore.QSize(100, 100))
sys.exit(app.exec_())

Related

PyQt label with pixmap not resizing correctly [duplicate]

This question already has answers here:
Resize multiple labels containing images with changing window size
(1 answer)
Auto Resize of Label in PyQt4
(1 answer)
Closed 1 year ago.
In PyQt I'm trying to create a label which contains a pixmap and have the pixmap automatically resize as the label resizes — say, as a result of the window the label is in resizing.
I am experiencing several problems:
The window refuses to resize smaller than its original size.
If you resize the window to a larger size, you can never resize back to a smaller size.
As the window resizes, the label also resizes correctly, but its pixmap does not repaint properly. It appears to "tear" or repeat pixels horizontally and/or vertically.
I started by creating a class PixmapLabel that inherits from QLabel since I wanted to override the resizeEvent event:
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
# You can plug in a path to an image of your choosing.
IMAGE_PATH = '../assets/launch_image.jpeg'
class PixmapLabel(QLabel):
"""
A label widget that has a pixmap. As the label resizes so does the
pixmap.
"""
def __init__(self, pixmap: QPixmap = None):
super().__init__()
self.setStyleSheet('background-color: lightgray')
self.setPixmap(pixmap)
def resizeEvent(self, event: QResizeEvent):
"""As the PixmapLabel resizes, resize its pixmap."""
super().resizeEvent(event)
self.setPixmap(self.pixmap())
def setPixmap(self, pixmap: QPixmap):
if pixmap is None:
return
# Resize the widget's pixmap to match the width of the widget preserving
# the aspect ratio.
width = self.width()
pixmap = pixmap.scaledToWidth(width)
super().setPixmap(pixmap)
Here is the code for the MainWindow (which derives from QDialog):
class MainWindow(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle('PyQt PixmapLabelWidget Test')
self.resize(300, 300)
# Set the window's main layout.
self.main_layout = QVBoxLayout()
self.setLayout(self.main_layout)
# Add a single widget — a PixmapLabel — to the main layout.
self.lbl_image = PixmapLabel()
self.main_layout.addWidget(self.lbl_image)
pixmap = QPixmap(IMAGE_PATH)
self.lbl_image.setPixmap(pixmap)
Finally, here's the code to run the Python app/script:
if __name__ == '__main__':
app = QApplication([])
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
Here's how the window initially renders with the label and its pixmap: https://cln.sh/gEldrO+.
Here's what it looks like after it has been resized: https://cln.sh/MuhknK+.
You should be able to see the "tearing."
Any suggestions about how to make this widget resize its pixmap correctly?

Resizing a window with PyQT5 - how do I reduce the size of a widget to allow the window to be shrunk?

I'm trying to learn it by re-making an old command line C program I've got for working with pixel art.
At the moment, the main window starts as a single QLabel set to show a 300 x 300 scaled up version of a 10 x 10 white image.
I'm using the resizeEvent (I've also tried using paintEvent with the same problem) to rescale the image to fill the window as the window size is increased.
My question is, how do I rescale the image to fit in the window as the window size is decreased? As it stands, the window can't be resized smaller than the widget displaying the image. Essentially, I can make the window (and image) bigger, but never smaller.
My code for this so far is below. As it stands it's only working based on changes to window width, just to keep it simple while I'm working this out. Is there a way to allow the window to be resized to be smaller than the largest widget? Or is there a better way to approach this problem?
#Create white 10*10 image
image = QImage(10,10,QImage.Format.Format_ARGB32)
image_scaled = QImage()
image.fill(QColor(255,255,255))
class Window(QMainWindow):
#scale image to change in window width (image is window width * window width square)
def resizeEvent(self,event):
if self.imageLabel.width()>self.imageLabel.height():
self.image_scaled = image.scaled(self.imageLabel.width(),self.imageLabel.width())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
QWidget.resizeEvent(self, event)
def __init__(self, parent=None):
super().__init__(parent)
self.setGeometry(100,100,300,300)
self.imageLabel = QLabel()
self.setCentralWidget(self.imageLabel)
self.image_scaled = image.scaled(self.imageLabel.width(),self.imageLabel.width())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
While the OP proposed solution might work, it has an important drawback: it uses a QScrollArea for the wrong purpose (since it's never used for scrolling). That approach creates unnecessary overhead while resizing, as the view will need to compute lots of things about its contents before "finishing" the resize event (including scroll bar ranges and geometries) that, in the end, will never be actually used.
The main problem comes from the fact that QLabel doesn't allow resizing to a size smaller than the original pixmap set. To work around this issue, the simplest solution is to create a custom QWidget subclass that draws the pixmap on its own.
class ImageViewer(QWidget):
pixmap = None
_sizeHint = QSize()
ratio = Qt.KeepAspectRatio
transformation = Qt.SmoothTransformation
def __init__(self, pixmap=None):
super().__init__()
self.setPixmap(pixmap)
def setPixmap(self, pixmap):
if self.pixmap != pixmap:
self.pixmap = pixmap
if isinstance(pixmap, QPixmap):
self._sizeHint = pixmap.size()
else:
self._sizeHint = QSize()
self.updateGeometry()
self.updateScaled()
def setAspectRatio(self, ratio):
if self.ratio != ratio:
self.ratio = ratio
self.updateScaled()
def setTransformation(self, transformation):
if self.transformation != transformation:
self.transformation = transformation
self.updateScaled()
def updateScaled(self):
if self.pixmap:
self.scaled = self.pixmap.scaled(self.size(), self.ratio, self.transformation)
self.update()
def sizeHint(self):
return self._sizeHint
def resizeEvent(self, event):
self.updateScaled()
def paintEvent(self, event):
if not self.pixmap:
return
qp = QPainter(self)
r = self.scaled.rect()
r.moveCenter(self.rect().center())
qp.drawPixmap(r, self.scaled)
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.imageLabel = ImageViewer(QPixmap.fromImage(image))
self.setCentralWidget(self.imageLabel)
Found a solution. Turns out putting the image inside a QScrollArea widget allows the window to be made smaller than the image it contains even if the scroll bars are disabled. This then allows the image to be rescaled to fit the window as the window size is reduced.
class Window(QMainWindow):
#scale image to change in window width (image is window width * window width square)
def resizeEvent(self,event):
self.image_scaled = image.scaled(self.scroll.width(),self.scroll.height())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
QMainWindow.resizeEvent(self, event)
def __init__(self, parent=None):
super().__init__(parent)
self.setGeometry(100,100,200,200)
self.imageLabel = QLabel()
self.scroll = QScrollArea()
self.scroll.setWidget(self.imageLabel)
self.setCentralWidget(self.scroll)
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.image_scaled = image.scaled(self.scroll.width(),self.scroll.width())
self.pixmap = QPixmap.fromImage(self.image_scaled)
self.imageLabel.setPixmap(self.pixmap)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

Create padding when fixed aspect ratio QWidget cannot fill entire window

A custom widget (class name MyLabel, inherits QLabel) has a fixed aspect ratio 16:9.
When I resize my window, the label is top-left aligned unless the window happens to be 16:9, in which case it fills the window perfectly.
How do I get the label to be centered? I have looked at size policies, alignments, using spaceitems and stretch, but I cannot seem to get it working as desired.
Here is a minimal reproducible example:
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import QSize, Qt
from PyQt5.Qt import QVBoxLayout, QWidget
class MyLabel(QLabel):
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen") # Just for visibility
def resizeEvent(self, event):
# Size of 16:9 and scale it to the new size maintaining aspect ratio.
new_size = QSize(16, 9)
new_size.scale(event.size(), Qt.KeepAspectRatio)
self.resize(new_size)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__(None)
# Main widget and layout, and set as centralWidget
self.main_layout = QVBoxLayout()
self.main_widget = QWidget()
self.main_widget.setLayout(self.main_layout)
self.setCentralWidget(self.main_widget)
# Add button to main_layout
label = MyLabel("Hello World")
self.main_layout.addWidget(label)
self.show()
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
Examples of desired outcome:
Examples of actual outcome:
Qt unfortunately doesn't provide a straight forward solution for widgets that require a fixed aspect ratio.
There are some traces in old documentation, but the main problem is that:
all functions related to aspect ratio (hasHeightForWidth() etc) for widgets, layouts and size policies are only considered for the size hint, so no constraint is available if the widget is manually resized by the layout;
as the documentation reports changing the geometry of a widget within the moveEvent() or resizeEvent() might lead to infinite recursion;
it's not possible to (correctly) control the size growth or shrinking while keeping aspect ratio;
For the sake of completeness, here's a partial solution to this issue, but be aware that QLabel is a very peculiar widget that has some constraints related to its text representation (most importantly, with rich text and/or word wrap).
class MyLabel(QLabel):
lastRect = None
isResizing = False
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen")
self.setScaledContents(True)
def restoreRatio(self, lastRect=None):
if self.isResizing:
return
rect = QRect(QPoint(),
QSize(16, 9).scaled(self.size(), Qt.KeepAspectRatio))
if not lastRect:
lastRect = self.geometry()
rect.moveCenter(lastRect.center())
if rect != lastRect:
self.isResizing = True
self.setGeometry(rect)
self.isResizing = False
self.lastRect = None
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
if self.pixmap():
return width * self.pixmap().height() / self.pixmap().width()
return width * 9 / 16
def sizeHint(self):
if self.pixmap():
return self.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.lastRect = self.geometry()
def resizeEvent(self, event):
self.restoreRatio(self.lastRect)
Since the purpose is to display an image, another possibility is to manually paint everything on your own, for which you don't need a QLabel at all, and you can just override the paintEvent of a QWidget, but for performance purposes it could be slightly better to use a container widget with a child QLabel: this would theoretically make things a bit faster, as all the computation is completely done in Qt:
class ParentedLabel(QWidget):
def __init__(self, pixmap=None):
super().__init__()
self.child = QLabel(self, scaledContents=True)
if pixmap:
self.child.setPixmap(pixmap)
def setPixmap(self, pixmap):
self.child.setPixmap(pixmap)
self.updateGeometry()
def updateChild(self):
if self.child.pixmap():
r = self.child.pixmap().rect()
size = self.child.pixmap().size().scaled(
self.size(), Qt.KeepAspectRatio)
r = QRect(QPoint(), size)
r.moveCenter(self.rect().center())
self.child.setGeometry(r)
def hasHeightForWidth(self):
return bool(self.child.pixmap())
def heightForWidth(self, width):
return width * self.child.pixmap().height() / self.child.pixmap().width()
def sizeHint(self):
if self.child.pixmap():
return self.child.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.updateChild()
def resizeEvent(self, event):
self.updateChild()
Finally, another possibility is to use a QGraphicsView, which is probably the faster approach of all, with a small drawback: the image shown based on the given size hint will probably be slightly smaller (a couple of pixels) than the original, with the result that it will seem a bit "out of focus" due to the resizing.
class ViewLabel(QGraphicsView):
def __init__(self, pixmap=None):
super().__init__()
self.setStyleSheet('ViewLabel { border: 0px solid none; }')
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scene = QGraphicsScene()
self.setScene(scene)
self.pixmapItem = QGraphicsPixmapItem(pixmap)
self.pixmapItem.setTransformationMode(Qt.SmoothTransformation)
scene.addItem(self.pixmapItem)
def setPixmap(self, pixmap):
self.pixmapItem.setPixmap(pixmap)
self.updateGeometry()
self.updateScene()
def updateScene(self):
self.fitInView(self.pixmapItem, Qt.KeepAspectRatio)
def hasHeightForWidth(self):
return not bool(self.pixmapItem.pixmap().isNull())
def heightForWidth(self, width):
return width * self.pixmapItem.pixmap().height() / self.pixmapItem.pixmap().width()
def sizeHint(self):
if not self.pixmapItem.pixmap().isNull():
return self.pixmapItem.pixmap().size()
return QSize(160, 90)
def resizeEvent(self, event):
self.updateScene()

expand the size of QPaintDevice after it is initialzed

A little Context
I'm trying to make a QSplashScreen with a custom animation. I've tried many different approaches, with each failures. Mostly, my main technique was to create a new Class which inherits from QSplashScreen and then plan with the paintEvent(). It worked... ish. The animation is not the problem, It is really the QPaintDevice which seems corrupted.
Because I was calling the super(classname, self).__init__(args) only way after in my init and passing it arguments that I modified in the init, I always had corrupted pixels; The image was in weird tones and had lines of colorful pixels in the background. Sometimes it is patterns sometimes it is completely random.
I have tried changing every line of code and the only thing that removed those lines was calling the super() at the beginning of the __init__. Unfortunately, I was making a frame that I was passing to the init. Now that this isn't possible, I would like to modify the size of the QPaintDevice on which my QSplashScreen initializes, because my animation is displayed beyond that frame. I won't post all the code since the custom animation is quite heavy.
Minimal Working Exemple
from PyQt5.QtWidgets import QApplication, QSplashScreen, QMainWindow
from PyQt5.QtCore import Qt, QSize, pyqtSignal, QPoint
from PyQt5.QtGui import QPixmap, QPainter, QIcon, QBrush
import time, sys
class FakeAnimatedSplash(QSplashScreen):
def __init__(self, image):
self.image = image
self.newFrame = QPixmap(self.image.size()+QSize(0, 20))
super(FakeAnimatedSplash, self).__init__(self.newFrame, Qt.WindowStaysOnTopHint)
def showEvent(self, event):
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
painter.drawPixmap(self.image.rect(), self.image)
painter.drawEllipse(QPoint(0, 110), 8, 8)
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.main = QMainWindow()
self.setAttribute(Qt.AA_EnableHighDpiScaling)
self.newSplash()
self.main.show()
def newSplash(self):
pixmap = QPixmap("yourImage.png")
smallerPixmap = pixmap.scaled(100, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation)
splash = FakeAnimatedSplash(smallerPixmap)
splash.setEnabled(False)
splash.show()
start = time.time()
while time.time() < start + 10:
self.processEvents()
def main():
app = App(sys.argv)
app.setWindowIcon(QIcon("ABB_icon.png"))
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Potential Solution
Changing the super() to the beginning makes it work but reduces the QPaintDevice window which hides my animation. I would like to expand it, but there are no methods which can do it after the initialization.
def __init__(self, image):
super(LoadingDotsSplash, self).__init__(image, QtCore.Qt.WindowStaysOnTopHint)
self.image = image
# here a function or method that changes the size of the QPaintDevice
The problem is that newFrame is an uninitialized QPixmap and for efficiency reasons the pixels are not modified so they have random values, and that is because the size of the FakeAnimatedSplash is larger than the QPixmap that is painted. The solution is to set the value of the newFrame pixels to transparent:
class FakeAnimatedSplash(QSplashScreen):
def __init__(self, image):
self.image = image
pix = QPixmap(self.image.size() + QSize(0, 20))
pix.fill(Qt.transparent)
super(FakeAnimatedSplash, self).__init__(pix, Qt.WindowStaysOnTopHint)
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
painter.drawPixmap(self.image.rect(), self.image)
painter.drawEllipse(QPoint(0, 110), 8, 8)

Resize multiple labels containing images with changing window size

I have a window that has six symmetrically placed labels, all showing images (designed using qt-designer with the help of layouts). I would like to resize these images according to the changing window size. I have found some help in previous questions like: PyQt: Detect resizing in Widget-window resized signal
At present, using resizeEvent() in my case does not shrink the images according to the resize function. It is already triggered with the display of my form window thereby making the pushButton useless. Above all, the resulting execution is very slow. My images are of 2058x1536 dimension and displayed transparently.
My qt-designer code is given here: https://pastebin.com/TzM6qiKZ
import Ui_ImageCrop_Test
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtGui import QPixmap, QImage, QPainter, QColor
from PyQt5.QtCore import Qt
class ImageCrop(Ui_ImageCrop_Test.Ui_MainWindow, QMainWindow):
def __init__(self, parent=None):
super(ImageCrop, self).__init__()
self.setupUi(self)
self.transparency = 220
with open("Img_files.txt") as file:
self.img_files = file.read().splitlines()
self.length = len(self.img_files)
self.pushButton_1.clicked.connect(self.click1)
self.label_1.resizeEvent = self.click1
def click1(self, event):
for i in range(6):
image = QImage(self.img_files[i])
image = image.convertToFormat(QImage.Format_ARGB8565_Premultiplied)
p = QPainter(image)
p.setCompositionMode(QPainter.CompositionMode_DestinationIn)
p.fillRect(image.rect(), QColor(0, 0, 0, self.transparency))
p.end()
pixmap = QPixmap(image)
w = int(self.label_1.width() - 4.0)
h = int(self.label_1.height() - 4.0)
smaller_pixmap = pixmap.scaled(w, h, Qt.IgnoreAspectRatio, Qt.FastTransformation)
if i == 0:
self.label_1.setPixmap(smaller_pixmap)
if i == 1:
self.label_2.setPixmap(smaller_pixmap)
if i == 2:
self.label_3.setPixmap(smaller_pixmap)
if i == 3:
self.label_4.setPixmap(smaller_pixmap)
if i == 4:
self.label_5.setPixmap(smaller_pixmap)
if i == 5:
self.label_6.setPixmap(smaller_pixmap)
def main():
app = QApplication(sys.argv)
form1 = ImageCrop()
form1.show()
app.exec_()
if __name__ == '__main__': main()
Is there any solution to run this code faster? For example, I was thinking to make all my labels turn blank during a mouse click at the edge of my window and then images reappear after the mouse button is released. This does not seem so neat. Also, I am not sure if using paintEvent can reduce my lag. Thank you for your suggestions and comments.
QLabel has the scaledContents property that allows the image to scale automatically:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
import Ui_ImageCrop_Test
class ImageCrop(QtWidgets.QMainWindow, Ui_ImageCrop_Test.Ui_MainWindow):
def __init__(self, parent=None):
super(ImageCrop, self).__init__()
self.setupUi(self)
self.pushButton_1.clicked.connect(self.click1)
self.transparency = 220
with open("Img_files.txt") as file:
self.img_files = file.read().splitlines()
#QtCore.pyqtSlot()
def click1(self):
labels = [self.label_1, self.label_2, self.label_3,
self.label_4, self.label_5, self.label_6]
for label, filename in zip(labels, self.img_files):
image = QtGui.QImage(filename)
image = image.convertToFormat(QtGui.QImage.Format_ARGB8565_Premultiplied)
p = QtGui.QPainter(image)
p.setCompositionMode(QtGui.QPainter.CompositionMode_DestinationIn)
p.fillRect(image.rect(), QtGui.QColor(0, 0, 0, self.transparency))
p.end()
pixmap = QtGui.QPixmap(image)
w = int(label.width() - 4.0)
h = int(label.height() - 4.0)
smaller_pixmap = pixmap.scaled(w, h, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.FastTransformation)
label.setPixmap(smaller_pixmap)
label.setScaledContents(True)
def main():
app = QtWidgets.QApplication(sys.argv)
form1 = ImageCrop()
form1.show()
app.exec_()
if __name__ == '__main__': main()

Categories