10

描述:我编写了一个自定义日志处理程序,用于捕获日志事件并将它们写入 QTextBrowser 对象(工作示例代码如下所示)。

问题:按下按钮调用someProcess()。这会将两个字符串写入logger对象。但是,字符串仅在someProcess()返回后出现。

问题:如何让记录的字符串立即/实时显示在 QTextBrowser 对象中?(即一旦logger调用输出方法)

from PyQt4 import QtCore, QtGui
import sys
import time
import logging
logger = logging.getLogger(__name__)

class ConsoleWindowLogHandler(logging.Handler):
    def __init__(self, textBox):
        super(ConsoleWindowLogHandler, self).__init__()
        self.textBox = textBox

    def emit(self, logRecord):
        self.textBox.append(str(logRecord.getMessage()))

def someProcess():
    logger.error("line1")
    time.sleep(5)
    logger.error("line2")

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = QtGui.QWidget()
    textBox = QtGui.QTextBrowser()
    button = QtGui.QPushButton()
    button.clicked.connect(someProcess)
    vertLayout = QtGui.QVBoxLayout()
    vertLayout.addWidget(textBox)
    vertLayout.addWidget(button)
    window.setLayout(vertLayout)
    window.show()
    consoleHandler = ConsoleWindowLogHandler(textBox)
    logger.addHandler(consoleHandler)
    sys.exit(app.exec_())

编辑:感谢@abarnert 的回答,我设法使用QThread 编写了这段工作代码。QThread为了someProcess在后台线程中运行某些功能,我进行了子类化。对于信号,我不得不求助于旧式 Signal and Slots(我不确定如何在新式中做到这一点)。我创建了一个虚拟 QObject 以便能够从日志处理程序发出信号。

from PyQt4 import QtCore, QtGui
import sys
import time
import logging
logger = logging.getLogger(__name__)

#------------------------------------------------------------------------------
class ConsoleWindowLogHandler(logging.Handler):
    def __init__(self, sigEmitter):
        super(ConsoleWindowLogHandler, self).__init__()
        self.sigEmitter = sigEmitter

    def emit(self, logRecord):
        message = str(logRecord.getMessage())
        self.sigEmitter.emit(QtCore.SIGNAL("logMsg(QString)"), message)

#------------------------------------------------------------------------------
class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()

        # Layout
        textBox = QtGui.QTextBrowser()
        self.button = QtGui.QPushButton()
        vertLayout = QtGui.QVBoxLayout()
        vertLayout.addWidget(textBox)
        vertLayout.addWidget(self.button)
        self.setLayout(vertLayout)

        # Connect button
        self.button.clicked.connect(self.buttonPressed)

        # Thread
        self.bee = Worker(self.someProcess, ())
        self.bee.finished.connect(self.restoreUi)
        self.bee.terminated.connect(self.restoreUi)

        # Console handler
        dummyEmitter = QtCore.QObject()
        self.connect(dummyEmitter, QtCore.SIGNAL("logMsg(QString)"),
                     textBox.append)
        consoleHandler = ConsoleWindowLogHandler(dummyEmitter)
        logger.addHandler(consoleHandler)

    def buttonPressed(self):
        self.button.setEnabled(False)
        self.bee.start()

    def someProcess(self):
        logger.error("starting")
        for i in xrange(10):
            logger.error("line%d" % i)
            time.sleep(2)

    def restoreUi(self):
        self.button.setEnabled(True)

#------------------------------------------------------------------------------
class Worker(QtCore.QThread):
    def __init__(self, func, args):
        super(Worker, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)

#------------------------------------------------------------------------------
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())
4

4 回答 4

8

这里真正的问题是你通过在主线程中休眠来阻塞整个 GUI 5 秒钟。您不能这样做,或者不会显示更新,用户将无法与您的应用程序交互等。日志记录问题只是该主要问题的次要结果。

如果你的真实程序从第三方模块调用一些代码需要 5 秒或者做一些阻塞,它会遇到完全相同的问题。

一般来说,有两种方法可以在不阻塞 GUI(或其他基于事件循环的)应用程序的情况下进行缓慢的阻塞操作:

  1. 在后台线程中完成工作。根据您的 GUI 框架,您通常不能从后台线程直接调用 GUI 上的函数或修改其对象;相反,您必须使用某种机制将消息发布到事件循环。在 Qt 中,您通常通过信号槽机制来执行此操作。有关详细信息,请参阅此问题

  2. 将作业分解为快速返回的非阻塞或仅保证非常短期阻塞的作业,每个作业在返回之前安排下一个。(对于一些 GUI 框架,您可以通过调用类似的东西safeYield或递归地调用事件循环来执行等效的内联操作,但您不能使用 Qt 来执行此操作。)

鉴于这someProcess是一些您无法修改的外部代码,它要么需要几秒钟才能完成,要么会阻塞,因此您不能使用选项 2。因此,选项 1 是:在后台线程中运行它。

幸运的是,这很容易。Qt 有办法做到这一点,但 Python 的方法更简单:

t = threading.Thread(target=someProcess)
t.start()

现在,您需要进行更改,以便它发送一个信号以在主线程中完成ConsoleWindowLogHandler.emit,而不是直接修改。textBox有关所有详细信息,请参阅线程和 QObjects,以及一些很好的示例。

更具体地说:Mandelbrot 示例使用的 aRenderThread实际上并不绘制任何东西,而是发送一个renderedImage信号;MandelbrotWidget然后有一个插槽updatePixmap,它连接到renderedImage信号。同样,您的日志处理程序实际上不会更新文本框,而是发送一个gotLogMessage信号;那么您将拥有一个连接到该信号的插槽LogTextWidgetupdateLog当然,对于您的简单情况,您可以将它们放在一个类中,只要您使用信号槽连接而不是直接方法调用将两侧连接起来。

您可能想在关机期间保留t在某个地方join,或者设置t.daemon = True.

无论哪种方式,如果您想知道何时someProcess完成,您需要在完成后使用其他方式与主线程进行通信——同样,对于 Qt,通常的答案是发送信号。这也可以让您从someProcess. 而且您无需进行修改someProcess即可执行此操作;只需定义一个调用someProcess并发出其结果信号的包装函数,然后从后台线程调用该包装函数。

于 2013-01-16T00:37:40.157 回答
3

Build on top of @Gilead's code and @Cecil's suggestions, I update the code by changing the old-style to new-style signal/slot and changing the QTextBrowser to QTextEdit.

import sys
import time
import logging
from qtpy.QtCore import QObject, Signal, QThread
from qtpy.QtWidgets import QWidget, QTextEdit, QPushButton, QVBoxLayout

logger = logging.getLogger(__name__)


class ConsoleWindowLogHandler(logging.Handler, QObject):
    sigLog = Signal(str)
    def __init__(self):
        logging.Handler.__init__(self)
        QObject.__init__(self)

    def emit(self, logRecord):
        message = str(logRecord.getMessage())
        self.sigLog.emit(message)


class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()

        # Layout
        textBox = QTextEdit()
        textBox.setReadOnly(True)
        self.button = QPushButton('Click')
        vertLayout = QVBoxLayout()
        vertLayout.addWidget(textBox)
        vertLayout.addWidget(self.button)
        self.setLayout(vertLayout)

        # Connect button
        #self.button.clicked.connect(self.someProcess) # blocking
        self.button.clicked.connect(self.buttonPressed)

        # Thread
        self.bee = Worker(self.someProcess, ())
        self.bee.finished.connect(self.restoreUi)
        self.bee.terminated.connect(self.restoreUi)

        # Console handler
        consoleHandler = ConsoleWindowLogHandler()
        consoleHandler.sigLog.connect(textBox.append)
        logger.addHandler(consoleHandler)

    def buttonPressed(self):
        self.button.setEnabled(False)
        self.bee.start()

    def someProcess(self):
        logger.error("starting")
        for i in range(10):
            logger.error("line%d" % i)
            time.sleep(2)

    def restoreUi(self):
        self.button.setEnabled(True)


class Worker(QThread):
    def __init__(self, func, args):
        super(Worker, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def main():
    from qtpy.QtWidgets import QApplication
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
于 2018-08-31T19:10:15.993 回答
1

这是另一种方法。在这个例子中,我通过继承和StreamHandler向写入缓冲区的记录器添加 a :当处理程序遇到非空字符串时,将发出信号并在槽中捕获信号。QObjectStringIObufferMessageon_bufferMessage

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import logging, StringIO, time

from PyQt4 import QtCore, QtGui

class logBuffer(QtCore.QObject, StringIO.StringIO):
    bufferMessage = QtCore.pyqtSignal(str)

    def __init__(self, *args, **kwargs):
        QtCore.QObject.__init__(self)
        StringIO.StringIO.__init__(self, *args, **kwargs)

    def write(self, message):
        if message:
            self.bufferMessage.emit(unicode(message))

        StringIO.StringIO.write(self, message)

class myThread(QtCore.QThread):
    def __init__(self, parent=None):
        super(myThread, self).__init__(parent)
        self.iteration = None

    def start(self):
        self.iteration = 3

        return super(myThread, self).start()

    def run(self):        
        while self.iteration:
            logging.info("Hello from thread {0}! {1}".format(0, self.iteration))
            self.iteration -= 1

            time.sleep(3)

class myThread1(QtCore.QThread):
    def __init__(self, parent=None):
        super(myThread1, self).__init__(parent)
        self.iteration = None
        self.logger = logging.getLogger(__name__)

    def start(self):
        self.iteration = 3

        return super(myThread1, self).start()

    def run(self):        
        time.sleep(1)
        while self.iteration:
            self.logger.info("Hello from thread {0}! {1}".format(1, self.iteration))
            self.iteration -= 1

            time.sleep(3)


class myWindow(QtGui.QWidget):
    def __init__(self, parent=None):
        super(myWindow, self).__init__(parent)

        self.pushButton = QtGui.QPushButton(self)
        self.pushButton.setText("Send Log Message")
        self.pushButton.clicked.connect(self.on_pushButton_clicked)

        self.pushButtonThread = QtGui.QPushButton(self)
        self.pushButtonThread.setText("Start Threading")
        self.pushButtonThread.clicked.connect(self.on_pushButtonThread_clicked)

        self.lineEdit = QtGui.QLineEdit(self)
        self.lineEdit.setText("Hello!")

        self.label = QtGui.QLabel(self)

        self.layout = QtGui.QVBoxLayout(self)
        self.layout.addWidget(self.lineEdit)
        self.layout.addWidget(self.pushButton)
        self.layout.addWidget(self.pushButtonThread)
        self.layout.addWidget(self.label)

        self.logBuffer = logBuffer()
        self.logBuffer.bufferMessage.connect(self.on_logBuffer_bufferMessage)

        logFormatter = logging.Formatter('%(levelname)s: %(message)s')

        logHandler = logging.StreamHandler(self.logBuffer)
        logHandler.setFormatter(logFormatter)

        self.logger = logging.getLogger()
        self.logger.setLevel(logging.INFO)
        self.logger.addHandler(logHandler)

        self.thread = myThread(self)
        self.thread1 = myThread1(self)

    @QtCore.pyqtSlot()
    def on_pushButtonThread_clicked(self):
        self.thread.start()
        self.thread1.start()

    @QtCore.pyqtSlot(str)
    def on_logBuffer_bufferMessage(self, message):
        self.label.setText(message)

    @QtCore.pyqtSlot()
    def on_pushButton_clicked(self):
        message = self.lineEdit.text()
        self.logger.info(message if message else "No new messages")

if __name__ == "__main__":
    import sys

    app = QtGui.QApplication(sys.argv)
    app.setApplicationName('myWindow')

    main = myWindow()
    main.show()

    sys.exit(app.exec_())

这种方法最好的一点是,您可以记录来自主应用程序的模块/线程的消息,而不必保留对记录器的任何引用,例如,通过调用logging.log(logging.INFO, logging_message)logging.info(logging_message)

于 2013-01-16T08:15:06.570 回答
1

翻译 JoeXinfa 对 PyQt5 的回答:

import sys
import time
import logging
from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import QWidget, QTextEdit, QPushButton, QVBoxLayout, QApplication

logger = logging.getLogger(__name__)


class ConsoleWindowLogHandler(logging.Handler, QObject):
    sigLog = pyqtSignal(str)
    def __init__(self):
        logging.Handler.__init__(self)
        QObject.__init__(self)

    def emit(self, logRecord):
        message = str(logRecord.getMessage())
        self.sigLog.emit(message)


class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()

        # Layout
        textBox = QTextEdit()
        textBox.setReadOnly(True)
        self.button = QPushButton('Click')
        vertLayout = QVBoxLayout()
        vertLayout.addWidget(textBox)
        vertLayout.addWidget(self.button)
        self.setLayout(vertLayout)

        # Connect button
        #self.button.clicked.connect(self.someProcess) # blocking
        self.button.clicked.connect(self.buttonPressed)

        # Thread
        self.bee = Worker(self.someProcess, ())
        self.bee.finished.connect(self.restoreUi)
        self.bee.terminate()

        # Console handler
        consoleHandler = ConsoleWindowLogHandler()
        consoleHandler.sigLog.connect(textBox.append)
        logger.addHandler(consoleHandler)

    def buttonPressed(self):
        self.button.setEnabled(False)
        self.bee.start()

    def someProcess(self):
        logger.error("starting")
        for i in range(10):
            logger.error("line%d" % i)
            time.sleep(2)

    def restoreUi(self):
        self.button.setEnabled(True)


class Worker(QThread):
    def __init__(self, func, args):
        super(Worker, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def main():
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
于 2020-11-12T08:17:59.057 回答