PyQt5-信号与槽的使用

信号与槽的使用

信号与槽(Signals/Slots)是Qt编程的基础,也是Qt的一大特色。因为有了信号与槽的编程机制,在Qt中处理界面组件的交互操作时变得比较直观和简单。

信号(Signal)就是在特定情况下被发射(emit)的一种通告,例如一个PushButton按钮最常见的信号就是鼠标单击时发射的clicked()信号,一个ComboBox最常见的信号是选择的项变化时发射的CurrentIndexChanged()信号。GUI程序设计的主要内容就是对界面上各组件发射的特定信号进行响应,只需要知道什么情况下发射了哪些信号,然后合理地去响应和处理这些信号就可以了。

槽(Slot)就是对信号响应的函数。槽实质上是一个函数,它可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数会被自动执行。Qt的类一般都有一些内建(build-in)的槽函数,例如QWidget有一个槽函数close(),其功能是关闭窗口。如果将一个PushButton按钮的clicked()信号与窗体的close()槽函数关联,那么点击按钮时就会关闭窗口。

将展示下面的结果

image

  • 上方的3个复选框可以控制文本框内的字体的下划线、斜体、粗体特性。

  • 3个RadioButton按钮可以控制文本框内的文字颜色。

  • “清空”按钮可以清空文本框内的文字。

  • “确定”和“退出”按钮都可以关闭窗口,但是表示对话框的不同选择结果。

Qt Creator的使用

启动Qt Creator,创建一个名为QtApp的C++ GUI应用程序项目

创建一个对话框,所以选择基类QDialog,新窗体的类名称就使用默认的Dialog,这将自动创建3个文件,即Dialog.h、Dialog.cpp和Dialog.ui。

image

设计成这样的界面

image

image

image

界面组件布局管理

界面组件的层次关系

为了将界面上的各个组件的分布设计得更加美观,经常使用一些容器类组件,如GroupBox、TabWidget、Frame等。例如,将3个CheckBox(复选框)组件放置在一个GroupBox组件里,这个GroupBox组件就是这3个复选框的容器,移动这个GroupBox就会同时移动其中的3个CheckBox。

image

布局管理

Qt为窗体设计提供了丰富的布局管理功能,在UI Designer里,组件面板里有Layouts和Spacers两个分组,在窗体上方的工具栏里有布局管理的按钮

image

组件面板里Layouts和Spacers这两个分组里的布局组件的功能如下表所示。

image

使用组件面板里的布局组件设计布局时,先拖放一个布局组件到窗体上,例如在设计上图窗体下方的3个按钮的布局时,先放一个Horizontal Layout到窗体上,布局组件会以红色矩形框显示。再向布局组件里拖放3个PushButton和两个HorizontalSpacer,就可以得到上图中3个按钮的水平布局效果。

image

每个布局还有layoutTopMargin、layoutBottomMargin、layoutLeftMargin、layoutRightMargin这4个属性用于调整布局边框与内部组件之间的上、下、左、右的边距大小。

在设计窗体的上方有一个工具栏,用于使界面进入不同的设计状态,以及进行布局设计。

伙伴关系与Tab顺序

在UI Designer工具栏上单击“Edit Buddies”按钮可以进入伙伴关系编辑状态,例如设计一个窗体时,进入伙伴关系编辑状态之后如下图所示。

image

伙伴关系(Buddy)是指界面上一个Label和一个具有输入焦点的组件相关联,在上图的伙伴关系编辑状态,单击一个Label,按住鼠标左键,然后拖向一个组件,就建立了Label和组件之间的伙伴关系。

伙伴关系是为了在程序运行时,在窗体上用快捷键快速将输入焦点切换到某个组件上。例如,在上图的界面上,设定“姓名”标签的text属性为“姓名(&N)”,其中符号“&”用来指定快捷字符,界面上并不显示“&”。这里指定快捷字母为N,那么程序运行时,如果用户按下Alt+N,输入焦点就会快速切换到“姓名”标签关联的文本框内。

image

组件的信号与内建槽函数的关联

Qt的界面组件都是从QWidget继承而来的,都支持信号与槽的功能。每个类都有一些内建的信号和槽函数,例如QPushButton按钮类常用的信号是clicked(),在按钮被单击时发射此信号。QDialog是对话框类,它有以下3个内建的槽函数。

· accept(),功能是关闭对话框,表示肯定的选择,例如“确定”。

· reject(),功能是关闭对话框,表示否定的选择,例如“取消”。

· close(),功能是关闭对话框。

这3个槽函数都可以关闭对话框,但是表示的对话框的返回值不同

在上图的对话框上,我们希望将“确定”按钮与对话框的accept()槽函数关联,将“退出”按钮与对话框的close()槽函数关联。

可以在UI Designer里使用可视化的方式实现信号与槽函数的关联。在UI Designer里单击上方工具栏里的“EditSignals/Slots”按钮,窗体进入信号与槽函数编辑状态,

image

信号与槽的关联器

image

image

PyQt5 GUI项目程序框架

窗体界面定义文件ui_QtApp.py

在完成上一步的窗体可视化设计后,就可以将窗体文件Dialog.ui编译转换为相应的Python类定义文件,并编写PyQt5GUI应用程序,测试程序运行效果。

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'QtApp.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(400, 298)
        self.groupBox = QtWidgets.QGroupBox(Dialog)
        self.groupBox.setGeometry(QtCore.QRect(10, 69, 371, 41))
        self.groupBox.setTitle("")
        self.groupBox.setObjectName("groupBox")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.groupBox)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.radioButton = QtWidgets.QRadioButton(self.groupBox)
        self.radioButton.setObjectName("radioButton")
        self.horizontalLayout_3.addWidget(self.radioButton)
        self.radioButton_2 = QtWidgets.QRadioButton(self.groupBox)
        self.radioButton_2.setObjectName("radioButton_2")
        self.horizontalLayout_3.addWidget(self.radioButton_2)
        self.radioButton_3 = QtWidgets.QRadioButton(self.groupBox)
        self.radioButton_3.setObjectName("radioButton_3")
        self.horizontalLayout_3.addWidget(self.radioButton_3)
        self.groupBox_2 = QtWidgets.QGroupBox(Dialog)
        self.groupBox_2.setGeometry(QtCore.QRect(10, 10, 371, 51))
        self.groupBox_2.setTitle("")
        self.groupBox_2.setObjectName("groupBox_2")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox_2)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.checkBox = QtWidgets.QCheckBox(self.groupBox_2)
        self.checkBox.setObjectName("checkBox")
        self.horizontalLayout_2.addWidget(self.checkBox)
        self.checkBox_2 = QtWidgets.QCheckBox(self.groupBox_2)
        self.checkBox_2.setObjectName("checkBox_2")
        self.horizontalLayout_2.addWidget(self.checkBox_2)
        self.checkBox_3 = QtWidgets.QCheckBox(self.groupBox_2)
        self.checkBox_3.setObjectName("checkBox_3")
        self.horizontalLayout_2.addWidget(self.checkBox_3)
        self.plainTextEdit = QtWidgets.QPlainTextEdit(Dialog)
        self.plainTextEdit.setGeometry(QtCore.QRect(10, 120, 371, 101))
        self.plainTextEdit.setTabStopWidth(80)
        self.plainTextEdit.setBackgroundVisible(False)
        self.plainTextEdit.setCenterOnScroll(True)
        self.plainTextEdit.setObjectName("plainTextEdit")
        self.groupBox_3 = QtWidgets.QGroupBox(Dialog)
        self.groupBox_3.setGeometry(QtCore.QRect(10, 240, 371, 43))
        self.groupBox_3.setTitle("")
        self.groupBox_3.setObjectName("groupBox_3")
        self.pushButton = QtWidgets.QPushButton(self.groupBox_3)
        self.pushButton.setGeometry(QtCore.QRect(10, 10, 75, 23))
        self.pushButton.setObjectName("pushButton")
        self.pushButton_2 = QtWidgets.QPushButton(self.groupBox_3)
        self.pushButton_2.setGeometry(QtCore.QRect(140, 10, 75, 23))
        self.pushButton_2.setObjectName("pushButton_2")
        self.pushButton_3 = QtWidgets.QPushButton(self.groupBox_3)
        self.pushButton_3.setGeometry(QtCore.QRect(280, 10, 75, 23))
        self.pushButton_3.setObjectName("pushButton_3")

        self.retranslateUi(Dialog)
        self.pushButton_2.clicked.connect(Dialog.accept)
        self.pushButton_3.clicked.connect(Dialog.close)
        QtCore.QMetaObject.connectSlotsByName(Dialog)
        Dialog.setTabOrder(self.checkBox, self.checkBox_2)
        Dialog.setTabOrder(self.checkBox_2, self.checkBox_3)
        Dialog.setTabOrder(self.checkBox_3, self.radioButton)
        Dialog.setTabOrder(self.radioButton, self.radioButton_2)
        Dialog.setTabOrder(self.radioButton_2, self.radioButton_3)
        Dialog.setTabOrder(self.radioButton_3, self.plainTextEdit)
        Dialog.setTabOrder(self.plainTextEdit, self.pushButton)
        Dialog.setTabOrder(self.pushButton, self.pushButton_2)
        Dialog.setTabOrder(self.pushButton_2, self.pushButton_3)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "信号与槽"))
        self.radioButton.setText(_translate("Dialog", "Black"))
        self.radioButton_2.setText(_translate("Dialog", "Red"))
        self.radioButton_3.setText(_translate("Dialog", "Blue"))
        self.checkBox.setText(_translate("Dialog", "Underline"))
        self.checkBox_2.setText(_translate("Dialog", "Bold"))
        self.checkBox_3.setText(_translate("Dialog", "Italic"))
        self.plainTextEdit.setPlainText(_translate("Dialog", "PyQt5编程指南\n"
"Python和Qt."))
        self.pushButton.setText(_translate("Dialog", "清空"))
        self.pushButton_2.setText(_translate("Dialog", "确定"))
        self.pushButton_3.setText(_translate("Dialog", "退出"))

窗体业务逻辑类文件myDialog.py

按照上节介绍的界面与业务逻辑分离且界面独立封装的方式定义一个类QmyDialog,并保存为文件myDialog.py。文件代码如下:

##与Ui窗口类对应的业务逻辑类
import sys
from PyQt5.QtWidgets import  QDialog,QApplication
from ui_QtApp import Ui_Dialog

class QmyDialog(QDialog):
    def __init__(self, parent=None) :
        super().__init__(parent)  #调用父类构造函数,创建QWidget窗体
        self.__ui=Ui_Dialog() #创建UI对象
        self.__ui.setupUi(self) #构造UI

if __name__ == "__main__": #用于当前窗体测试
    app = QApplication(sys.argv) # 创建app,用QApplication类
    myWidget = QmyDialog() #创建窗体
    myWidget.show()
    sys.exit(app.exec_())

这个文件有窗体测试程序,运行此文件时,就会执行文件后面部分的程序,其功能是创建应用程序和窗体,并运行应用程序。

运行结果如下:

image

现在运行程序myDialog.py就会出现所设计的窗体,点击窗体上的“确定”和“退出”按钮可以关闭窗体并退出程序,说明这两个按钮的功能实现了。这是因为在QmyDialog类的构造函数中,创建了窗体类的实例对象self.ui,并调用了其setupUi()函数,即下面这两行语句:

self.__ui=Ui_Dialog() #创建UI对象
self.__ui.setupUi(self) #构造UI

而Ui_Dialog的setupUi()函数实现了这两个按钮的信号与窗体相关槽函数的关联,所以点击按钮的操作起作用了。

应用程序主程序文件appMain.py

程序myDialog.py可以当作主程序直接运行,但是建议单独编写一个主程序文件appMain.py,此文件的代码如下:

## GUI应用程序主程序
import sys
from PyQt5.QtWidgets import  QApplication
from myDialog import  QmyDialog

app = QApplication(sys.argv) # 创建app,用QApplication类
myWidget = QmyDialog() #创建窗体
myWidget.show()
sys.exit(app.exec_())

依然能显示出窗体

image

appMain.py的功能是创建应用程序和主窗体,然后显示主窗体,并开始运行应用程序。它将myDialog.py文件的测试运行部分单独拿出来作为一个文件。当一个应用程序有多个窗体,并且窗体之间有数据传递时,appMain.py负责创建应用程序的主窗体并运行起来,这样使整个应用程序的结构更清晰。

image

为组件的内建信号编写槽函数

自动关联的槽函数

下面为窗体上的“清空”按钮编写槽函数,首先要找到应该使用该按钮的那个信号。

在myDialog.py文件中加入以下代码,为清空按钮绑定一个函数

self.ui.btnClear.clicked.connect(self.clear_text)

如下所示:

image

其次在myDialog类中编写clear_text()函数

def clear_text(self):
    self.__ui.plainTextEdit.clear()

这样清空按钮的功能就实现了。

那么整体的myDialog,py文件代码如下:

##与Ui窗口类对应的业务逻辑类
import sys
from PyQt5.QtWidgets import  QDialog,QApplication
from ui_QtApp import Ui_Dialog

class QmyDialog(QDialog):
    def __init__(self, parent=None) :
        super().__init__(parent)  #调用父类构造函数,创建QWidget窗体
        self.__ui=Ui_Dialog() #创建UI对象
        self.__ui.setupUi(self) #构造UI
        self.__ui.pushButton.clicked.connect(self.clear_text)

    def clear_text(self):
        self.__ui.plainTextEdit.clear()

if __name__ == "__main__": #用于当前窗体测试
    app = QApplication(sys.argv) # 创建app,用QApplication类
    myWidget = QmyDialog() #创建窗体
    myWidget.show()
    sys.exit(app.exec_())

image

这时候 点击清空按钮

image

同样,在UI Designer里可视化设计窗体时,选中“Bold”复选框,添加按钮参数

##与Ui窗口类对应的业务逻辑类
import sys
from PyQt5.QtWidgets import  QDialog,QApplication
from ui_QtApp import Ui_Dialog

class QmyDialog(QDialog):
    def __init__(self, parent=None) :
        super().__init__(parent)  #调用父类构造函数,创建QWidget窗体
        self.__ui=Ui_Dialog() #创建UI对象
        self.__ui.setupUi(self) #构造UI
        self.__ui.pushButton.clicked.connect(self.clear_text)
        self.__ui.checkBox_2.toggled.connect(self.on_chkBoxBold_toggled)

    def clear_text(self):
        self.__ui.plainTextEdit.clear()

    def on_chkBoxBold_toggled(self):
        checked=self.__ui.checkBox_2.isChecked() #读取勾选状态
        font=self.__ui.plainTextEdit.font()
        font.setBold(checked)
        self.__ui.plainTextEdit.setFont(font)

if __name__ == "__main__": #用于当前窗体测试
    app = QApplication(sys.argv) # 创建app,用QApplication类
    myWidget = QmyDialog() #创建窗体
    myWidget.show()
    sys.exit(app.exec_())

点击Bold之后 效果如下:

image

同样,在窗体可视化设计时,选中“Underline”复选框

在对话框里不选择toggled(bool)信号,而是选择clicked()信号。而且要注意,还有一个带参数的clicked(bool)信号,它会将点击复选框时的勾选状态当作一个参数传递给槽函数。

代码如下:

##与Ui窗口类对应的业务逻辑类
import sys
from PyQt5.QtWidgets import  QDialog,QApplication
from ui_QtApp import Ui_Dialog

class QmyDialog(QDialog):
    def __init__(self, parent=None) :
        super().__init__(parent)  #调用父类构造函数,创建QWidget窗体
        self.__ui=Ui_Dialog() #创建UI对象
        self.__ui.setupUi(self) #构造UI
        self.__ui.pushButton.clicked.connect(self.clear_text)
        self.__ui.checkBox_2.toggled.connect(self.on_chkBoxBold_toggled)
        self.__ui.checkBox.clicked.connect(self.on_chkBoxUnder_clicked)

    def clear_text(self):
        self.__ui.plainTextEdit.clear()

    def on_chkBoxBold_toggled(self):
        checked=self.__ui.checkBox_2.isChecked() #读取勾选状态
        font=self.__ui.plainTextEdit.font()
        font.setBold(checked)
        self.__ui.plainTextEdit.setFont(font)
    def on_chkBoxUnder_clicked(self):
        checked=self.__ui.checkBox.isChecked()
        font = self.__ui.plainTextEdit.font()
        font.setUnderline(checked)
        self.__ui.plainTextEdit.setFont(font)


if __name__ == "__main__": #用于当前窗体测试
    app = QApplication(sys.argv) # 创建app,用QApplication类
    myWidget = QmyDialog() #创建窗体
    myWidget.show()
    sys.exit(app.exec_())

点击Underline之后 效果如下:

image

image

overload型信号的处理

在QCheckBox类组件的Go to slot对话框中,有两个名称为clicked的信号,一个是不带参数的clicked()信号,“Underline”复选框使用这个信号生成槽函数是可以自动关联的;另一个是带参数的clicked(bool)信号,它将复选框的当前勾选状态作为参数传递给槽函数。这种名称相同但参数个数或类型不同的信号就是overload型信号。

对于窗体上的“Italic”复选框,在其Go to slot对话框中选择clicked(bool)信号生成槽函数原型,用相应的函数名在QmyDialog类中定义一个函数,代码如下:

    def on_chkBoxItalic_clicked(self):
        checked=self.__ui.checkBox_3.isChecked()
        font=self.__ui.plainTextEdit.font()
        font.setItalic(checked)
        self.__ui.plainTextEdit.setFont(font)

总代码如下:

##与Ui窗口类对应的业务逻辑类
import sys
from PyQt5.QtWidgets import  QDialog,QApplication
from ui_QtApp import Ui_Dialog

class QmyDialog(QDialog):
    def __init__(self, parent=None) :
        super().__init__(parent)  #调用父类构造函数,创建QWidget窗体
        self.__ui=Ui_Dialog() #创建UI对象
        self.__ui.setupUi(self) #构造UI
        self.__ui.pushButton.clicked.connect(self.clear_text)
        self.__ui.checkBox_2.toggled.connect(self.on_chkBoxBold_toggled)
        self.__ui.checkBox.clicked.connect(self.on_chkBoxUnder_clicked)
        self.__ui.checkBox_3.clicked.connect(self.on_chkBoxItalic_clicked)

    def clear_text(self):
        self.__ui.plainTextEdit.clear()

    def on_chkBoxBold_toggled(self):
        checked=self.__ui.checkBox_2.isChecked() #读取勾选状态
        font=self.__ui.plainTextEdit.font()
        font.setBold(checked)
        self.__ui.plainTextEdit.setFont(font)
    def on_chkBoxUnder_clicked(self):
        checked=self.__ui.checkBox.isChecked()
        font = self.__ui.plainTextEdit.font()
        font.setUnderline(checked)
        self.__ui.plainTextEdit.setFont(font)

    def on_chkBoxItalic_clicked(self):
        checked=self.__ui.checkBox_3.isChecked()
        font=self.__ui.plainTextEdit.font()
        font.setItalic(checked)
        self.__ui.plainTextEdit.setFont(font)

if __name__ == "__main__": #用于当前窗体测试
    app = QApplication(sys.argv) # 创建app,用QApplication类
    myWidget = QmyDialog() #创建窗体
    myWidget.show()
    sys.exit(app.exec_())

点击之后Italic效果如下:

image

手动关联信号与槽函数

很多情况下也需要手工编写代码进行信号与槽的关联,例如在上图的窗体上,希望将设置颜色的3个RadioButton按钮的clicked()信号与同一个槽函数关联。

在QmyDialog类里定义一个新的函数do_setTextColor(),并且在构造函数里进行关联,添加这些功能后的myDialog.py的完整代码如下:

##与Ui窗口类对应的业务逻辑类
import sys
from PyQt5.QtWidgets import  QDialog,QApplication
from ui_QtApp import Ui_Dialog
from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt,pyqtSlot

class QmyDialog(QDialog):
    def __init__(self, parent=None) :
        super().__init__(parent)  #调用父类构造函数,创建QWidget窗体
        self.__ui=Ui_Dialog() #创建UI对象
        self.__ui.setupUi(self) #构造UI
        self.__ui.pushButton.clicked.connect(self.clear_text)
        self.__ui.checkBox_2.toggled.connect(self.on_chkBoxBold_toggled)
        self.__ui.checkBox.clicked.connect(self.on_chkBoxUnder_clicked)
        self.__ui.checkBox_3.clicked.connect(self.on_chkBoxItalic_clicked)
        self.__ui.radioButton.clicked.connect(self.do_setTextColor)
        self.__ui.radioButton_2.clicked.connect(self.do_setTextColor)
        self.__ui.radioButton_3.clicked.connect(self.do_setTextColor)


    def clear_text(self):
        self.__ui.plainTextEdit.clear()

    def on_chkBoxBold_toggled(self):
        checked=self.__ui.checkBox_2.isChecked() #读取勾选状态
        font=self.__ui.plainTextEdit.font()
        font.setBold(checked)
        self.__ui.plainTextEdit.setFont(font)
    def on_chkBoxUnder_clicked(self):
        checked=self.__ui.checkBox.isChecked()
        font = self.__ui.plainTextEdit.font()
        font.setUnderline(checked)
        self.__ui.plainTextEdit.setFont(font)

    def on_chkBoxItalic_clicked(self):
        checked=self.__ui.checkBox_3.isChecked()
        font=self.__ui.plainTextEdit.font()
        font.setItalic(checked)
        self.__ui.plainTextEdit.setFont(font)

    def do_setTextColor(self):
        plet=self.__ui.plainTextEdit.palette() #获取palette
        if(self.__ui.radioButton.isChecked()):
            plet.setColor(QPalette.Text,Qt.black)
        elif(self.__ui.radioButton_2.isChecked()):
            plet.setColor(QPalette.Text,Qt.red)
        elif(self.__ui.radioButton_3.isChecked()):
            plet.setColor(QPalette.Text,Qt.blue)
        self.__ui.plainTextEdit.setPalette(plet)

if __name__ == "__main__": #用于当前窗体测试
    app = QApplication(sys.argv) # 创建app,用QApplication类
    myWidget = QmyDialog() #创建窗体
    myWidget.show()
    sys.exit(app.exec_())

效果如下所示:

image

代码里用到了QPalette、Qt、pyqtSlot等类或函数,所以需要用import语句从相应的模块导入。

image

提示 为了与connectSlotsByName()自动关联的槽函数区别,本章中自定义槽函数的函数名一律使用“do_”作为前缀。当然,这只是个人习惯的命名规则。

现在运行程序myDialog.py,整个窗体的所有功能都实现了。

posted @ 2022-06-05 17:30  司砚章  阅读(757)  评论(0编辑  收藏  举报