When I resize the window, my QLabel canvas is not resized, and the drawing coordinates are still coming from the window, not the canvas, so everything is shifted. How do I make a QPainter canvas on a QLabel resize properly with the window?
If you've built a drawing canvas using a QLabel and a QPixmap, you've probably noticed that resizing the window doesn't resize the canvas. The pixmap stays at its original size, and worse, the mouse coordinates can end up misaligned — so your drawing appears offset from where you're actually clicking. This is a common issue, and the fix involves a few small but important changes to how the canvas is set up and how it responds to resize events.
Let's walk through the problem and build a working solution.
Why the canvas doesn't resize
When you create a QPixmap with a fixed size and set it on a QLabel, the pixmap stays at that size no matter what happens to the window. The QLabel might grow or shrink, but the pixmap inside it doesn't follow along. On top of that, mouse events report coordinates relative to the QLabel widget, not the pixmap — so if the label is bigger than the pixmap, your drawing ends up in the wrong place.
To fix this, you need to do a few things:
- Set a size policy so the label participates properly in layouts.
- Set a minimum size so the label can shrink as well as grow.
- Handle the resize event to update the pixmap when the widget changes size.
- Optionally, keep the aspect ratio and center the pixmap.
Setting up the canvas class
Here's a basic Canvas class that handles resizing. We'll start with the setup:
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor, QGuiApplication, QPainter, QPen, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QSizePolicy,
QVBoxLayout,
QWidget,
)
import random
import sys
SPRAY_PARTICLES = 20
SPRAY_DIAMETER = 10
class Canvas(QLabel):
def __init__(self):
super().__init__()
self.pen_color = QColor("#000000")
self.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setMinimumSize(10, 10)
self._create_pixmap(600, 300)
def _create_pixmap(self, width, height):
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#ffffff"))
self.setPixmap(pixmap)
A few things to note here:
setSizePolicywithExpandingtells the layout system that this widget wants to grow to fill available space. Without this, the label stays at the pixmap's natural size. For more on how layouts work in PyQt6, see the layouts tutorial.setAlignment(Qt.AlignmentFlag.AlignCenter)centers the pixmap inside the label if they're different sizes.setMinimumSize(10, 10)allows the widget to shrink below the pixmap's original size. Without this, Qt prevents the widget from getting smaller than the pixmap.
Handling the resize event
When the window is resized, we need to create a new pixmap that matches the widget's new size. We do this by overriding resizeEvent:
def resizeEvent(self, event):
self._create_pixmap(self.width(), self.height())
This replaces the pixmap with a fresh one every time the widget changes size. The downside is that any existing drawing is lost. If you want to preserve the drawing, you can scale the old pixmap onto the new one instead:
def resizeEvent(self, event):
old_pixmap = self.pixmap()
new_pixmap = QPixmap(self.width(), self.height())
new_pixmap.fill(QColor("#ffffff"))
if old_pixmap:
scaled = old_pixmap.scaled(
self.width(),
self.height(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
painter = QPainter(new_pixmap)
# Center the scaled pixmap on the new canvas.
x = (new_pixmap.width() - scaled.width()) // 2
y = (new_pixmap.height() - scaled.height()) // 2
painter.drawPixmap(x, y, scaled)
painter.end()
self.setPixmap(new_pixmap)
This approach keeps the aspect ratio of the original drawing and centers it on the new canvas. The SmoothTransformation flag ensures the scaled image looks decent rather than blocky.
Be aware that repeatedly scaling a pixmap will gradually degrade its quality, since each scale is a lossy operation. For a simple drawing app this is usually fine, but for something more precise you might want to store the drawing operations themselves (as a list of points, for example) and replay them onto a fresh pixmap at the new size. For more advanced drawing with QPainter, take a look at the bitmap graphics tutorial.
Fixing the mouse coordinates
With the pixmap now matching the widget size, the mouse coordinates from mouseMoveEvent will line up correctly. Here's the drawing method:
def set_pen_color(self, c):
self.pen_color = QColor(c)
def mouseMoveEvent(self, event):
painter = QPainter(self.pixmap())
p = QPen()
p.setWidth(1)
p.setColor(self.pen_color)
painter.setPen(p)
pos = event.position()
for n in range(SPRAY_PARTICLES):
xo = random.gauss(0, SPRAY_DIAMETER)
yo = random.gauss(0, SPRAY_DIAMETER)
painter.drawPoint(int(pos.x() + xo), int(pos.y() + yo))
painter.end()
self.update()
In PyQt6, mouse events use event.position() which returns a QPointF with floating-point coordinates. We convert to int when passing to drawPoint.
A note about setScaledContents
You might think self.setScaledContents(True) would solve the problem — it makes the pixmap visually fill the label. But it causes issues with drawing, because the pixmap itself is still its original size. The coordinates you draw at won't match what you see on screen, since Qt is scaling the display but not the underlying pixmap. It's better to handle the resizing yourself as shown above.
Complete working example
Here's a full, runnable example that puts everything together. You can spray-paint on the canvas, and it resizes with the window. If you're new to building PyQt6 applications, you might want to start with the creating your first window tutorial and the widgets overview first.
import random
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor, QPainter, QPen, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
SPRAY_PARTICLES = 20
SPRAY_DIAMETER = 10
class Canvas(QLabel):
def __init__(self):
super().__init__()
self.pen_color = QColor("#000000")
self.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setMinimumSize(10, 10)
self._create_pixmap(600, 300)
def _create_pixmap(self, width, height):
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#ffffff"))
self.setPixmap(pixmap)
def set_pen_color(self, c):
self.pen_color = QColor(c)
def mouseMoveEvent(self, event):
painter = QPainter(self.pixmap())
pen = QPen()
pen.setWidth(1)
pen.setColor(self.pen_color)
painter.setPen(pen)
pos = event.position()
for n in range(SPRAY_PARTICLES):
xo = random.gauss(0, SPRAY_DIAMETER)
yo = random.gauss(0, SPRAY_DIAMETER)
painter.drawPoint(int(pos.x() + xo), int(pos.y() + yo))
painter.end()
self.update()
def resizeEvent(self, event):
old_pixmap = self.pixmap()
new_pixmap = QPixmap(self.width(), self.height())
new_pixmap.fill(QColor("#ffffff"))
if old_pixmap and not old_pixmap.isNull():
scaled = old_pixmap.scaled(
self.width(),
self.height(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
painter = QPainter(new_pixmap)
x = (new_pixmap.width() - scaled.width()) // 2
y = (new_pixmap.height() - scaled.height()) // 2
painter.drawPixmap(x, y, scaled)
painter.end()
self.setPixmap(new_pixmap)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Resizable Canvas")
layout = QVBoxLayout(self)
self.canvas = Canvas()
layout.addWidget(self.canvas)
# Color buttons.
button_layout = QHBoxLayout()
colors = [
("#000000", "Black"),
("#ff0000", "Red"),
("#00aa00", "Green"),
("#0000ff", "Blue"),
]
for color, name in colors:
btn = QPushButton(name)
btn.setStyleSheet(f"color: {color};")
btn.clicked.connect(
lambda checked, c=color: self.canvas.set_pen_color(c)
)
button_layout.addWidget(btn)
layout.addLayout(button_layout)
self.resize(700, 400)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this, try resizing the window, and you'll see the canvas expands and contracts with it. The drawing stays aligned with your mouse, and any existing spray paint is preserved (scaled) when you resize.
If you'd prefer the canvas to simply clear on resize — which avoids the gradual quality loss from repeated scaling — replace the resizeEvent with the simpler version:
def resizeEvent(self, event):
self._create_pixmap(self.width(), self.height())
Choose whichever approach fits your app best. If you want to build a more fully-featured drawing tool, you might also be interested in creating your own custom widgets where you have complete control over the paint event.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!