1

Problem: I have a PySide application that already uses logging for console output, but its logging should be extended in a way that LogRecords are also displayed immediately in a widget like a QTextBrowser. I am aware that this would usually be done via a worker thread that signals a slot in the main/gui thread, however as the code base is fairly big, and logging is probably used in a few blocking core operations it would be nice if an immediate feedback in the GUI could be achieved anyways without a bigger refactoring.

Example: Here is some example code for demonstration. It shows:

  • a logger with two handlers:
    1. a StreamHandler logging to the console
    2. a QSignalHandler emitting a signal with a message connected to a slot that appends the message to a QTextBrowser.
  • a method long_running_core_operation_that_should_log_immediately_to_ui() that simulates logging from a blocking core operation.
# -*- 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_())

Question: While the StreamHandler logging to stdout happens immediately, the QSignalHandler logging happens, when the PySide event loop processes events again, which happens after the for loop.

  • Is there a recommended way, to achieve immediate logging from the QSignalHandler without invoking a worker thread for the core operation?
  • 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).
  • 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?
timmwagener
  • 2,368
  • 2
  • 19
  • 27

1 Answers1

3

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

I don't know of any other way to trigger a repaint of the log widget than processing events.

Note that calling repaint() on the log widget is misleading and does not have the desired effect, it only forces the paintEvent() method of the log widget to be called. repaint() does not do crucial things like copying the window surface to the windowing system.

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).

Using a separate thread or asynchronous operations is the recommended way. Calling processEvents() is the recommended way if you can't do that, like in your case.. Even Qt uses it for the same purpose inside QProgressDialog::setValue().

In general, manually processing events can be dangerous and should be done with care. After the call to processEvents(), the complete application state might be different. For example the log widget might no longer exist because the user closed the window! In your example code that is no problem, as the signal/slot connection will automatically disconnect, but imagine if you had tried to access the log widget after it has been deleted due to it being closed - you would have gotten a crash. So be careful.

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?

The slot, in your case QTextBrowser::append(), is called immediately. However, QTextBrowser::append() does not immediately repaint. Instead, it schedules a repaint (via QWidget::update()), and the actual repainting happens when Qt gets around to process the events. That is either when you return to the event loop, or when you call processEvents() manually. So slots are indeed called right away when emitting a signal, at least when using the default DirectConnection. However repainting does not happen immediately.

Thomas McGuire
  • 5,308
  • 26
  • 45
  • Thought about it and came to the conclusion, that when I put the `QtGui.qApp.processEvents()` in the slot of a custom `QTextBrowser` subclass itself _(like `immediate_append()`)_, instead of the `QSignalHandler`, it should update correctly and be a lot safer, right? – timmwagener Apr 25 '17 at 07:25
  • @timmwagener: Yes, that would be slightly more correct and cleaner. It would mean that in case ``QTextBrowser::append()`` is *not* connected, no unnecessary event processing is done. – Thomas McGuire Apr 25 '17 at 17:45