after() Funktion nur einmal möglich?

Fragen zu Tkinter.
jake-the-snake

Sonntag 21. Januar 2018, 20:12

Abend Forum

Ich habe folgendes Gerüst:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *
from tkinter import messagebox
from tkinter.scrolledtext import ScrolledText

def short_message():
    messagebox.askyesno("System-Meldung", "Programm aktiv - Bitte warten...")
    print ("Funktion short_message ausgeführt!")

# Fenster-Titel
root = Tk(className=" Hauptfenster")
root.resizable(FALSE,FALSE)
root.attributes("-topmost", 1)
root.wm_attributes('-type', 'splash')

def center_window(width=300, height=200):
    # get screen width and height
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()

    # calculate position x and y coordinates
    x = (screen_width/2) - (width/2)
    y = (screen_height/2) - (height/2)
    root.geometry('%dx%d+%d+%d' % (width, height, x, y))
    
center_window(800, 480)

# Titel Schrift ueber Programm-Fenster 
zeroLabel = Label(root, text=">> Hauptfenster mit Aktivität <<", font=("TimesNewRoman", 10), bg=('#999999'))
zeroLabel.place(x=305,y=28)

root.after(2000, short_message)

root.mainloop()
Normalerweise sollte das Programm bis zum Fensteraufbau "root = Tk(className=" Hauptfenster")" durchlaufen. Wenn die Ablaufroutine auf "root.after(2000, short_message)" trifft, wird meine Funktion " short_message" aufgerufen und ausgeführt. Nach dem Klicken (ja oder nein ist erst mal Wurst), sollte doch der Programmablauf wieder in den mainloop springen. Dann sollte bei einem erneuten Durchlauf doch wieder "root.after(2000, short_message)" aufgerufen werden??? Zeile 13 bis 36 müsste doch endlos aktiv sein???

Fakt ist, dass die after()-Anweisung im mainloop nur einmal wirkt.

Warum?

Gruss jts
__deets__
User
Beiträge: 3111
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 21. Januar 2018, 21:01

Weil das eben so ist. after scheduled - wie in der Dokumentation auch klar beschrieben - nur einmal, und man muss re-schedulen (gerne auch im callback selbst):

http://effbot.org/tkinterbook/widget.ht ... ter-method
"""
after(delay_ms, callback=None, *args) [#]
Registers an alarm callback that is called after a given time.

This method registers a callback function that will be called after a given number of milliseconds. Tkinter only guarantees that the callback will not be called earlier than that; if the system is busy, the actual delay may be much longer.

The callback is only called once for each call to this method. To keep calling the callback, you need to reregister the callback inside itself:
"""
jake-the-snake

Sonntag 21. Januar 2018, 21:09

Abend _deets_

Ich habe die definierte Funktion durch die selbe Zeile wie in der mainloop ergänzt::

Code: Alles auswählen

def short_message():
    messagebox.askyesno("System-Meldung", "Programm aktiv - Bitte warten...")
    print ("Funktion short_message ausgeführt!")
    root.after(2000, short_message)
Jetzt fragt er immer wieder. Das heißt die messagebox fragt immer wieder nach. Soweit so gut. In wie weit mir das jetzt weiterhilft - mal schauen...

Also es geht mit der Ergänzung oben.

Gruss jts
__deets__
User
Beiträge: 3111
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 21. Januar 2018, 21:19

Na, wenn du programmierst, das alle 2 Sekunden eine MessageBox aufgeht, dann geht eben alles zwei Sekunden eine Messagebox auf. Ob das sinnvoll ist, kannst ja nur du beurteilen.
jake-the-snake

Sonntag 21. Januar 2018, 21:37

Hallo

Ich bin immer noch am Überlegen, wie der eigensinnige Programmablauf von der GUI beeinflusst werden kann.
Soweit ich das begreife, ist die GUI eine Endlosschleife. Sie bricht nur bei Ereignissen ab. Zum Beispiel wenn eine Funktion "app = App(root)" oder eben "after()" aufgerufen wird.

Also hat man, wie jetzt bei mir, oben einen Programmteil (nennen wir den mal Block A), und unten eine Endlosschleife, die tk-GUI-Fenstergeschichte (hier mal als Block B betitelt). Veränderungen in dem angezeigten Fenster sind nur dann möglich, wenn sie vollendet vorliegen und das Programm sich im Block B - der Endlosschleife - befindet.

Sollte sich jetzt das Python-Programm im Block A befinden, wo es in einer Rechenroutine oder Schleife steckt, dessen Ausführung eine gewisse Zeit benötigt, ist das Python-Programm also nicht in dem unteren Block B. Das heißt, dass auch, solange gerechnet wird, keine Änderungen an der Anzeige, bzw. dem Fensterinhalt getätigt werden können. Das ist ein Problem!

Bei langen Schleifendurchläufen könnte man dem Programm durchaus sagen: Setze alle bsp. 50 Durchläufe ein Signal ab, oder ändere irgendetwas an der Grafik. Leider ist man Programmtechnisch an der Stelle ja im Block A -> Und da kann man grafisch nichts ändern... schwierig - das ganze...

Gruss jts
__deets__
User
Beiträge: 3111
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 21. Januar 2018, 21:51

Auch wenn's etwas holperig ausgedrueckt ist, klingt das ganz richtig. Die Hauptschleife muss betreten werden, um die Ereignisbehandlung auszuloesen. Ereignisse sind natuerlich Benutzerinteraktionen, aber auch Timer oder zB Netzwerk-Kommunikation respektive genreller IO.

Wenn man nun laengere Aufgaben erledigen will, hat man vier Moeglichkeiten:

1) es einfach tun. Die GUI blockiert, und das ist doof. Windows sagt dann ggf. sogar so etwas wie "das Programm reagiert nicht mehr, soll ich es beenden?"
2) die Aufgabe in kleine Happen zerteilen, die man aus einem Timer heraus immer wieder ausfuehrt. Das ist etwas muehselig, weil man sehr aufpassen muss, wie man das anstellt, und sich schnell verprogrammiert, womit man dann wieder bei 1 landet.
3) man treibt den Event-Loop selbst. Das ist allerdings in Tkinter nicht vorgesehen, und hat den Nachteil, das die langwierige Berechnung zwangsweise verknuepft ist mit der Idee, das es eben eine GUI gibt. Ausserdem *kann* man auch da in die Falle von 2 tappen, und ist wieder bei 1...
4) es bleibt die einzig verlaessliche: einen neuen Thread starten, und in dem Thread die Berechnung durchfuehren. Dann kann die lang anhaltende Berechnung ungestoert bzw. ohne zu stoeren ablaufen. AAAABER: hier muss man aufpassen, das man *NICHT* aus dem Hintergrundthread GUI-Objekte manipuliert: das moegen GUIS ueberhautp nicht. Das heisst, das man das ueblicherweise mit zb einer Ergebnis-Queue verknuepft, die man im Event-Loop mit after periodisch checkt, und dann entsprechend reagiert (zB Button aktivieren/de-aktivieren, Fortschritt anzeigen, etc....)

Zu tkinter und Queues findet sich hier im Forum tonnenweise Beispielcode, gerne zB von BlackJack.
jake-the-snake

Sonntag 21. Januar 2018, 22:04

Abend nochmal

OK. Da habe ich eine Menge Input bekommen. Wird eine Zeit brauchen, das durch zu gehen. Vielen Dank für die Infos. Jetzt habe ich mal eine grobe Marschrichtung :wink:

zu 1) Macht Linux - Gott sei Dank - nicht!

zu 2) Wäre eine Option. Müsste da halt wissen, wie ich von Block A in die mainloop eintrete und wieder zurück kehre.

zu 3) Was meinst du mit selber treiben?

zu 4) An eine Auslagerung des rechenintensiven Programmteiles hatte ich auch schon gedacht. Doch wie so was von statten geht weiss ich nicht. Als extra Programm? Dann müsste ich ja auch eine Art pipe "|" legen, die die Programminfos vom ausgelagerten Teil dem Hauptprogramm zu schiebt :wink:

Wie gesagt, ich geh eine paar Runden im Forum "schnüffeln"...

Bis die Tage...

Gruss jts
Sirius3
User
Beiträge: 8112
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 21. Januar 2018, 22:14

@jake-the-snake: __deets__ hat die Punkte 1 bis 3 nur dazugeschrieben, dass, falls Du über solche „Lösungen“ stolperst, nicht denkst, sie wären gut; damit brauchst Du Dich also nicht beschäftigen. Bei Punkt 4 steht doch schon, dass Du Threads benutzen sollst, alternativ multiprocessing, wobei für beides Queues existieren, um Programminfos hin und her zu schieben.
Benutzeravatar
wuf
User
Beiträge: 1481
Registriert: Sonntag 8. Juni 2003, 09:50

Montag 22. Januar 2018, 08:07

Hi jake-the-snake

Habe leider nicht ganz verstanden was das Problem ist. Habe in deinem Skript einen Zähler 'count_up' eingebaut, welcher auch während der Messagebox-Aktion weiterläuft. Hier das modifizierte Skript:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
 
from tkinter import *
from tkinter import messagebox
from tkinter.scrolledtext import ScrolledText
 
def short_message():
    root.after(2000, show_message)

def show_message():
    messagebox.askyesno("System-Meldung", "Programm aktiv - Bitte warten...")
    print ("Funktion short_message ausgeführt!")
    
def count_up(counter=0):
    print(counter)
    counter += 1
    root.after(5, count_up, counter)
        
# Fenster-Titel
root = Tk(className=" Hauptfenster")
root.resizable(FALSE,FALSE)
root.attributes("-topmost", 1)
root.wm_attributes('-type', 'splash')

def center_window(width=300, height=200):
    # get screen width and height
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
 
    # calculate position x and y coordinates
    x = (screen_width/2) - (width/2)
    y = (screen_height/2) - (height/2)
    root.geometry('%dx%d+%d+%d' % (width, height, x, y))
   
center_window(800, 480)
 
# Titel Schrift ueber Programm-Fenster
zeroLabel = Label(root, text=">> Hauptfenster mit Aktivität <<", font=("TimesNewRoman", 10), bg=('#999999'))
zeroLabel.place(x=305,y=28)

short_message()
count_up()
 
root.mainloop()
Gruss wuf :wink:
Take it easy Mates!
jake-the-snake

Montag 22. Januar 2018, 10:29

Hallo wuf

Zwar nicht das Problem, an dem ich gerade herum knübel, aber dennoch interessant!
Die Messagebox müsste eigentlich die Programmausführung solange unterbrechen, bis ein Button gedrückt wurde.
Aber der Reihe nach:

Dein Programm läuft herunter bis es in der Mainloop auf "short_message()" trifft. Dann müsste es theoretisch oben in Zeile 8 weiter gehen.
Da wird 2 sec gewartet und zur Funktion "show_message" gesprungen.

Was jetzt passiert erschließt sich mir noch nicht ganz. Meine beiden Theorien:

1)
Nachdem zu "show_message" gesprungen wurde, gilt die Funktion "short_message()" als abgearbeitet und das Programm läuft unten in der Mainloop
mit der Aktion "count_up()" weiter? Das kann allerdings nicht sein, da von Anfang an gezählt wird.

2)
Startet die Mainloop etwa beide Funktionsaufrufe gleichzeitig? Also "short_message()" und "count_up()"?

Was passiert hier - Warum und wie funktioniert der Ablauf hier?

Gruss jts
Sirius3
User
Beiträge: 8112
Registriert: Sonntag 21. Oktober 2012, 17:20

Montag 22. Januar 2018, 11:06

@jake-the-snake: Du hast die Funktionsweise von `after` noch nicht verstanden. Da wird nirgends gewartet, sondern es wird ein Timer gestartet, der nach einer bestimmten Zeit ein Event auslöst. Deine Vorstellung von Springen ist bei einem GUI-Programm auch nicht ganz richtig. Denke eher an Event-Abarbeitung.
jake-the-snake

Montag 22. Januar 2018, 11:10

Hallo Sirius3

Aha, dann ist also Variante 1 meiner Erklärung richtig und kann doch so sein, da ja der timer gestartet wurde.

Gruss jts
jake-the-snake

Dienstag 23. Januar 2018, 22:49

Abend Forum

Ich poste das nun nochmal hier herein, weil es eventuell doch mit after zu tun hat:

Gegeben ist eine class App mit einem Knopf (Der heißt Verschlüsseln)

Außerdem eine Tk-Fensterloop.

Drückt man auf den Knopf, soll nicht die Verschlüsselungsroutine anlaufen, sondern nur ein Info-Fenster erscheinen (Funktion: write_precoding)
Gleichzeitig wird eine Variable (codeprocess) auf "yes" gestellt. Danach sollte das Programm wieder in den Mainloop eintreten.

In der Mainloop wird geschaut, ob die Variable codeprocess auf "yes" steht. Wenn ja soll die Funktion : write_coding ausgeführt werden.
Am Ende von write_coding muss das Label von write_precoding wieder weg, und die Variable precoding wieder auf "no" gesetzt werden.

Soweit bin ich bis jetzt gekommen:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

codeprocess = "no"

# Einleitung der App-Knoepfe / Hier gibt es jetzt nur einen zum Test
class App:
  
    def __init__(self, master):
        frame = Frame(master)
        frame.place(width=120, height=320, x=340, y=170)
        
       
        # Pre-Code-Button
        self.precoding = Button(frame, 
                         text="Verschlüsseln ►", fg="red", pady=5, padx=5,
                         command=self.write_precoding)
        self.precoding.pack()

####################
# Codierungs-Block #
####################

    def write_precoding(self):
        # Prozess-Meldung
        fourLabel = Label(root, text="Bitte warten...", font=("TimesNewRoman", 20), borderwidth=2, relief=("groove"), background=("#ff5a5a"), width=36, anchor=("center"))
        fourLabel.place(x=150,y=165)
        codeprocess = "yes"
        print("precoding Ansprung hat geklappt...")
        root.after(1000, write_coding)

    def write_coding(self):
        print("Beginn des Codierungsblock")

####################
# Mainloop - Block #
####################

# Fenster-Titel
root = Tk(className=" Fenstername Blablabla")
root.resizable(FALSE,FALSE)
root.attributes("-topmost", 1)
root.wm_attributes('-type', 'splash')

# Knoepfe-App-Ende
app = App(root)

def center_window(width=300, height=200):
    # get screen width and height
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()

    # calculate position x and y coordinates
    x = (screen_width/2) - (width/2)
    y = (screen_height/2) - (height/2)
    root.geometry('%dx%d+%d+%d' % (width, height, x, y))
    
center_window(800, 480)


if codeprocess == "yes":
    
    codeprocess = "no"
    print("if-Block mainloop erreicht") 

root.mainloop()
Das Testprogramm läuft bis Zeile 32. Aber es will die Funktion bzw. Sprungmarke "def write_coding(self):" nicht akzeptieren und behauptet die Funktion wäre nicht definiert - was sie aber doch ist? Oder ich bin blind...

Im Grunde brauche ich zwei Funktionen hintereinander, die aber nur durch einen Button-Klick stattfinden sollen. Zu allem Übel soll nach der ersten Anzeigefunktion noch mal kurz der mainloop konsultiert werden, bevor es mit der zweiten Funktion weiter geht...

Ich hol mir jetzt einen Kaffee... mein Schädel ist am Qualmen...

Gruss jts
Sirius3
User
Beiträge: 8112
Registriert: Sonntag 21. Oktober 2012, 17:20

Dienstag 23. Januar 2018, 23:21

@jake-the-snake: solange Du noch an Sprungmarken denkst, wird das nichts. »write_coding« wird nirgends definiert, weil es nur ein »write_coding« als Methode der Klasse »App« gibt. Die Variable »codeprocess« macht keinen Sinn, ebenso Zeilen 63ff; hier hast Du ereignisbasierte GUI-Programmierung immer noch nicht verstanden.

Daneben solltest Du immer noch keine *-Importe benutzen und die Kommentare mit den vielen ##### zeigen, dass Du als erstes Lernen solltest, was Funktionen sind, dann was Klassen sind, bevor Du dich weiter mit GUIs beschäftigen kannst.
jake-the-snake

Mittwoch 24. Januar 2018, 11:06

Hallo zusammen
Die Variable »codeprocess« macht keinen Sinn, ebenso Zeilen 63ff; hier hast Du ereignisbasierte GUI-Programmierung immer noch nicht verstanden.
Ja, da war ich auch noch nicht fertig. Das mit der Funktion ist jetzt klar. Die Funktion write_coding war Teil der Class App. Deshalb hat Python gemekert.
Ich habe es jetzt raus geschoben, in einen eigenen Block.

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *

global codeprocess
codeprocess = ""

# Einleitung der App-Knoepfe / Hier gibt es jetzt nur einen zum Test
class App:
  
    def __init__(self, master):
        frame = Frame(master)
        frame.place(width=120, height=320, x=340, y=170)
        
       
        # Pre-Code-Button
        self.precoding = Button(frame, 
                         text="Verschlüsseln ►", fg="red", pady=5, padx=5,
                         command=self.write_precoding)
        self.precoding.pack()

####################
# Codierungs-Block #
####################

    def write_precoding(self):
        # Prozess-Meldung
        fourLabel = Label(root, text="Bitte warten...", font=("TimesNewRoman", 20), borderwidth=2, relief=("groove"), background=("#ff5a5a"), width=36, anchor=("center"))
        fourLabel.place(x=150,y=165)
        codeprocess = "yes"
        print("precoding Ansprung hat geklappt...", codeprocess)

def write_coding(self):
    i = 0
    while i <= 10000000:
        i = i + 1
    print("Beginn des Codierungsblock")

####################
# Mainloop - Block #
####################

# Fenster-Titel
root = Tk(className=" Fenstername Blablabla")
root.resizable(FALSE,FALSE)
root.attributes("-topmost", 1)
root.wm_attributes('-type', 'splash')

# Knoepfe-App-Ende
app = App(root)

def center_window(width=300, height=200):
    # get screen width and height
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()

    # calculate position x and y coordinates
    x = (screen_width/2) - (width/2)
    y = (screen_height/2) - (height/2)
    root.geometry('%dx%d+%d+%d' % (width, height, x, y))
    
center_window(800, 480)

if codeprocess == "yes":
    print("if-Block mainloop erreicht")
    codeprocess = "no" 
    write_coding(self)

root.mainloop()
Das Programm läuft bis Zeile 32. Dort wird die Variable (jetzt global) auch umgestellt (habe mir die extra ausgeben lassen).
Eigentlich sollte es jetzt unten in der Mainloop, Zeile 65 weiter gehen. Aus irgend einem, mir noch nicht klaren Grund, tut es das aber nicht.

In einem Anderen Programm habe ich auch einen if-Block in der Mainloop, und der funktioniert. Wo ist das Problem?

Gruss jts
Antworten