《PyQt5高级编程实战》自定义信号详解

自定义信号详解

  1. 创建自定义信号

  2. 让自定义信号携带值

  3. 自定义信号的重载版本

  4. 窗口间通信

  5. 线程间通信

PyQt5中各个控件自带的信号已经能够让我们完成许多需求,但是如果想要更加个性化的功能,我们还得通过自定义信号来实现。在本节,笔者会详细介绍如何来自定义一个信号,并通过该方法来实现窗口间的通信以及线程间通信。

如果对信号的基础用法还不是很了解的读者,可以先去阅读下《快速掌握PyQt5》第二章 信号与槽。

  1. 创建自定义信号
    下面是一个简单的自定义信号使用例子:

import sys
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget

class Demo(QWidget):
my_signal = pyqtSignal()

def __init__(self):
    super(Demo, self).__init__()
    self.my_signal.connect(self.signal_slot)

def signal_slot(self):
    print('信号发射成功')
    
def mouseDoubleClickEvent(self, event):
    self.my_signal.emit()

if name == ‘main’:
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
首先我们实例化一个pyqtSignal对象,然后将其连接到signal_slot槽函数上。每当用户在窗口上双击时,我们就调用emit方法将自定义信号发射出去,这样槽函数就会执行,打印出“信号发射成功”字符串。

运行截图如下:

  1. 让自定义信号携带值
    但是如果我们想知道鼠标双击点的横坐标呢?可以通过信号一并发送过来吗?当然可以,请看下面这个例子:

import sys
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget

class Demo(QWidget):
my_signal = pyqtSignal(int) # 1

def __init__(self):
    super(Demo, self).__init__()
    self.my_signal.connect(self.signal_slot)

def signal_slot(self, x):               # 2
    print('信号发射成功')
    print(x)

def mouseDoubleClickEvent(self, event):
    pos_x = event.pos().x()             # 3
    self.my_signal.emit(pos_x)

if name == ‘main’:
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())

  1. 首先在实例化pyqtSignal对象时传入一个int参数,表明我们这个信号会携带一个整型值,而这个值将会被槽函数接收。

  2. 给槽函数添加一个接收参数x,并在函数内部打印该值。

  3. 在获取到横坐标后,通过emit方法发射出去就行了。

运行截图如下:

那如果我们想把纵坐标也一并发送过来呢?请看下面这个例子:

import sys
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget

class Demo(QWidget):
my_signal = pyqtSignal(int, int) # 1

def __init__(self):
    super(Demo, self).__init__()
    self.my_signal.connect(self.signal_slot)

def signal_slot(self, x, y):                 # 2
    print('信号发射成功')
    print(x)
    print(y)

def mouseDoubleClickEvent(self, event):
    pos_x = event.pos().x()                  # 3
    pos_y = event.pos().y()
    self.my_signal.emit(pos_x, pos_y)

if name == ‘main’:
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())

  1. 在实例化pyqtSignal时加多一个int参数,表明这个信号会一共会携带两个整型值。

  2. 修改槽函数接收参数数量。

  3. 首先获取x和y坐标值,然后通过emit方法一并发射出去。

运行截图如下:

除了int类型,我们还可以让自定义信号携带其他类型的值,包括python语言所支持的值类型和PyQt5自定义的数据类型。:

类型 实例化
整型 pyqtSignal(int)
浮点型 pyqtSignal(float)
复数 pyqtSignal(complex)
字符串 pyqtSignal(str)
布尔型 pyqtSignal(bool)
列表 pyqtSignal(list)
元组 pyqtSignal(tuple)
字典 pyqtSignal(dict)
集合 pyqtSignal(set)
QSize pyqtSignal(QSize)
QPoint pyqtSignal(QPoint)
… …
使用PyQt5自定义的数据类型时,必须先导入相应的模块。

比如要携带QSize类型值,那就必须先进行导入:from PyQt5.QtCore import QSize

我们拿QPoint来举个例子:

import sys
from PyQt5.QtCore import pyqtSignal, QPoint
from PyQt5.QtWidgets import QApplication, QWidget

class Demo(QWidget):
my_signal = pyqtSignal(QPoint)

def __init__(self):
    super(Demo, self).__init__()
    self.my_signal.connect(self.signal_slot)

def signal_slot(self, pos):
    print('信号发射成功')
    print(pos)

def mouseDoubleClickEvent(self, event):
    pos = event.pos()
    self.my_signal.emit(pos)

if name == ‘main’:
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
运行截图如下:

  1. 自定义信号的重载版本
    假如用户提出了一个这样的需求,并且规定要用到自定义信号:当鼠标按下时,打印出横坐标;当鼠标释放时,打印出横坐标和纵坐标。当然,我们可以在鼠标按下和释放事件中直接打印出相关信息,没必要使用自定义信号。

读者可能会想到创建两个自定义信号,一个在鼠标按下事件中发射,一个在鼠标释放事件中发射。例子如下:

import sys
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget

class Demo(QWidget):
press_signal = pyqtSignal(int)
release_signal = pyqtSignal(tuple)

def __init__(self):
    super(Demo, self).__init__()
    self.press_signal.connect(self.press_slot)
    self.release_signal.connect(self.release_slot)

def press_slot(self, x):
    print(x)

def release_slot(self, pos):
    print(pos)

def mousePressEvent(self, event):
    x = event.pos().x()
    self.press_signal.emit(x)

def mouseReleaseEvent(self, event):
    pos_x = event.pos().x()
    pos_y = event.pos().y()
    pos = (pos_x, pos_y)
    self.release_signal.emit(pos)

if name == ‘main’:
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())

但其实我们也可以只用一个自定义信号来实现,借助信号重载方式就可以了:

import sys
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget

class Demo(QWidget):
mouse_signal = pyqtSignal([int], [tuple]) # 1

def __init__(self):
    super(Demo, self).__init__()
    self.mouse_signal[int].connect(self.press_slot)     # 2
    self.mouse_signal[tuple].connect(self.release_slot)

def press_slot(self, x):
    print(x)

def release_slot(self, pos):
    print(pos)

def mousePressEvent(self, event):                       # 3
    x = event.pos().x()
    self.mouse_signal[int].emit(x)

def mouseReleaseEvent(self, event):
    pos_x = event.pos().x()
    pos_y = event.pos().y()
    pos = (pos_x, pos_y)
    self.mouse_signal[tuple].emit(pos)

if name == ‘main’:
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())

  1. 实例化一个pyqtSignal对象,并将参数用中括号[]包住,每一个中括号代表了该信号的一种形式,也就是说mouse_signal一共有两种形式,既可以携带一个整型值,也可以一个元组。笔者这里再举个例子:

mouse_siganl既可以携带一个整型值,也可以同时携带一个整型值和一个字符串

mouse_signal = pyqtSignal([int], [int, str])

mouse_signal可以携带一个整型值或一个浮点型数据或一个元组

mouse_signal = pyqtSignal([int], [float], [tuple])
注意不要将信号重载概念跟一个信号会携带两个值的概念混淆起来:

my_signal有两种形式,一种是可以携带一个整型值,另一种是可以携带一个元组

mouse_signal = pyqtSignal([int], [tuple])

my_signal只有一种形式,它携带一个整型值和一个元组

mouse_signal = pyqtSignal(int, tuple)
2. 将信号与槽函数进行连接,注意这里一定要明确所连接信号的重载类型。

  1. 同样,在发射信号时,也要写清楚重载类型。

如果在连接和发射时不写清楚重载类型的话,则默认使用第一种。也就是说self.mouse_signal.connect(self.press_slot)相当于self.mouse_signal[int].connect(self.press_slot)

运行截图如下:

  1. 窗口间通信
    当项目变得稍微复杂起来时,窗口间的通信需求就会出现。我们来看下如何通过自定义信号来实现这一通信功能。

import sys
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QTextBrowser, QLineEdit, QPushButton, QHBoxLayout

class Window1(QTextBrowser): # 1
def init(self):
super(Window1, self).init()

def show_msg_slot(self, msg):
    self.append(msg)

class Window2(QWidget): # 2
win2_signal = pyqtSignal(str)

def __init__(self):
    super(Window2, self).__init__()
    self.line = QLineEdit()
    self.send_btn = QPushButton('发送')
    self.send_btn.clicked.connect(self.send_to_win1_slot)

    h_layout = QHBoxLayout()
    h_layout.addWidget(self.line)
    h_layout.addWidget(self.send_btn)
    self.setLayout(h_layout)

def send_to_win1_slot(self):
    msg = self.line.text()
    self.win2_signal.emit(msg)

if name == ‘main’: # 3
app = QApplication(sys.argv)

win1 = Window1()
win1.show()

win2 = Window2()
win2.show()
win2.win2_signal.connect(win1.show_msg_slot)

sys.exit(app.exec_())
  1. 窗口一继承于QTextBrowser,类中有一个槽函数,它会将信号带过来的值添加到窗口上。

  2. 窗口2实例化了一个自定义信号,该信号会携带一个字符串。每当用户点击发送按钮后,窗口二会将输入框中的文本随信号一同发射出去。

  3. 在程序入口处,我们实例化了窗口一和窗口二,并将窗口二的信号同窗口一的槽函数连接起来。也就是说,每当用户在窗口二点击了发送按钮后,窗口一就会将输入框的文本显示到自己的界面上。

运行截图如下:

有些读者可能会说这个不用自定义信号也可以实现。确实,请看下面这个例子:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTextBrowser, QLineEdit, QPushButton, QHBoxLayout

class Window1(QTextBrowser):
def init(self):
super(Window1, self).init()

class Window2(QWidget):
def init(self, win1): # 1
super(Window2, self).init()
self.win1 = win1

    self.line = QLineEdit()
    self.send_btn = QPushButton('发送')
    self.send_btn.clicked.connect(self.send_to_win1_slot)

    h_layout = QHBoxLayout()
    h_layout.addWidget(self.line)
    h_layout.addWidget(self.send_btn)
    self.setLayout(h_layout)

def send_to_win1_slot(self):
    msg = self.line.text()
    self.win1.append(msg)                   # 2

if name == ‘main’:
app = QApplication(sys.argv)

win1 = Window1()
win1.show()

win2 = Window2(win1)                        # 3
win2.show()

sys.exit(app.exec_())
  1. 给窗口二的构造函数添加一个参数用于接收窗口一的实例。

  2. 在窗口二的槽函数中直接调用窗口一示例的append方法添加输入框文本。

  3. 在程序入口处将窗口一实例传给窗口二。

看起来好像还更加简洁点,但其实这样做会增加了代码的耦合度。如果项目变得庞大复杂起来,那么这样做无非是给后期代码维护埋下隐患。所以,笔者还是强烈建议使用自定义信号的方式来进行通信。

  1. 线程间通信
    在用PyQt5开发可视化爬虫软件时,我们会将爬取操作放在一个子线程中执行,主线程负责界面更新。当子线程中的爬虫运行结束时,应当通知主线程更新下相应的界面信息,好让用户知道爬取情况。下面我们通过自定义信号来实现这一通信操作:

import sys
import random
from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtWidgets import QApplication, QWidget, QTextBrowser, QPushButton, QVBoxLayout

class ChildThread(QThread):
child_signal = pyqtSignal(str) # 1

def __init__(self):
    super(ChildThread, self).__init__()

def run(self):                      # 2
    result = str(random.randint(1, 10000))
    for _ in range(100000000):
        pass

    self.child_signal.emit(result)

class Demo(QWidget):
def init(self):
super(Demo, self).init()
self.browser = QTextBrowser() # 3
self.btn = QPushButton(‘开始爬取’)
self.btn.clicked.connect(self.start_thread_slot)

    v_layout = QVBoxLayout()
    v_layout.addWidget(self.browser)
    v_layout.addWidget(self.btn)
    self.setLayout(v_layout)

    self.child_thread = ChildThread()   # 4
    self.child_thread.child_signal.connect(self.child_thread_done_slot)

def start_thread_slot(self):
    self.browser.clear()
    self.browser.append('爬虫开启')
    self.btn.setText('正在爬取')
    self.btn.setEnabled(False)
    self.child_thread.start()

def child_thread_done_slot(self, msg):
    self.browser.append(msg)
    self.browser.append('爬取结束')
    self.btn.setText('开始爬取')
    self.btn.setEnabled(True)

if name == ‘main’:
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())

  1. 在子线程中实例化一个自定义信号对象,该信号会携带一个字符串。

  2. 当子线程运行完毕,会将result这个随机值随信号一同发射出去。

  3. 在主窗口中实例化一个QTextBrowser控件和一个按钮控件,前者用于显示子线程传递过来的信息,后者用于开启子线程。

  4. 实例化子线程,并将子线程的自定义信号与child_thread_done_slot槽函数连接起来。每当子线程运行结束,child_signal就会被发射,而child_thread_done_slot槽函数也就会启动。槽函数中的代码相信读者都可以看懂,笔者这里就不再赘述。

运行截图如下:

开启线程:

线程运行中:

线程运行结束:

上一篇:「HNOI2013」消毒


下一篇:PAT刷题之旅 1151-LCA in a Binary Tree-甲级