1.6 信号与槽

对于可视化编程,需要将界面上的控件有机结合起来,实现控件功能的联动和交互操作。在上节中,建立了一个输入学生成绩,计算总成绩和平均分,并对学生成绩进行统计,把结果保存到文件中的简单界面程序。在这个程序中,通过单击“计算”按钮和“保存”按钮,实现上述功能。对按钮功能的定义,是通过信号(signal)与槽(slot)机制实现的。信号与槽是PySide6编程的基础,也是Qt的一大创新,有了信号与槽的编程机制,在PySide6中处理界面上各个控件的交互操作时变得更加直观和简单。

信号是指从QObject类继承的控件(窗口、按钮、文本框、列表框等)在某个动作下或状态发生改变时发出的一个指令或一个信息,例如一个按钮被单击(clicked)、右击一个窗口(customContextMenuRequested)、一个输入框中文字的改变(textChanged)等,当这些控件的状态发生变化或者外界对控件进行输入时,让这些控件发出一个信息,来通知系统其某种状态发生了变化或者得到了外界的输入,以便让系统对外界的输入进行响应。槽是系统对控件发出的信号进行的响应,或者产生的动作,通常用函数来定义系统的响应或动作。例如对于单击“计算”按钮,按钮发出被单击的信号,然后编写对应的函数,当控件发出信号时,就会自动执行与信号关联的函数。信号与槽的关系可以是一对一,也可以是多对多,即一个信号可以关联多个槽函数,一个槽函数也可以接收多个信号。PySide6已经为控件编写了一些信号和槽函数,使用前需要将信号和槽函数进行连接,另外用户还可以自定义信号和自定义槽函数。

1.6.1 内置信号与内置槽的连接

PySide6对控件已经定义的信号和槽可以在Qt Designer中查看。启动Qt Designer并打开前面的student.ui文件,在窗口上拖放一个新的Push Button按钮,如图1-18所示,并将objectName改成btnClose,将text设置成“关闭”。然后单击工具栏上的“编辑信号/槽”按钮,进入信号和槽的编辑界面,按住Shift键的同时,用鼠标左键拖拽“关闭”按钮到窗口的空白区,这时会出现一个红色线和接地符号,松开鼠标,弹出“配置连接”对话框,如图1-19所示。勾选“显示从QWidget继承的信号和槽”,这时对话框的左边列表框中显示按钮的所有已定义信号,右边列表框中显示窗口所有的槽函数。这里左边选择按钮的clicked()信号,右边选择窗口的close()函数,单击OK按钮,就建立了按钮的单击信号(clicked)和窗口的关闭(close)的连接。

图1-18 按钮信号与窗口槽函数的关联

图1-19 “配置连接”对话框

另外一种建立信号和槽的方法是使用“信号/槽编辑器”。在Qt Designer的右下角的“信号/槽编辑器”上单击按钮,如图1-20所示,双击发送者下的<发送者>,找到btnClose按钮,双击信号下的<信号>,找到clicked(),双击接收者下的<接收者>,找到Form,双击槽下的<槽>,找到close(),这样就建立了信号和槽的连接。如果要删除信号和槽的连接,应先选中信号槽,然后单击按钮。

图1-20 信号和槽编辑器

将以上窗口存盘,重新将ui文件编译成py文件,并用新的py文件替换旧的py文件,运行程序后,得到新的界面。打开新生成的py文件,可以发现在py中增加了一行新代码self.btnClose.clicked.connect(Form.close),用控件信号的connect()方法将信号和函数进行了连接,注意被连接的槽函数不需要带括号。

从上面的例子中可以看出,信号与槽的连接格式如下,其中sender是产生信号的控件名称;signalName是信号名称;receiver是接收信号的控件名称;slotName是接收信号的控件的槽函数名称,不需要带括号。

      sender.signalName.connect(receiver.slotName)

1.6.2 内置信号与自定义槽函数

除了可以将控件的内置信号与其他控件的内置槽函数进行连接外,还可以对控件的内置信号直接定义新的槽函数。上节中就是对“计算”按钮和“保存”按钮信号进行按钮单击信号与自定义槽函数连接,从而实现这两个按钮的功能。

1.自动关联内置信号的自定义槽函数

将ui文件编译成py文件后,打开py文件,可以发现在py文件中会出现下面的语句:

      QMetaObject.connectSlotsByName(Form)

该语句的作用是使用PySide6的元对象(QMetaObject)在窗口上搜索所有从QObject类继承的控件,将控件的信号自动与槽函数根据名称(objectName)进行匹配,这时自定义的槽函数必须具有如下格式,即可实现信号与槽函数的自动关联。

      @Slot()
      def on_objectName_signalName(self,signalParameter):
          函数语句

其中,@Slot()修饰符用于指定随后的函数是槽函数;def为函数定义关键词;on为函数名的前缀,是必需的;objectName是控件的objectName属性名称,例如前面学生成绩统计例子中定义“保存”按钮时给按钮起的名字是btnSave;signalName是控件的信号名称,如按钮的clicked信号;signalParameter是信号传递过来的参数,例如一个checkBox是否处于勾选状态,对checkBox可以定义def on_checkBox_toggled(self,checked)自动关联槽函数,自动接收来自checkBox的toggled(bool)的信号,其中checked是形参,表示toggled(bool)信号传递的状态。

2.重载型信号的处理

细心的读者会注意到,在Qt Designer中查询一个控件的信号时,会发现有些控件有多个名字相同但是参数不同的信号。例如对于按钮有clicked()和clicked(bool)两种信号,一种不需要传递参数的信号,另一种传递布尔型参数的信号。这种信号名称相同、参数不同的信号称为重载(overload)型信号。对于重载型信号定义自动关联槽函数时,需要在槽函数前加修饰符@Slot(type)声明是对哪个信号定义槽函数,其中type是信号传递的参数类型。例如如果对按钮的clicked(bool)信号定义自动关联槽函数,需要在槽函数前加入@Slot(bool)进行修饰;如果对按钮的clicked()信号定义自动关联槽函数,需要在槽函数前加入@Slot()进行修饰。需要注意的是,在使用@Slot(type)修饰符前,应提前用“from PySide6.QtCore import Slot”语句导入槽函数。

3.手动关联内置信号的自定义槽函数

除了使用控件内置信号定义自动连接的槽函数外,还可以将控件内置信号手动连接到其他函数上,这时需要用到信号的connect()方法。例如前面的输入学生成绩,计算总成绩和平均分的例子中,将“计算”按钮的click()信号关联的函数修改成“def scoreCalculate(self):”,然后在窗口初始化函数“__init__()”中用“self.ui.btnCalculate.clicked.connect(self.scoreCalculate)”语句将按钮的单击信号clicked与scoreCalculate()函数进行连接,也可以在主程序中,在消息循环语句前用“myWindow.ui.btnCalculate.clicked.connect(myWindow.scoreCalculate)”语句进行消息与槽函数的连接,程序代码如下所示。

1.6.3 自定义信号

除了可以用控件的内置信号外,还可以自定义信号。自定义信号可以不带参数,也可以带参数,可以带1个参数,也可以带多个参数。参数类型是任意的,如整数(int)、浮点数(float)、布尔(bool)、字符串(str)、列表(list)、元组(tuple)和字典(dict)等。参数类型需要在定义信号时进行声明。自定义信号通常需要在类属性位置用Signal类来创建,使用Signal前需要用“from PySide6.QtCore import Signal”语句导入Signal类。需要注意的是,只有继承自QObject的类才可以定义信号。

1.自定义信号的定义方式

定义非重载型信号的格式如下所示:

      signalName=Signal(type1,type2,…)

定义重载型信号的格式如下所示:

      signalName=Signal([type1],[type2],…)

其中,signalName为信号名称;Signal()用于创建信号实例对象,type为信号发送时附带的数据类型,这里数据类型不是形参也不是实参,只是类型的声明,参数类型任意,需根据实际情况确定,[]表示重载信号,如果重载信号不带参数,则只使用[],不用带type。

定义一个信号后,信号就有连接connect()、发送emit()和断开disconnect()属性,对于重载型信号,在进行连接、发送和断开时,需要用signalName[type]形式进行连接、发送和断开操作。第1个信号可以不用signalName[type]形式,而直接用signalName形式。需要注意的是,只有从QObject继承的类才可以定义信号。下面是创建不同信号的代码。

2.自定义信号的使用

下面通过一个具体的实例说明自定义信号的使用方法。仍以前面输入学生成绩的界面为例,在窗口上增加姓名和学号输入框,学生姓名可以相同,但是学号是唯一的,当输入完学号时,发送带有学号的信号,判断学号是否已经录入系统。如果是则弹出确认对话框,单击“保存”按钮后,把结果保存到Excel文档中。

第1步,用Qt Designer打开前面建立的student.ui窗口,从Containers控件中拖放一个Group Box到窗口中,并排列位置,将Group Box的title属性修改成“学生基本信息”,如图1-21所示。

图1-21 学生成绩输入对话框

第2步,从Display Widgets控件中拖放两个Label标签到Group Box中,将这两个标签的text属性分别改成“姓名”和“学号”。

第3步,从Input Widgets控件中拖放一个Line Edit和一个Spin Box到Goup Box中,分别放到“姓名”标签和“学号”标签的后面,将Line Edit的objectName属性修改成name,将Spin Box的objectName属性修改成number,并将Spin Box的maximum属性修改成10000。

第4步,将窗口另存为student_new.ui,然后再编译成py文件。

第5步,对窗口控件进行逻辑编程,下面是完成窗口控件功能的代码。在代码中使用了字典记录学生姓名、学号、各科成绩,并以学号为关键字,字典值是列表,列表的元素是姓名、学号、各科成绩。代码中定义了信号numberSignal,发送时传递学号编号,如果学号编号已经在字典中,则会弹出确认对话框,这里使用消息对话框QMessageBox,我们在后面还会详细介绍QMessageBox的使用方法。在输入学号的输入框中按Enter键,或者将光标移入到其他输入框中,会触发控件的editingFinished信号,程序中使用自动连接槽函数def on_number_editingFinished()来触发自定义信号的发送。