2.3 信号与槽的使用
2.3.1 信号与槽功能概述
信号与槽(Signals/Slots)是Qt编程的基础,也是Qt的一大特色。因为有了信号与槽的编程机制,在Qt中处理界面组件的交互操作时变得比较直观和简单。
信号(Signal)就是在特定情况下被发射(emit)的一种通告,例如一个PushButton按钮最常见的信号就是鼠标单击时发射的clicked()信号,一个ComboBox最常见的信号是选择的项变化时发射的CurrentIndexChanged()信号。GUI程序设计的主要内容就是对界面上各组件发射的特定信号进行响应,只需要知道什么情况下发射了哪些信号,然后合理地去响应和处理这些信号就可以了。
槽(Slot)就是对信号响应的函数。槽实质上是一个函数,它可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数会被自动执行。Qt的类一般都有一些内建(build-in)的槽函数,例如QWidget有一个槽函数close(),其功能是关闭窗口。如果将一个PushButton按钮的clicked()信号与窗体的close()槽函数关联,那么点击按钮时就会关闭窗口。
本节通过一个完整的示例Demo2_3介绍信号与槽的使用方法,示例的主程序appMain.py的运行结果如图2-10所示。界面上的所有功能都是可以操作的。
图2-10 示例Demo2_3的主程序appMain.py运行结果窗体
· 上方的3个复选框可以控制文本框内的字体的下划线、斜体、粗体特性。
· 3个RadioButton按钮可以控制文本框内的文字颜色。
· “清空”按钮可以清空文本框内的文字。
· “确定”和“退出”按钮都可以关闭窗口,但是表示对话框的不同选择结果。
这个示例的设计包含了PyQt5 GUI应用程序设计的完整过程,以及涉及的一些关键技术问题,本节将逐步展开讲解这些问题。
(1)在UI Designer里设计窗体的布局,使界面上的各个组件合理地分布,并且随窗体大小变化而自动调整大小和位置。
(2)在UI Designer里设计窗体时,设置组件的某个内建信号与窗体上其他组件的内建槽函数关联。
(3)在与UI窗体对应的业务逻辑类里,设计窗体组件内建信号的响应槽函数,并且与组件的信号关联起来。
(4)信号与槽设计和关联时的各种情况的处理方法。
详细地研究和实现这个示例后,基本上就掌握了用PyQt5编写GUI程序的完整流程。
2.3.2 Qt Creator的使用
Qt Creator是Qt的IDE,它可以管理、编译和调试Qt的C++项目。本节将用Qt Creator创建一个C++应用程序项目,主要是为了用Qt Creator内置的UI Designer可视化设计窗体,方便提取组件的信号并创建信号的槽函数原型,不需要编写C++程序,也无须对项目进行编译。
启动Qt Creator,创建一个名为QtApp的C++ GUI应用程序项目,步骤如下。
(1)点击Qt Creator的菜单项“File”→“New File or Project…”,出现如图2-11所示的对话框。在此对话框里选择Project类型为Application,中间的模板里选择Qt Widgets Application,这是常见的GUI应用程序项目。
图2-11 Qt Creator的新建项目对话框,选择新建Qt Widgets Application项目
(2)在图2-11的对话框中点击“Choose…”按钮,出现如图2-12所示的新建项目向导。在此对话框中,设置项目名称为QtApp,点击“Browse…”按钮选择示例Demo2_3所在的文件夹,例如“G:\PyQt5Book\Demo\chap02\demo2_3”,这样创建的项目将自动保存在“G:\PyQt5Book\Demo\chap02\demo2_3\QtApp”文件夹下。
图2-12 创建项目的名称设置为QtApp
(3)继续下一步,在出现选择编译工具的页面选择一个Desktop Qt 5.12.0 MinGW 64-bit即可,因为不需要编译项目,选择哪一个都可以。
(4)继续下一步,出现如图2-13所示的创建UI窗体的界面,这个窗体将作为应用程序的主窗体。本示例的目的是创建一个对话框,所以选择基类QDialog,新窗体的类名称就使用默认的Dialog,这将自动创建3个文件,即Dialog.h、Dialog.cpp和Dialog.ui。注意一定要勾选“Generate form”旁边的复选框,否则不会创建文件Dialog.ui,也就不能进行窗体的可视化设计了。
图2-13 创建UI窗体的设置,选择基类QDialog
完成向导创建项目QtApp后,Qt Creator的界面如图2-14所示。Qt Creator的界面非常简洁,本书不需要用它来编写C++程序,而只是用于窗体可视化设计和槽函数原型生成,所以用到的功能较少。
图2-14 Qt Creator项目管理与代码文件编辑器
窗体左侧是项目的文件管理目录树,与窗体相关的文件有以下3个。
· Dialog.ui是窗体UI文件,双击这个文件,会打开内置的UI Designer进行窗体可视化设计。
· Dialog.h和Dialog.cpp是定义窗体业务逻辑类的头文件和程序实现文件。
主窗体左侧的工具栏上是一些功能按钮,“Edit”按钮用于切换到如图2-14所示的项目文件管理界面,“Design”按钮用于在有ui文件被打开时,切换到UI Designer设计界面,“Help”按钮用于切换到内置的Qt Assistant界面查看Qt的帮助文档。还有其他一些用于项目编译、调试和运行的按钮,本书未用到。
该Qt项目的所有文件存放在Demo2_3目录的子目录“\QtApp”下,其中QtApp.pro是Qt项目文件。在Qt Creator里再次打开QtApp.pro文件时,就可以打开这个项目。
2.3.3 窗体可视化设计
在图2-14的项目文件管理目录树上,双击文件Dialog.ui可以打开内置的UI Designer对窗体进行可视化设计,界面如图2-15所示。图中显示的是本示例已经设计好的窗体,在界面设计中使用了布局管理功能。窗体中间的文本框是一个PlainTextEdit组件,在组件面板的Input Widgets分组里。
图2-15 在Qt Creator内置的UI Designer里进行窗体可视化设计
在界面可视化设计时,对于需要在窗体业务逻辑类里访问的界面组件,修改其objectName,例如各个按钮、需要读取输入的编辑框、需要显示结果的标签等,以便在程序里加以区分。对于不需要在程序里访问的界面组件则无须修改其objectName,例如用于界面上组件分组的GroupBox、Frame、布局等,UI Designer自动命名即可。
对图2-15所设计窗体的主要组件的命名、属性设置如表2-2所示。
表2-2 Dialog.ui中各个组件的相关设置
对于界面组件的属性设置,需要注意以下两点。
(1)objectName是窗体上的组件的实例名称,界面上的每个组件需要有一个唯一的objectName,程序里访问界面组件时都通过其objectName进行访问,自动生成的槽函数名称与objectName有关。所以,组件的objectName需要在设计程序之前设置好,设置好之后一般不再改动。若程序设计好之后再改动objectName,涉及的代码需要进行相应的改动。
(2)窗体的objectName是窗体的类名称,也就是利用向导新建窗体时设置的名称,在UI Designer里一般不修改窗体的objectName。
2.3.4 界面组件布局管理
Qt的窗体设计具有布局(layout)功能。所谓布局,就是界面上的组件的排列方式。使用布局管理功能可以使组件有规则地分布,并且随着窗体大小变化自动地调整大小和相对位置。布局管理是GUI设计的必备技巧,下面逐步讲解如何实现图2-15的窗体。
1.界面组件的层次关系
为了将界面上的各个组件的分布设计得更加美观,经常使用一些容器类组件,如GroupBox、TabWidget、Frame等。例如,将3个CheckBox(复选框)组件放置在一个GroupBox组件里,这个GroupBox组件就是这3个复选框的容器,移动这个GroupBox就会同时移动其中的3个CheckBox。
图2-16是设计图2-15所示的窗体的前期阶段。在窗体上放置了两个GroupBox组件,其中在groupBox1里放置3个CheckBox组件,在groupBox2里放置3个RadioButton按钮。图2-16右侧Object Inspector里显示了界面上各组件之间的层次关系。
图2-16 窗体上组件的放置与层次关系
2.布局管理
Qt为窗体设计提供了丰富的布局管理功能,在UI Designer里,组件面板里有Layouts和Spacers两个分组,在窗体上方的工具栏里有布局管理的按钮(如图2-17所示)。
图2-17 用于布局可视化设计的组件面板和工具栏
组件面板里Layouts和Spacers这两个分组里的布局组件的功能如表2-3所示。
表2-3 组件面板上用于布局的组件
使用组件面板里的布局组件设计布局时,先拖放一个布局组件到窗体上,例如在设计图2-17窗体下方的3个按钮的布局时,先放一个Horizontal Layout到窗体上,布局组件会以红色矩形框显示。再向布局组件里拖放3个PushButton和两个Horizontal Spacer,就可以得到图2-17中3个按钮的水平布局效果。
每个布局还有layoutTopMargin、layoutBottomMargin、layoutLeftMargin、layoutRightMargin这4个属性用于调整布局边框与内部组件之间的上、下、左、右的边距大小。
在设计窗体的上方有一个工具栏,用于使界面进入不同的设计状态,以及进行布局设计,工具栏上各按钮的功能如表2-4所示。
表2-4 内置的UI Designer工具栏各按钮的功能
使用工具栏上的布局设计按钮时,只需在窗体上选中需要设计布局的组件,然后点击某个布局按钮即可。在窗体上选择组件时同时按住Ctrl键,可以实现组件多选。选择某个容器类组件,相当于选择了其内部的所有组件。例如,在图2-16的窗体中,选中groupBox1,然后单击“Lay Out Horizontally”工具栏按钮,就可以对groupBox1内的3个复选框水平布局。
在图2-16的窗体上,使groupBox1里的3个复选框水平布局,groupBox2里的3个RadioButton按钮水平布局,下方放置的3个按钮水平布局,窗体上又放置一个PlainTextEdit组件。现在如果改变groupBox1、groupBox2或按钮的水平布局的大小,其内部组件会自动改变大小,但是如果改变窗体大小,界面上的各组件却并不会自动改变大小。
还需为窗体指定一个总的布局。选中窗体(即不选择任何组件),单击工具栏上的“Lay Out Vertically”按钮,使4个组件垂直分布。这样布局后,当窗体大小改变时,各个组件都会自动改变大小,且当窗体纵向增大时,只有中间的文本框增大,其他3个布局组件不增大。最终设计好的窗体的组件布局如图2-18所示,从图中可以清楚地看出组件的层次关系,以及布局的设置。
图2-18 设计好的窗体的组件布局与层次关系
在窗体可视化设计布局时,要善于利用水平和垂直空格组件,善于设置组件的最大、最小宽度或高度属性,善于设置布局的layoutStretch等属性来达到布局效果。
提示 窗体可视化设计和布局就是放置组件并合理地布局,设计过程如同拼图一般,设计的经验多了自然就熟悉了,所以本书后面的示例一般不会花篇幅来描述界面可视化设计的具体实现过程,读者看本书示例源程序里的窗体UI文件即可。
3.伙伴关系与Tab顺序
在UI Designer工具栏上单击“Edit Buddies”按钮可以进入伙伴关系编辑状态,例如设计一个窗体时,进入伙伴关系编辑状态之后如图2-19所示。
图2-19 编辑伙伴关系
伙伴关系(Buddy)是指界面上一个Label和一个具有输入焦点的组件相关联,在图2-19的伙伴关系编辑状态,单击一个Label,按住鼠标左键,然后拖向一个组件,就建立了Label和组件之间的伙伴关系。
伙伴关系是为了在程序运行时,在窗体上用快捷键快速将输入焦点切换到某个组件上。例如,在图2-19的界面上,设定“姓名”标签的text属性为“姓名(&N)”,其中符号“&”用来指定快捷字符,界面上并不显示“&”。这里指定快捷字母为N,那么程序运行时,如果用户按下Alt+N,输入焦点就会快速切换到“姓名”标签关联的文本框内。
在UI Designer工具栏上单击“Edit Tab Order”按钮进入Tab顺序编辑状态(如图2-20所示)。Tab顺序是指在程序运行时,按下键盘上的Tab键时输入焦点的移动顺序。一个好的用户界面,在按Tab键时焦点应该以合理的顺序在界面上移动。
图2-20 Tab顺序编辑状态
进入Tab顺序编辑状态后,在界面上会显示具有Tab顺序的组件的Tab顺序编号,依次按希望的顺序单击组件,就可以重排Tab顺序了。没有输入焦点的组件是没有Tab顺序的,例如Label组件。
2.3.5 组件的信号与内建槽函数的关联
Qt的界面组件都是从QWidget继承而来的,都支持信号与槽的功能。每个类都有一些内建的信号和槽函数,例如QPushButton按钮类常用的信号是clicked(),在按钮被单击时发射此信号。QDialog是对话框类,它有以下3个内建的槽函数。
· accept(),功能是关闭对话框,表示肯定的选择,例如“确定”。
· reject(),功能是关闭对话框,表示否定的选择,例如“取消”。
· close(),功能是关闭对话框。
这3个槽函数都可以关闭对话框,但是表示的对话框的返回值不同,关于对话框的显示和返回值在6.2节详细介绍。在图2-17的对话框上,我们希望将“确定”按钮与对话框的accept()槽函数关联,将“退出”按钮与对话框的close()槽函数关联。
可以在UI Designer里使用可视化的方式实现信号与槽函数的关联。在UI Designer里单击上方工具栏里的“Edit Signals/Slots”按钮,窗体进入信号与槽函数编辑状态,如图2-21所示。
图2-21 窗体进入Signals/Slots编辑状态(已设置好关联)
鼠标点选“确定”按钮,再按住鼠标左键拖动到窗体的空白区域后释放左键,这时出现如图2-22所示的关联设置对话框。此对话框左边的列表框里显示了btnOK的信号,选择clicked(),右边的列表框里显示了Dialog的槽函数,选择accept(),然后单击“OK”按钮。同样的方法可以将btnClose的clicked()信号与Dialog的close()槽函数关联。
图2-22 信号与槽关联编辑对话框
提示 在图2-22的右边列表框中没有close()函数,需要勾选下方的“Show signals and slots inherited from QWidget”才会出现close()函数。
设置好这两个按钮的信号与槽关联之后,在窗体下方的Signals Slots编辑器里就显示了这两个关联(如图2-23所示)。实际上,可以直接在Signals Slots编辑器进行某个组件的内建信号与其他组件的内建槽函数关联。
图2-23 信号与槽关联编辑器
2.3.6 PyQt5 GUI项目程序框架
1.项目文件组成
在完成上一步的窗体可视化设计后,就可以将窗体文件Dialog.ui编译转换为相应的Python类定义文件,并编写PyQt5 GUI应用程序,测试程序运行效果。
在这个示例中,我们对PyQt5 GUI应用程序的文件组成做了一个统一的规划。示例Demo2_3的文件夹下的文件组成如图2-24所示。各个文件或文件夹的作用如下。
图2-24 示例Demo2_3的文件夹下的文件组成
· 子文件夹_pycache_是Python运行程序时自动生成的临时文件夹。
· 子文件夹QtApp是创建的Qt C++ GUI项目QtApp所在的文件夹。由于Qt项目的文件比较多,而我们只是为了使用其中的ui文件,因此在Demo2_3项目的目录下单独建一个文件夹。
· 文件Dialog.ui是在Qt Creator里设计的窗体UI文件,将子文件夹QtApp下的同名文件复制到此。每次在QtApp项目里修改了窗体文件后,应该将新的文件复制到此处并覆盖原有文件。文件Dialog.ui的窗体类的名称是Dialog。
· 文件ui_Dialog.py是用pyuic5对文件Dialog.ui编译转换得到的Python程序文件,包含一个创建窗体界面的Python类的定义,文件ui_Dialog.py中的类名称是Ui_Dialog。
· 文件myDialog.py是采用2.2节介绍的单继承和界面独立封装方式定义的一个对窗体进行业务逻辑操作的类的文件,文件myDialog.py中的类的名称是QmyDialog(这个名称是可以根据个人命名规则自行决定的)。
· 文件appMain.py是创建应用程序和QmyDialog类窗体对象的实例,显示窗体并运行的主程序。
· 文件uic.bat是一个批处理文件,用于复制Dialog.ui文件,并用pyuic5指令编译Dialog.ui文件而生成文件ui_Dialog.py。
2.批处理文件uic.bat
用鼠标右键单击文件uic.bat(注意不要双击,双击是执行此文件),在快捷菜单里点击“编辑”会显示此文件的内容。uic.bat文件的内容如下:
echo off rem将子目录QtApp下的.ui文件复制到当前目录下 copy .\QtApp\Dialog.ui Dialog.ui rem用pyuic5编译.ui文件 pyuic5-o ui_Dialog.py Dialog.ui
uic.bat文件是指令批处理文件,运行批处理文件就相当于在Windows的cmd窗口里顺序执行文件里的操作指令。文件中的“rem”表示注释行,这个文件主要执行了以下两条指令。
· 第一条是复制文件Dialog.ui的指令,它将子文件夹QtApp下的文件Dialog.ui复制为uic.bat文件所在文件夹下的文件Dialog.ui,如果文件已存在会自动覆盖。
· 第二条是编译Dialog.ui文件的指令,使用工具软件pyuic5将UI文件Dialog.ui编译为Python程序文件ui_Dialog.py。
将操作指令编写为批处理文件uic.bat后,双击此文件就可以执行这些指令,避免了在cmd窗口里重复键入指令的麻烦。
3.窗体界面定义文件ui_Dialog.py
运行批处理文件uic.bat后,将得到窗体UI文件Dialog.ui编译后的窗体界面定义文件ui_Dialog.py,下面是这个文件的完整内容:
from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(337, 318) font = QtGui.QFont() font.setFamily("宋体") font.setPointSize(11) font.setBold(True) font.setWeight(75) Dialog.setFont(font) self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) self.verticalLayout.setContentsMargins(11, 11, 11, 9) self.verticalLayout.setSpacing(6) self.verticalLayout.setObjectName("verticalLayout") self.groupBox1 = QtWidgets.QGroupBox(Dialog) self.groupBox1.setTitle("") self.groupBox1.setObjectName("groupBox1") self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox1) self.horizontalLayout_2.setContentsMargins(11, 11, 11, 11) self.horizontalLayout_2.setSpacing(6) self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.chkBoxUnder = QtWidgets.QCheckBox(self.groupBox1) self.chkBoxUnder.setObjectName("chkBoxUnder") self.horizontalLayout_2.addWidget(self.chkBoxUnder) self.chkBoxItalic = QtWidgets.QCheckBox(self.groupBox1) self.chkBoxItalic.setObjectName("chkBoxItalic") self.horizontalLayout_2.addWidget(self.chkBoxItalic) self.chkBoxBold = QtWidgets.QCheckBox(self.groupBox1) self.chkBoxBold.setChecked(True) self.chkBoxBold.setObjectName("chkBoxBold") self.horizontalLayout_2.addWidget(self.chkBoxBold) self.verticalLayout.addWidget(self.groupBox1) self.groupBox2 = QtWidgets.QGroupBox(Dialog) self.groupBox2.setTitle("") self.groupBox2.setObjectName("groupBox2") self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.groupBox2) self.horizontalLayout_3.setContentsMargins(11, 11, 11, 11) self.horizontalLayout_3.setSpacing(6) self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.radioBlack = QtWidgets.QRadioButton(self.groupBox2) self.radioBlack.setChecked(True) self.radioBlack.setObjectName("radioBlack") self.horizontalLayout_3.addWidget(self.radioBlack) self.radioRed = QtWidgets.QRadioButton(self.groupBox2) self.radioRed.setObjectName("radioRed") self.horizontalLayout_3.addWidget(self.radioRed) self.radioBlue = QtWidgets.QRadioButton(self.groupBox2) self.radioBlue.setChecked(False) self.radioBlue.setObjectName("radioBlue") self.horizontalLayout_3.addWidget(self.radioBlue) self.verticalLayout.addWidget(self.groupBox2) self.textEdit = QtWidgets.QPlainTextEdit(Dialog) font = QtGui.QFont() font.setPointSize(20) font.setBold(True) font.setWeight(75) self.textEdit.setFont(font) self.textEdit.setObjectName("textEdit") self.verticalLayout.addWidget(self.textEdit) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setContentsMargins(-1, 10, -1, 10) self.horizontalLayout.setSpacing(6) self.horizontalLayout.setObjectName("horizontalLayout") spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.btnClear = QtWidgets.QPushButton(Dialog) self.btnClear.setObjectName("btnClear") self.horizontalLayout.addWidget(self.btnClear) spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) self.btnOK = QtWidgets.QPushButton(Dialog) self.btnOK.setObjectName("btnOK") self.horizontalLayout.addWidget(self.btnOK) self.btnClose = QtWidgets.QPushButton(Dialog) self.btnClose.setObjectName("btnClose") self.horizontalLayout.addWidget(self.btnClose) self.verticalLayout.addLayout(self.horizontalLayout) self.retranslateUi(Dialog) self.btnOK.clicked.connect(Dialog.accept) self.btnClose.clicked.connect(Dialog.close) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", " Demo2-3信号与槽")) self.chkBoxUnder.setText(_translate("Dialog", "Underline")) self.chkBoxItalic.setText(_translate("Dialog", "Italic")) self.chkBoxBold.setText(_translate("Dialog", "Bold")) self.radioBlack.setText(_translate("Dialog", "Black")) self.radioRed.setText(_translate("Dialog", "Red")) self.radioBlue.setText(_translate("Dialog", "Blue")) self.textEdit.setPlainText(_translate("Dialog", "PyQt5 编程指南\n" "Python和Qt")) self.btnClear.setText(_translate("Dialog", "清空")) self.btnOK.setText(_translate("Dialog", "确定")) self.btnClose.setText(_translate("Dialog", "退出"))
这个文件定义了一个Python类Ui_Dialog,在2.2.2节已经分析了这种类的基本构成,它主要完成两个任务:界面创建,信号与槽函数的关联。
(1)界面创建
setupUi()函数创建窗体上的各个组件,包括布局管理组件。布局管理也是通过相应的类实现的,例如,groupBox1组件内部是3个CheckBox组件的水平布局,相关代码是(省略了中间一些属性设置的代码行):
self.groupBox1 = QtWidgets.QGroupBox(Dialog) self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox1) self.chkBoxUnder = QtWidgets.QCheckBox(self.groupBox1) self.horizontalLayout_2.addWidget(self.chkBoxUnder) self.chkBoxItalic = QtWidgets.QCheckBox(self.groupBox1) self.horizontalLayout_2.addWidget(self.chkBoxItalic) self.chkBoxBold = QtWidgets.QCheckBox(self.groupBox1) self.horizontalLayout_2.addWidget(self.chkBoxBold)
第2行代码创建了一个水平布局horizontalLayout_2,其父容器是groupBox1。
创建CheckBox组件时指定父容器为groupBox1,然后添加到水平布局horizontalLayout_2里。这样依次添加的3个CheckBox组件就在groupBox1里水平分布了。同样,其他的布局管理的代码也是类似的。
分析这些代码可以发现代码化创建窗体组件和布局管理的编程方法。可视化设计的窗体最后其实也都是转换为代码来执行的,但显然,没几个人愿意手工编写这样的代码来创建窗体。但是我们需要知道这些代码的原理,一般情况下尽量用UI Designer可视化设计窗体,在必须手工编写代码创建界面时再编写代码,例如在后面要讲到的混合方式创建界面的时候。
(2)信号与槽的关联
在setupUi()函数的最后有3行这样的语句:
self.btnOK.clicked.connect(Dialog.accept) self.btnClose.clicked.connect(Dialog.close) QtCore.QMetaObject.connectSlotsByName(Dialog)
其中,第1行将界面上的按钮btnOK的clicked()信号与窗体对象Dialog的accept()槽函数关联起来;第2行将按钮btnClose的clicked()信号与Dialog的close()槽函数关联起来;第3行的作用在后面解释。
信号与槽函数关联使用connect()函数,语句如下:
sender.signalName.connect(receiver.slotName)
其中:
· sender表示发射信号的对象名称,如self.btnOK;
· signalName表示信号的名称,如clicked;
· receiver是对信号作出响应的接收者的名称,如Dialog;
· slotName是接收者的响应槽函数的名称,如accept。
所以,对于在图2-23的Signals Slots编辑器里可视化设置的关联,setupUi()函数将自动生成信号与槽关联的语句。
提示 在本书后面的示例程序中,将不会再显示.ui文件编译后生成的Python文件的内容,因为这个文件就是可视化设计的UI窗体的代码实现,代码多且没有显示的意义。如果需要分析某种界面效果的代码化构建方法,可以自行分析此文件代码。
4.窗体业务逻辑类文件myDialog.py
按照2.2节介绍的界面与业务逻辑分离且界面独立封装的方式定义一个类QmyDialog,并保存为文件myDialog.py。文件代码如下:
##与UI窗体类对应的业务逻辑类 import sys from PyQt5.QtWidgets import QApplication, QDialog from ui_Dialog import Ui_Dialog class QmyDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) #调用父类构造函数,创建窗体 self.ui=Ui_Dialog() #创建UI对象 self.ui.setupUi(self) #构造UI if __name__ == "__main__": #用于当前窗体测试 app = QApplication(sys.argv) #创建GUI应用程序 form=QmyDialog() #创建窗体 form.show() sys.exit(app.exec_())
这个文件有窗体测试程序,运行此文件时,就会执行文件后面部分的程序,其功能是创建应用程序和窗体,并运行应用程序。
现在运行程序myDialog.py就会出现所设计的窗体,点击窗体上的“确定”和“退出”按钮可以关闭窗体并退出程序,说明这两个按钮的功能实现了。这是因为在QmyDialog类的构造函数中,创建了窗体类的实例对象self.ui,并调用了其setupUi()函数,即下面这两行语句:
self.ui=Ui_Dialog() #创建UI对象 self.ui.setupUi(self) #构造UI
而Ui_Dialog的setupUi()函数实现了这两个按钮的信号与窗体相关槽函数的关联,所以点击按钮的操作起作用了。
5.应用程序主程序文件appMain.py
程序myDialog.py可以当作主程序直接运行,但是建议单独编写一个主程序文件appMain.py,此文件的代码如下:
## GUI应用程序主程序 import sys from PyQt5.QtWidgets import QApplication from myDialog import QmyDialog app = QApplication(sys.argv) #创建GUI应用程序 mainform=QmyDialog() #创建主窗体 mainform.show() #显示主窗体 sys.exit(app.exec_())
appMain.py的功能是创建应用程序和主窗体,然后显示主窗体,并开始运行应用程序。它将myDialog.py文件的测试运行部分单独拿出来作为一个文件。当一个应用程序有多个窗体,并且窗体之间有数据传递时,appMain.py负责创建应用程序的主窗体并运行起来,这样使整个应用程序的结构更清晰。
注意 为了避免混淆,我们在命名文件时,将文件名与文件内的类名称区分开来,将Python中针对UI窗体创建的业务逻辑类的名称与UI窗体名称区分开来,这与Eric中的命名方法不同。
例如,UI文件Dialog.ui中的窗体名称是Dialog,文件Dialog.ui编译后生成的Python类名称是固定的Ui_Dialog,我们设置保存为文件ui_Dialog.py,而Eric会自动保存为文件Ui_Dialog.py。
针对文件ui_Dialog.py中的类Ui_Dialog创建的窗体业务逻辑类是可以自由命名的,我们将类命名为QmyDialog,保存为文件myDialog.py。而在Eric中创建的业务逻辑类默认的文件名是Dialog.py,类名称也是Dialog,这对于初学者容易造成混淆。
2.3.7 为组件的内建信号编写槽函数
1.自动关联的槽函数
下面为窗体上的“清空”按钮编写槽函数,首先要找到应该使用该按钮的那个信号。在Qt Creator中打开本示例的QtApp项目,再打开窗体Dialog.ui,选中“清空”按钮,点击右键调出其快捷菜单,在菜单中点击“Go to slot...”菜单项,会打开如图2-25所示的Go to slot对话框。
图2-25 QPushButton类按钮的Go to slot对话框
这个对话框显示了所选组件类的所有可用信号。“清空”按钮是一个QPushButton类的按钮,所以图2-25显示的是QPushButton类及其所有父类的信号。按钮最常用的信号是clicked(),就是点击按钮时发射的信号。在图2-25中选择clicked()信号,然后点击“OK”按钮,这样会在QtApp项目的Dialog.cpp文件里生成下面这样一个C++槽函数框架:
void DialogText::on_btnClear_clicked() { }
按快捷键F4会在Dialog.h和Dialog.cpp文件之间切换,在Dialog.h文件里可看到自动生成的这个槽函数的C++原型定义:
void on_btnClear_clicked();
我们并不需要编写任何C++程序,而只需要自动生成的这个槽函数名称。复制此函数名称,在myDialog.py文件的QmyDialog类里定义一个同名的函数并编写代码:
def on_btnClear_clicked(self): self.ui.textEdit.clear()
现在若运行myDialog.py文件,会发现“清空”按钮可用了,它会将文本框里的内容全部清除。
同样,在UI Designer里可视化设计窗体时,选中“Bold”复选框,打开其Go to slot对话框(如图2-26所示),这里显示了QCheckBox类的所有信号。
图2-26 QCheckBox类组件的Go to slot对话框
其中的toggled(bool)信号在复选框的状态变化时发射,复选框的勾选状态作为参数传递给函数,点击“OK”按钮后生成其C++槽函数原型为:
void on_chkBoxBold_toggled(bool checked);
在myDialog.py文件的QmyDialog类里定义一个同名函数,并且具有相同类型的参数,代码如下:
def on_chkBoxBold_toggled(self, checked): font=self.ui.textEdit.font() font.setBold(checked) #参数checked表示勾选状态 self.ui.textEdit.setFont(font)
现在若运行myDialogText.py文件,会发现“Bold”复选框可用了,会使文本框的字体在粗体和正常之间切换。
同样,在窗体可视化设计时,选中“Underline”复选框,打开其Go to slot对话框如图2-26所示。在对话框里不选择toggled(bool)信号,而是选择clicked()信号。而且要注意,还有一个带参数的clicked(bool)信号,它会将点击复选框时的勾选状态当作一个参数传递给槽函数。现在暂时用clicked()信号,对于同名而参数不同的overload型信号的处理在后面讨论。
“Underline”复选框的clicked()信号的C++槽函数原型是:
void on_chkBoxUnder_clicked();
在myDialog.py文件的QmyDialog类里定义一个同名函数并编写代码。现在QmyDialog类的完整代码如下:
import sys from PyQt5.QtWidgets import QApplication, QDialog from ui_Dialog import Ui_Dialog class QmyDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) #调用父类构造函数,创建窗体 self.ui=Ui_Dialog() #创建UI对象 self.ui.setupUi(self) #构造UI ##==== 由connectSlotsByName() 自动与组件的信号关联的槽函数===== def on_btnClear_clicked(self): ##"清空"按钮 self.ui.textEdit.clear() def on_chkBoxBold_toggled(self, checked): ## "Bold" 复选框 font=self.ui.textEdit.font() font.setBold(checked) #参数checked表示勾选状态 self.ui.textEdit.setFont(font) def on_chkBoxUnder_clicked(self): ## "Underline"复选框 checked=self.ui.chkBoxUnder.isChecked() #读取勾选状态 font=self.ui.textEdit.font() font.setUnderline(checked) self.ui.textEdit.setFont(font)
现在运行文件myDialog.py, “Underline”复选框的功能也可用了。
在QmyDialog类里定义了3个函数,这3个函数与相应界面组件的信号关联起来了,实现了信号与槽的关联。但是,在QmyDialog类的构造函数里并没有添加任何代码进行信号与槽的关联,而Ui_Dialog类也没有做任何修改(在UI Designer里可视化生成槽函数框架时,并不会对Dialog.ui做任何修改,所以无须重新编译Dialog.ui),这些信号与槽的关联是如何实现的呢?
秘密在于ui_Dialog.py文件中的Ui_Dialog.setupUi()函数的最后一行语句
QtCore.QMetaObject.connectSlotsByName(Dialog)
使用了Qt的元对象(QMetaObject),它会搜索Dialog窗体上的所有从属组件,将匹配的信号和槽函数关联起来,它假设槽函数的名称是:
on_<object name>_<signal name>(<signal parameters>)
在组件的Go to slot对话框里,选择一个信号后生成的槽函数名称就是符合这个命名规则的。所以,如果在UI Designer里通过可视化设计自动生成槽函数框架,然后复制函数名到Python程序里,这样的槽函数就可以和组件的信号自动关联,而不用逐个手工编写关联的语句。
不符合这样的命名规则的函数不能自动与信号关联,即使非常小的改动,例如函数on_btnClear_clicked()改为on_btnClear_clicked2(),也不能与组件的信号自动关联。
提示 要在Qt Creator中通过Go to slot对话框为一个UI窗体上的组件自动生成槽函数框架,UI窗体文件必须是在一个Qt GUI项目里打开的,一个.ui文件有对应的.h和.cpp文件。像示例Demo2_2里那样只有一个独立的.ui文件是不能生成槽函数框架的。使用Qt的独立软件Qt Designer只能设计UI窗体,没有Go to slot对话框,不能生成槽函数框架,这就是为什么我们使用Qt Creator内置的UI Designer,而不使用独立的Qt Designer的原因。
2.overload型信号的处理
在图2-26的QCheckBox类组件的Go to slot对话框中,有两个名称为clicked的信号,一个是不带参数的clicked()信号,“Underline”复选框使用这个信号生成槽函数是可以自动关联的;另一个是带参数的clicked(bool)信号,它将复选框的当前勾选状态作为参数传递给槽函数。这种名称相同但参数个数或类型不同的信号就是overload型信号。
对于窗体上的“Italic”复选框,在其Go to slot对话框中选择clicked(bool)信号生成槽函数原型,用相应的函数名在QmyDialog类中定义一个函数,代码如下:
def on_chkBoxItalic_clicked(self, checked): font=self.ui.textEdit.font() font.setItalic(checked) self.ui.textEdit.setFont(font)
我们“以为”这个槽函数会和chkBoxItalic的clicked(bool)信号自动关联,运行文件myDialog.py,却发现点击“Italic”复选框时,程序出现异常直接退出了!为什么会出现这种情况呢?
这是因为有两个不同类型参数的clicked信号,connectSlotsByName()函数进行信号与槽函数的关联时会使用一个默认的信号,对QCheckBox来说,默认使用的是不带参数的clicked()信号。而现在定义的函数on_chkBoxItalic_clicked(self, checked)是需要传递进来一个参数的,程序运行到on_chkBoxItalic_clicked(self, checked)函数时,无法给它传递一个参数checked,所以发生了异常。
要解决这个问题,需要使用@pyqtSlot修饰符,用这个修饰符将函数的参数类型声明清楚。将on_chkBoxItalic_clicked()函数的代码修改为如下形式:
@pyqtSlot(bool) ##修饰符指定参数类型,用于overload型的信号 def on_chkBoxItalic_clicked(self, checked): font=self.ui.textEdit.font() font.setItalic(checked) self.ui.textEdit.setFont(font)
这样使用@pyqtSlot修饰符声明函数参数类型后,connectSlotsByName()函数就会自动使用clicked(bool)信号与这个槽函数关联,运行就没有问题了。
注意 对于非默认的overload型信号,槽函数必须使用修饰符@pyqtSlot声明函数参数类型。如果两种参数的overload型信号都要关联槽函数,那么两个槽函数名必须不同名,且在关联时要做设置(具体的设置方法在2.4节的示例里介绍)。
3.手动关联信号与槽函数
很多情况下也需要手工编写代码进行信号与槽的关联,例如在图2-18的窗体上,希望将设置颜色的3个RadioButton按钮的clicked()信号与同一个槽函数关联。
在QmyDialog类里定义一个新的函数do_setTextColor(),并且在构造函数里进行关联,添加这些功能后的myDialog.py的完整代码如下:
import sys from PyQt5.QtWidgets import QApplication, QDialog from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt, pyqtSlot from ui_Dialog import Ui_Dialog class QmyDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) #调用父类构造函数 self.ui=Ui_Dialog() #创建UI对象 self.ui.setupUi(self) #构造UI self.ui.radioBlack.clicked.connect(self.do_setTextColor) self.ui.radioRed.clicked.connect(self.do_setTextColor) self.ui.radioBlue.clicked.connect(self.do_setTextColor) ##==== 由connectSlotsByName() 自动与组件的信号关联的槽函数==== def on_btnClear_clicked(self): ##"清空" 按钮 self.ui.textEdit.clear() def on_chkBoxBold_toggled(self, checked): ##"Bold"复选框 font=self.ui.textEdit.font() font.setBold(checked) #函数参数checked表示勾选状态 self.ui.textEdit.setFont(font) def on_chkBoxUnder_clicked(self): ##"Underline"复选框 checked=self.ui.chkBoxUnder.isChecked() #读取勾选状态 font=self.ui.textEdit.font() font.setUnderline(checked) self.ui.textEdit.setFont(font) @pyqtSlot(bool) ##修饰符指定参数类型,用于overload型的信号 def on_chkBoxItalic_clicked(self, checked): #"Italic"复选框 font=self.ui.textEdit.font() font.setItalic(checked) self.ui.textEdit.setFont(font) ##=========自定义槽函数======== def do_setTextColor(self): ##设置文本颜色 plet=self.ui.textEdit.palette() #获取palette if (self.ui.radioBlack.isChecked()): plet.setColor(QPalette.Text, Qt.black) #black elif (self.ui.radioRed.isChecked()): plet.setColor(QPalette.Text, Qt.red) #red elif (self.ui.radioBlue.isChecked()): plet.setColor(QPalette.Text, Qt.blue) #blue self.ui.textEdit.setPalette(plet) #设置palette if __name__ == "__main__": ##用于当前窗体测试 app = QApplication(sys.argv) #创建GUI应用程序 form=QmyDialog() #创建窗体 form.show() sys.exit(app.exec_())
代码里用到了QPalette、Qt、pyqtSlot等类或函数,所以需要用import语句从相应的模块导入。
自定义的函数do_setTextColor()读取3个RadioButton按钮的选中状态,哪个按钮被选中就设置这个按钮文本框里文本的颜色为相应的颜色。do_setTextColor()的代码涉及的具体操作现在暂时不解释,第3章会介绍常用界面组件的使用。
在QmyDialog的构造函数中增加了下面3条语句:
self.ui.radioBlack.clicked.connect(self.do_setTextColor) self.ui.radioRed.clicked.connect(self.do_setTextColor) self.ui.radioBlue.clicked.connect(self.do_setTextColor)
这样就将3个RadioButton按钮的clicked()信号与同一个槽函数do_setTextColor()关联起来了,实现了信号与槽函数的关联。
提示 为了与connectSlotsByName()自动关联的槽函数区别,本书中自定义槽函数的函数名一律使用“do_”作为前缀。当然,这只是个人习惯的命名规则。
现在运行程序myDialog.py,就会发现3个设置颜色的RadioButton按钮都可以用了,整个窗体的所有功能都实现了。