Az előző bejegyzésben egy Excelszerű egyszerű számolótábla készítését kezdtük el az ott megadott specifikáció alapján. A megvalósítást három fő részre bontottuk, amelyből az elsőt, vagyis a táblázat kirajzolását, valamint az egyes cellákban levő beviteli mezőkhöz tartozó kontrollváltozók létrehozását, már megtettük. Ebben a részben a specifikáció szerinti eseményeket határozzuk meg és kötjük a beviteli mezőkhöz.
A legalapvetőbb esemény az, amikor egy cellába beírt képletnek (kifejezésnek) ki kell értékelődnie, ha Entert nyomunk, vagy a celláról a fókusz elkerül. Ez két eseményt jelent, amelyek bármelyikének hatására egy olyan eseménykezelőnek kell lefutni, amely az eseménnyel érintett cella beviteli mezőjének tartalmát kiértékeli. Legyen ennek neve eval_cell_content_even_handler.
Tehát az Enter megnyomása esetén az esemény és eseménykezelő összerendelése logikailag ez: „Enter billentyű megnyomás” -> eval_cell_content_even_handler.
Ha pedig az adott beviteli mező elveszíti a fókuszt, akkor az esemény és eseménykezelő összerendelés logikailag ez lesz: „Fókuszból kikerülés”-> eval_cell_content_even_handler
Egy beviteli mező akkor kerül ki a fókuszból, ha vagy a Tab billentyűt nyomjuk meg, vagy az egérrel egy másik cellába kattintunk. Mivel létezik a fókuszból kikerülés „FocusOut” esemény, ami egy beviteli mezőre is értelmezhető, ezért nem kell a Tab vagy egy eltérő cellába való kattintást külön lekezelni.
Bár az eseményeket a táblázat összes cellájában levő beviteli mezőjéhez kell kötni, de nem kell külön minden egyes beviteli mezőhöz külön hozzárendelni az eseményeket és az arra reagáló eseménykezelőket, hanem elég egyszer, amennyiben nem a példányokhoz, hanem azok osztályához, az Entry osztályhoz kötjük. Ezt a bind_class() metódussal tehetjük meg, amelyet a gyökérelemre (főablak) hívunk meg. Az előbb említett két eseményre így:
|
1 2 3 4 5 |
root.bind_class('Entry', '<Key Return>', eval_cell_content_even_handler) root.bind_class('Entry', '<FocusOut>', eval_cell_content_even_handler) |
Az is szerepel előírásként, hogy az Enter megnyomásakor a kurzor a cellából az alatta levő cellába kell, hogy átmenjen. Az „Enter lenyomása” eseményhez tehát pluszban még egy olyan eseménykezelőt is kell rendelni (legyen ennek neve move_cursor), amely a kurzort átteszi az alatta levő cellába, vagy ha nincs olyan, akkor a jobbra mellette levőbe. Ha az sincs, akkor pedig a cellában hagyja és nem viszi sehová. Ezt az eseménykezelőt szintén minden beviteli mezőhöz kötni kell, ezért a fentiekhez hasonló módon tesszük:
|
1 2 3 |
root.bind_class('Entry', '<Key Return>', move_cursor, add=True) |
Az add paraméter True értékre állításával jeleztük, hogy nem az előző eseménykezelő helyett akarjuk a move_cursor-t, hanem azon felül kívánjuk hozzáadni. Ez azt jelenti, hogy az Enter lenyomására először az eval_cell_content_even_handler kerül meghívásra, után pedig a move_cursor.
Még mindig nem szakadhatunk el az ‘Return’ és ‘FocusOut’ eseményektől. Ugyanis ezek bekövetkezésekor, és az előbb említett eseménykezelők lefutása után, a táblázat összes képletét aktualizálni kell, vagyis újra ki kell őket értékelni. Ehhez egy update_expressions nevű eseménykezelő függvényt fogunk bevetni, amely minden egyes beviteli mezőn végigmegy, és ha van benne képlet, akkor kiértékeli azt. Ennek megfelelően a két eseményre az eseménykezelő hozzárendelése így fest:
|
1 2 3 4 5 |
root.bind_class('Entry', '<Key Return>', update_expressions, add=True) root.bind_class('Entry', '<FocusOut>', update_expressions, add=True) |
Az add paraméter beállítása a fentebb már említett okból történik. Most már végeztünk a ‘Return’ és ‘FocusOut’ eseményekkel.
Meg kell még oldani azt, hogy ha a cellában kétszer kattintunk, akkor jöjjön elő a cella értékét meghatározó képlet, ha volt ilyen. Tehát az összerendelendő esemény és eseménykezelő sémája a „Dupla balegérgomb kattintás”-> reveal_cell_expression_event_handler. Az utóbbi nevű eseménykezelő azt fogja tenni, hogy az eseménnyel érintett cella sor- és oszlopindexei alapján kikeresi az ebx_vars nevű szótárból (ld előző bejegyzésben) a karakterláncként tárolt képletet (ha van), és azt megjeleníti a beviteli mezőben. Ebben az esetben a beviteli mezőket, az eseményt és eseménykezelőt összerendelő kód ez:
|
1 2 3 |
root.bind_class('Entry', '<Double ButtonPress 1>', reveal_cell_expression_event_handler) |
Azt is szeretnénk, hogy a Del (delete) gomb hatására a cella tartalma és a mögöttes képlet (ha volt) törlődjön. Ehhez a „Del gomb lenyomás” eseményhez az on_delete nevű eseménykezelőt rendeljük. Ez az ebx_vars szótárban az eseménnyel érintett beviteli mező kontrollváltozójának értékét és a képletet üres karaktersorra állítja. A beviteli mezőket, az eseményt és eseménykezelőt összekötő kód ebben ez esetben így néz ki:
|
1 2 3 |
root.bind_class('Entry', '<Key Delete>', on_delete) |
És végül azt is szeretnénk, hogy ha a jobb egérgombbal duplán kattintunk egy cellán, akkor az adott oszlop szélessége úgy változzon, hogy az oszlop cellái közül a leghosszabb szöveg is látszódjon. Ez a hosszabb képletek áttekinthetőségét nagyban segíti. Ezt a fit_column_width nevű eseménykezelő fogja megvalósítani. Az esemény-eseménykezelő kód az alábbi, ahol, mint eddig is, az eseményt az összes beviteli mezőhöz egyszerre rendeltük hozzá.
|
1 2 3 |
root.bind_class('Entry', '<Double Button 3>', fit_column_width) |
A fentiekben említett, az eseményeket és eseménykezelőket a beviteli mezőkhöz kötő kódsorokkal kiegészített programkód 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 |
import tkinter as tk from itertools import product root = tk.Tk() root.title('Számolótábla') # A táblázat sor- és oszlopszámának megadása. num_of_rows, num_of_columns = 20, 6 # A táblázat egy adott pozíciójában levő beviteli mezőhöz (entry box) tartozó kontrollváltozót és # képletet (kifejezést) egy szótárban tároljuk, ahol a kulcs egy kételemű tuple a sor- és oszlopindexekkel, a hozzá # rendelt érték pedig szintén egy kételemű tuple, amely első eleme a kontrollváltozó, a második a képletet leíró karaktersor. ebx_vars: dict[tuple[int, int], list] = {} # Táblázatrács kirajzolása. for ri, ci in product(range(num_of_rows + 1), range(num_of_columns + 1)): # A sor- és oszlopindexeken végighaladva felöltjük a szótárt kezdeti értékekkel (üres StringVar és üres string) ebx_vars[(ri, ci)] = [tk.StringVar(), ''] # Létrehozzuk az egyes cellákhoz tartozó grafikus beviteli mezőket, és kontrollváltozóként a szótárban előbb # létrehozott StringVar objektumot rendeljük. Beállítjuk a mezők betűtípusát és szélességét. ebx = tk.Entry(root, textvariable=ebx_vars.get((ri, ci))[0], font=('Noto Mono', 12), width=20) # A táblázat első sora és oszlopa a sor- és oszlopazonosítokat tartalmazzák, ezért e beviteli mezőket # letiltjuk, hogy tartalmuk ne legyen változtatható. A közös konfig paramétereiket egy szótárban határozzuk meg. headers_common_params = dict(state=tk.DISABLED, disabledbackground='gray95', justify=tk.CENTER, font=('Arial', 12, 'bold')) # Az első sor egyes beviteli mezőiben az angol abc nagybetűi lesznek mint oszlopazonosítók. if ri == 0 and ci > 0: ebx_vars[(ri, ci)][0].set(chr(ord('A') + ci - 1)) ebx.config(**headers_common_params) # Az első oszlop egyes beviteli mezőiben a sorszámok jelennek meg 1-től kezdődő egészekként. # Az első oszlop szélességét a legnagyobb sorszám szélességét figyelembe véve állítjuk be. if ci == 0: ebx.config(width=len(str(num_of_rows)) + 2, **headers_common_params) if ri > 0: ebx_vars[(ri, ci)][0].set(ri) # A beviteli mezőket lehelyezzük a rács sor- és oszlopindexekkel maghatározott pozíciójába. ebx.grid(row=ri, column=ci, sticky='we') # Események és eseménykezelők összerendelése és beviteli mezőkhöz kötése. # Egy cellában az Enter megnyomására kiértékeli a cell tartalmát, majd a kurzort az alatta levő # cellába viszi. Ha nincs alatta cella, akkor a jobbra mellette levőbe. Ha az sincs, akkor a cellában hagyja. # Végül, a táblázat összes celláját, amiben van képlet, újra kiértékeli, hogy az esetleges változások érvényesüljenek. root.bind_class('Entry', '<Key Return>', eval_cell_content_even_handler) root.bind_class('Entry', '<Key Return>', move_cursor, add=True) root.bind_class('Entry', '<Key Return>', update_expressions, add=True) # Ha a celláról elvesszük a fókuszt (Tab gomb lenyomással vagy másik cellába kattintással), akkor hasonlóan, mint az # Enter esetén az adott cella értéke meghatározásra kerül, majd ennek ismeretében a tábla összes képlete újraszámolódik. root.bind_class('Entry', '<FocusOut>', eval_cell_content_even_handler) root.bind_class('Entry', '<FocusOut>', update_expressions, add=True) # Ha a cellában kétszer kattintunk, akkor előjön a cella értékét meghatározó képlet, ha volt ilyen. root.bind_class('Entry', '<Double ButtonPress 1>', reveal_cell_expression_event_handler) # A Del gomb hatására a cella tartalma és a mögöttes képlet (ha volt) törlődik. root.bind_class('Entry', '<Key Delete>', on_delete) # A jobb egérgombbal duplán kattintva egy cellán, az adott oszlop szélessége úgy változik, hogy # az oszlop cellái közül a leghosszabb szöveg is látszódjon. root.bind_class('Entry', '<Double Button 3>', fit_column_width) root.mainloop() |
A következő bejegyzésben már az eseménykezelő függvények megvalósításával foglalkozunk.
E bejegyzéshez kapcsolódóan a Python tudásépítés lépésről lépésre című e-könyvben elsősorban a „Grafikus felhasználói felület készítése” fejezetben az „Események és programkód összerendelése” című alfejezetet érdemes tanulmányozni.