Python界面(GUI)编程PyQt5事件和信号

@高效码农  August 15, 2020

触发响应用户行为和GUI事件的操作

使用信号和槽构建复杂的应用程序行为,并使用自定义事件覆盖小部件事件处理。

如前所述,用户与Qt应用程序的每次交互都会生成一个事件。事件有多种类型,每一种都代表一种不同类型的交互—例如鼠标或键盘事件。

发生的事件被传递到发生交互的小部件上特定于事件的处理程序。例如,单击小部件事件将QMouseEvent发送到小部件上的. mousepressevent处理程序。此处理程序可以查看事件所有信息,比如触发事件的原因以及事件发生的具体位置。

信号/插槽机制具有以下功能。

  • 信号可以连接到许多插槽。
  • 一个信号也可以连接到另一个信号。
  • 信号参数可以是任何Python类型。
  • 插槽可以连接到许多信号。
  • 连接可以是直接的(即同步)或排队的(即异步)。
  • 可以跨线程进行连接。
  • 信号可能断开。

信号

信号(特别是未绑定的信号)是类属性。当信号被引用为该类实例的属性时,PyQt5会自动将该实例绑定到该信号,以创建绑定信号。这与Python本身用于从类函数创建绑定方法的机制相同。

一个绑定信号具有connect(),disconnect()和emit()实现相关联的功能的方法。它还具有一个signal属性,该属性是Qt SIGNAL() 宏将返回的信号的签名。

信号可能重载,即具有特定名称的信号可能支持多个签名。可以用签名对信号进行索引,以选择所需的信号。签名是一系列类型。类型可以是Python类型对象,也可以是C ++类型名称的字符串。C ++类型的名称会自动进行规范化,例如,QVariant可以使用它来代替non-normalized 。

如果信号重载,那么它将具有默认值。

发出信号时,如有可能,所有参数都将转换为C ++类型。如果参数没有对应的C ++类型,则将其包装在特殊的C ++类型中,该参数允许在Qt的元类型系统中传递该参数,同时确保正确维护其引用计数。

下面的代码给出了使用windowTitleChanged信号的一些示例。

import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import Qt

# Subclass QMainWindow to customise your application's main window
class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        super(MainWindow, self).__init__(*args, **kwargs)
        
        # 信号:只要窗口打开,连接函数就会被调用
        # 标题改变了。新的标题将被传递给函数。
        self.windowTitleChanged.connect(self.onWindowTitleChange)

        # 信号:只要窗口打开,连接函数就会被调用
        # 标题改变了. 在lambda中丢弃新标题,调用函数时不带参数。
        self.windowTitleChanged.connect(lambda x: self.my_custom_fn())

        # 信号:只要窗口打开,连接函数就会被调用
        # 新标题被传递给函数并替换默认参数
        self.windowTitleChanged.connect(lambda x: self.my_custom_fn(x))

        # 信号:只要窗口打开,连接函数就会被调用
        # 新标题被传递给函数并替换默认参数。其他参数是从lambda内部传递的。
        self.windowTitleChanged.connect(lambda x: self.my_custom_fn(x, 25))
        
        # 信号:只要窗口打开,连接函数就会被调用
        # 将新标题作为第一个参数发送给函数或lambdas。
        self.setWindowTitle("高效码农")
        
        label = QLabel("高效码农")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

    # 插槽:接收一个字符串,例如窗口标题,并打印它
    def onWindowTitleChange(self, s):
        print(s)

    # 插槽:有默认参数,可以不带值调用
    def my_custom_fn(self, a="HELLLO!", b=5):
        print(a, b)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec_()

2020-08-15T01:57:45.png

控制台打印:

高效码农
HELLLO! 5
高效码农 5
高效码农 25

可以尝试注释掉不同的信号,看看对插槽打印内容的影响。

事件

接下来,让我们快速看一下事件。由于有了信号,在大多数情况下,您都可以愉快地避免在Qt中使用事件,但是理解它们在必要时如何工作是很重要的。

作为示例,我们将拦截QMainWindow上的. contextmenuevent。每当要显示上下文菜单时,就会触发此事件,并传递一个类型为QContextMenuEvent的单值事件。

要拦截事件,只需使用同名的新方法覆盖对象方法。在本例中,我们可以在MainWindow子类上创建一个名为contextMenuEvent的方法,它将接收此类型的所有事件。

def contextMenuEvent(self, event):
        print("高效码农")

如果将上述方法添加到MainWindow类中并运行程序,您将发现右键单击窗口现在会在打印语句中显示该消息。

有时您可能希望拦截事件,但仍然触发默认的(父)事件处理程序。可以像Python类方法一样使用super调用父类上的事件处理程序。

def contextMenuEvent(self, event):
        print("高效码农")
        super(MainWindow, self).contextMenuEvent(event)

这允许您将事件传播到对象层次结构中,只处理您希望处理的事件处理程序的那些部分。

然而,在Qt中还有另一种类型的事件层次结构,它是围绕UI关系构建的。添加到布局中的小部件在另一个小部件中可以选择将它们的事件传递给它们的UI父组件。在具有多个子元素的复杂小部件中,可以为特定事件将事件处理委托给包含小部件。

但是,如果您已经处理了事件,并且不希望它以这种方式传播,则可以通过在事件上调用.accept()来标记它。

class CustomButton(QPushButton):

    def event(self, e):
        e.accept()

或者,如果您确实希望它传播调用,可以通过.ignore()实现。

class CustomButton(QPushButton):
    def event(self, e):
        e.ignore()

在本节中,我们讨论了信号、槽和事件。我们已经演示了一些简单的信号,包括如何使用lambdas传递数据。我们已经创建了自定义信号,并展示了如何拦截事件、传递事件处理以及如何使用.accept()和.ignore()将事件隐藏/显示给ui父部件。

在下一节中,我们将继续研究GUI的两个常见特性——工具栏和菜单。



评论已关闭