Python 立即登录 PySide 小部件

Python logging to PySide widget without delay

问题: 我有一个 PySide 应用程序,它已经使用日志记录作为控制台输出,但它的日志记录应该以某种方式扩展LogRecords立即 显示在像 QTextBrowser 这样的小部件中。我知道这通常是通过 一个工作线程来完成的,该工作线程在 main/gui 线程 中发出一个插槽信号,但是由于代码库相当大,并且可能使用了日志记录在一些阻塞的核心操作中,如果无论如何都可以在 GUI 中实现即时反馈而无需进行更大的重构,那就太好了。

示例: 下面是一些示例代码用于演示。它显示:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import logging
import sys

from PySide import QtCore
from PySide import QtGui


class QSignaler(QtCore.QObject):
    log_message = QtCore.Signal(unicode)


class SignalHandler(logging.Handler):
    """Logging handler to emit QtSignal with log record text."""

    def __init__(self, *args, **kwargs):
        super(SignalHandler, self).__init__(*args, **kwargs)
        self.emitter = QSignaler()

    def emit(self, logRecord):
        msg = "{0}".format(logRecord.getMessage())
        self.emitter.log_message.emit(msg)
        # When the line below is enabled, logging is immediate/otherwise events
        # on the queue will be processed when the slot has finished.
        # QtGui.qApp.processEvents()


# configure logging
logging.basicConfig(level=logging.DEBUG)  # adds StreamHandler

signal_handler = SignalHandler()

logger = logging.getLogger()
logger.addHandler(signal_handler)


class TestWidget(QtGui.QWidget):
    def __init__(self, *args, **kwargs):
        super(TestWidget, self).__init__(*args, **kwargs)

        layout = QtGui.QVBoxLayout(self)

        # text_browser
        self.text_browser = QtGui.QTextBrowser()
        layout.addWidget(self.text_browser)

        # btn_start_operation
        self.btn_start_operation = QtGui.QPushButton("Start operation")
        self.btn_start_operation.clicked.connect(
            self.long_running_core_operation_that_should_log_immediately_to_ui)
        layout.addWidget(self.btn_start_operation)

        # btn_clear
        self.btn_clear = QtGui.QPushButton("Clear")
        self.btn_clear.clicked.connect(self.text_browser.clear)
        layout.addWidget(self.btn_clear)

    def long_running_core_operation_that_should_log_immediately_to_ui(self):
        for index in range(10000):
            msg = "{0}".format(index)
            logger.info(msg)


# test
if (__name__ == "__main__"):
    app = QtGui.QApplication(sys.argv)
    test_widget = TestWidget()
    signal_handler.emitter.log_message.connect(test_widget.text_browser.append)
    test_widget.show()
    sys.exit(app.exec_())

问题: 虽然 StreamHandler 记录到 stdout 立即发生,但 QSignalHandler 记录发生,当 PySide 事件循环再次处理事件,这发生在 for 循环之后。

Is there a recommended way, to achieve immediate logging from the QSignalHandler without invoking a worker thread for the core operation?

除了处理事件,我不知道还有什么其他方法可以触发日志小部件的重绘。

请注意,在日志小部件上调用 repaint() 是一种误导,并且没有达到预期的效果,它只会强制调用日志小部件的 paintEvent() 方法。 repaint() 不会做重要的事情,例如将 window 表面复制到 windowing 系统。

Is it safe/recommended to just call QtGui.qApp.processEvents() after the QSignalHandler has emitted the logging signal? (When uncommented, logging to the GUI happens directly).

使用单独的线程或异步操作是推荐的方式。如果你不能这样做,推荐的方法是调用 processEvents(),就像你的情况一样。即使 Qt 在 QProgressDialog::setValue().

中也出于同样的目的使用它

一般来说,手动处理事件可能很危险,应该小心处理。在调用 processEvents() 之后,完整的应用程序状态可能会有所不同。例如,日志小部件可能不再存在,因为用户关闭了 window!在您的示例代码中,这没有问题,因为 signal/slot 连接会自动断开连接,但想象一下,如果您在日志小部件因关闭而被删除后尝试访问它 - 您会遇到崩溃。所以要小心。

When reading the documentation for signal connection types, where it says Qt.DirectConnection: The slot is invoked immediately, when the signal is emitted. I would have kind of thought the QSignalHandler should have updated immediately just as the StreamHandler does, shouldn't it?

插槽,在您的情况下 QTextBrowser::append() 立即被调用。但是,QTextBrowser::append() 不会立即重新绘制。相反,它会安排重绘(通过 QWidget::update()),而实际的重绘会在 Qt 开始处理事件时发生。那是当您 return 到事件循环时,或者当您手动调用 processEvents() 时。 所以插槽确实在发出信号时立即被调用,至少在使用默认 DirectConnection 时是这样。但是重绘不会立即发生。