A legutóbbi cikkben a számolótábla fejlesztésének második fázisát fejeztük be azzal, hogy végigvettük a tábla specifikáció szerinti működését lehető eseményeket, és az egyes eseményekhez hozzárendeltünk meghatározott nevű eseménykezelő függvényeket. Most áttérünk a harmadik fázisra, és ezen eseménykezelők megvalósításába kezdünk bele.
Elsőnek a cellatartalom kiértékelését végző eval_cell_content_event_handler() eseménykezelő és ehhez kapcsolódó függvények implementációjával foglalkozunk. Ez az eseménykezelő az Enter lenyomásának, vagy az adott cella fókuszból kikerülésének hatására fut le. Ennek eredményeképpen – ha az adott cellában egy kifejezés szerepel – az kiértékelődik. Ha ez sikeres, vagyis érvényes kifejezést írtunk a cellába, akkor annak tartalma az eredmény lesz. Ha azonban a képlet/kifejezés valamilyen szempontból érvénytelen, illetve hibás, akkor a cellában a hiba oka fog megjelenni.
Alább az eval_cell_content_event_handler() eseménykezelőt látjuk, feltüntetve a releváns kódkörnyezetet is.
|
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 |
import tkinter as tk from itertools import product root = tk.Tk() root.title('Számolótábla') 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) tároló szótár. ebx_vars = {} def eval_cell_content_event_handler(e): """Az eseménnyel érintett beviteli mező tartalmát kiértékeli meghívva az erre szolgáló függvényt.""" ebx = e.widget # Az eseménnyel érintett beviteli mező. eval_cell_content(ebx) def eval_cell_content(entry_widget): """Kiértékeli a megadott beviteli mező tartalmát, és ha érvényes, akkor az eredményt beírja a mezőbe. Ha nem értékelhető ki, akkor a hibaüzenetet lesz a beviteli mező tartalma. """ def get_cell_indexes(entry_widget) -> tuple[int, int]: """A megadott beviteli mező grid elrendezésbeli sor- és oszlopindexeit adja vissza""" def find_cellref(s: str, pattern='cell(??)') -> str: """A megadott karakterláncban megkeresi az első olyan részkaraktersort, amely megfelel a pattern-ben megadottnak, ahol a ?? helyén tetszőleges karakterek állhatnak. """ def converted_cellref(cell_str) -> str: """A 'cell(B:1)' forma konvertálása 'cell(2,1)' formára""" def get_cell_numvalue(ci, ri): """A megadott oszlop- és sorindexű beviteli mező tartalmának megfelelő számértékét adja vissza, ha az számként értelmezhető. Ha nem, akkor magát a tartalmat. """ def convertible_to_int(s: str) -> bool: ... def convertible_to_float(s: str) -> bool: ... def convertible_to_complex(s: str) -> bool: ... # Táblázatrács kirajzolása. # ... # Események és eseménykezelők összerendelése és beviteli mezőkhöz kötése. root.bind_class('Entry', '<Key Return>', eval_cell_content_event_handler) root.bind_class('Entry', '<FocusOut>', eval_cell_content_event_handler) # ... root.mainloop() |
Láthatjuk azt a szótárt, amely a táblázat beviteli mezőinek kontrollváltozóit, valamint a cellába beírt képletet (kifejezést) tárolja, valamint alul az eseményekhez való hozzárendelést. Ezek nem új dolgok, mert az előző bejegyzések ezekről szóltak. Ami új, azok a segédfüggvények, amelyek a kiértékelés egy-egy részfeladatát végzik el. Itt az áttekinthetőség kedvéért csak a függvények fejléce jelenik meg a függvény feladatának leírásával. Annyit azonban most is lehet látni, hogy maga az eseménykezelő nagyon egyszerű, és lényegében az eseménnyel érintett beviteli mezőt azonosítja, majd ezzel meghívja az eval_cell_content() függvényt. Valójában ez végzi az érdemi munkát. Az összes alatta felsorolt függvény neki dolgozik be.
A cellába írt, karakterláncként rendelkezésre álló képlet kiértékelését az eval() beépített függvénnyel végezzük. Ezért az eval_cell_content() függvény nagyon egyszerű lehetne, ha nem lenne a kifejezésben cellahivatkozás pl. cell(B:3) formában. Ez azonban ebben a formában nem értelmezhető az eval() számára, így minden olyan képlet eseténben, ahol cellahivatkozást írunk, hibajelzést kapnánk. Ezért az eval_cell_content() függvény kódja döntően arra irányul, hogy a cellahivatkozásokat 1) azonosítsa, majd pedig 2) átalakítsa egy olyan függvényhívássá, amelyet az eval() már ki tud értékelni. Az eval_cell_content() függvény definíciója a következő. A kommentek részletesek, hogy követni lehessen a logikát és működést.
|
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 |
def eval_cell_content(entry_widget): # Az argumentumban kapott beviteli mező kontrollváltozójának kikérése. ebx_var = ebx_vars.get(get_cell_indexes(entry_widget))[0] # A kontrollváltozó értékének, vagyis a beviteli mező tartalmának kikérése. cv = ebx_var.get() # Ha a mező nem üres, és az első karakter egy = jel, akkor kiértékeljük a tartalmát. if cv != '' and cv[0] == '=': # A továbbiakban az = jel utáni karaktersorral dolgozunk. expr = cv[1:] # Ha van kifejezést meghatározó karaktersor az = jel után, akkor azt az egyenlőségjellel együtt # eltároljuk, majd megkíséreljük kiértékelni. if expr: # Eltároljuk a nem üres kifejezést az adott cellához, hogy később elő lehessen hívni # megtekintéshez vagy szerkesztéshez. ebx_vars.get(get_cell_indexes(entry_widget))[1] = cv # Megkeressük az összes "cell(B:3)" formájú cellahivatkozást, ha van. while cellref_found := find_cellref(expr): # A "cell(B:3)" formájú cellahivatkozást jelentő karaktersort "get_cell_numvalue(2,3)" formára cseréljük. cellvalue = converted_cellref(cellref_found).replace('cell', 'get_cell_numvalue') # A kifejezésben minden "cell(B:3)" formájú karaktersort "get_cell_numvalue(2,3)" formára cseréljük, hogy # A kiértékeléskor a get_cell_numvalue() függvényt lehessen meghívni. expr = expr.replace(cellref_found, cellvalue) # Megkíséreljük kiértékelni a kifejezést az eval() beépített függvény segítségével. try: # A sikeres kiértékelés eredményét beírjuk a cellába. ebx_var.set(eval(expr)) except Exception as exc: # Ha valamiért hiba merül fel a kiértékelés során, akkor a hiba okát írjuk a cellába. ebx_var.set(type(exc).__name__) |
Ahogy említettük, az eval_cell_content() több más függvényt is használ. Ezek definícióit 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 |
def get_cell_indexes(entry_widget): """A megadott beviteli mező grid elrendezésbeli sor- és oszlopindexeit adja vissza""" gi = entry_widget.grid_info() return gi.get('row'), gi.get('column') def find_cellref(s: str, pattern='cell(??)'): """A megadott karakterláncban megkeresi az első olyan részkaraktersort, amely megfelel a pattern-ben megadottnak, ahol a ?? helyén tetszőleges karakterek állhatnak. """ eleje, vége = pattern.split('??') if eleje in s: k = s.find(eleje) v = s[k + len(eleje):].find(vége) return sub if ':' in (sub := s[k:k + len(eleje) + v + 1]) else '' return '' def find_cellref(s: str, pattern=r'cell\([A-Z]*:[0-9]*\)'): mo = re.search(pattern, s) return mo.group() if mo else '' def converted_cellref(cell_str) -> str: """A 'cell(B:1)' konvertálása 'cell(2,1)' formára""" arg: str = cell_str.strip('cell()') args = arg.split(':') ci, ri = ord(args[0].upper()) - ord('A') + 1, int(args[1]) return cell_str.replace(arg, f'{ci},{ri}') def get_cell_numvalue(ci, ri): """A megadott oszlop- és sorindexű beviteli mező tartalmának megfelelő számértéket adja vissza, ha az számként értelmezhető. Ha nem, akkor magát a tartalmat. """ cell_value: str = ebx_vars[(ri, ci)][0].get() if convertible_to_int(cell_value): return int(cell_value) elif convertible_to_float(cell_value): return float(cell_value) elif convertible_to_complex(cell_value): return complex(cell_value) else: return cell_value def convertible_to_int(s: str)->bool: try: int(s) return True except ValueError: return False def convertible_to_float(s: str)->bool: try: float(s) return True except ValueError: return False def convertible_to_complex(s: str)->bool: try: complex(s) return True except ValueError: return False |
Az első ilyen segédfüggvény a get_cell_indexes(). Ennek feladata, hogy azonosítsa, hogy a megadott beviteli mezőhöz a grid elrendezésben melyik sor- és oszlopindex tartozik. Ezekből képzett tuple lesz a visszatérési értéke, amely int értékeket a beviteli mezőre meghívott grid_info() metódus eredményeképpen kapott szótárból lehet megkapni.
A következő igénybe vett függvény a find_cellref(). Ez az első argumentumként megadott karakterláncban megkeresi az első olyan részkaraktersort, amely megfelel a pattern értékeként megadottnak, ahol a ?? helyén tetszőleges karakterek állhatnak. Jelen esetben tehát az első olyan karaktersorozattal visszatér, amely kezdete „cell(„ és a vége „)”. A find_cellref() megvalósítására két változatot is mutat az ábra. A másodikat azok kedvéért, akik a reguláris kifejezésekben jártasak. Ehhez a szabványos könyvtár re nevű beépített modulját be kell importálni. Egyébként ez az eset arra is példát mutat, hogy ha van egy egyszerű feladat (jelent esetben egy egyszerű mintakeresés), és nem ismerjük még az erre specializált modult, akkor csupán ehhez nem mindig kell megtanulni a modul használatát, mert némi gondolkodás után alapelemekből felépítve is meg lehet oldani a feladatot.
Nos, ha a find_cellref() talál megfelelő részkaraktersort, és az eleje és vége között a cellahivatkozásunk szintaxisának megfelelő formában megadott oszlop és sorazonosító van pl. B:3, akkor a converted_cellref() függvény ezt úgy alakítja át, hogy az oszlop is számmal legyen megadva. Tehát a példánál maradva a converted_cellref() visszatérési értéke „cell(2,3)” lesz. Ez már függvényhívásnak néz ki, amit az eval() tudna értelmezni. De ha cell() néven definiálnánk egy függvényt az nem lenne jó, mert akkor a mintakeresés ezt újra megtalálná. Ezért a „cell” karaktersort lecseréljük annak a függvénynek a nevére, amely a hivatkozott cella értékét fogja szolgáltatni. Ez pedig a get_cell_numvalue() függvény.
A get_cell_numvalue() függvény egy oszlop- és sorindexszel azonosított beviteli mező tartalmának megfelelő számértéket adja vissza, ha az int, float vagy complex típusú számként értelmezhető. Ha nem, akkor magát a tartalmat mint karakterláncot adja vissza. Abban, hogy egy karaktersor értelmezhető-e int, float vagy complex számként, a convertible_to_int(), convertible_to_float() és convertible_to_complex() függvények segítenek.
Mindennek eredményeképpen kialakult karakterlánc kerül kiértékelésre az eval_cell_content() függvény kivételkezelő szerkezetén belül. És ahogy korábban mondtuk, ha a kiértékelés sikeres, akkor az adott beviteli mező tartalma a kifejezés értéke lesz. Ha azonban a kiértékelés valamiért hibába ütközik, akkor a hiba típusa jelenik meg cellatartalomként. Ekkor a képletet elő lehet hívni és lehet szerkeszteni, javítani. De ez már egy másik eseményhez tartozik, amit a következő bejegyzésben tárgyalunk.
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 karakterláncok műveleteit (különösen a szeletképzést) és metódusait érdemes átnézni a „Műveletek” és „Beépített típusok nyilvános metódusai” fejezetekben, továbbá a kivételkezelést a „Kivételes bánásmód – kivételek és kezelésük” fejezetben.