Python Forum

Full Version: [PyQt] Tutorial bug with PyQt6 canvas
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
I'm following an online PyQt tutorial and the last example fails to run. I can see why, but don't know how to fix it. It looks like the instantiation of the Canvas fails to create the canvas, because the "canvas" variable isn't assigned anything.

It's the last example (in three separate code blocks) on this page:
https://www.pythonguis.com/tutorials/pyq...-graphics/

    #!/bin/python3
    
    import sys
    from PyQt6 import QtCore, QtGui, QtWidgets, uic
    from PyQt6.QtCore import Qt
    
    class Canvas(QtWidgets.QLabel):
        def __init__(self):
            super().__init__()
            pixmap = QtGui.QPixmap(600, 300)
            pixmap.fill(Qt.GlobalColor.white)
            self.setPixmap(canvas)
    
            self.last_x, self.last_y = None, None
            self.pen_color = QtGui.QColor('#000000')
    
        def set_pen_color(self, c):
            self.pen_color = QtGui.QColor(c)
    
        def mouseMoveEvent(self, e):
            if self.last_x is None: # First event.
                self.last_x = e.position().x()
                self.last_y = e.position().y()
                return # Ignore the first time.
    
            canvas = self.label.pixmap()
            painter = QtGui.QPainter(canvas)
            p = painter.pen()
            p.setWidth(4)
            p.setColor(self.pen_color)
            painter.setPen(p)
            painter.drawLine(self.last_x, self.last_y, e.position().x(), e.position().y())
            painter.end()
            self.label.setPixmap(canvas)
    
            # Update the origin for next time.
            self.last_x = e.position().x()
            self.last_y = e.position().y()
    
        def mouseReleaseEvent(self, e):
            self.last_x = None
            self.last_y = None
    
    
    COLORS = [
    # 17 undertones https://lospec.com/palette-list/17undertones
    '#000000', '#141923', '#414168', '#3a7fa7', '#35e3e3', '#8fd970', '#5ebb49',
    '#458352', '#dcd37b', '#fffee5', '#ffd035', '#cc9245', '#a15c3e', '#a42f3b',
    '#f45b7a', '#c24998', '#81588d', '#bcb0c2', '#ffffff',
    ]
    
    
    class QPaletteButton(QtWidgets.QPushButton):
    
        def __init__(self, color):
            super().__init__()
            self.setFixedSize(QtCore.QSize(24,24))
            self.color = color
            self.setStyleSheet("background-color: %s;" % color)
    
    
    class MainWindow(QtWidgets.QMainWindow):
    
        def __init__(self):
            super().__init__()
    
            self.canvas = Canvas()
    
            w = QtWidgets.QWidget()
            l = QtWidgets.QVBoxLayout()
            w.setLayout(l)
            l.addWidget(self.canvas)
    
            palette = QtWidgets.QHBoxLayout()
            self.add_palette_buttons(palette)
            l.addLayout(palette)
    
            self.setCentralWidget(w)
    
        def add_palette_buttons(self, layout):
            for c in COLORS:
                b = QPaletteButton(c)
                b.pressed.connect(lambda c=c: self.canvas.set_pen_color(c))
                layout.addWidget(b)
    
    
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec()
Not sure where that code came from, but not from a working example. This code runs:
class Canvas(QtWidgets.QLabel):
    def __init__(self):
        super().__init__()
        pixmap = QtGui.QPixmap(600, 300)
        pixmap.fill(Qt.GlobalColor.white)
        self.setPixmap(pixmap)

        self.last_x, self.last_y = None, None
        self.pen_color = QtGui.QColor("#000000")

    def set_pen_color(self, c):
        self.pen_color = QtGui.QColor(c)

    def mouseMoveEvent(self, e):
        if self.last_x is None:  # First event.
            self.last_x = e.position().x()
            self.last_y = e.position().y()
            return  # Ignore the first time.

        canvas = self.pixmap()
        painter = QtGui.QPainter(canvas)
        p = painter.pen()
        p.setWidth(4)
        p.setColor(self.pen_color)
        painter.setPen(p)
        painter.drawLine(int(self.last_x), int(self.last_y), int(e.position().x()), int(e.position().y()))
        painter.end()
        self.setPixmap(canvas)

        # Update the origin for next time.
        self.last_x = e.position().x()
        self.last_y = e.position().y()

    def mouseReleaseEvent(self, e):
        self.last_x = None
        self.last_y = None
Debugging something like this involves playing detective. This is suspicious:
        pixmap = QtGui.QPixmap(600, 300)
        pixmap.fill(Qt.GlobalColor.white)
        self.setPixmap(canvas)
Why create a pixmap and not use it? Did they mean to do this?
        canvas = QtGui.QPixmap(600, 300)
        canvas.fill(Qt.GlobalColor.white)
        self.setPixmap(canvas)
That gets past the first hurdle, but then you run into this:
        canvas = self.label.pixmap()
"self" is a QLabel. Maybe this code was originally written to draw inside a label object but was changed to be "self-contained". The self contained version was not well tested, or maybe the wrong version of code was published. Earlier the code creates a pixmap for self, so that is probably where we get the pixmap.
        canvas = self.pixmap()
There's also a problem with passing floats to drawline(). I did this
painter.drawLine(int(self.last_x), int(self.last_y), int(e.position().x()), int(e.position().y()))
But you could also do this:
    def mouseMoveEvent(self, e):
        x = int(e.position().x())
        y = int(e.position().y())
        if self.last_x is not None:
            canvas = self.pixmap()
            painter = QtGui.QPainter(canvas)
            p = painter.pen()
            p.setWidth(4)
            p.setColor(self.pen_color)
            painter.setPen(p)
            painter.drawLine(self.last_x, self.last_y, x, y)
            painter.end()
            self.setPixmap(canvas)
        self.last_x = x
        self.last_y = y
I've changed line 12 in your code and it shows.

self.setPixmap(pixmap)
A few more small changes that @deanhystad pointed out.

#!/bin/python3
     
import sys
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt6.QtCore import Qt
     
class Canvas(QtWidgets.QLabel):
    def __init__(self):
        super().__init__()
        pixmap = QtGui.QPixmap(600, 300)
        pixmap.fill(Qt.GlobalColor.white)
        self.setPixmap(pixmap)
     
        self.last_x, self.last_y = None, None
        self.pen_color = QtGui.QColor('#000000')
     
    def set_pen_color(self, c):
        self.pen_color = QtGui.QColor(c)
     
    def mouseMoveEvent(self, e):
        if self.last_x is None: # First event.
            self.last_x = e.position().x()
            self.last_y = e.position().y()
            return # Ignore the first time.
     
        canvas = self.pixmap()
        painter = QtGui.QPainter(canvas)
        p = painter.pen()
        p.setWidth(4)
        p.setColor(self.pen_color)
        painter.setPen(p)
        painter.drawLine(int(self.last_x), int(self.last_y), int(e.position().x()), int(e.position().y()))
        painter.end()
        self.setPixmap(canvas)
     
        # Update the origin for next time.
        self.last_x = e.position().x()
        self.last_y = e.position().y()
     
    def mouseReleaseEvent(self, e):
        self.last_x = None
        self.last_y = None
     
     
COLORS = [
# 17 undertones https://lospec.com/palette-list/17undertones
'#000000', '#141923', '#414168', '#3a7fa7', '#35e3e3', '#8fd970', '#5ebb49',
'#458352', '#dcd37b', '#fffee5', '#ffd035', '#cc9245', '#a15c3e', '#a42f3b',
'#f45b7a', '#c24998', '#81588d', '#bcb0c2', '#ffffff',
]
     
     
class QPaletteButton(QtWidgets.QPushButton):
     
    def __init__(self, color):
        super().__init__()
        self.setFixedSize(QtCore.QSize(24,24))
        self.color = color
        self.setStyleSheet("background-color: %s;" % color)
     
     
class MainWindow(QtWidgets.QMainWindow):
     
    def __init__(self):
        super().__init__()
     
        self.canvas = Canvas()
     
        w = QtWidgets.QWidget()
        l = QtWidgets.QVBoxLayout()
        w.setLayout(l)
        l.addWidget(self.canvas)
     
        palette = QtWidgets.QHBoxLayout()
        self.add_palette_buttons(palette)
        l.addLayout(palette)
     
        self.setCentralWidget(w)
     
    def add_palette_buttons(self, layout):
        for c in COLORS:
            b = QPaletteButton(c)
            b.pressed.connect(lambda c=c: self.canvas.set_pen_color(c))
            layout.addWidget(b)
     
     
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Wow, thanks for the help guys. I only started learning PyQt yesterday so it's all been uphill.

I wanted an easy way to make simple applications for myself when I get an idea and found that it wasn't simple at all with X11/Xlib.

At present I'm trying to create a font viewer/editor. I have a bunch of font definition files that will get used with a microcontroller and OLED and I wanted a simple way to view the fonts before trying to implement them on the micro.

Once I get the canvas display a single font I'm going to add a listbox to select a font from, and after that a single character editor.
Okay, so...The code does run of course. But it runs into a problem that the webpage already pointed out earlier. Namely, that resizing the window causes problem with how the canvas drawing works. And it does mention using a label and also that the code is intended to be self-contained.

If I run the code, do some scribbling, resize the window, and scribble some more...The pen is obviously offset. Apparently by the amount the widget is offset within the window.