我有一个需要很长时间的大型程序,需要充足的日志记录。我的前端有一个 GUI,其中包含自定义日志记录处理程序,定义如下:
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())
我有主 gui (QMainWindow),它通过以下方式添加此处理程序:
# 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')
我有一个 Controller 函数,可以通过以下方式初始化日志处理程序:
# 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)
一旦 GUI 启动并初始化日志记录,我将使用以下命令完成 python 执行:
app = QApplication.instance()
if (app is None):
app = QApplication([])
app.setStyle('Fusion')
app.exec_()
GUI 有几个连接到按钮信号的插槽,这些插槽会生成线程来执行实际处理。每个处理线程都有自己的日志记录调用,这似乎按预期工作。它们的定义如下:
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
当我在没有 GUI 的情况下运行应用程序时,甚至在 GUI 最小化的情况下运行应用程序时,它每次都运行良好。我可以在控制台(命令提示符或 spyder
)中看到我的日志消息。
如果我在没有最小化 GUI 的情况下运行相同的应用程序,我将在 GUI 中看到初始化的日志消息以及线程进程的一些第一部分,但随后它会在看似随机的时间挂起。没有错误消息,并且所使用的单核的 CPU 使用率似乎已达到最大值。我添加了一个锁,只是为了确保日志记录不会来自不同的线程,但这也没有帮助。
我尝试过使用 QPlainTextEdit
和 QListWidget
但每次都会遇到同样的问题。
有人知道为什么这个 GUI 元素会导致整个 Python 解释器在 View 中挂起并记录消息吗?
最佳答案
采样的QHandler
不是线程安全的,因此如果从另一个线程调用它会产生问题,因为它是一个GUI,一个可能的解决方案是从辅助线程发送数据( def emit(self, record):
) 通过 QMetaObject
发送到 GUI 线程,为此您必须使用 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())
示例:
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_())
关于python - 通过日志记录更新 QTextEdit 时 PyQt5 程序崩溃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53285181/