Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 24329

Vinay Sajip (Logging): A Qt GUI for logging

$
0
0

A question that comes up from time to time is about how to log to a GUI application. The Qt framework is a popular cross-platform UI framework with Python bindings using PySide2 or PyQt5 libraries.


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 logging messages at random levels with random short delays in between).

The worker thread is implemented using Qt’s QThread class rather than the 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 snippet for more detailed information.

importdatetimeimportloggingimportrandomimportsysimporttime# Deal with minor differences between PySide2 and PyQt5try:fromPySide2importQtCore,QtGui,QtWidgetsSignal=QtCore.SignalSlot=QtCore.SlotexceptImportError:fromPyQt5importQtCore,QtGui,QtWidgetsSignal=QtCore.pyqtSignalSlot=QtCore.pyqtSlotlogger=logging.getLogger(__name__)## Signals need to be contained in a QObject or subclass in order to be correctly# initialized.#classSignaller(QtCore.QObject):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 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.#classQtHandler(logging.Handler):def__init__(self,slotfunc,*args,**kwargs):super(QtHandler,self).__init__(*args,**kwargs)self.signaller=Signaller()self.signaller.signal.connect(slotfunc)defemit(self,record):s=self.format(record)self.signaller.signal.emit(s,record)## This example uses QThreads, which means that the threads at the Python level# are named something like "Dummy-1". The function below gets the Qt name of the# current thread.#defctname():returnQtCore.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# that connects to a slot in the worker.## Because the default threadName value in the LogRecord isn't much use, we add# a qThreadName which contains the QThread name as computed above, and pass that# value in an "extra" dictionary which is used to update the LogRecord with the# QThread name.## This example worker just outputs messages sequentially, interspersed with# random delays of the order of a few seconds.#classWorker(QtCore.QObject):@Slot()defstart(self):extra={'qThreadName':ctname()}logger.debug('Started work',extra=extra)i=1# Let the thread run until interrupted. This allows reasonably clean# thread termination.whilenotQtCore.QThread.currentThread().isInterruptionRequested():delay=0.5+random.random()*2time.sleep(delay)level=random.choice(LEVELS)logger.log(level,'Message after delay of %3.1f: %d',delay,i,extra=extra)i+=1## Implement a simple UI for this cookbook example. This contains:## * A read-only text edit window which holds formatted log messages# * A button to start work and log stuff in a separate thread# * A button to log something from the main thread# * A button to clear the log window#classWindow(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=appself.textedit=te=QtWidgets.QPlainTextEdit(self)# Set whatever the default monospace font is for the platformf=QtGui.QFont('nosuchfont')f.setStyleHint(f.Monospace)te.setFont(f)te.setReadOnly(True)PB=QtWidgets.QPushButtonself.work_button=PB('Start background work',self)self.log_button=PB('Log a message at a random level',self)self.clear_button=PB('Clear log window',self)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(fs)h.setFormatter(formatter)logger.addHandler(h)# Set up to terminate the QThread when we exitapp.aboutToQuit.connect(self.force_quit)# Lay out all the widgetslayout=QtWidgets.QVBoxLayout(self)layout.addWidget(te)layout.addWidget(self.work_button)layout.addWidget(self.log_button)layout.addWidget(self.clear_button)self.setFixedSize(900,400)# Connect the non-worker slots and signalsself.log_button.clicked.connect(self.manual_update)self.clear_button.clicked.connect(self.clear_display)# Start a new worker thread and connect the slots for the workerself.start_thread()self.work_button.clicked.connect(self.worker.start)# Once started, the button should be disabledself.work_button.clicked.connect(lambda:self.work_button.setEnabled(False))defstart_thread(self):self.worker=Worker()self.worker_thread=QtCore.QThread()self.worker.setObjectName('Worker')self.worker_thread.setObjectName('WorkerThread')# for qThreadNameself.worker.moveToThread(self.worker_thread)# This will start an event loop in the worker threadself.worker_thread.start()defkill_thread(self):# Just tell the worker to stop, then tell it to quit and wait for that# to happenself.worker_thread.requestInterruption()ifself.worker_thread.isRunning():self.worker_thread.quit()self.worker_thread.wait()else:print('worker has already exited.')defforce_quit(self):# For use when the window is closedifself.worker_thread.isRunning():self.kill_thread()# The functions below update the UI and run in the main thread because# that's where the slots are set up@Slot(str, logging.LogRecord)defupdate_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()defmanual_update(self):# 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()defclear_display(self):self.textedit.clear()defmain():QtCore.QThread.currentThread().setObjectName('MainThread')logging.getLogger().setLevel(logging.DEBUG)app=QtWidgets.QApplication(sys.argv)example=Window(app)example.show()sys.exit(app.exec_())if__name__=='__main__':main()

Viewing all articles
Browse latest Browse all 24329

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>