Как получить неблокирующее поведение в режиме реального времени из модуля ведения журнала Python? (вывод в PyQt 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_())

EDIT: благодаря ответу @abarnert мне удалось написать этот фрагмент рабочего кода, используя QThread. Я создал подкласс QThread, чтобы запустить некоторую функцию someProcess в фоновом потоке. Для сигнализации мне пришлось прибегнуть к старому стилю Signal и 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_())

person Gilead    schedule 16.01.2013    source источник
comment
Если бы ваше редактирование было правильным ответом, это было бы каноническим решением общей проблемы отображения сообщений журнала в виджетах PyQt и PySide. В отличие от всех подобных решений для всех подобных вопросов (например, это, < href="https://stackoverflow.com/questions/24469662/how-to-redirect-logger-output-into-pyqt-text-widget">это), ваше редактирование использует слоты и сигналы и, следовательно, работает без блокировки. Это фантастика.   -  person Cecil Curry    schedule 20.06.2017
comment
Что менее фантастично, так это использование слотов и сигналов старого стиля и гипертекста QTextBrowser, а не открытого текста QTextArea только для чтения. Использование слотов и сигналов нового стиля должно устранить необходимость в посреднике dummyEmitter. Аналогичным образом, цитируя официальную QTextBrowser документацию: Если вам нужен текстовый браузер без гипертекста для навигации используйте QTextEdit и используйте QTextEdit::setReadOnly(), чтобы отключить редактирование.   -  person Cecil Curry    schedule 20.06.2017


Ответы (4)


Настоящая проблема здесь в том, что вы блокируете весь графический интерфейс на 5 секунд, засыпая в основном потоке. Вы не можете этого сделать, иначе обновления не появятся, пользователь не сможет взаимодействовать с вашим приложением и т. д. Проблема с ведением журнала — лишь незначительное последствие этой серьезной проблемы.

И если ваша реальная программа вызывает какой-то код из стороннего модуля, который занимает 5 секунд или делает что-то блокирующее, у нее будет точно такая же проблема.

В общем, есть два способа сделать медленные, блокирующие вещи, не блокируя приложение с графическим интерфейсом (или другое приложение, основанное на цикле событий):

  1. Выполняйте работу в фоновом потоке. В зависимости от вашей инфраструктуры графического интерфейса, из фонового потока вы обычно не можете вызывать функции непосредственно в графическом интерфейсе или изменять его объекты; вместо этого вам нужно использовать какой-то механизм для отправки сообщений в цикл событий. В Qt это обычно делается с помощью механизма сигнальных слотов. Подробности см. в этом вопросе.

  2. Разбейте задание на неблокирующие или гарантированно-только-очень-кратковременно-блокирующие задания, которые быстро возвращаются, каждое из которых планирует следующее прямо перед возвратом. (В некоторых фреймворках с графическим интерфейсом вы можете сделать то же самое, вызвав что-то вроде safeYield или рекурсивно вызвав цикл событий, но вы не делаете этого с Qt.)

Учитывая, что someProcess — это какой-то внешний код, который вы не можете изменить, который либо завершается за секунды, либо делает что-то блокирующее, вы не можете использовать вариант 2. Итак, вариант 1: запустить его в фоновом потоке.

К счастью, это легко. В Qt есть способы сделать это, но способы Python еще проще:

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

Теперь вам нужно изменить ConsoleWindowLogHandler.emit так, чтобы вместо прямого изменения textBox он отправлял сигнал для выполнения этого в основном потоке. Подробнее см. Threads и QObjects, а также некоторые полезные сведения. Примеры.

Более конкретно: пример Мандельброта использует RenderThread, который на самом деле ничего не рисует, а вместо этого посылает сигнал renderedImage; MandelbrotWidget имеет слот updatePixmap, который он соединяется с сигналом renderedImage. Точно так же ваш обработчик журнала на самом деле не обновит текстовое поле, а вместо этого отправит сигнал gotLogMessage; тогда у вас будет LogTextWidget со слотом updateLog, который соединяется с этим сигналом. Конечно, для вашего простого случая вы можете держать их вместе в одном классе, если вы соединяете две стороны с помощью соединения сигнального слота, а не прямого вызова метода.

Вы, вероятно, хотите либо оставить где-то t и join во время выключения, либо установить t.daemon = True.

В любом случае, если вы хотите знать, когда someProcess будет выполнено, вам нужно использовать другие средства обратной связи с вашим основным потоком, когда это будет сделано — опять же, с Qt обычным ответом является отправка сигнала. И это также позволяет вам получить результат от someProcess. И вам не нужно модифицировать someProcess для этого; просто определите функцию-оболочку, которая вызывает someProcess и сигнализирует о ее результате, и вызовите эту функцию-оболочку из фонового потока.

person abarnert    schedule 16.01.2013
comment
Спасибо. В моем реальном коде someProcess() вызывает модуль Python, который вызывает регистратор для предоставления обновлений статуса, и я хотел бы иметь возможность отображать эти обновления статуса в режиме реального времени в QTextBrowser. Я знаю, что subprocess позволяет мне фиксировать stdout неблокирующим способом, но в этом случае я хочу фиксировать события регистрации неблокирующим способом. Модуль Python не знает о графическом интерфейсе. - person Gilead; 16.01.2013
comment
@Gilead: использовать subprocess (или QProcess, если хотите) из фонового потока не сложнее, чем делать что-либо еще из фонового потока. Я что-то упустил здесь? Вы читали Threads и QObjects? Создать фоновые потоки несложно, а отправка из них сигналов автоматически работает между потоками, и именно так вы справляетесь с блокировкой в ​​Qt. - person abarnert; 16.01.2013
comment
Спасибо за быстрый ответ. subProcess прост в использовании, но он должен вызывать внешнюю программу - я не уверен, как использовать его с модулем Python (который я не могу изменить), плюс я не уверен, как модуль ведения журнала будет общаться, если подпроцесс является внешним. QProcess похоже. - person Gilead; 16.01.2013
comment
@Gilead: я упомянул subprocess только потому, что вы это сделали, и я подумал, что, возможно, вашему решению присуще то, что вы запускали чужой скрипт в дочернем процессе. Если вы не хотите использовать это, поток - это все, что вам нужно. Я обновил ответ, чтобы объяснить. - person abarnert; 16.01.2013
comment
Спасибо за Ваш ответ. Я никогда раньше не писал ничего ветки, и я был удивлен, что в течение нескольких часов мне удалось выяснить, как это сделать. Вы правы — это не так сложно, особенно с Python. Я прикрепил новый код выше. Кажется, он делает то, что я хочу. Смешение сигналов и слотов нового и старого стилей выглядит несколько неэлегантно, но я пока оставлю это. Спасибо еще раз! - person Gilead; 16.01.2013
comment
@Gilead: многопоточность на самом деле очень проста и эффективна, когда вы можете свести всю связь между задачами к очередям сообщений, что в основном и делают сигналы. Он позволяет вам просто писать каждую задачу так, как если бы она была последовательной и блокирующей, но она не блокируется, как по волшебству. Когда многопоточность усложняется, вам нужно обмениваться данными напрямую между задачами. (Или когда у вас есть тысячи задач, или когда у вас есть несколько задач, привязанных к процессору, и вам нужна параллельная производительность…) - person abarnert; 16.01.2013
comment
Да, я немного беспокоился о том, чтобы усложнить мой код (особенно в отношении управления потоками). Вместо этого я рассматривал возможность использования процессов, но теперь вижу, что многопоточность — действительно хорошее решение для подобных проблем. Спасибо! - person Gilead; 16.01.2013

Основываясь на коде @Gilead и предложениях @Cecil, я обновляю код, меняя сигнал/слот старого стиля на новый и меняя QTextBrowser на 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()
person JoeXinfa    schedule 31.08.2018

Вот еще один метод. В этом примере я добавляю StreamHandler к регистратору, который записывает в буфер, наследуя как QObject, так и StringIO: когда обработчик встречает непустую строку, сигнал bufferMessage генерируется и фиксируется в слоте on_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)

person Community    schedule 16.01.2013
comment
О, это отлично! Я мог бы использовать это для другого фрагмента кода, который у меня есть. Мой текущий фрагмент кода зависит от нескольких обработчиков журналов, поэтому мне нужна ссылка на logger. Но благодаря вашему примеру я теперь вижу, как писать сигнал и слоты в новом стиле. Спасибо! - person Gilead; 16.01.2013
comment
@Gilead Я рад, что вы нашли это полезным, но я ненавижу настаивать! :) С этим кодом вам не нужно сохранять какие-либо ссылки на регистратор, пока он находится в том же процессе интерпретатора Python. Мой ответ демонстрирует это, вызывая logging.info вместо logger.info из myThread... вы также можете направить вывод в конкретный регистратор, вызвав logging.getLogger('someLogger') - person ; 16.01.2013
comment
как можно добавить пользовательские обработчики журналов без экземпляра журнала? Мне нужна эта функциональность, а у logging нет метода addHandler. Я не уверен, что понимаю (но заметьте, я впервые использую модуль logging, поэтому не понимаю нюансов!) - person Gilead; 16.01.2013
comment
Я буду рад помочь вам с этим, пожалуйста, напишите это как вопрос и упомяните меня в комментарии здесь - person ; 17.01.2013

Перевод ответа 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()
person the_economist    schedule 12.11.2020