代码改变世界

PyQt GUI--信号与槽

2016-02-27 14:38  云物互联  阅读(448)  评论(0编辑  收藏  举报

目录

前言

PyQt中的信号和槽,就是一个触发和执行的关系。

系统软件

  • 系统
    • Win 10
  • 软件
    • Python 3.4.3
    • IPython 4.0.0
    • PyCharm 5
    • PyQt 4

GUI的主循环

在理解信号和槽之前,首先先了解GUI的实现过程。
GUI程具有事件驱动的特性,当一个GUI程序完成了初始化启动后,就会进入一个”服务器式”的无限循环中。在PyQt中使用QtGui.QApplication.exec_()作为进入主循环的标志。进入了主循环后,在这个循环周期中GUI程序会等待事件、处理事件、然后返回等待下一个事件。这通常是最后一行代码,一旦进入了主循环,GUI程序从此获得了控制权。自此之后,GUI程序的所有动作都交由callback来处理。而Qt中的信号和槽就是GUI程序中事件触发和callback之间的通信机制。极大的简化了指针调用在GUI程序中的复杂实现。

信号与槽

信号(Signal)和槽(Slot)是一种高级接口,应用于QObject间的通信。在GUI开发中,窗口控件会有一个回调函数(callback)用于响应因其自身的状态改变而触发的动作。i.e.:当QObject的状态被改变,信号就由该QObject发射(emit)出去,而且为了做到信息封装,QObject并不关注发送之后的事情。 槽用于接收信号并执行动作,是一个对象的成员方法,能够直接被调用。同样的,槽也并不关注有哪个信号与自己连接。信号和槽能够进行任意数量和任意类型的参数传递,但信号和槽的参数个数与类型必须一致,并且槽的参数类型不能为缺省参数。
信号Signal
信号都可以被QObject包含,当QObject的状态发生改变时(e.g. Button被Clicked、QWidget被Clicked),QObject指定的信号就会被发射。而且信号自身并不知道那个槽会接收自己,QObject只管发射信号。当一个信号被发射时,与其连接的槽将被立刻执行,就象一个正常的函数调用一样。
只有当与信号连接的所有槽都返回了以后,信号发射函数(emit)才会被返回。 如果存在多个槽与某个信号连接,当这个信号被发射时,这些槽将会无序的逐一执行,它们执行的顺序将会是随机的、不确定的,不能人为设定。在PyQt的窗体控件类中已经有很多内置的信号,当然你也可以自己定义信号。
槽:
在PyQt中的槽就是一个经过装饰器@QtCore.pyqtSlot()处理过的成员方法。槽唯一的特殊性就是可以与多个信号连接。当与其连接的信号被发射出来时,这个槽就会接受信号并被调用。
信号与槽连接的方式:
1)多个信号可以与单个的槽连接
2)单个信号也可以与多个的槽进行连接
3)一个信号可以与另外一个信号连接,这时无论第一个信号什么时候发射,系统都将立刻再发射与之关联的第二个信号。
连接的状态:
1)可以能会直接连接(同步,一对一)或排队等待连接(异步,多对一)
2)连接可能会跨线程
3)信号可能会断开
总结:
1. 类型安全:只有参数匹配的信号与槽才可以连接成功(信号的参数可以更多,槽会忽略多余的参数)。
2. 线程安全:通过借助QT自已的事件机制,信号槽支持跨线程并且可以保证线程安全。
3. 松耦合:信号不关心有哪些或者多少个对象与之连接;槽不关心自己连接了哪些对象的哪些信号。这些都不会影响何时发出信号或者信号如何处理。
4. 信号与槽是多对多的关系:一个信号可以连接多个槽,一个槽也可以用来接收多个信号。

Example:

提示窗口QtGui.QMessageBox()类的原型:
QMessageBox.information(QWidget, str, str, int, int button1=0, int button2=0) -> int
from PyQt4 import QtGui, QtCore
app = QtGui.QApplication([])
w = QtGui.QWidget()
def showMsg():
    QtGui.QMessageBox.information(w, "Tip", "ok")     #QtGui.QMessageBox.information(QWidget,Title,information)弹出提示窗口,能够设定提示信息和按钮
btn = QtGui.QPushButton("Click", w)   
w.connect(btn, QtCore.SIGNAL("clicked()"), showMsg)
w.show()
app.exec_()
#Button和MessageBox都是是以w作为父窗口。并且Button和MessageBox做了信号与槽的连接,当call Button.clicked()信号时,发送信号。并由作为槽的MessageBox接受信号,再做出动作。

这个例子还可以有这一种写法(调用连接方法的另一个方式):

from PyQt4 import QtGui, QtCore

app = QtGui.QApplication([])

w = QtGui.QWidget()

def showMsg():
    QtGui.QMessageBox.information(w, u"信息", u"ok")

btn = QtGui.QPushButton(u"点我", w)

btn.clicked.connect(showMsg)   # 也可以使用这种方法来连接信号与槽,并且逻辑更加的清晰。w.connect(btn, QtCore.SIGNAL("clicked()"), showMsg)。这是类似于 JQuery 响应事件的方式

app.exec_()

在上面的两个例子中,连接信号clicked()与callback showMsg。这是使用成员方法作为槽的例子,在实际的项目中这并不是一个理想的写法。
这里写图片描述

信号的应用

使用控件类的内建信号

PushButton:
class QPushButton(QAbstractButton)
| QPushButton(QWidget parent=None)
| QPushButton(str, QWidget parent=None)
| QPushButton(QIcon, str, QWidget parent=None)

from PyQt4.QtGui import *  
from PyQt4.QtCore import *  
import sys  

app=QApplication(sys.argv)                       
b=QPushButton("Hello Kitty!")       #QPushButton(str, QWidget parent=None)  str参数设定ButtonText                    
b.show()                         
app.connect(b,SIGNAL("clicked()"),app,SLOT("quit()"))   #return bool;使用内建的clicked()信号对象
app.exec_()  #调用QApplication的exec_()方法,程序进入消息循环,等待可能的输入并进行响应。Qt完成事件处理及显示的工作,并在应用程序退出时返回exec_()的值。
###解析:
#class QApplication(QGuiApplication)创建app对象,所有Qt图形化应用程序都必须包含QApplication()类,它包含了Qt GUI的各种资源,基本设置,控制流以及事件处理等。
#采用sys.argv作为QApplication()的实参,是为了便于程序处理命令行参数。
#sys.argv[]用来获取命令行参数,sys.argv[0]表示程序文件本身(路径)
#创建了一个QPushButton对象,并设置它的显示文本为“Hello Kitty!”,由于此处并没有指定按钮的父窗体,因此自己作为顶层窗口对象。
#show(...) --> QWidget.show()  调用show()方法,显示此主窗口
#connect方法是Qt最重要的特征,即信号与槽的机制。当按钮被按下则触发clicked信号,与之相连的槽(PyQt4.QtGui.QApplication的实例化对象app的quit()成员方法)会接收clisked()信号,执行退出应用程序的操作
#部件是分层的,每一个小部件都可以依赖于父层(主窗口)之上,也可以自己作为主窗口
#当自己作为主窗口的时候可以call show()方法,本质也是call QWidget.show()

自定义信号

信号类
class pyqtSignal(builtins.object)
| pyqtSignal(*types, name=str) -> signal
不定长形参*types指定信号需要传递的参数类型,PyQt中的Signal可以接受任意Python数据类型。

class MyWidget(QWidget):  
    Signal_NoParameters = PyQt4.QtCore.pyqtSignal()                                  # 无参数信号  
    Signal_OneParameter = PyQt4.QtCore.pyqtSignal(int)                               # 一个参数(整数)的信号  
    Signal_OneParameter_Overload = PyQt4.QtCore.pyqtSignal([int],[str])              # 一个参数(整数或者字符串)重载版本的信号  
    Signal_TwoParameters = PyQt4.QtCore.pyqtSignal(int,str)                          # 二个参数(整数,字符串)的信号  
    Signal_TwoParameters_Overload = PyQt4.QtCore.pyqtSignal([int,int],[int,str])     # 二个参数([整数,整数]或者[整数,字符串])重载版本的信号  

Example1

from PyQt4 import QtGui, QtCore

class MyButton(QtGui.QPushButton):         #将自定义信号封装(绑定)到Class MyButton
    myclicked = QtCore.pyqtSignal()        #pyqtSignal(*types, name=str) -> signal创建自定义信号对象myclicked,具有成员方法emit()

    def __init__(self, *args, **kwargs):         #重写基类构造器,如果不重写将会自动继承QPushButton.__init__(self)
        QtGui.QPushButton.__init__(self, *args, **kwargs)     #在重写了基类构造器之后,需要显式写出,基类构造器才会被调用   
        self.connect(self, QtCore.SIGNAL("clicked()"), self.myclicked.emit)  #相当于内建信号与自定义信号的连接,在每次call clicked()后就会调用自定义信号的发射方法emit来发送信号。(self.myclicked.emit)      
app = QtGui.QApplication([])
w = QtGui.QWidget()

def showMsg():
    QtGui.QMessageBox.information(w, u"信息", u"ok")        #弹出信息提示窗口,使用callback作为槽。以QWidget作为父层控件

btn = MyButton(u"点我", w)      #创建Button对象btn,并将clicked()信号映射成为myclicked()信号(信号之间的连接)绑定在btn对象中。
w.connect(btn, QtCore.SIGNAL("myclicked()"), showMsg)     #将自定义信号与槽连接,QObject btn含有自定义信号myclicked。在触发clicked()事件后,btn发射myclicked()信号,callback showMsg执行动作。
w.show()
app.exec_()

为了发射自定义的信号, 需要对QPushButton进行封装, 实例化创建button对象时自动绑定myclicked()信号 。封装QPushButton让他在收到点击信号的同时发送myclicked()信号。在实际项目中,对信号进行封装是一件很有必要的事情,能够让整个项目更加的模块化和易于维护。
Example2:另外一种写法

from PyQt4 import QtGui, QtCore

class MyButton(QtGui.QPushButton):
    def __init__(self, *args, **kwargs):
        QtGui.QPushButton.__init__(self, *args, **kwargs)
        self.connect(self, QtCore.SIGNAL("clicked()"), self.emitClicked)  #self.connect(self, QtCore.SIGNAL("clicked()"), self.emit(QtCore.SIGNAL("myclicked()")))   

    def emitClicked(self):
        self.emit(QtCore.SIGNAL("myclicked()"))   #QtCore.SIGNAL(str) -> str 发送一个str作为信号


app = QtGui.QApplication([])
w = QtGui.QWidget()

def showMsg():
    QtGui.QMessageBox.information(w, u"信息", u"ok")


btn = MyButton(u"点我", w)
w.connect(btn, QtCore.SIGNAL("myclicked()"), showMsg)
w.show()

app.exec_()

注意:上述两种写法效果是一样的,但是实现的本质却不一样。
Example1中定义了一个新的signalObject,并且通过call signalObject.emit()来发送信号对象自身。
Example2中没有定义新的signalObject,而是连接了信号clicked()和”槽”(使用成员方法代替槽)emitClicked(),最后通过”槽”将信号(str)发射出去。
这里写图片描述

带参数的信号

信号可以传递参数给槽,但需要注意的是只有参数匹配的信号与槽才可以连接成功(信号的参数可以更多,槽会忽略多余的参数)。
控件类原型:

In [14]: help(QtGui.QPushButton.__init__)
Help on wrapper_descriptor:

__init__(self, /, *args, **kwargs)
    Initialize self.  See help(type(self)) for accurate signature.

Example1:

from PyQt4 import QtGui, QtCore

class MyButton(QtGui.QPushButton):
    myclicked = QtCore.pyqtSignal(int)               #自定义了一个带参数的信号对象myclicked,e.g. Signal_OneParameter = PyQt4.QtCore.pyqtSignal(int)

    def __init__(self, _id, *args, **kwargs):         #构造器,决定了实例化对象时的需要传递的实参数目。*args、**kwargs为不定长形参,以序列、字典的形式吸收实参冗余。_id为私有属性
        QtGui.QPushButton.__init__(self, *args, **kwargs)
        self._id = _id
        self.connect(self, QtCore.SIGNAL("clicked()"), self.emitMyclicked)   #self.connect(self, QtCore.SIGNAL("clicked()"), self.myclicked.emit(self._id))

    def emitMyclicked(self):
        self.myclicked.emit(self._id)         

app = QtGui.QApplication([])
w = QtGui.QWidget()
w.resize(100, 100)

def showMsg(_id):
    QtGui.QMessageBox.information(w, u"信息", u"查看 %d" % _id)     #接收到来自信号的参数


btn = MyButton(1, u"查看1", w)                  
w.connect(btn, QtCore.SIGNAL("myclicked(int)"), showMsg)        #声明信号是带有int类型参数的,并将信号和参数都发送给槽。信号和槽的参数类型必须一致,且数量最好相等。
btn2 = MyButton(2, u"查看2", w)
btn2.move(0, 30)
w.connect(btn2, QtCore.SIGNAL("myclicked(int)"), showMsg)

w.show()
app.exec_()

另一种写法

from PyQt4 import QtGui, QtCore

class MyButton(QtGui.QPushButton):
    def __init__(self, _id, *args, **kwargs):
        self._id = _id
        QtGui.QPushButton.__init__(self, *args, **kwargs)

        self.connect(self, QtCore.SIGNAL("clicked()"), self.emitClicked)

    def emitClicked(self):
        self.emit(QtCore.SIGNAL("myclicked(int)"), self._id)     #发送信号(str)并且指定需要传递的参数的值self._id


app = QtGui.QApplication([])

w = QtGui.QWidget()
w.resize(100, 100)

def showMsg(_id):
    QtGui.QMessageBox.information(w, u"信息", u"查看 %d" % _id)


btn = MyButton(1, u"查看1", w)
w.connect(btn, QtCore.SIGNAL("myclicked(int)"), showMsg)

btn2 = MyButton(2, u"查看2", w)
btn2.move(0, 30)
w.connect(btn2, QtCore.SIGNAL("myclicked(int)"), showMsg)
w.show()

app.exec_()

这里写图片描述

Example2

# coding=gbk
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class MyWidget(QWidget):                              #封装QWidget
    OnClicked = pyqtSignal([int,int],[int,str])       #自定义带有两个参数的Signal,可以传递任一组合类型实参
    def __init__(self, parent=None):                  #构造方法
        super(MyWidget,self).__init__(parent)            #调用MyWidget类的基类的构造方法,super()用于调用父类函数的成员方法

    def mousePressEvent(self, event):            #重写QtGui.QWidget.mousePressEvent(QMouseEvent)鼠标点击方法;行为:根据点击鼠标主窗口的方式来发射带有不同参数的自定义信号发射自定义信号
        if event.button() == Qt.LeftButton:           #QtCore.Qt.LeftButton ;QMouseEvent.button() -> Qt.MouseButton
            self.OnClicked.emit(event.x(),event.y())      #QMouseEvent.x() -> int;获取点击的坐标;发送两个Int类型参数
            event.accept()
        elif event.button() == Qt.RightButton:
            self.OnClicked[int,str].emit(event.x(),str(event.y()))    #发送一个Int类型参数,一个String类型参数
            event.accept()
        else:
            super(MyWidget,self).mousePressEvent(self, event)         #调用基类的mousePressEvent(self, event)方法

def OnValueChanged_int(x,y):          #充当槽的方法,接收两个Int类型的实参
    print("左键(%d,%d)" % (x,y))

def OnValueChanged_string(szX,szY):   #充当槽的方法,接收一个Int类型一个String类型实参
    print('右键(' + str(szX) + ',' + szY + ')')

app = QApplication(sys.argv)
widget = MyWidget()
widget.show()
widget.OnClicked.connect(OnValueChanged_int,Qt.QueuedConnection)  #将自定义的Signal OnClicked和槽OnValueChanged_int连接
widget.OnClicked[int,str].connect(OnValueChanged_string,Qt.QueuedConnection)
sys.exit(app.exec_())

槽的应用

创建槽

QtCore.pyqtSlot()函数返回一个装饰器用于装饰 QObject 的方法, 使之成为一个槽
定义槽

class MyWidget(QWidget):  
    ...  
    @PyQt4.QtCore.pyqtSlot()  
    def setValue_NoParameters(self):   
        '''无参数槽方法'''  
        pass  
    @PyQt4.QtCore.pyqtSlot(int)  
    def setValue_OneParameter(self,nIndex):   
       '''一个参数(整数)槽方法'''  
        pass  
    @PyQt4.QtCore.pyqtSlot(str)  
    def setValue_OneParameter_String(self,szIndex):   
       '''一个参数(字符串)的槽方法'''  
        pass  
    @PyQt4.QtCore.pyqtSlot(int,int)  
    def setValue_TwoParameters(self,x,y):   
       '''二个参数(整数,整数)槽方法'''  
        pass  
    @PyQt4.QtCore.pyqtSlot(int,str)  
    def setValue_TwoParameters_String(self,x,szY):   
       '''二个参数(整数,字符串)槽方法'''  
        pass  

Example:

from PyQt4 import QtGui, QtCore

class MainWidget(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        btn = QtGui.QPushButton(u"点我", self)             #创建一个Button对象
        self.connect(btn, QtCore.SIGNAL("clicked()"),self,QtCore.SLOT("onClicked()"))  #连接信号QtCore.SIGNAL("clicked()"),和槽QtCore.SLOT("onClicked()")

    @QtCore.pyqtSlot()    #装饰器;通过@PyQt4.QtCore.pyqtSlot装饰方法定义槽函数
    def onClicked(self):  #通过装饰器的处理,onClicked(self)成员方法就成为了一个槽,可以接收多个信号的连接
        QtGui.QMessageBox.information(self, u"信息", u"由槽弹出")


app = QtGui.QApplication([])
m = MainWidget()
m.show()
app.exec_()
#槽,可以简单理解为一个经过装饰器处理的方法。

上面这个例子跟之前的例子的表现结果没有任何区别,但是这个例子中的信号连接了真正的槽函数,而且这个槽可以被多个信号连接。

信号和槽的连接

连接类的原型

connect(...) 
    QObject.connect(QObject, SIGNAL(), QObject, SLOT(), Qt.ConnectionType=Qt.AutoConnection) -> bool
    QObject.connect(QObject, SIGNAL(), callable, Qt.ConnectionType=Qt.AutoConnection) -> bool
    QObject.connect(QObject, SIGNAL(), SLOT(), Qt.ConnectionType=Qt.AutoConnection) -> bool

连接信号和槽:
调用的方式:Signal.connect/QApplication.connect/QObject.connect/QObject.Signal.connect

app = QApplication(sys.argv)   
widget = MyWidget()   
widget.show()   
widget.Signal_NoParameters.connect(self.setValue_NoParameters,Qt.QueuedConnection)                                          # 连接无参数信号  
widget.Signal_OneParameter.connect(self.setValue_OneParameter,Qt.QueuedConnection)                                          # 连接一个参数(整数)信号  
widget.Signal_OneParameter_Overload[int].connect(self.setValue_OneParameter,Qt.QueuedConnection)                            # 连接一个参数(整数)重载版本信号  
widget.Signal_OneParameter_Overload[str].connect(self.setValue_OneParameter_String,Qt.QueuedConnection)                     # 连接一个参数(整数)重载版本信号  
widget.Signal_TwoParameters.connect(self.setValue_TwoParameters,Qt.QueuedConnection)                                        # 连接二个参数(整数)信号  
widget.Signal_TwoParameters_Overload[int,int].connect(self.setValue_TwoParameters,Qt.QueuedConnection)                      # 连接二个参数(整数,整数)重载版本信号  
widget.Signal_TwoParameters_Overload[int,str].connect(self.setValue_TwoParameters_String,Qt.QueuedConnection)               # 连接二个参数(整数,字符串)重载版本信号  

最后

在这篇博文中,主要记录了在PyQt中的信号和槽的定义和基本概念。还有在网上找的几个例子来加深对概念的理解,下一次我们结合QT Designer来实现一些窗体的小部件。

:- )