Komplexeres Programm mit Qt

L I V E Stammtisch ab 20:30 Uhr im Chat
  • Hallo zusammen,

    nachdem ich mit einer neuen SD-Karte meinen Pi wieder zum laufen gebracht hab, hab ich mich einem neuen Thema gewidmet, einem Python-Programm mit GUI via Qt.

    Bin da über ein Tutorial drauf gekommen, das aber nur kleine Programme als Beispiele hat, die nur über Buttons Funktionen ausführen.

    Das Programm, das ich vor hab, ist aber etwas komplexer:

    Es verbindet sich über TCP-Sockets mit einem Server und soll Aktionen aufgrund von GUI-Aktionen, als auch Befehlen über Sockets ausführen.

    Wie realisier ich sowas? GUI-Aktionen triggern Funktionen, da ist nichts dabei. Aber Sockets? Ich bräuchte ja eigentlich eine Schleife, die den Socket-Eingang immer wieder überprüft und dementsprechend abarbeitet. Aber nach kurzer Google-Suche darf man bei GUIs in Python keine while True-Schleifen verwenden?

    Wie kann ich das dann realisieren? Kann mir da mal jemand einen Tipp oder Pseudo-Code geben?

    Vielen Dank und

    liebe Grüße

    Fipsi

  • Hallo,

    für Netzwerkprogrammierung findet man zu Qt unter anderem soetwas:

    https://twistedmatrix.com/trac/wiki/QTReactor

    Wenn du allgemein während der Laufzeit eines GUI's eine Schleife benötigst, dann musst du schauen was dein gewünschtes Framework anbietet. Das ist dann in der Dokumentation zu finden.

    Mit "tkinter" wird soetwas zum Beispiel über die 'after'-Methode gemacht, Pseudo-Code:

    Grüße

    Dennis

    Edit: Bin gerade über einen Thread gestolpert, der dich interessieren könnte, siehe hier.

    🎧 With the music execution and the talk of revolution, it bleeds in me and it goes 🎧

    Einmal editiert, zuletzt von Dennis89 (27. Oktober 2021 um 09:10)

  • Danke für deine Antwort Dennis.

    Ich hab mir zuerst deinen Link angeschaut, damit war dann aber für mich nicht allzu viel anzufangen, weshalb ich ich QtReacter gegooglet hab und auch damit kam ich nicht sehr weit.

    Ich bin dann über die Google-Suche auf QTimer, QThread, QRunnable und solche Sachen gestoßen, aber so wie ich das verstanden hab, sind auch die nicht für meinen Zweck geeignet: QTimer mach nur ne aktuell alle x Sekunden - kann ich nicht brauchen, und QThread und QRunnable können zwar andere Threads ausführen, aber immer nur durch Anstoß über die GUI? Das geht zwar in die richtige Richtung, aber ich bräuchte sozusagen einen weiteren Thread, der von selbst permanent läuft und mit der GUI kommuniziert.

    Hat da noch jemand einen Tipp für mich? Oder hab ich da nur was falsch verstanden?

    Liebe Grüße

    Fipsi

  • Okay, ersten Tests zufolge hab ich es jetzt wohl doch mit QThread hinbekommen:

    Zumindest ändert sich im 2 Sekunden Takt der Button schon mal.

    Jetzt mal schauen, ob ich das mit den Sockets und so auch hinbekomm. Ich halt auch auf dem laufenden.

    Vielen Dank für die Anstöße und

    liebe Grüße

    Fipsi

  • Ja, die QTcpSocket hab ich auch schon gesehen bei meiner Recherche gestern, die Doku dazu ist aber etwas... dürftig (oder ich bin nur mal wieder zu blöd, das richtige zu finden/den richtigen Link noch zu klicken).

    Deswegen schau ich jetzt mal erst mal mit meinen "normalen" Sockets und wenn ich das nicht hinbekomm, durchforsch ich noch mal weiter das Internet nach den QTcpSockets. Aber vielen Dank für den Hinweis.

    Liebe Grüße

    FIpsi

  • Fipsi: Dein Thread-Programm ist kaputt. Man darf aus einem anderen Thread als dem in dem die GUI-Hauptschleife läuft, nichts an der GUI verändern. Das muss man über Signale und „queued connections“ machen.

    Die `give_window()`-Methode ist, sagen wir mal komisch. Warum hast Du das nicht gleich bei der `__init__()` mitgegeben? Ansonsten würde das per Konvention (nicht nur in Python) `set_window()` heissen. Und so triviale Setter (und auch Getter) schreibt man in Python nicht — da würde man einfach das Attribut zuweisen.

    Attribute sollten zudem auch alle nach ablauf der `__init__()` existieren und nicht nachträglich durch Methoden eingeführt werden.

    Und vor allem sollte man das dann auch *verwenden* und nicht auf ein globales `window` zugreifen. Ein Grund warum auf Modulebene Variablen nichts zu suchen haben — man macht leicht diesen Fehler und bemerkt ihn nicht. Wenn es das `window` auf Modulebene nicht gäbe, würde die `run()`-Methode auf die Nase fallen.

    Was dann etwas verwirrend ist, ist das bei `give_window()` gar nicht das `MainWindow`-Objekt übergeben wird, sondern das `ui`-Attribut. Der Code in `run` hat da also ein `ui` zu viel weil `self.window` dort das Objekt ist, was beim globalen `window` am Attribut `ui` hängt.

    Der Code müsste also so aussehen:

    Python
                    if self.window.button_retry_connection.isEnabled():
                        self.window.button_retry_connection.setEnabled(False)
                    else:
                        self.window.button_retry_connection.setEnabled(True)

    Das ``if``/``else`` ist ein bisschen umständlich wenn man einfach immer nur den entgegengesetzten Wert von `isEnabled()` setzen will. Das geht mit ``not``:

    Python
                    self.window.button_retry_connection.setEnabled(
                        not self.window.button_retry_connection.isEnabled()
                    )

    Es ist auch verwirrend, dass es das `ui`-Attribut überhaupt gibt, denn man kann die ganze Attribute und Methoden jetzt über `window` *und* über `window.ui` erreichen. Wonach entscheidest Du denn welches Du verwendest? Und warum zwei Wege um genau das gleiche erreichen zu können?

    Für dieses konkrete Beispiel würde man einen `QTimer` verwenden statt in einer „busy waiting“-Schleife CPU-Zeit zu verbrennen.

    `time.time()` ist für so etwas nicht wirklich geeignet. Dafür würde man `time.monotonic()` (oder `time.perf_counter()`) verwenden. Bei denen ist garantiert, dass die sich nur ”vorwärts” in der Zeit bewegen. Inklusive auch nicht mal eine Sekunde stehen bleiben können (Schaltsekunden).

    Der Code sieht aus als wenn da aus einer *.ui-Datei eine Python-Datei generiert wurde — das macht man nicht wirklich. Man kann einfach die *.ui-Dateien zur Laufzeit laden, ohne den zusätzlichen Schritt da Quelltext draus generieren zu müssen.

    “Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”

  • __blackjack__ dass ich über window.*.setEndabled(True) geh sind doch die Signale? Oder wie meinst du das?

    Ja, die give_window hab ich kurz nach dem Posten hier wieder raus geschmissen. Hatte das mit der __init__ erst nicht hinbekommen und hab danach gespannt, dass ich's der Klasse und nicht der Methode übergeben hatte.

    Das ui hab ich noch mit übergeben, weil ich mir dachte, so ein wenig Schreibarbeit sparen zu können, aber wenn ich das in der Methode weggelassen hab, hat Python den Button nicht gefunden (versteh ich jetzt noch nicht, wieso).

    Das mit dem ui hatte ich so aus dem Tutorial übernommen. Um auch den letzten Punkt gleich aufzugreifen: Ich erzeuge das GUI mit Qt Creater und lass dann die *.ui in *.py übersetzen. Und die *.py selbst ruft ich dann mit meinem Programm auf - Tutorial.

    Ja, für dieses Beispiel wäre wahrscheinlich tatsächlich QTimer besser gewesen. Das war nur das einzige, was mir auf die schnelle eingefallen ist, womit ich testen kann, ob der Thread tatsächlich läuft. Später hab ich aber Tasks, die Zeitunabhängig laufen, da kann ich QTimer dann nicht mehr gebrauchen.

    Ich werd mir die zwei time-Methoden mal genauer anschauen, vielen Dank.

    Danke mal wieder für deine ausführliche Antwort, ich hoffe, meine Gedankengänge sind verständlich.

    Liebe Grüße

    Fipsi

  • Fipsi Ich verstehe Deine erste Frage nicht, beziehungsweise worauf sich die bezieht?

    Und es gibt hier neben dem `window` noch `window.ui` und an anderer Stelle dann `ui` was aber dort `window` genannt wird. Eben das ist ja das verwirrende, weshalb ich auch Deinen 3. Absatz nicht verstehe was Du da meinst.

    Die Zeilen 17 und 18 sind so nicht sinnvoll:

    Code
            self.ui = Ui_Widget()
            self.ui.setupUi(self)        

    Danach ist `self` beziehungsweise wie auch immer Du das Objekt ausserhalb nennst, und `self.ui` mehr oder weniger austauschbar, also zwei Wege um an die selben Attribute heran zu kommen. Das macht wenig Sinn weil man entweder einen Weg nicht verwendet, oder man verwendet beide und das ist verwirrend, weil nicht klar ist warum mal der eine und mal der andere verwendet wird. Und wenn man zwei Wege sieht, denkt man ja auch erst einmal das wären auch unterschiedliche Objekte auf die da zugegriffen wird.

    Entweder man erbt von `QMainWindow` *und* der generierten UI-Klasse, schreibt nur `self.setupUi()`, und lässt das `ui`-Attribut weg. Oder man erbt von gar nichts, und erstellt nur das `ui`-Attribut. Letzteres würde ich vorziehen. Und dann auch keinen Quelltext generieren, sondern zur Laufzeit gleich die `*.ui`-Datei laden. Spart Arbeit weil ein unnötiger Zwischenschritt weg fällt. Man kommt nicht in Versuchung an generiertem Quelltext Änderungen vorzunehmen. Und wenn man das ganze unter Versionskontrolle stellt, braucht man auch nicht aufpassen, dass diese Dateien dort drin landen.

    “Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”

  • Die erste Frage bezieht sich darauf, dass du meintest, man greift mit Signalen/Slots auf die GUI zu. Das mach ich doch? Oder reden wir aneinander vorbei?

    Generell, um alle folgenden Absätze auch gleich aufzugreifen, muss ich sagen, dass ich das so von einem Tutorial übernommen hab. Die Umwandlung von *.ui zu *.py und wie dann die GUI geladen wird, hab ich so 1:1 aus dem Tutorial, von mir selbst stammt die Klasse Thread (aber auch ziemlich aus einem Tutorial übernommen) und dann weiter unten das ab "server_adress".

    Ich musste aber leider schon feststellen, dass das trotzdem nicht so ganz funktioniert, wie ich es mir vorgestellt hab.

    Wenn ich das Programm starte, läuft der QThread los und de-/aktiviert den Button im 2 Sek-Takt. Sobald ich dann aber einmal den Button gedrückt hab, "schläft" der Thread ein und macht nichts mehr, der sollte aber weiter laufen. :conf:

    Ja, ich versuch grad ehrlich gesagt aus der Verschaltelungsaffäre raus zu kommen und erst mal zu schauen, ob Python überhaupt das kann, was ich von ihm will :-/

    Liebe Grüße

    Fipsi

  • Fipsi Nein, Du greifst nicht über Signale die mit Slots verbunden sind vom Thread auf die GUI zu. Das ist ein einfacher Methodenaufruf von einem anderen Thread aus als dem in dem die GUI-Hauptschleife läuft. Üblich und empfohlen und auch in der Qt-Dokumentation gezeigt, ist es ein QThread-Objekt zu erstellen (statt davon abzuleiten), und ein eigenes `Worker`-Objekt von `QObjekt` abzuleiten, und das per `QObject.moveToThread()` in diesen Thread zu verschieben und zwischen den beiden Threads dann mit Signalen und Slots zu kommunizieren. Slots sind in Python quasi automatisch da, im Gegensatz zu C++, weil man einfach jedes aufrufbare Objekt dafür verwenden kann. Signale muss man erstellen mit PyQt5.QtCore.pyqtSignal().

    Dein Thread wird sehr wahrscheinlich einfach darüber stolpern, dass man das so eben nicht machen darf. Das nur der Thread nix mehr macht ist da ja noch nett — das könnte auch das gesamte Programm einfach hart abstürzen lassen.

    “Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”

  • Hallo zusammen,

    also ich hab jetzt noch mal etwas tiefer das Thema Qt mit QThread angeschaut und dabei folgendes geschrieben:

    Dabei hab ich jetzt noch Probleme, die ich nicht über die Tutorials verstanden hab:

    - Wie kann ich jetzt Funktionen der BackgroundJobs über Buttons im Window triggern?

    - Wie kann ich über Ereignisse im BackgroundJobs anzeigen im Window ändern?

    Ich hab da bei Tutorials immer was mit .connect() gefunden, aber ich versteh nicht ganz, wie:

    - Über das .emit kann ich von BackgroundJobs Variablen im Window bearbeiten, oder?

    - Wie kann ich über z. B. einen Button im Window die Funktion dosomething() im BackgroundJobs anstoßen?

    - Wie kann ich aus BackgroundJobs die Funktion connectBox() im Window anstoßen? (am besten noch mit der Möglichkeit Variablen übergeben zu können)

    Kann mir da mal bitte einer helfen, vom Schlauch aufzustehen?

    Vielen Dank schon mal,

    schönes Wochenende und

    liebe Grüße

    Fipsi

    Einmal editiert, zuletzt von Fipsi (26. November 2021 um 16:55)

  • Guten Morgen,

    ich hab jetzt noch ein wenig weiter experimentiert und bin dabei auf folgenden Stand gekommen:

    Dabei hab ich jetzt beim Thread (class BackgroundJobs) folgendes Problem:

    - wenn ich in der Methode run die while True-Schleife hab, reagiert der Thread nicht mehr auf das ansprechen der Methoden startCounter und stopCounter.

    - wenn ich die while True-Schleife in der Methode run entferne, dann reagiert der Thread zwar auf das ansprechen der anderen Methoden, aber die run läuft dann nicht mehr.

    Wie krieg ich das hin, dass die run-Methode der backgroundJobs dauerhaft läuft und ich gleichzeitig noch andere Methoden im Thread ansprechen kann?

    Vielen Dank und

    liebe Grüße

    Fipsi

    Ein Edit, damit ich hier nicht so viel rum Spam:

    Nachdem ich noch mal einiges gesucht, noch mehr gelesen und noch viel mehr experimentiert hab, bin ich auf folgendes gekommen:

    Erklärung dazu:
    Wenn man will, dass ein Thread dauerhaft im Hintergrund läuft, also nicht nur auf ein Event reagiert und dann wieder beendet wird, darf man nicht QObject und moveToThread verwenden, sondern muss QThread verwenden. In der QThread-Klasse muss man dann die run-Methode überschreiben und eh vola.

    Zu den Signals und Slots:

    Die Signale darf man nicht in der init-Methode definieren, sondern direkt in der Klasse, dann funktioniert auch das emit. Außerdem darf das Signal nicht die zu übermittelnde Variable sein, sondern ist eine weitere Variable.

    Die Slot-Methoden müssen mit @pyqtSlot deklariert werden.

    Jetzt kann ich richtig an die Arbeit gehen, ich denke, jetzt hab ich alles raus gefunden, was ich brauch :bravo2:

    Danke für die Denkanstöße und

    liebe Grüße

    Fipsi

    2 Mal editiert, zuletzt von Fipsi (28. November 2021 um 09:52)

Jetzt mitmachen!

Du hast noch kein Benutzerkonto auf unserer Seite? Registriere dich kostenlos und nimm an unserer Community teil!