如何从Python日志记录模块获取非阻塞/实时行为?(输出到PyQt QTextBrowser)

Gil*_*ead 8 python logging pyqt nonblocking qtextbrowser

描述:我编写了一个自定义日志处理程序,用于捕获日志事件并将它们写入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_())
Run Code Online (Sandbox Code Playgroud)

编辑:感谢@abarnert的回答,我设法使用QThread编写了这段工作代码.我QThread为了someProcess在后台线程中运行某些函数而进行了子类化.对于信令,我不得不求助于旧式的信号和插槽(我不知道如何以新的方式进行).我创建了一个虚拟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_())
Run Code Online (Sandbox Code Playgroud)

aba*_*ert 7

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

如果您的真实程序从第三方模块调用一些代码需要5秒钟或阻塞某些内容,那么它将会遇到完全相同的问题.

通常,有两种方法可以在不阻止GUI(或其他基于事件循环的)应用程序的情况下缓慢阻止事物:

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

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

鉴于这someProcess是一些您无法修改的外部代码,无论是花费几秒钟完成还是阻塞,您都无法使用选项2.因此,选项1是:在后台线程中运行它.

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

t = threading.Thread(target=someProcess)
t.start()
Run Code Online (Sandbox Code Playgroud)

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

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

您可能希望在关闭期间保持t在某处join,或者设置t.daemon = True.

无论哪种方式,如果你想知道什么时候someProcess完成,你需要使用其他方式在完成时回到主线程 - 再次使用Qt,通常的答案是发送信号.这也可以让你从中获得结果someProcess.你不需要修改someProcess来做到这一点; 只需定义一个调用someProcess并发出结果信号的包装函数,然后从后台线程调用该包装函数.