当控制台输出重新路由到 GUI 时暂停 Python 代码

Pausing Python Code When Console Output Has Been Re-Routed to GUI

我借用了我在 Whosebug 上找到的一个设计,将控制台输出重定向到 PyQt5 GUI textEdit 小部件。这工作正常,但文本未显示在 "real-time" 中。似乎在进程完成后将文本输出到 GUI。这一直不是问题,直到我尝试使用 time.sleep(secs) 打印一些东西,暂停,然后打印其他东西。最终发生的是程序暂停 secs,然后一次打印所有语句。

此 class 在 GUI 的主窗口文件中:

class EmittingStream(QtCore.QObject):

    textWritten = QtCore.pyqtSignal(str)

    def write(self, text):
        self.textWritten.emit(str(text))

这是在我的事件处理文件的 __init__ 方法中:

sys.stdout = EmittingStream(textWritten=self.normalOutputWritten)
self.case_setup_console.setReadOnly(True)
self.main_console.setReadOnly(True)

这个函数在事件处理文件的主要class中(在__init__之外):

    def normalOutputWritten(self, text):
        """Append text to the QTextEdit."""
        # Maybe QTextEdit.append() works as well, but this is how I do it:
        cursor = self.case_setup_console.textCursor()
        cursor.movePosition(QtGui.QTextCursor.End)
        cursor.insertText(text)
        self.case_setup_console.setTextCursor(cursor)
        self.case_setup_console.ensureCursorVisible()

这按预期工作,将输出重新路由到文本编辑小部件 self.case_setup_console。但是,当我尝试 运行 代码时,例如:

print('This is the first message')
time.sleep(5)
print('This should print 5 seconds later')

程序等待 5 秒,然后同时打印两条语句。

在为 GUI 代码编程时,程序的设计方式发生了根本性的转变。简而言之:构建和初始化后,程序始终运行在GUI框架提供的"event loop"中,只有在特定事件发生时才会调用您的代码地点。

这与您的代码始终处于 运行ning 的终端应用程序形成对比,您告诉何时执行 "print"s、"input"s 并使用 [ 暂停=42=].

GUI代码负责记录事件(键盘、UI、网络等...),重绘window内容并调用您的代码作为响应到事件中,或者在需要重新绘制代码中定义的内容时(如更新背景图像)。

因此,当控件被传回其事件循环时,它只能呈现本应显示在 window 中的文本,以及您重定向的 "print"。当您执行 time.sleep 时,您会暂停 return - 事件循环中没有代码 运行,当然,它不能进行任何屏幕绘制。

需要的是在程序中以一种方式编写暂停,在暂停期间,GUI 事件循环是 运行ning - 而不是 "time.sleep",即只是暂停你的整个线程。

在 Qt 中,这样做的方法是创建一个 QTimer 对象来调用要用于在特定时刻打印文本的代码,然后通过 returning 将执行交给 QtMainloop从你的功能。

感谢 Python 对嵌套函数的支持,这可以轻松完成,甚至在设置计时器本身时使用 lambda 函数。

...
print('This is the first message')
timer = QtCore.QTimer

timer.singleShot(5000, lambda *_: print('This should print 5 seconds later'))

应该适用于给定的示例。 (调用,像往常一样 UIs,暂停时间以毫秒为单位,而不是以秒为单位)。

如果您需要安排在每个短语输出后打印更多文本,您将需要在回调本身内部调用调度,并且需要更复杂一些,但它仍然可以像这样简单:

phrases = iter(("hello", "world", "I", "am", "talking", "slowly!"))
timer = QtCore.QTimer()

def talker(*_):
    phrase = next(phrases, None)
    if not phrase: 
         return
    print(phrase)
    timer.singleShot(1000, talker)

timer.singleShot(1000, talker)

(请注意,*_ 参数名称也没有什么特别之处:我只是表示回调可能有任意数量的位置参数 - (“*”部分,即 Python 语法)——那时我不会关心(我将参数序列称为“_”以表明我不关心它是如何被调用的,因为它无论如何都不会被使用——这是一个编码约定) )

iternext 调用比可能使用的要多 "Python dialect",但可以只使用一个列表和一个不超过列表长度的计数器。