1. 程式人生 > 程式設計 >python GUI庫圖形介面開發之PyQt5訊號與槽機制、自定義訊號基礎介紹

python GUI庫圖形介面開發之PyQt5訊號與槽機制、自定義訊號基礎介紹

訊號和槽機制是 QT 的核心機制,要精通 QT 程式設計就必須對訊號和槽有所瞭解。訊號和槽是一種高階介面,應用於物件之間的通訊,它是 QT 的核心特性,也是 QT 區別於其它工具包的重要地方。

在linux、windows等 GUI 工具包中,GUI元件都會註冊回撥函式用於處理元件所觸發的動作,通常是註冊對應的函式的函式指標。在之前關於Button的文章中提到了訊號與槽的機制的使用,通過該機制可以很好的將元件的訊號(如button的clocked、toggled、pressed等)和處理該訊號的槽關聯起來。通過 訊號與槽機制,能夠讓我們很簡潔和快速的來完成相關的功能。

訊號和槽是用來在物件間傳遞資料的方法:當一個特定事件發生的時候,signal會被emit出來,slot呼叫是用來響應相應的signal的。Qt中物件已經包含了許多預定義的 signal(基本元件都有各自特有的預定義的訊號),根據使用的場景我們可以新增新的signal。Qt的物件中已經包含了許多預定義的槽函式,但我們也根據使用的場景新增新的槽函式。

訊號

當物件的狀態發生改變的時候,訊號就由該物件發射 (emit) 出去。當一個訊號被髮射(emit)時候,與其關聯的槽函式被立刻執行。其中該物件只負責傳送訊號,發射該訊號的物件並不知道是那個物件在接收這個訊號。這樣保證了物件與物件之間的低耦合。

如果存在訊號和多個槽函式相關聯的時候,當訊號被髮射時,這些槽的執行順序將會是隨機的、不確定的。

用於接受訊號,而且槽只是普通的物件成員函式。當和槽連線的訊號被髮射時,槽會被呼叫。一個槽並不知道是否有任何訊號與自己相連線。

訊號和槽的繫結

通過呼叫 QObject 物件的 connect 函式來將某個物件的訊號與另外一個物件的槽函式相關聯,這樣當發射者發射訊號時,接收者的槽函式將被呼叫。該函式的定義如下::

connect(slot[,type=PyQt5.QtCore.Qt.AutoConnection[,no_receiver_check=False]])

Parameters: 

slot – the slot to connect to,either a Python callable or another bound signal.

type – the type of the connection to make.

no_receiver_check – suppress the check that the underlying C++ receiver instance still exists and deliver the signal anyway.

當訊號與槽沒有必要繼續保持關聯時,我們可以使用 disconnect 函式來斷開連線。其定義如下:

disconnect([slot])

Parameters: slot – the optional slot to disconnect from,either a Python callable or another bound signal. If it is omitted then all slots connected to the signal are disconnected.

訊號和槽的特點

1、一個訊號可以連線到多個槽;

當訊號發出後,槽函式都會被呼叫,但是呼叫的順序是隨機的,不確定的。

self.slider.valueChanged.connect(self.pBar.setValue) 

self.slider.valueChanged.connect(self.lcdNumber.display)

QSlider資料的變化同時繫結在setValue()和display()兩個槽上。

2、多個訊號可以連線到同一個槽;

其中任何一個訊號發出,槽函式都會被執行。

self.buttonOn.clicked.connect(self.showMessage)

self.buttonOff.clicked.connect(self.showMessage)

showMessage()同時繫結在兩個button的clicked訊號上

3、訊號的引數可以是任何的Python型別;

如list,dict等python獨有的型別。自定義訊號的時候舉例說明。

4、訊號和槽的連線可以被移除;

比如斷開某個特定訊號的關聯。

self.buttonOn.clicked.connect(self.showMessage)

5、訊號可以和另外一個訊號進行關聯;

第一個訊號發出後,第二個訊號也同時傳送。比如關閉系統的訊號發出之後,同時會發出儲存資料的訊號。

程式碼示例:

關於訊號和槽的式樣程式碼如下:

#-*- coding:utf-8 -*-
'''
Signal & Slot
'''
__author__ = 'Tony Zhu'

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QWidget,QLCDNumber,QSlider,QGridLayout,QLabel,QHBoxLayout,QGroupBox,QVBoxLayout,QApplication,QProgressBar,QPushButton,QMessageBox)


class SignalSlot(QWidget):

  def __init__(self):
    super(SignalSlot,self).__init__()   
    self.initUI()


  def initUI(self):

    self.controlsGroup = QGroupBox("執行樣本")
    self.lcdNumber = QLCDNumber(self)
    self.slider = QSlider(Qt.Horizontal,self)
    self.pBar = QProgressBar(self)
    vbox = QVBoxLayout()
    vbox.addWidget(self.pBar)
    vbox.addWidget(self.lcdNumber)
    vbox.addWidget(self.slider)
    self.controlsGroup.setLayout(vbox)

    controlsLayout = QGridLayout()
    self.label1 = QLabel("儲存狀態:")
    self.saveLabel = QLabel()
    self.label2 = QLabel("執行狀態:")
    self.runLabel = QLabel()
    self.buttonSave = QPushButton("儲存")
    self.buttonRun = QPushButton("執行")
    self.buttonStop = QPushButton("停止")
    self.buttonDisconnect = QPushButton("解除關聯")
    self.buttonConnect = QPushButton("繫結關聯")

    controlsLayout.addWidget(self.label1,0)
    controlsLayout.addWidget(self.saveLabel,1)
    controlsLayout.addWidget(self.label2,1,0)
    controlsLayout.addWidget(self.runLabel,1)
    controlsLayout.addWidget(self.buttonSave,2,0)
    controlsLayout.addWidget(self.buttonRun,1)
    controlsLayout.addWidget(self.buttonStop,2)
    controlsLayout.addWidget(self.buttonDisconnect,3,0)
    controlsLayout.addWidget(self.buttonConnect,1)

    layout = QHBoxLayout()
    layout.addWidget(self.controlsGroup)
    layout.addLayout(controlsLayout)
    self.setLayout(layout)

    self.buttonRun.clicked.connect(self.buttonSave.clicked)
    self.slider.valueChanged.connect(self.pBar.setValue)
    self.slider.valueChanged.connect(self.lcdNumber.display)
    self.buttonSave.clicked.connect(self.showMessage)
    self.buttonRun.clicked.connect(self.showMessage)
    self.buttonDisconnect.clicked.connect(self.unbindConnection)
    self.buttonConnect.clicked.connect(self.bindConnection)
    self.buttonStop.clicked.connect(self.stop)

    self.setGeometry(300,500,180)
    self.setWindowTitle('訊號和槽')

  def showMessage(self):
    if self.sender().text() == "儲存":
      self.saveLabel.setText("Saved")
    elif self.sender().text() == "執行":
      self.saveLabel.setText("Saved")
      self.runLabel.setText("Running")

  def unbindConnection(self):
    self.slider.valueChanged.disconnect()
  def bindConnection(self):
    self.slider.valueChanged.connect(self.pBar.setValue)
    self.slider.valueChanged.connect(self.lcdNumber.display)
  def stop(self):
    self.saveLabel.setText("")
    self.runLabel.setText("")

if __name__ == '__main__':

  app = QApplication(sys.argv)
  ex = SignalSlot()
  ex.show()
  sys.exit(app.exec_())

程式執行的結果:

python GUI庫圖形介面開發之PyQt5訊號與槽機制、自定義訊號基礎介紹

控制元件說明:

控制元件型別 控制元件名稱 作用
QLCDNumber lcdNumber 顯示slider滑動之後的資料
QProgressBar pBar 顯示slider滑動之後的資料(百分比資料)
QSlider slider 滑動塊調整資料
QPushButton buttonSave 在saveLabel顯示儲存的狀態”Saved”
QPushButton buttonRun 在runLabel顯示執行的狀態”Running”
QPushButton buttonDisconnect 解除slider.valueChanged訊號的繫結
QPushButton buttonConnect 連線slider.valueChanged訊號的繫結
QPushButton buttonStop 清除saveLabel和runLabel的資訊

示例說明:

程式樣本執行的介面邏輯,先設定執行的程式樣本數量,然後先儲存後執行的邏輯狀態。通過slider的滑動來改變progressBar和LCD的顯示資料;“儲存”按鈕儲存執行的樣本;“執行”按鈕執行程式樣本;“解除關聯”解除slider.valueChanged訊號的繫結,此時slider的滑動,不會改變progressBar和LCD的顯示

示例說明:

L22~30:

self.controlsGroup = QGroupBox("執行樣本")
self.lcdNumber = QLCDNumber(self)
self.slider = QSlider(Qt.Horizontal,self)
self.pBar = QProgressBar(self)
vbox = QVBoxLayout()
vbox.addWidget(self.pBar)
vbox.addWidget(self.lcdNumber)
vbox.addWidget(self.slider)
self.controlsGroup.setLayout(vbox)

例項化一個QGroupBox,在其中新增QSlider,QProgressBar,QLCDNumber控制元件。

L32~41:

controlsLayout = QGridLayout()
self.label1 = QLabel("儲存狀態:")

.....

self.buttonDisconnect = QPushButton("解除關聯")
self.buttonConnect = QPushButton("繫結關聯")

例項化,介面中右半部分的控制元件。

L58~65:

self.buttonRun.clicked.connect(self.buttonSave.clicked)
self.slider.valueChanged.connect(self.pBar.setValue)
self.slider.valueChanged.connect(self.lcdNumber.display)
self.buttonSave.clicked.connect(self.showMessage)
self.buttonRun.clicked.connect(self.showMessage)
self.buttonDisconnect.clicked.connect(self.unbindConnection)
self.buttonConnect.clicked.connect(self.bindConnection)
self.buttonStop.clicked.connect(self.stop)

signal和slot進行繫結。

1、一個訊號繫結多個槽:

self.slider.valueChanged.connect(self.pBar.setValue)

self.slider.valueChanged.connect(self.lcdNumber.display)

slider控制元件的valueChanged訊號,同時與QProgressBar的setValue(),QLCDNumber的display()槽函式繫結,當valueChanged訊號觸發的時候,這兩個槽函式均會被呼叫。

2、多個訊號繫結到一個槽:

self.buttonSave.clicked.connect(self.showMessage)

self.buttonRun.clicked.connect(self.showMessage)

buttonSave和buttonRun這兩個物件的clicked訊號,同時繫結到showMessage()這個槽函式。無論哪一個訊號被觸發,showMessage()這個槽函式均會被呼叫。

3、訊號和槽的連線可以被移除:

self.buttonDisconnect.clicked.connect(self.unbindConnection)

當buttonDisconnect訊號觸發之後,與其關聯的槽函式unbindConnection()中就會執行disconnect()方法,如下:

def unbindConnection(self):
self.slider.valueChanged.disconnect()

其中執行disconnect()的時候可以指定解除與某個特定的slot槽的關聯,比如self.slider.valueChanged.disconnect(self.pBar.setValue),此時解除和QProgressBar的setValue()的關聯;或者不指定,在不指定slot的場景下這樣將解除和這個訊號所有關聯的槽。

4、訊號與訊號的關聯:

self.buttonRun.clicked.connect(self.buttonSave.clicked)

在示例說明中提到,在執行之前要對樣本進行儲存,所以為了保證執行的時候執行了儲存的操作,所以將buttonRun.clicked訊號和buttonSave.clicked訊號關聯起來。

示例中在沒有執行“儲存”(buttonSave)的時候,執行“執行”(buttonRun),此時由於兩個物件的clicked訊號已經關聯,所以buttonSave的clicked同樣會執行。

PyQt5自定義訊號

PyQt5已經自動定義了很多QT內建的訊號。但是在實際的使用中為了靈活使用訊號與槽機制,我們可以根據需要自定義signal。可以使用pyqtSignal()方法定義新的訊號,新的訊號作為類的屬性。

自定義signal說明:

pyqtSignal()方法原型(PyQt官網的定義):

PyQt5.QtCore.pyqtSignal(types[,name[,revision=0[,arguments=[]]]])

Create one or more overloaded unbound signals as a class attribute.

Parameters: 

types – the types that define the C++ signature of the signal. Each type may be a Python type object or a string that is the name of a C++ type. Alternatively each may be a sequence of type arguments. In this case each sequence defines the signature of a different signal overload. The first overload will be the default.

name – the name of the signal. If it is omitted then the name of the class attribute is used. This may only be given as a keyword argument.

revision – the revision of the signal that is exported to QML. This may only be given as a keyword argument.

arguments – the sequence of the names of the signal's arguments that is exported to QML. This may only be given as a keyword argument.

Return type:    an unbound signal

新的訊號應該定義在QObject的子類中。新的訊號必須作為定義類的一部分,不允許將訊號作為類的屬性在類定義之後通過動態的方式進行新增。通過這種方式新的訊號才能自動的新增到QMetaObject類中。這就意味這新定義的訊號將會出現在Qt Designer,並且可以通過QMetaObject API實現內省。

通過下面的例子,瞭解一下關於signal的定義:

from PyQt5.QtCore import QObject,pyqtSignal

class NewSignal(QObject):

  # 定義了一個“closed”訊號,該訊號沒有引數據
  closed= pyqtSignal()

  # 定義了一個"range_changed"訊號,該訊號有兩個int型別的引數
  range_changed = pyqtSignal(int,int,name='rangeChanged')

自定義訊號的發射,通過emit()方法類實現,具體參見該函式的原型:

emit(*args)

Parameters: args – the optional sequence of arguments to pass to any connected slots.

通過下面的例子,瞭解一下關於emit()的使用:

from PyQt5.QtCore import QObject,pyqtSignal

class NewSignal(QObject):

  # 一個valueChanged的訊號,該訊號沒有引數.
  valueChanged = pyqtSignal()

  def connect_and_emit_valueChanged(self):
    # 繫結訊號和槽函式
    self.valueChanged.connect(self.handle_valueChanged)

    # 發射訊號.
    self.trigger.emit()

  def handle_valueChanged(self):
    print("trigger signal received")

示例說明:

自定義訊號的一般流程如下:

  1. 定義訊號
  2. 定義槽函式
  3. 繫結訊號和槽
  4. 發射訊號

通過程式碼示例來了解一下訊號的自定義過程:

#-*- coding:utf-8 -*-
'''
defined Signal
'''
__author__ = 'Tony Zhu'
import sys
from PyQt5.QtCore import pyqtSignal,QObject,Qt,pyqtSlot
from PyQt5.QtWidgets import QWidget,QCheckBox,QSpinBox,QComboBox,QGridLayout


class SignalEmit(QWidget):
  helpSignal = pyqtSignal(str)
  printSignal = pyqtSignal(list)
  #宣告一個多過載版本的訊號,包括了一個帶int和str型別引數的訊號,以及帶str引數的訊號
  previewSignal = pyqtSignal([int,str],[str])
  def __init__(self):
    super().__init__()    
    self.initUI()


  def initUI(self):      

    self.creatContorls("列印控制:")
    self.creatResult("操作結果:")

    layout = QHBoxLayout()
    layout.addWidget(self.controlsGroup)
    layout.addWidget(self.resultGroup)
    self.setLayout(layout)

    self.helpSignal.connect(self.showHelpMessage)
    self.printSignal.connect(self.printPaper)
    self.previewSignal[str].connect(self.previewPaper)
    self.previewSignal[int,str].connect(self.previewPaperWithArgs) 
    self.printButton.clicked.connect(self.emitPrintSignal)
    self.previewButton.clicked.connect(self.emitPreviewSignal)

    self.setGeometry(300,300,290,150)
    self.setWindowTitle('defined signal')
    self.show()

  def creatContorls(self,title):
    self.controlsGroup = QGroupBox(title)
    self.printButton = QPushButton("列印")
    self.previewButton = QPushButton("預覽")
    numberLabel = QLabel("列印份數:")
    pageLabel = QLabel("紙張型別:")
    self.previewStatus = QCheckBox("全屏預覽")
    self.numberSpinBox = QSpinBox()
    self.numberSpinBox.setRange(1,100)
    self.styleCombo = QComboBox(self)
    self.styleCombo.addItem("A4")
    self.styleCombo.addItem("A5")

    controlsLayout = QGridLayout()
    controlsLayout.addWidget(numberLabel,0)
    controlsLayout.addWidget(self.numberSpinBox,1)
    controlsLayout.addWidget(pageLabel,2)
    controlsLayout.addWidget(self.styleCombo,3)
    controlsLayout.addWidget(self.printButton,4)
    controlsLayout.addWidget(self.previewStatus,0)
    controlsLayout.addWidget(self.previewButton,1)
    self.controlsGroup.setLayout(controlsLayout)

  def creatResult(self,title):
    self.resultGroup = QGroupBox(title)
    self.resultLabel = QLabel("")
    layout = QHBoxLayout()
    layout.addWidget(self.resultLabel)
    self.resultGroup.setLayout(layout)

  def emitPreviewSignal(self):
    if self.previewStatus.isChecked() == True:
      self.previewSignal[int,str].emit(1080," Full Screen")
    elif self.previewStatus.isChecked() == False:
      self.previewSignal[str].emit("Preview")

  def emitPrintSignal(self):
    pList = []
    pList.append(self.numberSpinBox.value ())
    pList.append(self.styleCombo.currentText())
    self.printSignal.emit(pList)

  def printPaper(self,list):
    self.resultLabel.setText("Print: "+"份數:"+ str(list[0]) +" 紙張:"+str(list[1]))

  def previewPaperWithArgs(self,style,text):
    self.resultLabel.setText(str(style)+text)

  def previewPaper(self,text):
    self.resultLabel.setText(text)     

  def keyPressEvent(self,event):

    if event.key() == Qt.Key_F1:
      self.helpSignal.emit("help message")

  def showHelpMessage(self,message):
    self.resultLabel.setText(message)
    #self.statusBar().showMessage(message)


if __name__ == '__main__':

  app = QApplication(sys.argv)
  dispatch = SignalEmit()
  sys.exit(app.exec_())

執行該函式之後的效果如下:

python GUI庫圖形介面開發之PyQt5訊號與槽機制、自定義訊號基礎介紹

示例說明:

通過一個模擬列印的介面來詳細說明一下關於訊號的自定義,在列印的時候可以設定列印的分數,紙張型別,觸發“列印”按鈕之後,將執行結果顯示到右側;通過全屏預覽QCheckBox來選擇是否通過全屏模式進行預覽,將執行結果顯示到右側。

通過點選F1快捷鍵,可以顯示helpMessage資訊。

程式碼分析:

L12~15:

helpSignal = pyqtSignal(str)
printSignal = pyqtSignal(list)
#宣告一個多過載版本的訊號,包括了一個帶int和str型別引數的訊號,以及帶str引數的訊號
previewSignal = pyqtSignal([int,[str])

通過pyqtSignal()定義了三個訊號,helpSignal ,printSignal ,previewSignal 。其中:

  • helpSignal 為str引數型別的訊號;
  • printSignal 為list引數型別的訊號;
  • previewSignal為一個多過載版本的訊號,包括了一個帶int和str型別引數的訊號,以及str類行的引數。

L31~36:

self.helpSignal.connect(self.showHelpMessage)

self.printSignal.connect(self.printPaper)

self.previewSignal[str].connect(self.previewPaper) 

self.previewSignal[int,str].connect(self.previewPaperWithArgs)

self.printButton.clicked.connect(self.emitPrintSignal)

self.previewButton.clicked.connect(self.emitPreviewSignal)

繫結訊號和槽;著重說明一下多過載版本的訊號的繫結,previewSignal有兩個版本previewSignal(str),previewSignal(int,str)。由於存在兩個版本,從因此在繫結的時候需要顯式的指定訊號和槽的繫結關係。

具體如下:

self.previewSignal[str].connect(self.previewPaper)

self.previewSignal[int,str].connect(self.previewPaperWithArgs)

其中[str]引數的previewSignal訊號繫結previewPaper();[int,str]的previewSignal訊號繫結previewPaperWithArgs()

L72~76:

  def emitPreviewSignal(self):
    if self.previewStatus.isChecked() == True:
      self.previewSignal[int," Full Screen")
    elif self.previewStatus.isChecked() == False:
      self.previewSignal[str].emit("Preview")

多過載版本的訊號的發射也需要制定對應發射的版本,類似同訊號的版定。

L78~82:

  def emitPrintSignal(self):
    pList = []
    pList.append(self.numberSpinBox.value ())
    pList.append(self.styleCombo.currentText())
    self.printSignal.emit(pList)

如程式碼中所示,在訊號發射的時候可以傳遞python資料型別的引數,在本例中傳遞list型別的引數pList.

L93~96:

  def keyPressEvent(self,event):
    if event.key() == Qt.Key_F1:
      self.helpSignal.emit("help message")

通過複寫keyPressEvent()方法,將F1快捷鍵進行功能的拓展。在windows的大部分應用,我們都會使用一些快捷鍵來快速的完成某些特定的功能。比如F1鍵,會快速調出幫助介面。那我們就可以複寫keyPressEvent()方法來模擬傳送所需的訊號,來完成我們的對應任務.

注意事項:

  • 自定義的訊號在init()函式之前定義;
  • 自定義型號可以傳遞,str、int、list、object、float、tuple、dict等很多型別的引數;
  • 注意signal和slot的呼叫邏輯,避免signal和slot之間出現死迴圈。如在slot方法中繼續發射該訊號;

更多關於python GUI庫圖形介面開發之PyQt5文章請檢視下面的相關連結