Programmierdemo Python 3, Eventhandler von Canvas-Objekten (Software)

Martin Vogel ⌂ @, Dortmund / Bochum, Tue, 27.12.2016, 18:04 (vor 2845 Tagen)

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.

[image]

Download: [image]ZIP-Archiv_CHC1LODZ1.zip

  1. #!/usr/bin/python3
  2.  
  3. #
  4. # Demo für diverse Maus-Events. Auf einer tkinter-Canvas werden einige
  5. # Kreise angeordnet, die jeder für sich auf eine Reihe von Events reagieren.
  6. # Zusätzlich wird der Event abgefragt, der bei einer Größenänderung des
  7. # Programmfensters ausgelöst wird und der Inhalt der Canvas entsprechend
  8. # angepasst.
  9. # In einem zehnzeiligen Textfenster unterhalb der Canvas werden die
  10. # ausgewerteten Events protokolliert.
  11. #
  12. # Version vom 27. Dezember 2016
  13. # Autor: Martin Vogel, martinvogel.de
  14. # Lizenz: cc-by-3.0 https://creativecommons.org/licenses/by/3.0/de/
  15. #
  16.  
  17. from tkinter import Tk, Canvas
  18. from tkinter.scrolledtext import ScrolledText
  19. from random import randint
  20. from math import hypot
  21.  
  22. # Das Hauptfenster ist in der Größe veränderlich, soll aber eine Mindestgröße
  23. # von 400×400 Pixeln nicht unterschreiten.
  24. T = Tk()
  25. T.title("Bewege die Kreise mit der Maus!")
  26. T.minsize(width=400, height=400)
  27.  
  28. # Die Zeichenfläche ist anfangs 600×300 Pixel groß, hat einen weißen
  29. # Hintergrund und soll keinen Rand für die Fokusanzeige reservieren.
  30. C = Canvas(T, width=600, height=300, bg="white", highlightthickness=0)
  31.  
  32. # Bei Änderungen des Hauptfensters soll sich die Canvas anpassen.
  33. C.pack(expand=True, fill="both")
  34.  
  35. # Das Textfenster unterhalb der Canvas ist 10 Zeilen hoch.
  36. Protokoll = ScrolledText(T, height=10)
  37.  
  38. # Der Text soll immer die ganze Fensterbreite einnehmen.
  39. Protokoll.pack(fill="x")
  40.  
  41. def protokolliere(s):
  42. "Hängt eine neue Zeile ans Protokoll an."
  43. # Kurioserweise lässt sich die Zeilenzahl des Textfensters nicht
  44. # einfach auslesen. Wir ermitteln sie durch Abfragen der Position
  45. # der Endemarke.
  46. zeilenzahl = int(Protokoll.index('end').split('.')[0])-1
  47.  
  48. # Ans Ende scrollen
  49. Protokoll.see("end")
  50.  
  51. # Einfügen der neuen Zeile am Ende des Textfeldes.
  52. Protokoll.insert("end", "Event %i: %sn" % (zeilenzahl, s))
  53.  
  54.  
  55. # Alle Kreis-Instanzen werden in dieser Liste gespeichert.
  56. Kreise = []
  57.  
  58.  
  59. class Kreis:
  60. """Ein Canvas-Objekt, das auf Mausereignisse reagiert und Kollisionen
  61. mit anderen Kreisen oder den Canvasgrenzen erkennt."""
  62. def __init__(self, x, y, farbe="red", radius=10):
  63. self.x, self.y = x, y
  64. self.radius = radius
  65. self.farbe = farbe
  66.  
  67. # Der Kreis merkt sich seine Canvas-ID.
  68. self.ID = C.create_oval(0, 0, 0, 0, fill=farbe)
  69.  
  70. # Lage und Größe festlegen
  71. self.aktualisiere_position()
  72.  
  73. # Der Kreis reagiert auf acht verschiedene Mausereignisse:
  74.  
  75. # 1. Bewegung der Maus, während der Mauszeiger den Kreis berührt
  76. C.tag_bind(self.ID, "<Motion>", self.berühren)
  77.  
  78. # 2. Klicken der linken Maustaste
  79. C.tag_bind(self.ID, "<Button-1>", self.anklicken)
  80.  
  81. # 3. Ziehen mit gedrückter linker Maustaste
  82. C.tag_bind(self.ID, "<B1-Motion>", self.bewegen)
  83.  
  84. # 4. Loslassen der linken Maustaste
  85. C.tag_bind(self.ID, "<ButtonRelease-1>", self.loslassen)
  86.  
  87. # 5. Drücken der rechten Maustaste
  88. C.tag_bind(self.ID, "<Button-3>", self.rechtsklicken)
  89.  
  90. # 6. Beginn der Berührung durch den Mauszeiger
  91. C.tag_bind(self.ID, "<Enter>", self.betreten)
  92.  
  93. # 7. Ende der Berührung durch den Mauszeiger
  94. C.tag_bind(self.ID, "<Leave>", self.verlassen)
  95.  
  96. # 8. Doppelklick mit der linken Maustaste
  97. C.tag_bind(self.ID, "<Double-Button-1>", self.doppelklicken)
  98.  
  99. # Wenn demnächst mal nichts los ist, bitte einen Test auf Kollision
  100. # mit den anderen Kreisen durchführen:
  101. C.after_idle(self.kollisionstest)
  102.  
  103. def aktualisiere_position(self, event=None):
  104. """Setzt den Kreis auf seine neuen Koordinaten self.x und self.y."""
  105. # Die Koordinaten ggf. auf die Zeichenfläche zurückholen
  106. self.randkontrolle()
  107.  
  108. # Canvas-Objekt umsetzen.
  109. # Der Kreis wird durch sein umhüllendes Quadrat definiert.
  110. C.coords(self.ID,
  111. self.x-self.radius, self.y-self.radius,
  112. self.x+self.radius, self.y+self.radius)
  113.  
  114. def randkontrolle(self, event=None):
  115. """Setzt die Kreiskoordinaten auf einen Punkt innerhalb der Canvas."""
  116. # Die Canvas darf vom Kreis nicht verlassen werden. Der Mittelpunkt
  117. # muss also mindestens um Radiuslänge vom Rand entfernt sein.
  118. self.x = min(max(self.x, self.radius), C.winfo_width()-self.radius-1)
  119. self.y = min(max(self.y, self.radius), C.winfo_height()-self.radius-1)
  120.  
  121. def kollisionstest(self, event=None):
  122. """Schaut nach, ob sich dieser Kreis mit irdendwelchen anderen Kreisen
  123. überschneidet und schiebt in dem Fall beide ein Stückchen
  124. auseinander. Wenn die Kreise unterschiedlich groß sind, wird der
  125. kleinere der beiden weiter geschoben als der größere."""
  126. # Leider kann die Canvas nicht selbst Objektkollisionen erkennen.
  127. # Wir müssen uns da in mehreren Schritten annähern.
  128. # Die Canvas kann uns mit der Methode "find_overlapping" ein Tupel
  129. # aller Canvas-Objekte liefern, die ein bestimmtes Rechteck berühren
  130. # oder sich darin befinden.
  131. # Alle zu überprüfenden Kandidaten berühren zunächst das umhüllende
  132. # Rechteck "bbox" dieser Kreis-Instanz. Einige von denen berühren
  133. # vielleicht auch den Kreis selbst.
  134. bbox = C.bbox(self.ID)
  135.  
  136. # Weil es in diesem Programm möglich ist, Kreise zu löschen, kann
  137. # es sein, dass diese Methode hier (kollisionstest) nach dem Löschen
  138. # von "self" noch einmal aufgerufen wird. Dann hat bbox den Wert None.
  139. if bbox:
  140. # Falls dieser Kreis nicht gerade von der Canvas gelöscht wurde,
  141. # lassen wir uns die Liste der Canvas-ID-Nummern aller
  142. # Kollisionskandidaten geben.
  143. Kandidaten = C.find_overlapping(*bbox)
  144. else:
  145. # Ansonsten können wir uns alles weitere sparen.
  146. return
  147.  
  148. # Möglicherweise gibt es gar keine Kollisionen. Das sehen wir später
  149. # daran, dass die Variable "Kollision" immer noch den Wert "False" hat.
  150. Kollision = False
  151.  
  152. # Wir erzeugen eine Liste mit den Instanzen aller Kollisionskandidaten,
  153. # indem wir nachschauen, welche Kreise eine Canvas-ID haben, die sich
  154. # in der Kandidatenliste befindet, wobei dieser Kreis selbst natürlich
  155. # nicht eingeschlossen sein darf, und iterieren uns anschließend da
  156. # durch.
  157. for K in [k for k in Kreise
  158. if k.ID in Kandidaten and k.ID != self.ID]:
  159. # Wie weit sind die Kreismittelpunkte in x- und y-Richtung
  160. # voneinander entfernt?
  161. dx = self.x-K.x
  162. dy = self.y-K.y
  163.  
  164. # Wie nah dürfen sie sich ohne Berührung kommen?
  165. radien = self.radius+K.radius
  166.  
  167. # Wie weit sind die Kreismittelpunkte tatsächlich entfernt?
  168. abstand = hypot(dx, dy)
  169.  
  170. # Wie weit überlappen sie sich also?
  171. überlappung = radien-abstand
  172.  
  173. # Wenn sie sich um mindestens ein halbes Pixel überlappen …
  174. if überlappung >= 0.5:
  175. # … dann haben wir eine Kollision.
  176. # Das merken wir uns für später.
  177. Kollision = True
  178.  
  179. # Beide Kreise, self und K, werden nun zurückgeschoben.
  180. # Je kleiner der Radius, desto stärker die Verschiebung.
  181. überlappung_relativ = überlappung/radien
  182. eigenanteil = überlappung_relativ*(K.radius/radien)
  183. fremdanteil = überlappung_relativ*(self.radius/radien)
  184.  
  185. # Die Verschiebung erfolgt getrennt nach x- und y-Wert.
  186. self.x += dx*eigenanteil
  187. self.y += dy*eigenanteil
  188.  
  189. # Der andere Kreis wird entgegengesetzt verschoben.
  190. K.x -= dx*fremdanteil
  191. K.y -= dy*fremdanteil
  192.  
  193. # Grafik nachführen:
  194. self.aktualisiere_position()
  195. K.aktualisiere_position()
  196.  
  197. if Kollision:
  198. # Falls es eine Kollision gab, so schnell wie möglich noch einmal
  199. # testen:
  200. C.after_idle(self.kollisionstest)
  201. else:
  202. # Ansonsten erst nach 1/25s nachschauen, ob sich hier
  203. # vielleicht zwischendurch irgendwas getan hat.
  204. # Bei sehr langsamen Rechnern sollte dieser Wert erhöht werden,
  205. # falls es Darstellungsprobleme gibt.
  206. C.after(40, self.kollisionstest)
  207.  
  208. def anklicken(self, event):
  209. "Event-Handler für das Klicken der linken Maustaste"
  210. # Diesen Kreis über alle anderen Canvasobjekte legen
  211. C.lift(self.ID)
  212.  
  213. # Mauscursor als zupackende Hand (Mac OS, Linux) oder
  214. # Verschiebepfeilkreuz (Windows) darstellen
  215. C.config(cursor="fleur")
  216.  
  217. # Zeigen, dass dieser Event ausgelöst wurde
  218. protokolliere("Kreis %i wurde bei (%i, %i) angeklickt." % (
  219. self.ID, event.x, event.y))
  220.  
  221. def berühren(self, event):
  222. "Event-Handler für Bewegung mit der Maus (ohne Tastendruck)"
  223. # Zeigen, dass dieser Event ausgelöst wurde
  224. protokolliere("Kreis %i wird auf (%i, %i) berührt." % (
  225. self.ID, event.x, event.y))
  226.  
  227. def bewegen(self, event):
  228. "Event-Handler für Bewegung mit der Maus mit gedrückter Maustaste"
  229. # Aktuelle Mauskoordinaten werden zum neuen Kreismittelpunkt.
  230. self.x, self.y = event.x, event.y
  231.  
  232. # Canvasobjekt wird umgesetzt.
  233. self.aktualisiere_position()
  234.  
  235. # Zusätzlich im Protokoll zeigen, dass dieser Event ausgelöst wurde
  236. protokolliere("Kreis %i wird zu (%i, %i) bewegt." % (
  237. self.ID, event.x, event.y))
  238.  
  239. def loslassen(self, event):
  240. "Event-Handler für das Loslassen der linken Maustaste"
  241. # Mauscursor als Hand darstellen
  242. C.config(cursor="hand1")
  243.  
  244. # Zeigen, dass dieser Event ausgelöst wurde
  245. protokolliere("Kreis %i wurde an (%i, %i) losgelassen." % (
  246. self.ID, event.x, event.y))
  247.  
  248. def betreten(self, event):
  249. "Event-Handler für das Berühren mit dem Mauszeiger"
  250. # Mauscursor als Hand darstellen
  251. C.config(cursor="hand1")
  252.  
  253. # Zeigen, dass dieser Event ausgelöst wurde
  254. protokolliere("Kreis %i wurde an (%i, %i) betreten." % (
  255. self.ID, event.x, event.y))
  256.  
  257. def verlassen(self, event):
  258. "Event-Handler für das Verlassen durch den Mauszeiger"
  259. # Mauscursor als Pfeil darstellen
  260. C.config(cursor="arrow")
  261.  
  262. # Zeigen, dass dieser Event ausgelöst wurde
  263. protokolliere("Kreis %i wurde an (%i, %i) verlassen." % (
  264. self.ID, event.x, event.y))
  265.  
  266. def rechtsklicken(self, event):
  267. "Event-Handler für das Klicken der rechten Maustaste"
  268. # Mauscursor als Pfeil darstellen
  269. C.config(cursor="arrow")
  270.  
  271. # Canvas-Objekt löschen
  272. C.delete(self.ID)
  273.  
  274. # Sich selbst aus der Kreise-Liste löschen
  275. Kreise.remove(self)
  276.  
  277. # Zeigen, dass dieser Event ausgelöst wurde
  278. protokolliere("Kreis %i wurde von (%i, %i) gelöscht." % (
  279. self.ID, event.x, event.y))
  280.  
  281. def doppelklicken(self, event):
  282. "Event-Handler für Doppelklick mit der linken Maustaste"
  283. # Diesen Kreis umfärben
  284. self.farbe = zufallsfarbe()
  285. C.itemconfig(self.ID,fill=self.farbe)
  286. protokolliere("Kreis %i wurde auf (%i, %i) gedoppelklickt." % (
  287. self.ID, event.x, event.y))
  288.  
  289.  
  290. def zufallsfarbe():
  291. "Erzeugt einen zufälligen RGB-Farbwert"
  292. return "#%02x%02x%02x" % (randint(0, 255),
  293. randint(0, 255),
  294. randint(0, 255))
  295.  
  296.  
  297. def zufallspunkt(r=0):
  298. """Erzeugt zufällige Koordinaten eines Punktes auf der Canvas, ggf. mit
  299. einem Randabstand r."""
  300. return randint(r, C.winfo_width()-r-1), randint(r, C.winfo_height()-r-1)
  301.  
  302.  
  303. def zufallsradius():
  304. """Erzeugt einen zufälligen Radiuswert zwischen 4 und 24."""
  305. return randint(4, 24)
  306.  
  307.  
  308. def größenänderung(event=None):
  309. """Event, der bei Änderungen der Größe oder der Position des Hauptfensters
  310. ausgelöst wird."""
  311. # Uns interessieren dabei die Größenänderungen, weil bei Verkleinerungen
  312. # des Fensters einige Kreise in den neuen sichtbaren Bereich der Canvas
  313. # zurückgeschoben werden sollen.
  314. for K in Kreise:
  315. K.aktualisiere_position()
  316.  
  317. # Wir binden den gerade definierten Eventhandler an sein auslösendes Ereignis:
  318. T.bind("<Configure>", größenänderung)
  319.  
  320. # Vor der Erzeugung der Kreise müssen wir dafür sorgen, dass das Hauptfenster
  321. # aufgebaut wird, damit die Canvas Auskunft über ihre eigene Größe geben kann
  322. # um wiederum zufällige Koordinaten innerhalb ihrer Grenzen ermitteln zu
  323. # können.
  324. T.update()
  325.  
  326. # Wir wollen 200 Kreise erzeugen …
  327. for i in range(200):
  328. # … die einen zufälligen Radius …
  329. r = zufallsradius()
  330.  
  331. # … sowie eine zufällige Farbe haben …
  332. f = zufallsfarbe()
  333.  
  334. # und irgendwo auf der Canvas liegen.
  335. x, y = zufallspunkt(r)
  336.  
  337. # Jede neue Kreisinstanz wird an die Liste "Kreise" angehängt.
  338. Kreise.append(Kreis(x, y, f, r))
  339.  
  340. # … und hopp!
  341. T.mainloop()

--
Dipl.-Ing. Martin Vogel
Leiter des Bauforums

Bücher:
CAD mit BricsCAD
Bauinformatik mit Python

Tags:
Python, Events, Grafik

RSS-Feed dieser Diskussion
powered by my little forum