简体   繁体   English

PyQt5 左键单击不适用于 mouseMoveEvent

[英]PyQt5 left click not working for mouseMoveEvent

I'm trying to learn PyQt5, and I've got this code:我正在尝试学习 PyQt5,我得到了这段代码:

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.我可以使用右键单击来绘制,但是当我左键单击时,它会拖动 window 而不是绘图。 This even happens when I make the window fullscreen so I can't move it.当我将 window 全屏显示时甚至会发生这种情况,所以我无法移动它。 How can I stop it from dragging the window so it will draw instead?我怎样才能阻止它拖动 window 所以它会绘制?

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.在某些配置中(具体来说,在 Linux 上,并且取决于 window 管理器设置),在 QMainWindow 的空白(非交互式)区域上拖动鼠标左键允许拖动整个 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.必须考虑的另一个方面是仅仅更新 QLabel 像素图是不够的,因为它不会自动强制更新。 Also, since Qt 5.15, QLabel.pixmap() doesn't return a pointer to the pixmap, but rather its copy.此外,自 Qt 5.15 起, QLabel.pixmap()不再返回指向像素图的指针,而是返回其副本。 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.这意味着您应该在访问它所需的整个时间内始终保持对像素图的本地引用(否则您的程序将崩溃),然后在“结束”画家后使用更新的像素图再次调用setPixmap() This will automatically schedule an update of the label.这将自动安排 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:如果您不习惯允许指针为 arguments 的语言,上面的内容可能有点令人困惑,但是,为了阐明它是如何工作的,您可以将pixmap()属性视为类似于text() ) 的属性:

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.以上显然不会改变 label 的文本,最重要的是,在大多数语言(包括 Python)中,字符串始终是不可变的对象,因此text +=...实际上将text引用替换为另一个字符串 object。

To clarify, consider the following:为了澄清,请考虑以下几点:

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

Which will return False .这将返回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.这将返回True ,因为list是可变类型,并且在 python 中更改可变类型的内容将影响对它的任何引用[1] ,因为它们引用相同的 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).在 Qt < 5.15 之前,QLabel 的像素图的行为类似于列表,这意味着label.pixmap()上的任何绘画实际上都会更改显示的像素图的内容(需要label.update()才能实际显示更改)。 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.在 Qt 5.15 之后,这不再有效,因为返回的像素图的行为类似于返回的字符串:更改其内容不会更改标签的像素图。

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;在 label 实例中处理鼠标事件(通过子类化或使用事件过滤器),而不是在父实例中;
  2. get the pixmap, keep its reference until painting has completed, and call setPixmap() afterwards (mandatory since Qt 5.15, but also suggested anyway);获取像素图,保留其引用直到绘制完成,然后调用setPixmap() (自 Qt 5.15 起强制执行,但无论如何也建议这样做);

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.最后,QLabel 有一个alignment属性,在使用像素图时,用于将像素图的 alignment 设置为布局管理器提供的可用空间。 The default is left aligned and vertically centered ( Qt.AlignLeft|Qt.AlignVCenter ).默认是左对齐和垂直居中( 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). QLabel 还具有scaledContents属性,它始终将像素图缩放到 label 的当前大小(考虑纵横比)。

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;如果scaledContents属性为True ,则忽略 alignment 并且像素图将始终缩放到其可用空间的全部范围; whenever that property is True , the resulting pixmap is also cached, so you have to clear its cache every time (at least, with Qt5);每当该属性为True时,生成的像素图也会被缓存,因此您每次都必须清除其缓存(至少使用 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;如果您需要始终保持纵横比,使用 QLabel 可能毫无意义,您可能更喜欢在paintEvent()覆盖中主动绘制像素图的普通 QWidget;

Considering the above, here is a possible implementation of the label (ignoring the ratio):考虑到上述情况,这里是 label 的可能实现(忽略比率):

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 .请注意,如果您需要对像素图显示和绘画进行更高级的控制(包括宽高比,还包括适当的缩放功能和任何可能的复杂功能),那么常见的建议是完全忽略 QLabel,如上所述:要么使用基本的 QWidget ,或者考虑更复杂(但更强大)的图形视图框架 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.这也将允许适当的编辑功能,因为您可以添加非破坏性编辑来显示(“绘制”)结果而不影响实际的原始 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. [1]:以上基于 function 或运算符实际上可以改变object 的事实: +=运算符实际上调用了__add__魔术方法,在列表的情况下,该方法更新同一列表的内容。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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