staging.inyokaproject.org

Python: Anfängerfrage - print() in Qt TextBrowser umleiten

Status: Gelöst | Ubuntu-Version: Ubuntu 20.04 (Focal Fossa)
Antworten |

jAIk

Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

tl;dr: In meinem python Programm kommt in einer Methode mehrfach der Befehl print(foo) vor. Statt print(foo) soll die Meldung im TextBrowser meines Qt Fensters angezeigt werden. Wie setze ich das am besten um?

Hallo zusammen,

ich mache gerade meine ersten Erfahrungen mit Qt in der Programmiersprache Python. In Python habe ich ein kleines Programm geschrieben. Das lief bislang immer nur auf der Kommandozeile.

Nun spendiere ich der Anwendung eine grafische Oberfläche und mache daher erste Gehversuche in Qt. Meine Qt Klasse sieht dabei wie folgt aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys
from qtpy import QtWidgets

from qt.surface.mainwindow import Ui_MainWindow

app = QtWidgets.QApplication(sys.argv)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.ui.btKlickMich.clicked.connect(self.on_button_clicked)

    def on_button_clicked(self):
        self.ui.tbConsole.append("Hallo Welt")


...

window = MainWindow()
window.show()

sys.exit(app.exec_())

Statusinformationen habe ich in meinem Programm zuvor immer über die print() Funktion ausgegeben.

Die gezeigt Qt-Klasse ruft, z.B. beim Klick auf einen Button, Methoden anderer Klassen meines Programms auf. Einziges Problem: Die Statusmeldungen der einzelnen Methoden erscheinen natürlich weiterhin in der Konsole. In meinem Qt Fenster gibt es einen TextBrowser, den ich innerhalb der oben gezeigten Klasse via self.ui.tbConsole.append("Foo") ansteuern kann. Jetzt frage ich mich: Wie ist der gängige Weg, um Statusmeldungen von Methoden außerhalb der Qt-Klasse an mein TextBrowser Element zu übergeben?

  • Referenziere ich beide Klassen jeweils aufeinander? Kommt mir irgendwie komisch vor.

  • Kopiere ich die besagten Methoden in die Qt-Klasse?

  • Gebe ich Statusmeldungen über return zurück und zeige diese "erst" nach dem Aufruf der Methode an?

  • ...

Würde mich über eure Hilfe sehr freuen.

ChickenLipsRfun2eat Team-Icon

Anmeldungsdatum:
6. Dezember 2009

Beiträge: 12067

Hallo!

Bei Python muss ich leider passen, da ich Qt bisher nur mit C++ verwendet habe. Da gibt es (wie immer) viele Wege, die nach Rom führen. Ein eigener Nachrichten-handler könnte mittels QAbstractMessageHandler eingerichtet werden, das ist aber für einfache Statusaufgaben uninteressant.
Für solche Nachrichten kann man entweder QMessageBox verwenden oder sich ein „Statusanzeigeobjekt“ anlegen, wie eine QListView mit QStringList als model, oder einfach ein QLabel, dessen Text man immer durch die aktuelle Nachricht austauscht. Für dein „Hallo Welt“ würde ich wohl die QMessageBox nutzen.

QMainWindow liefert übrigens eine Statusbar mit: statusBar() / setStatusBar(). Diese kannst du auch nutzen und dort dein QLabel reinsetzen.

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Eine recht elegante Möglichkeit wäre von print() auf das logging-Modul umzustellen (Einführung dazu).

Für den Logger kann man dann einen (oder mehrere) Handler setzen, die dafür sorgen, dass die Logmessages dort landen, wo man sie haben will - ein Beispiel wie man das mit Signalen an ein QT-Widget anflanschen kann, gibt es z.B. in der Lösung https://stackoverflow.com/questions/14349563/how-to-get-non-blocking-real-time-behavior-from-python-logging-module-output-t.

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Danke! Genau das, wonach ich gesucht habe.

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Nach ein paar Versuchen bin mir eherlich gesagt noch nicht ganz sicher, ob ich die Theorie in der Praxis korrekt umsetze. Das Beispiel von StackOverflow gefällt mir super. Die Implementierung klappt noch nicht so richtig. Daher möchte ich gerne nachvollziehen, wie ich Signale von einer Funktion an Qt sende.

Das folgende Ergebnis funktioniert. Mich würde aber interessieren: Habe ich die Theorie hier in (zugegeben sehr simpler Form) korrekt umgesetzt oder ist das aus irgend einem Grund schlechter Stil?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#Emitter Klasse
class Emiterer(QtCore.QThread):
    f = QtCore.pyqtSignal(int)

    def __init__(self):
        super(Emiterer, self).__init__()

    def run(self):
        self.f.emit(42)

#Qt Fenster
class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent = None):
        super().__init__(parent)

        self.e = Emiterer()
        self.e.f.connect(self.receive_signal)

        #Klick auf btGo: execute_method
        self.ui.btGo.clicked.connect(self.execute_method)

    def execute_method(self):
        #run method
        self.e.start()

    def receive_signal(self, return_value):
        print(return_value)
        self.ui.tbConsole.append(str(return_value))

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

jAIk schrieb:

Das folgende Ergebnis funktioniert. Mich würde aber interessieren: Habe ich die Theorie hier in (zugegeben sehr simpler Form) korrekt umgesetzt oder ist das aus irgend einem Grund schlechter Stil?

Wenn ich das richtig verstehe, kann man die init Methode der Klasse Emiterer einfach weglassen, dann wird automatisch die von QThread ausgeführt.

Ansonsten umgehst du die Nutzung des Logging-Moduls komplett - das führt zu Code, der von QT-Signalen abhängig ist. Das nette wenn man Bibliotheken mit dem Logging-Modul bestückt ist, dass die Anwendung, die den Logger konfiguriert entscheiden kann, was mit dem Logmeldungen passieren soll und welche Handler sich darum kümmern, dass sie ausgeführt werden.

Mal als Beispiel für das Logging aus einem anderen Modul:

mylib.py

1
2
3
4
5
import logging
logger = logging.getLogger(__name__)

def do_something():
    logger.info("I am doing something")

main.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python3
from datetime import datetime
import sys
import time
import logging
from qtpy.QtCore import QObject, Signal, QThread
from qtpy.QtWidgets import QWidget, QTextEdit, QPushButton, QVBoxLayout

import mylib

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


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

    def emit(self, logRecord):
        ts = datetime.fromtimestamp(logRecord.created)
        message = f"{datetime.isoformat(ts, timespec='seconds')}:{logRecord.levelname}:{logRecord.name}: {logRecord.getMessage()}"
        self.sigLog.emit(message)


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

        # Size
        self.setMinimumSize(640, 480)

        # Layout
        self.textBox = QTextEdit()
        self.textBox.setReadOnly(True)
        self.button = QPushButton('Click Me')
        vertLayout = QVBoxLayout()
        vertLayout.addWidget(self.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)

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

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

    def someProcess(self):
        logger.info("starting process")
        for i in range(10):
            logger.debug(f"iteration {i}")
            mylib.do_something()
            time.sleep(1)

    def restoreUi(self):
        logger.debug("re-enable button")
        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()
    mylib.logger.addHandler(window.consoleHandler)  # wire up the Handler for the module
    mylib.logger.setLevel(logging.INFO)  # set the desired log level for the module's logger
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Super cooler Code ☺. Danke!

Ich habe mich dann direkt noch ein bisschen mit Logging beschäftigt[1] und glaube, dass ich ein erstes oberflächliches Verständnis gewinnen konnte.

In Anlehnung an deinen Code habe ich nun folgendes geschrieben:

UI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import ...
import mylib

...

logger = logging.getLogger(__name__)

#keine Veränderung der ConsoleWindowLogHandler Klasse
class ConsoleWindowLogHandler(logging.Handler, QObject):
    sigLog = Signal(str)
    def __init__(self):
        logging.Handler.__init__(self)
        QObject.__init__(self)

    def emit(self, logRecord):
        ts = datetime.fromtimestamp(logRecord.created)
        message = f"{datetime.isoformat(ts, timespec='seconds')}:{logRecord.levelname}:{logRecord.name}: {logRecord.getMessage()}"
        self.sigLog.emit(message)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent = None):
        ...
        super().__init__(parent)
        #Nach meinem Verständnis: Erstellung des Handlers und des Connectors
        self.consoleHandler = ConsoleWindowLogHandler()
        self.consoleHandler.setLevel(logging.DEBUG)
        self.consoleHandler.sigLog.connect(self.ui.TextBox.append)
        ...
        #Klick auf den Button
        self.ui.mybutton.clicked.connect(self.on_button_click_test)
        ...
        #Aufruf der mylib.do_something Methode (s.u.)
    def on_button_click_test(self):
        mylib.logger.addHandler(self.consoleHandler)  # wire up the Handler for the module #Nach meinem Verständnis: Übergabe des Handlers
        mylib.logger.setLevel(logging.DEBUG)  # set the desired log level for the module's logger #Nach meinem Verständnis: Log-Level. Sollte gesetzt werden, da sonst WARNING
        mylib.do_something()

mylib.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import logging
import time
logger = logging.getLogger(__name__)

def do_something():
    print("Hello World")
    logger.debug("Debug-Level")
    time.sleep(2)
    logger.info("Info-Level")
    time.sleep(2)
    logger.warning("Warning-Level")
    time.sleep(1)

Dazu zwei Fragen:

  1. Viele Wege führen bekanntlich nach Rom. Aber habe ich das Konzept soweit verstanden?

2. Bei diesem Stand muss die Methode mylib.do_something() erst durchlaufen, bevor die Log-Einträge in meiner TextBox angezeigt werden. Das wäre im Vergleich zur Konsolen-Version mit "print" durchaus ein Nachteil. Geht das vielleicht in Echtzeit? *schnell wegduck*

Danke soweit auf jeden Fall für die Hilfe!

VG jaik

[1]https://www.youtube.com/watch?v=-ARI4Cz-awo

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

jAIk schrieb:

Dazu zwei Fragen:

  1. Viele Wege führen bekanntlich nach Rom. Aber habe ich das Konzept soweit verstanden?

Da bin ich mir nicht sicher.

2. Bei diesem Stand muss die Methode mylib.do_something() erst durchlaufen, bevor die Log-Einträge in meiner TextBox angezeigt werden. Das wäre im Vergleich zur Konsolen-Version mit "print" durchaus ein Nachteil. Geht das vielleicht in Echtzeit? *schnell wegduck*

Ja, siehe mein Beispiel. Was du machst ist Code linear auszuführen - damit kann das UI-Framework nicht auf Dinge wie Signale reagieren, solange etwas anderes läuft. Daher der Ansatz aus dem StackOverflow-Thread die Funktionsaufrufe, die durch den Button getriggert werden, jeweils in einen eigenen Thread auszulagern.

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Mist, hast recht... 😀

Prinzip ist mir noch nicht ganz klar. Danke für die Erklärung, ich lese dann nochmal ein bisschen... ☺

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

It works. Vielen Lieben Dank für Deine / Eure Hilfe!

Antworten |