Programmierdemo Python 3, Eventhandler von Canvas-Objekten (Software)
Dieses kleine Pythonprogramm zeigt, wie man diverse Mausereignisse an einzelne Objekte einer Canvasgrafik binden kann. Jedes einzelne Kreisobjekt reagiert auf Mausklicks, erkennt, wann es von der Maus berührt oder wieder verlassen wird, ob es doppelgeklickt oder rechtsgeklickt wird und kontrolliert nebenbei einige Male pro Sekunde, ob es mit einem der anderen Kreisobjekte oder dem Canvasrand kollidiert, selbst wenn dieser sich bei Größenänderungen des Fensters verschiebt. Alle Events werden protokolliert und „live“ angezeigt.
Download: ZIP-Archiv_CHC1LODZ1.zip
#!/usr/bin/python3 # # Demo für diverse Maus-Events. Auf einer tkinter-Canvas werden einige # Kreise angeordnet, die jeder für sich auf eine Reihe von Events reagieren. # Zusätzlich wird der Event abgefragt, der bei einer Größenänderung des # Programmfensters ausgelöst wird und der Inhalt der Canvas entsprechend # angepasst. # In einem zehnzeiligen Textfenster unterhalb der Canvas werden die # ausgewerteten Events protokolliert. # # Version vom 27. Dezember 2016 # Autor: Martin Vogel, martinvogel.de # Lizenz: cc-by-3.0 https://creativecommons.org/licenses/by/3.0/de/ # from tkinter import Tk, Canvas from tkinter.scrolledtext import ScrolledText from random import randint from math import hypot # Das Hauptfenster ist in der Größe veränderlich, soll aber eine Mindestgröße # von 400×400 Pixeln nicht unterschreiten. T = Tk() T.title("Bewege die Kreise mit der Maus!") T.minsize(width=400, height=400) # Die Zeichenfläche ist anfangs 600×300 Pixel groß, hat einen weißen # Hintergrund und soll keinen Rand für die Fokusanzeige reservieren. C = Canvas(T, width=600, height=300, bg="white", highlightthickness=0) # Bei Änderungen des Hauptfensters soll sich die Canvas anpassen. C.pack(expand=True, fill="both") # Das Textfenster unterhalb der Canvas ist 10 Zeilen hoch. Protokoll = ScrolledText(T, height=10) # Der Text soll immer die ganze Fensterbreite einnehmen. Protokoll.pack(fill="x") def protokolliere(s): "Hängt eine neue Zeile ans Protokoll an." # Kurioserweise lässt sich die Zeilenzahl des Textfensters nicht # einfach auslesen. Wir ermitteln sie durch Abfragen der Position # der Endemarke. zeilenzahl = int(Protokoll.index('end').split('.')[0])-1 # Ans Ende scrollen Protokoll.see("end") # Einfügen der neuen Zeile am Ende des Textfeldes. Protokoll.insert("end", "Event %i: %sn" % (zeilenzahl, s)) # Alle Kreis-Instanzen werden in dieser Liste gespeichert. Kreise = [] class Kreis: """Ein Canvas-Objekt, das auf Mausereignisse reagiert und Kollisionen mit anderen Kreisen oder den Canvasgrenzen erkennt.""" def __init__(self, x, y, farbe="red", radius=10): self.x, self.y = x, y self.radius = radius self.farbe = farbe # Der Kreis merkt sich seine Canvas-ID. self.ID = C.create_oval(0, 0, 0, 0, fill=farbe) # Lage und Größe festlegen self.aktualisiere_position() # Der Kreis reagiert auf acht verschiedene Mausereignisse: # 1. Bewegung der Maus, während der Mauszeiger den Kreis berührt C.tag_bind(self.ID, "<Motion>", self.berühren) # 2. Klicken der linken Maustaste C.tag_bind(self.ID, "<Button-1>", self.anklicken) # 3. Ziehen mit gedrückter linker Maustaste C.tag_bind(self.ID, "<B1-Motion>", self.bewegen) # 4. Loslassen der linken Maustaste C.tag_bind(self.ID, "<ButtonRelease-1>", self.loslassen) # 5. Drücken der rechten Maustaste C.tag_bind(self.ID, "<Button-3>", self.rechtsklicken) # 6. Beginn der Berührung durch den Mauszeiger C.tag_bind(self.ID, "<Enter>", self.betreten) # 7. Ende der Berührung durch den Mauszeiger C.tag_bind(self.ID, "<Leave>", self.verlassen) # 8. Doppelklick mit der linken Maustaste C.tag_bind(self.ID, "<Double-Button-1>", self.doppelklicken) # Wenn demnächst mal nichts los ist, bitte einen Test auf Kollision # mit den anderen Kreisen durchführen: C.after_idle(self.kollisionstest) def aktualisiere_position(self, event=None): """Setzt den Kreis auf seine neuen Koordinaten self.x und self.y.""" # Die Koordinaten ggf. auf die Zeichenfläche zurückholen self.randkontrolle() # Canvas-Objekt umsetzen. # Der Kreis wird durch sein umhüllendes Quadrat definiert. C.coords(self.ID, self.x-self.radius, self.y-self.radius, self.x+self.radius, self.y+self.radius) def randkontrolle(self, event=None): """Setzt die Kreiskoordinaten auf einen Punkt innerhalb der Canvas.""" # Die Canvas darf vom Kreis nicht verlassen werden. Der Mittelpunkt # muss also mindestens um Radiuslänge vom Rand entfernt sein. self.x = min(max(self.x, self.radius), C.winfo_width()-self.radius-1) self.y = min(max(self.y, self.radius), C.winfo_height()-self.radius-1) def kollisionstest(self, event=None): """Schaut nach, ob sich dieser Kreis mit irdendwelchen anderen Kreisen überschneidet und schiebt in dem Fall beide ein Stückchen auseinander. Wenn die Kreise unterschiedlich groß sind, wird der kleinere der beiden weiter geschoben als der größere.""" # Leider kann die Canvas nicht selbst Objektkollisionen erkennen. # Wir müssen uns da in mehreren Schritten annähern. # Die Canvas kann uns mit der Methode "find_overlapping" ein Tupel # aller Canvas-Objekte liefern, die ein bestimmtes Rechteck berühren # oder sich darin befinden. # Alle zu überprüfenden Kandidaten berühren zunächst das umhüllende # Rechteck "bbox" dieser Kreis-Instanz. Einige von denen berühren # vielleicht auch den Kreis selbst. bbox = C.bbox(self.ID) # Weil es in diesem Programm möglich ist, Kreise zu löschen, kann # es sein, dass diese Methode hier (kollisionstest) nach dem Löschen # von "self" noch einmal aufgerufen wird. Dann hat bbox den Wert None. if bbox: # Falls dieser Kreis nicht gerade von der Canvas gelöscht wurde, # lassen wir uns die Liste der Canvas-ID-Nummern aller # Kollisionskandidaten geben. Kandidaten = C.find_overlapping(*bbox) else: # Ansonsten können wir uns alles weitere sparen. return # Möglicherweise gibt es gar keine Kollisionen. Das sehen wir später # daran, dass die Variable "Kollision" immer noch den Wert "False" hat. Kollision = False # Wir erzeugen eine Liste mit den Instanzen aller Kollisionskandidaten, # indem wir nachschauen, welche Kreise eine Canvas-ID haben, die sich # in der Kandidatenliste befindet, wobei dieser Kreis selbst natürlich # nicht eingeschlossen sein darf, und iterieren uns anschließend da # durch. for K in [k for k in Kreise if k.ID in Kandidaten and k.ID != self.ID]: # Wie weit sind die Kreismittelpunkte in x- und y-Richtung # voneinander entfernt? dx = self.x-K.x dy = self.y-K.y # Wie nah dürfen sie sich ohne Berührung kommen? radien = self.radius+K.radius # Wie weit sind die Kreismittelpunkte tatsächlich entfernt? abstand = hypot(dx, dy) # Wie weit überlappen sie sich also? überlappung = radien-abstand # Wenn sie sich um mindestens ein halbes Pixel überlappen … if überlappung >= 0.5: # … dann haben wir eine Kollision. # Das merken wir uns für später. Kollision = True # Beide Kreise, self und K, werden nun zurückgeschoben. # Je kleiner der Radius, desto stärker die Verschiebung. überlappung_relativ = überlappung/radien eigenanteil = überlappung_relativ*(K.radius/radien) fremdanteil = überlappung_relativ*(self.radius/radien) # Die Verschiebung erfolgt getrennt nach x- und y-Wert. self.x += dx*eigenanteil self.y += dy*eigenanteil # Der andere Kreis wird entgegengesetzt verschoben. K.x -= dx*fremdanteil K.y -= dy*fremdanteil # Grafik nachführen: self.aktualisiere_position() K.aktualisiere_position() if Kollision: # Falls es eine Kollision gab, so schnell wie möglich noch einmal # testen: C.after_idle(self.kollisionstest) else: # Ansonsten erst nach 1/25s nachschauen, ob sich hier # vielleicht zwischendurch irgendwas getan hat. # Bei sehr langsamen Rechnern sollte dieser Wert erhöht werden, # falls es Darstellungsprobleme gibt. C.after(40, self.kollisionstest) def anklicken(self, event): "Event-Handler für das Klicken der linken Maustaste" # Diesen Kreis über alle anderen Canvasobjekte legen C.lift(self.ID) # Mauscursor als zupackende Hand (Mac OS, Linux) oder # Verschiebepfeilkreuz (Windows) darstellen C.config(cursor="fleur") # Zeigen, dass dieser Event ausgelöst wurde protokolliere("Kreis %i wurde bei (%i, %i) angeklickt." % ( self.ID, event.x, event.y)) def berühren(self, event): "Event-Handler für Bewegung mit der Maus (ohne Tastendruck)" # Zeigen, dass dieser Event ausgelöst wurde protokolliere("Kreis %i wird auf (%i, %i) berührt." % ( self.ID, event.x, event.y)) def bewegen(self, event): "Event-Handler für Bewegung mit der Maus mit gedrückter Maustaste" # Aktuelle Mauskoordinaten werden zum neuen Kreismittelpunkt. self.x, self.y = event.x, event.y # Canvasobjekt wird umgesetzt. self.aktualisiere_position() # Zusätzlich im Protokoll zeigen, dass dieser Event ausgelöst wurde protokolliere("Kreis %i wird zu (%i, %i) bewegt." % ( self.ID, event.x, event.y)) def loslassen(self, event): "Event-Handler für das Loslassen der linken Maustaste" # Mauscursor als Hand darstellen C.config(cursor="hand1") # Zeigen, dass dieser Event ausgelöst wurde protokolliere("Kreis %i wurde an (%i, %i) losgelassen." % ( self.ID, event.x, event.y)) def betreten(self, event): "Event-Handler für das Berühren mit dem Mauszeiger" # Mauscursor als Hand darstellen C.config(cursor="hand1") # Zeigen, dass dieser Event ausgelöst wurde protokolliere("Kreis %i wurde an (%i, %i) betreten." % ( self.ID, event.x, event.y)) def verlassen(self, event): "Event-Handler für das Verlassen durch den Mauszeiger" # Mauscursor als Pfeil darstellen C.config(cursor="arrow") # Zeigen, dass dieser Event ausgelöst wurde protokolliere("Kreis %i wurde an (%i, %i) verlassen." % ( self.ID, event.x, event.y)) def rechtsklicken(self, event): "Event-Handler für das Klicken der rechten Maustaste" # Mauscursor als Pfeil darstellen C.config(cursor="arrow") # Canvas-Objekt löschen C.delete(self.ID) # Sich selbst aus der Kreise-Liste löschen Kreise.remove(self) # Zeigen, dass dieser Event ausgelöst wurde protokolliere("Kreis %i wurde von (%i, %i) gelöscht." % ( self.ID, event.x, event.y)) def doppelklicken(self, event): "Event-Handler für Doppelklick mit der linken Maustaste" # Diesen Kreis umfärben self.farbe = zufallsfarbe() C.itemconfig(self.ID,fill=self.farbe) protokolliere("Kreis %i wurde auf (%i, %i) gedoppelklickt." % ( self.ID, event.x, event.y)) def zufallsfarbe(): "Erzeugt einen zufälligen RGB-Farbwert" return "#%02x%02x%02x" % (randint(0, 255), randint(0, 255), randint(0, 255)) def zufallspunkt(r=0): """Erzeugt zufällige Koordinaten eines Punktes auf der Canvas, ggf. mit einem Randabstand r.""" return randint(r, C.winfo_width()-r-1), randint(r, C.winfo_height()-r-1) def zufallsradius(): """Erzeugt einen zufälligen Radiuswert zwischen 4 und 24.""" return randint(4, 24) def größenänderung(event=None): """Event, der bei Änderungen der Größe oder der Position des Hauptfensters ausgelöst wird.""" # Uns interessieren dabei die Größenänderungen, weil bei Verkleinerungen # des Fensters einige Kreise in den neuen sichtbaren Bereich der Canvas # zurückgeschoben werden sollen. for K in Kreise: K.aktualisiere_position() # Wir binden den gerade definierten Eventhandler an sein auslösendes Ereignis: T.bind("<Configure>", größenänderung) # Vor der Erzeugung der Kreise müssen wir dafür sorgen, dass das Hauptfenster # aufgebaut wird, damit die Canvas Auskunft über ihre eigene Größe geben kann # um wiederum zufällige Koordinaten innerhalb ihrer Grenzen ermitteln zu # können. T.update() # Wir wollen 200 Kreise erzeugen … for i in range(200): # … die einen zufälligen Radius … r = zufallsradius() # … sowie eine zufällige Farbe haben … f = zufallsfarbe() # und irgendwo auf der Canvas liegen. x, y = zufallspunkt(r) # Jede neue Kreisinstanz wird an die Liste "Kreise" angehängt. Kreise.append(Kreis(x, y, f, r)) # … und hopp! T.mainloop()
--
Dipl.-Ing. Martin Vogel
Leiter des Bauforums
Bücher:
CAD mit BricsCAD
Bauinformatik mit Python