自定义信号详解
-
创建自定义信号
-
让自定义信号携带值
-
自定义信号的重载版本
-
窗口间通信
-
线程间通信
PyQt5中各个控件自带的信号已经能够让我们完成许多需求,但是如果想要更加个性化的功能,我们还得通过自定义信号来实现。在本节,笔者会详细介绍如何来自定义一个信号,并通过该方法来实现窗口间的通信以及线程间通信。
如果对信号的基础用法还不是很了解的读者,可以先去阅读下《快速掌握PyQt5》第二章 信号与槽。
- 创建自定义信号
下面是一个简单的自定义信号使用例子:
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方法将自定义信号发射出去,这样槽函数就会执行,打印出“信号发射成功”字符串。
运行截图如下:
- 让自定义信号携带值
但是如果我们想知道鼠标双击点的横坐标呢?可以通过信号一并发送过来吗?当然可以,请看下面这个例子:
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_())
-
首先在实例化pyqtSignal对象时传入一个int参数,表明我们这个信号会携带一个整型值,而这个值将会被槽函数接收。
-
给槽函数添加一个接收参数x,并在函数内部打印该值。
-
在获取到横坐标后,通过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_())
-
在实例化pyqtSignal时加多一个int参数,表明这个信号会一共会携带两个整型值。
-
修改槽函数接收参数数量。
-
首先获取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_())
运行截图如下:
- 自定义信号的重载版本
假如用户提出了一个这样的需求,并且规定要用到自定义信号:当鼠标按下时,打印出横坐标;当鼠标释放时,打印出横坐标和纵坐标。当然,我们可以在鼠标按下和释放事件中直接打印出相关信息,没必要使用自定义信号。
读者可能会想到创建两个自定义信号,一个在鼠标按下事件中发射,一个在鼠标释放事件中发射。例子如下:
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_())
- 实例化一个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. 将信号与槽函数进行连接,注意这里一定要明确所连接信号的重载类型。
- 同样,在发射信号时,也要写清楚重载类型。
如果在连接和发射时不写清楚重载类型的话,则默认使用第一种。也就是说self.mouse_signal.connect(self.press_slot)相当于self.mouse_signal[int].connect(self.press_slot)
运行截图如下:
- 窗口间通信
当项目变得稍微复杂起来时,窗口间的通信需求就会出现。我们来看下如何通过自定义信号来实现这一通信功能。
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_())
-
窗口一继承于QTextBrowser,类中有一个槽函数,它会将信号带过来的值添加到窗口上。
-
窗口2实例化了一个自定义信号,该信号会携带一个字符串。每当用户点击发送按钮后,窗口二会将输入框中的文本随信号一同发射出去。
-
在程序入口处,我们实例化了窗口一和窗口二,并将窗口二的信号同窗口一的槽函数连接起来。也就是说,每当用户在窗口二点击了发送按钮后,窗口一就会将输入框的文本显示到自己的界面上。
运行截图如下:
有些读者可能会说这个不用自定义信号也可以实现。确实,请看下面这个例子:
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_())
-
给窗口二的构造函数添加一个参数用于接收窗口一的实例。
-
在窗口二的槽函数中直接调用窗口一示例的append方法添加输入框文本。
-
在程序入口处将窗口一实例传给窗口二。
看起来好像还更加简洁点,但其实这样做会增加了代码的耦合度。如果项目变得庞大复杂起来,那么这样做无非是给后期代码维护埋下隐患。所以,笔者还是强烈建议使用自定义信号的方式来进行通信。
- 线程间通信
在用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_())
-
在子线程中实例化一个自定义信号对象,该信号会携带一个字符串。
-
当子线程运行完毕,会将result这个随机值随信号一同发射出去。
-
在主窗口中实例化一个QTextBrowser控件和一个按钮控件,前者用于显示子线程传递过来的信息,后者用于开启子线程。
-
实例化子线程,并将子线程的自定义信号与child_thread_done_slot槽函数连接起来。每当子线程运行结束,child_signal就会被发射,而child_thread_done_slot槽函数也就会启动。槽函数中的代码相信读者都可以看懂,笔者这里就不再赘述。
运行截图如下:
开启线程:
线程运行中:
线程运行结束: