PyQt5 - Timer in zweitem Fenster öffnen+starten

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Bodo96
User
Beiträge: 6
Registriert: Montag 17. Juli 2023, 21:50

Grüße :)

Ich bin Neuling auf dem Gebiet PyQt. Mein Plan war eine Oberfläche zu erstellen, bei der ich per externen Button (angesteuert via GPIO) ein zweites Fenster öffne und gleichzeitig einen Timer starte, der angezeigt werden soll. Nunja, die Reaktion des Buttons wird eingelesen und es öffnet sich auch ein zweites Fenster. Leider wird das Label aber nicht aktualisiert und bleibt beim "ersten" Funktionsdurchlauf. In der Konsole sehe ich den Timer auch laufen...
Leider finde ich keine Lösung, bzw. scheine ich das "Threading" nicht richtig umgesetzt zu haben?

Vielen Dank für eure Hilfe!

Code: Alles auswählen

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import *
from gpiozero import Button
import sys
import time
import threading
from threading import Timer, Thread


class MainWindow (QMainWindow):
    def __init__(self):
        super(MainWindow,self).__init__()
        self.initUI()
        
        ###Hier startet die Spielwiese der GUI
        
    def initUI(self):
        self.setGeometry (200,200,300,300)
        self.setWindowTitle ("Test Button Fotobox")
        
        self.label = QtWidgets.QLabel(self)
        self.label.setText('First Label!')
        self.label.move(50,50)

        self.b1 = QtWidgets.QPushButton(self)
        self.b1.setText("Button1")
        self.b1.move(100,100)
        
        self.button = Button(24)
        self.button.when_pressed = self.button_clicked

    def button_clicked (self):
        print('Button wurde gedrückt!')
        self.window2 = QtWidgets.QDialog()
        self.dialog = dialog_1()
        self.dialog.setupUI(self.window2)
        self.window2.show()
    
class dialog_1():
    
    def setupUI (self, SecondWindow):
        SecondWindow.setObjectName("Second_Window")
        SecondWindow.resize(700, 700)
        self.label = QLabel("3456", SecondWindow)
        self.label.move(350,350)
        
        self.i=0
        
        def run(self):
            threading.Thread(target=timer, args= (self,)).start()
            threading.Thread(target=update, args= (self,)).start()
                    
        def timer(self):
            for i in range(10,0,-1):
                print (i)
                time.sleep(1)
                update(self)
    
        def update(self):
            self.label.setText(str(self.i))
            
        run(self)
  
def window():
    app=QApplication(sys.argv)
    win=MainWindow()
    win.show()
    sys.exit(app.exec())

window()
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Bodo96: Sternchen-Importe sind Böse™. Das macht Programme unnötig unübersichtlicher und fehleranfälliger und es besteht die Gefahr von Namenskollisionen. Und obwohl *alles* aus dem Modul importiert wird, werden manche Sachen dann doch über das Modul angesprochen. Warum so inkonsistent?

`Thread`, `Timer`, `QtGui`, `QtCore`, `QTimer` werden importiert, aber nirgends verwendet.

`window` wäre ein passender Name für ein Fenster, aber nicht für eine Funktion. Funktionen und Methoden werden üblicherweise nach Tätigkeiten benannt, damit der Leser weiss was die tun und um sie von eher passiven Werten unterscheiden zu können. Wobei die Hauptfunktion traditionell `main()` heisst.

Die Argumente bei `super()` sind überflüssig. Ebenso die `initUI()`. Es macht keinen Sinn eine so gut wie leere `__init__()` zu haben die eine andere Methode aufruft in der dann die eigentliche Initialisierung steht.

Bitte mal gleich so Sachen wie `setGeometry()`, `move()`, und `resize()` zum festlegen von absoluten Grössen und Positionen in Qt vergessen und sich mit Layouts vertraut machen.

Namen sollten keine kryptischen Abkürzungen enthalten oder gar nur daraus bestehen. Der Name soll dem Leser vermitteln was der Wert dahinter im Programm bedeutet, nicht zum rätseln zwingen.

Man nummeriert keine Namen. Dann will man sich entweder bessere Namen überlegen, oder gar keine Einzelnamen/-werte verwenden, sondern eine Datenstruktur. Oft eine Liste.

Ausserhalb der `__init__()` führt man keine neuen Attribute ein. Die `__init__()` sollte das Objekt komplett initialisieren, so dass man es benutzen kann. Man muss auch nicht alles an das Objekt binden.

`dialog_1` ist falsch geschrieben für eine Klasse, die 1 macht keinen Sinn, und das ganze ist auch gar keine Klasse, sondern eine sehr umständlich geschriebene Funktion. `SecondWindow` ist als Argumentname falsch geschrieben. Und an der Stelle sollte auch niemanden interessieren das wievielte Fenster das ist.

`self.i` ist was anderes als nur `i`. `self.i` wird nirgends verändert nachdem es auf 0 gesetzt wurde.

In den lokalen Funktionen immer noch mal `self` zu übergeben macht keinen Sinn, denn `self` ist ja bereits Teil des Closures. Allerdings würde man auch keine Funktionen lokal in einer Funktion oder Methode auf diese Art definieren. Das macht bei Closures Sinn, die dann die umgebene Funktion tatsächlich als Rückgabewert verlassen, aber hier…

`run()` ist auch in jedem Fall unsinnig weil man die beiden Zeilen auch einfach an der Aufrufstelle schreiben könnte.

`update()` als Thread zu starten macht keinen Sinn. Das wird ja schon regelmässig von dem anderen Thread aufgerufen.

Dialoge zeigt man nicht einfach nur an, die führt man aus. Sonst hätte da auch einfach ein `QWidget` gereicht. So wie das auch anstelle des `QMainWindow` ausreicht wenn man überhaupt nichts verwendet was ein `QMainWindow` bietet.

Zwischenstand, der allerdings ernsthaft kaputt ist:

Code: Alles auswählen

#!/usr/bin/env python3
import sys
import time
from threading import Thread

from gpiozero import Button, Device
from gpiozero.pins.mock import MockFactory
from PyQt5.QtWidgets import (
    QApplication,
    QDialog,
    QLabel,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

Device.pin_factory = MockFactory()


def timer(label):
    for i in range(10, 0, -1):
        print(i)
        label.setText(str(i))
        time.sleep(1)


class MainWindow(QWidget):
    def __init__(self):
        super().__init__(windowTitle="Test Button Fotobox")

        layout = QVBoxLayout()
        layout.addWidget(QLabel(self, text="First Label!"))
        button = QPushButton("Button", self, clicked=self.button_clicked)
        layout.addWidget(button)
        self.setLayout(layout)

        physical_button = Button(24)
        physical_button.when_pressed = self.button_clicked

    def button_clicked(self):
        print("Button wurde gedrückt!")
        dialog = QDialog(self)
        layout = QVBoxLayout()
        label = QLabel(dialog)
        layout.addWidget(label)
        dialog.setLayout(layout)

        Thread(target=timer, args=(label,), daemon=True).start()

        dialog.exec()


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
GUIs sind in der Regel nicht threadsicher. Das gilt auch für Qt. Auch falls das hier jetzt scheinbar funktionieren mag, es ist undefiniertes Verhalten, weil man Qt-Objekte nicht einfach so von anderen Threads aus verwenden darf.

Das fängt im Grunde schon bei dem `when_pressed()` an, weil das auch schon ein zusätzlicher Thread ist, von dem ein Rückruf passiert, der in die laufende GUI eingreift. Hier geht es ans eingemachte, weil man etwas machen muss, was eher selten vorkommt: ein eigenes Event mit `postEvent()` von dem anderen Thread aus aufrufen, dass dann beispielsweise auf Qt-Seite ein Signal abgibt, auf das dann reagiert werden kann.

Und den Countdown würde man einfach über einen `QTimer` lösen.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import sys

from gpiozero import Button, Device
from gpiozero.pins.mock import MockFactory
from PyQt5.QtCore import QEvent, QTimer
from PyQt5.QtWidgets import (
    QApplication,
    QDialog,
    QLabel,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

Device.pin_factory = MockFactory()


class Countdown:
    def __init__(self, parent, label):
        self.label = label
        self.number = 10
        self.timer = QTimer(parent)
        self.timer.timeout.connect(self.on_tick)
        self.update_ui()

    def start(self):
        self.timer.start(1000)

    def update_ui(self):
        self.label.setText(str(self.number))

    def on_tick(self):
        if self.number <= 0:
            self.timer.stop()
        else:
            self.number -= 1
            self.update_ui()


class MainWindow(QWidget):
    def __init__(self):
        super().__init__(windowTitle="Test Button Fotobox")

        layout = QVBoxLayout()
        layout.addWidget(QLabel(self, text="First Label!"))
        button = QPushButton("Button", self, clicked=self.button_clicked)
        layout.addWidget(button)
        self.setLayout(layout)

        physical_button = Button(24)
        physical_button.when_pressed = (
            lambda: QApplication.instance().postEvent(
                self, QEvent(QEvent.User)
            )
        )

    def button_clicked(self):
        print("Button wurde gedrückt!")
        dialog = QDialog(self)
        layout = QVBoxLayout()
        label = QLabel(dialog)
        layout.addWidget(label)
        dialog.setLayout(layout)

        Countdown(dialog, label).start()

        dialog.exec()

    def event(self, event):
        if event.type() == QEvent.User:
            self.button_clicked()
            return True

        return super().event(event)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Bodo96
User
Beiträge: 6
Registriert: Montag 17. Juli 2023, 21:50

@__blackjack__:
Vielen Dank für deine Rückmeldung! Damit werde ich weiterarbeiten und danke für die Hinweise, diese werde ich gern umsetzen!
LG
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Gut das ich da „ungetestet“ dran geschrieben habe. Das `Countdown`-Objekt muss man am Leben erhalten. Und ich habe dann auch gleich noch einen Button eingebaut um den Mock-Pin auszulösen, damit man das auch testen kann:

Code: Alles auswählen

#!/usr/bin/env python3
import sys

from gpiozero import Button, Device
from gpiozero.pins.mock import MockFactory
from PyQt5.QtCore import QEvent, QTimer
from PyQt5.QtWidgets import (
    QApplication,
    QDialog,
    QLabel,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

Device.pin_factory = MockFactory()


class Countdown:
    def __init__(self, parent, label):
        self.label = label
        self.number = 10
        self.timer = QTimer(parent)
        self.timer.timeout.connect(self.on_tick)
        self.update_ui()

    def start(self):
        self.timer.start(1000)

    def update_ui(self):
        self.label.setText(str(self.number))

    def on_tick(self):
        if self.number <= 0:
            self.timer.stop()
        else:
            self.number -= 1
            self.update_ui()


class MainWindow(QWidget):
    def __init__(self):
        super().__init__(windowTitle="Test Button Fotobox")

        physical_button = Button(24)
        physical_button.when_pressed = (
            lambda: QApplication.instance().postEvent(
                self, QEvent(QEvent.User)
            )
        )

        layout = QVBoxLayout()
        layout.addWidget(QLabel(self, text="First Label!"))
        layout.addWidget(QPushButton("Button", clicked=self.button_clicked))
        layout.addWidget(
            QPushButton(
                "Trigger Mock Pin",
                pressed=lambda: physical_button.pin.drive_high(),
                released=lambda: physical_button.pin.drive_low(),
            )
        )
        self.setLayout(layout)

    def button_clicked(self):
        dialog = QDialog(self)
        layout = QVBoxLayout()
        label = QLabel(dialog)
        layout.addWidget(label)
        dialog.setLayout(layout)

        countdown = Countdown(dialog, label)
        countdown.start()
        dialog.exec()

    def event(self, event):
        if event.type() == QEvent.User:
            self.button_clicked()
            return True

        return super().event(event)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Bodo96
User
Beiträge: 6
Registriert: Montag 17. Juli 2023, 21:50

Hallo nochmal __blackjack__,

Ich habe wiedermal ein wenig gerumgespielt und festgestellt,
dass ich um keinen Preis diesen externen Button mit der GUI nicht verbinden kann.
Auf jedenfall funktioniert dein "Testbutton" einwandfrei. Auch habe ich das Prinzip dahinter verstanden, indem du die Input als "high" Level simulierst und dadurch die Funktion event triggerst.
Sobald ich aber den realen Button betätige passiert nichts. Deshalb habe ich logischerweise alles überprüft, lasse ich den Button in einem extra Script laufen funktioniert es problemlos. Selbst eine Oszimessung zeigt zumindest elektrische Funktionalität...
Kann es es sein, dass die "Button"-Funktion in dem Init der Klasse ein Problem hat? (Ich gebe zu mir ist allerdings auch noch nicht die QApplication.instance() bewusst...

Falls du nochmal supporten kannst wäre super.
Ansonsten schonmal einen guten Start in die Woche!

LG Bodo96
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Bodo96: Du benutzt das aber jetzt nicht 1:1 mit dem Mock-Pin‽ Das musste ich ja machen weil ich hier am PC gar keine GPIOs habe.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Bodo96
User
Beiträge: 6
Registriert: Montag 17. Juli 2023, 21:50

Moinsen,

Nein, ich hatte es nur nochmal mit dem virtuellen Button probiert, da der reale wie erwähnt nicht funktioniert.
Kann es sein, dass die gpiozero Button Funktion ein Problem hat bzw. Nicht beachtet/aktualisiert wird?
Habe den ganzen Spaß mal mit der RPI.GPIO Ungebung probiert. Auch hier das gleiche Verhalten, beim Einlesen der Zustände in einem extra Skript gibt es keine Probleme.
Lasse ich mir den Zustand vom Pin jedoch in der class Main ausgeben, bleibt dieser permanent auf "high".

Grüße
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

__blackjack__ hat geschrieben: Sonntag 23. Juli 2023, 22:15 Das musste ich ja machen weil ich hier am PC gar keine GPIOs habe.
Hattest du nicht neulich einen Grund gesucht, um einen RPi4 zu kaufen?
Taadaaa 😅


SCNR! :D
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Der Haken daran ist, dass ich ja noch ältere Raspis in der Schublade habe, die ich dafür auch verwenden könnte. 😉
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Bodo96
User
Beiträge: 6
Registriert: Montag 17. Juli 2023, 21:50

Hat jemand vielleicht schonmal die Möglichkeit gehabt, die ganze Sache mit GPIOs zu testen?
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

was funktioniert denn bei dir mit welchem Code nicht?


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Bodo96
User
Beiträge: 6
Registriert: Montag 17. Juli 2023, 21:50

Hallo,

Nunja der Status vom realen GPIO wird anscheinend nicht beachtet, der Mock Pin von __blackjack__ jedoch funktioniert. Komisch, da dieser auch nur den drive auf "high" setzt...
Außerhalb der Klasse funktioniert der Button auch...

Vielen Dank und Gruß!
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nicht umschreiben, sondern den konkreten Code zeigen.
Antworten