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

1、问题

在使用pyqt开发界面时,遇到了一种情况,就是在点击按钮之后,响应函数中会启动一个循环,该循环会一直执行,然后就造成界面无响应,如下所示,由于我是在Linux下运行的,所以界面直接显示成灰色(windows应该显示“无响应”):

PyQt - 使用多线程避免界面卡顿_第1张图片

这是因为对于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 类中并进行显示,如下所示:

PyQt - 使用多线程避免界面卡顿_第2张图片

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

 该界面包含了一个进度条、一个开始按钮、一个停止按钮。当点击“开始”按钮之后,进度条会开始运行;当点击“停止”按钮时,进度条会停止运行,如下所示:

PyQt - 使用多线程避免界面卡顿_第3张图片

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

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

  1. self.myT=Runthread();  // 创建线程处理函数类对象,需要注意的是创建时不能指定父对象,要不然moveToThread函数会报错
  2. 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() 函数了
  3. self.myT.signal.connect(self.call_backlog);   // 为了获取RunThread类中的处理数据,也只能通过signal-slot的方式进行获取;
  4. 当thread调用quit()和wait()函数之后,此时该线程已经停止运行,但是线程处理函数run()还未停止运行,所以,需要在run函数中的循环中添加一个判断标志位,当thread线程停止后,将该标志位置为False,这样退出循环之后run函数就退出了;
  5. 当run函数退出之后,此时RunThread类对象myT并没有销毁,因为它是Example类的成员,所以只有当Example销毁时,myT才会销毁;

4、关于connect的连接方式

在QT中,查看connect函数原型:

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

PyQt - 使用多线程避免界面卡顿_第4张图片

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

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

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

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

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

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

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

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

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

你可能感兴趣的:(PyQT,QT,多线程,qt,pyqt)