10000 Add Qt GUI example to the logging cookbook. (GH-14978) · python/cpython@1ed915e · GitHub
[go: up one dir, main page]

Skip to content

Commit 1ed915e

Browse files
authored
Add Qt GUI example to the logging cookbook. (GH-14978)
1 parent 46ebd4a commit 1ed915e

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

Doc/howto/logging-cookbook.rst

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2744,3 +2744,216 @@ And if we want less:
27442744
27452745
In this case, the commands don't print anything to the console, since nothing
27462746
at ``WARNING`` level or above is logged by them.
2747+
2748+
.. _qt-gui:
2749+
2750+
A Qt GUI for logging
2751+
--------------------
2752+
2753+
A question that comes up from time to time is about how to log to a GUI
2754+
application. The `Qt <https://www.qt.io/>`_ framework is a popular
2755+
cross-platform UI framework with Python bindings using `PySide2
2756+
<https://pypi.org/project/PySide2/>`_ or `PyQt5
2757+
<https://pypi.org/project/PyQt5/>`_ libraries.
2758+
2759+
The following example shows how to log to a Qt GUI. This introduces a simple
2760+
``QtHandler`` class which takes a callable, which should be a slot in the main
2761+
thread that does GUI updates. A worker thread is also created to show how you
2762+
can log to the GUI from both the UI itself (via a button for manual logging)
2763+
as well as a worker thread doing work in the background (here, just random
2764+
short delays).
2765+
2766+
The worker thread is implemented using Qt's ``QThread`` class rather than the
2767+
:mod:`threading` module, as there are circumstances where one has to use
2768+
``QThread``, which offers better integration with other ``Qt`` components.
2769+
2770+
The code should work with recent releases of either ``PySide2`` or ``PyQt5``.
2771+
You should be able to adapt the approach to earlier versions of Qt. Please
2772+
refer to the comments in the code for more detailed information.
2773+
2774+
.. code-block:: python3
2775+
2776+
import datetime
2777+
import logging
2778+
import random
2779+
import sys
2780+
import time
2781+
2782+
# Deal with minor differences between PySide2 and PyQt5
2783+
try:
2784+
from PySide2 import QtCore, QtGui, QtWidgets
2785+
Signal = QtCore.Signal
2786+
Slot = QtCore.Slot
2787+
except ImportError:
2788+
from PyQt5 import QtCore, QtGui, QtWidgets
2789+
Signal = QtCore.pyqtSignal
2790+
Slot = QtCore.pyqtSlot
2791+
2792+
logger = logging.getLogger(__name__)
2793+
2794+
#
2795+
# Signals need to be contained in a QObject or subclass in order to be correctly
2796+
# initialized.
2797+
#
2798+
class Signaller(QtCore.QObject):
2799+
signal = Signal(str)
2800+
2801+
#
2802+
# Output to a Qt GUI is only supposed to happen on the main thread. So, this
2803+
# handler is designed to take a slot function which is set up to run in the main
2804+
# thread. In this example, the function takes a single argument which is a
2805+
# formatted log message. You can attach a formatter instance which formats a
2806+
# LogRecord however you like, or change the slot function to take some other
2807+
# value derived from the LogRecord.
2808+
#
2809+
# You specify the slot function to do whatever GUI updates you want. The handler
2810+
# doesn't know or care about specific UI elements.
2811+
#
2812+
class QtHandler(logging.Handler):
2813+
def __init__(self, slotfunc, *args, **kwargs):
2814+
super(QtHandler, self).__init__(*args, **kwargs)
2815+
self.signaller = Signaller()
2816+
self.signaller.signal.connect(slotfunc)
2817+
2818+
def emit(self, record):
2819+
s = self.format(record)
2820+
self.signaller.signal.emit(s)
2821+
2822+
#
2823+
# This example uses QThreads, which means that the threads at the Python level
2824+
# are named something like "Dummy-1". The function below gets the Qt name of the
2825+
# current thread.
2826+
#
2827+
def ctname():
2828+
return QtCore.QThread.currentThread().objectName()
2829+
2830+
#
2831+
# This worker class represents work that is done in a thread separate to the
2832+
# main thread. The way the thread is kicked off to do work is via a button press
2833+
# that connects to a slot in the worker.
2834+
#
2835+
# Because the default threadName value in the LogRecord isn't much use, we add
2836+
# a qThreadName which contains the QThread name as computed above, and pass that
2837+
# value in an "extra" dictionary which is used to update the LogRecord with the
2838+
# QThread name.
2839+
#
2840+
# This example worker just outputs messages sequentially, interspersed with
2841+
# random delays of the order of a few seconds.
2842+
#
2843+
class Worker(QtCore.QObject):
2844+
@Slot()
2845+
def start(self):
2846+
extra = {'qThreadName': ctname() }
2847+
logger.debug('Started work', extra=extra)
2848+
i = 1
2849+
# Let the thread run until interrupted. This allows reasonably clean
2850+
# thread termination.
2851+
while not QtCore.QThread.currentThread().isInterruptionRequested():
2852+
delay = 0.5 + random.random() * 2
2853+
time.sleep(delay)
2854+
logger.debug('Message after delay of %3.1f: %d', delay, i, extra=extra)
2855+
i += 1
2856+
2857+
#
2858+
# Implement a simple UI for this cookbook example. This contains:
2859+
#
2860+
# * A read-only text edit window which holds formatted log messages
2861+
# * A button to start work and log stuff in a separate thread
2862+
# * A button to log something from the main thread
2863+
# * A button to clear the log window
2864+
#
2865+
class Window(QtWidgets.QWidget):
2866+
2867+
def __init__(self, app):
2868+
super(Window, self).__init__()
2869+
self.app = app
2870+
self.textedit = te = QtWidgets.QTextEdit(self)
2871+
# Set whatever the default monospace font is for the platform
2872+
f = QtGui.QFont('nosuchfont')
2873+
f.setStyleHint(f.Monospace)
2874+
te.setFont(f)
2875+
te.setReadOnly(True)
2876+
PB = QtWidgets.QPushButton
2877+
self.work_button = PB('Start background work', self)
2878+
self.log_button = PB('Log a message at a random level', self)
2879+
self.clear_button = PB('Clear log window', self)
2880+
self.handler = h = QtHandler(self.update_status)
2881+
# Remember to use qThreadName rather than threadName in the format string.
2882+
fs = '%(asctime)s %(qThreadName)-12s %(levelname)-8s %(message)s'
2883+
formatter = logging.Formatter(f)
2884+
h.setFormatter(formatter)
2885+
logger.addHandler(h)
2886+
# Set up to terminate the QThread when we exit
2887+
app.aboutToQuit.connect(self.force_quit)
2888+
2889+
# Lay out all the widgets
2890+
layout = QtWidgets.QVBoxLayout(self)
2891+
layout.addWidget(te)
2892+
layout.addWidget(self.work_button)
2893+
layout.addWidget(self.log_button)
2894+
layout.addWidget(self.clear_button)
2895+
self.setFixedSize(900, 400)
2896+
2897+
# Connect the non-worker slots and signals
2898+
self.log_button.clicked.connect(self.manual_update)
2899+
self.clear_button.clicked.connect(self.clear_display)
2900+
2901+
# Start a new worker thread and connect the slots for the worker
2902+
self.start_thread()
2903+
self.work_button.clicked.connect(self.worker.start)
2904+
# Once started, the button should be disabled
2905+
self.work_button.clicked.connect(lambda : self.work_button.setEnabled(False))
2906+
2907+
def start_thread(self):
2908+
self.worker = Worker()
2909+
self.worker_thread = QtCore.QThread()
2910+
self.worker.setObjectName('Worker')
2911+
self.worker_thread.setObjectName('WorkerThread') # for qThreadName
2912+
self.worker.moveToThread(self.worker_thread)
2913+
# This will start an event loop in the worker thread
2914+
self.worker_thread.start()
2915+
2916+
def kill_thread(self):
2917+
# Just tell the worker to stop, then tell it to quit and wait for that
2918+
# to happen
2919+
self.worker_thread.requestInterruption()
2920+
if self.worker_thread.isRunning():
2921+
self.worker_thread.quit()
2922+
self.worker_thread.wait()
2923+
else:
2924+
print('worker has already exited.')
2925+
2926+
def force_quit(self):
2927+
# For use when the window is closed
2928+
if self.worker_thread.isRunning():
2929+
self.kill_thread()
2930+
2931+
# The functions below update the UI and run in the main thread because
2932+
# that's where the slots are set up
2933+
2934+
@Slot(str)
2935+
def update_status(self, status):
2936+
self.textedit.append(status)
2937+
2938+
@Slot()
2939+
def manual_update(self):
2940+
levels = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
2941+
logging.CRITICAL)
2942+
level = random.choice(levels)
2943+
extra = {'qThreadName': ctname() }
2944+
logger.log(level, 'Manually logged!', extra=extra)
2945+
2946+
@Slot()
2947+
def clear_display(self):
2948+
self.textedit.clear()
2949+
2950+
def main():
2951+
QtCore.QThread.currentThread().setObjectName('MainThread')
2952+
logging.getLogger().setLevel(logging.DEBUG)
2953+
app = QtWidgets.QApplication(sys.argv)
2954+
example = Window(app)
2955+
example.show()
2956+
sys.exit(app.exec_())
2957+
2958+
if __name__=='__main__':
2959+
main()

0 commit comments

Comments
 (0)
0