Skip to content

Refined Qt GUI example in the logging cookbook. #15045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 31, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 39 additions & 17 deletions Doc/howto/logging-cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2760,16 +2760,16 @@ The following example shows how to log to a Qt GUI. This introduces a simple
``QtHandler`` class which takes a callable, which should be a slot in the main
thread that does GUI updates. A worker thread is also created to show how you
can log to the GUI from both the UI itself (via a button for manual logging)
as well as a worker thread doing work in the background (here, just random
short delays).
as well as a worker thread doing work in the background (here, just logging
messages at random levels with random short delays in between).

The worker thread is implemented using Qt's ``QThread`` class rather than the
:mod:`threading` module, as there are circumstances where one has to use
``QThread``, which offers better integration with other ``Qt`` components.

The code should work with recent releases of either ``PySide2`` or ``PyQt5``.
You should be able to adapt the approach to earlier versions of Qt. Please
refer to the comments in the code for more detailed information.
refer to the comments in the code snippet for more detailed information.

.. code-block:: python3

Expand All @@ -2789,22 +2789,24 @@ refer to the comments in the code for more detailed information.
Signal = QtCore.pyqtSignal
Slot = QtCore.pyqtSlot


logger = logging.getLogger(__name__)


#
# Signals need to be contained in a QObject or subclass in order to be correctly
# initialized.
#
class Signaller(QtCore.QObject):
signal = Signal(str)
signal = Signal(str, logging.LogRecord)

#
# Output to a Qt GUI is only supposed to happen on the main thread. So, this
# handler is designed to take a slot function which is set up to run in the main
# thread. In this example, the function takes a single argument which is a
# formatted log message. You can attach a formatter instance which formats a
# LogRecord however you like, or change the slot function to take some other
# value derived from the LogRecord.
# thread. In this example, the function takes a string argument which is a
# formatted log message, and the log record which generated it. The formatted
# string is just a convenience - you could format a string for output any way
# you like in the slot function itself.
#
# You specify the slot function to do whatever GUI updates you want. The handler
# doesn't know or care about specific UI elements.
Expand All @@ -2817,7 +2819,7 @@ refer to the comments in the code for more detailed information.

def emit(self, record):
s = self.format(record)
self.signaller.signal.emit(s)
self.signaller.signal.emit(s, record)

#
# This example uses QThreads, which means that the threads at the Python level
Expand All @@ -2827,6 +2829,13 @@ refer to the comments in the code for more detailed information.
def ctname():
return QtCore.QThread.currentThread().objectName()


#
# Used to generate random levels for logging.
#
LEVELS = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
logging.CRITICAL)

#
# This worker class represents work that is done in a thread separate to the
# main thread. The way the thread is kicked off to do work is via a button press
Expand All @@ -2851,7 +2860,8 @@ refer to the comments in the code for more detailed information.
while not QtCore.QThread.currentThread().isInterruptionRequested():
delay = 0.5 + random.random() * 2
time.sleep(delay)
logger.debug('Message after delay of %3.1f: %d', delay, i, extra=extra)
level = random.choice(LEVELS)
logger.log(level, 'Message after delay of %3.1f: %d', delay, i, extra=extra)
i += 1

#
Expand All @@ -2864,10 +2874,18 @@ refer to the comments in the code for more detailed information.
#
class Window(QtWidgets.QWidget):

COLORS = {
logging.DEBUG: 'black',
logging.INFO: 'blue',
logging.WARNING: 'orange',
logging.ERROR: 'red',
logging.CRITICAL: 'purple',
}

def __init__(self, app):
super(Window, self).__init__()
self.app = app
self.textedit = te = QtWidgets.QTextEdit(self)
self.textedit = te = QtWidgets.QPlainTextEdit(self)
# Set whatever the default monospace font is for the platform
f = QtGui.QFont('nosuchfont')
f.setStyleHint(f.Monospace)
Expand All @@ -2880,7 +2898,7 @@ refer to the comments in the code for more detailed information.
self.handler = h = QtHandler(self.update_status)
# Remember to use qThreadName rather than threadName in the format string.
fs = '%(asctime)s %(qThreadName)-12s %(levelname)-8s %(message)s'
formatter = logging.Formatter(f)
formatter = logging.Formatter(fs)
h.setFormatter(formatter)
logger.addHandler(h)
# Set up to terminate the QThread when we exit
Expand Down Expand Up @@ -2932,21 +2950,25 @@ refer to the comments in the code for more detailed information.
# that's where the slots are set up

@Slot(str)
def update_status(self, status):
self.textedit.append(status)
def update_status(self, status, record):
color = self.COLORS.get(record.levelno, 'black')
s = '<pre><font color="%s">%s</font></pre>' % (color, status)
self.textedit.appendHtml(s)

@Slot()
def manual_update(self):
levels = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
logging.CRITICAL)
level = random.choice(levels)
# This function uses the formatted message passed in, but also uses
# information from the record to format the message in an appropriate
# color according to its severity (level).
level = random.choice(LEVELS)
extra = {'qThreadName': ctname() }
logger.log(level, 'Manually logged!', extra=extra)

@Slot()
def clear_display(self):
self.textedit.clear()


def main():
QtCore.QThread.currentThread().setObjectName('MainThread')
logging.getLogger().setLevel(logging.DEBUG)
Expand Down