mehrer Timer gleichzeitig

Fragen zu Tkinter.
Antworten
Moere
User
Beiträge: 10
Registriert: Sonntag 10. Januar 2021, 17:23

Hallo,

ich bin am Verzweifeln.
Ich möchte mehrer Timer gleichzeitig laufen lassen, aber irgenwie bin ich zu Doof dazu.
Es öffnen sich schon mal mehrer Fenster , was ja so gar nicht gewollt ist.
Vieleicht könnte sich einer meinen "grottenschlechten Quelltext" ansehen und sagen wo der Denkfehler ist.
P.S. bin blutiger Anfänger .


import time
import tkinter as tk
from tkinter import *
from tkinter import messagebox
from multiprocessing import Process

# creating Tk window
root = Tk()
root.geometry("1200x600")
root.title("Timerversuch")

# variablen
mhour=StringVar()
mminute=StringVar()
msecond=StringVar()
mthour=StringVar()
mtminute=StringVar()
mtsecond=StringVar()

# startwerte
mhour.set("00")
mminute.set("00")
msecond.set("05")
mthour.set("00")
mtminute.set("00")
mtsecond.set("10")

# Eingabeposition T1
mhourEntry= Entry(root, width=3, font=("Arial",18,""), textvariable=mhour)
mhourEntry.place(x=50,y=220)

mminuteEntry= Entry(root, width=3, font=("Arial",18,""), textvariable=mminute)
mminuteEntry.place(x=100,y=220)

msecondEntry= Entry(root, width=3, font=("Arial",18,""), textvariable=msecond)
msecondEntry.place(x=150,y=220)

# Eingabeposition T2
mthourEntry= Entry(root, width=3, font=("Arial",18,""), textvariable=mthour)
mthourEntry.place(x=50,y=280)

mtminuteEntry= Entry(root, width=3, font=("Arial",18,""), textvariable=mtminute)
mtminuteEntry.place(x=100,y=280)

mtsecondEntry= Entry(root, width=3, font=("Arial",18,""), textvariable=mtsecond)
mtsecondEntry.place(x=150,y=280)

#Berechnung 1
def func1():
try:
mtemp = int(mhour.get())*3600 + int(mminute.get())*60 + int(msecond.get())
except:
print("Please input the right value")
while mtemp >-1:
mmins,msecs = divmod(mtemp,60)
mhours=0
if mmins >60:
mhours, mmins = divmod(mmins, 60)
mhour.set("{0:2d}".format(mhours))
mminute.set("{0:2d}".format(mmins))
msecond.set("{0:2d}".format(msecs))
root.update()
time.sleep(1)
if (mtemp == 0):
messagebox.showinfo("Hinweis", "Zeit 1 abgelaufen ", )
mtemp -= 1

#Berechnung 2
def func2():
try:
mttemp = int(mthour.get())*3600 + int(mtminute.get())*60 + int(mtsecond.get())
except:
print("Please input the right value")
while mttemp >-1:
mtmins,mtsecs = divmod(mttemp,60)
mthours=0
if mtmins >60:
mthours, mtmins = divmod(mtmins, 60)
mthour.set("{0:2d}".format(mthours))
mtminute.set("{0:2d}".format(mtmins))
mtsecond.set("{0:2d}".format(mtsecs))
root.update()
time.sleep(1)
if (mttemp == 0):
messagebox.showinfo("Hinweis", "Zeit 2 abgelaufen ")
mttemp -= 1


mbtn = Button(root, text='Zeit 1', bd='5',
command= func1)
mbtn.place(x = 55,y = 250)

tbtn = Button(root, text='Zeit 2', bd='5',
command= func2)
tbtn.place(x = 55,y = 310)

if __name__ == '__main__':
p1 = Process(target=func1)
p1.start()
p2 = Process(target=func2)
p2.start()
p1.join()
p2.join()

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

@Moere: Das als `tk` importierte `tkinter` wird nirgends verwendet. Sollte es aber, denn der Sternchen-Import sollte nicht sein, denn Sternchen-Importe sind Böse™. Da holt man sich gerade bei `tkinter` fast 200 Namen ins Modul von denen nur ein kleiner Bruchteil verwendet wird. Auch Namen die gar nicht in `tkinter` definiert werden, sondern ihrerseits von woanders importiert werden. Das macht Programme unnötig unübersichtlicher und fehleranfälliger und es besteht die Gefahr von Namenskollisionen.

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.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Besonders unübersichtlich wird es wenn man das Hauptprogramm auf Modulebene auch noch mit Funktionsdefinitionen mischt. Und das sorgt auch in Deinem Fall dafür, dass sich mehrere Fenster öffnen, weil der Code auf Modulebene für jedem `multiprocessing.Process()` erneut ausgeführt wird.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Bei den `StringVar`-Objekten sollte zwischen dem Präfix und dem Rest ein Unterstrich stehen. Und selbst dann sind die beiden Sätze an Objekten schlecht am Namen zu unterscheiden. Verwende keine kryptischen Abkürzungen. Ich kann nicht mal raten was die `m`- beziehungsweise `mt`-Präfixe bedeuten sollen.

Die Namen für die `Button`\s sind auch schlecht, weil kryptisch. Die brauchen aber auch gar keine Namen, genau wie die `Entry`-Objekte keine Namen brauchen.

Die Werte bei `tkinter.Variable`-Objekten kann man schon beim erstellen festlegen.

Vergiss `place()`. Das funktioniert nur auf dem Rechner mit dem Monitor und den Systemeinstellungen bei Dir oder bei Leuten die das sehr ähnlich haben. Hier sieht das beispielsweise so kaputt aus:
Bild

Die Codestruktur zum erstellen der Eingabefelder + Button für die beiden Timer ist fast gleich, das sollte man nicht zweimal hinschreiben, sondern mindestens eine Funktion schreiben die das erstellt.

`func1` und `func2` sind schlechte Namen. Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt die sie ausführen, damit der Leser weiss was die machen.

Die beiden Funktionen sind von der Struktur her auch vollkommen identisch, das sind also eigentlich gar nicht zwei verschiedene Funktionen, sondern nur eine, die entsprechend parametrisiert werden muss. Das muss sowieso passieren, denn alles was Funktionen und Methoden ausser Konstanten benötigen bekommen sie als Argument(e) übergeben. Dazu braucht man mindestens `functools.partial()`, bei jeder nicht-trivialen GUI aber objektorientierte Programmierung (OOP). Also Klassen selber schreiben.

Keine nackten ``except``\s ohne konkrete Ausnahme(n). Du behandelst *alle* Ausnahmen mit einer `print()`-Ausgabe die gar nichts mit der Ausnahme zu tun haben muss und die Fehlersuche unnötig schwer macht. Was hier behandelt werden sollte ist ein `ValueError` wenn `int()` fehlschlägt, und nichts anderes.

Der nächste Fehler bei der Fehlerbehandlung ist es nach der Ausnahme so weiter zu machen als wäre nichts passiert. Das führt hier unweigerlich zu einem Folgefehler, denn wenn eine Ausnahme aufgetreten ist, dann ist `mtemp` undefiniert, der Versuch darauf zuzugreifen wird also zwingend einen `NameError` zur Folge haben.

Hier wieder die Frage was der `m`-Präfix bedeuten soll? Welche Temperatur ist denn diese `mtemp`? Ja, auch `temp` ist ein ungünstiger Name. Was dieser Wert tatsächlich bedeutet wäre wohl `total_seconds`.

Die Sonderbehandlung ``if mmins > 60`` macht nicht wirklich Sinn und ist zudem fehlerhaft, denn auch bei 60 Minuten will man das mit ``divmod()`` aufteilen, denn das ist ja bereits eine volle Stunde und Null Minuten.

Das mit `multiprocessing` ist totaler Unsinn weil Prozesse per Definition in unterschiedlichen Adressräumen laufen, also keinen Zugriff auf Variablen in anderen Prozessen haben.

Was auch nicht geht ist das `update()` in der Schleife. Da muss man auf einige Dinge achten. Unter anderem das während des `update()`-Aufrufs nicht ein weiterer `update()`-Aufruf passieren darf. Was aber bei Deinen zwei Timern problemlos passieren kann, nämlich dann wenn in dem `update()`-Aufruf ein `Button`-Ereignis verarbeitet wird das ja in einer ``while``-Schleife mit einem `update()`-Aufruf landen wird. Selbst wenn `update()` „re-entrant“ *wäre*, würde das effektiv den vorherigen Timer solange stoppen bis der jeweils zuletzt gestartete Timer abgelaufen ist. Sich selbst eine Hauptschleife mit `update()` basteln zu wollen kann sehr leicht in die Hose gehen, darum sollte man das gar nicht erst anfangen. Die Hauptschleife ist `mainloop()`. Und die sagt, Du sollst keine selbstgebastelten Hauptschleifen neben mir haben. 😉

`time.sleep()` pro Sekunde ist problematisch weil das *mindestens* eine Sekunde schläft. Es kann auch länger. Und das bei jedem Aufruf, dass heisst die Verzögerungen addieren sich, so das die Ungenauigkeit mit der Länge des eingestellten Timers wächst. Das gilt auch für `tkinter.Widget.after()` was man nutzen muss um das Problem mit der ``while``-Schleife und `update()` zu lösen.

Der überarbeitete Code, mit den genannten Problemen, bis hierher:

Code: Alles auswählen

#!/usr/bin/env python3
import time
import tkinter as tk
from functools import partial
from tkinter import messagebox


def do_countdown(widget, timer_number, hour_var, minute_var, second_var):
    try:
        total_seconds = (
            int(hour_var.get()) * 3600
            + int(minute_var.get()) * 60
            + int(second_var.get())
        )
    except ValueError:
        print("Please input a valid value")
    else:
        while True:
            minutes, seconds = divmod(total_seconds, 60)
            hours, minutes = divmod(minutes, 60)

            hour_var.set(f"{hours:02d}")
            minute_var.set(f"{minutes:02d}")
            second_var.set(f"{seconds:02d}")
            #
            # FIXME This is broken by design.  Don't use `update()`.  *The* main
            #   loop is `mainloop()`.
            #
            widget.update()
            #
            # FIXME Not accurate.  May sleep longer than 1 second.  Each time it
            #   is called, i.e. this adds up.  Same is true for
            #   `Widget.after()`!
            #
            time.sleep(1)
            
            if total_seconds <= 0:
                messagebox.showinfo(
                    "Hinweis", f"Zeit {timer_number} abgelaufen"
                )
                break
            
            total_seconds -= 1


def add_timer_widgets(master, timer_number, start_value):
    hour, minute, second = start_value.split(":")
    hour_var = tk.StringVar(value=hour)
    minute_var = tk.StringVar(value=minute)
    second_var = tk.StringVar(value=second)

    frame = tk.Frame(master)
    for variable in [hour_var, minute_var, second_var]:
        tk.Entry(
            frame, width=3, font=("Arial", 18), textvariable=variable
        ).pack(side=tk.LEFT)
    frame.pack(side=tk.TOP)

    tk.Button(
        master,
        text=f"Zeit {timer_number}",
        bd=5,
        command=partial(
            do_countdown,
            master,
            timer_number,
            hour_var,
            minute_var,
            second_var,
        ),
    ).pack(side=tk.TOP, anchor=tk.W)


def main():
    root = tk.Tk()
    root.title("Timerversuch")

    for timer_number, start_value in enumerate(["00:00:05", "00:00:10"], 1):
        add_timer_widgets(root, timer_number, start_value)

    root.mainloop()


if __name__ == "__main__":
    main()
Was hier auch noch problematisch ist, ist das während ein Timer läuft a) Eingaben in den Eingabefeldern gemacht werden können und b) der Button weiterhin aktiv ist und weitere Aufrufe des gleichen Timer-Codes ausgelöst werden können.

Vielleicht etwas weniger gravierend: Negative Eingaben für die Zeitkomponenten eines Timers sind möglich.

Und Fehleingaben sollte man vielleicht nicht per `print()` bekannt machen, sondern auch über die GUI, beispielsweise per `messagebox.showerror()`.

Um das sinnvoll lauffähig zu bekommen darf ein Button keine lang laufende ``while``-Schleife auslösen, sondern das muss in kleineren Schritten per `Widget.after()`-Aufrufen gelöst werden. Also jeder Schleifendurchlauf muss durch einen solchen Aufruf ersetzt werden.

Und die Ungenauigkeit wird man los in dem man nicht die Sekunden selbst runterzählt, sondern den Zeitpunkt von jetzt bis zum Ende des Countdowns ausrechnet, und dann die Anzeige regelmässig aktualisiert bis bis ”jetzt” grösser oder gleich dem Countdown-Ende ist. ”Jetzt” ermittelt man dabei jeweils mit `time.monotonic()`. Als Frequenz für die Aktualisierung bietet sich die Hälfte der kleinsten Einheit an. Also jede halbe Sekunde sollte man die Anzeige aktualisieren, dann kann man sicher sein, dass jede Sekunde angezeigt wird.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Moere
User
Beiträge: 10
Registriert: Sonntag 10. Januar 2021, 17:23

@ __blackjack__

Danke für deine schnelle Antwort.
Sehr kompakter Text von dir :-)
Das mein Erstelltes ein sehr schlecht zusammen gewürfelte Etwas ist, ist mir durchaus bewusst . Ich möchte es ja lernen .

Worauf es mir ankam, war eigentlich das beide Timer auch mal gleichzeitig laufen könnten. Später dann mal mit Soundausgabe falls einer abgelaufen ist.

LG
Moere
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

Moere hat geschrieben: Montag 11. Januar 2021, 09:29Worauf es mir ankam, war eigentlich das beide Timer auch mal gleichzeitig laufen könnten. Später dann mal mit Soundausgabe falls einer abgelaufen ist.
Dazu mußt Du den Code erst einmal auf `after` umschreiben und am besten gleich eine Klasse für einen Zähler implementieren. Dann kannst Du beliebig viele Zähler erzeugen.
Moere
User
Beiträge: 10
Registriert: Sonntag 10. Januar 2021, 17:23

Sirius3 hat geschrieben: Montag 11. Januar 2021, 09:34
Moere hat geschrieben: Montag 11. Januar 2021, 09:29Worauf es mir ankam, war eigentlich das beide Timer auch mal gleichzeitig laufen könnten. Später dann mal mit Soundausgabe falls einer abgelaufen ist.
Dazu mußt Du den Code erst einmal auf `after` umschreiben und am besten gleich eine Klasse für einen Zähler implementieren. Dann kannst Du beliebig viele Zähler erzeugen.
Und das als Anfänger, das wird nix !
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

Wenn man mit der Einstellung an alles rangeht ...
Moere
User
Beiträge: 10
Registriert: Sonntag 10. Januar 2021, 17:23

Danke für Eure Bemühungen, aber ich merk das es im Alter doch nicht mehr so geht .
Bin 74 Jahre und da fällt einem das nicht mehr so leicht .

Ich habe noch einen alten 286 -Rechner auf dem MS-Dos mit Basic (+compiler ) läuft, da kann ich das wenigstens Zeilenweise noch nachverfolgen .

Danke für euer Interesse
Moere
User
Beiträge: 10
Registriert: Sonntag 10. Januar 2021, 17:23

Hab mal versucht nach after um zu bauen - Flop ...


from tkinter import *
import time
from playsound import playsound

root = Tk()
root.geometry('400x300')
root.resizable(0,0)
root.config(bg ='blanched almond')
root.title('Test Timer')
Label(root, text = 'Scheiss Zeit geht nicht ' , font = 'arial 20 bold', bg ='papaya whip').pack()


sec = StringVar()
Entry(root, textvariable = sec, width = 2, font = 'arial 12').place(x=250, y=155)
sec.set('00')

mins= StringVar()
Entry(root, textvariable = mins, width =2, font = 'arial 12').place(x=225, y=155)
mins.set('00')

hrs= StringVar()
Entry(root, textvariable = hrs, width =2, font = 'arial 12').place(x=200, y=155)
hrs.set('00')


def countdown():
times = int(hrs.get())*3600+ int(mins.get())*60 + int(sec.get())
while times > -1:
minute,second = (times // 60 , times % 60)

hour = 0
if minute > 60:
hour , minute = (minute // 60 , minute % 60)

sec.set(second)
mins.set(minute)
hrs.set(hour)

root.update()
countdown.after(1000,countdown)

if(times == 0):
#playsound('Loud_Alarm_Clock_Buzzer.mp3')
sec.set('00')
mins.set('00')
hrs.set('00')
times -= 1


Label(root, font ='arial 15 bold', text = 'Zeit eingeben ').place(x = 40 ,y = 150)

Button(root, text='START', bd ='5', command = countdown, bg = 'antique white', font = 'arial 10 bold').place(x=150, y=210)

root.mainloop()
Benutzeravatar
Dennis89
User
Beiträge: 1125
Registriert: Freitag 11. Dezember 2020, 15:13

Moere hat geschrieben: Donnerstag 21. Januar 2021, 18:27 Hab mal versucht nach after um zu bauen - Flop ...
__blackjack__ hat geschrieben: Montag 11. Januar 2021, 04:54 das muss in kleineren Schritten per `Widget.after()`-Aufrufen gelöst werden
Du hast aber versucht 'after' von deiner eigenen Funktion aufzurufen, die gar kein 'after' besitzt.
Du musst 'after' von einem tkinter-Widget aufrufen, wenn ich das richtig verstanden habe.


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

Welche der Anmerkungen von __blackjack__ hast Du nicht verstanden, dass Du sie nicht gleich umgesetzt hast?
Zumindest das mit den *-Importen und Funktionen ist ja nicht sehr komplex.

Code: Alles auswählen

import tkinter as tk
from functools import partial
import time

def countdown(hrs, mins, sec, root):
    times = int(hrs.get())*3600+ int(mins.get())*60 + int(sec.get())
    while times > -1:
        minute,second = (times // 60 , times % 60)        
        hour = 0
        if minute > 60:
            hour , minute = (minute // 60 , minute % 60)
        sec.set(second)
        mins.set(minute)
        hrs.set(hour)
    
        root.update()
        # countdown.after(1000,countdown)
        time.sleep(1)
  
        if times == 0:
            #playsound('Loud_Alarm_Clock_Buzzer.mp3')
            sec.set('00')
            mins.set('00')
            hrs.set('00')
        times -= 1


def main():
    root = tk.Tk()
    root.geometry('400x300')
    root.resizable(0,0)
    root.config(bg ='blanched almond')
    root.title('Test Timer')
    tk.Label(root, text = 'Scheiss Zeit geht nicht ' , font = 'arial 20 bold',  bg ='papaya whip').pack()
    sec = tk.StringVar()
    tk.Entry(root, textvariable = sec, width = 2, font = 'arial 12').place(x=250, y=155)
    sec.set('00')
    mins= tk.StringVar()
    tk.Entry(root, textvariable = mins, width =2, font = 'arial 12').place(x=225, y=155)
    mins.set('00')
    hrs= tk.StringVar()
    tk.Entry(root, textvariable = hrs, width =2, font = 'arial 12').place(x=200, y=155)
    hrs.set('00')
    tk.Label(root, font ='arial 15 bold', text = 'Zeit eingeben ').place(x = 40 ,y = 150)
    tk.Button(root, text='START', bd ='5', command = partial(countdown, hrs, mins, sec, root), bg = 'antique white', font = 'arial 10 bold').place(x=150, y=210)
    root.mainloop()

if __name__ == '__main__':
    main()
Das mit dem `after` tut natürlich nicht (wäre schön gewesen, wenn Du auch die passende Fehlermeldung gepostet hättest), weil eine Funktion keine `after`-Methode hat, sondern diese nur bei Tkinter-Widgets existieren.

Code: Alles auswählen

def countdown(hrs, mins, sec, root):
    times = int(hrs.get())*3600 + int(mins.get())*60 + int(sec.get())
    times -= 1
    hours, seconds = divmod(times, 3600)
    minutes, seconds = divmod(seconds, 60)
    sec.set(f"{seconds:02d}")
    mins.set(f"{minutes:02d}")
    hrs.set(f"{hours:02d}")
    if times > 0:
        root.after(1000, countdown, hrs, mins, sec, root)
Moere
User
Beiträge: 10
Registriert: Sonntag 10. Januar 2021, 17:23

Vielen Dank, jetzt habe ich es hinbekommen.

Das Programm läuft auch wie erwartet und es wird eine *.ini datei geschrieben.
Leider will das einlesen selbiger zum Fiasko .
Die Funktion "onSafe" tut es,
onRestore bricht mit Fehler [no section"Allgemein"] ab .


def onSafe():
filename = "zeit.ini"
file = open(filename, 'w')
Config = configparser.ConfigParser()
Config.add_section('Allgemein')
Config.set("Allgemein", "Name", name.get())
Config.set("Allgemein", "Vorname", vorname.get())
Config.write(file)
file.close()

def onRestore():
filename = "zeit.ini"
file = open(filename, 'r')
Config = configparser.ConfigParser()
name = str(Config.get("Allgemein", 'name'))
vorname = str(Config.get("Allgemein", 'vorname'))
file.close()

Ich vermute mal, die Datei wird gar nicht gelesen ??


LG
Moere
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

Safe ist nicht gleich save.
Dateien öffnet man immer innerhalb des with-Statements, da dann die Datei auch wieder sicher geschlossen wird.
`filename` sollte nicht mehrfach im Code vorkommen, also entweder als Parameter übergeben werden, oder als Konstante definieren. Soll zeit.ini wirklich im aktuellen Arbeitsverzeichnis liegen?
name und vorname kommen aus dem nichts, sollten aber Argumente sein. Variablennamen werden generell klein geschrieben.

Bei onRestore fehlt ja das Lesen. Du öffnest nur eine Datei und machst nichts damit.
Auch wenn Du die Datei liest, machst Du nichts damit, weil name und vorname nur lokale Variablen sind. Aus der onSafe-Funktion würde ich raten, dass Du eigentlich das möchtest:

Code: Alles auswählen

FILENAME = "zeit.ini"

def on_save(name, vorname):
    config = configparser.ConfigParser()
    config.add_section('Allgemein')
    config.set("Allgemein", "Name", name.get())
    config.set("Allgemein", "Vorname", vorname.get())
    with open(FILENAME, "w", encoding="utf8") as output:
        config.write(output)

def on_restore(name, vorname):
    config = configparser.ConfigParser()
    config.read(FILENAME)
    name.set(config.get("Allgemein", 'Name'))
    vorname.set(config.get("Allgemein", 'Vorname'))
Antworten