如何在 PyQT 中使用 QThread

Posted

技术标签:

【中文标题】如何在 PyQT 中使用 QThread【英文标题】:How to QThread in PyQT 【发布时间】:2017-11-13 23:17:23 【问题描述】:

我的代码有很多问题 - 具体来说,是实现某种信号/插槽/线程。我是新手,我在网上阅读的内容并没有太大帮助。

无论如何,我使用 PyQT Designer 制作了一个简单的 GUI。原来是这样的:

from PySide2 import QtCore, QtGui, QtWidgets

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(687, 514)
        self.browse_btn = QtWidgets.QPushButton(Dialog)
        self.browse_btn.setGeometry(QtCore.QRect(20, 440, 61, 27))
        self.browse_btn.setObjectName("browse_btn")
        self.play_btn = QtWidgets.QPushButton(Dialog)
        self.play_btn.setGeometry(QtCore.QRect(100, 440, 61, 27))
        self.play_btn.setObjectName("play_btn")
        self.cur_scene_header = QtWidgets.QTextEdit(Dialog)
        self.cur_scene_header.setGeometry(QtCore.QRect(20, 20, 191, 21))
        self.cur_scene_header.setObjectName("cur_scene_header")
        self.prev_scene_header = QtWidgets.QTextEdit(Dialog)
        self.prev_scene_header.setGeometry(QtCore.QRect(20, 90, 191, 21))
        self.prev_scene_header.setObjectName("prev_scene_header")
        self.pause_btn = QtWidgets.QPushButton(Dialog)
        self.pause_btn.setGeometry(QtCore.QRect(180, 440, 61, 27))
        self.pause_btn.setObjectName("pause_btn")
        self.reset_btn = QtWidgets.QPushButton(Dialog)
        self.reset_btn.setGeometry(QtCore.QRect(260, 440, 61, 27))
        self.reset_btn.setObjectName("reset_btn")
        self.prev_scenes = QtWidgets.QListWidget(Dialog)
        self.prev_scenes.setGeometry(QtCore.QRect(20, 120, 331, 301))
        self.prev_scenes.setObjectName("prev_scenes")
        self.current_scene = QtWidgets.QListWidget(Dialog)
        self.current_scene.setGeometry(QtCore.QRect(20, 50, 331, 31))
        self.current_scene.setObjectName("current_scene")

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

    def retranslateUi(self, Dialog):
        Dialog.setWindowTitle(QtWidgets.QApplication.translate("Dialog", "Dialog", None, -1))
        self.browse_btn.setText(QtWidgets.QApplication.translate("Dialog", "Browse", None, -1))
        self.play_btn.setText(QtWidgets.QApplication.translate("Dialog", "Play", None, -1))
        self.cur_scene_header.sethtml(QtWidgets.QApplication.translate("Dialog", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li  white-space: pre-wrap; \n"
"</style></head><body style=\" font-family:\'Sans Serif\'; font-size:9pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Current Scene</p></body></html>", None, -1))
        self.prev_scene_header.setHtml(QtWidgets.QApplication.translate("Dialog", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li  white-space: pre-wrap; \n"
"</style></head><body style=\" font-family:\'Sans Serif\'; font-size:9pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Previous Scenes</p></body></html>", None, -1))
        self.pause_btn.setText(QtWidgets.QApplication.translate("Dialog", "Pause", None, -1))
        self.reset_btn.setText(QtWidgets.QApplication.translate("Dialog", "Reset", None, -1))

那里没有什么真正重要的。有四个按钮:浏览、播放、重置和暂停。我还没有实现重置和暂停按钮,但我稍后会。

有两个QWidgetList。第一个“curr_scene”(当前场景)显示一个字符串。第二个,“prev_scenes”是先前场景的列表(过去的 current_scenes)。

当用户点击“浏览”按钮时,他们可以使用 TKinter 文件对话模块选择一个文件(一个 csv 文件)。选择后,程序将加载 csv 文件的内容。我想要接下来发生的是它转到(csv 文件的)第一行,将该文本放入 current_scene 列表中,然后等待五秒钟,将该行发送到 previous_scene 列表,然后从 csv 文件中取出下一行,将其放入 current_scene 中,依此类推,直到到达最后一行。

发生的事情现在是我想要的——除了它没有显示。它没有显示在 gui 上发生了这种情况。我只能让 prev_scene 显示文本,或者让 current_scene 显示文本,不能同时使用两者,这是通过使用repaint 完成的。

我知道我必须使用线程。但我真的不知道怎么做。能给我看看么?以下是程序其余部分的代码:

import sys
import PySide2
from PySide2.QtCore import *
from PySide2.QtWidgets import *
import pull_csv_data
import main
import time
from tkinter import filedialog

class MainDialog(QWidget, main.Ui_Dialog):

    def __init__(self, parent = None):
        super(MainDialog, self).__init__(parent)
        self.setupUi(self)

        self.connect(self.browse_btn, SIGNAL("clicked()"), self.browse_for_file)
        self.list = []
        self.connect(self.play_btn, SIGNAL("clicked()"), self.list_items)
        self.csv_path = ""
        self.file_watch = QFileSystemWatcher()

    def browse_for_file(self):
        if not self.list:
            self.csv_path = filedialog.askopenfilename()
            self.list = pull_csv_data.pull_data(self.csv_path)
            self.update_watcher()

    def update_watcher(self):
        self.file_watch.addPath(self.csv_path)
        self.file_watch.connect(self.file_watch, SIGNAL('fileChanged(QString)'), self.update_list)

    def update_list(self):
        print("here1")
        old_list = self.list
        self.list = pull_csv_data.pull_data(self.csv_path)
        print("here2")
        if len(old_list) < len(self.list):
            for i in range(len(old_list), len(self.list)):
                item = QListWidgetItem(self.list[i][0])
                self.prev_scenes.addItem(item)
                print("here6")
        else:
            self.list = pull_csv_data.pull_data(self.csv_path)
            self.prev_scenes.clear()
            for i in self.list:
                item = QListWidgetItem(i[0])
                self.prev_scenes.addItem(item)

    def list_items(self):
        if self.prev_scenes.count() == 0:
            for i in self.list:
                item = QListWidgetItem(i[0])
                self.update_current(item)

    def update_current(self, item):
        self.current_scene.addItem(item)
        time.sleep(0.5)
        self.update_prev(item)
        self.current_scene.clear()


    def update_prev(self, item):
        print("hello")
        self.prev_scenes.addItem(item)
        self.prev_scenes.repaint()
        self.prev_scenes.repaint()



app = QApplication(sys.argv)
form = MainDialog()
form.show()
sys.exit(app.exec_()) 

这里是 pull_csv_data:

def pull_data(file_path):
    king = []
    with open(file_path) as csvDataFile:
        csvReader = csv.reader(csvDataFile)
        for row in csvReader:
            king.append(row)
    return king

【问题讨论】:

什么是 pull_csv_data? 这是一个函数...让我发布它。它只从 csv 文件中提取数据并将其保存在列表中。 如果是函数,请放。 你可以举一个.csv文件的例子 @eyllanesc 不要担心 csv 文件......它不相关。想象一下,您只有一个包含 50 个字符串的列表分配给 self.list。 【参考方案1】:

不建议将执行相同任务的库组合在一起,更糟糕的库可以被阻止为 tkinterQt 并且都创建内部主循环。 Qt 是一个库,它包含许多组件,还可以选择文件:QFileDialog.getOpenFileName()

def browse_for_file(self):
    if not self.list:
        self.csv_path, _ = QFileDialog.getOpenFileName()
        self.list = pull_csv_data.pull_data(self.csv_path)
        self.update_watcher()

另一件不应该做的事情是做诸如sleep()之类的阻塞任务,这些任务不会让GUI工作,每个GUI库都提出了不必使用这些功能的选项,在我们的例子中是QEvenLoopQTimer 是正确的选项此外,您不能将一个项目从一个 QListWidget 分配给另一个,您必须先将其从一个 takeItem() 删除并分配给另一个:

def update_current(self, item):
    self.current_scene.addItem(item)
    loop = QEventLoop()
    QTimer.singleShot(500, loop.quit)
    loop.exec_()
    it = self.current_scene.takeItem(0)
    self.update_prev(it)

def update_prev(self, item):
    self.prev_scenes.addItem(item)

【讨论】:

非常感谢,成功了!你有一些好的资源来学习线程吗?我应该什么时候使用它?我担心我认为我应该在这里使用它,而我不必这样做 基本 Qt 规则:GUI 不应直接从与主线程不同的线程更新,该线程称为 GUI 线程:doc.qt.io/qt-5/thread-basics.html#gui-thread-and-worker-thread,在您的情况下,您想这样做看看对于另一种选择,上面我并不是说您不能使用来自另一个线程的数据来更新 GUI,但是您必须使用信号,或者可能是 QMetaObject::invokeMethod,我也建议使用与 Qt 相关的线程或类,而不是使用其他类型的线程,例如 QThread、QRunnable、QThreadPool。 Qt 文档非常好,推荐阅读。

以上是关于如何在 PyQT 中使用 QThread的主要内容,如果未能解决你的问题,请参考以下文章

如何在Python PyQt中中断QThread上的脚本执行?

PyQt5:如何在 QThread 和某些子类之间“通信”?

PyQt5 - 如何在使用 QThread 时减少 CPU 使用率(低于 50%)?

PyQt:如何从 QThread 获取 UI 数据

PyQt:如何终止可重用的 QThread

完成后如何自动退出PyQT QThread?