Qt5 WebEngineView URL über Digital IO ändern

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
technikasg
User
Beiträge: 4
Registriert: Sonntag 30. Juli 2023, 16:43

Hallo liebe Gemeinde,

ich möchte ein Infoboard erstellen, bei welchem man über extern verbaute Hardwaretaster, welche über ein FT232H USB-GPIO Modul angebunden werden, die Anzeige ändern kann.
Dazu habe ich eine Funktion erstellt, die über den QTimer zyklisch aufgerufen wird und den Status der Taster abfragt und infolgedessen die URL anpasst.

Leider habe ich aktuell das Problem, dass beim ersten Aufruf der Funktion zum Auswerten der Taster die GUI unvermittelt geschlossen wird.
In der Konsolenausgabe erhalte ich jeweils nur die Meldung === RESTART: Shell ===. Weitere Fehlermeldungen oder ähnliches habe ich nicht.
Meine bisherigen Debugging-Versuche zeigen mir, dass bereits die erste if-Anweisung schon nicht mehr durchlaufen wird.
Ich bin momentan ratlos, wo mein Fehler liegt und würde mich über einen hilfreichen Tipp als Lösungsansatz sehr freuen.

Vielen Dank für Eure Hilfe!

Code: Alles auswählen

import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QLabel, QLayout, QHBoxLayout, QVBoxLayout, QGraphicsView
from PyQt5 import uic
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from time import strftime
import threading
import time
import board
import digitalio

class UI(QMainWindow):
    def __init__(self):
        super(UI, self).__init__()

        # Tastereingänge
        key_1 = digitalio.DigitalInOut(board.C0)
        key_1.direction = digitalio.Direction.INPUT
        key_2 = digitalio.DigitalInOut(board.C1)
        key_2.direction = digitalio.Direction.INPUT
        key_3 = digitalio.DigitalInOut(board.C2)
        key_3.direction = digitalio.Direction.INPUT

        self.currDP = 1
        self.setWindowTitle('Infoboard')
        self.setWindowFlag(Qt.FramelessWindowHint)
        self.setGeometry(-2560,0,2560,1300)

        # Definition der Widgets für die Zeilenbereiche
        self.main_widget = QWidget(self)
        self.line1_widget = QWidget(self)
        self.line2_widget = QWidget(self)
        self.line3_widget = QWidget(self)
        self.line4_widget = QWidget(self)

        # Einrichtung der Widget-Layouts
        self.main_layout = QVBoxLayout(self.main_widget)
        self.line1_layout = QHBoxLayout(self.line1_widget)
        self.line2_layout = QHBoxLayout(self.line2_widget)
        self.line3_layout = QHBoxLayout(self.line3_widget)
        self.line4_layout = QHBoxLayout(self.line4_widget)

        # Definition der Zeilenhöhen
        self.line1_widget.setFixedHeight(100)

        # Size Constraints ??? TBD
        #self.main_layout.sizeConstraint = QLayout.SetFixedSize
        

        # Kopfzeile
        self.line1_left = QLabel(self)
        self.line1_left.setPixmap(QPixmap('image_head.png').scaled(250,50,aspectRatioMode = 2))

        self.line1_center = QLabel(self)
        self.line1_center.setText('Informationsboard')
        self.line1_center.setAlignment(Qt.AlignCenter)
        self.line1_center.setFont(QFont('Arial', 35))

        self.line1_right = QLabel(self)
        self.line1_right.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.line1_right.setFont(QFont('Arial', 20))

        # Kopfzeile zusammenstellen
        self.line1_layout.addWidget(self.line1_left)
        self.line1_layout.addWidget(self.line1_center)
        self.line1_layout.addWidget(self.line1_right)
        self.line1_widget.setLayout(self.line1_layout)

        # Timer für Ausgabe der aktuellen Uhrzeit
        self.timer = QTimer()
        self.timer.timeout.connect(self.akt_uhrzeit)
        self.timer.start(1000)

        # Browserfenster
        self.line2_left = QWebEngineView()
        self.line2_right = QWebEngineView()

        self.line2_left.setUrl(QUrl("https://www.google.de/"))
        self.line2_right.setUrl(QUrl("https://www.ccc.de/"))

        # Browserfenster zusammenstellen
        self.line2_layout.addWidget(self.line2_left)
        self.line2_layout.addWidget(self.line2_right)
        self.line2_widget.setLayout(self.line2_layout)

        # Masterlayout zusammenstellen        
        self.main_layout.addWidget(self.line1_widget)
        self.main_layout.addWidget(self.line2_widget)
        self.main_widget.setLayout(self.main_layout)

        # Masterlayout ausgeben
        self.setCentralWidget(self.main_widget)

        # Timer für Auslesen der Taster
        self.taster_timer = QTimer()
        self.taster_timer.timeout.connect(self.change_dienstplan)
        self.taster_timer.start(2500)

    def akt_uhrzeit(self):
        self.line1_right.setText(strftime('%d.%m.%y - %H:%M:%S'))

    def change_dienstplan(self):
        print("Start")
        check = self.key_1.value
        print(check)
        if check == False:
            print("Inside")
            if self.currDP != 1:
                self.currDP = 1
                self.line2_left.setUrl(QUrl("https://www.bing.de/"))
        elif self.key_2.value == False:
            if self.currDP != 2:
                self.currDP = 2
                self.line2_left.setUrl(QUrl("https://www.youtube.com/"))
        elif self.key_3.value == False:
            if self.currDP != 3:
                self.currDP = 3
                self.line2_left.setUrl(QUrl("https://www.microsoft.com/"))
        else:
            print("None")
            pass
                
# Initialize the App
class GUI_Action():
    def start_gui():
        app = QApplication(sys.argv)
        app.setApplicationName('Infoboard')
        UIWindow = UI()
        UIWindow.show()
        app.exec_()

# Setting Threads
gui_thread = threading.Thread(target=GUI_Action.start_gui, args=())

gui_thread.start()
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

GUIs darf man nur im Hauptthread laufen lassen, Dein zusätzlicher Thread macht auch überhaupt keinen Sinn, weil ja sonst nichts gemacht wird.
Die Klasse GUI_Action kann genauso weg; Du erzeugst nicht einmal ein Exemplar der Klasse.
Statt dessen fehlt eine `main`-Funktion

Code: Alles auswählen

def main():
    # Initialize the App
    app = QApplication(sys.argv)
    app.setApplicationName('Infoboard')
    window = UI()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()
Was ist das für ein Moduel `digitalio`? Ich hab nur ein Modul für CircuitPython gefunden.
technikasg
User
Beiträge: 4
Registriert: Sonntag 30. Juli 2023, 16:43

Danke für die Antwort. Den Hinweis habe ich gleich mit eingearbeitet.
Der Thread war nur als Vorbereitung für mögliche parallel laufende Erweiterungen gedacht, ist aber im Moment nicht erforderlich. Ich habe ihn daher entfernt.

Das Modul digitalio gehört zu CircuitPython und ist hier ein Bestandteil, der als Abghängigkeit vom BLINKA FT232H mit installiert wird. Das ganze sind von Adafruit bereitgestellte Bibliotheken.

Leider hat die Änderung aber das Problem noch nicht beseitigt. Die GUI bricht bei dem print(check) Aufruf ohne Fehlermeldung ab.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Funktioniert denn das Abfragen der Pins ohne QT fehlerfrei?
technikasg
User
Beiträge: 4
Registriert: Sonntag 30. Juli 2023, 16:43

Ja, das habe ich schon in verschiedenen Anwendungen im Einsatz, es ist hier für mich die erste Anwendung von Qt, da ich sonst immer eher hardwarenah ohne GUI unterwegs war bisher.
Hier ein kurzes Codebeispiel aus einer anderen funktionierenden Anwendung.

Code: Alles auswählen

import board
import digitalio
import time

key_1 = digitalio.DigitalInOut(board.C0)
key_1.direction = digitalio.Direction.INPUT

while(True):
    if key_1.value == False:
        print("Taste gedrückt")
    else:
        pass
    time.sleep(0.5)
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,
technikasg hat geschrieben: Sonntag 30. Juli 2023, 21:14 Die GUI bricht bei dem print(check) Aufruf ohne Fehlermeldung ab.
Ich hoffe ich hab das richtig gesehen, aber mal heruntergebrochen und ohne das "digitalio", hast du sowas gemacht:

Code: Alles auswählen

class Test:
    def __init__(self):
        key = 'Irgendwas_weil_kein_digitalio'

    def do_something(self):
        check = self.key
        print(check)


def main():
    test = Test()
    test.do_something()


if __name__ == '__main__':
    main()
Das gibt dann auch eine eindeutige Fehlermeldung aus.

Code: Alles auswählen

class Test:
    def __init__(self):
        self.key = 'Irgendwas_weil_kein_digitalio'

    def do_something(self):
        check = self.key
        print(check)


def main():
    test = Test()
    test.do_something()


if __name__ == '__main__':
    main()
Du musst das was du dir in der Klasse "merken" willst an 'self' binden.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Whitie
User
Beiträge: 216
Registriert: Sonntag 4. Juni 2006, 12:39
Wohnort: Schulzendorf

Ich würde aus deinem ersten Beitrag vermuten, dass du das Programm aus einer interaktiven Shell (Idle?) ausführst? Die benutzt selbst eine Hauptschleife, da kann schon was durcheinander geraten. Probier mal das Programm direkt aus der Bash (o. ä.) zu starten, also "python mein_programm.py". Das könnte schon helfen.

Viele Grüße
Whitie
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@technikasg: Neben den Anmerkungen von Sirius3 bezüglich Threads und GUI + `main()`-Funktion und den Fehler den Dennis89 mit den Attributen angesprochen hat, noch ein paar Sachen:

Sternchen-Importe sind Böse™. Das macht Programme unnötig unübersichtlicher und fehleranfälliger und es besteht die Gefahr von Namenskollisionen.

`time`, und `uic`, `QLayout`, und `QGraphicsView` von PyQt werden importiert, aber nirgends verwendet.

Die `exec_()`-Methode ist veraltet und wird in absehbarer Zeit verschwinden, die heisst jetzt `exec()` ohne den Unterstrich.

`super()` braucht keine Argumente.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Der Quelltext in der `__init__()` ist ungünstig sortiert und enthält nummerierte Namen und Namensteile wie `rechts` und `links`. Quelltext sollte so angeordnet sein, dass zusammengehörende Zeilen auch zusammenstehen, und nicht erst alle `QWidget` und dann alle Layouts für diese `QWidget` erzeugt und jeweils an nummerierte Namen gebunden werden. Das ist unübersichtlich weil man schwerer erkennt was denn nun in die jeweiligen Widgets thematisch gehört und man kann auch schwerer einzelne Widgets in eigene Klassen herausziehen, weil der Code dafür nicht beisammen steht, sondern man sich den über die `__init__()` verteilt erst zusammensuchen muss.

Bezüglich der Namen will der Leser doch gar nicht wissen in welcher Zeile und ob Links oder Rechts, sondern das `line1_right` die *Uhrzeit* anzeigt. Egal wo.

Die Geometrie des Fensters sollte man so nicht vorgeben. Die ergibt sich aus dem Inhalt des Fensters. Beziehungsweise wenn man Vollbild haben möchte, dann sorgt man dafür, dass das Fenster maximiert angezeigt wird. Dann muss man sich nicht um Details wie die konkrete Desktopgrösse/-auflösung kümmern.

Man nummeriert keine Namen. Entweder will man sich da bessere Namen überlegen, oder gar keine Einzelnamen/-werte sondern eine Datenstruktur. Oft eine Liste. So auch im Fall der Taster.

Was bei den Tastern fehlt ist der `deinit()`-Aufruf am Ende des Programms um hinter sich wieder sauber aufzuräumen. Dazu würde ich den Umstand nutzen das diese Objekte Kontextmanager sind, und die ausserhalb der `__init__()` im Hauptprogramm erzeugen und dort rein reichen.

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. `currDP`? Das sollte auch keine Zahl sein die von 1 bis 3 geht, sondern ein Index von 0 bis 2, weil die Taster ja in einer Liste stehen, die man dann in einer Schleife abarbeiten kann. Beziehungsweise ist die Frage ob man dieses Attribut überhaupt braucht, denn eigentlich ist das ja nur eine Indirektion ob eine bestimmte URL bereits geladen wurde, was man ja auch über die URL selbst prüfen könnte, statt da eine nichtssagende Zahl als Stellvertreter zu verwenden.

`akt_uhrzeit`? Ist das `aktuelle_uhrzeit` oder `aktualisiere_uhrzeit()`? Das sollte man nicht raten müssen.

Man muss auch nicht alles an das Objekt binden, sondern wirklich nur die Sachen, die man nachher auch tatsächlich irgendwo braucht. Ansonsten einfach lokale Namen (wieder)verwenden, oder wo möglich auch gar nicht erst an einen Namen binden.

Beim skalieren sollte man statt einer magischen 2 besser die entsprechende Konstante verwenden, damit der Leser weiss was da passiert.

`setFixedHeight()` ist keine gute Idee wenn da am Ende Widgets drin stecken deren Grösse durch Angaben in „point“ als Schriftgrösse beeinflusst sind. Denn wie viele Pixel ein „point“ letztlich ausmacht, ist nicht fest, sondern von der Plattform auf der das läuft abhängig.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import sys
from contextlib import ExitStack
from time import strftime

import board
import digitalio
from PyQt5.QtCore import Qt, QTimer, QUrl
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class Window(QMainWindow):
    def __init__(self, keys, urls):
        super().__init__(windowTitle="Infoboard")
        self.setWindowFlag(Qt.FramelessWindowHint)

        self.keys = keys
        self.urls = urls

        main_widget = QWidget(self)
        main_layout = QVBoxLayout()
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)
        #
        # Kopfzeile.
        #
        widget = QWidget(self)
        main_layout.addWidget(widget)
        layout = QHBoxLayout()
        widget.setLayout(layout)
        layout.addWidget(
            QLabel(
                self,
                pixmap=QPixmap("image_head.png").scaled(
                    250, 50, aspectRatioMode=Qt.KeepAspectRatioByExpanding
                ),
            )
        )
        layout.addWidget(
            QLabel(
                self,
                text="Informationsboard",
                alignment=Qt.AlignCenter,
                font=QFont("Arial", 35),
            )
        )
        self.clock_label = QLabel(
            self,
            alignment=Qt.AlignRight | Qt.AlignVCenter,
            font=QFont("Arial", 20),
        )
        layout.addWidget(self.clock_label)
        #
        # Browserfenster.
        #
        widget = QWidget(self)
        main_layout.addWidget(widget, 1)
        layout = QHBoxLayout()
        widget.setLayout(layout)

        self.dienstplan_webview = QWebEngineView(
            url=QUrl("https://www.google.de/")
        )
        layout.addWidget(self.dienstplan_webview)
        layout.addWidget(QWebEngineView(url=QUrl("https://www.ccc.de/")))

        self.clock_timer = QTimer(timeout=self.update_clock)
        self.clock_timer.start(1000)
        self.taster_timer = QTimer(timeout=self.change_dienstplan)
        self.taster_timer.start(2500)

    def update_clock(self):
        self.clock_label.setText(strftime("%d.%m.%y - %H:%M:%S"))

    def change_dienstplan(self):
        print("Start")
        for key, url in zip(self.keys, self.urls):
            if not key.value:
                if self.dienstplan_webview.url() != url:
                    self.dienstplan_webview.setUrl(url)
                break
        else:
            print("None")


def main():
    app = QApplication(sys.argv, applicationName="Infoboard")

    with ExitStack() as stack:
        keys = []
        for pin in [board.C0, board.C1, board.C2]:
            key = stack.enter_context(digitalio.DigitalInOut(pin))
            key.direction = digitalio.Direction.INPUT
            keys.append(key)

        urls = list(
            map(
                QUrl,
                [
                    "https://www.bing.de/",
                    "https://www.youtube.com/",
                    "https://www.microsoft.com/",
                ],
            )
        )
        assert len(keys) == len(urls)

        window = Window(keys, urls)
        window.showMaximized()

        sys.exit(app.exec())


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
technikasg
User
Beiträge: 4
Registriert: Sonntag 30. Juli 2023, 16:43

__blackjack__ hat geschrieben: Dienstag 1. August 2023, 12:59 @technikasg: Neben den Anmerkungen von Sirius3 bezüglich Threads und GUI + `main()`-Funktion und den Fehler den Dennis89 mit den Attributen angesprochen hat, noch ein paar Sachen:

[...]
Vielen Dank blackjack für die vielen Anmerkungen.
Diese helfen mir sehr mich weiter in das ganze Thema einzuarbeiten.

Dein Lösungsansatz funktioniert hervorragend und im wesentlichen habe ich auch schon verstanden was du geändert hast.
Das ist wirklich deutlich smarter als meine bisherigen Ideen. Vielen Dank dafür, dann kann ich jetzt an den weiteren Funktionen der GUI basteln.
Antworten