|
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
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)
Anmeldungsdatum: 7. Juni 2007
Beiträge: 254
|
Danke! Genau das, wonach ich gesucht habe.
|
|
jAIk
(Themenstarter)
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
| 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)
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:
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:
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)
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)
Anmeldungsdatum: 7. Juni 2007
Beiträge: 254
|
It works. Vielen Lieben Dank für Deine / Eure Hilfe!
|