Az előző bejegyzésekben fokozatosan fejlesztettük a közlekedési tiltó táblákat rajzoló programunkat, és ott tartunk, hogy ki tudjuk rajzolni a „Mindkét oldalról behajtani tilos” táblát, valamint ezen tábla osztályára alapozva, abból örökölve, egy utódosztályt definiáltunk, amiből elő tudunk állítani különböző sebességkorlátozó táblákat. A létrejövő táblák méretét az egérgörgő forgatásával vagy bizonyos billentyűkombinációkkal változtatni is tudjuk. Láttuk azt is, hogy a szöveges tartalmat megjelenítő sebességkorlátozó táblák méretváltoztatásának megoldása nem magától értetődő feladat, kellett némi előzetes megfontolás a kivitelezés módjára.
A mostani, és e program fejlesztésére vonatkozó utolsó bejegyzésben egy újabb osztályt definiálunk, amivel olyan közlekedési tiltó táblákat jeleníthetünk meg, és méretezhetünk át, amelyek közepén valamilyen piktogram, vagyis képi ábrázolás szerepel.
Ennek megoldását hasonló megközelítéssel fogjuk végezni, mint azt tettük előzőleg, a szöveget (számot) tartalmazó táblák esetében:
1) Definiálunk egy új, PictoProhibitorySign osztályt a ProhibitorySign osztály alosztályaként. Ezzel biztosítjuk, hogy a tábla körformája a piros szegéllyel rendelkezésre álljon, valamint, hogy a körtábla (a külső és belső körök és a befoglaló vászon) méretét változtatni tudjuk.
2) Beszerezzük a középen megjeleníteni kívánt piktogram képét. (Vagy, ha van hozzá tehetségünk, akár magunk is megrajzolhatjuk egy megfelelő képszerkesztővel)
3) Az osztály __init__ metódusában létrehozzuk a képobjektumot a tkinter PhotoImage osztályának használatával.
4) Ugyanitt, a vászon példányra (ami most a self) meghívott create_image() metódussal létrehozzuk, és középre helyezve megjelenítjük a vásznon a kép rajzelemet.
5) És végül, biztosítjuk azt, hogy a piktogram mérete a tábla aktuális méretéhez automatikusan igazodjon, ami azt jelenti, hogy egy előre meghatározott határoló négyzet oldalait nem lépheti túl. Ehhez a külön definiált _fit_image_size() metódust hívjuk meg.
6) Ugyanezt az automatikus méretkövetést biztosítjuk akkor is, ha valamilyen eseményre a tábla átméretezésének kezdeményezése történik. Ehhez felülírjuk a set_radius() metódust, amelyben hasonló lépéseket teszünk, mint a szöveges tartalom esetén tettünk ugyanezen metódusban: a) megváltoztatjuk a szülőosztály köreinek méretét. b) a képet az új középpontba helyezzük, és végül c) a képet az aktuális táblamérethez igazítjuk, meghívva a _fit_image_size() metódust.
A fentieket követhetjük nyomon a PictoProhibitorySign osztály __init__ és set_radius() metódusaiban.
|
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 |
class PictoProhibitorySign(ProhibitorySign): def __init__(self, master, image_filepath, size: int = None, name=''): super().__init__(master, size=size) self.name = name # A megadott útvonalon található képfájból képobjektumot hozunk létre és tábla közepén helyezünk el. # Utána a méretét a tábla aktuális méretéhez igazítjuk. self.img = tk.PhotoImage(file=image_filepath) self.create_image(*self._ck, image=self.img, tags='piktogram') self._fit_image_size() def set_radius(self, r): # A szülőosztály körei méretének változtatása. super().set_radius(r) # A képet az új középpontba helyezzük. self.coords('piktogram', *self._ck) # A képet az aktuális táblamérethez igazítjuk. self._fit_image_size() def _fit_image_size(self): # Az előírt határoló négyzet és a kép befoglaló négyzetének aktuális koordinátáinak lekérdezése. k2x1, k2y1, k2x2, k2y2 = self._bbox() x1, y1, x2, y2 = self.bbox('piktogram') # A túllógási feltétel meghatározása. túllóg_a_kép = x1 < k2x1 or y1 < k2y1 or x2 > k2x2 or y2 > k2y2 # Ha az előírt határoló négyzeten túllóg a kép, akkor meghívjuk a méretcsökkentő metódust. # Ha pedig nem lóg túl, akkor a méretnövelő metódussal a lehető legnagyobb méretűre próbáljuk nagyítani. if túllóg_a_kép: self._reduce_size_to_fit() else: self._increase_size_up_to_boundary() def _bbox(self): """Visszaadja azon belső négyszög koordinátáit, amelyből a piktogram nem lóghat ki""" # Belső kör befoglaló négyzetének koordinátái k2x1, k2y1, k2x2, k2y2 = self.bbox(self._circle2) d = 0.10 * (k2x2 - k2x1) # reduklás mértéke return k2x1 + d, k2y1 + d, k2x2 - d, k2y2 - d def _reduce_size_to_fit(self): """Ha a képobjektum mérete kilóg az előírt határoló négyzetből, akkor a képmértét csökkenti mindaddig, amíg bele nem fér""" # Az előírt határoló négyzet aktuális koordinátáinak lekérdezése. k2x1, k2y1, k2x2, k2y2 = self._bbox() while True: # Adott arányszámmal csökkentjük a képobjektum méretét, majd a megjelenítést ennek megfelelően aktualizáljuk. self.img = self._image_resize(0.95) self.itemconfig('piktogram', image=self.img) # Lekérdezzük a módosított kép befoglaló négyzetének koordinátáit, és ellenőrizzük, hogy túllóg-e még. # Ha még most is túllóg, akkor folytatjuk a csökkentést. Ha már nem, akkor célt értünk, és kilépünk. x1, y1, x2, y2 = self.bbox('piktogram') if not (x1 < k2x1 or y1 < k2y1 or x2 > k2x2 or y2 > k2y2): break def _increase_size_up_to_boundary(self): """Ha a képobjektum befoglaló négyzete benne van az előírt határoló négyzetből, akkor a képméretet mindaddig növeljük, amíg a lehető legnagyobb mértékben ki nem tölti azt.""" # Az előírt határoló négyzet aktuális koordinátáinak lekérdezése. k2x1, k2y1, k2x2, k2y2 = self._bbox() while True: # Adott arányszámmal növeljük a képobjektum méretét, majd a megjelenítést ennek megfelelően aktualizáljuk. self.img = self._image_resize(1.05) self.itemconfig('piktogram', image=self.img) # Lekérdezzük a módosított kép befoglaló négyzetének koordinátáit, és ellenőrizzük, hogy túllóg-e még. x1, y1, x2, y2 = self.bbox('piktogram') # Ha az aktuális méretnöveléssel túllógna a kép, akkor visszacsökkentjük, majd kilépünk. if x1 < k2x1 or y1 < k2y1 or x2 > k2x2 or y2 > k2y2: self._reduce_size_to_fit() break def _image_resize(self, ratio): """Az inicializáláskor létrehozott képobjektum méretét módosítja, és a módosított képobjektummal tér vissza.""" q = Fraction(ratio).limit_denominator(1000) img2 = self.img.zoom(q.numerator) img3 = img2.subsample(q.denominator) return img3 |
A kérdés, hogy hogyan tudjuk megvalósítani a kép átméretezését.
Ennek alaplogikáját a _fit_image_size() metódus tartalmazza. Itt első lépésben lekérdezzük az előírt határoló négyzet és a kép befoglaló négyzetének aktuális koordinátáit. Ezek alapján megvizsgáljuk, hogy a kép kilóg-e az előírt határokból. Ha igen, akkor meghívunk egy segédmetódust _reduce_size_to_fit() néven, amely iteratívan addig csökkenti a kép méretét, ameddig az már teljesen a határokon belül lesz. Ha nincs túllógás, akkor sem vagyunk kész, mert lehet, hogy a kép mérete túl kicsi a tábla méretéhez képest. Ezért ilyenkor egy másik, _increase_size_up_to_boundary() nevű segédmetódust hívunk meg, amely szintén iteratívan megpróbálja a képméretet addig növelni, míg épp belefér az előírt határokba. Ezek definícióit is láthatjuk a fenti kódban. A működés megértését a kommentek segítik.
Egyetlen kérdés marad hátra, mégpedig az, hogy hogyan lehet egy képet nagyítani vagy kicsinyíteni. A tkinter modul nem rendelkezik egyetlen, erre alkalmas átméretező metódussal, hanem a nagyításhoz a képobjektumra meg kell hívni a zoom() metódust, a kicsinyítéshez pedig a subsample() metódust. Mindkettő két, x és y paramétert fogad, és egy PhotoImage példányt ad vissza ugyanazzal a képi tartalommal, mint az eredeti, amire meg lettek hívva. Míg a zoom() az X tengely irányban x, és az Y irányban y faktorral nagyít, addig a subsample() x és y mértékben kicsinyít. Ha y nincs megadva, az alapértelmezett érték megegyezik az x értékével. E metódusok korlátja, hogy az argumentumok csak egész számok lehetnek. Ezért ahhoz, hogy tört értékkel is lehessen méretet változtatni azt a „trükköt” kell megtenni, hogy a törtnek képezzük a számlálóját és nevezőjét, majd a számláló mértékvel növeljük a méretet, utána pedig a nevezővel csökkentjük. Ezt láthatjuk az ábrán az _image_resize() metódusban.
Miután a fentiek szerint minden szükséges metódus definiált, így tesztelhetjük a művünket. Ehhez a teljes program kódja az alábbi:
|
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 |
import tkinter as tk from collections import namedtuple import tkinter.font as ft from fractions import Fraction # A koordinátákkal való munka megkönnyítésére egy Point típust hozunk létre. Point = namedtuple('Point', 'x y') class ProhibitorySign(tk.Canvas): def __init__(self, master, size: int = None): super().__init__(master, bg='gray90') self.name = 'Mindkét irányból behajtani tilos' self.addtag_all('átméretezhető') self.addtag_all('áthelyezhető') # Ha nem adjuk meg létrehozáskor a vászonpéldány oldalméretét, akkor a szülőelem méretéhez igazítjuk azt. if size is None: # A helyes méretek lekérdezéséhez előbb frissíteni kell az ablakot. master.update() m_w, m_h = master.winfo_width(), master.winfo_height() size = min(int(m_h), int(m_w)) self.config(height=size, width=size) # A körök közös középpontja, amelynek koordinátái a vászon aktuális oldalméretének felével egyezik meg. cc = Point(size / 2, size / 2) # Külső kör sugara, amely a vászon oldalméretének fele. r1 = size / 2 # A számítási pontosság korlátai miatt a kör széle nem mindig látszana, ezért egy egész kicsit csökkentjük a sugarat. r1 -= 2 # Beállítjuk a belső és a külső kör sugarának arányát, ami meghatározza, hogy milyen széles legyen a tiltó tábla piros szegélye. r2 = r1 * 0.8 # A későbbi használathoz a középpontot és sugarakat attribútumként tároljuk. self._ck, self._r1, self._r2 = cc, r1, r2 # A külső kör befoglaló négyzetének pontjai a középponttal és sugárral meghatározva. k1p1, k1p2 = Point(cc.x - r1, cc.y - r1), Point(cc.x + r1, cc.y + r1) # A külső kör létrehozása a befoglaló négyzet pontjait használva. # A külső kört piros színnel töltjük ki. A kisebb belső kör pedig fehér lesz. Így alakul ki majd a piros szegély. self._circle1 = self.create_oval(*k1p1, *k1p2, fill='red') # A belső kör befoglaló négyzetének pontjai a középponttal és sugárral meghatározva. k2p1, k2p2 = Point(cc.x - r2, cc.y - r2), Point(cc.x + r2, cc.y + r2) # A belső kör létrehozása a befoglaló négyzet pontjait használva. self._circle2 = self.create_oval(*k2p1, *k2p2, fill='white') self.bind_events_and_event_handlers() def bind_events_and_event_handlers(self): """Események és eseménykezelők összerendelése""" self.bind('<Control MouseWheel>', self.resizing_mousewheel_handler) self.bind('<Shift MouseWheel>', lambda e: self.resizing_mousewheel_handler(e, 0.1)) self.bind('<Button 1>', lambda e: self.focus_set()) self.bind('<Control plus>', lambda e: self.resizing_event_handler(e, 0.1)) self.bind('<Control minus>', lambda e: self.resizing_event_handler(e, 0.1, False)) def resizing_event_handler(self, e: tk.Event, g=0.01, increase=True): if increase: self.set_radius(self.get_radius() * (1 + g)) else: self.set_radius(self.get_radius() * (1 - g)) def resizing_mousewheel_handler(self, e: tk.Event, g=0.01): self.set_radius(self.get_radius() * (1 - g) if getattr(e, 'delta') < 0 else self.get_radius() * (1 + g)) def get_radius(self): return self._r1 def set_radius(self, r): scalefactor_x = scalefactor_y = r / self._r1 old_ck = self._ck self.config(width=int(self.cget('width')) * scalefactor_x, height=int(self.cget('height')) * scalefactor_y) self._ck = Point(int(self.cget('width')) / 2, int(self.cget('height')) / 2) new_ck = self._ck self.move(self._circle1, new_ck.x - old_ck.x, new_ck.y - old_ck.y) self.move(self._circle2, new_ck.x - old_ck.x, new_ck.y - old_ck.y) self.scale(self._circle1, *self._ck, scalefactor_x, scalefactor_y) self.scale(self._circle2, *self._ck, scalefactor_x, scalefactor_y) class SpeedLimitSign(ProhibitorySign): def __init__(self, master, speed_limit, size: int = None): super().__init__(master, size=size) self.speed_limit = speed_limit self.name = 'Sebességkorlátozás' # A tábla közepén megjelenítendő szöveg betűtípusának definiálása. self.font_type = ft.Font(family='Noto Sans Cond', size=0, weight="bold") self.txt = self.create_text(*self._ck, text=str(self.speed_limit), font=self.font_type) # A szöveget az aktuális táblamérethez igazítjuk. self._fit_text_size() def _fit_text_size(self): # Belső kör befoglaló négyzetének koordinátái. k2x1, k2y1, k2x2, k2y2 = self.bbox(self._circle2) # Csökkentett meretű négyzet koordináták előállítása. d = 0.08 * (k2x2 - k2x1) k2x1, k2y1, k2x2, k2y2 = k2x1 + d, k2y1 + d, k2x2 - d, k2y2 - d font_size = 0 self.font_type.config(size=font_size) self.itemconfig(self.txt, font=self.font_type) while True: # Előállítjuk a szöveg rajzelem befoglaló négyzetének koordinátáit. x1, y1, x2, y2 = self.bbox(self.txt) # Ha ez túllóg a belső kör redukált befoglaló négyzetén, akkor elértük a megfelelő méretet, ezért kilépünk. if x1 < k2x1 or y1 < k2y1: break # Ha a szöveg mérete nem lép túl a belső kör redukált befoglaló négyzetén, akkor eggyel növeljük # a betűméretet, és ezzel újrakonfiguráljuk a betűtípust, és azzal pedig a szöveg rajzelemet. font_size += 1 self.font_type.config(size=font_size) self.itemconfig(self.txt, font=self.font_type) def set_radius(self, r): # A szülőosztály körei méretének változtatása. super().set_radius(r) # A szöveget az új középpontba helyezzük self.coords(self.txt, *self._ck) # A szöveget az aktuális táblamérethez igazítjuk. self._fit_text_size() class PictoProhibitorySign(ProhibitorySign): def __init__(self, master, image_filepath, size: int = None, name=''): super().__init__(master, size=size) self.name = name # A megadott útvonalon található képfájból képobjektumot hozunk létre és tábla közepén helyezünk el. # Utána a méretét a tábla aktuális méretéhez igazítjuk. self.img = tk.PhotoImage(file=image_filepath) self.create_image(*self._ck, image=self.img, tags='piktogram') self._fit_image_size() def set_radius(self, r): # A szülőosztály körei méretének változtatása. super().set_radius(r) # A képet az új középpontba helyezzük. self.coords('piktogram', *self._ck) # A képet az aktuális táblamérethez igazítjuk. self._fit_image_size() def _fit_image_size(self): # Az előírt határoló négyzet és a kép befoglaló négyzetének aktuális koordinátáinak lekérdezése. k2x1, k2y1, k2x2, k2y2 = self._bbox() x1, y1, x2, y2 = self.bbox('piktogram') # A túllógási feltétel meghatározása. túllóg_a_kép = x1 < k2x1 or y1 < k2y1 or x2 > k2x2 or y2 > k2y2 # Ha az előírt határoló négyzeten túllóg a kép, akkor meghívjuk a méretcsökkentő metódust. # Ha pedig nem lóg túl, akkor a méretnövelő metódussal a lehető legnagyobb méretűre próbáljuk nagyítani. if túllóg_a_kép: self._reduce_size_to_fit() else: self._increase_size_up_to_boundary() def _bbox(self): """Visszaadja azon belső négyszög koordinátáit, amelyből a piktogram nem lóghat ki""" # Belső kör befoglaló négyzetének koordinátái k2x1, k2y1, k2x2, k2y2 = self.bbox(self._circle2) d = 0.10 * (k2x2 - k2x1) # reduklás mértéke return k2x1 + d, k2y1 + d, k2x2 - d, k2y2 - d def _reduce_size_to_fit(self): """Ha a képobjektum mérete kilóg az előírt határoló négyzetből, akkor a képmértét csökkenti mindaddig, amíg bele nem fér""" # Az előírt határoló négyzet aktuális koordinátáinak lekérdezése. k2x1, k2y1, k2x2, k2y2 = self._bbox() while True: # Adott arányszámmal csökkentjük a képobjektum méretét, majd a megjelenítést ennek megfelelően aktualizáljuk. self.img = self._image_resize(0.95) self.itemconfig('piktogram', image=self.img) # Lekérdezzük a módosított kép befoglaló négyzetének koordinátáit, és ellenőrizzük, hogy túllóg-e még. # Ha még most is túllóg, akkor folytatjuk a csökkentést. Ha már nem, akkor célt értünk, és kilépünk. x1, y1, x2, y2 = self.bbox('piktogram') if not (x1 < k2x1 or y1 < k2y1 or x2 > k2x2 or y2 > k2y2): break def _increase_size_up_to_boundary(self): """Ha a képobjektum befoglaló négyzete benne van az előírt határoló négyzetből, akkor a képméretet mindaddig növeljük, amíg a lehető legnagyobb mértékben ki nem tölti azt.""" # Az előírt határoló négyzet aktuális koordinátáinak lekérdezése. k2x1, k2y1, k2x2, k2y2 = self._bbox() while True: # Adott arányszámmal növeljük a képobjektum méretét, majd a megjelenítést ennek megfelelően aktualizáljuk. self.img = self._image_resize(1.05) self.itemconfig('piktogram', image=self.img) # Lekérdezzük a módosított kép befoglaló négyzetének koordinátáit, és ellenőrizzük, hogy túllóg-e még. x1, y1, x2, y2 = self.bbox('piktogram') # Ha az aktuális méretnöveléssel túllógna a kép, akkor visszacsökkentjük, majd kilépünk. if x1 < k2x1 or y1 < k2y1 or x2 > k2x2 or y2 > k2y2: self._reduce_size_to_fit() break def _image_resize(self, ratio): """Az inicializáláskor létrehozott képobjektum méretét módosítja, és a módosított képobjektummal tér vissza.""" q = Fraction(ratio).limit_denominator(1000) img2 = self.img.zoom(q.numerator) img3 = img2.subsample(q.denominator) return img3 # TESZT root = tk.Tk() root.geometry('500x300') for sk in (ProhibitorySign(root, 150), SpeedLimitSign(root, 90, 150), PictoProhibitorySign(root, r'D:\PYTHON\előzni_tilos_piktogram.png', 150, 'Előzni tilos')): sk.pack(side=tk.LEFT) root.mainloop() |
A teszteléshez három, azonos méretű Kresz táblát hozunk létre: egy „Mindkét oldalról behajtani tilos” táblát a ProhibitorySign osztályból, egy 90 km/hó sebességhatárt megadó táblát a SpeedLimitSign osztályból, és végül egy „Előzni tilos” táblát a most készített, új PictoProhibitorySign osztályból. A futtatás után megjelenő azonos méretű táblákat az egérgörgővel, valamint a Ctrl+ és Ctrl- billentyűkombinációkkal egyenként nagyítottuk vagy kicsinyítettük. Ezek egy-egy változatát mutatják az eredményképek:

A program tehát működik.
Mindazonáltal, a képméretezésre alkalmazott megoldásunknak több korlátja van. Az egyik, hogy a tkinter modul PhotoImage osztálya csak kevés képformátumot fogad el; gyakorlatban csak png és gif jön szóba. Ennél talán még lényegesebb, hogy a subsample() a kicsinyítést úgy végzi, hogy veszi az argumentumában megadott x, y értékeket és csak minden x-edik és y-adik képpontot használja a kicsinyített kép előállításához. Ez információvesztéssel jár, aminek következménye, hogy ha a képet sokszor oda-vissza növeljük és csökkentjük, akkor a kép torzul, sőt a képtartalom egy idő után el is tűnhet.
Ezért ilyen dinamikus képméretezésre a tkinter eszköztára nem ideális, célszerű más, képfeldolgozásra specializált csomagokat használni.
Gyakorlásképpen meg lehet próbálni e programot alkalmassá tenni arra, hogy a „Behajtani tilos” tábla (piros szín, vékony fehér szegély középen fehér téglalap) előállítására is képes legyen.
Bár e közlekedési tábla rajzoló program fejlesztése mint feladat látszólag nem néz ki túl bonyolultnak, mégis talán érzékelhető volt a négy bejegyzésben szereplő egy-egy fejlesztési fázisban, hogy még ilyen esetben is mennyi nyelvi eszközt és ismeretet kellett használni. Az egyes fázisoknál jeleztük is a főbb témákat.
Most összegezve, e feladat megoldásához az alábbi témákban érdemes elmélyedni, amelyek a Python tudásépítés lépésről lépésre című e-könyvben részletesen ki vannak fejtve az alábbi részekben: Az „Osztály vigyázz! – típuslétrehozás osztályokkal”, és „Öröklődés” fejezetek. A „Matematikai számítások támogatása” fejezet „Közönséges törtekkel való számolás” alfejezet. A „Grafikus felhasználói felület készítése” fejezet „Események és programkód összerendelése” alfejezete, valamint „A grafikus elemek fajtái, létrehozásuk és konfigurálásuk” alfejezete, és ebben a „Vászon” alcím, amin belül különösen a szöveg és kép rajzelemek létrehozás és konfigurálása.