PyQt - 使用多线程避免界面卡顿

PYQT作为界面程序包,为Pythoner快速构建界面,提供了便利性。特别是结合Pycharm扩展工具(QTdesigner)能够通过“拖拖拽拽”的方式构建简单界面。通过UIC将UI文件快速转化为PY文件,节省了时间。

  PYQT的项目实践,必须参照MVC模式,才能形成多人工作合力,关于MVC的实践,会另起它文进行总结归纳,本文重点总结归纳PYQT项目中,因耗时操作产生界面卡顿现象时,如何通过分线程的方式,解决主界面线程等待造成界面卡顿的问题。

  一、问题的出现

  设计了一个界面,去局域网共享文件夹中查看是否存在文件,若存在,则在textBrowser中显示文件数量,每隔几秒钟执行一次,同时将信息Append到textBrowser中。

  使用os.listdir(target_path)打开共享文件夹时,由于网络故障,待反馈耗时大,由于Ui_MainWindow中的方法都在主线程中进行,造成界面的卡顿。

  二、解决思路

  将耗时操作分离至新线程,主界面保留资源用于交互。查询网络上现有的解决思路,大致如下:利用PYQT中的QThread类,新起一个线程,用来处理共享文件夹查阅的操作,待该线程处理完成后,主线程接收子线程处理结果,append到textBrowser中。

  三、难点

  利用信号与槽的机制,将主、子线程建立关联关系。

  四、如何实现

  1、实例化Ui_MainWindow类的MyThread对象,用于填装子线程处理任务。

  self.my_thread = MyThread()

  2、将事件信号与槽函数建立联系,通过count函数启用线程。

  self.StopButton.clicked.connect(self.CountFilesNum)

  3、Count函数:

  self.textBrowser_2.append("开始检测……")
  self.my_thread.is_on = True
  self.my_thread.start()#启动线程

  4、设计MyThread类,继承Qthread类

  my_signal = pyqtSignal(str)  #

  tipwords = ''
  def __init__(self):
      super(MyThread, self).__init__()
      self.tipwords = ''
      self.is_on = True

  定义一个信号(my_signal)用于向槽函数传递字符型变量(字符会输出到textBrowser中),调用父类的INIT方法,完成初始化设置。

  5、定义线程中的主逻辑函数:

  all_content = os.listdir(target_path)

  ……

  self.my_signal.emit(str(self.tipwords))  

  函数执行完,释放信号。线程中的信号关联到append函数

  self.my_thread.my_signal.connect(self.appendTextBrowser)  

  实现了线程中数据返回主线程的目的。

  五、总结

  除了button中的信号与槽机制外,主、子线程也是通过信号与槽机制完成的关联,可见,信号与槽机制,确实是PYQT的核心。
      在使用pyqt开发界面时,遇到了一种情况,就是在点击按钮之后,响应函数中会启动一个循环,该循环会一直执行,然后就造成界面无响应,如下所示,由于我是在Linux下运行的,所以界面直接显示成灰色(windows应该显示“无响应”):

这是因为对于pyqt来说,界面线程是主线程,如果我们在主线程函数里面调用了一个耗时比较久的循环,可能就会造成主界面线程卡死在循环中,从而造成无法操作主界面或者主界面卡顿、卡死。

所以这种情况下必须使用多线程的方式来解决,即在主界面线程中在启动一个新的子线程,利用该子线程处理比较耗时的操作,然后通过signal-slot机制将子线程的数据反馈到主界面线程中,而且在子线程中不能操作界面。这就是所说的:UI只用来操作UI,子线程只用来处理数据,就是将UI的操作与耗时数据的处理进行分开处理。

在pyqt中,可以通过QThread建立一个线程,

2、使用多线程解决界面卡顿 - 方式1
下面介绍 QThread 的第一种用法:新建一个类 RunThread 继承自 QThread,然后在 RunThread 类中重写 run() 函数,在 run() 函数中进行耗时数据的处理。下面是它的用法:


#!/usr/bin/python
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time


# 继承QThread
class Runthread(QtCore.QThread):
    #  通过类成员对象定义信号对象
    _signal = pyqtSignal(str)
 
    def __init__(self):
        super(Runthread, self).__init__()
 
    def __del__(self):
        self.wait()
 
    def run(self):
 
        for i in range(100):
            time.sleep(0.1)
            self._signal.emit(str(i))  # 注意这里与_signal = pyqtSignal(str)中的类型相同


class Example(QtWidgets.QWidget):
    def __init__(self):
        super(Example, self).__init__()
        # 按钮初始化
        self.button = QtWidgets.QPushButton('开始', self)
        self.button.move(120, 80)
        self.button.clicked.connect(self.start_login)  # 绑定多线程触发事件
 
        # 进度条设置
        self.pbar = QtWidgets.QProgressBar(self)
        self.pbar.setGeometry(50, 50, 210, 25)
        self.pbar.setValue(0)
 
        # 窗口初始化
        self.setGeometry(300, 300, 300, 200)
        self.show()
 
        self.thread = None  # 初始化线程
 
    def start_login(self):
        # 创建线程
        self.thread = Runthread()
        # 连接信号
        self.thread._signal.connect(self.call_backlog)  # 进程连接回传到GUI的事件
        # 开始线程
        self.thread.start()
 
    def call_backlog(self, msg):
        self.pbar.setValue(int(msg))  # 将线程的参数传入进度条
 
 
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myshow = Example()
    myshow.show()
    sys.exit(app.exec_())

 上面的代码建立一个界面,界面中只包含了一个进度条和一个按钮,点击按钮之后,进度条开始运行。

在上面的代码中,新建了一个 RunThread 类,该类继承自 QThread 类,在 RunThread 中重写了 run() 函数,并将耗时处理放在了 run() 函数中,点击按钮之后,触发 start_login() 函数,在start_login() 中,先创建了 RunThread 线程类的对象,然后将该类中的 _signal 信号与 Example 类中的 call_back() 函数建立连接,这样,就可以在run()函数运行时,将运行时的数据传递(异步,因为信号的传递与触发有一定的延时)到主机面 Example 类中并进行显示,如下所示:

3、使用多线程解决界面卡顿 - 方式2
在pyqt中多线程的使用还有另外一种方式:RunThread 类继承自 QObject,而非继承自 QThread。这种方式使用起来比第一种要复杂,但是这种方法将数据的处理与线程的创建与启动分开进行处理,在某些场景下,采用这种方式会比较方便。

下面是第二种方式的代码:

#!/usr/bin/python
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time
 
 
# 继承 QObject
class Runthread(QtCore.QObject):
    #  通过类成员对象定义信号对象
    signal = pyqtSignal(str)
 
    def __init__(self):
        super(Runthread, self).__init__()
        self.flag = True
 
    def __del__(self):
        print(">>> __del__")
 
    def run(self):
        i = 0
        while self.flag:
            time.sleep(1)
            if i <= 100:
                self.signal.emit(str(i))  # 注意这里与_signal = pyqtSignal(str)中的类型相同
                i += 1
        print(">>> run end: ")
 
 
class Example(QtWidgets.QWidget):
    #  通过类成员对象定义信号对象
    _startThread = pyqtSignal()
 
    def __init__(self):
        super(Example, self).__init__()
        # 按钮初始化
        self.button_start = QtWidgets.QPushButton('开始', self)
        self.button_stop = QtWidgets.QPushButton('停止', self)
        self.button_start.move(60, 80)
        self.button_stop.move(160, 80)
        self.button_start.clicked.connect(self.start)  # 绑定多线程触发事件
        self.button_stop.clicked.connect(self.stop)  # 绑定多线程触发事件
 
        # 进度条设置
        self.pbar = QtWidgets.QProgressBar(self)
        self.pbar.setGeometry(50, 50, 210, 25)
        self.pbar.setValue(0)
 
        # 窗口初始化
        self.setGeometry(300, 300, 300, 200)
        self.show()
 
        self.myT = Runthread()          # 创建线程对象
        self.thread = QThread(self)     # 初始化QThread子线程
 
        # 把自定义线程加入到QThread子线程中
        self.myT.moveToThread(self.thread)
 
        self._startThread.connect(self.myT.run)     # 只能通过信号-槽启动线程处理函数
        self.myT.signal.connect(self.call_backlog)
 
    def start(self):
        if self.thread.isRunning():     # 如果该线程正在运行,则不再重新启动
            return
 
        # 先启动QThread子线程
        self.myT.flag = True
        self.thread.start()
        # 发送信号,启动线程处理函数
        # 不能直接调用,否则会导致线程处理函数和主线程是在同一个线程,同样操作不了主界面
        self._startThread.emit()
 
    def stop(self):
        if not self.thread.isRunning():     # 如果该线程已经结束,则不再重新关闭
            return
        self.myT.flag = False
        self.stop_thread()
 
    def call_backlog(self, msg):
        self.pbar.setValue(int(msg))  # 将线程的参数传入进度条
 
    def stop_thread(self):
        print(">>> stop_thread... ")
        if not self.thread.isRunning():
            return
        self.thread.quit()      # 退出
        self.thread.wait()      # 回收资源
        print(">>> stop_thread end... ")
 
 
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myshow = Example()
    myshow.show()
    sys.exit(app.exec_())
 该界面包含了一个进度条、一个开始按钮、一个停止按钮。当点击“开始”按钮之后,进度条会开始运行;当点击“停止”按钮时,进度条会停止运行,如下所示:

其中 RunThread 类是线程处理函数类,该类继承自 QObject,然后通过 moveToThread 函数将该线程处理函数类添加进一个线程中。

在使用这种方式时需要注意一下几点:

self.myT=Runthread();  // 创建线程处理函数类对象,需要注意的是创建时不能指定父对象,要不然moveToThread函数会报错
self._startThread.emit();  // 虽然说,将myT添加进thread线程,并且调用thread.start(),但是,并不能通过直接调用的方式来调用RunThread类中的线程处理函数run(),如果直接调用的话,相当于还是主界面线程在调用,依然会造成主界面卡顿。此时应该使用signal-slot的方式进行调用,即在Example类中声明一个信号_startThread,并通过self._startThread.connect(self.myT.run) 将该信号与 RunThread 类中的线程处理函数建立连接,这样当调用 thread.start() 后,再调用 self._startThread.emit() 函数就可以调用 run() 函数了
self.myT.signal.connect(self.call_backlog);   // 为了获取RunThread类中的处理数据,也只能通过signal-slot的方式进行获取;
当thread调用quit()和wait()函数之后,此时该线程已经停止运行,但是线程处理函数run()还未停止运行,所以,需要在run函数中的循环中添加一个判断标志位,当thread线程停止后,将该标志位置为False,这样退出循环之后run函数就退出了;
当run函数退出之后,此时RunThread类对象myT并没有销毁,因为它是Example类的成员,所以只有当Example销毁时,myT才会销毁;
4、关于connect的连接方式
在QT中,查看connect函数原型:

当然,connect有多重函数重载形式,以上只是其中的一种。其中的第五个参数type指明了signal-slot的连接方式,Qt::ConnectionType有一些几种类型:

前面三种是比较常用的,其中QueuedConnection方式是用在上面多线程的情况下。

QueuedConnection:槽函数所在线程和接收所在线程是一样的;

DirectConnection: 槽函数所在线程和发送者所在线程是一样的;

不过大多数情况下,调用connect是使用默认参数就可以了,当使用默认参数AutoConnection时:

在多线程情况下,默认使用QueuedConnection;

在单线程下,默认使用DirectConnection;

同理,在pyqt中也一样,pyqt中connect函数原型:

使用方式也一样,直接使用默认连接方式就可以了。

其实pyqt和qt差别不大,就只有语言上的差别,使用方式还都是一样的,我一般都是先查qt上资料然后在套用到pyqt上。
 

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PyQt,如果在主界面线程执行耗时操作,会导致界面卡死的问题。这是因为PyQt应用程序是基于事件驱动的,主线程负责处理GUI事件。当有耗时的操作任务时,GUI事件会被阻塞,导致应用程序处于假死状态,无法与应用程序进行交互。为了解决这个问题,可以采用多线程的方式。一种方式是继承自QThread类,将耗时的操作放在子线程进行处理,然后通过signal-slot机制将子线程的数据反馈到主界面线程。需要注意的是,在子线程不能操作界面,只能处理数据。这样可以将UI的操作与耗时数据的处理进行分开处理,避免界面卡死的问题。另一种方式是使用RunThread类继承自QObject,而非继承自QThread。这种方式将数据的处理与线程的创建与启动分开进行处理,适用于某些场景下比较方便。虽然这种方式比较复杂,但在特定情况下可以更灵活地使用多线程。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [PyQt - 使用多线程避免界面卡顿](https://blog.csdn.net/bailang_zhizun/article/details/109240670)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [解决PySide6/PyQT界面卡死问题(PySide6/PyQT多线程](https://blog.csdn.net/weixin_45081575/article/details/130210522)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值