2007.04_Events Controller–kontroler zdarzeń_[Programowanie].pdf

(1229 KB) Pobierz
439031205 UNPDF
dla programistów
Events Controller – kontroler zdarzeń
– kontroler zdarzeń
Piotr Pszczółkowski
W artykule chciałbym zaprezentować własną ideę kontrolera zdarzeń (zdarzenie – ang. Event ). Idea
zdarzeń jest powszechnie znana i stosowana w systemach operacyjnych komputerów. Dla nas jednak
bardziej interesujący jest fakt, że systemy obsługi graicznego interfejsu użytkownika (GUI) w takich
środowiskach graicznych jak KDE czy GNOME także wykorzystują zdarzenia jako sposób komunikacji.
Programista piszący oprogramowanie dla tych środowisk powinien umieć je obsłużyć. Za zdarzenie
można uznać w zasadzie prawie wszystko, zazwyczaj jest to ruch myszką czy naciśnięcie klawisza.
jak by to było, gdybym zastosował po-
dobny mechanizm we własnych pro-
gramach, z deiniowanymi przeze mnie
zdarzeniami. Po namyśle stwierdziłem, że sterowanie
przebiegiem wykonania programu poprzez wysyłanie
i odbieranie zdarzeń ma same plusy. Nie dopatrzyłem
się minusów. Dzięki mechanizmowi zdarzeń kod pro-
gramu staje się krótszy, zdecydowanie mniej skompli-
kowany (i co za tym idzie – bardziej zrozumiały) oraz
dużo łatwiejszy w dalszej rozbudowie. Ponieważ piszę
programy przy użyciu biblioteki Qt irmy Trolltech, za-
prezentuję moją implementację, którą wykonałem z uży-
ciem mechanizmów oferowanych przez tę właśnie biblio-
tekę.
cuje w innej irmie. W takiej sytuacji musimy znać kogoś,
kto zna panią B. Jeszcze gorzej jest, jeśli ten łańcuch zna-
jomości się wydłuża. Zaczyna to przypominać zabawę
w „głuchy telefon“. Kolejne osoby przekazują informację
innym, znajdującym się w łańcuchu znajomości za nimi.
Aż do celu, czyli do pani B. Odpowiedź oczywiście mo-
Listing 1. Przykład handlera
//********************************
// showEvent PRIVATE inherited
//********************************
void DbgRegisters :: showEvent ( QShowEvent *
const in_event )
{
DbgToolDialog ::
showEvent ( in_event ); // ( 1 )
// nasza wlasna reakcja na zdarzenie
..........
}
// end of showEvent
Jak to się zazwyczaj robi
Zanim przejdziemy do meritum, chciałbym przedstawić
pewną analogię ze świata ludzi. Nie ma problemu, gdy
pan A chce coś powiedzieć pani B, gdy ta siedzi w pra-
cy w pokoju obok niego (zakładamy, że nie ma telefonów
i e-maili). Sytuacja jest znacznie gorsza, jeśli pani B pra-
44
maj 2007
Events Controller
W pewnym momencie zastanawiałem się,
439031205.036.png 439031205.037.png 439031205.038.png 439031205.039.png
 
dla programistów
Events Controller – kontroler zdarzeń
że wrócić tą samą drogą lub też przez łań-
cuszek innych znajomości. W programach
komputerowych jest podobnie. Jak wiado-
mo, żaden większy program nie wykonu-
je się jednym ciągiem (w funkcji main). Pro-
gramista rozdziela to, co ma wykonać pro-
gram, na logiczne segmenty, które zawar-
te są w funkcjach. Funkcje są to bloki kodu
przeznaczone do wykonania ściśle określo-
nych zadań. Programista poprzez wywoły-
wanie funkcji powoduje wykonywanie ko-
lejnych podzadań, koniecznych w określo-
nych sytuacjach dla prawidłowego wyko-
nania zadania postawionego przed progra-
mem jako całością. Obecnie w dobie obiek-
towości podział na podzadania poszedł da-
lej. Teraz określone zadania dotyczące kon-
kretnych obiektów ukryte są w klasach. Wy-
konanie określonych czynności odbywa się
poprzez interfejs udostępniany przez klasę.
Interfejs klasy to po prostu funkcje publicz-
ne, które można wywoływać z innych obiek-
tów i funkcji. Krótko mówiąc – przebieg wy-
konywania zadania głównego odbywa się
poprzez aktywowanie kolejnych obiektów
stosownego typu (klasa to typ). I tu poja-
wia się problem, nad którym chciałbym się
na chwilę zatrzymać. W każdym w miarę
dużym programie ciąg wywołań kolejnych
obiektów(/funkcji) może być bardzo, bar-
dzo długi. Tak długi, że czasami trzeba so-
bie uzmysłowić to stosownymi diagramami
UML. Zaczyna się pojawiać problem nad za-
panowaniem nad setkami lub tysiącami wy-
wołań różnych obiektów w różnych miej-
scach. Nie ma problemu, kiedy obiekt kla-
sy A wywołuje funkcje obiektu klasy B, je-
śli go zna, tzn. obiekt A sam utworzył obiekt
B (tzn. w obiekcie A utworzono obiekt B na
stosie lub poprzez new). Ale powstaje pro-
blem, jeśli chcemy wywołać funkcję obiek-
tu klasy M, który w hierarchii zależności
(A, B, C, ......., M) znajduje się daleko, dale-
ko. Obiekt klasy M nie jest znany w obiekcie
A. Sytuacja może się jeszcze bardziej skom-
plikować, jeśli to obiekt M chce wywoływać
funkcję obiektu A, ale także obiektów F, H
i N. Można to oczywiście wykonać. Ale żad-
na z implementacji nie jest tak elegancka
jak implementacja za pomocą zdarzeń. Po
pierwsze można w jednym z plików źródło-
wych programu utworzyć zmienne globalne,
w których będziemy przechowywać wskaź-
niki do stosownych obiektów. Ale zmienne
globalne są źle widziane. Duża liczba zmien-
nych globalnych świadczy o złym stylu pro-
gramowania. Zmiennych globalnych stara-
Listing 2. Klasa zdarzeń Event
#ifndef INCLUDED_Event_h
#deine INCLUDED_Event_h
/*------- include iles:
------------------------------------------------*/
#include <QEvent>
#include <QString>
#include <QVariant>
#include <QVector>
/*------- class declaration:
-----------------------------------------------*/
class Event : public Qevent ( 1 )
{
// ******* TYPES *******
public :
enum {
OPEN_FILE_REQUEST = ( QEvent :: User + 1 ) , ( 2 )
NEW_FILE_REQUEST ,
SAVE_FILE_REQUEST ,
SAVE_FILE_AS_REQUEST ,
SAVE_ALL_FILES_REQUEST ,
PRINT_REQUEST ,
PROJECT_SELECTED_REQUEST ,
.....,
DBG_MEMORY_STOP ,
DBG_MEMORY_READ ,
DBG_MEMORY_DATA ,
} ;
public :
Event ( int const in_event_id )
: QEvent ( static_cast < QEvent :: Type >( in_event_id ))
{
d_data . reserve ( 0 );
}
Event ( const int in_event_id , const QVariant in_val
)
: QEvent ( static_cast < QEvent :: Type >( in_event_id ) )
{
d_data . reserve ( 1 );
d_data << in_val ;
}
Event ( const int in_event_id , const QVariant in_val1 ,
const QVariant in_val2 )
: QEvent ( static_cast < QEvent :: Type >( in_event_id ) )
{
d_data . reserve ( 2 );
d_data << in_val1 << in_val2 ;
}
Event ( const int in_event_id , const QVariant
in_val1 , const
QVariant in_val2 , const QVariant in_val3 )
: QEvent ( static_cast < QEvent :: Type >( in_event_id ) )
{
d_data . reserve ( 3 );
d_data << in_val1 << in_val2 << in_val3 ;
}
Event ( const int in_event_id , const QVariant
in_val1 , const
QVariant in_val2 , const QVariant in_val3 , const
QVariant in_val4 )
: QEvent ( static_cast < QEvent :: Type >( in_event_id ) )
{
d_data . reserve ( 4 );
d_data << in_val1 << in_val2
<< in_val3 << in_val4 ;
}
public :
QVector < QVariant > d_data ; ( 3 )
private :
Event ( const Event & );
Event & operator =( const Event & );
} ;
# endif // INCLUDED_Event_h
www.lpmagazine.org
45
439031205.001.png 439031205.002.png 439031205.003.png 439031205.004.png 439031205.005.png 439031205.006.png 439031205.007.png 439031205.008.png 439031205.009.png 439031205.010.png 439031205.011.png 439031205.012.png 439031205.013.png
 
dla programistów
Events Controller – kontroler zdarzeń
Listing 3. Funkcja main programu tworząca obiekt kontrolera
Każde zdarzenie jest określonego typu.
Jest to liczba zawarta w typie wyliczenio-
wym enum QEvent::Type . Zawiera ona wie-
le predeiniowanych typów używanych we-
wnętrznie przez bibliotekę Qt. Ale, co ważne,
istnieje możliwość deiniowania własnych ty-
pów zdarzeń. Prywatne zdarzenia użytkow-
nika muszą zawierać się w zakresie od QE-
vent::User (wartość 1000) do QEvent::MaxU-
ser (wartość 65535).
Zdarzenia wysyła się z użyciem funkcji:
//*******************************************************************
// main
//*******************************************************************
int main ( int in_argc , char ** in_argv )
{
....
EventsController :: instance (); // <<<<<< --------
....
}
// end of main
my się unikać. Więc to rozwiązanie odrzu-
camy. Druga metoda to utworzenie w każ-
dym obiekcie w ciągu (A,.....,M) funkcji po-
mocniczych wywołujących swój odpowied-
nik w następnym obiekcie w ciągu. Tak, aby
w końcu została wywołana stosowna funk-
cja obiektu M. Czyli mamy następującą sy-
tuację: aby wywołać jedną funkcję, wygene-
rujemy kilka, czy nawet dziesiątki, wywo-
łań pomocniczych. Można także zrobić tak,
aby obiekt A wywołał bezpośrednio funkcję
obiektu M. Ale, aby to było możliwe, trze-
ba jakoś przekazać do obiektu A wskaźnik
do obiektu M. Jeśli odrzucamy wersję ze
zmiennymi globalnymi, to pozostaje nam
ponownie ciąg funkcji przekazujących po-
między obiektami stosowny wskaźnik. Jest
to złe rozwiązanie. Nie dość, że tworzy-
my dużą liczbę funkcji pomocniczych, czy-
li wzrasta rozmiar programu, to jeszcze pro-
gram staje się coraz wolniejszy (każde wy-
wołanie kolejnej funkcji trwa przecież jakiś
czas). W Qt istnieje mechanizm signal/slot
lub signal/signal. Niestety to rozwiązanie
nie jest lepsze od poprzedniego. Znowu po-
jawia się konieczność kaskadowego łącze-
nia sygnałów i odpowiednich slotów w ca-
łym ciągu obiektów. Innym problemem mo-
że być sytuacja, gdy chcą się ze sobą komu-
nikować klasy, które nie są w tym samym
ciągu zależności. Trzeba tworzyć pomocni-
cze klasy do przekazywania informacji po-
między obiektami znajdującymi się w róż-
nych ciągach zależności. To, co powyżej opi-
sałem, to nie jedyny problem. Problemem,
i to dużym, staje się rozbudowa lub mody-
ikacja programu jako całości. Jeżeli zmieni-
my w jakiś sposób ciąg wzajemnych zależ-
ności obiektów bądź chcemy go rozbudo-
wać o nową funkcjonalność, trzeba pamię-
tać o konieczności zmodyikowania dzie-
siątek obiektów i funkcji. Dopisanie kolej-
nej klasy, z której chce korzystać obiekt A,
wymaga modyikacji wszystkich klas biorą-
cych udział w komunikacji. Usunięcie jednej
z klas wymaga usunięcia stosownych funk-
cji i ich wywołań we wszystkich klasach.
Możliwość zrobienia błędu czy zapomnienia
o czymś jest olbrzymia.
bool QCoreApplication::sendEvent(
QObject* receiver, QEvent* event )
oraz
Zdarzenia w Qt to podklasy
klasy QEvent
Jak już wspomniałem, systemy operacyjne
i graiczne (KDE, GNOME) wykorzystują
w swojej pracy tak zwane zdarzenia. Taką
możliwość udostępnia też swojemu użyt-
kownikowi biblioteka Qt. Z punktu widze-
nia programisty zdarzenie to obiekt, który
dziedziczy po abstrakcyjnej klasie QEvent.
W obiekcie mogą być zawarte informacje
(dane) dokładniej opisujące zdarzenie. Jeśli
będziemy wysyłać własne zdarzenia, to my
zdecydujemy, jakie informacje powinny się
w nim znaleźć. Zdarzenia mogą obsługiwać
wszystkie klasy pochodzące od QObject
(o tym należy pamiętać). Liczba wyspecja-
lizowanych zdarzeń w Qt jest naprawdę du-
ża: QMouseEvent , QKeyEvent , QPaintEvent ,
QTimerEvent itd. Zdarzenia w programie
napisanym w Qt obsługiwane są w tak zwa-
nej Pętli Obsługi Zdarzeń (Event-Loop).
Każdy interaktywny program musi posiadać
taką pętlę w obiekcie typu QApplication.
Także wygenerowane przez nas wątki (thre-
ad) mogą posiadać własne Pętle Obsługi
Zdarzeń. To ta właśnie pętla dba o rozsyła-
nie określonych zdarzeń do odpowiednich
obiektów.
void QCoreApplication::postEvent(
QObject* receiver, QEvent* event )
Różnica pomiędzy obiema funkcjami jest ta-
ka, że sendEvent wysyła zdarzenie do od-
biorcy bezpośrednio, ignorowane są inne zda-
rzenia znajdujące się w Pętli Obsługi Zda-
rzeń, a postEvent dokłada zdarzenie do wy-
słania na końcu kolejki pętli. Ta druga możli-
wość daje kolejce pewne możliwości optyma-
lizacji obsługi zdarzeń.
Funkcje służące do przechwytywania
zdarzeń to tak zwane handlery (Event-Han-
dler). Są to funkcje wirtualne, które można
(/należy) reimplementować we własnych
klasach.
Funkcje obsługujące zdarzenia powstają-
ce wewnątrz biblioteki Qt mają nazwy opiso-
we zaczynające się małą literą i kończące się
słowem Event, np. showEvent, keyPressE-
vent itd.
Proszę zwrócić uwagę na punkt 1. Aby
dialog, który jest klasą bazową dla DbgRegi-
sters, się faktycznie ukazał, zdarzenie należy
przesłać do klasy bazowej. A później może-
my się zastanowić, co chcemy zrobić w mo-
mencie zajścia zdarzenia. Jeżeli chcemy prze-
chwycić zdarzenia zanim zostaną wysłane do
Listing 4. Deklaracja dziedziczenia
#include <QApplication>
//*******************************************************************
// EventsController CONSTRUCTOR private
//*******************************************************************
EventsController :: EventsController ()
: QObject ()
{
setParent ( qApp );
}
// end of EventsControler
46
maj 2007
439031205.014.png 439031205.015.png 439031205.016.png 439031205.017.png 439031205.018.png
 
dla programistów
Events Controller – kontroler zdarzeń
wyspecjalizowanych handlerów, możemy
zaimplementować funkcje: bool Qwidget::
event( QEvent* event ).
No i najważniejsze (w tym artykule oczy-
wiście). Do obsługi zdarzeń użytkownika,
czyli typów o numerze większym niż QEven-
tUser, służy specjalny wirtualny handler:
Listing 5. Deklaracja klasy EventsController
#ifndef INCLUDED_EventsController_h
#deine INCLUDED_EventsController_h
/*------- include iles:
-------------------------------------------------------------------*/
# include "Event.h"
#include <QObject>
#include <QMutex>
#include <QHash>
#include <QSet>
void QObject::customEvent( QEvent*
event ).
Reimplementując tę funkcję w naszej kla-
sie, będziemy mogli obsługiwać swoje wła-
sne zdarzenia.
/*------- class declaration:
-------------------------------------------------------------------*/
class EventsController : public QObject
{
Q_OBJECT
Jak ja to zrobiłem
Szczegóły implementacji można znaleźć w pli-
kach Event.h, EventsController.cpp i Event-
sController.h mojego projektu o nazwie Co-
bras (www.beesoft.org).
Przede wszystkim utworzyłem własną
klasę zdarzeń o nazwie Event (bez Q):
// ******* CONSTRUCTION/DESTRUCTION *******
private :
EventsController ();
~ EventsController ();
EventsController ( const EventsController & );
EventsController & operator =( const EventsController & );
• Jak widać – moja klasa Event jest podkla-
są klasy QEvent . I tak być musi.
• Wszystkie zdarzenia w Qt muszą być po-
chodną tej właśnie klasy.
• Należy zauważyć, że typy moich zdarzeń
są zadeklarowane jako enum i pierwsze
zdarzenie ma numer QEventUser + 1.
Jest to po to, aby system wiedział, że to
moje zdarzenia i że to ja będę je obsługi-
wał poprzez customEvent.
• Teoretycznie zdarzenie może przekazy-
wać dowolną liczbę wartości QVariant
przechowywanych w wektorze QVector.
Na swoje potrzeby zaimplementowałem
konstruktory klasy pozwalające na prze-
kazanie od 0 do 4 wartości. Typ QVa-
riant zastosowałem, aby uniezależnić się
(częściowo przynajmniej) od typów prze-
kazywanych wartości.
// ******* TYPES *******
private :
typedef QHash < qint32 , QSet < QObject * > > EventRegister ;
// ******* MEMBERS *******
private :
static EventsController * d_instance ;
EventRegister d_items ;
QMutex d_mutex ;
// ******* METHODS *******
public :
static EventsController * instance ();
bool append ( QObject * in_receiver , qint32 in_event_id );
void remove ( QObject * in_receiver );
void send_event ( qint32 in_event_id );
void send_event ( qint32 in_event_id , QVariant in_val );
void send_event ( qint32 in_event_id , QVariant in_val1 ,
Qvariant in_val2 );
void send_event ( qint32 in_event_id , QVariant in_val1 ,
QVariant in_val2 , QVariant in_val3 );
void send_event ( qint32 in_event_id , QVariant in_val1 ,
QVariant in_val2 , QVariant in_val3 , QVariant in_val4 );
I jeszcze uwaga. W zasadzie należy bezwzglę-
dnie unikać przekazywania do funkcji/kons-
truktorów parametrów przez wartość (tutaj:
obiektów typu Qvariant). Jednak w tym przy-
padku musiałem od tej reguły odstąpić. Je-
śli pokusimy się o przekazywanie parame-
trów przez wskaźniki lub referencję, możemy
mieć problemy, gdy obiekt wysyłający zdarze-
nie zostanie natychmiast zlikwidowany po je-
go wysłaniu. Może się okazać, że referencja lub
wskaźnik pokazuje na coś czego już nie ma.
Wiem, bo miałem taką właśnie sytuację
private :
bool exists ( QObject * in_receiver , qint32 in_event_id );
} ;
# endif // INCLUDED_EventsController_h
EventsController
Przede wszystkim klasa EventsController jest
zaimplementowana jako Singleton. Oznacza
48
maj 2007
439031205.019.png 439031205.020.png 439031205.021.png 439031205.022.png
 
dla programistów
Events Controller – kontroler zdarzeń
Listing 6. Implementacja funkcji MainWindow
Destruktor obiektu zostanie wywołany
automatycznie w momencie zakończenia pro-
gramu. Wynika to z faktu, że jako obiekt ma-
cierzysty obiektu kontrolera został w jego
konstruktorze wskazany obiekt qApp, czyli
sam program.
W celu zabezpieczenia danych prze-
chowywanych w zmiennej EventRegister d_
items , wprowadziłem mechanizm blokowa-
nia jednoczesnego dostępu za pomocą mu-
teksa.
MainWindow :: MainWindow ()
: QmainWindow ()
, ....
, ....
{
...
EventsController :: instance ()-> append ( this , Event :: FILE_OPENED );
EventsController :: instance ()-> append ( this , Event :: FILE_CLOSED );
EventsController :: instance ()-> append ( this , Event :: ALL_FILES_CLOSED );
EventsController :: instance ()-> append ( this , Event :: FILE_RENAMED );
Jak to działa
Zasada działania kontrolera jest banalnie
prosta. Dowolny obiekt dowolnego typu mo-
że wysłać dowolne zdarzenie w następu-
jący sposób:
.....
EventsController :: instance ()-> append ( this , Event :: PROJECT_OPENED );
EventsController :: instance ()-> append ( this , Event :: PROJECT_CLOSED );
EventsController :: instance ()-> append ( this , Event :: UNDO_AVAILABLE );
EventsController :: instance ()-> append ( this , Event :: REDO_AVAILABLE );
EventsController :: instance ()-> append ( this , Event :: COPY_AVAILABLE );
EventsController :: instance ()-> append ( this , Event :: PROCESS_STARTED );
EventsController :: instance ()-> append ( this , Event :: PROCESS_BROKEN );
EventsController :: instance ()-> append ( this , Event :: DBG_STARTED );
EventsController :: instance ()-> append ( this , Event :: DBG_FINISHED );
#include "EventsController.h"
.....
EventsController::instance()->send_
event( Event::DBG_CONTINUE_REQUEST );
.....
.....
I tu od razu widać potężny plus sterowania
programem za pomocą zdarzeń. Załóżmy hi-
potetycznie, że wiele obiektów, których nasz
obiekt nie zna oraz że obiekty, które jesz-
cze nie zostały zaimplementowane (!) są za-
interesowane tym zdarzeniem. W obiekcie,
w którym wysyłamy zdarzenie, zupełnie nas
to nie interesuje. Nie musimy znać odbior-
ców zdarzenia. Nie musimy znać ich wskaź-
ników ani nazw funkcji, które obsłużą zda-
rzeniem. Wysyłamy zdarzenie i już. Rozbu-
dowując program o kolejne klasy zaintere-
}
// end of MainWindow
to, że w programie można utworzyć jeden
i tylko jeden obiekt tego typu. Jest on tworzo-
ny tylko raz, przy starcie programu.
Ponieważ konstruktor klasy jest prywat-
ny, jedyną metodą dostępu do obiektu jest
wywołanie EventsController::instance(). Ta
funkcja zwraca wskaźnik do klasy. I to ta
funkcja tworzy obiekt, jeśli on jeszcze nie ist-
nieje. A jeśli obiekt istnieje, kolejne jej wywo-
łanie nie spowoduje ponownego utworze-
nia obiektu lecz zwróci wskaźnik do już ist-
niejącego.
Listing 7. Implementacja Funkcji customEvent
//*****************************************************
// customEvent PRIVATE inherited
//*****************************************************
void MainWindow :: customEvent ( QEvent * const in_event )
{
Event * const event = dynamic_cast < Event * >
( in_event );
const qint32 type = static_cast < qint32 >
( in_event -> type () );
case Event :: FILE_RENAMED :
ile_renamed ( event );
break ;
case Event :: ACTIVE_DOCUMENT_CHANGED :
active_document_changed ( event );
break ;
case Event :: CURSOR_POSITION_CHANGED :
cursor_position_changed ( event );
break ;
case Event :: PROJECT_OPENED :
project_opened ( event );
break ;
......
case Event :: DBG_DISABLE_RUN_REQUEST :
active_debug_items ( DBG_DISABLE_RUN );
break ;
}
}
// end of customEvent
switch ( type ) {
case Event :: FILE_OPENED :
ile_opened ( event );
break ;
case Event :: FILE_CLOSED :
ile_closed ( event );
break ;
case Event :: ALL_FILES_CLOSED :
all_iles_closed ();
break ;
www.lpmagazine.org
49
439031205.023.png 439031205.024.png 439031205.025.png 439031205.026.png 439031205.027.png 439031205.028.png 439031205.029.png 439031205.030.png 439031205.031.png 439031205.032.png 439031205.033.png 439031205.034.png 439031205.035.png
 
Zgłoś jeśli naruszono regulamin