Blinken auf Canvas während das Hauptprogramm weiter läuft

Fragen zu Tkinter.
Antworten
Benutzeravatar
Fly
User
Beiträge: 2
Registriert: Dienstag 28. August 2018, 23:58

Hallo,
ich möchte ein Programm zur Gartenbewässerung schreiben, dass den Raspery Pi steuert. Dabei muss ich diverse Ventile mit den IO´s schalten und gleichzeitig den Zustand auf dem Touchscreen visualisieren.
Wenn also Ventil geschlossen, dann ist nur ein Punkt zu sehen. Wenn das Ventil offen ist soll der Punkt blinken. Dabei soll das Programm aber weiter laufen andere Ventile zu schalten, Sensoren aus zu lesen oder eingaben am Touch auf zu nehmen.

Das Blinken lasse ich also in einem eigenen Thread laufen, und bekomme natürlich prompt die Fehlermeldung RuntimeError: main thread is not in main loop

Wie komme ich aus der Nummer raus, bzw. wie kann man so etwas realisieren?
Hier mein Versuch (Sorry bin Laie, will aber mal groß werden :mrgreen: ):

Code: Alles auswählen

from tkinter import *
import _thread, time


#############################################################################
class Background(Canvas):
    def __init__(self, window):
        self.garten = PhotoImage(file="Gartenhintergrund.pgm")
        Canvas.__init__(self, master=window, width=800, height=480)
        #Entspricht RPI Touch mit 800x480 Pixeln
        self.create_image(0,0, anchor=NW, image=self.garten)
        self.pack()



#############################################################################
class Regner:
    def __init__(self, background, x, y):
        self.background = background
        self.x = x
        self.y = y
        self.aktiv = False
        self.farbeRuhend="#2007ad"
        self.farbeAktiv= "#299be6"
        self.r=20
        self.iD = self.background.create_oval(self.x-self.r/2,
                                                self.y-self.r/2,
                                                self.x+self.r/2,
                                                self.y+self.r/2,
                                                fill=self.farbeRuhend,
                                                outline=self.farbeRuhend)

    #......................................................................
    def an(self):
        self.aktiv = True
        _thread.start_new_thread(self.run, ())

    #......................................................................
    def run(self):
        self.aktiv = True
        while self.aktiv:
            time.sleep(1)
            self.background.itemconfigure(self.iD,
                                                  fill="white",
                                                  outline="white")

            time.sleep(1)
            self.background.itemconfigure(self.iD,
                                                  fill="red",
                                                  outline="red")

    #......................................................................
    def aus(self):
       self.aktiv = False
       self.background.itemconfigure(self.iD,
                                      fill=self.farbeRuhend,
                                      outline=self.farbeRuhend)

 

window = Tk()
background = Background(window)
rasen = Regner(background, 150, 100)
_thread.start_new_thread(rasen.run,())
time.sleep(3)
rasen.aus()
window.mainloop()
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

GUIs und Threads verträgt sich so ohne weiteres nicht. Wie du ja schon merkst. Periodische Ereignisse musst du über timer lösen. In tkinter legt man die mit after an. Schau mal hier im tkinter Forum, da gibt es viele Diskussionen und Beispiele.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Fly: Noch ein paar Anmerkungen zum Code:

Selbst wenn man Threads benutzt, dann *nicht* über das `_thread`-Modul. Das ist nicht für die Öffentlichkeit gedacht, wie der führende Unterstrich deutlich macht, sondern nur für den internen Gebrauch. Die öffentliche API ist das `threading`-Modul.

Sternchen-Importe sollte man nicht machen. Im Fall von Tkinter holt man sich ca. 190 Namen ins Modul von denen nur ein Bruchteil tatsächlich benötigt wird. Man verliert so schneller die Übersicht was wo definiert wurde, und es besteht die Gefahr von Namenskollisionen. Tkinter wird üblicherweise als `tk` importiert und dann über diesen Modulnamen auf den Inhalt des Moduls zugegriffen.

Auf Modulebene steht üblicherweise nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steckt in einer Funktion die traditionell `main()` heisst, und die nur ausgeführt wird wenn das Modul als Programm gestartet wurde, aber nicht wenn es als Modul importiert wird.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

Kommentare stehen über dem Code den man kommentiert.

Widgets layouten sich nicht selbst. Das `pack()` im `Background.__init__()` gehört da also nicht hin. Man nimmt sich damit die Freiheit ein Widget anderweitig zu verwenden.

Das das `Regner`-Exemplar `rasen` heisst ist komisch. Das ``rasen.aus()`` klingt dann so als wenn man Rasen ausschalten könnte. :-)

In `Regner` wird mehr an das Objekt gebunden als tatsächlich benötigt wird. `farbeAktiv` wird nicht verwendet‽

`Regner.r` ist als Name falsch. Erstmal sind Abkürzungen nicht so gut, weil der Leser dann raten muss was das bedeuten mag. Und hier würde ich von der Verwendung her raten das vielleicht „radius“ gemeint war, aber verwendet wird der Wert als Durchmesser.

Im Hauptprogramm steht Code um das Blinken zu starten der so noch mal in der `Regner.an()`-Methode steht. Warum wird die nicht auferufen statt Code zu kopieren?

Es wäre gut auch zu prüfen ob das Blinken schon aktiv ist, bevor man das noch mal anstösst.

Zwischenstand könnte dann so aussehen:

Code: Alles auswählen

from itertools import cycle
import tkinter as tk


class Background(tk.Canvas):
    
    def __init__(self, window):
        # 
        # Entspricht RPI Touch mit 800x480 Pixeln.
        #
        tk.Canvas.__init__(self, master=window, width=800, height=480)
        self.garten = tk.PhotoImage(file='Gartenhintergrund.pgm')
        self.create_image(0, 0, anchor=tk.NW, image=self.garten)


class Regner:
    
    def __init__(self, background, x, y):
        self.background = background
        self.blink_id = None
        self.farbe_ruhend = '#2007ad'
        radius = 10
        self.id = self.background.create_oval(
            x - radius, y - radius, x + radius, y + radius,
            fill=self.farbe_ruhend,
            outline=self.farbe_ruhend,
        )

    def do_blink(self, colors):
        color = next(colors)
        self.background.itemconfig(self.id, fill=color, outline=color)
        self.blink_id = self.background.after(1000, self.do_blink, colors)

    def an(self):
        if not self.blink_id:
            self.do_blink(cycle(['white', 'red']))
    
    def aus(self):
        self.background.after_cancel(self.blink_id)
        self.blink_id = None
        self.background.itemconfig(
            self.id, fill=self.farbe_ruhend, outline=self.farbe_ruhend
        )


def main():
    window = tk.Tk()
    
    background = Background(window)
    background.pack()
    
    regner = Regner(background, 150, 100)
    regner.an()
    regner.background.after(3000, regner.aus)
    
    window.mainloop()


if __name__ == '__main__':
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
Fly
User
Beiträge: 2
Registriert: Dienstag 28. August 2018, 23:58

Klasse, super geil, danke für die vielen Kommentare! Da geht's jetzt nach zwei Tagen rumgebastel endlich weiter.
Eine generelle Frage: Meine Aufgabe ist vom Prinzip eine Ablaufsteuerung, ähnlich einer SPS, mit zusätzlicher GUI. Macht es da Sinn überhaupt Tk für die GUI zu verwenden, wegen der Timing Schwächen? Oder gibt es dafür etwas viel passenderes oder ist das Timing generell bei GUI´s schwierig?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ein timer ist nicht schwieriger als ein Thread. Nur anders. Aber bei allen GUIs muss man sich damit auf die ein oder andere Art auseinandersetzen.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Fly: ”Timing” ist bei GUIs eigentlich generell so. Ich weiss nicht ob man das als schwierig bezeichnen kann/sollte — es ist halt anders wenn man etwas in die GUI-Hauptschleife integrieren will/muss.

Man kann auch mit Threads arbeiten, dann muss man allerdings beachten, dass die GUI im Hauptthread läuft und nur vom Hauptthread aus auf die GUI zugegriffen werden darf. Bei Tkinter kommuniziert man dann am besten über `Queue`\s die man regelmässig mit `after()` abfragt mit weiteren Threads.

Es macht in der Regel Sinn Programmlogik und GUI zu trennen, da ist dann die Frage wie weit man hier gehen möchte.

Ich würde aber bei so einer Gartensteuerung wahrscheinlich sowieso nicht selbst mit `sleep()` oder `after()` arbeiten wollen, sondern so etwas wie das APScheduler-Paket verwenden. Denn während das „mach das jetzt mal x Sekunden lang“ noch einfach ist, sind so Sachen wie „fang damit um x Uhr an“, schon kompliziert genug, um zu schauen was es da schon an fertigen Bibliotheken für gibt. Man muss das Rad ja nicht neu erfinden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten