Carlo Angelo
Carlo Angelo

Reputation: 423

Best way to display logs in pyqt?

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.

Upvotes: 27

Views: 62239

Answers (8)

Pavlo Sharhan
Pavlo Sharhan

Reputation: 194

My solution redirects stdout to QTextEdit, It works with multithreaded apps. You can also log all the data to the file if you edit text_edit_out method

class YourWidget(QWidget):
    out_signal = pyqtSignal(object)

    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.log_display = QTextEdit()
        self.log_display.setReadOnly(True)
        layout.addWidget(self.log_display)
        self.setLayout(layout)

        self.out_signal.connect(self.text_edit_out)
        sys.stdout = SignalOut(self.out_signal)
    
    def text_edit_out(self, out):
        self.log_display.insertPlainText(out)
        self.log_display.moveCursor(QTextCursor.End)

class SignalOut:
    def __init__(self, signal):
        self.signal = signal

    def write(self, text):
        self.signal.emit(text)

    def flush(self):
        pass

Upvotes: 0

Sifferman’s answer appears to be the most elegant to me. Regardless, I've tried all in this post. They all work, but notice that if you try, e.g., to write a test and create a log entry on it you might get a nasty error like

RuntimeError: wrapped C/C++ object of type QPlainTextEdit has been deleted

After loosing my mind for a couple hours, I noticed that it's quite important to delete the handler manually when closing the window,

def closeEvent(self, event):
    ...
    root_logger = logging.getLogger()
    root_logger.removeHandler(self.logger)
    ... 
    super().closeEvent(event)

Upvotes: 0

Alex
Alex

Reputation: 475

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_())

Threading issues

Note: as of late 2022, this is still the highest ranked answer. The OP doesn't seem to be active anymore, so I took the liberty of editing it because the lack of explanation of the implications of its usage is a common cause of closely related questions.

Be aware that the above code will only work for single threaded programs. If you are sure that your program doesn't use threading, then it's fine. Be aware, though: you have to be completely sure about that, also considering external modules. Qt included.

Don't underestimate this aspect: even some Qt classes that are thread safe are actually not single threaded; for instance, QFileSystemModel is a thread safe object, but it does use threading for file system crawling. If it faces a problem for any reason (ie. access permissions), it will output some debug messages from those threads, and you'll probably still get issues.

Possible results of underestimated threading issues while accessing UI elements include:

  • unexpected behavior;
  • drawing artifacts;
  • lots of "unrelated" debug messages;
  • possible fatal crash of the program;

So, as a rule of thumb, the above has to be considered as unsafe and should not be used, since it attempts to access the QPlainTextEdit widget even from external threads: widgets are not thread-safe, and can only be accessed from the main thread.
Since there is no immediate way to know if the logging source is in the same thread or not without pointlessly affecting performance, it's better to always use a thread safe solution in any case (see the other answers that consider this aspect).

Upvotes: 44

mfitzp
mfitzp

Reputation: 15545

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)

Upvotes: 23

tobilocker
tobilocker

Reputation: 981

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)

Upvotes: 23

Chweng Mega
Chweng Mega

Reputation: 1658

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
   ...

Upvotes: 5

Todd Vanyo
Todd Vanyo

Reputation: 593

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_()

Upvotes: 15

Christopher Peterson
Christopher Peterson

Reputation: 1671

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.

Upvotes: 1

Related Questions