Seite 1 von 1
Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Mittwoch 8. Dezember 2010, 23:49
von NilsV
Hallo,
ich habe als Übung ein kleines Countdown Programm in Python 3 geschrieben und dabei, unerwarteter weise, sogar etwas neues gelernt (mein erster Kontakt mit Threads).
Das ganze lässt sich bestimmt noch optimieren, aber es funktioniert ohne Probleme.
In dem Programm kann man...
- eine Dauer (in Sekunden) für den Countdown festlegen.
- den laufenden Countdown abbrechen.
- optional am Ende des Countdown einen Systembefehl ausführen lassen.
Die letzten 10 Sekunden wird der Countdown in rot (statt schwarz) angezeigt.
Bild:
Code: Alles auswählen
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import tkinter as tk
import time
import _thread
import os
def countdownThread():
bstart["state"] = "disabled"
bstop["state"] = "normal"
global anhalten
anhalten = 0
t = eingabe.get()
try:
t = int(t)
_thread.start_new_thread(countdown, (t,))
except:
status["text"] = "Ungültige Eingabe"
bstart["state"] = "normal"
bstop["state"] = "disabled"
def countdown(t):
zeit = t
status["text"] = "Countdown läuft"
while zeit >= 0 and anhalten == 0:
anzeige["text"] = zeit
if zeit <= 10:
anzeige["fg"] = "#FF0000"
else:
anzeige["fg"] = "#000000"
time.sleep(1)
zeit = zeit - 1
bstart["state"] = "normal"
bstop["state"] = "disabled"
if anhalten == 0:
status["text"] = "Countdown beendet"
befehl()
else:
status["text"] = "Countdown abgebrochen"
def stop():
global anhalten
anhalten = 1
bstop["state"] = "disabled"
def befehl():
b = ebefehl.get()
if b != "":
os.system(b)
anhalten = 0
main = tk.Tk()
main.title("Countdown")
status = tk.Label(main, text = "")
status.pack()
anzeige = tk.Label(main, font = "Arial 32 bold", text = "")
anzeige.pack()
lbeingabe = tk.Label(main, text = "Countdown in Sekunden:")
lbeingabe.pack()
eingabe = tk.Entry(main)
eingabe.pack()
lbbefehl = tk.Label(main, text = "Auszuführender Befehl:")
lbbefehl.pack()
lbbefehl2 = tk.Label(main, text = "(optional)")
lbbefehl2.pack()
ebefehl = tk.Entry(main)
ebefehl.pack()
bstart = tk.Button(main, text = "Start", command = countdownThread)
bstart.pack()
bstop = tk.Button(main, text = "Abbrechen", state = "disabled", command = stop)
bstop.pack()
main.mainloop()
Gruß
Nils
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Donnerstag 9. Dezember 2010, 01:45
von BlackJack
@NilsV: Vergiss es -- das funktioniert nicht. Du magst jetzt vielleicht einwenden, dass es bei Dir doch funktioniert, aber das ist dann nur Zufall das es nicht abstürzt. Man darf nicht aus einem anderen Thread auf die GUI zugreifen als aus dem, in dem die `mainloop()` läuft.
Ausserdem ist der führende Unterstrich im Namen vom `_thread`-Modul ein Hinweis, dass es nicht zur öffentlichen API gehört. Wenn Du Threads haben möchtest, solltest Du das `threading`-Modul verwenden.
In diesem Fall vielleicht nicht so wichtig, aber `time.sleep()` garantiert nicht, dass es genau die angegebene Zeit "schläft", also kann sich eine eventuelle Ungenauigkeit jede Sekunde aufsummieren.
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Donnerstag 9. Dezember 2010, 15:01
von syntor
BlackJack hat geschrieben:In diesem Fall vielleicht nicht so wichtig, aber `time.sleep()` garantiert nicht, dass es genau die angegebene Zeit "schläft", also kann sich eine eventuelle Ungenauigkeit jede Sekunde aufsummieren.
Genau. Stattdessen solltest du dir die Zeit merken, wo du angefangen hast, und sie mit der aktuellen vergleichen.
Code: Alles auswählen
from time import time, sleep
duration = get_duration()
initial = time()
while time() < initial + duration:
sleep(1)
# do something
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Sonntag 12. Dezember 2010, 16:04
von NilsV
Hi,
Wenn Du Threads haben möchtest, solltest Du das `threading`-Modul verwenden.
Habe mich in der Zwischenzeit mit dem Modul threading auseinandergesetzt und kann nun den Thread über dieses Modul starten.
Man darf nicht aus einem anderen Thread auf die GUI zugreifen als aus dem, in dem die `mainloop()` läuft.
Vielen Dank für den Hinweis, habe nach dem ersten "Aber es funktioniert doch!" Reflex ein wenig recherchiert und muss wohl "leider" akzeptieren das dem so ist.
Ich habe mir die letzten Tage den Kopf darüber zerbrochen wie ich Informationen zwischen Threads austauschen soll, wenn ich nicht direkt Informationen von einem Thread an einen anderen schicken darf und finde für das Problem keine Lösung. In der Standard Library bin ich über queue gestolpert, bin aber leider nicht wirklich schlau aus der Erklärung geworden. Ist queue das was ich brauche? Wenn ja, gibt es ein halbwegs einsteiger freundliches Tutorial/Einführung/Anleitung dazu?
Stattdessen solltest du dir die Zeit merken, wo du angefangen hast, und sie mit der aktuellen vergleichen.
Der Ansatz gefällt mir, vielen Dank für den Tipp.
Gruß
Nils
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Sonntag 12. Dezember 2010, 17:45
von BlackJack
@NilsV: `queue` wäre schon das Richtige, aber bei diesem kleinen Programm sehe ich den Sinn von Threads nicht wirklich. Du wirst Dich in jedem Fall mit der `after()`-Methode auf Tkinter-Widgets auseinander setzen müssen und damit kann man dieses kleine Programm auch völlig ohne Threads realisieren.
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Montag 13. Dezember 2010, 19:15
von NilsV
Hallo,
ich verstehe die Logik hinter der after() Methode nicht. Nach allem was ich gelesen habe scheint diese Methode keine Schleife zu erzeugen, sondern einen Zeit verzögerten Funktionsaufruf zu ermöglichen (ohne das die aufgerufene Funktion das GUI einfriert). Also muss wohl die aufgerufene Funktion die schleife erzeugen.
Da ich keine Zeitverzögerung möchte, habe ich als Verzögerung 0 angegeben, bekomme im Terminal aber eine Meldung "TypeError: 'int' object is not callable" und mein Label zeigt "after#0" an. Wenn ich die 0 weglasse zeigt mein Label "none" an.
Ich habe versucht die after() Methode auf main und auf mein Label anzuwenden, beide Varianten bringen kein brauchbares Ergebnis.
Wenn ich das ganze so umstelle das "lbcounter["text"] = str(zeit)" mit in der Funktion ist, um "zeit = main.after(0, countdown(zeit))" ohne "zeit = " aufrufen zu können, bricht das ganze mit einem regressions Fehler ab.
Kann mir bitte jemand erklären wie die after() Methode richtig anzuwenden ist, oder mir einen Link zu einer verständlichen Erklärung geben?
Code: Alles auswählen
import tkinter as tk
import time
def countdown(zeit):
while zeit >= 0:
zeit = zeit - 1
time.sleep(1)
return(zeit)
main = tk.Tk()
zeit = 10
zeit = main.after(0, countdown(zeit))
lbcounter = tk.Label(main)
lbcounter["text"] = str(zeit)
lbcounter.pack()
main.mainloop()
Gruß
Nils
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Montag 13. Dezember 2010, 19:59
von BlackJack
@NilsV: Die Aufgerufene Funktion darf natürlich keine länger laufende Schleife beinhalten sondern darf nur ganz kurz laufen. Solange die läuft blockiert ja die GUI wieder.
Du übergibst `after()` da nicht die Funktion sondern Du rufst die Funktion auf und übergibst damit `after()` den Rückgabewert. Und der ist in diesem Fall `None` und *das* versucht `after()` dann nach der angegebenen Zeit aufzurufen -- was natürlich nicht geht.
Ein Verzögerung willst Du sehr wohl haben, denn Du musst `after()` *statt* einer Schleife benutzen und zwar immer wieder. Zum Beispiel einmal in der Sekunde. Du musst da eine Funktion übergeben, die die Anzeige aktualisiert. Nur kurz, und nur einmal. Und dann muss die Funktion selber dafür sorgen, dass sie nach einer gewissen Zeit wieder aufgerufen wird, für die nächste Aktualisierung.
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Montag 13. Dezember 2010, 20:39
von NilsV
Hallo,
so wie ich das verstanden habe, könnte das dann so aussehen:
Code: Alles auswählen
import tkinter as tk
import time
def countdown(zeit):
zeit = zeit
if zeit > 0:
zeit = zeit - 1
lbcounter["text"] = str(zeit)
main.after(1000, countdown(zeit))
main = tk.Tk()
zeit = 10
lbcounter = tk.Label(main)
lbcounter.pack()
lbcounter["text"] = str(zeit)
main.after(1000, countdown(zeit))
main.mainloop()
Jetzt bekomme ich aber erst nach Ablauf der ca. 10 Sekunden ein Fenster angezeigt, mit dem Label in dem dann bereits 0 steht.
Gruß
Nils
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Montag 13. Dezember 2010, 20:47
von DaMutz
Diese Bemerkung hast du nicht umgesetzt:
BlackJack hat geschrieben:Du übergibst `after()` da nicht die Funktion sondern Du rufst die Funktion auf und übergibst damit `after()` den Rückgabewert. Und der ist in diesem Fall `None` und *das* versucht `after()` dann nach der angegebenen Zeit aufzurufen -- was natürlich nicht geht.
partial ist dein Freund.
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Montag 13. Dezember 2010, 20:59
von BlackJack
Und die erste Zeile in der `countdown()`-Funktion ist ein bisschen sinnfrei.
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Dienstag 14. Dezember 2010, 03:20
von NilsV
Hi,
ich habe es zum laufen gekriegt. Es reichte nicht nur "zeit" an die Funktion zu übergeben. Wenn ich den Text des Labels innerhalb der Funktion ändern möchte, muss ich auch das Label selber mitliefern, damit die Zeile "lbcounter["text"] = str(zeit)" ordnungsgemäß den in einen String umgewandelten Wert "zeit" an das Label zurückgeben kann (so habe ich das jetzt zumindest verstanden).
Code: Alles auswählen
import tkinter as tk
def countdown(lbcounter, zeit):
if zeit > 0:
zeit = zeit - 1
lbcounter["text"] = str(zeit)
main.after(1000, countdown, lbcounter, zeit)
main = tk.Tk()
zeit = 10
lbcounter = tk.Label(main)
lbcounter.pack()
lbcounter["text"] = str(zeit)
main.after(1000, countdown, lbcounter, zeit)
main.mainloop()
Ohne die Erläuterung zum Rückgabewert von after() hätte ich da noch Jahre dran sitzen können, also vielen Dank für eure Hilfe und Geduld.
Auch wenn ich hier ohne "partial" ausgekommen bin, finde ich die Informationen über das functools Modul, das leider in keinem meiner Pythonbücher erwähnt wird, sehr interessant.
Nun kann ich versuchen mein kleines Countdown Programm, mit dem neuen Wissen, neu zu schreiben. Einiges andere lässt sich sicher auch noch verbessern (z.B. eine separate Funktion zu nutzen, die den state Wert der Buttons einfach vertauscht.)
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Dienstag 14. Dezember 2010, 21:29
von NilsV
Hallo,
hier die aktuelle Version:
Code: Alles auswählen
import tkinter as tk
import os
def countdown(lbCountdown, zeit):
global abbruch
if abbruch == False:
lbCountdown["text"] = zeit
zeit = int(zeit)
if zeit > 0:
if int(zeit) <= 10:
lbCountdown["fg"] = "#FF0000"
zeit = zeit - 1
zeit = str(zeit)
main.after(1000, countdown, lbCountdown, zeit)
else:
status(2)
befehl()
def countdownStart():
status(1)
lbCountdown["fg"] = "#000000"
global abbruch
abbruch = False
zeit = eZeit.get()
main.after(0, countdown, lbCountdown, zeit)
def countdownAbbruch():
status(3)
global abbruch
abbruch = True
def status(s):
"""s erwartet 1, 2 oder 3:
1 = Countdown läuft
2 = Countdown beendet
3 = Countdown abgebrochen"""
if s == 1:
lbStatus["text"] = "Countdown läuft"
bStart["state"] = "disabled"
bAbbruch["state"] = "normal"
if s == 2:
lbStatus["text"] = "Countdown beendet"
bStart["state"] = "normal"
bAbbruch["state"] = "disabled"
if s == 3:
lbStatus["text"] = "Countdown abgebrochen"
bStart["state"] = "normal"
bAbbruch["state"] = "disabled"
def befehl():
b = eBefehl.get()
if b != "":
os.system(b)
main = tk.Tk()
lbStatus = tk.Label(main)
lbStatus.pack()
lbCountdown = tk.Label(main, font = "Arial 32 bold")
lbCountdown.pack()
lbZeit = tk.Label(main, text = "Dauer in Sekunden:")
lbZeit.pack()
eZeit = tk.Entry(main)
eZeit.pack()
lbBefehl = tk.Label(main, text = "Auszuführender Befehl:")
lbBefehl.pack()
lbBefehl2 = tk.Label(main, text = "(optional)")
lbBefehl2.pack()
eBefehl = tk.Entry(main)
eBefehl.pack()
bStart = tk.Button(main, text = "Start", command = countdownStart)
bStart.pack()
bAbbruch = tk.Button(main, text = "Abbruch", command = countdownAbbruch)
bAbbruch["state"] = "disabled"
bAbbruch.pack()
main.mainloop()
Das einzige was mich noch stört ist dass das Programm beim ausführen eines externen Programmes einfriert, bis dieses wieder beendet wurde. Kann man das auch ohne Threads verhindern, oder muss ich "os.system(b)" in einem neuen Thread ausführen?
Gruß
Nils
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Donnerstag 16. Dezember 2010, 10:12
von BlackJack
@NilsV: Das externe Programm könntest Du mit dem `subprocess`-Modul asynchron starten.
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Donnerstag 16. Dezember 2010, 14:55
von NilsV
Hi,
Code: Alles auswählen
def befehl():
b = eBefehl.get()
if b != "":
subprocess.Popen(b)
Funktioniert bei vielen Programmen einwandfrei, wenn ich in dem Ordner in dem sich mein Countdown Programm befindet aber z. B. eine Audio-Datei "countdown.wav" habe und diese mit dem Befehl "play countdown.wav" (unter Linux mit installiertem sux) abspielen lassen möchte, funktioniert das leider nicht. Mit "os.system(b)" ging das. Als Fehlermeldung bekomme ich:
Code: Alles auswählen
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python3.1/tkinter/__init__.py", line 1402, in __call__
return self.func(*args)
File "/usr/lib/python3.1/tkinter/__init__.py", line 490, in callit
func(*args)
File "tk_countdown-v2.py", line 21, in countdown
befehl()
File "tk_countdown-v2.py", line 58, in befehl
subprocess.Popen(b)
File "/usr/lib/python3.1/subprocess.py", line 647, in __init__
errread, errwrite)
File "/usr/lib/python3.1/subprocess.py", line 1158, in _execute_child
raise child_exception
OSError: [Errno 2] No such file or directory
Gruß
Nils
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Donnerstag 16. Dezember 2010, 15:42
von Leonidas
Wie sieht denn ``b`` aus?
Re: Countdown (Zeitverzögert Systembefehle ausführen)
Verfasst: Donnerstag 16. Dezember 2010, 21:51
von NilsV
Hi,
b ist ein String, der aus einem tkinter.Entry ausgelesen wird. Also z. B.:
(funktioniert)
(funktioniert nicht)
Gruß
Nils