A tkinter modul vászon (Canvas) grafikus elemén a különféle típusú rajzelemekkel (ellipszis, ellipszisív, sokszög, téglalap, vonal) akár meglehetősen összetett grafikákat is tudunk alkotni. A rajzelemek létrehozása általában a vászonelemre meghívott konstruktormetódusokkal történik. De lehetséges az is, hogy egy rajzszerkesztő programot készítünk és/vagy használunk, ahol a rajzelemeket a program által biztosított módon helyezzük a vászonra és vizuálisan rendezzük el egér- vagy billentyűműveletekkel.
A kérdés az, hogy hogyan őrizhetnénk meg az alkotásunk eredményét egy fájlban úgy, hogy később a fájlból a grafika megjeleníthető legyen? És mindezt lehetőleg úgy, hogy nem támaszkodunk a szabványos könyvtáron túlmutató külső csomagokra vagy alkalmazásokra.
Nyilván egy lehetséges út a képernyőképmentés (printscreen). Ez azonban rasztergrafikát eredményez, aminek meg vannak a maga hátrányos jellemzői, pl. rögzített felbontás, nagyításnál minőségromlás, és formátumtól és képmérettől függő képfájlméret. Ráadásul a tkinter PhotoImage objektumot csak meglehetősen korlátozott számú képformátum esetén tudjuk használni.
E helyett, mivel a megjelenített grafikához a vásznon rendelkezésre állnak a rajzelemek, vektorgrafikusan fogjuk leírni a képet. Ennek elvét olvashatjuk ezen az ábrán:

Az elvet programban a következőképpen valósítjuk meg.
Az egyes rajzelemek adatait egy szótárban gyűjtjük össze, amelyben a kulcsok a rajzelemek egyedi azonosítói, a kulcsokhoz tartozó értékek pedig sorozat típusú konténerek, amelyek elemei a rajzelem típusa, a koordináták sorozata, valamint egy, a konfigurációs paraméternév-érték párokat tartalmazó szótár.
Ezen adatok mindegyike egyszerűen kinyerhető a vászon elemre meghívott megfelelő metódussal. A típust a type(), a koordinátákat a coords(), a konfigurációt tartalmazó szótárt a itemconfig() metódus hívásával kaphatjuk meg.
A rajzelemek adatait tartalmazó szótárt fájlba mentjük, mégpedig JSON formátumban, mert így a mentést és majd a visszaolvasást egyszerűen el tudjuk végezni.
Ahhoz, hogy tudjuk, hogy a fájl mit tartalmaz, legyen a fájl kiterjesztése .tcg, ami a tkinter canvas grafika kezdőbetűiből lett képezve.
A canvas grafikák ilyen fájlokba történő mentését teszi lehetővé az alább látható TcgFileMaker osztály.
|
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 |
# modul: tcg.py import tkinter as tk from pathlib import Path import json from collections.abc import Callable, Iterable from typing import Any import sys assert sys.version_info[:2] >= (3, 10) # A működéshez Python 3.10+ verzió szükséges. class TcgFileMaker: """Az osztály példánya egy .tcg kiterjesztésű, tkinter canvas grafikát leíró fájlt készít a generate_tcg_file_from_factory() vagy generate_tcg_file_from_canvas() metódusok meghívásával. Az előbbit akkor kell meghívni, ha a grafika egy grafikaelőállító függvényben van definiálva. Az utóbbi metódust pedig akkor, ha a grafika egy vászon elemen van létrehozva és megjelenítve. """ def __init__(self, master): super().__init__() self.canvas = tk.Canvas(master, width=master.winfo_screenwidth() / 2, height=master.winfo_screenheight() / 2) def _write_itemconfigs(self, filename: str | Path, canvas: tk.Canvas = None): """Az aktuális vászon elemen létrehozott grafika rajzelemeinek adatait (típus, koordinták és konfigurációs paraméterek értékei) JSON formátumban fájlba menti a megadott fájlnévvel. """ # Ha a canvas argumentum meg van gadva, akkor azt, egyébként a példány saját canvas objektumát vesszük. _canvas = canvas if canvas is not None else self.canvas # A fill_transparent és outline_transparent tag-ek kivételével mindent törlünk az elmentendő rajzelemekről, hogy # későbbi felhasználásnál ne lehessen azonosító-tag egyezés más grafikákkal. for oid in _canvas.find_all(): tags_string = _canvas.itemcget(oid, 'tags') tags_set = set(tags_string.split()) _canvas.itemconfig(oid, tags=tuple({'outline_transparent', 'fill_transparent'} & tags_set)) # A grafikát alkotó rajzelemek elmentendő adatait egy szótárban gyűjtjük össze, amelynek kulcsai a rajzelemazonosítók. # A kulcshoz tartozó érték egy háromelemű tuple, amelyben az elemek tartalma sorrendben: # - a rajzelem típusa ('rectangle', 'oval', arc' stb), # - a rajzelem koordinátáit tartalmazó lista, # - a rajzelem konfigurációs paramétereinek aktuális értékét tartalmazó szótár. canvas_items_data_to_be_saved: dict = {oid: (_canvas.type(oid), _canvas.coords(oid), {option_name: _canvas.itemcget(oid, option_name) for option_name in _canvas.itemconfig(oid)} ) for oid in _canvas.find_all() } # Az adatokat tartalmazó szótárt JSON formátummal fájlba mentjük. json.dump(canvas_items_data_to_be_saved, open(Path(filename), "w", encoding='UTF8'), indent=4) def generate_tcg_file_from_factory(self, filename: str | Path, canvas_graphics_factory_function: Callable[[tk.Canvas], Any]): """Az inicializáláskor létrejövő canvas elemen előállítja a grafikát, meghívva a megadott canvas_graphics_factory_function grafikaelőállító függvényt. E függvény egyetlen, kötelezően megadandó pozícionális argumentumot fogad, egy vászon elemet, amelyen a grafikát az összetevő rajzelemek (téglalap, sokszög, ellipszis, ellipszisív és vonal) létrehozásával valósítja meg. Ezt követően a grafikát alkotó rajzelemek adatai a megadott nevű fájlba .tcg kiterjesztéssel el lesznek mentve. """ canvas_graphics_factory_function(self.canvas) filepath = Path(filename).with_suffix('.tcg') self._write_itemconfigs(filepath) self.canvas.delete('all') def generate_tcg_file_from_canvas(self, filename: str | Path, canvas: tk.Canvas): """A megadott canvas elemen meglévő grafika rajzelemeinek adatait a megadott nevű fájlba .tcg kiterjesztéssel elmenti. """ filepath = Path(filename).with_suffix('.tcg') self._write_itemconfigs(filepath, canvas) |
A TcgFileMaker példánya egy .tcg kiterjesztésű, tkinter canvas grafikát leíró fájlt készít a generate_tcg_file_from_factory() vagy a generate_tcg_file_from_canvas() metódusok meghívásával. Az előbbit akkor kell használni, ha a grafika egy grafika-előállító függvényben van definiálva. Az utóbbi metódust pedig akkor, ha a grafika egy vászon elemen van létrehozva és megjelenítve.
A grafika-előállító függvény egy olyan függvény, amely egyetlen, kötelezően megadandó pozícionális argumentumot fogad, mégpedig egy vászon elemet, amelyen a grafika az összetevő rajzelemek (téglalap, sokszög, ellipszis, ellipszisív és vonal) létrehozásával és megfelelő konfigurációjával valósul meg a függvény törzsében szereplő utasításokkal. Ilyen grafika-előállító függvényekre láthatunk alább példát.
|
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 |
# modul: tcg_factories1.py import tkinter as tk from typing import NamedTuple import math, cmath # A grafikaelőállító függvények a következő jellemzőkkel bírnak: # - csak egyetlen kötelezően megadandó pozícionális argumentumot fogadnak, ami a Canvas példány, # - az átlátszóvá tenni kívánt rajzelemhez a 'fill_transparent' tag társul. # - az aktuális canvas háttérszínnel egyező körvonal szín eléréséhez a rajzelemhez az 'outline_transparent' tag van hozzáadva. # - nevük create_ kezdetű, # - visszatérési értékük egy karakterlánc, amely a grafika összes rajzeleméhez tag-ként hozzá van rendelve. class Point(NamedTuple): x: int | float y: int | float def create_yin_yang(canvas: tk.Canvas) -> str: cnv_width, cnv_height = canvas.winfo_reqwidth(), canvas.winfo_reqheight() cnv_center = Point(cnv_width / 2, cnv_height / 2) r = min(cnv_width, cnv_height) / 3 # Nagy félkörök. canvas.create_arc(- r, - r, + r, + r, start=270, extent=180, fill='black', outline='black', tags='yin_yang') canvas.create_arc(- r, - r, + r, + r, start=90, extent=180, fill='white', outline='white', tags='yin_yang') # Kis félkörök. canvas.create_arc(- r / 2, 0, + r / 2, + r, start=270, extent=+180, fill='white', width=1, outline='white', tags='yin_yang') canvas.create_arc(- r / 2, 0, + r / 2, - r, start=90, extent=+180, fill='black', width=1, outline='black', tags='yin_yang') # Kis körök. dx = dy = r / 8 canvas.create_oval(- dx, + r / 2 - dy, + dx, + r / 2 + dy, fill='black', outline='black', tags='yin_yang') canvas.create_oval(- dx, - r / 2 - dy, + dx, - r / 2 + dy, fill='white', outline='white', tags='yin_yang') canvas.move('yin_yang', *cnv_center) return 'yin_yang' def create_ankh(canvas: tk.Canvas) -> str: cnv_width, cnv_height = canvas.winfo_reqwidth(), canvas.winfo_reqheight() cnv_center = Point(cnv_width / 2, cnv_height / 2) canvas.create_polygon((0, -40), (25, 180), (-25, +180), tags='ankh') canvas.create_polygon((+20, 0), (-80, 25), (-80, -25), tags='ankh') canvas.create_polygon((-20, 0), (+80, 25), (+80, -25), tags='ankh') canvas.create_oval(-40, -40 - 65, +40, +40 - 65, fill='black', tags='ankh') canvas.create_arc(-50, -50, +50, +50, start=52, extent=76, fill='black', tags='ankh') canvas.create_oval(-40, -40 - 65, +40, +40 - 65, fill=canvas.cget('bg'), width=0, tags=('inner', 'fill_transparent', 'outline_transparent', 'ankh')) canvas.create_arc(-50, -50, +50, +50, start=52, extent=76, fill=canvas.cget('bg'), width=0, outline=canvas.cget('bg'), tags=('inner', 'fill_transparent', 'outline_transparent', 'ankh')) canvas.scale('inner', 0, 0, 0.7, 0.7) canvas.move('inner', 0, -20) canvas.move('ankh', *cnv_center) canvas.scale('ankh', *cnv_center, 2, 2) canvas.dtag('ankh', 'inner') return 'ankh' |
Ezekhez nem fűzünk kommentet, mert most nem ezek kialakításának magyarázata a cél; a kívánt grafika rajzelemekből való felépítésének fejben vagy papíron való megtervezését követően a megvalósításhoz csak középszintű koordinátageometriai ismeretekre van szükség. Azt viszont megfigyelhetjük, hogy ha átlátszóság érzetét akarjuk kelteni, vagyis a grafika adott rajzelemének kitöltő színe a háttérszínnel egyezzen meg, akkor egyrészt a fill konfigurációs paraméterhez a vászon aktuális kitöltőszínét kell rendelni, másrészt, ahhoz hogy később a fájlba mentésnél a transzparenciára vonatkozó információ is rögzüljön, az adott rajzelemhez a „fill_transparent” tag-et kell társítani. Hasonlóan kell eljárni, ha a rajzelem körvonalát az aktuális háttérszínnel egyezőnek akarjuk, csak ilyenkor a rajzelemhez az „outline_transparent” tag-et kell adni.
A TcgFileMaker példányosításakor létrejön egy Canvas példány, amelynek szülőelemét a konstruktorban kell megadni. Ez legtöbbször a gyökérelem (főablak), de lehet más is, például egy keret (Frame) elem.
Ha a TcgFileMaker példánnyal létrehoztunk egy vagy több .tcg fájlt, akkor a következő kérdés, hogy hogyan tudjuk ezeket használni a GUI programunkban. Erre szolgál a Tcg osztály, amelynek definíciója ez:
|
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 |
# modul: tcg.py class Tcg: """Az osztály példánya az inicializáláskor megadott fájl által definiált tkinter canvas grafikát állítja elő és jeleníti meg a megadott vászon elemen, amikor a render() metódus meghívásra kerül. Ezt követően a grafika áthelyezhető és átméretezhető a példányon meghívott megfelelő metódusokkal. A példány metódusainak meghívása helyett a Canvas metódusaival is megvalósíthatók a műveletek, amelyekhez a példány id_tag azonosító tag-e használható a metódus által elvárt tag_or_id argumentumként. Ha grafika előállításakor egy rajzelemen a 'fill_transparent' tag található, akkor az adott rajzelem kitöltőszíne a vászon aktuális háttérszínével fog megegyezni, az átlátszóság érzetét keltve. Ha a grafika előállításakor a rajzelemen az 'outline_transparent' tag található, akkor az adott rajzelem körvonalának színe a vászon aktuális háttérszínével fog megegyezni. """ def __init__(self, canvas: tk.Canvas, tcg_filepath: str | Path): self.canvas = canvas # Az a Canvas példány, amelyen a grafikát megjelenítjük. self.id_tag = Path(tcg_filepath).stem + str(id(self)) # A grafika egyedi azonosító tag-e. self._filepath = str(tcg_filepath) # A grafikát leíró adatokat tartalmazó fájl elérési útvonala. # A grafikát leíró JSON fájlból az adatok szótárba olvasása. self._graphics_definitions: dict = json.load(open(Path(tcg_filepath), "r", encoding='UTF8')) def __str__(self) -> str: return f'{type(self).__name__} object | obj id = {hex(id(self))} | id tag = "{self.id_tag}"' def _create_canvas_item(self, item_data: tuple[str, tuple | list, dict], id_tag: str) -> None: """A példány canvas elemén létrehoz egy, az item_data argumentumban foglalt adatokkal jellemzett rajzelemet, és ehhez az id_tag tag-et adja hozzá. Az item_data egy olyan tuple, amelynek elemei sorrendben: - a rajzelem típusa ('rectangle', 'oval', arc', 'line', 'polygon'), - a rajzelem koordinátáit tartalmazó tuple vagy lista, - a rajzelem konfigurációs paramétereinek aktuális értékét tartalmazó szótár. """ # Ellenőrizzük, hogy az item_data megfelel a követelményeknek. match item_data: case ['arc' | 'oval' | 'rectangle' | 'line' | 'polygon' as item_type, [*coords], dict() as configs]: pass case _: raise ValueError('A rajzelemleíró szekvencia nem megfelelő') # A példány canvas elemén az adott koornitákkal létrehozzuk a megfelelő típusú rajzelemet, kihasználva, hogy a # Canvas rajzelem-létrehozó metódusainak neve egységesen 'create_' + típusnév felépítésűek. oid = getattr(self.canvas, 'create_' + item_type)(*coords) # A kapott egyedi rajzelem azonosítót felhasználva konfiguráljuk a rajzelemet az item_data harmadik eleme szerint. self.canvas.itemconfig(oid, **configs) # Ha a konfigurációs paraméterek között a tags opció tartalmazza a 'fill_transparent' tag-et, akkor a rajzelem háttérszínét # a canvas aktuális háttérszínére változtatjuk, amivel az átlátszóság hatását keltjük. if 'fill_transparent' in configs['tags']: self.canvas.itemconfig(oid, fill=self.canvas.cget('bg')) # Ha a konfigurációs paraméterek között a tags opció tartalmazza az 'outline_transparent' tag-et, akkor a rajzelem körvonalszínét # a canvas aktuális háttérszínére változtatjuk. if 'outline_transparent' in configs['tags']: self.canvas.itemconfig(oid, outline=self.canvas.cget('bg')) # Az így létrehozott rajzelemhez az id_tag argumentum szerinti tag-et mint azonosítócímkét rendeljük. self.canvas.addtag_withtag(id_tag, oid) def render(self, x, y) -> str: """Az inicializáláskor megadott fájlból származó rajzelemadatok alapján előállítja és megjeleníti a grafikát a vásznon úgy, hogy befoglaló téglalapjának bal felső sarokpontja az x, y koordinátákra kerül. Visszatérési értéke a grafika egyedi azonosító tag-e. """ for item_data in self._graphics_definitions.values(): self._create_canvas_item(item_data, self.id_tag) x1, y1, *_ = self.canvas.bbox(self.id_tag) self.canvas.move(self.id_tag, x - x1, y - y1) return self.id_tag @property def file(self) -> str: """Visszaadja a grafikát leíró adatokat tartalmazó fájl elérési útvonalát.""" return self._filepath @property def center_point(self) -> tuple[float, float]: """A megjelenített grafika középpontjának koordinátáit adja vissza egy tuple objektumban.""" x1, y1, x2, y2 = self.canvas.bbox(self.id_tag) return (x1 + x2) / 2, (y1 + y2) / 2 @property def dimensions(self) -> tuple[int, int]: """A megjelenített grafika pixelben mért szélességét és magasságát adja vissza egy tuple objektumban.""" x1, y1, x2, y2 = self.canvas.bbox(self.id_tag) return x2 - x1, y2 - y1 @property def width(self) -> int: """Visszaadja a megjelenített grafika szélességét pixelben.""" return self.dimensions[0] @property def height(self) -> int: """Visszaadja a megjelenített grafika magasságát pixelben.""" return self.dimensions[1] def move(self, dx, dy): """A megjelenített grafikát jelenlegi pozícióhoz képest x irányban dx, y irányban dy értékekkel növelt koordinátákra helyezi át a vásznon. """ self.canvas.move(self.id_tag, dx, dy) def move_to(self, x, y): """Áthelyezi a megjelenített grafikát úgy, hogy befoglaló téglalapjának bal felső sarokpontja az x, y koordinátákra kerül.""" x1, y1, *_ = self.canvas.bbox(self.id_tag) self.move(x - x1, y - y1) def move_center_to(self, x, y): """Áthelyezi a megjelenített grafikát úgy, hogy annak középpontja az x, y koordinátákra kerül.""" cpx, cpy = self.center_point self.move(x - cpx, y - cpy) def scale(self, x_scale, y_scale): """A megjelenített grafika méretét x irányban x_scale, y irányban y_scale szeresre változtatja.""" self.canvas.scale(self.id_tag, *self.center_point, x_scale, y_scale) |
A Tcg osztály példánya az inicializáláskor megadott fájl által definiált tkinter canvas grafikát állítja elő és jeleníti meg az inicializáláskor megadott vászon elemen, amikor a render() metódus meghívásra kerül. Ezt követően a grafika áthelyezhető és átméretezhető a példányon meghívott megfelelő metódusokkal. E metódusok használata helyett a Canvas saját metódusaival is megvalósíthatók a műveletek, amelyekhez a Tcg példány id_tag azonosító tag-e használható az adott Canvas metódus által elvárt rajzelem-azonosítóként.
Ha a render() metódus hívásakor, vagyis ha a grafika előállításakor egy rajzelemen a „fill_transparent” tag található, akkor az adott rajzelem kitöltőszíne a vászon aktuális háttérszínével fog megegyezni, az átlátszóság érzetét keltve. Ha a grafika előállításakor a rajzelemen az „outline_transparent” tag található, akkor az adott rajzelem körvonalának színe a vászon aktuális háttérszínével fog megegyezni. Ebből következik, hogy a megjelenített grafika traszparens részei csak akkor hatnak átlátszónak, ha alattuk csak a vászon elem van. Ha a megjelenítési sorrendben más grafika van alattuk, akkor nem érvényesül az átlátszóság. Ez kompromisszum a grafika előállításának (a grafika-előállító függvények kódolásának) egyszerűsége és a teljes, valódi átlátszóság között. Ez utóbbi ugyanis megvalósítható, de ekkor a grafikát sokkal bonyolultabb geometriával, sok csúcsponttal rendelkező sokszögekből kell felépíteni.
Arra is lehet igény, hogy mielőtt egy .tcg fájlt használnánk, megtekintsük, hogy milyen grafikát képvisel. Erre szolgál a következőkben mutatott view_tcg() és view_tcg_files() függvény. Az előbbi egyetlen .tcg fájl által definiált grafikát jelenít meg egy ablakban. Az utóbbi több .tcg fájl által definiált grafikákat egy közös ablakban táblázatos elrendezésben jeleníti meg. Ezek a függvények már a Tcg osztályt használják, így egyúttal a Tcg osztály alkalmazási példájaként is tekinthetünk rájuk.
|
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 |
# modul: tcg.py def view_tcg(root, filename: str | Path, **canvas_configs): """A filename argumentummal megadott .tcg fájl által definiált grafikát egy ablakban elhelyezett vászon elemen megjeleníti. Ha a fájlból bármilyen okból nem lehet a grafikát előállítani, akkor az nem fog az ablakban megjelenni. A root argumentumként a főablakot (gyökérelemet) kell megadni. """ # Az ablak létrehozása. window = tk.Toplevel(root) window.title(f'Tkinter Canvas Graphics (TCG) Viewer - File: {filename}') scr_w, scr_h = window.winfo_screenwidth(), window.winfo_screenheight() cnv_w, cnv_h = scr_w / 2, scr_h / 2 # A vászon elem létrehozása, konfigurálása és lehelyezése az ablakban. canvas = tk.Canvas(window, width=cnv_w, height=cnv_h) canvas.config(**canvas_configs) canvas.pack() try: # A vászon és a megadott fájl ismeretében a Tcg objektum létrehozása. tcg = Tcg(canvas, filename) # A Tcg példányt használva a grafika előállítása és megjelenítése. tcg.render(0, 0) # A megjelenített grafikát a vászon középére helyezzük és átméretezzük úgy, hogy a vászon területén # teljes egészében látszódjon. tcg.move_center_to(cnv_w / 2, cnv_h / 2) tcg.scale(k := min(cnv_w, cnv_h) * 0.8 / max(tcg.dimensions), k) except Exception: pass def view_tcg_files(root, filenames: Iterable[str | Path], **canvas_configs): """A filenames argumentummal megadott létező .tcg fájlok által definiált grafikákat egy közös ablakban táblázatos elrendezésben megjeleníti. Ha egy fájlból bármilyen okból nem lehet a grafikát előállítani, akkor az nem fog az ablakban megjelenni. A root argumentumként a főablakot (gyökérelemet) kell megadni. """ # Az iterálható objektumként átadott fájlútvonalakból csak a létező, .tcg kiterjesztéssel rendelkezőket tartjuk meg. filenames = tuple(filename for filename in filenames if Path(filename).exists() and Path(filename).suffix == '.tcg') # Ha van legalább egy érvényes fájl, akkor az vagy azok által definiált grafikákat táblázatosan megjelenítjük. if filenames: # Az ablak létrehozása a képernyő közepén a képernyőmérethez igazított szélességgel és magassággal. window = tk.Toplevel(root) window.title('Tkinter Canvas Graphics (TCG) Viewer') scr_w, scr_h = window.winfo_screenwidth(), window.winfo_screenheight() window_width, window_height = int(scr_w * 0.8), int(scr_h * 0.8) window_x = scr_w // 2 - window_width // 2 window_y = scr_h // 2 - window_height // 2 window.geometry(f'{window_width}x{window_height}+{window_x}+{window_y}') # A táblázatos elrendezés sor- és oszlopszámának meghatározása a megjelenítendő grafikák # száma alapján úgy, hogy a sor- és oszlopszám minél közelebb legyen egymáshoz. n = len(filenames) rowcount = round(n ** 0.5) columncount = n // rowcount if n % rowcount == 0 else n // rowcount + 1 # A létező fájlok által definiált grafikákat külön rácscellákban, saját vásznon jelenítjünk meg. for i, filename in enumerate(filenames): canvas = tk.Canvas(window, width=window_width / columncount, height=window_height / rowcount) canvas.config(**canvas_configs) ri, ci = divmod(i, columncount) canvas.grid(row=ri, column=ci) try: # A vászon és a megadott fájl ismeretében a Tcg objektum létrehozása. tcg = Tcg(canvas, filename) # A Tcg példányt használva a grafika előállítása és megjelenítése. tcg.render(0, 0) # A megjelenített grafikát az aktuális vászon középére helyezzük és átméretezzük úgy, hogy a vászon területén # teljes egészében látszódjon. canvas.update_idletasks() # A canvas elem méretének lekérdezése előtt annak frissítése. cnv_width, cnv_height = canvas.winfo_width(), canvas.winfo_height() # A canvas elem méretének lekérdezése. tcg.move_center_to(cnv_width / 2, cnv_height / 2) tcg.scale(k := min(cnv_width, cnv_height) * 0.8 / max(tcg.dimensions), k) except Exception: pass |
A TcgFileMaker és Tcg osztályok, valamint a view_tcg() és view_tcg_files() függvények alkotják a tkinter canvas grafikák fájlba mentésének és onnan történő megjelenítésének és használatának eszköztárát. Ezért szerepelnek ezek egy közös, tcg nevű modulban. Ha egy GUI programban a grafikákat menteni akarjuk, és/vagy már .tcg fájlokba elmentett grafikákat akarunk behívni és azokkal dolgozni, akkor a tcg modult kell beimportálni, és használni a benne foglalt osztály- és függvényobjektumokat.
A tcg modul objektumai használatának szemléltetéséhez két GUI alkalmazást mutatunk.
Tcg fájlokat létrehozó alkalmazás
Az alkalmazást megvalósító TcgFileCreatorApp osztály a tcg_file_creations nevű modulban van definiálva. A modult szkriptként futtatva egy oly GUI alkalmazást indít el, amely egy megadható modulfájlban definiált egy vagy több tkinter canvas grafikát előállító függvény alapján a grafikákat leíró .tcg kiterjesztésű fájlokat készít és ment el egy előre megadható mappába. E mappába elmentett létező .tcg fájlok által definiált grafikákat egy közös ablakban táblázatos elrendezésben is meg lehet jeleníteni.
|
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 |
# modul: tcg_file_creations.py import tkinter as tk from tkinter.simpledialog import askstring from tkinter.filedialog import askopenfilename, askdirectory from tkinter.messagebox import showinfo, showerror from pathlib import Path from importlib import import_module from tcg import TcgFileMaker, view_tcg_files import sys class TcgFileCreatorApp(tk.Tk): """ GUI alkalmazás, amely egy megadható modulfájlban definiált egy vagy több tkinter canvas grafikát előállító függvény alapján a grafikákat leíró .tcg kiterjesztésű fájlokat készít és ment el egy előre megadható mappába. E mappába elmentett létező .tcg fájlok által definiált grafikákat egy közös ablakban táblázatos elrendezésben is meg lehet jeleníteni. A működéshez a grafikaelőállító függvényekre vonatkozóan vannak követelmények, amelyek az alábbiak: - nevük create_ kezdetű, - csak egyetlen kötelezően megadandó pozícionális argumentumot fogadhatnak, ami a Canvas példány, - az átlátszóvá tenni kívánt rajzelemhez a 'fill_transparent' tag van rendelve. - az aktuális canvas háttérszínnel egyező körvonal szín eléréséhez a rajzelemhez az 'outline_transparent' tag van rendelve. Az alkalmazás indítása után beviteli mezőkből és vezérlő gombokból álló felhasználó felület jelenik meg. A grafikaelőállító függvényeket tartalmazó modulfájl elérési útvonalát és az elkészült .tcg kiterjesztésű fájlok mentési mappájának útvonalát a megfelelő beviteli mezőkben két módon adhatjuk meg. Vagy manuálisan begépelve, vagy a beviteli mezők jobb szélén található, három ponttal jelzett nyomógomb megnyomására felugró párbeszédablak segítségével. Ha ezeket megadtuk, akkor a "TCG fájlnevek generálása" gombot megnyomva a modulfáljban található grafikaelőállító függvények nevei alapján a listadobozban egymás alatt fájlnevek jelennek meg, amely neveken a grafikák .tcg kiterjesztésű fájlokba menthetők. Ezek a nevek azonban csak ajánlott, alapértelmezett nevek. Ha mást szeretnénk, akkor módosítani lehet úgy, hogy kétszer a névre kell kattintani a bal egérgombbal, és a felugró beviteli párbeszédablakba az új nevet kell beírni, majd az OK gomb lenyomásával érvényesíteni. A "Grafika leíró fájlok készítése" gomb lenyomására a .tcg kiterjesztésű fájlok elkészülnek és a korábbban megadott mappába mentődnek. A sikeres műveletetről egy üzenetablak tájékoztat. Ha a fájlok készítése valamiért nem sikerül, akkor a hiba valószínű okáról szintén egy üzenetablakban kapunk információt. A megadott mentési mappában található .tcg fájlok által leírt grafikákat egyetlen ablakban táblázatos elrendezésben tekinthetjük meg, ha a "Mentett grafikák megjelenítése" gombot megnyomjuk. """ def __init__(self): super().__init__() self.title('TCG fájlok készítése grafikaelőállító függvények alapján'.upper()) self.resizable(False, False) self._tcgmaker = TcgFileMaker(self) # A fájlkészítést ténylegesen végző objektum. # A fájlnevek és grafikaelőállító függvényobjektumok összerendelését tartalmazó szótár. self._filename_factory_functions = {} # Kontrollváltozók. # A grafikaelőállító függvények definícióit tartalmazó modulfájl elérési útvonalának változója. self._factories_module_path_var = tk.StringVar(self) # A .tcg fájlok mentési mappája útvonalának változója. self._tcg_files_folderpath_var = tk.StringVar(self) # A .tcg fájlneveket tároló változó. self._tcgfilenames_var = tk.StringVar(self) # A felhasználói felület felépítése a grafikus elemek létrehozásával. common_configs = dict(font=('Consolas', 14, 'bold')) lblfrm1 = tk.LabelFrame(self, text='Grafikaelőállító függvényeket tartalmazó modulfájl', **common_configs) ent1 = tk.Entry(lblfrm1, width=70, textvariable=self._factories_module_path_var, **common_configs) btn_module_selection_dialog = tk.Button(lblfrm1, text=chr(0x22ee), **common_configs, command=self._select_module_file) lblfrm2 = tk.LabelFrame(self, text='TCG fájlok mentési mappája', **common_configs) ent2 = tk.Entry(lblfrm2, width=70, textvariable=self._tcg_files_folderpath_var, **common_configs) btn_dir_selection_dialog = tk.Button(lblfrm2, text=chr(0x22ee), **common_configs, command=lambda: self._tcg_files_folderpath_var.set(askdirectory(title='TCG fájlok mentési mappája'.upper()))) btn_gen_filenames = tk.Button(self, text='TCG fájlnevek generálása'.upper(), **common_configs, bg='gray87', command=self._get_factory_function_objects) lblfrm3 = tk.LabelFrame(self, text='TCG fájlok alapértelmezett nevei', **common_configs) lbox = tk.Listbox(lblfrm3, height=8, width=70, listvariable=self._tcgfilenames_var, **common_configs) yscb = tk.Scrollbar(lblfrm3, orient=tk.VERTICAL) lbox.config(yscrollcommand=yscb.set) yscb.config(command=lbox.yview) btn_makefiles = tk.Button(self, text='Grafika leíró fájlok készítése'.upper(), bg='gray87', **common_configs, command=self._create_tcg_files) btn_view = tk.Button(self, text='Mentett grafikák megjelenítése'.upper(), bg='gray87', **common_configs, command=self._display_saved_graphics) # Grafikus elemek lehelyezése. common_grid_options = dict(sticky='news', padx=10, pady=10) lblfrm1.grid(row=0, column=0, **common_grid_options) ent1.grid(row=0, column=1, **common_grid_options) btn_module_selection_dialog.grid(row=0, column=2, **common_grid_options) btn_module_selection_dialog.grid_configure(padx=(0, 10)) lblfrm2.grid(row=1, column=0, **common_grid_options) ent2.grid(row=1, column=1, **common_grid_options) btn_dir_selection_dialog.grid(row=1, column=2, **common_grid_options) btn_dir_selection_dialog.grid_configure(padx=(0, 10)) btn_gen_filenames.grid(row=2, column=0, **common_grid_options) lblfrm3.grid(row=3, column=0, **common_grid_options) lbox.grid(row=3, column=0, **common_grid_options) yscb.grid(row=3, column=1, sticky='ns') btn_makefiles.grid(row=4, column=0, **common_grid_options) btn_view.grid(row=5, column=0, **common_grid_options) # Események és eseménykezelők listadobozhoz rendelése. lbox.bind('<Double Button 1>', self._modify_filename) lbox.bind('<Key Delete>', self._delete_filename) def _select_module_file(self): """Párbeszédablak megjelenítésével lehetővé teszi a grafikaelőállító függvények definícióit tartalmazó modulfájl útvonalának megadását. """ module_path = askopenfilename(title='válaszd ki a grafikaelőállító függvények definícióit tartalmazó modult'.upper(), defaultextension='.py', initialdir=Path()) # Ha a párbeszédablak nem üres karakterlánccal tér vissza, akkor a fájlútvonalat eltároljuk a megfelelő kontrollváltozóban. if module_path: self._factories_module_path_var.set(module_path) def _get_factory_function_objects(self): """A grafikaelőállító függvényeket tartalmazó modul beimportálása után a modulobjektumból kinyeri a függvényobjektumokat. A függvényobjektumok fájlnevekhez lesznek rendelve és e párok egy szótárban tárolódnak. A fájlnevek a grafikaelőállító függvények nevéból képződnek olyan módon, hogy a függvénynév 'create_' kezdete levágásra kerül. Az így kapott fájlnevekkel lesznek a listadoboz sorai feltöltve. """ module_dir: str = str(Path(self._factories_module_path_var.get()).parent) # A modulfájlt tartalmazó mappa. modulename: str = str(Path(self._factories_module_path_var.get()).stem) # A modulnév a modulfájlnév alapján. # A grafikaelőállító függvényeket tartalmazó modul mappájának útvonalát felvesszük a rendszer keresési útvonalai közé. sys.path.append(module_dir) try: module_obj = import_module(modulename) # A modulobjektum beimportálása. except (ValueError, ModuleNotFoundError): showerror('modulmegadási hiba'.upper(), 'Nem létező vagy hibásan megadott modul.') return except Exception: showerror('modulmegadási hiba'.upper(), 'Nem megfelelő modul lett importálva.') return # A függvényobjektumokat fájlnevekhez rendeljük és ezeket együtt egy szótárban tároljuk. # A fájlnevek a grafikaelőállító függvények nevéből képződnek a függvénynév 'create_' után maradt részéből. self._filename_factory_functions.update({attn.removeprefix('create_'): getattr(module_obj, attn) for attn in dir(module_obj) if attn.startswith('create_')}) # A fájlnevekkel a listadoboz sorait feltöltjük. self._tcgfilenames_var.set(' '.join(self._filename_factory_functions.keys())) # A grafikaelőállító függvényeket tartalmazó modul mappájának útvonalát eltávolítjuk a rendszer keresési útvonalai közül. sys.path.remove(module_dir) def _delete_filename(self, event): """Eseménykezelő, amely a listadoboz kiválasztott sorában szereplő fájlnevet törli. Törölt fájlnévhez tartozó .tcg fájl nem fog készülni. """ lbox: tk.Listbox = event.widget i, *_ = event.widget.curselection() # A hívás eredménye a listadoboz kiválasztott sorának indexét tartalmazó tuple. # A kiválasztott fájlnevet mind a listadobozból, mind a fájlnév-előállítőfüggvény nyilvántartásból töröljük. del self._filename_factory_functions[lbox.get(i)] lbox.delete(i) def _modify_filename(self, event): """Eseménykezelő, amely a listadoboz kiválasztott sorában szereplő fájlnév módosítását teszi lehetővé. Ehhez egy beviteli párbeszédablakot jelenít meg, amelyben az új fájlnév adható meg. """ try: i, *_ = event.widget.curselection() # A hívás eredménye a listadoboz kiválasztott sorának indexét tartalmazó tuple. names = list(eval(self._tcgfilenames_var.get())) # A listadobozban felsorolt összes név listája. new_name = askstring('fájlnév megváltoztatás'.upper(), '{:80}'.format('Add meg az új fájlnevet!'), initialvalue=names[i]) # Ha a párbeszédablak nem üres karakterlánccal tér vissza, akkor a régi nevet az újra cseréljük. if new_name: old_name = names[i] names[i] = new_name self._tcgfilenames_var.set(' '.join(names)) self._filename_factory_functions.update({new_name: self._filename_factory_functions[old_name]}) del self._filename_factory_functions[old_name] except ValueError: showerror('fájlnévmódosítási hiba'.upper(), 'Fájlnevek nem állnak rendelkezésre, ezért \nnincs mit módosítani.') def _create_tcg_files(self): """Az aktuális fájlnevekkel létrehozza a grafikaelőlállító függvényekkel definiált .tcg fájlokat. Sikeres fájlkészítés esetén tájékoztató üzenetablak ugrik fel. Ha a listadobozban nem szerepelnek fájlnevek, vagy a fájlkészítés bármilyen más okból nem lehetséges, akkor hibaüzenetetablak jelenik meg a hiba lehetséges okát leírva. """ if self._filename_factory_functions: try: for filename, graphics_factory_function in self._filename_factory_functions.items(): self._tcgmaker.generate_tcg_file_from_factory(Path(self._tcg_files_folderpath_var.get()) / filename, graphics_factory_function) showinfo('fájlkészítés végrehajtva'.upper(), 'A listában szereplő nevekkel a .tcg kiterjesztésű fájlok elkészültek és ' 'megtalálhatók a megadott mappában.') except TypeError: showerror('fájlkészítési hiba'.upper(), f'A {filename} névhez tartozó grafikaelőállító függvény nem megfelelő. ' f'A hiba oka lehet például:\n' f'- nem fogad argumentumot\n' f'- egynél több pozicionális argumentumot kell megadni.') else: showerror('fájlkészítési hiba'.upper(), 'Fájlnevek nem állnak rendelkezésre.') def _display_saved_graphics(self): """A korábban megadott mappába elmentett létező .tcg fájlok által definiált grafikákat egy közös ablakban táblázatos elrendezésben megjeleníti. Ha fáljból bármilyen okból nem lehet a grafikát előállítani, akkor az nem fog az ablakban megjelenni. """ view_tcg_files(self, Path(self._tcg_files_folderpath_var.get()).glob('*.tcg'), bg='gray85') def run(self): self.mainloop() TcgFileCreatorApp().run() |
A működéshez a grafika-előállító függvényekre vonatkozóan vannak követelmények, amelyek az alábbiak:
- nevük create_ kezdetű,
- csak egyetlen kötelezően megadandó pozícionális argumentumot fogadhatnak, ami a Canvas példány,
- az átlátszóvá tenni kívánt rajzelemhez a „fill_transparent” tag van rendelve,
- az aktuális canvas háttérszínnel egyező körvonal szín eléréséhez a rajzelemhez az „outline_transparent” tag van rendelve.
Az alkalmazás indítása után beviteli mezőkből és vezérlő gombokból álló felhasználó felület jelenik meg.
A grafika-előállító függvényeket tartalmazó modulfájl elérési útvonalát és az elkészült .tcg kiterjesztésű fájlok mentési mappájának útvonalát a megfelelő beviteli mezőkben két módon adhatjuk meg. Vagy manuálisan begépelve, vagy a beviteli mezők jobb szélén található, három ponttal jelzett nyomógomb megnyomására felugró párbeszédablak segítségével.
Ha ezeket megadtuk, akkor a „TCG fájlnevek generálása” gombot megnyomva a modulfáljban található grafika-előállító függvények nevei alapján a listadobozban egymás alatt fájlnevek jelennek meg, amely neveken a grafikák .tcg kiterjesztésű fájlokba menthetők. Ezek a nevek azonban csak ajánlott, alapértelmezett nevek. Ha mást szeretnénk, akkor módosítani lehet úgy, hogy kétszer a névre kell kattintani a bal egérgombbal, és a felugró beviteli párbeszédablakba az új nevet kell beírni, majd az OK gomb lenyomásával érvényesíteni.
A „Grafika leíró fájlok készítése” gomb lenyomására a .tcg kiterjesztésű fájlok elkészülnek és a korábban megadott mappába mentődnek. A sikeres műveletekről egy üzenetablak tájékoztat. Ha a fájlok készítése valamiért nem sikerül, akkor a hiba valószínű okáról szintén egy üzenetablakban kapunk információt. A megadott mentési mappában található .tcg fájlok által leírt grafikákat egyetlen ablakban táblázatos elrendezésben tekinthetjük meg, ha a „Mentett grafikák megjelenítése” gombot megnyomjuk.
Az alkalmazás grafikus felhasználói felületét és egy eredményképet mutat az alábbi ábra, ahol a használat lépéseit sorszámokkal jeleztük a megfelelő nyomógombokon. Itt az is látható, hogy akár több modulban szereplő grafika-előállító függvényekkel is dolgozhatunk.

Montázskészítő alkalmazás
Ezt az alkalmazást a TcgMontageMakerApp osztály képviseli, amely a tcg_montage_maker modulban definiált. E modult szkriptként futtatva egy olyan GUI alkalmazás indul el, amellyel .tcg fájlokban definiált grafikák felhasználásával egy új grafikát lehet készíteni az alkalmazás erre szolgáló felületén. Az egyéni terv szerint elrendezett (áthelyezett és/vagy átméretezett) komponens grafikákból álló montázst egy megadható mappába lehet elmenteni .tcg fájl formátumban. Ezt követően a montázst meg is lehet jeleníteni egy külön ablakban. Az alkalmazás indítása után a beviteli mezők és vezérlő gombok a bal oldalon, a vizuális grafikai tervező felület a jobb oldalon látható.
A tcg_montage_maker modul tartalma a következő:
|
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 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# modul: tcg_montage_maker.py import tkinter as tk from tkinter.colorchooser import askcolor from tkinter.filedialog import askdirectory, asksaveasfilename, askopenfilename from pathlib import Path from itertools import count from operator import itemgetter from tcg import Tcg, TcgFileMaker, view_tcg class TcgMontageMakerApp(tk.Tk): """ GUI alkalmazás, amellyel .tcg fájlokban definiált grafikák felhasználásával egy új grafikát lehet készíteni az alkalmazás erre szolgáló felületén. Az egyéni terv szerint elrendezett (áthelyezett és/vagy átméretezett) komponens grafikákból álló montázst egy megadható mappába lehet elmenteni .tcg fájl formátumban. Ezt követően a montázst meg is lehet jeleníteni egy külön ablakban. Az alkalmazás indítása után a beviteli mezők és vezérlő gombok a bal oldalon, a vizuális grafikai tervező felület a jobb oldalon látható. A mappaútvonalakat a beviteli mezőkben két módon adhatjuk meg. Vagy manuálisan begépelve, vagy a beviteli mezők jobb szélén található, három ponttal jelzett nyomógomb megnyomására felugró párbeszédablak segítségével. A "Komponens Tcg fájlok beolvasása" gombra kattintva az első beviteli mezőben megadott mappában levő .tcg fájlok beolvasása megtörténik, és a fájlnevek a listadobozban jelennek meg egymás alatt. A felsorolt fájlnevek közül kiválaszthatjuk, hogy melyekhez tartozó grafikákat akarjuk megjeleníteni a jobb oldali felületen. Ha egy sorban a bal egérgombbal kattintunk, akkor csak az a fájlnév lesz kiválasztva. Ha egyszerre többet akarunk kiválasztani, akkor ezt több módon is megtehetjük. Ha egymást követő több sort akarunk egyszerre kijelölni, akkor a kezdősoron nyomjuk le a bal egérgombot, majd a blokk utolsó során a Shift billentyű lenyomva tartása mellett újra nyomjuk le a bal egérgombot. Ha több, de nem egymást követő sorokat akarunk kijelölni, akkor a Control billentyűt lenyomva tartva nyomjuk le a bal egérgombot a kívánt sorokon. Azt, hogy mely sorok vannak kijelölve onnan látható, hogy a háttérszínük megváltozik. A legutoló kijelölést az ESC billentyűvel lehet törölni. A kívánt sorok kijelölése után a "A kijelölt egy vagy több fájl grafikájának megjelenítése" gombra kattintva a grafikák a jobb oldali felület közepén egymás felett jelennek meg. Innen az adott grafikát elmozgathatjuk a szokásos módon, azaz a bal egérgombot a grafikán lenyomva tartva az egérmutatót a kívánt helyre húzzuk és ott az egérgombot elengedjük. A felületen megjelent komponens grafikák nagyíthatók vagy kicsinyíthetők. Ehhez az egeret az adott grafika fülé kell vinni és a görgővel felfelé vagy lefelé görgetni. Az előbbi esetben a rajz mérete finom léptékben nő, az utóbbi esetben csökken. Ha gyorsabban, azaz nagyobb léptékben akarjuk a méretváltoztatást, akkor a görgetés közben a Control billentyűt lenyomva kell tartani. A grafikák, ha területük fedik egymást, akkor az átfedő területen az egyik kitakarja a másikat. Az, hogy melyik grafika melyiket takarja el, egy megjelenítési sorrendet határoz meg. Ha azt szeretnénk, hogy egy adott grafika ebben a sorrendben egy szinttel feljebb kerüljön, akkor az egérmutatót vigyük a grafika fölé, és nyomjuk le a Shift billentyűt majd a bal egérgombot. Ha egy szinttel lejjebb akarjuk küldeni, akkor nyomjuk le a Control billentyűt majd a bal egérgombot. Ha a grafikát a megjelenítési lista legtetejére akarjuk hozni, akkor nyomjuk le az Alt és Shift billentyűt majd a bal egérgombot. Ha a grafikát a megjelenítési lista legaljára akarjuk küldeni, akkor nyomjuk le az Alt és Control billentyűt majd a bal egérgombot. A tervezőfelületről grafikát úgy lehet eltávolítani, ha az egermutatót a grafika felett van és lenyomjuk a jobb egérgombot. A vászon színének megváltoztatására is van lehetőség, ha a vászon egy pontján az Alt billentyűt lenyomva tartva lenyomjuk a jobb egérgombot. Az ezt követően felugró színválasztó párbeszédablakban ki kell választani az új háttérszínt. Ha a montázs elkészült, akkor azt a "A létrehozott montázs grafika mentése TCG fájlba" gomb megnyomásával menthetjük el megadva a fájlnevet a felugró párbeszédablakban. A mentés után a montázs törlődik, tiszta felületet adva a következő alkotáshoz. A mentési mappába került montázsok megtekinthetők, ha a "Mentett grafikák megjelenítése" gomb lenyomása után felugró párbeszédablakban kiválasztunk egy .tcg fájlt. Alapértelmezésben az ablak a legutoljára elmentett montázs fájlnevét kínálja fel. """ _cntr = count() # Sorszámgenerátor az ugyanolyan grafikák másolatainak megkülönböztetéséhez. def __init__(self): super().__init__() self.resizable(False, False) self.title('tkinter canvas grafika montázs készítő'.title()) self._tcgfilemaker = TcgFileMaker(self) # A fájlkészítést ténylegesen végző objektum. # Kontrollváltozók self._input_tcg_folderpath_var = tk.StringVar(self) # A montázs komponesek .tcg fájljainak a mappaútvonalát tároló változó. self._output_tcg_folderpath_var = tk.StringVar(self) # Az elkészült montázs .tcg fájljának mentési mappaútvonalát tároló változó. self._tcgfilenames_var = tk.StringVar(self) # A listadobozban felsorolt .tcg fájlok neveit tároló változó. self._tcg_objects: list[Tcg] = [] # A komponens .tcg fájlokból előállított Tcg objektumok. self._rendered_tcg_objects: list[Tcg] = [] # Az előállított és megjelenített grafikákhoz tartozó Tcg objektumok. self._filename = '' # A grafikus felhasználói felület elemeinek létrehozása. frm_left, frm_right = tk.Frame(self), tk.Frame(self) self._canvas = tk.Canvas(frm_right, width=800, bg='ivory2') common_configs = dict(font=('Consolas', 14, 'bold')) lblfrm1 = tk.LabelFrame(frm_left, text='Kompones TCG fájlok mappája', **common_configs) ent1 = tk.Entry(lblfrm1, width=70, textvariable=self._input_tcg_folderpath_var, **common_configs) btn_input_dir_dialog = tk.Button(lblfrm1, text=chr(0x22ee), **common_configs, command=lambda: self._input_tcg_folderpath_var.set(askdirectory(title='A komponens TCG fájlok mappája'.upper()))) lblfrm2 = tk.LabelFrame(frm_left, text='A készített montázs TCG fájl mentési mappája', **common_configs) ent2 = tk.Entry(lblfrm2, width=70, textvariable=self._output_tcg_folderpath_var, **common_configs) btn_output_dir_dialog = tk.Button(lblfrm2, text=chr(0x22ee), **common_configs, command=lambda: self._output_tcg_folderpath_var.set(askdirectory(title='A TCG fájl mentési mappája'.upper()))) btn_gen_filenames = tk.Button(frm_left, text='Komponens TCG fájlok beolvasása'.upper(), **common_configs, bg='gray87', command=self._creat_tcg_objects_from_files) lblfrm3 = tk.LabelFrame(frm_left, text='TCG fájlok', **common_configs) self._lbox = tk.Listbox(lblfrm3, height=8, width=70, listvariable=self._tcgfilenames_var, selectmode=tk.EXTENDED, **common_configs) yscb = tk.Scrollbar(lblfrm3, orient=tk.VERTICAL) self._lbox.config(yscrollcommand=yscb.set) yscb.config(command=self._lbox.yview) btn_render = tk.Button(frm_left, text='A kijelölt egy vagy több fájl grafikájának megjelenítése'.upper(), bg='gray87', **common_configs, command=self._render_selected_items) btn_save = tk.Button(frm_left, text='A létrehozott montázs grafika mentése TCG fájlba'.upper(), bg='gray87', **common_configs, command=self._save_graphics) btn_view = tk.Button(frm_left, text='Mentett grafikák megjelenítése'.upper(), bg='gray87', **common_configs, command=self._show_saved_graphics) # Grafikus elemek lehelyezése. frm_left.grid(row=0, column=0, sticky='news') frm_right.grid(row=0, column=1, sticky='news') self.grid_columnconfigure((0, 1), weight=1, uniform='a') self._canvas.pack(fill=tk.BOTH, expand=True) common_grid_options = dict(sticky='news', padx=10, pady=10) lblfrm1.grid(row=0, column=0, **common_grid_options) ent1.grid(row=0, column=1, **common_grid_options) btn_input_dir_dialog.grid(row=0, column=2, **common_grid_options) btn_input_dir_dialog.grid_configure(padx=(0, 10)) lblfrm2.grid(row=1, column=0, **common_grid_options) ent2.grid(row=1, column=1, **common_grid_options) btn_output_dir_dialog.grid(row=1, column=2, **common_grid_options) btn_output_dir_dialog.grid_configure(padx=(0, 10)) btn_gen_filenames.grid(row=2, column=0, **common_grid_options) lblfrm3.grid(row=3, column=0, **common_grid_options) self._lbox.grid(row=3, column=0, **common_grid_options) yscb.grid(row=3, column=1, sticky='ns') btn_render.grid(row=4, column=0, **common_grid_options) btn_save.grid(row=5, column=0, **common_grid_options) btn_view.grid(row=6, column=0, **common_grid_options) # Események és eseménykezelők hozzárendelése a vászon grafikus elemhez. self._canvas.bind('<Alt Button 3>', self._change_canvas_bg) self._canvas.bind('<MouseWheel>', lambda e: self._resize(e, 0.01)) self._canvas.bind('<Control MouseWheel>', lambda e: self._resize(e, 0.05)) def _make_item_draggable(self, tag_or_id): """A tag_or_id azonosítóval rendelkező grafikus elemek mozgatását (vonszolását) valósítja meg. A mozgatandó elemen a bal egérgombot le kell nyomni, és lenyomva tartva az egeret a kívánt pozícióig kell mozgatni, majd ott felengedni a bal egérgombot. """ def grab_item(e: tk.Event): """Az eseménnyel érintett grafika mozgatásra kijelölése.""" tcg_to_grab = self._get_tcg(tk.CURRENT) e.widget.addtag_withtag('to_be_moved', tcg_to_grab.id_tag) e.widget.x0, e.widget.y0 = e.x, e.y def dragging(e: tk.Event): """A mozgatásra kijelölt grafika mozgatása (vonszolása).""" dx, dy = e.x - e.widget.x0, e.y - e.widget.y0 e.widget.move('to_be_moved', dx, dy) e.widget.x0, e.widget.y0 = e.x, e.y def stop_dragging(e: tk.Event): """A mozgatásra kijelölt grafika vonszolásának befejezése a mozgatásra kijelöltség megszűntetésével.""" e.widget.dtag('to_be_moved') # Események és eseménykezelők hozzárendelése az adott tag_or_id azonosítóval rendelkező grafikához. self._canvas.tag_bind(tag_or_id, '<ButtonPress 1>', grab_item) self._canvas.tag_bind(tag_or_id, '<B1-Motion>', dragging) self._canvas.tag_bind(tag_or_id, '<ButtonRelease 1>', stop_dragging) def _get_tcg(self, tag_or_id): """A tag_or_id rajzelem-azonosító alapján visszaadja azt a Tcg objektumot, amelyhez a rajzelem tartozik.""" # Mivel tag_or_id azonosítóhoz tartozó rajzelem egy grafika része lehet, ezért meghatározzuk, hogy a rajzelemnek # milyen más tag-ei vannak. potential_idtags = self._canvas.gettags(tag_or_id) # Ha a rajzelem egy már előállított és megjelenített (renderelt) grafikához (Tcg objektumhoz) tartozik, akkor a # kapott tag-ek között a grafika id_tag azonosítója is szerepelni fog. Az ehhez tartozó Tcg objektummal tér vissza a metódus. for idtag in potential_idtags: for tcg in self._rendered_tcg_objects: if tcg.id_tag == idtag: return tcg def _change_canvas_bg(self, e: tk.Event): """Eseménykezelő, amely a vászon háttérszínét változtatja meg a megjelenített színpaletta párbeszédablakból kíválasztott színnek megfelelően. """ color = askcolor(title='Canvas háttérszín beállítás'.upper(), color='ivory2')[1] e.widget.config(bg=color) def _resize(self, e: tk.Event, resolution=0.05): """Eseménykezelő, amely az egérgörgő-forgatás eseménnyel érintett grafika méretét minden felfelé görgetéssel resolution mértékkel növeli, illetve minden lefelé görgetéssel resolution mértékkel csökkenti. """ # Meghatározzuk, hogy melyik az eseménnyel érintett rajzelem. object_ids: tuple = self._canvas.find_withtag(tk.CURRENT) if object_ids: # Meghatározzuk, hogy melyik az eseménnyel érintett grafika (Tcg objektum). tcg_to_resize: Tcg = self._get_tcg(*object_ids) if tcg_to_resize: # Az egérgörgő-forgatás esemény "delta" attribútumának előjelétől függően növeljük vagy csökkentjük a # grafika méretét a Tcg objektum scale() metódusának meghívásával. scale_factor = 1 + resolution if e.delta > 0 else 1 - resolution tcg_to_resize.scale(scale_factor, scale_factor) def _remove_item(self, e: tk.Event): """Eseménykezelő, amely az eseménnyel érintett grafikát eltávolítja a vászonról.""" canvas: tk.Canvas = e.widget # Meghatározzuk, hogy melyik az eseménnyel érintett grafika (Tcg objektum). tcg = self._get_tcg(tk.CURRENT) canvas.delete(tcg.id_tag) # Töröljük a grafikát a vászonról. self._rendered_tcg_objects.remove(tcg) # Töröljük a grafikát a megjelenített grafikák nyilvántartásából. def _bring_forward(self, e: tk.Event): """Eseménykezelő, amely az eseménnyel érintett grafikát a megjelenítési listában egy szinttel feljebb levő grafika fölé hozza. Ezt követően az eseménnyel érintett grafika az alatta levőket részben vagy egészben kitakarja. """ # Meghatározzuk, hogy melyik az eseménnyel érintett grafika. tcg: Tcg = self._get_tcg(tk.CURRENT) # Meghatározzuk, hogy melyik az eseménnyel érintett grafika feletti rajzelem a megjelenítési listában. items_id_above: tuple = self._canvas.find_above(tcg.id_tag) if items_id_above: # Ha van feljebb levő rajzelem, akkor meghatározzuk, hogy az melyik grafikához tartozik. tcg_above = self._get_tcg(*items_id_above) # Az eseménnyel érintett grafikát a felette levő grafika fölé visszük a megjelenítési listában. self._canvas.tag_raise(tcg.id_tag, tcg_above.id_tag) def _send_backward(self, e: tk.Event): """Eseménykezelő, amely az eseménnyel érintett grafikát a megjelenítési listában egy szinttel lejjebb levő grafika alá teszi. Ezt követően az eseménnyel érintett grafikát a felette levők részben vagy egészben kitakarják. """ # A metódus logikája hasonló a _bring_forward() metóduséhoz. tcg = self._get_tcg(tk.CURRENT) items_id_below: tuple = self._canvas.find_below(tcg.id_tag) if items_id_below: tcg_below = self._get_tcg(*items_id_below) self._canvas.tag_lower(tcg.id_tag, tcg_below.id_tag) def _bring_to_front(self, e: tk.Event): """Eseménykezelő, amely az eseménnyel érintett grafikát a megjelenítési lista tetejére teszi. Ezt követően az eseménnyel érintett grafika minden mást, amely részben vagy egészben átfedő, kitakar. """ # Meghatározzuk, hogy melyik az eseménnyel érintett grafika. tcg: Tcg = self._get_tcg(tk.CURRENT) # Ha az eseménnyel érintett grafika azonosító tag-ével úgy hívjuk meg a Canvas tag_raise() metódust, hogy # nem határozzuk meg mi fölé kerüljön, akkor a megjelenítési lista tetejére kerül, vagyis minden más felett jelenik meg. self._canvas.tag_raise(tcg.id_tag) def _send_to_back(self, e: tk.Event): """Eseménykezelő, amely az eseménnyel érintett grafikát a megjelenítési lista aljára teszi. Ezt követően az eseménnyel érintett grafikát minden más, amely részben vagy egészben átfedő, kitakarja. """ # Meghatározzuk, hogy melyik az eseménnyel érintett grafika. tcg = self._get_tcg(tk.CURRENT) # Ha az eseménnyel érintett grafika azonosító tag-ével úgy hívjuk meg a Canvas tag_lower() metódust, hogy # nem határozzuk meg mi alá kerüljön, akkor a megjelenítési lista aljára kerül, vagyis minden más alatt jelenik meg. self._canvas.tag_lower(tcg.id_tag) def _creat_tcg_objects_from_files(self): """A komponens grafikák .tcg fájljaiból Tcg objektumokat állít elő, és a fájlneveket a listadobozban felsorolja.""" self._tcg_objects.extend(Tcg(self._canvas, fpath) for fpath in Path(self._input_tcg_folderpath_var.get()).glob('*.tcg')) filenames = [fpath.name for fpath in Path(self._input_tcg_folderpath_var.get()).glob('*.tcg')] listbox_items = list(eval(self._tcgfilenames_var.get())) if self._tcgfilenames_var.get() != '' else [] listbox_items.extend(filenames) self._tcgfilenames_var.set(' '.join(listbox_items)) def _render_selected_items(self): """A listadobozból kiválasztott fájlnevekhez tartozó Tcg objektumok által képviselt grafikákat megjeleníti a vászon közepén.""" items: tuple = self._lbox.curselection() # A hívás eredménye a listadoboz kiválasztott sorának indexét tartalmazó tuple. selected_tcg_objects: tuple = () # A kiválasztott sorokhoz tartozó Tcg objektumok tárolója. if items: # Ha van legalább egy kiválasztott sor, akkor az annak/azoknak megfelőlő egy vagy több Tcg objektumot eltároljuk. selected_tcg_objects: tuple[Tcg] = (selected,) if isinstance(selected := itemgetter(*items)(self._tcg_objects), Tcg) else selected # A kiválasztott Tcg objektumok által képviselt grafikákat előállítjuk és megjelenítjük. A már megjelenített grafikákhoz tartozó # Tcg objektumokról egy külön nyilvántartást vezetünk, hogy egy újabb kiválasztás esetén vizsgálhassuk, hogy mi van már megjelenítve. for tcg_obj in selected_tcg_objects: if tcg_obj not in self._rendered_tcg_objects: tcg = tcg_obj else: tcg = Tcg(self._canvas, tcg_obj.file) tcg.id_tag += str(next(self._cntr)) self._rendered_tcg_objects.append(tcg) # Az Tcg objektum alapján a grafikát előállítjuk és megjelenítjük a (0,0) koordinátákon. tcg.render(0, 0) # Lekérdezzük a vászon méreteit. tcg.canvas.update() cnv_w, cnv_h = tcg.canvas.winfo_width(), tcg.canvas.winfo_height() # A megjelenített grafikát áthelyezzük úgy, hogy a középpontja a vászon középpontjával essen egybe. tcg.move_center_to(cnv_w / 2, cnv_h / 2) # A grafikát átméretezzük. tcg.scale(k := min(cnv_w, cnv_h) * 0.25 / max(tcg.dimensions), k) # Események és eseménykezelők hozzárendelése a grafikákhoz. # Vonszolhatóvá tétel. self._make_item_draggable(tcg.id_tag) # Eltávolíthatóvá tétel. self._canvas.tag_bind(tcg.id_tag, '<Button 3>', self._remove_item) # A megjelenítési sorrendben egy szinttel előrébb és hátrébb küldhetővé tétel. self._canvas.tag_bind(tcg.id_tag, '<Shift Button 1>', self._bring_forward) self._canvas.tag_bind(tcg.id_tag, '<Control Button 1>', self._send_backward) # A megjelenítési sorrend legtetejére és legaljára küldhetővé tétel. self._canvas.tag_bind(tcg.id_tag, '<Alt Shift Button 1>', self._bring_to_front) self._canvas.tag_bind(tcg.id_tag, '<Alt Control Button 1>', self._send_to_back) def _save_graphics(self): """Az összetevő grafikákból a vászonon megalkotott montázs grafikát .tcg fájlba menti a felugró párbeszédablakban kiválasztott mappába és az ott megadott névvel. Alapértelmezésben a korábban meghatározott mentési mappa lesz felkínálva. A mentés után a montázs a vászonról törlődik.""" self._filename = asksaveasfilename(title='canvas montázs grafika mentése TCG fájlba'.upper(), defaultextension='.tcg', confirmoverwrite=True, initialdir=self._output_tcg_folderpath_var.get(), filetypes=(('TCG fájl', '.tcg'),)) if self._filename: self._tcgfilemaker.generate_tcg_file_from_canvas(Path(self._output_tcg_folderpath_var.get()) / self._filename, self._canvas) self._canvas.delete('all') def _show_saved_graphics(self): """Egy, a felugró párbeszédablakban kiválasztható mappában elmentett .tcg fájl által definiált grafikát jelenít meg egy új ablakban. Alapértelmezésben a legutoljára elmentett montázs fájlnevét kínálja fel. """ filename = askopenfilename(title='mentett canvas grafika megjelenítése', defaultextension='.tcg', initialdir=self._output_tcg_folderpath_var.get(), initialfile=Path(self._output_tcg_folderpath_var.get()) / self._filename, filetypes=(('TCG files', '.tcg'), ('All files', '*'))) if filename: view_tcg(self, filename) def run(self): self.mainloop() TcgMontageMakerApp().run() |
Az alkalmazás grafikus felhasználói felületét és példa eredményképeket a következő ábrák mutatják. Itt szintén feltüntettük a használati lépések sorszámait a megfelelő nyomógombokon.


Ez az alkalmazás – minthogy alapvetően demonstrációs célú – nem rendelkezik egy képszerkesztő teljes funkcionalitásával. De persze egyéni igényeknek megfelelően továbbfejleszthető: pl. beviteli mezők és nyomógombok helyett menürendszer, és funkcionalitásban kiegészíthető a tükrözés és forgatás geometriai transzformációkkal. Ez utóbbi alkalmazásához azonban alaktartóan forgatható rajzelemekből felépített grafikák szükségesek. Ehhez lásd a „Forgatható ellipszis, ellipszisív és téglalap” című korábbi bejegyzést. Ha pedig az alkotásunk rajzelemekből való felépítését mások elől el akarjuk rejteni, akkor .tcg fájlokat akár titkosítani is lehet.
A fenti forráskódokat a https://github.com/pythontudasepites/saved_canvas_graphics linken is el lehet érni. Itt az alkalmazások használatának leírása szintén megtalálható.
A tkinter canvas grafikák megtervezése és .tcg fájlok készítése igényel némi időt, de ha olyan képekre, grafikákra van szükség, amelyek a Canvas rajzelemeiből viszonylag egyszerűen létrehozhatók, akkor érdemes megtenni, mert akkor a vektorgrafika előnyeit élvezhetjük, és a mások által készített képek liszensz kérdése sem merül fel.
E bejegyzés témájához a Python tudásépítés lépésről lépésre című e-könyv számos fejezete kapcsolódik. Elsősorban a „Grafikus felhasználói felület készítése”, a „Panelprogram-modulok” és a „Mentsük, ami menthető! – fájlok és mappák” fejezetek. A „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezetből pedig a „Dinamikus modulimportálás” alfejezetet is érdemes még átfutni.