23

I am currently working on a GUI using qt designer. I am wondering how I should go about printing strings on the GUI that acts like a logger window. I am using pyqt5.

ajsmart
  • 183
  • 10
Carlo Angelo
  • 363
  • 1
  • 5
  • 12
  • Bing users should go [here](http://stackoverflow.com/questions/24469662/how-to-redirect-logger-output-into-pyqt-text-widget) :P – Carel May 02 '16 at 10:44

6 Answers6

39

Adapted from Todd Vanyo's example for PyQt5:

import sys
from PyQt5 import QtWidgets
import logging

# Uncomment below for terminal log messages
# logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(name)s - %(levelname)s - %(message)s')

class QTextEditLogger(logging.Handler):
    def __init__(self, parent):
        super().__init__()
        self.widget = QtWidgets.QPlainTextEdit(parent)
        self.widget.setReadOnly(True)

    def emit(self, record):
        msg = self.format(record)
        self.widget.appendPlainText(msg)


class MyDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit):
    def __init__(self, parent=None):
        super().__init__(parent)

        logTextBox = QTextEditLogger(self)
        # You can format what is printed to text box
        logTextBox.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
        logging.getLogger().addHandler(logTextBox)
        # You can control the logging level
        logging.getLogger().setLevel(logging.DEBUG)

        self._button = QtWidgets.QPushButton(self)
        self._button.setText('Test Me')

        layout = QtWidgets.QVBoxLayout()
        # Add the new logging box widget to the layout
        layout.addWidget(logTextBox.widget)
        layout.addWidget(self._button)
        self.setLayout(layout)

        # Connect signal to slot
        self._button.clicked.connect(self.test)

    def test(self):
        logging.debug('damn, a bug')
        logging.info('something to remember')
        logging.warning('that\'s not right')
        logging.error('foobar')

app = QtWidgets.QApplication(sys.argv)
dlg = MyDialog()
dlg.show()
dlg.raise_()
sys.exit(app.exec_())
Alex
  • 401
  • 4
  • 6
  • 3
    how to use in in other case, I have *.ui with QMainWindow, QTabWidget with tab (name tab_log) all defind in ui file. How to add QTextEditLogger to tab in QTabWidget??? – emcek Jan 17 '19 at 17:17
  • 3
    This is not thread safe. `appendPlainText` should be connected to a signal instead of calling it. – tobilocker Jan 22 '20 at 11:25
  • I get the following error message: `AttributeError: module 'logging' has no attribute 'Handler'` – the_economist Nov 05 '20 at 13:03
  • WOW, this was a simple copy and paste for me. Alex, you're a god. – Kris Kizlyk Feb 14 '21 at 22:39
  • Gives me an error `Make sure 'QTextCursor' is registered using qRegisterMetaType()` while logging via callbacks. The thread safe version worked quite well for me. https://stackoverflow.com/a/60528393/3598205 – sa_penguin Jul 20 '21 at 20:57
22

If you are using the Python logging module to can easily create a custom logging handler that passes the log messages through to a QPlainTextEdit instance (as described by Christopher).

To do this you first subclass logging.Handler. In this __init__ we create the QPlainTextEdit that will contain the logs. The key bit here is that the handle will be receiving messages via the emit() function. So we overload this function and pass the message text into the QPlainTextEdit.

import logging

class QPlainTextEditLogger(logging.Handler):
    def __init__(self, parent):
        super(QPlainTextEditLogger, self).__init__()

        self.widget = QPlainTextEdit(parent)
        self.widget.setReadOnly(True)

    def emit(self, record):
        msg = self.format(record)
        self.widget.appendPlainText(msg)

    def write(self, m):
        pass

Create an object from this class, passing it the parent for the QPlainTextEdit (e.g. the main window, or a layout). You can then add this handler for the current logger.

# Set up logging to use your widget as a handler
log_handler = QPlainTextEditLogger(<parent widget>)
logging.getLogger().addHandler(log_handler)
mfitzp
  • 14,247
  • 6
  • 48
  • 66
16

Here's a complete working example based on mfitzp's answer:

import sys
from PyQt4 import QtCore, QtGui
import logging

# Uncomment below for terminal log messages
# logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(name)s - %(levelname)s - %(message)s')    

class QPlainTextEditLogger(logging.Handler):
    def __init__(self, parent):
        super().__init__()
        self.widget = QtGui.QPlainTextEdit(parent)
        self.widget.setReadOnly(True)    

    def emit(self, record):
        msg = self.format(record)
        self.widget.appendPlainText(msg)    


class MyDialog(QtGui.QDialog, QPlainTextEditLogger):
    def __init__(self, parent=None):
        super().__init__(parent)    

        logTextBox = QPlainTextEditLogger(self)
        # You can format what is printed to text box
        logTextBox.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
        logging.getLogger().addHandler(logTextBox)
        # You can control the logging level
        logging.getLogger().setLevel(logging.DEBUG)

        self._button = QtGui.QPushButton(self)
        self._button.setText('Test Me')    

        layout = QtGui.QVBoxLayout()
        # Add the new logging box widget to the layout
        layout.addWidget(logTextBox.widget)
        layout.addWidget(self._button)
        self.setLayout(layout)    

        # Connect signal to slot
        self._button.clicked.connect(self.test)    

    def test(self):
        logging.debug('damn, a bug')
        logging.info('something to remember')
        logging.warning('that\'s not right')
        logging.error('foobar')

if (__name__ == '__main__'):
    app = None
    if (not QtGui.QApplication.instance()):
        app = QtGui.QApplication([])
    dlg = MyDialog()
    dlg.show()
    dlg.raise_()
    if (app):
        app.exec_()
Community
  • 1
  • 1
Todd Vanyo
  • 517
  • 4
  • 15
  • 5
    This might be a stupid question, but what is the purpose of inheriting from `QPlainTextEditLogger` in `MyDialog`? I am trying to convert this example to PyQt5, and couldn't get it to work without removing that second inheritance. It seems to work fine without it. – Filip S. Jan 26 '18 at 08:23
14

Thread-safe version

class QTextEditLogger(logging.Handler, QtCore.QObject):
    appendPlainText = QtCore.pyqtSignal(str)

def __init__(self, parent):
    super().__init__()
    QtCore.QObject.__init__(self)
    self.widget = QtWidgets.QPlainTextEdit(parent)
    self.widget.setReadOnly(True)
    self.appendPlainText.connect(self.widget.appendPlainText)

def emit(self, record):
    msg = self.format(record)
    self.appendPlainText.emit(msg)

Usage

    logTextBox = QTextEditLogger(self)

    # log to text box
    logTextBox.setFormatter(
        logging.Formatter(
            '%(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s'))
    logging.getLogger().addHandler(logTextBox)
    logging.getLogger().setLevel(logging.DEBUG)

    # log to file
    fh = logging.FileHandler('my-log.log')
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(
        logging.Formatter(
            '%(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s'))
    logging.getLogger().addHandler(fh)
Community
  • 1
  • 1
tobilocker
  • 821
  • 8
  • 25
  • Thanks, that was quick! – Swedgin Mar 04 '20 at 16:05
  • Thank you for the answer. Although I am not sure why, with PySide2 (5.12.0) the double inheritance does not work for me. The error is that "emit() takes 2 positional argument but 3 are given". (this is the signal emit function, I think it means that there's some inheritance issue of the signal object) A working example for me is to create a new QThread/ QObject class with the Signal, and inject an instance to the log handler constructor. Somewhat like [this](https://stackoverflow.com/a/52492689/7542501) example. – Peter Jan 24 '21 at 15:51
  • 1
    Just an FYI to future readers. If you get problems with the emit method, it is probably because of naming conflict between logging.Handler and QObject. See this answer for a solution: [https://stackoverflow.com/a/66664679/4454875](https://stackoverflow.com/a/66664679/4454875) – Emil Hansen Sep 07 '21 at 08:48
3

Alex's answer should be ok in a single thread scenario, but if you are logging in another thread (QThread) you may get the following warning:

QObject::connect: Cannot queue arguments of type 'QTextCursor'
(Make sure 'QTextCursor' is registered using qRegisterMetaType().)

This is because you are modifying the GUI (self.widget.appendPlainText(msg)) from a thread other than the main thread without using the Qt Signal/Slot mechanism.

Here is my solution:

# my_logger.py

import logging
from PyQt5.QtCore import pyqtSignal, QObject

class Handler(QObject, logging.Handler):
    new_record = pyqtSignal(object)

    def __init__(self, parent):
        super().__init__(parent)
        super(logging.Handler).__init__()
        formatter = Formatter('%(asctime)s|%(levelname)s|%(message)s|', '%d/%m/%Y %H:%M:%S')
        self.setFormatter(formatter)

    def emit(self, record):
        msg = self.format(record)
        self.new_record.emit(msg) # <---- emit signal here

class Formatter(logging.Formatter):
    def formatException(self, ei):
        result = super(Formatter, self).formatException(ei)
        return result

    def format(self, record):
        s = super(Formatter, self).format(record)
        if record.exc_text:
            s = s.replace('\n', '')
        return s

   # gui.py

   ... # GUI code
   ...
   def setup_logger(self)
        handler = Handler(self)
        log_text_box = QPlainTextEdit(self)
        self.main_layout.addWidget(log_text_box)
        logging.getLogger().addHandler(handler)
        logging.getLogger().setLevel(logging.INFO)
        handler.new_record.connect(log_text_box.appendPlainText) # <---- connect QPlainTextEdit.appendPlainText slot
   ...
sifferman
  • 2,805
  • 2
  • 26
  • 34
Chweng Mega
  • 1,437
  • 10
  • 21
  • 1
    I just couldn't make this to work on PySide2 as you can't have several inheritance with QObject. To make it work I had to use old signals syntax like [this](https://stackoverflow.com/questions/14349563/how-to-get-non-blocking-real-time-behavior-from-python-logging-module-output-t) – Regnareb Jan 21 '20 at 18:57
  • Very good answer! This should be the accepted one IMHO. – blunova May 31 '22 at 22:55
0

Sounds like you'll want to use a QPlainTextEdit widget set to read-only.

Consider changing the background color to gray to give the user a hint that it is not editable. It is also up to you if you want it to be scrollable or the text selectable.

This answer can get you started subclassing QPlainTextEdit to scroll with output, save to a file, whatever.

Community
  • 1
  • 1