Az előző bejegyzésben az MVC szerkezetre épülő, nem grafikus felhasználói felületű alkalmazásra adtunk egy szemléltető példát, ahol a megfigyelő programtervezési mintát (observer design pattern) alkalmaztuk a megjelenítést végző objektumok értesítésére, hogy a modell állapota megváltozott, és ennek megfelelően módosuljon a megjelenítés.
Mindazonáltal az MVC architektúra leginkább a grafikus felhasználói felületet (GUI) kínáló alkalmazásokban használatos, ezért most az előző bejegyzésben szereplő modellhez (gépjármű gyorsítása, lassítása) elkészítjük a GUI-t.
Ahogy arról előzőleg szó volt, az MVC struktúra előnye, hogy a felhasználói felületet anélkül tudjuk változtatni, hogy a modellt képviselő kódot módosítani kellene. Ez a helyzet a példánk esetében is, vagyis a modell kódja nem változik, amit itt mutatunk:
|
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 |
# modul: model from abc import ABC, abstractmethod class Subscriber(ABC): """A feliratkozók ezt öröklik, hogy mindegyik példányban egységesen ugyanaz a metódus szolgáljon az értesítés fogadására.""" @abstractmethod def update_view(self, *args, **kwargs): raise NotImplementedError class Publisher: def __init__(self): # A feliratkozókat nyilvántartó halmaz. self._subscribers = set() def add_subscriber(self, subscriber: Subscriber): """Az argumentumként megadott feliratkozót felveszi a nyilvántartásba.""" self._subscribers.add(subscriber) def remove_subscriber(self, subscriber: Subscriber): """Az argumentumként megadott feliratkozót törli a nyilvántartásból.""" self._subscribers.discard(subscriber) def notify_subscribers(self, *args, **kwargs): """ Minden feliratkozó értesítést kap, amelyben átadásra kerül a változásssal érintett érték. """ for subscriber in self._subscribers: subscriber.update_view(*args, **kwargs) class Car(Publisher): def __init__(self): super().__init__() self.speed: float = 0 def accelerate(self, delta_speed: float = 1): self.speed += delta_speed if self.speed < 0: self.speed = 0 self.notify_subscribers(self.speed) |
A programunkban az MVC egyes összetevőit külön modulokba szerveztük, ami az áttekinthetőséget javítja. Ennek megfelelően a Car, Publisher és Subscriber osztályokat tartalmazó modell a model.py modulfájlba kerül.
A Car példány aktuális sebességét az előző bejegyzésben foglaltakhoz hasonlóan két módon jelenítjük meg: vizuálisan és szövegesen. Az erre szolgáló két osztály nevén sem változtatunk, vagyis a két View osztály a CarSpeedView1 és CarSpeedView2 lesz most is. Az implementációjuk természetesen eltér az előzőkétől, viszont az nem változik, hogy mindketten megfigyelő objektumok lesznek, ezért a Subscriber osztályt öröklik. Ami a grafikus megvalósítást illeti, alapból mindketten a tkinter modul Frame objektumai lesznek, ezért a Frame osztályt is öröklik. Mivel Pythonban lehetőség van a többszörös öröklésre (multiple inheritance), így a CarSpeedView1 és CarSpeedView2 mind a Frame, mind a Subscriber osztályokat örökölheti problémamentesen, hiszen ez utóbbi két osztálynak nincs azonos metódusa.
A CarSpeedView1 a sebességérték vizuális megjelenítését egy értékskálán mutatja, amit a tkinter Scale grafikus objektumával valósítunk meg. A CarSpeedView2 a sebességre vonatkozó szöveges tájékoztatást egy címke (Label) grafikus elemen jeleníti meg. Itt nem csak az aktuális sebességről kapunk információt, hanem egy figyelmeztető szöveg is ki lesz írva akkor, ha az aktuális pillanatnyi sebesség a megadott sebességkorlátot túllépi.
Mindkét osztályt a view_gui.py modulban definiáljuk, amelyek definícióit alább láthatjuk:
|
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 |
# modul: view_gui from model import Subscriber import tkinter as tk class CarSpeedView1(tk.Frame, Subscriber): """Az aktuális sebességet vizuálisan jeleníti meg egy értékskálán.""" def __init__(self, master): super().__init__(master, bg='black') self.carspeedview1_var = tk.StringVar(master, '') # Az értékskála kontrollváltozója. self.scale = tk.Scale(self, orient=tk.HORIZONTAL, label='Sebesség km/h', variable=self.carspeedview1_var, from_=0, to=150, resolution=1, tickinterval=30, sliderlength=5, width=30, fg='blue', font=('Segoe UI', 10, 'bold')) self.scale.pack(fill=tk.X, padx=1, pady=1) def show(self, speed): self.carspeedview1_var.set(speed) def update_view(self, data): self.show(data) class CarSpeedView2(tk.Frame, Subscriber): """Az aktuális sebességet szövegesen jeleníti meg egy címkén, és figyelmeztet, ha a megadott sebességet túllépjük.""" def __init__(self, master): super().__init__(master, bg='black') self.speed_limit = 130 # Maximális megengedett sebesség. self.carspeedview2_var = tk.StringVar(master, '\n') # Az címke kontrollváltozója. self.lbl2 = tk.Label(self, bg='white', textvariable=self.carspeedview2_var, font=('Calibre', 10, 'bold')) self.lbl2.pack(fill=tk.BOTH, padx=1, pady=1) def show(self, speed): txt = f"A sebességed {speed} km/h" warning_text = '' if speed > self.speed_limit: warning_text = f'Túllépted a megengedett maximális {self.speed_limit} km/h sebességet!' self.carspeedview2_var.set('\n'.join((txt, warning_text))) def update_view(self, data): self.show(data) |
Ugyanebben a modulban definiáljuk azon objektum osztályát (DataInputGUI), amely az autó gyorsításának/lassításának értékét a felhasználótól fogadni képes, és egy felhasználói eseményre, amely jelen esetben egy gombnyomás, kezdeményezi (a Controller-en keresztül) a modell állapotának (autó sebessége) módosítását a beviteli mező aktuális értéke alapján.
Az eddig említett osztályok (DataInputGUI, CarSpeedView1 és CarSpeedView2) felelősek tehát a felhasználói adatbevitel lekezeléséért, valamint a modell aktuális állapotának eltérő megjelenítésért. Ahhoz, hogy ezek működjenek, egy főablakot megjelenítő osztály részeivé kell tenni. Ezt a MainWindow nevű osztályt szintén a view_gui.py modulban definiáljuk. A DataInputGUI és MainWindow osztályokat a következő programkód mutatja:
|
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 |
# modul: view_gui class DataInputGUI(tk.Frame): def __init__(self, master): super().__init__(master) self.controller = None # Beviteli mező létrehozása és lehelyezése. self.accelerate_var = tk.StringVar(master, '15') self.accelerate_entry = tk.Entry(self, textvariable=self.accelerate_var, font=('Consolas', 12, 'bold'), width=5) self.accelerate_entry.pack(side=tk.LEFT) # Nyomógomb létrehozása és lehelyezése. self.accelerate_button = tk.Button(self, text='Accelerate', command=self.process_input, repeatinterval=400, repeatdelay=400) self.accelerate_button.pack(side=tk.LEFT) def set_controller(self, controller_obj): self.controller = controller_obj def process_input(self): """A nyomógomb lenyomásakor meghívott metódus, amely a controlleren keresztül kezdeményezi a modell állapotának módosítását a beviteli mező értéke alapján. """ self.controller.change_model_state(self.accelerate_var.get()) class MainWindow(tk.Tk): def __init__(self): super().__init__() self.title('MVC') self.geometry('400x200') # A megjelenítésért és adatbevitelért felelős grafikus objektumok létrehozása és lehelyezése. self.datainput_gui = DataInputGUI(self) self.views = [CarSpeedView1(self), CarSpeedView2(self)] self.datainput_gui.pack() for view in self.views: view.pack(fill=tk.BOTH, padx=5, pady=5) def set_controller(self, controller): self.datainput_gui.set_controller(controller) def run(self): self.mainloop() |
Végül nem maradt más hátra, mint a Controller osztály definiálása. Ez lényegében abból áll, hogy eltárolja a modell objektum (Car példány) referenciáját és a View objektumokat (CarSpeedView1, CarSpeedView2) felveszi a modell értesítési listájára. Egyetlen metódusával a modell állapotváltozását lehet kezdeményezni, a sebességváltozás értékének megadásával. A main_controller.py modulba kerülő Controller osztály látható alább. E modul egyben a fő modul is, ami a létrehozza a MainWindow és Controller objektumokat és elindítja az alkalmazá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 |
from view_gui import MainWindow from model import Car class Controller: def __init__(self, main_gui, car:Car): # A modell referenciájának létrehozása. self._model = car # A megfigyelő View példányok felvétele a modell mint megfigyelt objektum # értesítési listájára. for view in main_gui.views: self._model.add_subscriber(view) def change_model_state(self, delta_speed): self._model.accelerate(float(delta_speed)) # Alkalmazás indítása. main_window = MainWindow() main_window.set_controller(Controller(main_window, Car())) main_window.run() |
A következő ábrán a megjelenő kezdőablak látható, valamint azon fázisok képei, amikor a gyorsítást kezdeményező gombot nyomogatjuk, vagy nyomva tartjuk. Példát láthatunk arra, amikor túllépjük a sebességlimitet, amit utána lassítással hozunk vissza a megengedett tartományba.

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 következő részei kapcsolódnak: az „Osztály vigyázz! – típuslétrehozás osztályokkal” fejezet, a „Készétel fogyasztás a szabványos könyvtár moduljainak használata” fejezet „Absztrakt osztályok” alfejezet, az „Öröklődés” fejezeten belül a „Öröklés több szülőtől” alfejezet, a „Grafikus felhasználói felület készítése” fejezet.