自定义信号详解
创建自定义信号
让自定义信号携带值
自定义信号的重载版本
窗口间通信
线程间通信
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_())
mouse_signal = pyqtSignal([int], [int, str])
mouse_signal = pyqtSignal([int], [float], [tuple])
注意不要将信号重载概念跟一个信号会携带两个值的概念混淆起来:
mouse_signal = pyqtSignal([int], [tuple])
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方法添加输入框文本。
在程序入口处将窗口一实例传给窗口二。
看起来好像还更加简洁点,但其实这样做会增加了代码的耦合度。如果项目变得庞大复杂起来,那么这样做无非是给后期代码维护埋下隐患。所以,笔者还是强烈建议使用自定义信号的方式来进行通信。
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槽函数也就会启动。槽函数中的代码相信读者都可以看懂,笔者这里就不再赘述。
运行截图如下:
开启线程:
线程运行中:
线程运行结束: