5

I'm learning python and PySide2 and following up some tutorials from learnpytq, specifically https://www.learnpyqt.com/courses/custom-widgets/bitmap-graphics/ and I'm stuck at a point.

Down the line, after creating the pixmap canvas, we move the mouseMoveEvent on the widget in order to ensure that the coordinates of the mouse are always relative to the canvas. I've copied the source provided but still in my running app, the mouse position is relative to the window (or parent widget, I'm not sure), resulting in a line drawn offset to the mouse position.

Here's the code:

import sys
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt

class Canvas(QtWidgets.QLabel):

    def __init__(self):
        super().__init__()
        pixmap = QtGui.QPixmap(600, 300)
        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.x()
            self.last_y = e.y()
            return # Ignore the first time.

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

        # Update the origin for next time.
        self.last_x = e.x()
        self.last_y = e.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_()

Can anyone spot what I'm doing wrong?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
pepeday
  • 89
  • 5

1 Answers1

4

The problem comes from the fact that you're drawing according to the widget coordinates, and not those of the actual "canvas" (the "embedded" pixmap), which can be translated if the space available to the QLabel is bigger than the QPixmap size.

If, for example, the image is vertically centered, you resize the window and the label height becomes 400 (which is bigger than the pixmap height), whenever you click at position 100, 100, that position will be actually vertically translated by 50 pixel (the height of the label minus the height of the image, divided by 2).

To actually get the position according to the pixmap you have to compute it by yourself, and then translate the mouse point accordingly:

    def mouseMoveEvent(self, e):
        if self.last_x is None: # First event.
            self.last_x = e.x()
            self.last_y = e.y()
            return # Ignore the first time.
        rect = self.contentsRect()
        pmRect = self.pixmap().rect()
        if rect != pmRect:
            # the pixmap rect is different from that available to the label
            align = self.alignment()
            if align & QtCore.Qt.AlignHCenter:
                # horizontally align the rectangle
                pmRect.moveLeft((rect.width() - pmRect.width()) / 2)
            elif align & QtCore.Qt.AlignRight:
                # align to bottom
                pmRect.moveRight(rect.right())
            if align & QtCore.Qt.AlignVCenter:
                # vertically align the rectangle
                pmRect.moveTop((rect.height() - pmRect.height()) / 2)
            elif align &  QtCore.Qt.AlignBottom:
                # align right
                pmRect.moveBottom(rect.bottom())

        painter = QtGui.QPainter(self.pixmap())
        p = painter.pen()
        p.setWidth(4)
        p.setColor(self.pen_color)
        painter.setPen(p)
        # translate the painter by the pmRect offset; note the negative sign
        painter.translate(-pmRect.topLeft())
        painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
        painter.end()
        self.update()

        # Update the origin for next time.
        self.last_x = e.x()
        self.last_y = e.y()
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • This is great thanks! I haven't tested yet but I understand what you wrote more or less. Another solution would be to limit the dimensions of the QLabel to the pixmap, correct? – pepeday Nov 26 '19 at 12:19
  • 1
    Yes, of course. Also, note that it seems that there's a bug preventing the repainting the pixmap if you set `setScaledContents(True)` (which resizes the pixmap to fill the whole area). To avoid that, just reset that property to false and true rightafter, instead of calling update (which would be called internally anyway). – musicamante Nov 26 '19 at 12:33