10

説明:ログイベントをキャプチャしてQTextBrowserオブジェクトに書き込むためのカスタムログハンドラーを作成しました(以下に示す作業サンプルコード)。

問題:ボタンを押すとが呼び出されますsomeProcess()。これにより、2つの文字列が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信号を送るために、私は古いスタイルの信号とスロットに頼らなければなりませんでした(新しいスタイルでそれを行う方法がわかりません)。ロギングハンドラーからシグナルを送信できるようにするために、ダミーの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(または他のイベントループベース)アプリをブロックせずにブロックすることで、速度を落とすには2つの方法があります。

  1. バックグラウンドスレッドで作業を行います。GUIフレームワークによっては、バックグラウンドスレッドから、通常、GUIで関数を直接呼び出したり、そのオブジェクトを変更したりすることはできません。代わりに、何らかのメカニズムを使用してメッセージをイベントループに投稿する必要があります。Qtでは、通常、シグナルスロットメカニズムを介してこれを行います。詳細については、この質問を参照してください。

  2. ジョブを非ブロッキングジョブまたは保証付きのみの非常に短期間のブロッキングジョブに分割します。これらのジョブはすぐに戻り、それぞれが戻る直前に次のスケジュールを設定します。(一部のGUIフレームワークでは、のようなものを呼び出すか、イベントループを再帰的に呼び出すことで、同等のインラインsafeYieldを実行できますが、Qtでは実行しません。)

これsomeProcessは変更できない外部コードであり、終了するのに数秒かかるか、何かをブロックするため、オプション2を使用することはできません。したがって、オプション1は、バックグラウンドスレッドで実行します。

幸い、これは簡単です。Qtにはこれを行う方法がありますが、Pythonの方法はさらに簡単です。

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

ConsoleWindowLogHandler.emitここで、を直接変更するのではなくtextBox、メインスレッドでそれを実行するためのシグナルを送信するように変更する必要があります。すべての詳細といくつかの良い例については、スレッドとQObjectを参照してください。

より具体的には、マンデルブロの例では、RenderThread実際には何も描画せず、代わりにrenderedImage信号を送信するを使用しています。MandelbrotWidget次に、信号に接続するスロットがupdatePixmapあります。renderedImage同様に、ログハンドラーは実際にはテキストボックスを更新しませんが、代わりにgotLogMessageシグナルを送信します。次に、その信号に接続するスロットがありますLogTextWidgetupdateLogもちろん、単純なケースでは、直接のメソッド呼び出しではなく、信号スロット接続で2つの側を接続する限り、それらを1つのクラスにまとめることができます。

おそらく、シャットダウン中にtどこかにそれを保持するか、設定する必要があります。joint.daemon = True

いずれにせよ、いつ完了したかを知りたい場合someProcessは、完了時にメインスレッドに通信する他の手段を使用する必要があります。Qtでは、通常の答えはシグナルを送信することです。また、これにより、から結果を取り戻すこともできますsomeProcess。そして、これを行うために変更する必要はありませんsomeProcess。結果を呼び出して通知するラッパー関数を定義し、someProcessそのラッパー関数をバックグラウンドスレッドから呼び出すだけです。

于 2013-01-16T00:37:40.157 に答える
3

@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()
于 2018-08-31T19:10:15.993 に答える
1

別の方法があります。この例では、ロガーにを追加します。ロガーは、とStreamHandlerの両方から継承してバッファーに書き込みます。ハンドラーが空でない文字列に遭遇すると、信号が送信され、スロットにキャプチャされます。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 に答える