Qt 事件管理

参考:Events and signals in PyQt5

所有的应用都是事件驱动的。事件大部分都是由用户的行为产生的,当然也有其他的事件产生方式,比如网络的连接,窗口管理器或者定时器等。调用应用的 exec_() 方法时,应用会进入主循环,主循环会监听和分发事件。

在事件模型中,有三个角色:

  • 事件源(event source):发生了状态改变的对象,用于生成事件。
  • 事件对象(event object):将状态更改封装在事件源中。
  • 事件目标(event target):即要通知的对象。事件源对象将处理事件的任务委托给事件目标。

1 事件和信号及槽的区别

信号与槽可以说是对事件处理机制的高级封装,如果说事件是用来创建窗口控件的,那么信号与槽就是用来对这个窗口控件进行使用的。比如一个按钮,当我们使用这个按钮时,只关心 clicked 信号,至于这个按钮如何接收并处理鼠标点击事件,然后再发射这个信号,则不用关心。但是如果要重载一个按钮,这时就要关心这个问题了。比如可以改变它的行为:在鼠标按键按下时触发 clicked 信号,而不是在释放时。

PyQt5/PySide2 是对 Qt 的封装,Qt 程序是事件驱动的,它的每个动作都由幕后某个事件所触发。Qt 事件的类型有很多,常见的 Qt 事件如下:

  • 键盘事件:按键按下和松开。
  • 鼠标事件:鼠标指针移动、鼠标按键按下和松开。
  • 拖放事件:用鼠标进行拖放。
  • 滚轮事件:鼠标滚轮滚动。
  • 绘屏事件:重绘屏幕的某些部分。
  • 定时事件:定时器到时。
  • 焦点事件:键盘焦点移动。
  • 进入和离开事件:鼠标指针移入 Widget 内,或者移出。
  • 移动事件:Widget 的位置改变。
  • 大小改变事件:Widget 的大小改变。
  • 显示和隐藏事件:Widget 显示和隐藏。
  • 窗口事件:窗口是否为当前窗口。

还有一些常见的 Qt 事件,比如 Socket 事件、剪贴板事件、字体改变事件、布局改变事件等。

下面看一个 LCD 的例子:

from xinet import QtWidgets, QtCore, QtGui, Signal
from xinet.run_qt import run


class LCDNumber(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.initUI()
        lcd = QtWidgets.QLCDNumber(self)
        sld = QtWidgets.QSlider(QtCore.Qt.Horizontal)  # 横向滑块
        vbox = QtWidgets.QVBoxLayout()
        vbox.addWidget(lcd)
        vbox.addWidget(sld)
        self.setLayout(vbox)
        sld.valueChanged.connect(lcd.display)

    def initUI(self):
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Signal and slot')


if __name__ == "__main__":
    run(LCDNumber)

效果:

其中 sld.valueChanged 是滑块的值改变的信号,lcd.display 是 LCD 数字的槽函数。即 sld 发送被改变的值给 lcd 并显示出来。

2 使用事件处理的方法

PyQt5/PySide2 提供了如下 5 种事件处理和过滤方法(由弱到强),其中只有前两种方法使用最频繁。

  1. 重新实现事件函数:比如 mousePressEvent()keyPressEvent()paintEvent()。这是最常规的事件处理方法。
  2. 重新实现 QObject.event():一般用在 PyQt5/PySide2 没有提供该事件的处理函数的情况下,即增加新事件时。
  3. 安装事件过滤器 :如果对 QObject 调用 installEventFilter,则相当于为这个 QObject 安装了一个事件过滤器,对于 QObject 的全部事件来说,它们都会先传递到事件过滤函数 eventFilter 中,在这个函数中我们可以抛弃或者修改这些事件,比如可以对自己感兴趣的事件使用自定义的事件处理机制,对其他事件使用默认的事件处理机制。由于这种方法会对调用 installEventFilter 的所有 QObject 的事件进行过滤,因此如果要过滤的事件比较多,则会降低程序的性能。
  4. QApplication 中安装事件过滤器 :这种方法比上一种方法更强大:QApplication 的事件过滤器将捕获所有 QObject 的所有事件,而且第一个获得该事件。也就是说,在将事件发送给其他任何一个事件过滤器之前(就是在第三种方法之前),都会先发送给 QApplication 的事件过滤器。
  5. 重新实现 QApplicationnotify() 方法 :使用 notify()来分发事件。要想在任何事件处理器之前捕获事件,唯一的方法就是重新实现 QApplicationnotify()。在实践中,在调试时才会使用这种方法。

3 重新实现事件函数

重载 keyPressEvent 函数,实现按下 Esc 键程序就会退出:

from xinet import QtWidgets, QtCore, QtGui, Signal
from xinet.run_qt import run


class EscWin(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.initUI()
        
    def initUI(self):
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Event handler')

    def keyPressEvent(self, e):
        if e.key() == QtCore.Qt.Key_Escape:
            self.close()


if __name__ == "__main__":
    run(EscWin)

4 事件对象

事件对象是用来描述一系列的事件自身属性的对象。

这个示例中,我们在一个组件里显示鼠标的 X 和 Y 坐标。

from xinet import QtWidgets, QtCore, QtGui, Signal
from xinet.run_qt import run


class Window(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.initUI()

    def initUI(self):
        grid = QtWidgets.QGridLayout()
        x = 0
        y = 0
        self.text = f'x: {x},  y: {y}'
        self.label = QtWidgets.QLabel(self.text, self)
        grid.addWidget(self.label, 0, 0, QtCore.Qt.AlignTop)
        # 事件追踪默认没有开启,当开启后才会追踪鼠标的点击事件
        self.setMouseTracking(True)
        self.setLayout(grid)
        self.setGeometry(300, 300, 450, 300)
        self.setWindowTitle('Event object')

    def mouseMoveEvent(self, e):
        x = e.x()
        y = e.y()
        text = f'x: {x},  y: {y}'
        self.label.setText(text)


if __name__ == "__main__":
    run(Window)

效果:

5 事件发送

有时候我们会想知道是哪个组件发出了一个信号,sender() 方法能搞定这件事。

from xinet import QtWidgets, QtCore, QtGui, Signal
from xinet.run_qt import run


class Window(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.init_Ui()

    def init_Ui(self):
        btn1 = QtWidgets.QPushButton("Button 1", self)
        btn1.move(30, 50)
        btn2 = QtWidgets.QPushButton("Button 2", self)
        btn2.move(150, 50)

        btn1.clicked.connect(self.buttonClicked)
        btn2.clicked.connect(self.buttonClicked)

        self.statusBar()
        self.setGeometry(300, 300, 450, 350)
        self.setWindowTitle('Event sender')

    def buttonClicked(self):
        sender = self.sender()
        self.statusBar().showMessage(sender.text() + ' was pressed')


if __name__ == "__main__":
    run(Window)

这个例子里有两个按钮,buttonClicked() 方法决定了是哪个按钮能调用 sender() 方法。我们用调用 sender() 方法的方式决定了事件源。状态栏显示了被点击的按钮。

程序展示:

如果在信号激活的插槽中调用,则PySide2.QtCore.QObject.sender()返回指向发送信号的对象的指针;否则返回None。指针仅在执行从该对象的线程上下文调用此函数的插槽期间有效。

如果发送方被销毁或插槽与发送方的信号断开连接,则此函数返回的指针将变为无效。

警告:此功能违反了模块化的面向对象原则。但是,当多个信号连接到单个插槽时,访问发送者可能会很有用。

警告:如上所述,当通过DirectConnection从不同于该对象线程的线程中调用插槽时,此函数的返回值无效。不要在这种情况下使用此功能。

6 发送自定义信号

QObject 实例能发送事件信号。下面的例子是发送自定义的信号。

from xinet import QtWidgets, QtCore, QtGui, Signal
from xinet.run_qt import run


class Communicate(QtCore.QObject):
    closeApp = Signal()


class Window(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.init_Ui()

    def init_Ui(self):
        self.c = Communicate()
        self.c.closeApp.connect(self.close)
        self.setGeometry(300, 300, 450, 350)
        self.setWindowTitle('Emit signal')

    def mousePressEvent(self, event):
        self.c.closeApp.emit()


if __name__ == "__main__":
    run(Window)

创建了一个叫 closeApp 的信号,这个信号会在鼠标按下的时候触发,事件与 QMainWindow 的 槽函数 close 绑定 。

点击窗口的时候,发送 closeApp 信号,程序终止。

7 经典案例

from xinet import QtWidgets, QtCore, QtGui, Signal
from xinet.run_qt import run

QPainter = QtGui.QPainter
QMenu = QtWidgets.QMenu
QEvent, QTimer, Qt = QtCore.QEvent, QtCore.QTimer, QtCore.Qt


class Window(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.init_Ui()
        # 避免窗口大小重绘事件的影响,可以把参数0改变成3000(3秒),然后在运行,就可以明白这行代码的意思。
        QTimer.singleShot(0, self.give_help)
        self.justDoubleClicked = False
        self.key = ""
        self.text = ""
        self.message = ""

    def init_Ui(self):
        self.resize(400, 300)
        self.move(100, 100)
        self.setWindowTitle("Events")

    def give_help(self):
        self.text = "请点击这里触发追踪鼠标功能"
        self.update()  # 重绘事件,也就是触发paintEvent函数。

    def closeEvent(self, event):
        '''重新实现关闭事件'''
        print("Closed")

    def one(self):
        '''上下文菜单槽函数'''
        self.message = "Menu option One"
        self.update()

    def two(self):
        self.message = "Menu option Two"
        self.update()

    def three(self):
        self.message = "Menu option Three"
        self.update()

    def contextMenuEvent(self, event):
        '''重新实现上下文菜单事件'''
        menu = QMenu(self)
        oneAction = menu.addAction("&One")
        twoAction = menu.addAction("&Two")
        oneAction.triggered.connect(self.one)
        twoAction.triggered.connect(self.two)
        if not self.message:
            menu.addSeparator()
            threeAction = menu.addAction("Thre&e")
            threeAction.triggered.connect(self.three)
        menu.exec_(event.globalPos())

    def clearMessage(self):
        '''清空消息文本的槽函数'''
        self.message = ""

    def paintEvent(self, event):
        '''重新实现绘制事件'''
        text = self.text
        i = text.find("\n\n")
        if i >= 0:
            text = text[0:i]
        if self.key:  # 若触发了键盘按钮,则在文本信息中记录这个按钮信息。
            text += "\n\n你按下了: {0}".format(self.key)
        painter = QPainter(self)
        painter.setRenderHint(QPainter.TextAntialiasing)
        painter.drawText(self.rect(), Qt.AlignCenter, text)  # 绘制信息文本的内容
        if self.message:  # 若消息文本存在则在底部居中绘制消息,5秒钟后清空消息文本并重绘。
            painter.drawText(self.rect(), Qt.AlignBottom | Qt.AlignHCenter,
                             self.message)
            QTimer.singleShot(5000, self.clearMessage)
            QTimer.singleShot(5000, self.update)

    def resizeEvent(self, event):
        '''重新实现调整窗口大小事件'''
        self.text = "调整窗口大小为: QSize({0}, {1})".format(
            event.size().width(), event.size().height())
        self.update()

    def mouseReleaseEvent(self, event):
        '''重新实现鼠标释放事件'''
        # 若鼠标释放为双击释放,则不跟踪鼠标移动
        # 若鼠标释放为单击释放,则需要改变跟踪功能的状态,如果开启跟踪功能的话就跟踪,不开启跟踪功能就不跟踪
        if self.justDoubleClicked:
            self.justDoubleClicked = False
        else:
            self.setMouseTracking(not self.hasMouseTracking())  # 单击鼠标
            if self.hasMouseTracking():
                self.text = "开启鼠标跟踪功能.\n" + \
                            "请移动一下鼠标!\n" + \
                            "单击鼠标可以关闭这个功能"
            else:
                self.text = "关闭鼠标跟踪功能.\n" + \
                            "单击鼠标可以开启这个功能"
            self.update()

    def mouseMoveEvent(self, event):
        '''重新实现鼠标移动事件'''
        if not self.justDoubleClicked:
            globalPos = self.mapToGlobal(event.pos())  # 窗口坐标转换为屏幕坐标
            self.text = """鼠标位置:
            窗口坐标为:QPoint({0}, {1}) 
            屏幕坐标为:QPoint({2}, {3}) """.format(event.pos().x(), event.pos().y(), globalPos.x(), globalPos.y())
            self.update()

    def mouseDoubleClickEvent(self, event):
        '''重新实现鼠标双击事件'''
        self.justDoubleClicked = True
        self.text = "你双击了鼠标"
        self.update()

    def keyPressEvent(self, event):
        '''重新实现键盘按下事件'''
        self.key = ""
        if event.key() == Qt.Key_Home:
            self.key = "Home"
        elif event.key() == Qt.Key_End:
            self.key = "End"
        elif event.key() == Qt.Key_PageUp:
            if event.modifiers() & Qt.ControlModifier:
                self.key = "Ctrl+PageUp"
            else:
                self.key = "PageUp"
        elif event.key() == Qt.Key_PageDown:
            if event.modifiers() & Qt.ControlModifier:
                self.key = "Ctrl+PageDown"
            else:
                self.key = "PageDown"
        elif Qt.Key_A <= event.key() <= Qt.Key_Z:
            if event.modifiers() & Qt.ShiftModifier:
                self.key = "Shift+"
            self.key += event.text()
        if self.key:
            self.key = self.key
            self.update()
        else:
            super().keyPressEvent(event)

    def event(self, event):
        '''重新实现其他事件,适用于PyQt没有提供该事件的处理函数的情况,
           Tab键由于涉及焦点切换,不会传递给keyPressEvent,因此,需要在这里重新定义。
        '''
        if (event.type() == QEvent.KeyPress and
                event.key() == Qt.Key_Tab):
            self.key = "在event()中捕获Tab键"
            self.update()
            return True
        return super().event(event)


if __name__ == "__main__":
    run(Window)

update() 函数的作用是更新窗口。由于在窗口更新过程中会触发一次 paintEvent 函数(paintEvent 是窗口基类 QWidget 的内部函数),因此在本例中 update 函数的作用等同于 paintEvent 函数。

对于上下文菜单事件,主要影响 message 变量的结果,paintEvent 负责把这个变量在窗口底部输出。

绘制事件是代码的核心事件,它的主要作用是时刻跟踪 textmessage 这两个变量的信息,并把 text 的内容绘制到窗口的中部,把 message 的内容绘制到窗口的底部(保持 5 秒后就会被清空)。

8 安装事件过滤器

from xinet import QtWidgets, QtCore, QtGui, Signal
from xinet.run_qt import run

QPainter = QtGui.QPainter
QMenu = QtWidgets.QMenu
QLabel = QtWidgets.QLabel
QImage = QtGui.QImage
QEvent, QTimer, Qt = QtCore.QEvent, QtCore.QTimer, QtCore.Qt


class EventFilter(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("事件过滤器")

        self.label1 = QLabel("请点击")
        self.label2 = QLabel("请点击")
        self.label3 = QLabel("请点击")
        self.LabelState = QLabel("test")

        self.image1 = QImage("images/cartoon1.ico")
        self.image2 = QImage("images/cartoon1.ico")
        self.image3 = QImage("images/cartoon1.ico")

        self.width = 600
        self.height = 300

        self.resize(self.width, self.height)

        self.label1.installEventFilter(self)
        self.label2.installEventFilter(self)
        self.label3.installEventFilter(self)

        mainLayout = QtWidgets.QGridLayout(self)
        mainLayout.addWidget(self.label1, 500, 0)
        mainLayout.addWidget(self.label2, 500, 1)
        mainLayout.addWidget(self.label3, 500, 2)
        mainLayout.addWidget(self.LabelState, 600, 1)
        self.setLayout(mainLayout)

    def eventFilter(self, watched, event):
        if watched == self.label1:  # 只对label1的点击事件进行过滤,重写其行为,其他的事件会被忽略
            if event.type() == QEvent.MouseButtonPress:  # 这里对鼠标按下事件进行过滤,重写其行为
                mouseEvent = QtGui.QMouseEvent(event)
                if mouseEvent.buttons() == Qt.LeftButton:
                    self.LabelState.setText("按下鼠标左键")
                elif mouseEvent.buttons() == Qt.MidButton:
                    self.LabelState.setText("按下鼠标中间键")
                elif mouseEvent.buttons() == Qt.RightButton:
                    self.LabelState.setText("按下鼠标右键")

                '''转换图片大小'''
                transform = QtCore.QTransform()
                transform.scale(0.5, 0.5)
                tmp = self.image1.transformed(transform)
                self.label1.setPixmap(QPixmap.fromImage(tmp))
            if event.type() == QEvent.MouseButtonRelease:  # 这里对鼠标释放事件进行过滤,重写其行为
                self.LabelState.setText("释放鼠标按钮")
                self.label1.setPixmap(QtGui.QPixmap.fromImage(self.image1))
        return super().eventFilter(watched, event)  # 其他情况会返回系统默认的事件处理方法。

if __name__ == "__main__":
    run(EventFilter)

效果:

对于使用事件过滤器,关键是要做好两步。
对要过滤的控件设置 installEventFilter,这些控件的所有事件都会被 eventFilter 函数接收并处理。

installEventFilter的使用方法如下:

self.label1.installEventFilter(self) 
self.label2.installEventFilter(self) 
self.label3.installEventFilter(self)

在 QApplication 中安装事件过滤器)的使用也非常简单,与第三种事件处理方法相比,只需要简单地修改两处代码即可。

屏蔽三个 label 标签控件的 installEventFilter 代码:

# self.label1.installEventFilter(self) 
# self.label2.installEventFilter(self) 
# self.label3.installEventFilter(self) 

对于在 QApplication 中安装 installEventFilter,下面代码的意思是 dialog 的所有事件都要经过 eventFilter 函数处理,而不仅仅是三个标签控件的事件。

if __name__=='__main__': 
    app=QApplication(sys.argv) 
    dialog=EventFilter() 
    app.installEventFilter(dialog) 
    dialog.show() 
    app.exec_() 

完整代码:

# -*- coding: utf-8 -*-
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys


class EventFilter(QDialog):
    def __init__(self, parent=None):
        super(EventFilter, self).__init__(parent)
        self.setWindowTitle("事件过滤器")

        self.label1 = QLabel("请点击")
        self.label2 = QLabel("请点击")
        self.label3 = QLabel("请点击")
        self.LabelState = QLabel("test")

        self.image1 = QImage("images/cartoon1.ico")
        self.image2 = QImage("images/cartoon1.ico")
        self.image3 = QImage("images/cartoon1.ico")

        self.width = 600
        self.height = 300

        self.resize(self.width, self.height)

        # self.label1.installEventFilter(self)
        # self.label2.installEventFilter(self)
        # self.label3.installEventFilter(self)

        mainLayout = QGridLayout(self)
        mainLayout.addWidget(self.label1, 500, 0)
        mainLayout.addWidget(self.label2, 500, 1)
        mainLayout.addWidget(self.label3, 500, 2)
        mainLayout.addWidget(self.LabelState, 600, 1)
        self.setLayout(mainLayout)

    def eventFilter(self, watched, event):
        print(type(watched))
        if watched == self.label1:  # 只对label1的点击事件进行过滤,重写其行为,其他的事件会被忽略
            if event.type() == QEvent.MouseButtonPress:  # 这里对鼠标按下事件进行过滤,重写其行为
                mouseEvent = QMouseEvent(event)
                if mouseEvent.buttons() == Qt.LeftButton:
                    self.LabelState.setText("按下鼠标左键")
                elif mouseEvent.buttons() == Qt.MidButton:
                    self.LabelState.setText("按下鼠标中间键")
                elif mouseEvent.buttons() == Qt.RightButton:
                    self.LabelState.setText("按下鼠标右键")

                '''转换图片大小'''
                transform = QTransform()
                transform.scale(0.5, 0.5)
                tmp = self.image1.transformed(transform)
                self.label1.setPixmap(QPixmap.fromImage(tmp))
            if event.type() == QEvent.MouseButtonRelease:  # 这里对鼠标释放事件进行过滤,重写其行为
                self.LabelState.setText("释放鼠标按钮")
                self.label1.setPixmap(QPixmap.fromImage(self.image1))
        return QDialog.eventFilter(self, watched, event)  # 其他情况会返回系统默认的事件处理方法。


if __name__ == '__main__':
    app = QApplication(sys.argv)
    dialog = EventFilter()
    app.installEventFilter(dialog)
    dialog.show()
    sys.exit(app.exec_())

可见。第四种事件处理方法确实过滤了所有事件,而不像第三种方法那样只过滤三个标签控件的事件。

你可能感兴趣的:(Qt 事件管理)