Qt Dialog窗口关闭时如何终止QEventLoop

Posted

技术标签:

【中文标题】Qt Dialog窗口关闭时如何终止QEventLoop【英文标题】:How to terminate a QEventLoop when a Qt Dialog window is closed 【发布时间】:2021-04-09 02:01:22 【问题描述】:

为了获得更多使用 Python 和创建 GUI 的实践经验,我决定创建一个抽认卡测验应用程序。最初,我创建了一个简单的函数,它接受一个 csv 文件名,将答案和问题打乱,并实现了一个for 循环来要求用户为每个问题输入一个答案。我通过 CMD 运行它,它运行良好。

接下来,我使用 Qt 设计器创建了一个简单的 Qt 对话框窗口,其中 textBrowser 用于输出,lineEdit 用于输入。 (旁注:我知道你不应该修改生成的 ui 文件,所以我复制了代码并将其保存到不同的目录中,这样我就可以安全地使用它了。)我将测验函数放在 Dialog 类中,并让它调用应用程序的执行。但是,为了等待用户输入,我需要在提问后启动并在触发lineEdit.returnPressed 时退出的测验功能中添加一个QEventLoop。如果我循环浏览整副纸牌,洗牌功能就会完成,当我关闭 GUI(通过 X 按钮)时,代码会定期停止。但是,如果我尝试在问题被询问和被回答之间关闭窗口(@98​​7654328@ 正在运行),GUI 将关闭但功能仍在运行,并且我设置的 aboutToQuit 检测器不会被触发。

我很确定这个问题是因为在执行QEventLoop 时,测验功能被挂起,到目前为止,我还没有找到成功的方法来注册 GUI 已关闭并退出 QEventLoop 没有完成整个问题/答案循环。 让窗口和QEventLoop 同步运行可以解决我的问题吗?在函数的窗口关闭等事件的情况下,有没有办法过早地突破QEventLoop?或者我应该在这里使用像QTimer 这样的不同进程?

# If this helps, here's the code for the program. 

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import *
import csv
import random
import sys

class Ui_Dialog(QWidget):
    loop = QtCore.QEventLoop()

    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(361, 163)

        self.lineEdit = QtWidgets.QLineEdit(Dialog)
        self.lineEdit.setGeometry(QtCore.QRect(20, 120, 321, 20))
        self.lineEdit.setObjectName("lineEdit")
        self.lineEdit.returnPressed.connect(self.acceptText)
        self.textBrowser = QtWidgets.QTextBrowser(Dialog)
        self.textBrowser.setGeometry(QtCore.QRect(20, 20, 321, 91))
        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))

    def printText(self, contents):
        self.textBrowser.append(contents)

    def acceptText(self):
        input = self.lineEdit.text().strip().lower()
        self.loop.quit()
        return input

    def shuffleDeck(self, filename):
        # sets up values to be referenced later
        questions = []
        answers = []
        index = 0
        numright = 0
 
        # contains the entire reading, shuffling, and quizzing process
        with open(filename, encoding='utf-8') as tsv:
            reader = csv.reader(tsv, delimiter="\t")

            for row in reader:
                questions.append(row[0][::-1])
                answers.append(row[1].lower())
            seed = random.random()
            random.seed(seed)
            random.shuffle(questions)
            random.seed(seed)
            random.shuffle(answers)

            for question in questions:
                # handles input & output
                self.printText("What does " + question + " mean?")
                self.loop.exec_()
                guess = self.acceptText()
                self.textBrowser.append(guess)
                self.lineEdit.clear()

            # compares input to answer, returns correct/incorrect prompts accordingly
                if guess == answers[index]:
                    self.printText("You are right!")
                    index += 1
                    numright += 1
                else:
                    self.printText("You are wrong. The answer is " + str(answers[index]) + "; better luck next time!")
                    index += 1
            self.printText("You got " + str(round(numright / len(questions), 2) * 100) + "% (" + str(
            numright) + ") of the cards right.")

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    Dialog = QtWidgets.QDialog()
    ui = Ui_Dialog()
    ui.setupUi(Dialog)
    Dialog.show()
    # I temporarily hardcoded a csv file
    ui.shuffleDeck("Decks/Animals.csv")
    # linear processing requires shuffleDeck to be completed before the window loops, right?
    sys.exit(app.exec_())

#An example of the csv text would be:
מאוד    VERY
עוד MORE
כמו כ   AS
שם  THERE
#I would have included only English characters, but this is the deck that I hardcoded in. 

【问题讨论】:

请提供minimal reproducible example 您将 pyuic 文件复制到其他地方这一事实并没有改变任何东西:这些文件必须被修改,并且您的程序应该始终在 another 仅使用 pyuic 文件作为导入的文件。有很多理由永远不应该被修改,不仅仅是因为 ui 可能会被修改。编辑和使用程序逻辑的 ui 类被认为是不好的做法,因为它通常会导致难以跟踪的意外问题和错误。 好的,感谢@musicamante 的评论。但是,修改生成的 ui 文件与编写自己的文件有何不同? @khayni 请分享.csv @khayni 它差别很多。 pyuic 生成的类是一个简单的 python 对象类,当您像您一样实现时,它不会直接访问 ui 元素和小部件方法。这使得使用创建的 QWidget 的基本方法和实现变得非常困难,就像您的情况一样。例如,如果您改为将 QDialog 子类化(如指南中有关 using Designer 的建议),您将能够通过在子类中实现 closeEvent 直接检测关闭事件。 【参考方案1】:

正如您已经指出的那样,您不应该修改pyuic生成的代码,因此您必须使用以下命令重新生成文件:

pyuic5 filename.ui -o design_ui.py -x

另一方面,没有必要也不建议使用 QEventLoop,因为它们会产生意想不到的行为,在这种情况下,使用迭代器就足够了。

您还必须将逻辑与 GUI 分开,例如创建一个将问题与答案相关联的数据结构,以及另一个提供和管理测试的类。

最后,您只需实现 GUI 逻辑来处理用户交互时发出的信号。

ma​​in.py

import csv
from dataclasses import dataclass
import random
import sys

from PyQt5 import QtWidgets

from design_ui import Ui_Dialog


@dataclass
class Test:
    question: str
    answer: str

    def verify(self, answer):
        return self.answer == answer


class TestProvider:
    def __init__(self, tests):
        self._tests = tests
        self._current_test = None
        self.init_iterator()

    def init_iterator(self):
        self._test_iterator = iter(self.tests)

    @property
    def tests(self):
        return self._tests

    @property
    def number_of_tests(self):
        return len(self._tests)

    @property
    def current_test(self):
        return self._current_test

    def next_text(self):
        try:
            self._current_test = next(self._test_iterator)
        except StopIteration as e:
            return False
        else:
            return True


class Dialog(QtWidgets.QDialog, Ui_Dialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)

        self.lineEdit.returnPressed.connect(self.handle_pressed)
        self.lineEdit.setEnabled(False)

    def load_tests_from_filename(self, filename):
        self._number_of_correct_answers = 0
        tests = []
        with open(filename, encoding="utf-8") as tsv:
            reader = csv.reader(tsv, delimiter="\t")
            for row in reader:
                question, answer = row
                test = Test(question, answer)
                tests.append(test)
        seed = random.random()
        random.seed(seed)
        random.shuffle(tests)
        self._test_provider = TestProvider(tests)
        self.load_test()
        self.lineEdit.setEnabled(True)

    def load_test(self):
        if self._test_provider.next_text():
            self.print_text(
                f"What does self._test_provider.current_test.question mean?"
            )
            return True
        return False

    def handle_pressed(self):
        if self._test_provider is None:
            return
        guess = self.lineEdit.text().strip().lower()
        self.textBrowser.append(guess)
        if self._test_provider.current_test.answer.strip().lower() == guess:
            self.print_text("You are right!")
            self._number_of_correct_answers += 1
        else:
            self.print_text(
                f"You are wrong. The answer is self._test_provider.current_test.answer; better luck next time!"
            )

        self.lineEdit.clear()
        if not self.load_test():
            self.print_text(
                f"You got (round(self._number_of_correct_answers / self._test_provider.number_of_tests, 2) * 100)% (self._number_of_correct_answers) of the cards right."
            )
            self.lineEdit.setEnabled(False)

    def print_text(self, text):
        self.textBrowser.append(text)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = Dialog()
    w.load_tests_from_filename("Decks/Animals.csv")
    w.show()
    sys.exit(app.exec_())

【讨论】:

以上是关于Qt Dialog窗口关闭时如何终止QEventLoop的主要内容,如果未能解决你的问题,请参考以下文章

QT下怎么实现一个窗口弹出来然后另外一个窗口关闭

Qt中如何设置按钮点击终止线程

QT中Dialog去掉标题后就不能像QMessageBox那样让其它窗口失效,怎么办才好?

程序终止时如何让 Visual Studio 不关闭终端窗口?

qt中将窗口关闭后为啥还会显示到历史窗口

Qt窗口关闭时如何释放内存?