@@ -2744,3 +2744,216 @@ And if we want less:
2744
2744
2745
2745
In this case, the commands don't print anything to the console, since nothing
2746
2746
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