Python 立即登录 PySide 小部件
Python logging to PySide widget without delay
问题: 我有一个 PySide 应用程序,它已经使用日志记录作为控制台输出,但它的日志记录应该以某种方式扩展LogRecords 也立即 显示在像 QTextBrowser
这样的小部件中。我知道这通常是通过 一个工作线程来完成的,该工作线程在 main/gui 线程 中发出一个插槽信号,但是由于代码库相当大,并且可能使用了日志记录在一些阻塞的核心操作中,如果无论如何都可以在 GUI 中实现即时反馈而无需进行更大的重构,那就太好了。
示例: 下面是一些示例代码用于演示。它显示:
- a
logger
有两个处理程序:
- a
StreamHandler
记录到控制台
- a
QSignalHandler
发出一个信号,其中有一条消息连接到一个插槽,该插槽将消息附加到 QTextBrowser
。
- 一种方法
long_running_core_operation_that_should_log_immediately_to_ui()
模拟阻塞核心操作的日志记录。
# -*- 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
循环之后。
- 是否有推荐的方法,可以在不为核心操作调用工作线程的情况下从
QSignalHandler
实现 立即 日志记录?
- safe/recommended 是在
QSignalHandler
发出记录信号后才调用 QtGui.qApp.processEvents()
吗? (取消注释时,直接登录到 GUI)。
- 当阅读信号连接类型的 documentation 时,上面写着
Qt.DirectConnection: The slot is invoked immediately, when the signal is emitted.
我会觉得 QSignalHandler
应该像 StreamHandler
一样立即更新是,不是吗?
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
时是这样。但是重绘不会立即发生。
问题: 我有一个 PySide 应用程序,它已经使用日志记录作为控制台输出,但它的日志记录应该以某种方式扩展LogRecords 也立即 显示在像 QTextBrowser
这样的小部件中。我知道这通常是通过 一个工作线程来完成的,该工作线程在 main/gui 线程 中发出一个插槽信号,但是由于代码库相当大,并且可能使用了日志记录在一些阻塞的核心操作中,如果无论如何都可以在 GUI 中实现即时反馈而无需进行更大的重构,那就太好了。
示例: 下面是一些示例代码用于演示。它显示:
- a
logger
有两个处理程序:- a
StreamHandler
记录到控制台 - a
QSignalHandler
发出一个信号,其中有一条消息连接到一个插槽,该插槽将消息附加到QTextBrowser
。
- a
- 一种方法
long_running_core_operation_that_should_log_immediately_to_ui()
模拟阻塞核心操作的日志记录。
# -*- 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
循环之后。
- 是否有推荐的方法,可以在不为核心操作调用工作线程的情况下从
QSignalHandler
实现 立即 日志记录? - safe/recommended 是在
QSignalHandler
发出记录信号后才调用QtGui.qApp.processEvents()
吗? (取消注释时,直接登录到 GUI)。 - 当阅读信号连接类型的 documentation 时,上面写着
Qt.DirectConnection: The slot is invoked immediately, when the signal is emitted.
我会觉得QSignalHandler
应该像StreamHandler
一样立即更新是,不是吗?
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
时是这样。但是重绘不会立即发生。