Countdown (Zeitverzögert Systembefehle ausführen)

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

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:
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
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.
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

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
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

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
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.
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

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
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.
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

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
Benutzeravatar
DaMutz
User
Beiträge: 202
Registriert: Freitag 31. Oktober 2008, 17:25

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.
BlackJack

Und die erste Zeile in der `countdown()`-Funktion ist ein bisschen sinnfrei.
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

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.)
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

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
BlackJack

@NilsV: Das externe Programm könntest Du mit dem `subprocess`-Modul asynchron starten.
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

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
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Wie sieht denn ``b`` aus?
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
NilsV
User
Beiträge: 30
Registriert: Dienstag 17. August 2010, 12:35

Hi,

b ist ein String, der aus einem tkinter.Entry ausgelesen wird. Also z. B.:

Code: Alles auswählen

"mousepad"
(funktioniert)

Code: Alles auswählen

"play countdown.wav"
(funktioniert nicht)

Gruß

Nils
Antworten