【问题标题】:Understanding threading and PyQT了解线程和 PyQT
【发布时间】:2017-05-15 02:21:23
【问题描述】:

我正在尝试在 PyQT GUI 中实现线程,但遇到了麻烦。一些背景,我有一个独立的脚本卸载了一些软件,删除了一些文件夹,然后重新安装了一个更新的版本。我使用线程模块删除文件夹,为每个文件夹启动一个新线程。有几个文件夹很大并且需要一些时间,所以我会遍历单独的线程,但会跳过较大的文件夹并加入线程:

    thread = threading.Thread(name=portalDirToDelete,target=deleteFolder,args=(portalDirToDelete,))
    thread.start()
    ....
    for thread in threading.enumerate():
      if not "MainThread" in thread.getName() and not "content" in thread.getName() and not "temp" in thread.getName():
        thread.join()

一旦我开始使用 PyQT4 制作 UI,我发现线程在我尝试加入它们之前不会启动。我做了一些阅读并了解到使用线程模块不再起作用,(How to keep track of thread progress in Python without freezing the PyQt GUI?)所以我开始研究 QThreads,但运气不佳。以下是我的其他脚本正在执行的简化版本:

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'Samples\ThreadUI1.ui'
#
# Created by: PyQt4 UI code generator 4.11.4
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui
import shutil, os, time

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

try:
    _encoding = QtGui.QApplication.UnicodeUTF8
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig)

class workerThread(QtCore.QObject):

    finished = QtCore.pyqtSignal()

    def deleteFolder(self,path):
        if os.path.exists(path):
            shutil.rmtree(path)
        self.finished.emit()

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName(_fromUtf8("Form"))
        Form.resize(115, 66)
        self.runApp = QtGui.QPushButton(Form)
        self.runApp.setGeometry(QtCore.QRect(20, 20, 75, 23))
        self.runApp.setObjectName(_fromUtf8("runApp1"))

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

        self.runApp.clicked.connect(lambda:self.runSetups())

    def retranslateUi(self, Form):
        Form.setWindowTitle(_translate("Form", "Form", None))
        self.runApp.setText(_translate("Form", "Run App", None))

    def usingMoveToThread(self,path):
        self.app = QtCore.QCoreApplication([])
        self.objThread = QtCore.QThread()
        self.obj = workerThread()
        self.obj.moveToThread(self.objThread)
        self.obj.finished.connect(self.objThread.quit)
        self.objThread.started.connect(self.obj.deleteFolder(path))
        self.objThread.finished.connect(app.exit)
        self.objThread.start()

    def runSetups(self):
        self.usingMoveToThread(r"C:\arcgisportal")
        for x in range(1,11):
            print(x)
            time.sleep(1)

if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    Form = QtGui.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

我的想法可能是错误的,但我的方法是在新线程中启动删除文件夹的过程,然后查看它是否继续循环打印。我一直使用Threading and PyQT 作为主要指南。关于子类化 QThread 似乎有不同的意见,所以我只是去了这个博客,(https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/)不这样做。

我使用的是 Python 2.7、PyQt4 4.11.4,这个应用是通过 UI Designer 制作的。我知道更新生成的 ui/py 文件不是一个好主意,我在其他地方没有这样做,只是将此示例作为上下文提供。

感谢任何帮助!

更新

如果我打开一个模态 QMessageBox() 并添加一个关闭 QMessageBox 的按钮,当它打开时,较大文件夹中的文件夹将被删除。一旦我关闭 QMessageBox(),文件夹就不再被删除,所以我对线程和 UI 有一些不清楚的地方。

【问题讨论】:

    标签: python multithreading pyqt


    【解决方案1】:

    您所写的内容存在几个问题(希望我已经全部发现,我不愿意在我的机器上运行与删除文件相关的代码!)

    1. 您正在usingMoveToThread 方法中构造一个新的QCoreApplication。每个 python 实例应该只有一个QApplication。我不确定这样做会做什么,但我怀疑它解释了您在创建模态对话框时看到的行为。创建QApplication(或QCoreApplication)会启动主要的Qt事件循环,它处理从处理信号/调用槽到处理窗口重绘的所有事情。您在主线程中创建了第二个事件循环,但没有通过调用exec_() 启动它。我怀疑当您创建对话框时,这是在调用exec_()(或创建第三个事件循环,它以某种方式解决了由第二个事件循环创建的问题)。 无论如何,没有理由手动创建第二个事件循环。主线程可以很好地处理线程。(注意:每个 pyqt 应用程序可以有多个事件循环,例如线程可以有一个事件循环(见下文),对话框有时有自己的事件循环。但是,Qt 为您处理创建!)

    2. runSetups 中的循环将阻塞主线程,因此您的 GUI 将无响应。这不应该影响线程,但它引出了一个问题,如果您只是要阻止 GUI,将其卸载到线程有什么意义!请注意,Python GIL 无论如何都会阻止多个线程同时运行,因此如果您希望多个线程同时删除单独的文件夹,您需要研究多处理来避免 Python GIL。然而,这可能是过早的优化。可能存在磁盘 I/O 考虑因素,您可能会发现其中一种方法可以提供任何有意义的加速。

    3. 主要问题是以下行:self.objThread.started.connect(self.obj.deleteFolder(path))。您实际上是在主线程中运行deleteFolder(path),并将该函数的返回值传递给connect 方法。这导致线程中没有运行任何内容,并且主线程中发生了所有事情(并阻塞了 GUI)。出现此错误是因为您想将参数传递给deleteFolder 方法。但是你不能因为你没有发出started 信号(Qt 做到了),所以你不能提供参数。通常,您可以通过在lambdapartial 中封装您的方法来解决此问题,但这会在使用线程时带来其他问题(请参阅here)。相反,您需要在 Ui_Form 对象中定义一个新信号,该对象接受 str 参数,将该信号连接到您的 deleteFolder 插槽并在启动线程后手动发出信号。

    类似这样的:

    class Ui_Form(object):
        deleteFolder = QtCore.pyqtSignal(str)
    
        ...
    
        def usingMoveToThread(self,path):
            self.objThread = QtCore.QThread()
            self.obj = workerThread()
            self.obj.moveToThread(self.objThread)
            self.deleteFolder.connect(self.obj.deleteFolder)
            self.objThread.start()
            self.deleteFolder.emit(path)
    

    您可能想查看this 问题以了解有关此方法的更多信息(基本上,Qt 可以处理不同线程中的信号/插槽之间的连接,因为每个线程都有自己的事件循环)。

    【讨论】:

    • 非常感谢您的帮助,真的帮助我了解我做错了什么!让我试一试,如果我有任何问题,请回来。为了回应您的一些 cmets,我不太关心阻止 GUI。该应用程序的目的是卸载并重新安装具有最新版本的软件堆栈。一旦运行,我实际上隐藏了主 GUI 并打开一个新对话框来报告进度。关于多个线程的好点,我从未仔细查看是否每个文件夹都被同时删除。再次感谢您的帮助!
    • 如果我有兴趣隐藏主 GUI 并打开另一个对话框来记录进度,您是否建议我重新分类 runSetups 以便我可以在新的 QThread 中启动它?然后我只是将进度传递给打开的新对话框?
    • 您可能想要继承QDialog 并将runSetups 放入该子类中。然后你只需从你的主窗口实例化你的对话框,隐藏主窗口,并在子类中调用一个显示对话框并启动线程的方法。然后可以在子类中建立线程和对话框进度条之间的连接,然后您将拥有一个模块化对话框,其中所有内容都是独立的
    • 那么既然QDialog是从主窗口实例化的,那么它会在主窗口的线程中运行,不会因为runSetups而被阻塞?然后我使用自定义信号来更新有关当前进度的对话框?您为什么建议继承 QDialog 而不是将其保留在主类(或 Ui_Form 类)中,隐藏主窗口并打开对话框?
    【解决方案2】:
    # your app initialization:
    # application = QApplication(sys.argv)
    # main_window.show()
    # application.exec_()
    ...
    thread = threading.Thread(name=portalDirToDelete,target=deleteFolder,args=(portalDirToDelete,))
        thread.start()
        ....
        for thread in threading.enumerate():
          if not "MainThread" in thread.getName() and not "content" in thread.getName() and not "temp" in thread.getName():
            while thread.is_alive():
                application.processEvents()
    

    【讨论】:

    • 也许我遗漏了一些东西,但我不希望整个脚本在删除较大的文件夹时挂起,我希望它在后台运行。如果我在每个循环之后调用 processEvents(),我会看到文件夹从较大的文件夹中被删除,但它似乎只有几个。我假设它只能在 processEvents() 调用期间删除文件夹。
    • 我想补充一点,根据stackoverflow.com/q/1595649/1994235,不建议在 PyQt 应用程序中使用 Python 线程
    • 所以GUI冻结对您来说不是问题吗?你只想使用多线程?
    • 我使用 Python 线程和 PyQt5,我没有任何问题。但是你必须使用 Qt Signals 与主线程进行通信。但它是一个伪线程(阅读 GIL)。
    猜你喜欢
    • 2011-04-20
    • 2012-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-01
    • 2013-07-24
    • 2021-06-29
    相关资源
    最近更新 更多