Ha a tkinter modul Canvas példányán meghívott create_oval(), create_arc() és create_rectangle() metódusokkal létrehozott ellipszist, ellipszisívet és téglalapot egy adott forgáspont körül forgatjuk, akkor azt tapasztaljuk, hogy azok nem a vártnak megfelelően, vagyis nem alaktartó módon forognak el, hanem torzulnak, és az ellipszis tengelyei, valamint a téglalap oldalai továbbra is a vízszintes és függőleges koordináta-tengelyekkel lesznek párhuzamosak. Ennek oka, hogy az említett síkalakzatokat a befoglaló téglalap két pontjával, az ellentétes sarokpontokkal lehet és kell meghatározni a létrehozó metódusokban. Forgatáskor ezek a sarokpontok fordulnak, és az új két pont egy új befoglaló téglalapot határoz meg, amelyben az adott síkalakzat létrejön.
Ez a működésmód azonban korlátozza a vászon elemen létrehozható grafikákat. Hogyan lehetne ezt a korlátot feloldani?
Egy lehetséges megoldás, hogy a Canvas create_polygon() metódusával létrehozható sokszöggel valósítjuk meg a téglalapot. Az alaktartóan forgatható ellipszist, valamint az ellipszisív alapjául szolgáló ellipszist pedig megfelelően nagy számú csúcsponttal rendelkező sokszöggel közelítjük. /A megfelelően nagy szám itt azt jelenti, hogy a kirajzoláskor már nem látszik, hogy valójában sokszög jelenik meg./
Ehhez a Canvas osztáy egy altípusát definiáljuk CustomCanvas néven. Ennek egy lehetséges megvalósítását láthatjuk alább. Ez kiterjeszti a Canvas képességeit a create_rotatable_rectangle(), create_rotatable_ellipse() és create_rotatable_arc() nyilvános metódusokkal, amelyek alaktartóan forgatható téglalapot, ellipszist és ellipszisívet jelenítenek meg sokszögekkel megvalósítva. Ezeket használja a create_rotatable() metódus is, amelynek egy str típusú argumentummal lehet megadni, hogy melyik alakzatot akarjuk előállíttatni. Ezeken felül az osztályban definiált még a rotate() nyilvános metódus, amely egy adott azonosítójú alakzatot megadott szöggel, megadott forgáspont körül forgat el.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# modul: custom_canvas.py import tkinter as tk from itertools import chain from math import cos, sin, radians, tau import sys assert sys.version_info[:2] >= (3, 10) # A futtatáshoz Python 3.10+ szükséges. class CustomCanvas(tk.Canvas): """A create_rotatable_ellipse(), create_rotatable_arc() és create_rotatable_rectangle() metódusokkal olyan ellipszist, ellipszisívet és téglalapot lehet létrehozni, amelyeket alaktartóan lehet forgatni a rotate() metódussal. A rotate() metódust más érvényes Canvas rajzelemre alkalmazva az azokat definiáló pontokat forgatja, és ezért az alaktartás nem minden esetben biztosított. """ def __init__(self, master, **options): super().__init__(master, **options) @staticmethod def _get_cornercoords_and_centerpoint(*coords) -> tuple: """Argumentumként két pont koordinátáit lehet megadni. Ezt vagy négy értékkel, x1, y1, x2, y2 sorrendben, vagy két kételemű sorozat típusú konténerrel /pl. (x1, y1), (x2, y2)/ lehet megadni. Visszatérési értéke egy tuple, amelyben a négy koordináta értéket kapjuk meg x1, y1, x2, y2 sorrendben, valamint az ezek által meghatározott téglalap középpontjának koordinátáit egy kételemű tuple-ban. """ match coords: case [int() | float(), int() | float(), int() | float(), int() | float()]: x1, y1, x2, y2 = coords case [[int() | float(), int() | float()], [int() | float(), int() | float()]]: x1, y1, x2, y2 = chain.from_iterable(coords) case _: raise ValueError('Érvénytelen koordinátamegadás.') center_point: tuple = ((x2 + x1) / 2, (y2 + y1) / 2) return x1, y1, x2, y2, center_point def create_rotatable_rectangle(self, *coords, **options): """Két, szemközti sarokpont koordinátáival meghatározott téglalapot jelenít meg. A téglalapot sokszög valósítja meg. A téglalap standard konfigurációs paramétereit kulcsszavas argumentumokkal lehet megadni. """ x1, y1, x2, y2, center_point = self._get_cornercoords_and_centerpoint(*coords) cpx, cpy = center_point a, b = x2 - x1, y2 - y1 return self.create_polygon([(cpx - a / 2, cpy - b / 2), (cpx + a / 2, cpy - b / 2), (cpx + a / 2, cpy + b / 2), (cpx - a / 2, cpy + b / 2)], **options) def create_rotatable_ellipse(self, *coords, **options): """Két, szemközti sarokpont koordinátáival meghatározott befoglaló téglalappal rendelkező ellipszist jelenít meg. Az ellipszis elegendően nagy számú csúcspponttal rendelkező sokszöggel van közelítve. Az ellipszis standard konfigurációs paramétereit kulcsszavas argumentumokkal lehet megadni. """ x1, y1, x2, y2, center_point = self._get_cornercoords_and_centerpoint(*coords) cpx, cpy = center_point # Az ellipszis fél nagy-, és fél kistengelyének hossza. a, b = (x2 - x1) / 2, (y2 - y1) / 2 n = 300 # A teljes ellipszist képviselő pontok száma. dfi = tau / n # dfi a szögfelbontás; tau = 2*pi. return self.create_polygon([(a * cos(i * dfi) + cpx, b * sin(i * dfi) + cpy) for i in range(n)], **options) def create_rotatable_arc(self, *coords, **options): """Két, szemközti sarokpont koordinátáival meghatározott befoglaló téglalappal rendelkező ellipszisívet jelenít meg. Az alapul szolgáló ellipszis elegendően nagy számú csúcspponttal rendelkező sokszöggel van közelítve. Az ellipszisív standard konfigurációs paramétereit kulcsszavas argumentumokkal lehet megadni. Tehát pl. a kezdőszöget és szögtartományt a start és extent paraméternevekkel. """ x1, y1, x2, y2, center_point = self._get_cornercoords_and_centerpoint(*coords) cpx, cpy = center_point # A style paraméter aktuális értékének kikérése. Az alapértelmezett értéke 'pieslice'. arc_style = options.get('style', 'pieslice') # Ha a style 'pieslice', akkor az alapul szolgáló ellipszis középpontját is fel kell venni a rajzelem pontjai közé. extrapoint = [(cpx, cpy)] if arc_style == 'pieslice' else [] # Az ellipszis fél nagy-, és fél kistengelyének hossza. a, b = (x2 - x1) / 2, (y2 - y1) / 2 n = 300 # A teljes ellipszist képviselő pontok száma. start_angle = radians(options.get('start', 0)) # A start alapértelmezett értéke 0 fok. extent = radians(options.get('extent', 90)) # Az extent alapértelmezett értéke 90 fok. k = int(extent / tau * n) # Ennyi pont lesz az extent szögtartományon belül; tau = 2*pi. dfi = extent / k # dfi a szögfelbontás. # Mivel egyenes szakaszokkal vagy zárt sokszöggel valósítjuk meg az elipszisívet, ezért mielőtt # annak átadnánk a konfigurációs argumentumokat, azok közül az ellipszisív specifikus paramétereit ki kell venni. config_params = {k: v for k, v in options.items() if k not in {'start', 'extent', 'style'}} # Az ellipszisív style paraméterének értékétől függően vagy sokszöggel vagy egyenes szakaszokkal közelítjük az # ellipszisívet az alapul szolgáló ellipszis egyenletéből az adott ívszakaszra számolt pontok előállításával. if arc_style in {'pieslice', 'chord'}: return self.create_polygon([(a * cos(i * dfi + start_angle) + cpx, b * sin(-(i * dfi + start_angle)) + cpy) for i in range(k)] + extrapoint, **config_params) if arc_style == 'arc': return self.create_line([(a * cos(i * dfi + start_angle) + cpx, b * sin(-(i * dfi + start_angle)) + cpy) for i in range(k)], **config_params) def create_rotatable(self, item_type: str, *coords, **options): """Alaktartóan forgatható téglalapot, ellipszist vagy ellipszisívet jelenít meg ha az item_type értéke 'rectangle', 'ellipse' vagy 'arc'. Ezt követően a befoglaló téglalapot kell definiálni megadva a két, szemközti sarokpont koordinátáit. Az alakzatok standard konfigurációs paramétereit kulcsszavas argumentumokkal lehet megadni. """ item_types = ('rectangle', 'ellipse', 'arc') if item_type not in item_types: raise ValueError(f'Az item_type csak {str(item_types).strip("()")} lehet.') rotatables = dict(zip(item_types, (self.create_rotatable_rectangle, self.create_rotatable_ellipse, self.create_rotatable_arc))) return rotatables.get(item_type)(*coords, **options) def rotate(self, tag_or_id: int | str, angle_of_rotation: 'degree', center_of_rotation: tuple): """A tag_or_id argumentummal azonosított rajzelemet vagy rajzelemeket angle_of_rotation fokban mért szöggel forgatja el a center_of_rotation forgáspont körül. """ for oid in self.find_withtag(tag_or_id): xypoint_iterator = iter(self.coords(oid)) # Az aktuális rajzelem koordinátáit szolgáltató iterátor. corx, cory = center_of_rotation t = -radians(angle_of_rotation) # Az aktuális rajzelem pontjainak forgatás utáni koordinátáinak előállítása. rotated_points = (((x - corx) * cos(t) - (y - cory) * sin(t) + corx, (x - corx) * sin(t) + (y - cory) * cos(t) + cory) for x, y in zip(xypoint_iterator, xypoint_iterator)) # Az aktuális rajzelem kirajzolása a forgatott pontok koordinátái szerint. self.coords(oid, *chain.from_iterable(rotated_points)) |
A CustomCanvas osztály, illetve metódusainak használatára illusztratív alkalmazási példaként egy stilizált macskafejet rajzolunk ki ellipsziseket és ellipszisíveket használva, valamint egy téglalapokból kialakított kalapot, amelyet a macskafej tetejére helyezünk. Végül az egész rajzot kicsit jobbra elforgatjuk. Ennek programkódja:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
modul: cat_and_hat.py import tkinter as tk from math import sqrt, cos, sin, radians from collections import namedtuple from custom_canvas import CustomCanvas Point = namedtuple('Point', 'x y') def corner_points(width, height, center_point: tuple) -> tuple: """Az alakzat befoglaló négyszögének szélessége és magassága, valamint a középpontja alapján a bal felső és jobb alsó sarokpontokat adja vissza.""" cpx, cpy = center_point p_topleft = (cpx - width / 2, cpy - height / 2) p_bottomright = (cpx + width / 2, cpy + height / 2) return p_topleft, p_bottomright class Cat: """Az osztály példánya a megadott CustomCanvas példányon egy macskafejet jelenít meg. A rajzelemek a megadott origóponthoz képest vannak pozícionálva. """ def __init__(self, canvas: CustomCanvas, origin_point: Point, name=None): self.cnv = canvas self.origin = origin_point self._id: str = str(id(self)) self.name = name if name is not None else 'cat' + self._id # A macska feje (koponyája) egy r sugarú kör lesz. self.r = root.winfo_fpixels('7c') self.skull = cnv.create_rotatable('ellipse', self.origin.x - self.r, self.origin.y - self.r, self.origin.x + self.r, self.origin.y + self.r, fill='gray65', tags=(self.name,)) self._draw_ears() # A fülek rajzolása. self._draw_eyes() # A szemek rajzolása. self._draw_pupilla() # A pupillák rajzolása. self._draw_muzzles() # Az orr alatti pofapárnák rajzolása. self._draw_nose() # Az orr rajzolása. self._draw_tongue() # A nyelv rajzolása. self._draw_whiskers() # A bajuszok rajzolása. def _draw_ellipse(self, shape_width, shape_height, center_point: tuple, angle, **options): """Egy olyan, shape_width szélességű és shape_height magasságú ellipszist rajzol a vászon elemen, amelynek középppontja center_point, és angle fokkal el van forgatva. Az ellipszis konfigurációs paramétereit kulcsszavas argumentumokkal lehet megadni. """ rellipse = self.cnv.create_rotatable_ellipse(*corner_points(shape_width, shape_height, center_point), **options) self.cnv.rotate(rellipse, angle, center_point) self.cnv.addtag_withtag(self.name, rellipse) return rellipse def _draw_arc(self, shape_width, shape_height, center_point: tuple, **options): """Egy olyan, shape_width szélességű és shape_height magasságú ellipszis ívét rajzolja a vászon elemen, amelynek középppontja center_point. Az ellipszisív konfigurációs paramétereit kulcsszavas argumentumokkal lehet megadni, tehát a kezdőszöget és szögtartományt a start és extent paraméternevekkel. """ rarc = self.cnv.create_rotatable_arc(*corner_points(shape_width, shape_height, center_point), **options) self.cnv.addtag_withtag(self.name, rarc) return rarc def _draw_ears(self): """A fülek rajzolása.""" right_ear_configs = dict(center_point=(self.origin.x + self.r * sqrt(2) / 2, self.origin.y - self.r * sqrt(2) / 2), angle=45, tags=('fül' + str(id(self)),)) self._draw_ellipse(0.8 * self.r, 0.45 * self.r, fill='gray65', **right_ear_configs) self._draw_ellipse(0.5 * self.r, 0.25 * self.r, fill='gray75', **right_ear_configs) left_ear_configs = dict(center_point=(self.origin.x - self.r * sqrt(2) / 2, self.origin.y - self.r * sqrt(2) / 2), angle=135, tags=('fül' + str(id(self)),)) self._draw_ellipse(0.8 * self.r, 0.45 * self.r, fill='gray65', **left_ear_configs) self._draw_ellipse(0.5 * self.r, 0.25 * self.r, fill='gray75', **left_ear_configs) self.cnv.tag_lower('fül' + str(id(self)), self.skull) def _draw_eyes(self): """A szemek rajzolása.""" eye_configs = dict(shape_width=0.55 * self.r, shape_height=0.3 * self.r, fill='white', width=2, outline='black', tags=('szem' + self._id,)) self._draw_ellipse(center_point=(self.origin.x + self.r * sqrt(2) / 4, self.origin.y - self.r * sqrt(2) / 4), angle=30, **eye_configs) self._draw_ellipse(center_point=(self.origin.x - self.r * sqrt(2) / 4, self.origin.y - self.r * sqrt(2) / 4), angle=150, **eye_configs) def _draw_pupilla(self): """A pupillák rajzolása.""" pupilla_configs = dict(shape_width=0.3 * self.r, shape_height=0.1 * self.r, angle=90, fill='black', tags=('pupilla' + self._id,)) self._draw_ellipse(center_point=(self.origin.x + self.r * sqrt(2) / 4, self.origin.y - self.r * sqrt(2) / 4), **pupilla_configs) self._draw_ellipse(center_point=(self.origin.x - self.r * sqrt(2) / 4, self.origin.y - self.r * sqrt(2) / 4), **pupilla_configs) def _draw_muzzles(self): """A bajuszpofák rajzolása.""" muzzle_configs = dict(shape_width=0.72 * self.r, shape_height=0.35 * self.r, fill='gray85', tags=('pofa' + self._id,), width=2, outline='black') self._draw_ellipse(center_point=(self.origin.x + 0.4 * self.r * cos(radians(30)), self.origin.y - 0.35 * self.r * sin(radians(-30))), angle=20, **muzzle_configs) self._draw_ellipse(center_point=(self.origin.x - 0.4 * self.r * cos(radians(30)), self.origin.y - 0.35 * self.r * sin(radians(-30))), angle=160, **muzzle_configs) def _draw_nose(self): """Az orr rajzolása.""" self._draw_arc(0.5 * self.r, 0.5 * self.r, (self.origin.x, self.origin.y + 0.25 * self.r), start=45, extent=90, fill='gray42', tags=('orr' + self._id,)) def _draw_tongue(self): """A nyelv rajzolása.""" self._draw_ellipse(0.5 * self.r, 0.3 * self.r, (self.origin.x, self.origin.y + 0.37 * self.r), 0, fill='#FF8787', width=2, outline='black', tags=('nyelv' + self._id,)) self.cnv.tag_lower('nyelv' + self._id, 'pofa' + self._id) def _draw_whiskers(self): """A bajuszok rajzolása.""" whiskers_configs = dict(shape_width=1.5 * self.r, shape_height=0.35 * self.r, start=10, extent=80, fill='black', width=2, style='arc', tags=('bajusz' + self._id,)) def _draw_whisker(start_from_origin: Point, angle_of_rotation): sp = Point(self.origin.x + start_from_origin.x, self.origin.y + start_from_origin.y) cp = Point(sp.x, sp.y + whiskers_configs.get('shape_height') / 2) w = self._draw_arc(center_point=cp, **whiskers_configs) self.cnv.rotate(w, angle_of_rotation, sp) # Jobb oldali bajuszok. _draw_whisker(Point(0.55 * self.r, 0.1 * self.r), 0) _draw_whisker(Point(0.37 * self.r, 0.17 * self.r), -10) _draw_whisker(Point(0.25 * self.r, 0.25 * self.r), -20) # Bal oldali bajuszok. whiskers_configs['start'] = 90 _draw_whisker(Point(-0.55 * self.r, 0.1 * self.r), 0) _draw_whisker(Point(-0.37 * self.r, 0.17 * self.r), 10) _draw_whisker(Point(-0.25 * self.r, 0.25 * self.r), 20) class Hat: """Az osztály példánya a megadott CustomCanvas példányon egy kalapot jelenít meg. A rajzelemek a megadott origóponthoz képest vannak pozícionálva. """ def __init__(self, canvas: CustomCanvas, origin_point: Point, name=None): self.cnv = canvas self.origin = origin_point self._id: str = str(id(self)) self.name = name if name is not None else 'hat' + self._id self.r = root.winfo_fpixels('7c') cnv.create_rotatable_rectangle((self.origin.x - 0.5 * self.r, self.origin.y - 0.04 * self.r), (self.origin.x + 0.5 * self.r, self.origin.y + 0.04 * self.r), fill='black', tags=('karima' + self._id, self.name)) cnv.create_rotatable_rectangle((self.origin.x - 0.4 * self.r, self.origin.y - 0.03 * self.r), (self.origin.x + 0.4 * self.r, self.origin.y + 0.03 * self.r), fill='gray95', tags=('szalag' + self._id, self.name)) cnv.move('szalag' + self._id, 0, -0.07 * self.r) cnv.create_rotatable_rectangle((self.origin.x - 0.4 * self.r, self.origin.y - 0.08 * self.r), (self.origin.x + 0.4 * self.r, self.origin.y + 0.08 * self.r), fill='black', tags=('korona' + self._id, self.name)) cnv.move('korona' + self._id, 0, -0.18 * self.r) root = tk.Tk() cnv_w, cnv_h = 800, 800 cnv = CustomCanvas(root, bg='light yellow', width=cnv_w, height=cnv_h, highlightthickness=0) cnv.pack() origin: Point = Point(cnv_w / 2, cnv_h / 2) # Az origó, amihez képest a rajzelemeket pozícionáljuk. cat = Cat(cnv, origin, 'cirmi') # A cicafej kirajzolása. hat = Hat(cnv, origin, 'kalap') # A kalap kirajzolása. cnv.move(hat.name, 0, -0.95 * cat.r) # A kalapot felhelyezzük a cica fejére. cnv.addtag_all('cirmi_kalapban') # A teljes rajz (cica és kalap) elforgatása 15 fokkal jobbra. cnv.rotate('cirmi_kalapban', -15, origin) root.mainloop() |
A programkódok elérhetők itt is: https://github.com/python…/rotatable_ellipse_arc_rectangle
Az eredmény a következő ábrán látható. Ilyen grafikát csupán a Canvas alapmetódusait használva nem tudnánk megalkotni.

Ha ez így egész jól működik, akkor felmerülhet a kérdés, hogy miért nem eleve így van megvalósítva a tkinter ellipszise, ellipszisíve és téglalapja? Ennek alapvetően memóriatakarékossági és feldolgozási idő csökkentési oka van. Ha a befoglaló téglalappal határozzuk meg e síkidomokat, akkor összesen két pontot, vagyis négy koordinátaértéket, azaz négy float számot kell tárolni minden egyes alakzathoz. Ha sokszöggel valósítjuk meg ezeket, akkor sokkal többet. Bár téglalap esetén csak négyet (már ez is duplázás), de ellipszis esetén több százat. Ez sok száz vagy ezer alakzat esetén tekintélyes memóriafelhasználást eredményez. Továbbá, a Canvas implementáció így jól optimalizált sok rajzelem kezelésére. Ha azonban egy rajzelem, mint például egy sokszög, sok pontból áll, akkor a kódnak sorban egymás után végig kell pásztáznia ezeket a pontokat. Ez viszont hátrányosan befolyásolhatja az egéresemények feldolgozásának idejét a vászon ezen objektumot tartalmazó területén.
A grafikus felhasználói felület létrehozásával, példákkal illusztrált részletes leírásával a Python tudásépítés lépésről lépésre című e-könyv „Grafikus felhasználói felület készítése” fejezete foglalkozik. E bejegyzés példája kapcsán érdemes még a „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezet „Speciális konténer típusok” és „Speciális iterátorok” alfejezeteit is átnézni.