简体   繁体   中英

PyQt5 left click not working for mouseMoveEvent

I'm trying to learn PyQt5, and I've got this code:

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.label = QLabel()
        canvas = QPixmap(400, 300)
        canvas.fill(Qt.white)
        self.label.setPixmap(canvas)

        self.setCentralWidget(self.label)
    

    def mouseMoveEvent(self, e):
        painter = QPainter(self.label.pixmap())
        painter.drawPoint(e.x(), e.y())
        painter.end()
        self.update()


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

And I can draw using right click to draw, but when I left click, it drags the window instead of drawing. This even happens when I make the window fullscreen so I can't move it. How can I stop it from dragging the window so it will draw instead?

In some configurations (specifically, on Linux, and depending on the window manager settings), dragging the left mouse button on an empty (non interactive) area of a QMainWindow allows dragging the whole window.

To prevent that, the mouse move event has to be accepted by the child widget that receives it.

While this can be achieved with an event filter, it's usually better to use a subclass, and this is even more important whenever the widget has to deal with mouse events it receives, exactly like in this case.

Another aspect that has to be considered is that just updating the QLabel pixmap is not completely sufficient, because it doesn't automatically force its update. Also, since Qt 5.15, QLabel.pixmap() doesn't return a pointer to the pixmap, but rather its copy. This means that you should always keep a local reference to the pixmap for the whole time required to access it (otherwise your program will crash ), and then call setPixmap() again with the updated pixmap after "ending" the painter. This will automatically schedule an update of the label.

The above may be a bit confusing if you're not used to languages that allow pointers as arguments, but, in order to clarify how it works, you can consider the pixmap() property similarly to the text() one:

text = self.label.text()
text += 'some other text'

The above will obviously not change the text of the label, most importantly because, in most languages (including Python) strings are always immutable objects, so text +=... actually replaces the text reference with another string object.

To clarify, consider the following:

text1 = text2 = self.label.text()
text1 += 'some other text'
print(text1 == text2)

Which will return False .

Now consider this instead:

list1 = list2 = []
list1 += ['item']
print(list1 == list2)

Which will return True , because list is a mutable type, and in python changing the content of a mutable type will affect any reference to it [1] , since they refer to the same object.

Until Qt < 5.15, the pixmap of QLabel behaved similarly to a list, meaning that any painting on the label.pixmap() would actually change the content of the displayed pixmap (requiring label.update() to actually show the change). After Qt 5.15 this is no longer valid, as the returned pixmap behaves similarly to a returned string: altering its contents won't change the label's pixmap.

So, the proper way to update the pixmap is to:

  1. handle the mouse event in the label instance (either by subclassing or using an event filter), and not in a parent;
  2. get the pixmap, keep its reference until painting has completed, and call setPixmap() afterwards (mandatory since Qt 5.15, but also suggested anyway);

Finally, QLabel has an alignment property that, when using a pixmap, is used to set the alignment of the pixmap to the available space that the layout manager provides. The default is left aligned and vertically centered ( Qt.AlignLeft|Qt.AlignVCenter ).
QLabel also features the scaledContents property, which always scales the pixmap to the current size of the label ( not considering the aspect ratio).

The above means one of the following:

  • the pixmap will always be displayed at its actual size, and eventually aligned within its available space;
  • if the scaledContents property is True , the alignment is ignored and the pixmap will be always scaled to the full extent of its available space; whenever that property is True , the resulting pixmap is also cached, so you have to clear its cache every time (at least, with Qt5);
  • if you need to always keep aspect ratio, using QLabel is probably pointless, and you may prefer a plain QWidget that actively draws the pixmap within a paintEvent() override;

Considering the above, here is a possible implementation of the label (ignoring the ratio):

class PaintLabel(QLabel):
    def mouseMoveEvent(self, event):
        pixmap = self.pixmap()
        if pixmap is None:
            return
        pmSize = pixmap.size()
        if not pmSize.isValid():
            return

        pos = event.pos()

        scaled = self.hasScaledContents()
        if scaled:
            # scale the mouse position to the actual pixmap size
            pos = QPoint(
                round(pos.x() * pmSize.width() / self.width()), 
                round(pos.y() * pmSize.height() / self.height())
            )
        else:
            # translate the mouse position depending on the alignment
            alignment = self.alignment()
            dx = dy = 0
            if alignment & Qt.AlignRight:
                dx += pmSize.width() - self.width()
            elif alignment & Qt.AlignHCenter:
                dx += round((pmSize.width() - self.width()) / 2)
            if alignment & Qt.AlignBottom:
                dy += pmSize.height() - self.height()
            elif alignment & Qt.AlignVCenter:
                dy += round((pmSize.height() - self.height()) // 2)
            pos += QPoint(dx, dy)

        painter = QPainter(pixmap)
        painter.drawPoint(pos)
        painter.end()

        # this will also force a scheduled update
        self.setPixmap(pixmap)

        if scaled:
            # force pixmap cache clearing
            self.setScaledContents(False)
            self.setScaledContents(True)

    def minimumSizeHint(self):
        # just for example purposes
        return QSize(10, 10)

And here is an example of its usage:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.label = PaintLabel()
        canvas = QPixmap(400, 300)
        canvas.fill(Qt.white)
        self.label.setPixmap(canvas)

        self.hCombo = QComboBox()
        for i, hPos in enumerate(('Left', 'HCenter', 'Right')):
            hAlign = getattr(Qt, 'Align' + hPos)
            self.hCombo.addItem(hPos, hAlign)
            if self.label.alignment() & hAlign:
                self.hCombo.setCurrentIndex(i)

        self.vCombo = QComboBox()
        for i, vPos in enumerate(('Top', 'VCenter', 'Bottom')):
            vAlign = getattr(Qt, 'Align' + vPos)
            self.vCombo.addItem(vPos, vAlign)
            
            if self.label.alignment() & vAlign:
                self.vCombo.setCurrentIndex(i)

        self.scaledChk = QCheckBox('Scaled')

        central = QWidget()
        mainLayout = QVBoxLayout(central)

        panel = QHBoxLayout()
        mainLayout.addLayout(panel)
        panel.addWidget(self.hCombo)
        panel.addWidget(self.vCombo)
        panel.addWidget(self.scaledChk)
        mainLayout.addWidget(self.label)

        self.setCentralWidget(central)

        self.hCombo.currentIndexChanged.connect(self.updateLabel)
        self.vCombo.currentIndexChanged.connect(self.updateLabel)
        self.scaledChk.toggled.connect(self.updateLabel)

    def updateLabel(self):
        self.label.setAlignment(Qt.AlignmentFlag(
            self.hCombo.currentData() | self.vCombo.currentData()
        ))
        self.label.setScaledContents(self.scaledChk.isChecked())


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)

    window = MainWindow()
    window.show()

    sys.exit(app.exec())

Note that if you need more advanced control over the pixmap display and painting (including aspect ratio, but also proper zoom capabilities and any possible complex feature), then the common suggestion is to completely ignore QLabel, as said above: either use a basic QWidget, or consider the more complex (but much more powerful) Graphics View Framework . This will also allow proper editing features, as you can add non-destructive editing that will show ("paint") the result without affecting the actual, original object.

[1]: The above is based on the fact that a function or operator can actually mutate the object: the += operator actually calls the __add__ magic method that, in the case of lists, updates the contents of the same list.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM