PyQt5 Program Crashes While Updating QTextEdit via

2019-02-20 04:59发布

问题:

I have a large program that takes a long time that needs ample logging. I have a GUI that is on the front end that includes a custom logging handler as defined below:

class QHandler(logging.Handler, QTextEdit):
    def __init__(self, parent=None):
        QTextEdit.__init__(self, parent)
        logging.Handler.__init__(self)

        self.setLineWrapMode(QTextEdit.NoWrap)
        self.setReadOnly(True)

        self.emit_lock = Lock()

    def emit(self, record):
        with self.emit_lock:
            self.append(self.format(record))
            self.autoScroll()

    def format(self, record):
        if (record.levelno <= logging.INFO):
            bgcolor = WHITE
            fgcolor = BLACK
        if (record.levelno <= logging.WARNING):
            bgcolor = YELLOW
            fgcolor = BLACK
        if (record.levelno <= logging.ERROR):
            bgcolor = ORANGE
            fgcolor = BLACK
        if (record.levelno <= logging.CRITICAL):
            bgcolor = RED
            fgcolor = BLACK
        else:
            bgcolor = BLACK
            fgcolor = WHITE

        self.setTextBackgroundColor(bgcolor)
        self.setTextColor(fgcolor)
        self.setFont(DEFAULT_FONT)
        record = logging.Handler.format(self, record)
        return record

    def autoScroll(self):
        self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().maximum())

I have the main gui (QMainWindow) that adds this handler via:

# inside __init__ of main GUI (QMainWindow):
self.status_handler = QHandler()
# Main gui is divided into tabs and the status handler box is added to the second tab
main_tabs.addTab(self.status_handler, 'Status') 

And I have the controller function that initializes the logging handler via:

# inside controller initializing function
gui = gui_class() # this is the main gui that initializes the handler among other things
logger = logging.getLogger()
gui.status_handler.setFormatter(file_formatter) # defined elsewhere
logger.addHandler(gui.status_handler)

Once the GUI is raised and logging is initialized, I finish the python execution with:

app = QApplication.instance()
if (app is None):
    app = QApplication([])
    app.setStyle('Fusion')
app.exec_()

The GUI has a few slots connected to pushbutton signals that spawns threads to do the actual processing. Each processing thread has it's own logging call which seems to work as intended. They are defined like follows:

class Subprocess_Thread(Thread):
    def __init__(self, <args>):
        Thread.__init__(self)
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.info('Subprocess Thread Created')

    def run(self):
        # does a bunch of stuff
        self.logger.info('Running stuff')
        # iterates over other objects and calls on them to do stuff
        # where they also have a logger attached and called just like above

When I run my application without a GUI or even with the GUI minimized, it runs fine every time. I can see my log messages in the console (either command prompt or spyder).

If I run the same application without minimizing the GUI, I will see the log messages in the GUI for initialization and some of the first parts of the threaded process, but then it will hang at seemingly random times. There is no error message and the CPU usage seems to be maxed out for the single core that is being used. I included a lock just to make sure logging wasn't coming in from different threads, but that also didn't help.

I've tried going to a QPlainTextEdit and a QListWidget but I get the same problem every time.

Does anyone have an idea of why this GUI element would cause the entire Python interpreter to hang when in view and messages are logged to it?

回答1:

The QHandler that samples is not thread-safe so it will generate problems if you call it from another thread since it is a GUI, a possible solution is to send the data from the secondary thread(def emit(self, record):) to the thread of the GUI through QMetaObject for this you must use pyqtSlot:

class QHandler(logging.Handler, QtWidgets.QTextEdit):
    def __init__(self, parent=None):
        QtWidgets.QTextEdit.__init__(self, parent)
        logging.Handler.__init__(self)

        self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
        self.setReadOnly(True)

        self.emit_lock = threading.Lock()

    def emit(self, record):
        with self.emit_lock:
            QtCore.QMetaObject.invokeMethod(self, 
                "append",  
                QtCore.Qt.QueuedConnection,
                QtCore.Q_ARG(str, self.format(record)))
            QtCore.QMetaObject.invokeMethod(self, 
                "autoScroll",
                QtCore.Qt.QueuedConnection)

    def format(self, record):
        if record.levelno == logging.INFO:
            bgcolor = WHITE
            fgcolor = BLACK
        elif record.levelno == logging.WARNING:
            bgcolor = YELLOW
            fgcolor = BLACK
        elif record.levelno == logging.ERROR:
            bgcolor = ORANGE
            fgcolor = BLACK
        elif record.levelno == logging.CRITICAL:
            bgcolor = RED
            fgcolor = BLACK
        else:
            bgcolor = BLACK
            fgcolor = WHITE

        self.setTextBackgroundColor(bgcolor)
        self.setTextColor(fgcolor)
        self.setFont(DEFAULT_FONT)
        record = logging.Handler.format(self, record)
        return record

    @QtCore.pyqtSlot()
    def autoScroll(self):
        self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().maximum())

Example:

import random
import logging
import threading
from PyQt5 import QtCore, QtGui, QtWidgets

WHITE, BLACK, YELLOW, ORANGE, RED = QtGui.QColor("white"), QtGui.QColor("black"), QtGui.QColor("yellow"), QtGui.QColor("orange"), QtGui.QColor("red")
DEFAULT_FONT = QtGui.QFont()

class QHandler(logging.Handler, QtWidgets.QTextEdit):
    def __init__(self, parent=None):
        QtWidgets.QTextEdit.__init__(self, parent)
        logging.Handler.__init__(self)

        self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
        self.setReadOnly(True)

        self.emit_lock = threading.Lock()

    def emit(self, record):
        with self.emit_lock:
            QtCore.QMetaObject.invokeMethod(self, 
                "append",  
                QtCore.Qt.QueuedConnection,
                QtCore.Q_ARG(str, self.format(record)))
            QtCore.QMetaObject.invokeMethod(self, 
                "autoScroll",
                QtCore.Qt.QueuedConnection)

    def format(self, record):
        if record.levelno == logging.INFO:
            bgcolor = WHITE
            fgcolor = BLACK
        elif record.levelno == logging.WARNING:
            bgcolor = YELLOW
            fgcolor = BLACK
        elif record.levelno == logging.ERROR:
            bgcolor = ORANGE
            fgcolor = BLACK
        elif record.levelno == logging.CRITICAL:
            bgcolor = RED
            fgcolor = BLACK
        else:
            bgcolor = BLACK
            fgcolor = WHITE

        self.setTextBackgroundColor(bgcolor)
        self.setTextColor(fgcolor)
        self.setFont(DEFAULT_FONT)
        record = logging.Handler.format(self, record)
        return record

    @QtCore.pyqtSlot()
    def autoScroll(self):
        self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().maximum())


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.status_handler = QHandler()
        self.setCentralWidget(self.status_handler)

        logging.getLogger().addHandler(self.status_handler)
        self.status_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
        logging.getLogger().setLevel(logging.DEBUG)
        timer = QtCore.QTimer(self, interval=1000, timeout=self.on_timeout)
        timer.start()

    def on_timeout(self):
        logging.info('From Gui Thread {}'.format(QtCore.QDateTime.currentDateTime().toString()))


class Subprocess_Thread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.info('Subprocess Thread Created')

    def run(self):
        while True:
            t = random.choice(["info", "warning", "error", "critical"])
            msg = "Type: {}, thread: {}".format(t, threading.currentThread())
            getattr(self.logger, t)(msg)
            QtCore.QThread.sleep(1)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
        app.setStyle('Fusion')
    w = MainWindow()
    w.show()
    th = Subprocess_Thread()
    th.daemon = True
    th.start()
    sys.exit(app.exec_())