TI/Programowanie dla Fizyków Medycznych:Eventy
Programy napisane w Pythonie większość czasu czekają na nadejście eventów i niekiedy reagują na nie - eventy mogą być generowane przez czynności użytkownika takie jak kliknięcie myszką czy wciśnięcie klawisza lub przez sam program. W tym rozdziale opiszę jakie rodzaje eventów występują w wxPythonie, jak wiąże się metody z wystąpieniem evetów, jak wygląda poszukiwanie kodu obsługującego dany event i w końcu jak pisać własne eventy. Klasa wx.EvtHandler (i jej klasy pochodne) mogą reagować na nadejście eventów.
Do obsługi eventów wykorzystuje się normalne metody klasy, które możemy napisać sami, jedyny warunek jest taki, że muszą one przyjmować (poza self) dokładnie jeden parametr - na niego zostanie przekazany event. Łączenia eventu z obsługującą go metodą dokonuje się przy pomocy metody Bind - ma ona dwa parametry, które musimy podać: event - obiekt klasy event binder określający klasę i typ eventu z którym chcemy związać pewną metodę i handler - metoda którą chcemy związać z wystąpieniem eventu. Jeśli podamy tylko dwa argumenty to połączymy eventy pochodzące od obiektu self, jeśli mają one pochodzić od innego obiektu to należy podać trzeci parametr source - obiekt od którego eventy mają być odbierane lub id - id obiektu od którego spodziewamy się eventów. Metodę Bind można też wywołać z czterema argumentami, poza event i handler, id i id2 - wtedy odbierane będą eventy danego typu od wszystkich obiektów o numerach id od id od id2.
Jak wiemy obiekty takie jak wx.TextCtrl muszą znajdować się w jakiś kontenerach (np wx.Panel), a ten z kolei często znajduje się w innym panelu, obiekcie klasy wx.Dialog czy wx.Frame itd aż w końcu nad wszystkimi obiektami jest obiekt klasy wx.App czyli aplikacja. Na każdym z tych poziomów może znajdować się metoda połączona z danym eventem. Każdy obiekt klasy wx.EvtHandler można uczynić głuchym na nadchodzące eventy (czyli dezaktywować) przy pomocy metody wx.EvtHandler.SetEvtHandlerEnabled(self, bool) dając jako argument False. W wxPythonie mamy dwa typy eventów - propagujące się (dziedziczą po wx.CommandEvent) i niepropagujące się (dziedziczą po wx.Event ale nie po wx.CommandEvent). Po zgłoszeniu eventu następuje szukanie metody, która jest połączona z danym eventem, po znalezieniu pierwszej takiej metody uznaje się, że event został obsłużony i nic dalej się nie dzieje, chyba, że na evencie wywołano metodę wx.Event.Skip(self) - wtedy prowadzone są dalsze poszukiwania. Opiszę teraz jak przebiegają te poszukiwania.
Gdy do pewnego obiektu dociera event i jest on aktywny (w sensie metody SetEvtHandlerEnabled) to sprawdzane jest czy w danej instancji jest metoda połączona z danym eventem, jeśli tak to jest wykonywana. Jeśli nie wywołano Skip na evencie to sprawdzany jest jeszcze obiekt klasy wx.App i jeśli tam znaleziono metodę związaną z danym eventem to jest ona wykonywana po czym życie eventu się kończy. Wynika z tego, że jeśli w instancji klasy wx.TextCtrl połączymy event wx.EVT_LEFT_DOWN z jakąś metodą i nie wywołamy Skip to nie zostaną wywołane metody istniejące wcześniej w klasie wx.TextCtrl i nadklasach, a wiec nie zostanie przestawiony wskaźnik zachęty, dlatego dopisując obsługę do eventów na poziomie instancji zazwyczaj należy wywołać Skip. Jeśli w danym obiekcie i nadklasach nie została znaleziona metoda połączona z danym eventem to jeśli event był niepropagujący się to sprawdzany jest tylko obiekt klasy wx.App po czym życie obiektu się kończy, natomiast w przypadku eventów propagujących się sprawdzany jest kontener w którym znajdował się aktualny obiekt. Te idee dobrze prezentuje poniższy kod (wx.EVT_BUTTON jest podklasą wx.CommandEvent, a wx.EVT_CHAR nie), proszę sprawdzić i zrozumieć działanie kodu po odkomentowaniu linii dezaktywujących poszczególne obiekty, sprawdzić co się stanie po wykomentowaniu instrukcji Skip i odkomentowaniu Disable:
import wx
class MyFrame(wx.Frame):
def __init__(self, *a, **b):
super(MyFrame, self).__init__(*a, **b)
panel = wx.Panel(self)
hBox = wx.BoxSizer(wx.HORIZONTAL)
txt = wx.TextCtrl(panel)
button = wx.Button(panel)
hBox.Add(txt)
hBox.Add(button)
panel.SetSizer(hBox)
self.Show()
button.Bind(wx.EVT_BUTTON, self.OnInstanceButton) #bindowanie na poziomie instancji
panel.Bind(wx.EVT_BUTTON, self.OnPanelButton, button) #bindowanie na poziomie kontenera zawierającego instancję
self.Bind(wx.EVT_BUTTON, self.OnSelfButton, button) #bindowanie na poziomie kontenera zawierającego kontener zawierający instancję
txt.Bind(wx.EVT_CHAR, self.OnInstanceButton, txt)
panel.Bind(wx.EVT_CHAR, self.OnPanelButton, txt)
self.Bind(wx.EVT_CHAR, self.OnSelfButton, txt)
#txt.Disable()
#button.Disable()
#txt.SetEvtHandlerEnabled(False)
#button.SetEvtHandlerEnabled(False)
#panel.SetEvtHandlerEnabled(False)
#self.SetEvtHandlerEnabled(False)
def OnSelfButton(self, evt):
wx.MessageBox("frame")
evt.Skip()
def OnPanelButton(self, evt):
wx.MessageBox("panel")
evt.Skip()
def OnInstanceButton(self, evt):
wx.MessageBox("instance")
evt.Skip()
class MyApp(wx.App):
def OnInit(self):
myFrame = MyFrame(None)
self.Bind(wx.EVT_CHAR, self.OnApp)
self.Bind(wx.EVT_BUTTON, self.OnApp)
#self.SetEvtHandlerEnabled(False)
return True
def OnApp(self, evt):
wx.MessageBox("app")
app = MyApp()
app.MainLoop()
Własne eventy należy pisać jako klasy dziedziczące po wx.PyEvent (jeśli event ma być niepropagujący się) lub wx.PyCommandEvent (jeśli event ma być propagujący się), klasa nie musi spełniać żadnych wymagań poza tym, że konstruktor musi przyjmować dwa parametry commandType - typ eventu i winid - id obiektu zgłaszającego event. Po utworzeni klasy należy wygenerować dla niej nowy typ eventu (aby nie pokrywać się z żadnym już istniejącym) przy pomocy funkcji wx.NewEventType() i utworzyć event binder przy pomocy wx.PyEventBinder(eventType, ids) gdzie ids to 0, 1 albo 2 - odpowiada liczbie id, którą będziemy później podawać w wywołaniu metody Bind. Aby nasz event wstawić do kolejki eventów należy utworzyć obiekt reprezentującej go klasy i podać go jako argument do metody ProcessEvent w wx.EventHandler:
import wx
class MyEvt(wx.PyCommandEvent):
def __init__(self, evtType, id):
super(MyEvt, self).__init__(evtType, id)
myEvtType = wx.NewEventType() #tworzenie nowego typu
EVT_MYEVT = wx.PyEventBinder(myEvtType, 1) #tworzenie event bindera dla eventu danego typu i danej klasy
class MyPanel(wx.Panel):
def __init__(self, *a, **b):
super(MyPanel, self).__init__(*a, **b)
but1 = wx.Button(self, 10, label = "Przycisk 1")
but2 = wx.Button(self, 11, label = "Przycisk 2")
hBox = wx.BoxSizer(wx.HORIZONTAL)
hBox.Add(but1)
hBox.Add(but2)
self.SetSizer(hBox)
self.pressed = {but1.GetId() : False, but2.GetId() : False}
self.Bind(wx.EVT_BUTTON, self.OnButtonPressed, id = 10, id2 = 11)
def OnButtonPressed(self, evt):
self.pressed[evt.GetId()] = True
self.OnClick()
def OnClick(self):
if all(self.pressed.values()):
for button in self.pressed:
self.pressed[button] = False
myEvt = MyEvt(myEvtType, self.GetId())
self.GetEventHandler().ProcessEvent(myEvt)
class MyFrame(wx.Frame):
def __init__(self, *a, **b):
super(MyFrame, self).__init__(*a, **b)
self.panel = MyPanel(self)
self.Show()
self.Bind(EVT_MYEVT, self.OnMyEvt, self.panel)
def OnMyEvt(self, evt):
wx.MessageBox("Oba przyciski kliknięte")
app = wx.App()
MyFrame(None)
app.MainLoop()