Fenster erscheint erst nach Ende des Programms

Fragen zu Tkinter.
Antworten
Markus74
User
Beiträge: 6
Registriert: Freitag 22. März 2019, 20:39

Hallo zusammen!

Ich habe völlig neu mit Python begonnen, bin also blutiger Anfänger. Ich hoffe, dass ich hier Hilfe bekomme und bedanke mich schon im Voraus für jegliche Unterstützung.

Mein Problem:
Ich habe folgenden (nicht sehr anspruchsvollen) Code aus einem Buch abgeschrieben:

Code: Alles auswählen

from random import *
from tkinter import *
from time import sleep
size = 500
window = Tk()
canvas = Canvas(window, width=size, height=size)
canvas.pack()

for i in range(1000):
   col = choice(['pink', 'orange', 'purple', 'yellow', 'black', 'green'])
   x0 = randint(0, size)
   y0 = randint(0, size)
   d = randint(0, size/5)
   canvas.create_oval(x0, y0, x0+d, y0+d, fill = col)
   window.update
   sleep(0.01)
Wenn ich das ausführe, öffnet sich das Fenster mit den Kreisen erst, wenn alle 1000 gezeichnet sind. Ich möchte aber, dass ich beim Zeichnen der Kreise zusehen kann. Schließlich will ich ja zukünftig z.B. bei Spielen auch nicht erst das Fenster sehen, wenn alles vorbei ist.
Kann mir bitte jemand sagen, was ich anders machen muss?

Danke, Markus

---------------------------------------------
Windows 10
Python 3.7.1
IDLE
---------------------------
Windows 10
IDLE
Python 3.7.2
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi Markus74

Hier eine vom Buch abweichende Variante:

Code: Alles auswählen

from random import *
import tkinter as tk
from time import sleep

APP_TITLE = "Paul Signac"

APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 350
APP_HEIGHT = 200


SIZE = 500
NUM_OF_POINTS = 1000

class Application(object):

    def __init__(self, main_win):
        self.main_win = main_win
        
        self.build()
        self.painter()

    def build(self):
        self.main_frame = tk.Frame(self.main_win)
        self.main_frame.pack(fill='both', expand=True)
        
        self.canvas = tk.Canvas(self.main_frame, width=SIZE, height=SIZE)
        self.canvas.pack()
    
    def painter(self, steps=NUM_OF_POINTS):
        col = choice(['pink', 'orange', 'purple', 'yellow', 'black', 'green'])
        x0 = randint(0, SIZE)
        y0 = randint(0, SIZE)
        d = randint(0, SIZE/5)
        self.canvas.create_oval(x0, y0, x0+d, y0+d, fill = col)
        steps -= 1
        if steps == 0: return
        
        self.main_win.after(10, self.painter, steps)

            
def main():
    main_win = tk.Tk()
    main_win.title(APP_TITLE)
    #main_win.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
    #main_win.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
    
    app = Application(main_win)
    
    main_win.mainloop()
 
 
if __name__ == '__main__':
    main()      
Viel Spass mit Python & Tkinter

Gruss wuf :-)
Take it easy Mates!
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Markus74: Das Buch scheint einige nicht so gute Praktiken zu vermitteln. So ganz grundsätzlich funktioniert GUI-Programmierung nicht so wie das da versucht wird. *Du* hast bei GUI-Programmierung nicht die Kontrolle über einen linearen Programmablauf, sondern die GUI-Hauptschleife hat das Heft in der Hand. Du registrierst für bestimmte Ereignisse, zum Beispiel das der Benutzer auf eine Schaltfläche klickt, Rückruffunktionen/-methoden die beim Eintreten des jeweiligen Ereignisses von der GUI-Hauptschleife aufgerufen werden *kurz* darauf reagieren, und dann die Kontrolle wieder an die GUI-Hauptschleife zurück geben. Ein `time.sleep()` hat da nichts verloren.

Ein mögliches Ereignis ist das eine gewisse Zeit verstrichen ist. Eine Rückruffunktion dafür kann man mit der `after()`-Methode registrieren, wie das Programm von wuf zeigt.

Weitere Anmerkungen: Sternchen-Importe sind Böse™. Damit holt man sich im Fall von `tkinter` Unmengen von Namen ins Modul von denen nur ein Bruchteil verwendet wird. Man kann schwerer nachvollziehen woher welcher Name kommt, und es besteht die Gefahr von Namenskollisionen.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Das Dir überhaupt das Ergebnis angezeigt wird, liegt daran, dass Du das aus IDLE heraus ausführst. Ohne diese IDE muss man bei tkinter-Programmen auch dafür sorgen, dass die GUI-Hauptschleife aufgerufen wird.

Namen sollte man nicht abkürzen. `x` und `y` für Koordinaten sind einige der wenigen Ausnahmen wo einbuchstabige Namen tatsächlich aussagekräftig sind. Aber `col` statt `color` und `d` statt `diameter` sind weniger gute Namen.

Mein Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
from random import choice, randint
import tkinter as tk

SIZE = 500
COLORS = ['black', 'green', 'orange', 'pink', 'purple', 'yellow']
CIRCLE_COUNT = 1000
DELAY = 10  # in ms.


def draw_circles(canvas, count):
    if count > 0:
        x = randint(0, SIZE)
        y = randint(0, SIZE)
        diameter = randint(0, SIZE // 5)
        canvas.create_oval(
            x, y, x + diameter, y + diameter, fill=choice(COLORS)
        )
        canvas.after(DELAY, draw_circles, canvas, count - 1)


def main():
    root = tk.Tk()
    canvas = tk.Canvas(root, width=SIZE, height=SIZE)
    canvas.pack()
    draw_circles(canvas, CIRCLE_COUNT)
    root.mainloop()


if __name__ == '__main__':
    main()
Das kommt noch ganz gut ohne Klasse aus, aber grundsätzlich solltest Du damit rechnen das jedes nicht-triviale GUI-Programm objektorientiert geschrieben wird, damit man nicht den Überblick verliert.

Technisch finde ich an dem Programm nicht so schön das die Anzeige etwas ”unsymmetrisch” ist – es gibt keinen Kreis der links oder oben aus dem Canvas heraus ragt, aber ganz viele die das rechts oder unten tun. Schöner fände ich ja wenn man den Mittelpunkt der Kreise über die ganze Canvasfläche verteilt zufällig wählen würde und nicht die obere linke Ecke der „bounding box“ der Kreise.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Markus74
User
Beiträge: 6
Registriert: Freitag 22. März 2019, 20:39

@wuf

Danke für die Antwort! Zugegeben tu ich mir schwer zu verstehen, was Du da genau geschrieben hast, mit Klassendefinitionen schwimm ich völlig dahin. Es scheint aber, wie auch blackjack schreibt, dass ich mich damit auseinander setzen muss, wenn ich auf Dauer weiter kommen will.
Gibt es da (am besten online) ein Tutorial, das für Anfänger verständlich ist und Du empfehlen kannst?

LG, Markus
---------------------------
Windows 10
IDLE
Python 3.7.2
Markus74
User
Beiträge: 6
Registriert: Freitag 22. März 2019, 20:39

Hallo blackjack!
Erstmal danke für die sehr ausführliche Antwort.
__blackjack__ hat geschrieben: Samstag 23. März 2019, 11:11 @Markus74: Das Buch scheint einige nicht so gute Praktiken zu vermitteln.
Das ist nicht so toll, als Anfänger verlässt man sich natürlich drauf, dass die abgedruckten Codes "gut" sind und vor allem auch funktionieren :?
So ganz grundsätzlich funktioniert GUI-Programmierung nicht so wie das da versucht wird.
Offensichtlich muss ich mich also mit Klassen und Objektorientierung auseinandersetzen, bis jetzt versteh ich da gar nichts. Hast Du einen Tipp für ein brauchbares Tutorial, am besten online?
Weitere Anmerkungen: Sternchen-Importe sind Böse™. Damit holt man sich im Fall von `tkinter` Unmengen von Namen ins Modul von denen nur ein Bruchteil verwendet wird. Man kann schwerer nachvollziehen woher welcher Name kommt, und es besteht die Gefahr von Namenskollisionen.
Versteh ich das richtig, dass man also gezielt die Funktionen importiert, die man auch tatsächlich benötigt?
Das Dir überhaupt das Ergebnis angezeigt wird, liegt daran, dass Du das aus IDLE heraus ausführst. Ohne diese IDE muss man bei tkinter-Programmen auch dafür sorgen, dass die GUI-Hauptschleife aufgerufen wird.
Ist IDLE also eher nicht empfehlenswert?
def draw_circles(canvas, count):
if count > 0:
x = randint(0, SIZE)
y = randint(0, SIZE)
diameter = randint(0, SIZE // 5)
canvas.create_oval(
x, y, x + diameter, y + diameter, fill=choice(COLORS)
)
canvas.after(DELAY, draw_circles, canvas, count - 1)
Bis auf die after-Anweisung ist mir das klar. Diese wartet DELAY ms und ruft dann wieder die Funktion draw_circles auf, wobei sie count um 1 reduziert. Hab ich das richtig verstanden?
root.mainloop()
Diese Anweisung hab ich jetzt schon ein paar Mal in Programmen gesehen, aber was macht die genau?

if __name__ == '__main__':
main()
Dieser kleine Teil ist das eigentliche Hauptprogramm, oder? Ich versteh die Bedingung der if-Anweisung leider nicht, was prüft die genau?
Technisch finde ich an dem Programm nicht so schön das die Anzeige etwas ”unsymmetrisch” ist – es gibt keinen Kreis der links oder oben aus dem Canvas heraus ragt, aber ganz viele die das rechts oder unten tun. Schöner fände ich ja wenn man den Mittelpunkt der Kreise über die ganze Canvasfläche verteilt zufällig wählen würde und nicht die obere linke Ecke der „bounding box“ der Kreise.
Da geb ich Dir recht :wink: Mit den ästhetischen Problemen wart ich aber, bis ich das Handwerkzeug beherrsche :D

Danke jedenfalls für die Erklärungen, Markus
---------------------------
Windows 10
IDLE
Python 3.7.2
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

IDLE ist nicht gut, aber auch nicht schlecht. Der Kommentar bezog sich darauf, dass die Hauptschleife schon durch IDLE selbst läuft, weil man in ein und demselben Python Interpreter den Code laufen lässt. Das hat halt Seiteneffekte, einer davon, dass man nicht merkt, wenn man den mainloop nicht selbst betreibt.

Und die Anweisung “root.mainloop()” sagt “ab hier die Kontrolle an das UI Rahmenwerk geben”. Erst ab dann können Ereignisse abgearbeitet werden.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Markus74: Tipp für ein OOP-Tutorial für Python habe ich leider nicht. Das Tutorial in der Python-Dokumentation geht da kaum weiter als oberflächlich ein bisschen von der Syntax für Klassen und den Sichtbarkeitsregeln von Namen zu erklären.

Entweder man importiert nur das Modul, möglicherweise mit einer Umbenennung, oder man importiert direkt die Objekte aus einem Modul die man benötigt. Ersteres habe ich mit `tkinter` in dem Programm gemacht, letzteres mit den beiden Funktionen aus dem `random`-Modul.

IDLE ist als erste IDE die direkt bei Python dabei ist nicht schlecht. Grundsätzlich würde ich aber auch bei jeder anderen IDE empfehlen Programme ausserhalb der IDE zu starten, denn auf die eine oder andere Weise verändert jede IDE die Umgebung in der ein Programm abläuft. Und man will ja üblicherweise wissen ob/wie das Programm ohne IDE läuft.

Also nach Deiner Beschreibung hast Du die `after()`-Methode doch verstanden. :-)

Die Bedingung am Ende prüft ob dieses Modul als Programm aufgerufen wurde, denn nur dann hat `__name__` den Wert '__main__'. Wenn man das Modul hingegen importiert, hat `__name__` den Namen des Moduls als Wert.

Auf diese Weise kann man das Modul importieren, ohne dass die Hauptfunkion ausgeführt wird. Das ist praktisch um beispielsweise einzelne Funktionen in einer interaktiven Python-Shell ausprobieren zu können, zur Fehlersuche. Oder automatisierte Tests zu schreiben. Und es gibt auch noch ein paar andere Werkzeuge, zum Beispiel zur Dokumentationserstellung, die erwarten das man Module ohne Seiteneffekte importieren kann.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Tholo
User
Beiträge: 177
Registriert: Sonntag 7. Januar 2018, 20:36

@BlackJack
Hast du dafür schon Makros erstellt? Ich hab ein Dejavu...
__blackjack__ hat geschrieben: Samstag 23. März 2019, 11:11 Ein `time.sleep()` hat da nichts verloren.

Ein mögliches Ereignis ist das eine gewisse Zeit verstrichen ist. Eine Rückruffunktion dafür kann man mit der `after()`-Methode registrieren, wie das Programm von wuf zeigt.

Weitere Anmerkungen: Sternchen-Importe sind Böse™. Damit holt man sich im Fall von `tkinter` Unmengen von Namen ins Modul von denen nur ein Bruchteil verwendet wird. Man kann schwerer nachvollziehen woher welcher Name kommt, und es besteht die Gefahr von Namenskollisionen.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.
@Markus74
Schau mal ob in dem Buch nicht vielleicht OOP noch erklärt wird. Ansonsten gibt es auch viele Tutorials auf Youtube oder Udemy (auch in deutscher Sprache)
Markus74
User
Beiträge: 6
Registriert: Freitag 22. März 2019, 20:39

Vielen Dank nochmal an alle, die hier ihre Inputs geliefert haben. Ich habe beschlossen mich mit entsprechender Literatur einzudecken und OOP "sauber" zu lernen, sonst wird das nix. Ich hoffe, ich darf mich bei Unklarheiten in Zukunft wieder an die Community wenden!

LG, Markus
---------------------------
Windows 10
IDLE
Python 3.7.2
Antworten