《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方法将自定义信号发射出去,这样槽函数就会执行,打印出“信号发射成功”字符串。

 

运行截图如下:

《PyQt5高级编程实战》自定义信号详解_第1张图片

 

2. 让自定义信号携带值

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

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方法发射出去就行了。

 

运行截图如下:

《PyQt5高级编程实战》自定义信号详解_第2张图片

 

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

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方法一并发射出去。

 

运行截图如下:

《PyQt5高级编程实战》自定义信号详解_第3张图片

 

除了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_())

运行截图如下:

《PyQt5高级编程实战》自定义信号详解_第4张图片

 

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

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

 

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

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. 将信号与槽函数进行连接,注意这里一定要明确所连接信号的重载类型。

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

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

 

运行截图如下:

《PyQt5高级编程实战》自定义信号详解_第5张图片

 

4. 窗口间通信

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

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

 

运行截图如下:

《PyQt5高级编程实战》自定义信号详解_第6张图片

 

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

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. 在程序入口处将窗口一实例传给窗口二。

 

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

 

5. 线程间通信

在用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槽函数也就会启动。槽函数中的代码相信读者都可以看懂,笔者这里就不再赘述。

 

运行截图如下:

开启线程:

《PyQt5高级编程实战》自定义信号详解_第7张图片

线程运行中:

《PyQt5高级编程实战》自定义信号详解_第8张图片

线程运行结束:

《PyQt5高级编程实战》自定义信号详解_第9张图片

你可能感兴趣的:(《PyQt5高级编程实战》,PyQt5,自定义信号,信号与槽)