PyQt/PySide2 中的 信号与槽 (pyqtSignal/Signal) 、多线程 (QThread) 和 定时器 (Timer)

本文深入解析PyQt5和PySide2在多线程处理、信号与槽机制上的应用与区别,通过具体示例展示如何使用线程类、信号与槽以及定时器进行GUI应用程序的开发。

写在前面

PyQt5 和 PySide2 的区别

他们背后原理的差别我就不细说了(我也不知道),你只要记住使用上基本差不多就行,网上搜索他们用法的时候,以哪个为关键词搜索都行吧,官网 给出了他们的差异,聚焦我们要讲的问题,在信号与槽机制和多线程机制上,他们的差别如下:

# PyQt5 引入线程类和信号
from PyQt5.QtCore import QThread, pyqtSignal

# PySide2 引入线程类和信号
from PySide2.QtCore import QThread, Signal # 注意区别就在于这里的信号是 Signal,和 PyQt5 不一样,而线程类是一样的

信号与槽

信号是一个载体,装着自定义类型的数据(例如下面的object),将数据传送到绑定(connect)的函数(连接槽)中(数据作为参数传入函数)

信号与槽的基本框架如下:

signal = pyqtSignal(object) # 自定义信号传送的数据类型为 object

signal.emit(object) # 发射信号 object,每发射一次,连接槽函数就执行一次

signal.connect(custom_function) # 信号绑定的连接槽函数

# 自定义的连接槽函数,传入的参数就是信号
def custom_function(object):
    pass

线程类

线程类用于实现函数并行执行,假如我在动态的画函数曲线的同时想显示已经画了多长时间,这个情况在串行下就不好实现,因为要等到函数曲线画完才能开始执行下面的函数,就不能实现同步了。

而且 PyQt5/PySide2 不支持 python 的多线程类 threading,会报错 QObject: Cannot create children for a parent that is in a different thread. 。

线程类的基本框架如下:

class NewThread(QThread):

    signal = pyqtSignal(object) # 自定义信号,其中 object 为信号承载数据的类型

    def __init__(self, parent=None):
        super().__init__()
        self.x = 0 # 线程中自定义变量

    # 线程内可自定义其他函数
    def custom_function(self):
		pass
    
    # new_thread = NewThread()
    # 通过 new_thread.start() 调用此 run() 函数
    def run(self):
        self.custon_function()
        self.signal.emit(self.x) # 发射信号
        
new_thread = NewThread()

new_thread.start() # 为线程分配资源,让它执行

# 下面两个都是停止执行,但我一般用第二个
new_thread.wait()
new_thread.terminate()

定时器

定时器顾名思义就是一个计时的东西,按照指定的时间间隔执行一次指定函数。

定时器的基本框架如下:

timer = QTimer() # 生成一个定时器

timer.timeout.connect(custom_function) # 将定时器和自定义函数绑定,每隔下面指定时间执行一次自定义函数。注意它一般在生成定时器时就指定了,不要和 start 同时调用,否则会绑定多个自定义函数,导致每隔一定时间同时执行多个绑定函数(我就是因为这个 bug 才被迫学了其他两个骚操作,奇怪现象在最后一个参考上,果然踩坑的不止我一个)

timer.start(interval) # interval 是没两次执行的时间间隔,interval 的单位是毫秒,例如 interva=1000 即间隔时间为1秒

timer.stop() # 停止定时器

示例

下面是我结合了上面3个工具写的小玩意儿:

在这里插入图片描述

左边是计时器,点击“开始”按钮,然后按钮变成了“结束”,左边开始计时,右边同时画出函数图像,运行过程中点击“结束”则结束运行。其中 timer 间隔为1秒,即每隔1秒刷新一次左侧界面,输出时间值。

完整示例代码

import sys
import pyqtgraph as pg # 画图的工具,安装方法:pip install pyqtgraph
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QThread, pyqtSignal, QTimer
import numpy as np

class PlotSin(QThread):

    signal = pyqtSignal(object) # 定义信号,self.y 是 numpy.array,所以信号数据类型为 object

    def __init__(self, parent=None):
        super().__init__()
        self.y = None
        self.phase = 0

    def sin(self):
        self.x = np.arange(0, 3.0, 0.01)
        self.y = np.sin(2 * np.pi * self.x + self.phase)
        self.phase += 0.1
        QThread.msleep(200) # 等待200毫秒

    def run(self):
        for _ in range(300):
            self.sin()
            self.signal.emit(self.y) # 向连接槽发射信号 self.y

class PlotSin_MainWindow(QDialog):

    def __init__(self):
        super().__init__()        
        self.initUI()
        self.clock_time = 0
        self.timer = QTimer(self) # 生成定时器
        self.timer.timeout.connect(self.clock) # 绑定计时函数 self.clock

    def initUI(self):           

        self.creatContorls("时间显示:")
        self.creatResult("函数绘制:")

        layout = QHBoxLayout()
        layout.addWidget(self.controlsGroup)
        layout.addWidget(self.resultGroup)
        self.setLayout(layout)
        self.beginButton.clicked.connect(self.clock_begin)
        self.setGeometry(300, 300, 600, 300)
        self.setWindowTitle('Plot Sine')
        self.show()

    def creatContorls(self,title):
        self.controlsGroup = QGroupBox(title)
        self.beginButton  = QPushButton("开始")
        numberLabel = QLabel("运行时间:")
        self.clockLabel = QLabel("")
        controlsLayout = QGridLayout()
        controlsLayout.addWidget(numberLabel, 0, 0)
        controlsLayout.addWidget(self.clockLabel, 0, 1)
        controlsLayout.addWidget(self.beginButton, 3, 0)
        self.controlsGroup.setLayout(controlsLayout)

    def creatResult(self,title):
        self.resultGroup = QGroupBox(title)
        self.guiplot = pg.PlotWidget()
        gridLayout = QGridLayout()
        gridLayout.addWidget(self.guiplot,0,2,2,3)
        self.resultGroup.setLayout(gridLayout)

    def clock_begin(self):
        if not self.timer.isActive():
            self.recorder_thread = PlotSin() 
            self.recorder_thread.signal.connect(self.displaySin) # 绑定信号槽函数
            self.recorder_thread.start() # 线程执行
            self.clock()
            self.timer.start(1000)
        else:
            self.beginButton.setText("开始")
            self.clockLabel.setText("")
            self.recorder_thread.terminate() # 终止线程
            self.timer.stop() # 终止定时器
            self.clock_time = 0
            text = str(self.clock_time) + "s"
            self.clockLabel.setText(text)

    def clock(self):
        text = str(self.clock_time) + "s"
        self.clockLabel.setText(text)
        if self.clock_time == 0:
            self.beginButton.setText("结束")
            
        self.clock_time += 1

    def plotSin(self, y):
        self.guiplot.clear()
        self.guiplot.plot(y)

    def displaySin(self, y):
        self.plotSin(y)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = PlotSin_MainWindow()
    window.show()
    sys.exit(app.exec_())

参考

  1. Differences Between PySide and PyQt/zh

  2. PySide/PyQt Tutorial: Creating Your Own Signals and Slots

  3. Pyqt5系列(八)-自定义信号

  4. QT信号槽机制

  5. PyQt5进阶(二)——多线程:QThread & 事件处理

  6. Is it possible to get an array from a thread in PyQt5?

  7. Pyside2 - Unexpected behaviour

### PySide6 多线程 TCP 编程 示例 #### 创建多线程环境下的 TCP 客户端服务端 为了在 PySide6 中实现多线程的 TCP 编程,可以利用 `QThread` 类来管理后台线程中的操作。下面是一个简单的例子展示如何设置一个多线程的服务端客户端。 #### 服务端代码示例: ```python import sys from PySide6.QtCore import QThread, Signal, Slot from PySide6.QtWidgets import QApplication, QMainWindow, QTextEdit, QVBoxLayout, QWidget from PyQt6.QtNetwork import QTcpServer, QTcpSocket, QHostAddress class TcpServer(QTcpServer): message_received = Signal(str) def __init__(self): super().__init__() self.clients = [] @Slot() def incomingConnection(self, socketDescriptor: int): client_socket = QTcpSocket() client_socket.setSocketDescriptor(socketDescriptor) self.clients.append(client_socket) client_socket.readyRead.connect(lambda: self.read_client_data(client_socket)) client_socket.disconnected.connect(lambda: self.remove_client(client_socket)) def read_client_data(self, client_socket): data = client_socket.readAll().data().decode() if data: print(f"Received {data}") self.message_received.emit(data) def remove_client(self, client_socket): if client_socket in self.clients: self.clients.remove(client_socket) class MainWindow(QMainWindow): send_message = Signal(str) def __init__(self): super().__init__() central_widget = QWidget() layout = QVBoxLayout() self.text_edit = QTextEdit(readOnly=True) layout.addWidget(self.text_edit) central_widget.setLayout(layout) self.setCentralWidget(central_widget) server_thread = ServerThread(parent=self) server_thread.start() class ServerThread(QThread): def run(self): tcp_server = TcpServer() if not tcp_server.listen(QHostAddress.Any, 12345): print("Unable to start the server!") return print("Listening...") if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ``` 此部分展示了如何构建一个基本的服务端应用程序,在单独的工作线程中启动并监听来自客户端的消息[^1]。 #### 客户端代码示例: ```python import sys from PySide6.QtCore import QTimer, QObject, Signal, Slot from PySide6.QtWidgets import QApplication, QLineEdit, QPushButton, QVBoxLayout, QWidget from PyQt6.QtNetwork import QTcpSocket, QHostAddress class Client(QObject): connected = Signal(bool) received_message = Signal(str) def __init__(self, parent=None): super().__init__(parent=parent) self.socket = QTcpSocket() self.socket.connected.connect(lambda: self.connected.emit(True)) self.socket.errorOccurred.connect(lambda err: (print(err), self.connected.emit(False))) self.socket.readyRead.connect(self._read_data) def _read_data(self): while self.socket.canReadLine(): line = str(self.socket.readLine(), encoding='utf8').strip('\n') self.received_message.emit(line) def connect_to_host(self, address="127.0.0.1", port=12345): self.socket.connectToHost(address, port) def disconnect_from_host(self): self.socket.disconnectFromHost() def write(self, msg): self.socket.write(msg.encode('utf-8')) self.socket.flush() class MainWidget(QWidget): def __init__(self): super().__init__() layout = QVBoxLayout() self.line_edit = QLineEdit() button_send = QPushButton("Send") layout.addWidget(self.line_edit) layout.addWidget(button_send) self.client = Client() button_send.clicked.connect( lambda: ( self.client.write(self.line_edit.text()), self.line_edit.clear(), ) ) self.client.received_message.connect(print) self.client.connect_to_host() self.setLayout(layout) if __name__ == '__main__': app = QApplication([]) widget = MainWidget() widget.setWindowTitle("TCP Client") widget.resize(300, 100) widget.show() timer = QTimer.singleShot(0, lambda: None) # Ensure event loop starts immediately. exit_code = app.exec() sys.exit(exit_code) ``` 这段程序实现了客户端的功能,它可以在主线程之外发送消息给服务器,并接收响应数据。
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值