A korábbi bejegyzésekben többször foglalkoztunk azzal a kérdéskörrel, hogy a tkinter vászon (Canvas) grafikus elemén hogyan lehet egy pontot forgatni és tükrözni. Ezek elvi alapjait példákkal a „Hogyan forgassunk el egy adott pontot adott szöggel, adott forgáspont körül?” és „Hogyan valósítsuk meg egy pont tengelyes tükrözését?” korábbi cikkek tartalmazzák. Mindezt azért is tettük, mert a vásznon alapban megjeleníthető rajzelemek többsége (ellipszis, ellipszisív és téglalap) nem forgatható el alaktartóan, vagyis a forgatás után 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.
Ha alaktartó forgatást szeretnénk, akkor azt csak úgy érhetjük el, hogy az említett síkidomokat egy-egy sokszöggel valósítjuk meg, mert ha egy sokszög minden csúcspontjára alkalmazzuk a forgatást, akkor maga a sokszög alaktartóan fog elfordulni az adott forgáspont körül. Ezzel foglalkoztunk a „Sokszög forgatása és tükrözése vizuálisan grafikus alkalmazással” című bejegyzésben. Ezen ismeretek birtokában pedig megnyílik az út ellipszis, ellipszisív és téglalap forgatására is. Ennek egy lehetséges megoldásával foglalkozott a „Forgatható ellipszis, ellipszisív és téglalap” című bejegyzés. Itt a Canvas osztályt specializáltuk egy olyan alosztállyal, amely képes a címében szereplő olyan síkidomokat előállítani, amelyeket alaktartóan lehet forgatni egy pont körül. Ha nem csak e három síkidomot kívánjuk forgathatóvá tenni, hanem másokat is, akkor minden ilyenhez egy új létrehozó metódust kell definiálni a specializált Canvas alosztályban. Ez azonban nem igazán jó megközelítés, mert vagy az alosztályt kell módosítani minden új síkidomhoz, ami a nyitott-zárt elvet (open-closed principle) sérti, vagy ezt elkerülendő mindig egy új alosztályt kell létrehozni, aminek szintén vannak hátrányai (pl. áttekinthetőség és karbantarthatóság csökkenése; az öröklési hierarchia változtatással szembeni sérülékenysége; a kliens kód esetleges módosítási szükségessége.)
Mindezek miatt egy másik megközelítést mutatunk e bejegyzésben.
Az elv az, hogy számbavesszük, hogy egy vászon grafikus elemen megjelenített sokszöggel megvalósított síkidomokhoz milyen számunkra fontos, általános, vagyis minden síkidomra értelmezhető műveletek rendelhetők. Ezeket egy közös alaposztályba szervezzük, aminek neve legyen PolygonGraphics. Ezen osztály metódusaiként definiáljuk tehát például
- a geometriai transzformációkat (pl. forgatás, tükrözés, eltolás, áthelyezés, átméretezés),
- a grafika befoglaló téglalapja ellentétes sarokpontjainak meghatározását, és e téglalap középpontjának meghatározását,
- a sokszög csúcspontjainak középpontjának (súlypontjának) meghatározását,
- a sokszög oldalhosszainak kikérését,
- a sokszög csúcspontjainak kikérését és megváltoztatását,
- a sokszög egy adott, vagy minden konfigurációs paramétere értékének kinyerését,
- a sokszög konfigurációs jellemzőinek (pl. kitöltőszín, körvonalszín, körvonal vastagság) beállíthatóságát,
- címkék (tag-ek) hozzárendelését, törlését, lekérdezését,
- események és eseménykezelők hozzárendelését és törlését
A felsorolt metódusok működéséhez kell néhány privát segédmetódus, mint például a korábbi „Sokszögek csúcspontjainak sorbarendezése a helyes megjelenítéshez” bejegyzésben ismertetettek. De ilyen lehet egy ellipszis pontjait adott szögtartományban szolgáltató metódus is, ami egyéni síkidomok megalkotásakor könnyítheti a munkát.
Az egyes, sokszöggel megvalósított síkidomokat szintén egy-egy osztályban definiáljuk, amelyek öröklik a PolygonGraphics osztályt, vagyis a konkrét síkidomosztályok a PolygonGraphics alosztályai lesznek. Ha így teszünk, akkor az egyes síkidomosztályok szerkezete egyszerű lesz, mert kötelezően csak egy olyan metódust kell tartalmazni, ami a síkidom grafikát létrehozza sokszögből a Canvas osztály create_polygon() metódusának hívásával vagy egy már létező sokszög felhasználásával. A kötelezően implementálandó metódusnak azonos neve lehet minden alosztályban, ami legyen most _create_graphics(). Mivel a grafika létrehozás és kirajzolás minden síkidom esetén egy kötelezően megvalósítandó feladat, ezért a _create_graphics() meghívását betehetjük a PolygonGraphics szülőosztályba, konkrétan annak __init__ metódusába. Ezzel párhuzamosan a PolygonGraphics osztályban definiálunk egy absztrakt _create_graphics() metódust, ami kikényszeríti az alosztályokban történő implementálást. Ezzel a PolygonGraphics osztály absztrakt osztály lesz, ezért az abc modul ABC osztályát örökölnie kell.
A grafika létrehozás és kirajzolás természetesen egy adott Canvas példányon történik, ezért mind a PolygonGraphics mind alosztályai konstruktora kell, hogy egy Canvas példányt fogadjon. És ugyanezen okból mind a PolygonGraphics mind alosztályai konstruktorában lehetőséget kell biztosítani kulcsszavas argumentumokkal, hogy a sokszög konfigurációját meg lehessen adni.
Ezeken felül a konkrét síkidomokat megvalósító alosztályok konstruktorában kell megadni a sokszög csúcspontjait is olyan síkidomoknál, ahol a síkidom maga ténylegesen sokszög. Ott, ahol ez releváns lehet, az ilyen síkidom geometriai meghatározására alternatív módot is lehet biztosítani egy-egy osztálymetódussal. Így például a téglalap és négyzet esetén az osztálymetódus a csúcspont-koordináták helyett az oldalhosszakat és a kezdeti pozíció koordinátáit fogadhatja. Bármilyen geometriai adatokkal is határozzuk is meg a síkidomokat a példányosító osztálymetódusokban, követelmény, hogy a Canvas példányt és a sokszög konfigurációját is meg lehessen adni.
Az olyan síkidomok esetén, amelyek nem sokszögek, hanem csak sokszögeket használunk a közelítésükre (pl. ellipszis vagy kör) az implementáló osztály konstruktora természetesen a csúcspontok helyett más adatot fog fogadni. Például kör esetén ezek lehetnek a sugárhossz és a középpont.
Négyszögek esetén a konstruktorokban felhasználhatjuk az előző, „Négyszögfajták ellenőrzése” című bejegyzésben bemutatott ellenőrzőfüggvényeket.
Hasznos művelet még egy síkidom másolása, klónozása. Mivel ez minden síkidom esetén elvégezhető, ezért a PolygonGraphics szülőosztályban definiáljuk clone() néven. Az egyetlen gond, hogy e metódus által visszaadott új példányt az adott konkrét síkidomtól függően más-más módon kell előállítani. Ezt a problémát úgy oldjuk fel, hogy egy _instance_factory() nevű közös absztrakt példánylétrehozó metódust definiálunk a PolygonGraphics osztályban, amit a clone() meghív. Mivel az _instance_factory() absztrakt, ezért azt kötelező minden konkrét síkidomosztálynak implementálnia a sajátosságainak megfelelően. Ez azonban nem bonyolult, többnyire egy egyszerű konstruktorhívás lesz.
Az eddig ismertetett metódusokon felül igény szerint mások is definiálhatók, legyenek azok általánosak, mint például a terület és kerület, vagy síkidomspecifikusak mint például háromszög esetén annak nevezetes pontjait meghatározók.
A grafikák előállításának lehetőségeit nagymértékben bővíti, ha az egyes síkidomokat csoportba tudjuk foglalni, és a különféle műveleteket (pl. forgatást, tükrözést, áthelyezést) a csoport egészére tudjuk végezni. Ezt egy Group nevű osztállyal valósítjuk meg. Mivel ez nem grafikákat hoz létre, hanem meglévő grafikus objektumokat tárol és ezen végez műveleteket, ezért ennek konstruktora csak a csoportba foglalni kívánt PolygonGraphics típusú objektumokat kell, hogy fogadja, a Canvas példányt és a konfigurációs értékeket nem. Ami a metódusokat illeti, a tartalmazásvizsgálaton, iterálhatóságon és igazságérték meghatározásán, valamint a csoporthoz adás és abból való eltávolítás műveletén felül a PolygonGraphics metódusai közül azokat valósítjuk meg, amelyek csoportra értelmezhetők.
A fenti elvek alapján elkészített PolygonGraphics osztály definícióját láthatjuk alább:
|
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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# modul: fundamental_classes # Python 3.12+ import cmath import contextlib import tkinter as tk from abc import ABC, abstractmethod from itertools import count, batched, starmap, pairwise from statistics import mean from typing import Iterable, Self, Annotated, Generator, Callable from math import radians, cos, sin, dist, atan2, isclose type PointType = tuple[int | float, int | float] type AngleDegree = Annotated[int | float, 'szögérték fokban'] class PolygonGraphics(ABC): """Absztrakt alaposztály, amelyet a sokszögekből kialakított síkidom konkrét osztályának örökölni kell.""" _instance_counter = count(1) # Az azonosítócímke létrehozásához használt, példányonkénti egyedi szám generátor. def __init__(self, canvas: tk.Canvas, **options): self.canvas = canvas # Az új síkidompéldány azonosítócímkéjének előállítása a típusnév és egyedi szám kombinációjával. self.id_tag: str = type(self).__name__ + str(next(self._instance_counter)) # A grafika létrehozása. self._create_graphics() # Alapértelmezésben az alakzat nincs kitöltve, csak a körvonal látszik. self.config(fill='', outline='black', width=1) self.config(**options) # A megadott konfigurációs beállítások érvényesítése. @abstractmethod def _create_graphics(self) -> None: """A sokszögekből kialakított síkidom konkrét osztályában implementálandó metódus, amely a síkidom grafikát létrehozza sokszögből a Canvas create_polygon(self.id_tag,...) metódus hívásával vagy egy már létező sokszög felhasználásával. """ raise NotImplementedError @abstractmethod def _instance_factory(self) -> Self: """A sokszögekből kialakított síkidom konkrét osztályában implementálandó metódus, amely e konkrét osztály konstruktorával előállított új példánnyal tér vissza. """ raise NotImplementedError def clone(self) -> Self: """A konkrét síkidom olyan új példányával tér vissza, amely konfigurációs jellemzői megegyeznek az eredeti példányéval. """ new_inst = self._instance_factory() # Az új konkrét példány létrehozása. new_inst.config(**self.all_cget()) # Az új példány konfigurációjának beállítása az eredetivel megegyezően. new_inst.dtag(self.id_tag) # Az eredeti példány azonosítócímkéjének eltávolítása az új példányról. new_inst.set_coords(self.get_coords()) # Az új példány csúcspontjainak beállítása az eredetivel megegyezően. return new_inst def _flatten_xycoords(self, coords: Iterable): """Az x és y koordinátákat adja vissza egymás után függetlenül attól, hogy azokat az argumentum közvetlenül szolgáltatja, vagy iterálható objektumból származnak. Pl. coords elemei x1, y1, x2, y2 -> kimenet: x1, y1, x2, y2 coords elemei (x1, y1), [x2, y2] -> kimenet: x1, y1, x2, y2 """ for coord in coords: try: iter(coords) if isinstance(coord, (str, bytes, bytearray)): raise TypeError yield from self._flatten_xycoords(coord) except TypeError: if isinstance(coord, (int, float)): yield coord else: raise TypeError('A koordináták csak valós számok lehetnek.') def vertices_centroid(self, *vertices: Iterable) -> tuple[int | float, int | float]: """A megadott csúcspontok középpontját (súlypontját) adja vissza.""" xy_coordinates = list(self._flatten_xycoords(vertices)) # A kapott lista: [x1, y1, x2, y2, ..., xn, yn] # Kinyerjük az x koordináták sorozatát és az y koordináták sorozatát. x_coords, y_coords = xy_coordinates[::2], xy_coordinates[1::2] # Meghatározzuk a csúcspontok középpontját (súlypontját), ami mint az a geometriából ismert, a # csúcspontok számtani közepe. center_x, center_y = mean(x_coords), mean(y_coords) return center_x, center_y def _sort_vertices_for_proper_plotting(self, *vertices) -> tuple[PointType, ...]: """Bármilyen sorrendben vannak a kirajzolandó sokszög csúcspontjai megadva, a visszatérési érték a csúcspontok olyan sorozata lesz, amellyel a sokszög megfelelően, azaz keresztező vonalak nélkül lesz megjelenítve. A csúcspontokat az x, y koordináták egymást követő felsorolsával, vagy az x, y párokat szolgáltató iterálható objektumok felsorolásával lehet megadni. """ # Meghatározzuk a csúcspontok középpontját (súlypontját) center_x, center_y = self.vertices_centroid(vertices) xy_coordinates = list(self._flatten_xycoords(vertices)) # A kapott lista: [x1, y1, x2, y2, ..., xn, yn] # Kiszámítjuk az egyes csúcspontok középponttól vett szögét. vertices_angles = {(x, y): atan2(x - center_x, y - center_y) for x, y in batched(xy_coordinates, 2)} # A csúcspontokat a szögeik szerint rendezzük. sorted_vertices_angles = sorted(vertices_angles.items(), key=lambda t: t[1]) # A rendezett csúcspontokkal térünk vissza. return tuple(point for point, angle in sorted_vertices_angles) @staticmethod def _all_equal(*numbers: int | float, relative_tolerance=1e-9) -> bool: """True értéket ad vissza, ha az összes argumentum egyenlő az adott relatív tolerancián belül.""" # Azt, hogy minden érték egyenlő, úgy határozzuk meg, hogy kihasználjuk az egyenlőségi reláció # tranzitivitását: ha n1==n2 és n1==n3, akkor ebből következik, hogy n2==n3. return all(isclose(numbers[0], number, rel_tol=relative_tolerance) for number in numbers[1:]) def side_lengths(self, *vertices: Iterable) -> list[float]: """A csúcspontok által meghatározott sokszög oldalhosszainak listáját adja vissza.""" sorted_points = self._sort_vertices_for_proper_plotting(vertices) # Ahhoz, hogy a sokszög csúcspontpárokból számolt oldalhosszai között a sorban utolsó oldal is # szerepeljen, az első csúcspontot a sorozat végén, utolsóként fel kell venni. return [dist(p1, p2) for p1, p2 in pairwise([*sorted_points, sorted_points[0]])] def config(self, **options) -> None: """A sokszöggel megvalósított síkidom jellemzőit állítja be a kulcsszavas argumentumokkal. A konfigurációs opciók megegyeznek a vászon (Canvas) sokszög rajzelemére beállíthatókkal. """ # Ha az argumentum a "tags" opciót is meghatározza, akkor gondoskodni kell arról, hogy # az azonosítócímke ne vesszen el. tgs = options.get('tags', '') if tgs: if isinstance(tgs, (tuple, list)): tags_string = ' '.join((*tgs, self.id_tag)) elif isinstance(tgs, str): tags_string = ' '.join((tgs, self.id_tag)) else: raise ValueError('A "tags" konfigurációs paraméter értéke string vagy string sorozat lehet') options['tags'] = tags_string self.canvas.itemconfig(self.id_tag, **options) configure = config def cget(self, option: str) -> str: """A sokszög option által megadott konfigurációs paraméterének aktuális értékével tér vissza.""" return self.canvas.itemcget(self.id_tag, option) config_option_value = cget def all_cget(self) -> dict: """A sokszög összes konfigurációs paraméterét és aktuális értékét adja vissza.""" return {k: v[-1] for k, v in self.canvas.itemconfigure(self.id_tag).items()} all_config_options = all_cget def get_coords(self) -> list[float]: """A sokszög pontjainak x, y koordinátáit adja vissza egy listában.""" return self.canvas.coords(self.id_tag) def set_coords(self, *vertices) -> None: """A sokszög pointjait a megadottakra változtatja.""" self.canvas.coords(self.id_tag, *self._flatten_xycoords(vertices)) def gettags(self) -> tuple[str, ...]: """A sokszöghöz rendelt tag-eket adja vissza.""" return self.canvas.gettags(self.id_tag) def add_tag(self, new_tag: str) -> None: """A megadott tag-et hozzárendeli a sokszöghöz.""" self.canvas.addtag_withtag(new_tag, self.id_tag) def dtag(self, tag_to_delete: str) -> None: """A megadott tag-et eltávolítja a sokszögről. Az azonosítócímkét nem lehet törölni.""" if tag_to_delete != self.id_tag: self.canvas.dtag(self.id_tag, tag_to_delete) delete_tag = dtag def bind(self, event_pattern_sequence: str | None = None, func: Callable[[tk.Event], None] | None = None, add: bool | None = None) -> str: """Az első argumentummal meghatározott eseményt vagy eseménysorozatot és eseménykezelőt társítja a sokszöghöz. Ha az add True igazságértékű objektum, akkor a func függvény a korábban hozzárendelt más eseménykezelők lefutása után lesz meghívva, egyébként az eseményre csak a func lesz végrehajtva. A metódus visszatérési értéke egy azonosítő, ami lehetővé teszi a func eseménykezelő törlését a unbind() metódussal. """ return self.canvas.tag_bind(self.id_tag, event_pattern_sequence, func, add) def unbind(self, event_pattern_sequence: str, func_id: str | None = None) -> None: """Az első argumentummal meghatározott eseményhez vagy eseménysorozathoz kötött, és a funcid értékével azonosított eseménykezelőt eltávolítja. """ self.canvas.tag_unbind(self.id_tag, event_pattern_sequence, func_id) def bbox(self) -> tuple[int, int, int, int]: """A sokszög befoglaló téglalapja bal felső és jobb alsó sarokpontjának koordinátáival tér vissza.""" return self.canvas.bbox(self.id_tag) def bbox_center(self) -> tuple[int | float, int | float]: """A sokszög befoglaló téglalapja középpontjának koordinátáival tér vissza.""" x1, y1, x2, y2 = self.bbox() return (x1 + x2) / 2, (y1 + y2) / 2 def move(self, dx, dy) -> None: """A sokszöget az x tengely irányában dx, az y tengely irányában dy értékkel tolja el.""" self.canvas.move(self.id_tag, dx, dy) def moveto(self, x, y) -> None: """A sokszöget áthelyezi olyan módon, hogy befoglaló téglalapjának bal felső pontja az x, y koordinátákkal megadott ponton legyen. """ self.canvas.moveto(self.id_tag, x, y) def scale(self, ref_x, ref_y, scalefactor_x, scalefactor_y) -> None: """A sokszöget átméretezi az első két argumentummal meghatározott referenciaponthoz képest. A sokszög minden pontja x koordinátájának referenciaponttól vett távolsága szorzódik a scalefactor_x valós számmal, az y koordinátájának referenciaponttól vett távolsága pedig az scalefactor_y valós számmal. """ self.canvas.scale(self.id_tag, ref_x, ref_y, scalefactor_x, scalefactor_y) def rotate(self, angle: int | float, center_of_rotation: PointType = (0, 0), in_degrees=True) -> None: """A sokszöget, annak minden pontját angle szöggel forgatja el a második argumentummal megadott forgáspont körül. Ha az utolsó paraméter értéke True akkor a szög fokokban értendő, False esetén radiánban. """ cor = complex(*center_of_rotation) points_to_rotate: Iterable[complex] = starmap(complex, batched(self.canvas.coords(self.id_tag), 2)) rotated_complex_points: Iterable[complex] = (cmath.exp(1j * (radians(angle) if in_degrees else angle)) * (point - cor) + cor for point in points_to_rotate) rotated_points: Iterable[tuple[float, float]] = ((c.real, c.imag) for c in rotated_complex_points) self.canvas.coords(self.id_tag, *rotated_points) def reflect(self, *one_or_two_points) -> None: """A grafikát középpontosan vagy tengelyesen tükrözi. Ha az argumentum egy pontot határoz meg, akkor erre a pontra vonatkozó tükrözést végez. Ha az argumentum két pontot határoz meg, akkor a két ponttal jellemzett egyenesre vonatkozó tengelyes tükrözést hajt végre. A pontokat meg lehet adni vagy az x, y koordinták felsorolásával, vagy olyan iterálható objektumok sorozatával, amelyek az x és y koordintát szolgáltatják. Pl.: x1, y1, x2, y2 vagy (x1, y1), (x2, y2) """ xy_coords: tuple = tuple(self._flatten_xycoords(one_or_two_points)) if all(isinstance(e, (int, float)) for e in xy_coords): if len(xy_coords) == 2: self._reflect_across_a_point(*xy_coords) elif len(xy_coords) == 4: self._reflect_across_a_line(*xy_coords) return raise ValueError('Tükrözésehez egy vagy két pontot kell megadni.') def _reflect_across_a_point(self, x, y) -> None: """Középpontos tükrözést végez az x, y koordinátákkal megadott pontra vonatkozóan.""" self.rotate(180, (x, y)) @staticmethod def _reflect_point_across_line_complex(x, y, x1, y1, x2, y2) -> tuple[float, float]: """A megadott x, y pontnak az x1, y1 és x2, y2 pontok által meghatározott tengelyre vett tükörkép pontjával tér vissza. """ # A tükörpont meghatározása komplex számokkal való műveletekkel valósul meg. pc, p1c, p2c = complex(x, y), complex(x1, y1), complex(x2, y2) psi = (pc - p1c).conjugate() psir = psi * (p2c - p1c) / (p2c - p1c).conjugate() p_reflected = psir + p1c return p_reflected.real, p_reflected.imag def _reflect_across_a_line(self, x1, y1, x2, y2) -> None: """Az alakzatot az x1, y1 és x2, y2 pontok által meghatározott tengelyre vonatkozóan tükrözi.""" points_to_reflect = batched(self.canvas.coords(self.id_tag), 2) reflected_points = (self._reflect_point_across_line_complex(*p, x1, y1, x2, y2) for p in points_to_reflect) self.canvas.coords(self.id_tag, *reflected_points) @staticmethod def ellipse_arc_points(semi_major_axis: int | float, semi_minor_axis: int | float, center_x: int | float, center_y: int | float, start_angle: AngleDegree = 0, stop_angle: AngleDegree = 360, number_of_points: int | None = None) -> Generator[tuple[int | float, int | float], None, None]: """Egy olyan generátorobjektummal tér vissza, amely egy nagy és kis féltengelyével és középppontjával megadott ellipszis megadható számú pontjait szolgáltatja egy kezdő és végszöggel meghatározható szögtartományban. """ if semi_major_axis < 0 or semi_minor_axis < 0: raise ValueError('Az ellipszis féltengelyeinek hossza vagy a kör sugara nemnegatív szám kell, hogy legyen.') if stop_angle < start_angle: raise ValueError('Az induló szög nem lehet nagyobb a végszögnél.') if number_of_points is None: # 1000 pixel hosszú fél nagytengelynél 360 fok felosztása elég 600 pontban. Rövidebb esetben vagy kisebb szögnél # arányosan kevesebb pont kell, de egy adott pontszámnál nem lehet kevesebb az ábrázolhatósághoz. n1, n2 = 800 * semi_major_axis / 1000 + 64, (stop_angle - start_angle) / 360 number_of_points = int(round(n1 * n2, 0)) angle_increment = (stop_angle - start_angle) / number_of_points # A pontok koordinátáit az ellipszis paraméteres egyenletrendszere alapján határozzuk meg. return ((semi_major_axis * cos(radians(alpha)) + center_x, semi_minor_axis * sin(radians(alpha)) + center_y) for alpha in (start_angle + angle_increment * i for i in range(number_of_points))) |
A Group osztály definíciója pedig így néz ki:
|
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 |
# modul: fundamental_classes class Group: """Olyan iterálható konténerobjektum, amely csoportba foglalja a megadott, sokszögből előállított grafikaobjektumokat. A csoportba foglalással a grafikaobjektumok együttesen mint egyetlen grafika kezelhetők bizonyos műveletekhez (pl. áthelyezés, forgatás, tükrözés). """ _instance_counter = count(1) # Az azonosítócímke létrehozásához használt, példányonkénti egyedi szám generátor. def __init__(self, *polygon_graphics_objects: PolygonGraphics): self._id_tag: str = type(self).__name__ + str(next(self._instance_counter)) self.graphics_objects: list[PolygonGraphics] = [] self.add_graphics(*polygon_graphics_objects) def __contains__(self, polygon_graphics_object: PolygonGraphics) -> bool: return polygon_graphics_object in self.graphics_objects def __bool__(self) -> bool: return bool(self.graphics_objects) def __iter__(self): return iter(self.graphics_objects) def add_graphics(self, *polygon_graphics_objects: PolygonGraphics): """Grafikaobjektumok hozzáadása a csoporthoz.""" self.graphics_objects.extend(polygon_graphics_objects) for g in self.graphics_objects: g.add_tag(self._id_tag) # A grafikaobjektumokat ellátjuk a csoport azonosító címkéjével. def remove_graphics(self, *polygon_graphics_objects: PolygonGraphics): """Grafikaobjektumok eltávolítása a csoportból.""" for g in polygon_graphics_objects: g.dtag(self._id_tag) # Az eltávolítandó grafikaobjektumokról töröljük a csoport azonosító címkéjét. # A grafikaobjektumot eltávolítjuk a csoport konténeréből. Ha eleve nincs benne, akkor nem történik semmi. with contextlib.suppress(ValueError): self.graphics_objects.remove(g) def _get_canvas(self) -> tk.Canvas: """A csoportba fogalalt grafikákhoz tartozó vászon elemmel tér vissza, vagy hibaüzenettel, ha a csoport üres. """ try: return list(self.graphics_objects)[0].canvas except IndexError: raise ValueError('A csoport nem tartalmaz grafikát') def bind(self, event_pattern_sequence: str | None = None, func: Callable[[tk.Event], None] | None = None, add: bool | None = None) -> str: """Az első argumentummal meghatározott eseményt vagy eseménysorozatot és eseménykezelőt társítja a csoportgrafikához. Ha az add True igazságértékű objektum, akkor a func függvény a korábban hozzárendelt más eseménykezelők lefutása után lesz meghívva, egyébként az eseményre csak a func lesz végrehajtva. A metódus visszatérési értéke egy azonosítő, ami lehetővé teszi a func eseménykezelő törlését az unbind() metódussal. """ return self._get_canvas().tag_bind(self._id_tag, event_pattern_sequence, func, add) def unbind(self, event_pattern_sequence: str, func_id: str | None = None): """Az első argumentummal meghatározott eseményhez vagy eseménysorozathoz kötött, és a funcid értékével azonosított eseménykezelőt eltávolítja. """ self._get_canvas().tag_unbind(self._id_tag, event_pattern_sequence, func_id) def bbox(self) -> tuple[int, int, int, int]: """A teljes csoportgrafika befoglaló téglalapja bal felső és jobb alsó sarokpontjának koordinátáival tér vissza. """ return self._get_canvas().bbox(self._id_tag) def bbox_center(self) -> tuple[int | float, int | float]: """A teljes csoportgrafika befoglaló téglalapja középpontjának koordinátáival tér vissza.""" x1, y1, x2, y2 = self.bbox() return (x1 + x2) / 2, (y1 + y2) / 2 def move(self, dx, dy) -> None: """A teljes csoportgrafikát az x tengely irányában dx, az y tengely irányában dy értékkel tolja el.""" self._get_canvas().move(self._id_tag, dx, dy) def moveto(self, x, y) -> None: """A teljes csoportgrafikát áthelyezi olyan módon, hogy befoglaló téglalapjának bal felső pontja az x, y koordinátákkal megadott ponton legyen. """ self._get_canvas().moveto(self._id_tag, x, y) def scale(self, x_origin, y_origin, scalefactor_x, scalefactor_y) -> None: """A teljes csoportgrafikát átméretezi az első két argumentummal meghatározott referenciaponthoz képest. A sokszögek minden pontja x koordinátájának referenciaponttól vett távolsága szorzódik a scalefactor_x valós számmal, az y koordinátájának referenciaponttól vett távolsága pedig az scalefactor_y valós számmal. """ self._get_canvas().scale(self._id_tag, x_origin, y_origin, scalefactor_x, scalefactor_y) def rotate(self, angle: int | float, center_of_rotation: PointType = (0, 0), in_degrees=True) -> None: """A teljes csoportgrafikát az angle szöggel elforgatja a második argumentummal megadott forgáspont körül. Ha az utolsó paraméter értéke True akkor a szög fokokban értendő, False esetén radiánban. """ for g in self.graphics_objects: g.rotate(angle, center_of_rotation, in_degrees) def reflect(self, *one_or_two_points) -> None: """A teljes csoportgrafikát középpontosan vagy tengelyesen tükrözi. Ha az argumentum egy pontot határoz meg, akkor erre a pontra vonatkozó tükrözést végez. Ha az argumentum két pontot határoz meg, akkor a két ponttal jellemzett egyenesre vonatkozó tengelyes tükrözést hajt végre. A pontokat meg lehet adni vagy az x, y koordinták felsorolásával, vagy olyan iterálható objektumok sorozatával, amelyek az x és y koordintát szolgáltatják. Pl.: x1, y1, x2, y2 vagy (x1, y1), (x2, y2) """ for g in self.graphics_objects: g.reflect(*one_or_two_points) def clone(self) -> Self: """Olyan új csoporttal tér vissza, amelyben új grafikaobjektumok vannak, de az eredeti csoportban foglaltakal megegyező jellemzőkkel. """ return type(self)(*[g.clone() for g in self.graphics_objects]) |
Mivel ezek az alapvető osztályok a sokszögekkel megvalósított síkidomokkal készítendő grafikákákhoz, ezért ezek egy közös, fundamental_classes nevű modulba szerepelnek.
A négyszögek alosztályai (deltoid, trapéz, paralelogramma, rombusz, téglalap, négyzet) a quadrilaterals modulba vannak foglalva:
|
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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# modul: quadrilaterals from typing import Iterable, Self from math import dist, isclose import tkinter as tk from fundamental_classes import PolygonGraphics class Quadrilateral(PolygonGraphics): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): self._vertices_coords: tuple = tuple(self._flatten_xycoords(vertex_points)) try: if not len(self._vertices_coords) == 8: raise ValueError except ValueError: raise ValueError('A megadott pontok nem négyszöget határoznak meg.') super().__init__(canvas, **options) def _create_graphics(self): self.canvas.create_polygon(*self._sort_vertices_for_proper_plotting(self._vertices_coords), tags=(self.id_tag,)) def _instance_factory(self) -> Self: return type(self)(self.canvas, *self.get_coords()) class Kite(Quadrilateral): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): super().__init__(canvas, *vertex_points, **options) try: if not self._is_kite(vertex_points): raise ValueError except ValueError: raise ValueError('A megadott pontok nem deltoidot határoznak meg.') def _is_kite(self, *vertices: Iterable) -> bool: """Akkor ad vissza True értéket, ha a megadott pontok egy deltoidot alkotnak. Egy sokszög deltoid, ha négyszög és két-két szomszédos oldala egyenlő hosszú. A csúcspontokat az x, y koordináták egymást követő felsorolsával, vagy az x, y párokat szolgáltató iterálható objektumok felsorolásával lehet megadni. """ sides = self.side_lengths(vertices) # Ha két-két szomszédos oldal egyenlő, akkor deltoid. Ez a rendezett csúcspontokból meghatározott oldalak sorozatában # úgy fordulhat elő, hogy vagy az első kettő és a második kettő oldal egyenlő, vagy a középső kettő és a két szélső. return len(sides) == 4 and (self._all_equal(*sides[:2]) and self._all_equal(*sides[2:])) or \ (self._all_equal(*sides[1:3]) and self._all_equal(sides[0], sides[-1])) class Trapezoid(Quadrilateral): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): super().__init__(canvas, *vertex_points, **options) try: if not self.is_trapezoid(vertex_points): raise ValueError except ValueError: raise ValueError('A megadott pontok nem trapézt határoznak meg.') def is_trapezoid(self, *vertices: Iterable) -> bool: """Akkor ad vissza True értéket, ha a megadott pontok egy trapézt alkotnak. Egy sokszög trapéz, ha négyszög és vannak párhuzamos szemközti oldalai. A csúcspontokat az x, y koordináták egymást követő felsorolsával, vagy az x, y párokat szolgáltató iterálható objektumok felsorolásával lehet megadni. """ sorted_points = self._sort_vertices_for_proper_plotting(vertices) if len(sorted_points) == 4: x1, y1, x2, y2, x3, y3, x4, y4 = self._flatten_xycoords(sorted_points) # A csúcspontok rendezettségéből adódóan, ha a négyszög trapéz, akkor két eset lehet: # 1) az (x1, y1) és (x2, y2) pontokkal meghatározott oldal, valamint az (x3, y3) és (x4, y4) pontokkal # meghatározott oldal szemközti oldalak, és ezek párhuzamosak, vagy # 2) az (x2, y2) és (x3, y3) pontokkal meghatározott oldal, valamint az (x4, y4) és (x1, y1) pontokkal # meghatározott oldal szemközti oldalak, és ezek párhuzamosak. # A párhuzamosságot a szemközti oldalak meredekségének összevetésével ellenőrizzük. if isclose((y2 - y1) / (x2 - x1), (y4 - y3) / (x4 - x3)) or isclose((y3 - y2) / (x3 - x2), (y4 - y1) / (x4 - x1)): return True return False class Parallelogram(Quadrilateral): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): super().__init__(canvas, *vertex_points, **options) # print('paralogramma methods', [mn for mn in dir(self) if not mn.startswith('__')]) try: if not self._is_parallelogram(vertex_points): raise ValueError except ValueError: raise ValueError('A megadott pontok nem paralelogrammát határoznak meg.') def _is_parallelogram(self, *vertices: Iterable) -> bool: """Akkor ad vissza True értéket, ha a megadott pontok egy paralelogrammát alkotnak. Egy sokszög paralelogramma, ha négyszög és szemközti oldalai párhuzamosak. A paralelogramma szemközti oldalai egyenlő hosszúak. A csúcspontokat az x, y koordináták egymást követő felsorolsával, vagy az x, y párokat szolgáltató iterálható objektumok felsorolásával lehet megadni. """ sorted_points = self._sort_vertices_for_proper_plotting(vertices) if len(sorted_points) == 4: x1, y1, x2, y2, x3, y3, x4, y4 = self._flatten_xycoords(sorted_points) # A csúcspontok rendezettségéből adódóan, ha a négyszög paralelogramma, akkor két eset lehet: # 1) az (x1, y1) és (x2, y2) pontokkal meghatározott oldal, valamint az (x3, y3) és (x4, y4) pontokkal # meghatározott oldal szemközti oldalak, és ezek párhuzamosak, vagy # 2) az (x2, y2) és (x3, y3) pontokkal meghatározott oldal, valamint az (x4, y4) és (x1, y1) pontokkal # meghatározott oldal szemközti oldalak, és ezek párhuzamosak. # A párhuzamosságot a szemközti oldalak meredekségének összevetésével ellenőrizzük. # Párhuzamosság esetén a szakaszhosszok egyenlőségét is ellenőrizni kell. if isclose((y2 - y1) / (x2 - x1), (y4 - y3) / (x4 - x3)): return isclose(dist((x1, y1), (x2, y2)), dist((x3, y3), (x4, y4))) elif isclose((y3 - y2) / (x3 - x2), (y4 - y1) / (x4 - x1)): return isclose(dist((x2, y2), (x3, y3)), dist((x4, y4), (x1, y1))) return False class Rhombus(Quadrilateral): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): super().__init__(canvas, *vertex_points, **options) try: if not self._is_rhombus(vertex_points): raise ValueError except ValueError: raise ValueError('A megadott pontok nem rombuszt határoznak meg.') def _is_rhombus(self, *vertices: Iterable) -> bool: """Akkor ad vissza True értéket, ha a megadott pontok egy rombuszt alkotnak. Egy sokszög rombusz, ha négyszög és minden oldala egyenlő. A csúcspontokat az x, y koordináták egymást követő felsorolsával, vagy az x, y párokat szolgáltató iterálható objektumok felsorolásával lehet megadni. """ sides = self.side_lengths(vertices) # Ha négyszög (négy oldala van) és minden oldala egyenlő, akkor rombusz. return len(sides) == 4 and self._all_equal(*sides) class Rectangle(Quadrilateral): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): super().__init__(canvas, *vertex_points, **options) if not self._is_rectangle(vertex_points): raise ValueError('A megadott pontok nem téglalapot határoznak meg.') @classmethod def from_sides(cls, canvas: tk.Canvas, a: int | float, b: int | float, upperleft_x=0, upperleft_y=0, **options): """A téglalapot az a és b oldalainak hosszával lehet megadni. Ekkor egy olyan téglalap jön létre, amelynek a oldala az x, b oldala az y tengellyel párhuzamos. A négyzetet létrehozáskor elhelyezni a bal felső sarkának koordinátáival lehet, amely alapértelmezetten az origó. """ points = ((upperleft_x, upperleft_y), (upperleft_x + a, upperleft_y), (upperleft_x + a, upperleft_y + b), (upperleft_x, upperleft_y + b)) return cls(canvas, *points, **options) def _is_rectangle(self, *vertices: Iterable) -> bool: """Akkor ad vissza True értéket, ha a megadott pontok egy téglalapot alkotnak. Egy sokszög téglalap, ha négyszög és minden szöge egyenlő. Ebből következik, hogy a csúcsok középponttól (sűlyponttól) mért távolsága egyenlő. A csúcspontokat az x, y koordináták egymást követő felsorolsával, vagy az x, y párokat szolgáltató iterálható objektumok felsorolásával lehet megadni. """ # Az ellenörzés módja: négy csúcs van-e, és a csúcsok középponttól (súlyponttól) mért távolsága egyenlő-e. sorted_points = self._sort_vertices_for_proper_plotting(vertices) # Meghatározzuk a csúcspontok középpontját (súlypontját). centroid_x, centroid_y = self.vertices_centroid(vertices) # Ha négyszög és a csúcsok távolsága a középpontól egyenlő, akkor téglalap. distances_to_vertices = (dist((centroid_x, centroid_y), vertex) for vertex in sorted_points) return len(sorted_points) == 4 and self._all_equal(*distances_to_vertices) class Square(Quadrilateral): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): super().__init__(canvas, *vertex_points, **options) try: if not self._is_square(vertex_points): raise ValueError except ValueError: raise ValueError('A megadott pontok nem négyzetet határoznak meg.') @classmethod def from_side(cls, canvas: tk.Canvas, side: int | float, upperleft_x=0, upperleft_y=0, **options): """A négyzetet a side oldalhosszával lehet megadni. Ekkor egy olyan négyzet jön létre, amelynek oldalai az x és y tengelyekkel párhuzamosak. A négyzetet létrehozáskor elhelyezni a bal felső sarkának koordinátáival lehet, amely alapértelmezetten az origó. """ points = ((upperleft_x, upperleft_y), (upperleft_x + side, upperleft_y), (upperleft_x + side, upperleft_y + side), (upperleft_x, upperleft_y + side)) inst = cls(canvas, *points, **options) inst._vertices_coords = points return inst def _is_square(self, *vertices: Iterable) -> bool: """Akkor ad vissza True értéket, ha a megadott pontok egy négyzetet alkotnak. Egy sokszög négyzet, ha négyszög és minden szöge egyenlő és minden oldala egyenlő. Ebből következik, hogy a csúcsok középponttól (sűlyponttól) mért távolsága egyenlő. A csúcspontokat az x, y koordináták egymást követő felsorolsával, vagy az x, y párokat szolgáltató iterálható objektumok felsorolásával lehet megadni. """ sorted_points = self._sort_vertices_for_proper_plotting(vertices) sides = self.side_lengths(vertices) # Meghatározzuk a csúcspontok középpontját (súlypontját). centroid_x, centroid_y = self.vertices_centroid(vertices) # Ha négyszög és a csúcsok távolsága a középpontól egyenlő, akkor téglalap. distances_to_vertices = (dist((centroid_x, centroid_y), vertex) for vertex in sorted_points) # Ha téglalap és minden oldala egyenlő, akkor négyzet. return self._all_equal(*distances_to_vertices) and self._all_equal(*sides) |
Az ellipszis és kör osztályai:
|
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 |
# modul: ellipse_and_circle import tkinter as tk from typing import Self from fundamental_classes import PolygonGraphics class Ellipse(PolygonGraphics): def __init__(self, canvas: tk.Canvas, semi_major_axis: int | float, semi_minor_axis: int | float, center_x: int | float = 0, center_y: int | float = 0, **options): self.semi_major_axis, self.semi_minor_axis = semi_major_axis, semi_minor_axis self._center_point = (center_x, center_y) super().__init__(canvas, **options) @property def center_point(self) -> tuple[int | float, int | float]: """Az elliszis aktuális középpontjának koordinátáit adja vissza.""" # Ha már létrejött a grafika, akkor annak aktuális befoglaló téglalapjából határozzuk meg a középpontot. # Ha még nem jött létre (példányosításkor), akkor a konstruktorban megadott lesz. try: return self.bbox_center() except (TypeError, AttributeError): return self._center_point def _create_graphics(self) -> None: arc_points = (p for p in self.ellipse_arc_points(self.semi_major_axis, self.semi_minor_axis, *self.center_point)) self.canvas.create_polygon(*arc_points, tags=(self.id_tag,)) def _instance_factory(self) -> Self: return type(self)(self.canvas, self.semi_major_axis, self.semi_minor_axis, *self.center_point) class Circle(PolygonGraphics): def __init__(self, canvas: tk.Canvas, radius: int | float, center_x: int | float = 0, center_y: int | float = 0, **options): self.radius = radius self._center_point = (center_x, center_y) self._circle = Ellipse(canvas, radius, radius, center_x, center_y, **options) super().__init__(canvas, **options) @property def center_point(self) -> tuple[int | float, int | float]: """A kör középpontjának koordinátáit adja vissza.""" try: return self.bbox_center() except (TypeError, AttributeError): return self._center_point def _create_graphics(self) -> None: # Az ellipszis grafikához hozzáadjuk a körpéldány azonosítócímkéjét, majd töröljül az ellipszis eredeti azonosítóját. self.canvas.addtag_withtag(self.id_tag, self._circle.id_tag) self.canvas.dtag(self.id_tag, self._circle.id_tag) def _instance_factory(self) -> Self: return type(self)(self.canvas, self.radius, *self.center_point) |
A háromszög osztálydefiníciója:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# modul: triangles from fundamental_classes import PolygonGraphics from typing import Self import tkinter as tk class Triangle(PolygonGraphics): def __init__(self, canvas: tk.Canvas, *vertex_points, **options): self._vertices_coords: tuple = tuple(self._flatten_xycoords(vertex_points)) if not len(self._vertices_coords) == 6: raise ValueError('A megadott pontok nem háromszöget határoznak meg.') super().__init__(canvas, **options) def _create_graphics(self): self.canvas.create_polygon(*self._sort_vertices_for_proper_plotting(self._vertices_coords), tags=(self.id_tag,)) def _instance_factory(self) -> Self: return type(self)(self.canvas, *self.get_coords()) |
És végül egy egyéni tervezésű alakzat osztály definíciója:
|
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 |
# modul: custom_shapes import tkinter as tk from itertools import chain from math import sqrt, acos, degrees from fundamental_classes import PolygonGraphics class ConcaveCircularHypotenuse(PolygonGraphics): def __init__(self, canvas: tk.Canvas, side: int | float, k: int | float = 1, **options): if side < 0: raise ValueError('Az oldalhossz csak pozitív szám lehet.') if k < 1: raise ValueError('A görbület mértékét meghatározó paraméter értéke nem lehet kisebb, mint 1.') self.side = side self._r = side * sqrt(k) super().__init__(canvas, **options) def _create_graphics(self) -> None: cx = cy = 0.5 * (self.side + sqrt(2 * self._r ** 2 - self.side ** 2)) delta = degrees(acos(cx / self._r)) arc_points = [p for p in self.ellipse_arc_points(self._r, self._r, cx, cy, 180 + delta, 270 - delta)] self.canvas.create_polygon(*chain(((0, 0), (0, self.side)), arc_points, ((self.side, 0),)), tags=(self.id_tag,)) def _instance_factory(self): return type(self)(self.canvas, self.side, (self._r / self.side) ** 2) |
A részletes kommentek segítik az osztálydefiníciók megértését. A működéshez Python 12+ verzió szükséges.
Alkalmazási példák
Két alkalmazási példát mutatunk ezen osztályok használatára. Ezek kódja az alábbi
Alkalmazási példa 1: Két, keresztező kard.
|
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 |
import tkinter as tk from fundamental_classes import Group from quadrilaterals import Rectangle, Trapezoid from ellipse_and_circle import Ellipse, Circle from triangles import Triangle from custom_shapes import ConcaveCircularHypotenuse root = tk.Tk() root.title('Sokszögből előállított síkidomok alkalmazása összetett grafika készítéséhez') canvas_width, canvas_height = 600, 600 cnv = tk.Canvas(root, bg='light yellow', width=canvas_width, height=canvas_height) cnv.pack() # A PENGE, A HEGY ÉS HORONY LÉTREHOZÁSA. blade_width, blade_length = 40, 350 # A penge szélessége és hossza. blade_rectangle = Rectangle(cnv, (- blade_width / 2, 0), (+ blade_width / 2, 0), (-blade_width / 2, blade_length * 0.85), (blade_width / 2, blade_length * 0.85), fill='silver', outline='') blade_point = Triangle(cnv, (-blade_width / 2, blade_length * 0.85), (blade_width / 2, blade_length * 0.85), (0, blade_length * 0.85 * 1.15), fill='silver', outline='') blade_fuller = Triangle(cnv, (- blade_width * 0.05, 0), (+ blade_width * 0.05, 0), (0, blade_length * 0.8), fill='gray90', outline='') blade = Group(blade_rectangle, blade_fuller, blade_point) # A KERESZTVAS ÉS SAROKFORMÁINAK LÉTREHOZÁSA. guard_width, guard_height = 120, 15 # A keresztvas szélessége és magassága. # A kézfejet védő keresztvasat egy téglalappal jelenítjük meg. cross_rectangle = Rectangle.from_sides(cnv, guard_width, guard_height, 0, 0, fill='gray50', outline='') # A keresztvas négy sarkára egy-egy íves elemet helyezünk, ami fokozza a védelmet és díszítésként is szolgál. corner_shape1 = ConcaveCircularHypotenuse(cnv, guard_height, fill='gray50', outline='') corner_shape1.move(0, guard_height-1) corner_shape2 = corner_shape1.clone() corner_shape2.reflect(cross_rectangle.bbox_center()) corner_shape3 = corner_shape2.clone() corner_shape3.reflect(cross_rectangle.bbox_center(), (cross_rectangle.bbox_center()[0], cross_rectangle.bbox_center()[1] - 10)) corner_shape4 = corner_shape3.clone() corner_shape4.reflect(cross_rectangle.bbox_center()) # A teljes keresztvas grafikát a keresztvas téglalap és a sarokelemek együtteseként állítjuk elő. cross_guard = Group(cross_rectangle, corner_shape1, corner_shape2, corner_shape3, corner_shape4) # BANDZSOLT MARKOLAT ÉS VÉGSÚLY LÉTREHOZÁS. grip_width, grip_length = blade_width * 0.8, 100 # A markolat szélessége és hossza. # A markolaton körbetekert bandázst elolt ellipszisekkel jelenítjük meg. grip_wrapping_shapes = [Ellipse(cnv, grip_width / 2, grip_width / 4, fill='brown3') for _ in range(20)] for i, grip_wrap in enumerate(grip_wrapping_shapes): grip_wrap.move(0, grip_width / 8 * i) # A markolat nyakát, amely a pengéhez köti trapéz formával ábrázoljuk. grip_neck = Trapezoid(cnv, (-grip_width / 2, 0), (+grip_width / 2, 0), (-blade_width / 2, 10), (+blade_width / 2, 10), fill='black') # A bandázs grafikák és a markolatnyak egy csoportba fogásával előállítjuk a markolat grafikát. grip_wrappings = Group(*grip_wrapping_shapes) grip_neck.move(0, grip_wrappings.bbox()[-1] - grip_width / 4) grip = Group(*grip_wrappings, grip_neck) # A végsúly létrehozása. pommel = Circle(cnv, grip_width / 2, fill='green') # A markolat végén elehelyzkedő végsúlyt egy körrel ábrázoljuk. pommel.move(0, -blade_width / 2) # A végsúlyt a markolat végére helyezzük. grip_and_pommel = Group(*grip, pommel) # A markolat és végsúly együtteséből álló grafika. # A KARDRÉSZEKBŐL A TELJES KARD ÖSSZEÁLLÍTÁSA. grip_and_pommel.move(canvas_width / 2 - grip_and_pommel.bbox_center()[0], canvas_height / 2 - grip_and_pommel.bbox_center()[1]) x1, y1, x2, y2 = grip_and_pommel.bbox() grip_and_pommel.move(0, -(y2 - y1) / 2 - guard_height / 2 + 3) blade.move(canvas_width / 2, canvas_height / 2) cross_guard.move(canvas_width / 2 - cross_guard.bbox_center()[0], canvas_height / 2 - cross_guard.bbox_center()[1]) sword = Group(*blade, *cross_guard, *grip_and_pommel) # AZ ELKÉSZÜLT KARD GRAFIKA KÖZÉPRE HELYEZÉSE, FELNAGYÍTÁSA ÉS ELFORGATÁSA. sword.move(0, canvas_height / 2 - sword.bbox_center()[1]) sword.scale(*sword.bbox_center(), 1.5, 1.5) sword.rotate(45, sword.bbox_center()) # sword2 = sword.clone() sword2.rotate(-90, sword2.bbox_center()) root.mainloop() |
Futtatás után megjelenő kép:

Alkalmazási példa 2: Absztrakt alkotás, ami felülnézetben lehet akár egy park a közepén szökőkúttal, vagy akár egy futópálya.
|
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 |
import tkinter as tk from fundamental_classes import Group from quadrilaterals import Rectangle, Square from ellipse_and_circle import Ellipse, Circle from custom_shapes import ConcaveCircularHypotenuse root = tk.Tk() root.title('Sokszögből előállított síkidomok alkalmazása összetett grafika készítéséhez') cnv = tk.Canvas(root, bg='light yellow', width=800, height=600) cnv.pack() rectangle = Rectangle.from_sides(cnv, 600, 400, 0, 0, fill='light blue') cx, cy = rectangle.bbox_center() group = Group(*(ConcaveCircularHypotenuse(cnv, 150, fill='light green') for _ in range(4))) cch1, cch2, cch3, cch4 = group group.scale(0, 0, 1.5, 1) group.move(35, 35) cch2.reflect(cx, cy) cch3.reflect(cx, cy, cx - 10, cy) cch4.reflect((cx, cy), (cx, cy + 10)) ellipse = Ellipse(cnv, 230, 140, cx, cy, fill='gray90', outline='blue', width=5) square = Square.from_side(cnv, 120, cx - 60, cy - 60, fill='white') circle = Circle(cnv, 6, *ellipse.center_point, fill='red') group.add_graphics(rectangle, ellipse, circle, square) cnv_cx, cnv_cy = cnv.winfo_reqwidth() / 2, cnv.winfo_reqheight() / 2 group.move(cnv_cx - cx, cnv_cy - cy) group.rotate(-5, (cx, cy)) root.mainloop() |
Futtatás utáni kép:

A forráskódok letölthetők a https://github.com/pythontudasepites/polygon_graphics linkről.
A bemutatott programkódok megértéséhez, megírásához vagy továbbfejlesztéséhez a tkinter modul használatával történő grafikus felhasználói felület készítésének ismerete nélkülözhetetlen. Ezzel, illetve ezen belül a Canvas rajzelemekkel, példákkal illusztrálva 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 mellett természetesen számos más ismeret is szükséges, amit az e-könyv szintén tartalmaz, például e bejegyzésben is megjelenő absztrakt osztályok, amelyekkel a könyv „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezeten belül az „Absztrakt osztályok” alfejezet tárgyal részletesen.