生命不息
折腾不止

PyQt5信号-槽

PyQt5信号-槽

信号-槽机制

GUI程序一般引入一种事件和信号机制。简单来说,就是一个循环事件,这个循环程序等到某个时刻程序会自动做某些事情比如刷新程序界面,或扫描键盘鼠标之类的,等用户点击鼠标或者按了键盘之后,它会接受这个信号然后做出相应的反应。

close函数就是退出这个循环程序,而调用的app.exec()方法,就是开启这个循环程序。

PyQt4的旧的信号-槽连接语句已经被丢弃,PyQt5不在支持旧的信号-槽连接语句。

act_exit.triggered.connect(self.close)

新的连接语句更加简单易懂。

信号(singal)可以连接无数多个槽(slot),或者没有连接槽也没问题,信号也可以连接其他的信号。正如前面所述,连接的语句:who.singal.connect(slot)。比如说按钮最常用的内置信号triggered,而槽实际上就是函数,比如窗体的self.close方法。

信号就是Qobject的一个属性,PyQt有很多内置信号,你可以自定义信号。信号还没和槽连接起来就是一个属性,只有通过connect方法连接起来,信号-槽机制就建立起来了。类似信号还有disconnect方法和emit方法。disconnect断开信号-槽机制,emit激活那个信号。

PyQt很多内置信号和内置槽将GUI的事件驱动细节给隐藏了,如果自定义信号和槽可能对who.singal.connect(slot)这样简洁的形式如何完成工作的,感觉困惑。下面简单介绍一下:

信号都是类的一个属性,新的辛哈必须继承自QObject,然后由PyQt5.QtCore.pyqtSingal方法创建,这个方法接收的参数中最重要的是types类型,比如:int,bool之类的,可以认为这是信号传递的参数类型,但实际传递这些参数的是emit方法,然后槽实际上就是经过特殊封装的函数,这些函数当然需要接收一些参数或者不接受参数,而这些参数具体的值传进来的是由emit 方法执行的,然后我们通过who.singal.connect(slot)这样的形式将某个信号和某个槽连接起来,who的信号,然后信号类自带的连接方法,然后连接到slot某个函数上,在这里隐藏一个重要细节就是emit方法,比如:定义一个新的信号,需要将点击屏幕的具体x,y坐标发出去,内置的信号-槽将这一机制都完成了,如果要自己定义信号和槽的话,如:pyqtSingal(init, int),发送给func(x,y),具体的x和y的值需要你通过emit(x,y)来发送,至于什么时候发送,已经发送的x,y值的获取,这应该又是另外一个信号-槽机制的细节。

如下实例:

import sys
from PyQt5.QtWidgets import QHBoxLayout, QSlider, QSpinBox, QApplication, QWidget
from PyQt5.QtCore import Qt

app = QApplication(sys.argv)

window = QWidget()
window.setWindowTitle('Test')
spinBox = QSpinBox()
slider = QSlider(Qt.Horizontal)
spinBox.setRange(0, 130)
slider.setRange(0, 130)

spinBox.valueChanged.connect(slider.setValue)
slider.valueChanged.connect(spinBox.setValue)

spinBox.setValue(35)

layout = QHBoxLayout()
layout.addWidget(spinBox)
layout.addWidget(slider)

window.setLayout(layout)
window.show()

sys.exit(app.exec())

自定义信号

自定义信号有PyQt5.QtCore.pyqtSingal方法创建,具体格式如下:

from PyQt5.QtCore import QObject, pyqtSignal

class Foo(QObject):
    closed = pyqtSignal()
    range_changed = pyqtSignal(int, int, name='rangeChanged')

上面Foo类里面定义了一个新的信号,它必须是GObject的子类,然后定义了一个closed信号,没有任何参数。下面是range_changed信号,接受了一个initint类型,然后这个信号的名字是rangeChangedname选项是一个可选项,如果不填,那么信号的名字range_changed

信号还可以overload

注意:信号必须定义为类的属性,同时必须是QObject的子类。

自定义槽

按照python格式自己定义的函数就是所谓的自定义槽了,推荐用pyqt的槽装饰器来定义槽。

from PyQt5.QtCore import pyqtSlot

@pyqtSlot()    #不接受任何参数
def foo(self):
    pass

@pyqtSlot(int, int) #接收两个参数
def foo(self, arg1, arg2):
    pass

@pyqtSlot(int, name='bar') #接收参数,并命名
def foo(self, arg1):
    pass

@pyqtSlot(int, result=int) #接收一个参数,返回一个值
def foo(self, arg1):
    pass

@pyqtSlot(int, QObject) #接收参数和其他窗体类型
def foo(self, arg1):
    pass

发送信号

信号对象有emit方法来发射信号,然后信号对象还有disconnect方法断开某个信号和槽的连接。

一个信号可以连接多个槽,多个信号可以连接同一个槽,一个信号可以与另一个信号连接。

自建信号和自建槽并建立发射机制的情况:

from PyQt5.QtWidgets import QDialog, QLabel, QLineEdit, \
            QCheckBox, QPushButton,QHBoxLayout,QVBoxLayout, QApplication

from PyQt5.QtCore import Qt, pyqtSignal, QObject, pyqtSlot

class FindDialog(QDialog):
    findNext = pyqtSignal(str, Qt.CaseSensitivity) #定义信号,并接受参数,限制大小写
    findPrevious = pyqtSignal(str, Qt.CaseSensitivity)  #定义信号,并接受参数,忽略大小写

    def __init__(self, parent=None):
        super(FindDialog, self).__init__(parent)
        label = QLabel(self.tr('Find &what:'))
        self.lineEdit = QLineEdit()
        label.setBuddy(self.lineEdit)

        self.caseCheckBox = QCheckBox(self.tr('Match &case'))
        self.backwardCheckBox = QCheckBox(self.tr('Search &backward'))
        self.findButton = QPushButton(self.tr('&Find'))
        self.findButton.setDefault(True)
        self.findButton.setEnabled(False)
        closeButton = QPushButton(self.tr('Close'))

        self.lineEdit.textChanged.connect(self.enableFindButton)
        self.findButton.clicked.connect(self.findClicked)
        closeButton.clicked.connect(self.close)

        topLeftLayout = QHBoxLayout()
        topLeftLayout.addWidget(label)
        topLeftLayout.addWidget(self.lineEdit)
        leftLayout = QVBoxLayout()
        leftLayout.addLayout(topLeftLayout)
        leftLayout.addWidget(self.caseCheckBox)
        leftLayout.addWidget(self.backwardCheckBox)
        rightLayout = QVBoxLayout()
        rightLayout.addWidget(self.findButton)
        rightLayout.addWidget(closeButton)
        rightLayout.addStretch()
        mainLayout = QHBoxLayout()
        mainLayout.addLayout(leftLayout)
        mainLayout.addLayout(rightLayout)
        self.setLayout(mainLayout)

        self.setWindowTitle(self.tr('Find'))
        self.setFixedHeight(self.sizeHint().height())

    def enableFindButton(self, text):
        self.findButton.setEnabled(bool(text))

    #定义槽,函数
    @pyqtSlot()
    def findClicked(self):
        text = self.lineEdit.text()
        if self.caseCheckBox.isChecked():
            cs = Qt.CaseSensitive
        else:
            cs = Qt.CaseInsensitive

        if self.backwardCheckBox.isChecked():
            self.findPrevious.emit(text, cs)
        else:
            self.findNext.emit(text, cs)

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    findDialog = FindDialog()
    def find(text, cs):
        print('find', text, 'CS', cs)
    def findp(text, cs):
        print('findp', text, 'CS', cs)

    findDialog.findNext.connect(find)
    findDialog.findPrevious.connect(findp)
    findDialog.show()
    sys.exit(app.exec())

信号-槽机制的反思

在接下来Qt designer这一章也会详细讨论这个问题,我们使用Qt designer来设计和修改ui文件——对应程序中大部分的静态视图元素,主要的目的倒不是为了快速GUI程序编写,其实写代码也挺快的,主要的目的就是为了代码复用。当我们养成习惯,强迫自己程序中的静态视图元素都进入ui文件,这不仅增强了ui文件的复用性,而且也增强了剩下来的python代码的复用性。这其中很大一部分就是这里讨论的信号-槽机制的功劳。

当我们自定义的类加载好ui文件之后,该类里面的代码实际上就剩下两个工作:

把本窗体的信号和槽都编写好
把母窗体和子窗体和信号-槽接口写好。
一般程序的用户互动接口大多在最顶层,也就是用户一般喜欢在菜单栏找到所有可能对程序的控制,这些控制的实现函数如果放在都放在母窗体,那么整个程序的代码复用性会降到最低,而如果我们将这些实现函数分别移到和其视图窗体最紧密的窗体类中,那么不仅代码复用性会大大提高,而且这些槽或函数的编写也会简单很多。那么我们该如何组织这些信号和槽(实现函数)呢?我在这里提出组织学上的一些抽象原则:

  1. 最小组织原则,凡是小组织能够自我实现的功能绝不上传到更大一级的组织中去。
  2. 大组织对小组织元素的某些实现的引用,采用明文引用原则。比如说母窗体中有一个小窗体有一个编辑器,母窗体想要操控这个编辑器执行剪切操作,那么采用明文引用,也就是self.textEdit.cut。
  3. 小组织对大组织属性的引用采用信号激活原则,比如说某个编辑器发生了内容修改,你可以自定义一个信号,该信号为标题修改信号,然后信号触发母窗体的某个方法,这样达到修改母窗体的标题的目的。而在母窗体中,只需要在声明是将小组织的信号和大组织的某个方法连接起来即可。

来源:http://cdwanze.github.io/%E7%94%B5%E8%84%91/python/PyQt5/PyQt5%E5%85%A5%E9%97%A8.html

分享到: 更多 (0)