Państwowa Wyższa Szkoła Zawodowa w Tarnowie Instytut Politechniczny Kierunek: Informatyka Specjalność: Informatyka Stosowana 2011/2012 Karol Grzybowski Praca Inżynierska Narzędzia budowy interfejsów graficznych w środowisku Windows kierunki rozwoju. Opiekun pracy: prof. dr hab. inż. Jan T. Duda Tarnów, 2012
Oświadczam, że niniejszą pracę wykonałem samodzielnie oraz że praca nie narusza praw autorskich innych osób.... podpis Oświadczam, że wersja elektroniczna pracy znajdująca się na płycie jest zgodna z niniejszym wydrukiem.... podpis
Spis treści Wykaz pojęć 9 1 Wstęp 13 1.1 Cele pracy................................. 13 1.2 Struktura i organizacja pracy...................... 14 1.3 Wykorzystane narzędzia......................... 14 2 Przegląd istniejących rozwiązań 17 2.1 Cechy charakterystyczne bibliotek GUI................. 19 2.1.1 Hierarchia komponentów..................... 19 2.1.2 Struktura aplikacji........................ 20 2.1.3 Elementy okna głównego..................... 20 2.1.4 Wsparcie dla architektury Dokument / Widok......... 21 2.1.5 Mechanizm RTTI......................... 22 2.1.6 Wielowątkowość.......................... 23 2.1.7 Obsługa zdarzeń......................... 24 2.1.8 Wspomaganie tworzenia interfejsu graficznego......... 25 2.2 Podsumowanie.............................. 25 3 Założenia projektowe 27 3.1 Podstawowe założenia.......................... 27 3.1.1 Licencja.............................. 28 3.1.2 Język programowania....................... 28 3.1.3 Windows API........................... 28 3.1.4 Użycie łańcuchów Unicode.................... 29 5
3.1.5 Narzut pamięciowy........................ 29 3.2 Elementy interfejsu graficznego..................... 29 3.3 Mechanizmy biblioteki.......................... 30 3.3.1 RTTI................................ 30 3.3.2 Wielowątkowość.......................... 30 3.3.3 Manager zasobów......................... 31 4 Implementacja 33 4.1 Organizacja kodu............................. 34 4.2 Modułowa organizacja frameworku................... 35 4.2.1 uixprocessstate stan procesu................. 37 4.2.2 uixmodulestate stan modułu................. 37 4.2.3 uixthreadstate stan wątku.................. 38 4.2.4 uixprocessstatelock globalne blokady procesu....... 39 4.2.5 Aplikacja............................. 40 4.2.6 Rozszerzenia............................ 41 4.3 Zarządzanie zasobami........................... 41 4.4 Struktura aplikacji............................ 43 4.4.1 Aplikacja............................. 43 4.4.2 Ramki i okna dialogowe..................... 44 4.4.3 Kontrolki............................. 44 4.5 Obsługa błędów asercje i wyjątki................... 45 4.5.1 Wyjątki uixexception..................... 45 4.5.2 Asercje uixassert...................... 46 4.5.3 Zapewnienia uixensure................... 47 4.5.4 Walidacje uixvalidate................... 48 4.5.5 Zrzuty uixdump....................... 48 4.5.6 Śledzenie uixtrace..................... 49 4.6 Wielowątkowość.............................. 49 4.6.1 Wątek............................... 50 4.6.2 Wątek UI............................. 50 4.6.3 Prymitywy synchronizacyjne................... 50 4.6.4 Obiekty lokalne wątku i procesu................. 52 6
4.7 Mechanizm RTTI............................. 52 4.8 Przetwarzanie komunikatów....................... 54 4.8.1 Rejestrowanie klasy okna..................... 55 4.8.2 Procedura obsługi komunikatów................. 55 4.8.3 Subclassing............................ 57 4.8.4 Mapy zdarzeń........................... 58 4.8.5 Mechanizm refleksji komunikatów................ 60 4.8.6 Hierarchia zdarzeń........................ 61 4.8.7 Zdarzenia globalne komendy................. 63 4.8.8 Obsługa zdarzeń......................... 64 5 Dokumentacja, testy, przykłady 65 5.1 Dokumentacja............................... 65 5.2 Prezentacja zaimplementowanych kontrolek............... 66 5.3 Przykładowa aplikacja klon programu notatnik........... 68 5.4 Kontrolka niestandardowy panel................... 70 5.5 Kontrolka okno typu Splash Screen................. 71 5.6 Tworzenie prostej aplikacji........................ 71 5.6.1 Klasa aplikacji........................... 71 5.6.2 Klasa ramki głównej....................... 72 6 Podsumowanie rezultatów 75 6.1 Charakterystyka biblioteki........................ 75 6.2 Bezpieczeństwo.............................. 77 6.2.1 Asercje............................... 77 6.2.2 Statyczna analiza kodu źródłowego............... 78 6.2.3 Testy jednostkowe......................... 78 6.3 Możliwości rozwoju............................ 78 6.3.1 Aplikacje MDI oraz TDI..................... 78 6.3.2 Implementacja architektury dokument / widok......... 79 6.3.3 Zakładki właściwości....................... 79 6.3.4 Rozszerzenie istniejących kontrolek o style renderowania... 79 6.3.5 Automatyczne układy kontrolek................. 79 7
6.3.6 Mechanizm właściwości...................... 80 6.3.7 Dodatkowe kontrolki biblioteka UixExt........... 81 6.3.8 Automatyczna aktualizacja interfejsu.............. 85 6.3.9 Internacjonalizacja........................ 85 6.3.10 Usprawnienia przetwarzania zdarzeń.............. 85 6.3.11 Język opisu interfejsu graficznego................ 86 6.4 Wieloplatformowość............................ 86 6.5 Rozwój Open Source........................... 87 Dodatek A Zawartość płyty CD 88 Dodatek B Hierarchia kontrolek interfejsu graficznego 89 Listingi 93 Bibliografia 94 8
Wykaz pojęć API (ang. Application Programming Interface) interfejs programowania aplikacji zdefiniowany na poziomie kodu źródłowego zestaw ściśle określonych reguł komunikacji między aplikacjami. API zdefiniowane jest dla takich elementów oprogramowania jak aplikacje, biblioteki czy też system operacyjny. Głównym założeniem API jest dostarczenie programiście struktur danych, funkcji, klas czy też wymaganych protokołów komunikacyjnych, Biblioteka DLL (ang. Dynamic-Link Library) w środowisku Windows jest to biblioteka współdzielona pomiędzy uruchomionymi procesami, która przechowuje implementacje różnych funkcji oraz zasobów programu, Cache mechanizm przechowywania najczęściej używanych danych w buforze pamięci podręcznej. Zastosowanie tego mechanizmu umożliwia znaczące przyspieszenie dostępu do najczęściej wykorzystywanych danych, Delegat ang. delegate jest to rozszerzenie mechanizmu wskaźników do funkcji o mechanizmy bezpiecznego zwracania typów danych oraz liczby podawanych parametrów wraz z ich typami. W skład delegatu wchodzą adres metody oraz obiekt, na którym ta metoda ma zostać wywołana. Ideą delegatów jest oddelegowywanie wywołania metody do innego obiektu, który takiego delegata zaakceptuje, Framework jest szkieletem do budowy aplikacji. Definiuje on strukturę aplikacji oraz ogólny mechanizm jej działania, a także dostarcza zestaw komponentów i bibliotek ogólnego przeznaczenia do wykonywania określonych zadań. W niniejszej pracy wyrażenia framework oraz biblioteka używane są zamiennie, 9
Formatka (ang. frame, form) element graficznego interfejsu użytkownika będący samodzielnym kontenerem na inne kontrolki. Nazywany takze oknem, często utożsamiany z programem, IDE (ang. Integrated Development Environment) zintegrowane środowisko programistyczne jest to pojedyncza aplikacja bądź zespół aplikacji (środowisko) służących do tworzenia, modyfikowania, testowania i konserwacji oprogramowania, Ingresja tzw. punkt wejścia (ang. entry point) jest to miejsce w programie, od którego rozpoczyna się wykonywanie instrukcji zawartych w bloku kodu. W niniejszej pracy, poprzez punkt wejścia rozumiane będą funkcje, od których rozpoczyna się działanie modułu aplikacji. Przykładami takich funkcji są: main(...) WinMain(...) DllMain(...) Instancja reprezentacja obiektu konkretnego typu w pamięci. W szczególności: w systemie Windows instancja jest reprezentacją modułu z kodem wykonywalnym, załadowanym do pamięci i zainicjaliozwanym. Reprezentowana przez uchwyt HINSTANCE, Kompilacja Debug kompilacja, w której wyłączone są wszystkie optymalizacje oraz generowane są symbole dla debuggera, Kompilacja Release kompilacja wydaniowa najczęściej z włączonymi wszystkimi optymalizacjami; często generuje osobne pliki z symbolami dla debugera, Lokalizacja i internacjonalizacja proces przystosowywania aplikacji i zasobów do obsługi wielu kultur i języków. W szczególności oznacza proces tłumaczenia interfejsu użytkownika na dany język (lokalizacja), stosowanie nazw własnych, jednostek miar i walut odpowiednich dla ustawień systemu operacyjnego, 10
MDI (ang. Multiple Document Interface) rodzaj interfejsu graficznego aplikacji, w którym formatka główna aplikacji może wyświetlać wiele dokumentów. W systemie Windows aplikacje MDI wyświetlają widoki dokumentu wewnątrz okna głównego, Metoda (w programowaniu obiektowym) jest to funkcja składowa klasy Okno modalne rodzaj okna, które przejmuje fokus (ang. ognisko) okna nadrzędnego na sobie wymuszając na użytkowniku zamknięcie okna modalnego przed przejściem do okna nadrzędnego, Powłoka systemowa (ang. shell) część systemu operacyjnego, odpowiedzialna za komunikację pomiędzy użytkownikiem a systemem operacyjnym. W systemach Windows rolę powłoki pełni program Microsoft Windows Explorer oraz konsola tekstowa 1, Projektowanie kontraktowe (ang. Design by contract, DbC ) jest to metodologia organizowania kodu źródłowego w taki sposób, aby wynikało z niego nie tylko jak program ma działać, ale też w jaki sposób zweryfikować poprawność działania elementów aplikacji. Jest związane z programowaniem obiektowym, Przestrzeń nazw (ang. namespace) jest kontenerem który dostarcza kontekst dla identyfikatorów które zawiera, oraz rozwiązuje niejednoznaczność pomiędzy identycznymi identyfikatorami znajdującymi się w różnych przestrzeniach nazw, RAII (ang. Resource Acquisition Is Initialization) pozyskanie zasobu jest inicjalizacją idiom języka C++, który opiera się na mechanizmie automatycznego wywoływania destruktorów dla obiektów, które w trakcie wykonywania programu wyjdą poza zasięg. RTTI (ang. Run Time Type Information) informacja o typie w trakcie wykonania programu jest to mechanizm znajdujący się w nowoczesnych obiektowych językach programowania. Polega ona na dołączaniu do kodu aplikacji 1 dostępna jest także nowoczesna konsola Power Shell 11
dodatkowych informacji o typach, często także ich właściwościach i dostępnych metodach, SDI (ang. Single Document Interface) rodzaj interfejsu graficznego, w którym formatka główna aplikacji może wyświetlać tylko jeden dokument, Sygnał Slot jest to jedna z metod organizacji dyspozytora zdarzeń w aplikacjach z graficznym interfejsem użytkownika. Polega ona na podłączaniu metod obsługi do slotu obsługującego dany sygnał. W reakcji na otrzymanie sygnału slot wywołuje zarejestrowane metody obsługi w (najczęściej) niezdefiniowanej kolejności, Singleton wzorzec projektowy [11, str. 130], gwarantujący że dana klasa będzie posiadała tylko jeden egzemplarz oraz zapewnia globalny interfejs dostępu do niego, TDI (ang. Tabbed Document Interface) rodzaj interfejsu użytkownika, w którym dokumenty i widoki dostępne są jako karty, TLS (ang. Thread Local Storage) lokalny magazyn wątku mechanizm stosowany w systemach operacyjnych, pozwalający na automatyczne tworzenie obiektów unikalnych dla każdego wątku. W systemach Windows pozwala on na przechowywanie limitowanej ilości wskaźników do danych, Uchwyt (ang. handle) identyfikator liczbowy reprezentujący obiekt często używany zamiast wskaźników, Zasób dynamiczny zasób przyznawany dla uruchomionego procesu, poprzez wywoływanie funkcji systemowych. Przykładem zasobu dynamicznego jest zaalokowany blok pamięci czy też uchwyt otwartego pliku, Zasób statyczny w systemach z rodziny Windows zasób, który ładowany jest wraz z plikiem wykonywalnym do pamięci. Znajduje się on w osobnej sekcji rsrc 2, 2 resource 12
Rozdział 1 Wstęp 1.1 Cele pracy W informatyce, graficzny interfejs użytkwnika (GUI - Graphical User Interface) jest jednym z rodzajów interfejsów, który pozwala użytkownikowi na interakcję z urządzeniami elektronicznymi poprzez elementy wizualne; w przeciwieństwie do interfejsów głosowych czy też tekstowych (CLI - Command Line Interface). Stosowanie interfejsu graficznego w aplikacji znacznie upraszcza obsługę programu przez zwykłego użytkownika. Operacje wykonywane poprzez wpisywanie długich i skomplikowanych poleceń z poziomu okna konsoli można zastąpić wciśnięciem jednego przycisku. Celem niniejszej pracy jest stworzenie prostej w obsłudze biblioteki do tworzenia aplikacji z interfejsem graficznym, o kodowej nazwie UIX User Interface Framework. Głównym założeniem autora jest napisanie możliwie prostego frameworku, który w jak najmniejszym stopniu ingerowałby w środowisko 1 tworzonej aplikacji. Jednym z ważniejszych celów niniejszej pracy, jest przedstawienie możliwości integracji API systemu Windows z językiem C++ oraz, co stanowi niejako główny cel pracy przedstawienie najprostszej możliwości tworzenia aplikacji z graficznym interfejsem użytkownika. Sama zaś integracja, pomimo iż wydaje się być trywialną 1 Autor ma tutaj na myśli m.in. rdzeń tworzonej przez programistę aplikacji oraz inne wykorzystane przez niego biblioteki 13
w implementacji, wymusza wykorzystanie zaawansowanych technik programowania udostępnionych przez Twórców systemu Windows. Framework ten tworzony był z myślą o pisaniu prostych aplikacji narzędziowych, czyli wszędzie tam, gdzie sam wygląd programu nie ma aż tak istotnego znaczenia jak funkcjonalność którą oferuje. W przykładowych aplikacjach autor pracy starał się przedstawić możliwości tworzenia prostych aplikacji oraz pokazaniu zaimplementowanych mechanizmów w działaniu, skupiając się na samym użyciu biblioteki pomijając implementację funkcjonalności związanych z przykładowymi programami. 1.2 Struktura i organizacja pracy Praca inżynierska składa się z 6 rozdziałów. Pierwszy z nich to wstęp, który opisuje motywacje autora do zgłębienia takiej tematyki oraz cele jakie sobie postawił, wyjaśnia terminologię oraz zawiera niniejszy przewodnik po treści pracy. Rozdział drugi stanowi przegląd istniejących na rynku bibliotek GUI, a także opisuje ich wady i zalety. W rozdziale trzecim opisane zostały założenia projektowe tworzonej w ramach pracy biblioteki. Rozdział czwarty opisuje implementację autorskiego frameworku UIX, wykorzystane techniki programistyczne oraz algorytmiczne. Rozdział piąty pokazuje możliwości wykorzystania biblioteki UIX w tworzeniu prostych aplikacji oraz nowych komponentów interfejsu graficznego. Rozdział szósty podsumowuje całą wykonaną przez autora pracę, pokazuje przyszłe kierunki rozwoju biblioteki a także możliwe zmiany i ulepszenia w jej architekturze. 1.3 Wykorzystane narzędzia Implementacja biblioteki UIX powstała w pełni w języku C++ na platformie Microsoft Windows. Framework ten napisany został przy użyciu darmowego środowiska programistycznego Microsoft Visual C++ Express 2008, którego darmową wersję można ściągnąć z oficjalnej strony internetowej pakietu 2. Pakiet ten można 2 http://www.microsoft.com/express 14
wykorzystać bez żadnych opłat do tworzenia legalnego oraz komercyjnego oprogramowania. Wszystkie grafiki wykorzystane zarówno w przykładowych aplikacjach, jak i samej pracy wykonano przy użyciu programów: paint.net, Inkscape. Diagramy wykorzystane w pracy stworzone zostały w programach Microsoft Visio 2010 oraz Star UML. Interfejs użytkownika przykładowych aplikacji (w postaci okien dialogowych) oraz inne zasoby pliku wykonywalnego powstały w darmowym programie ResEdit, którego najnowszą wersję można pobrać ze strony internetowej programu 3. W trakcie tworzenia pracy, autor posiłkował się narzędziem do statycznej analizy kodu źródłowego programu Cppcheck 4. Narzędzie takie w przeciwieństwie do kompilatora nie wychwytuje błędów składniowych jego podstawowym zadaniem jest ostrzeganie przed typami błędów, które kompilator nie jest w stanie wychwycić w trakcie kompilacji. Niniejsza praca inżynierska powstała w edytorze Notepad++, wykorzystując popularny w środowisku naukowym system składania tekstu L A TEX. 3 http://www.resedit.net/ 4 http://cppcheck.sourceforge.net/ 15
16
Rozdział 2 Przegląd istniejących rozwiązań Rozdział ten opisuje istniejące biblioteki tworzenia aplikacji z graficznym interfejsem użytkownika. Opis ten obejmuje zarówno rozwiązania komercyjne jak i biblioteki rozwijane jako wolne oprogramowanie. Autor pracy skupił się na opisie tych cech, które brane są pod uwagę przez programistów przy wyborze biblioteki GUI dla tworzonego oprogramowania. W rozdziale tym autor stara się obiektywnie przedstawić cechy wybranych bibliotek programistycznych. Ostateczna decyzja przy wyborze biblioteki nie powinna zależeć od prywatnych upodobań programisty, ale właśnie od obiektywnie przedstawionych zalet i wad 1. Niniejszy rozdział przedstawia charakterystyczne elementy bibliotek GUI na przykładzie istniejących bibliotek. Przy czym autor pracy ograniczył się do przedstawienia implementacji mechanizmów w bibliotekach takich jak: MFC (ang. Microsoft Foundation Classes) jest biblioteką programistyczną napisaną w języku C++ oraz dostarczaną wraz z płatną wersją pakietu Microsoft Visual Studio. Samo MFC stanowi zbiór klas, które pozwalają na tworzenie aplikacji pracujących w środowisku Windows. Biblioteka ta stanowi obiektową 1 Wystarczającym przykładem subiektywnej oceny może być fakt, że autor pracy do stworzenia bardziej rozbudowanej aplikacji wykorzystałby bibliotekę Qt ze względu na doświadczenie w korzystaniu z niej. 17
oraz uproszczoną nakładkę na API systemu Windows. Posiada swój bezpłatny odpowiednik w postaci biblioteki WTL (ang. Windows Template Library) różniący się od niej jedynie podejściem do wykorzystania zaawansowanych mechanizmów języka C++ takich jak m.in szablonów. Obie biblioteki bazują na bibliotece ATL (ang. Active Template Library). Qt jest to wieloplatformowa biblioteka, szeroko stosowana w tworzenia aplikacji z graficznym interfejsem użytkownika. Wykorzystywana jest zarówno do tworzenia aplikacji z GUI, narzędzi konsolowych jak i konsol dla serwerów. Qt zostało wykorzystane w takich aplikacjach jak Autodesk Maya, Adobe Photoshop Elements, Skype czy też VirtualBox. Framework ten jest także wykorzystywany w takich firmach jak DreamWorks, Google, Panasonic, Philips, Samsung czy też Siemens. [23] Oryginalnie Qt zostało napisane w jęzku C++, jednak istnieje wiele jej portów na różne języki programowania, takie jak: Ada, C#, D, Java, Pascal, Perl, PHP, Python. Biblioteka ta jest dostępna w wersjach dla wielu platform uruchomieniowych, takich jak Linux / X11, Mac OS X, Windows, Symbian, Maemo / MeeGo. Dostępne są także zewnętrzne 2 porty na inne platformy, takie jak Open Solaris, Haiku, OS/2, Amiga OS4, webos, iphone. wxwidgets jest biblioteką dla języka C++ która pozwala programistom na tworzenie aplikacji dla Windows, OS X, Linux oraz UNIX zarówno dla 32 / 64 bitowych architektur jak i na wiele platform mobilnych. Posiada porty dla języków takich jak Python, Perl, Ruby, Java, Lua, Lisp, Erlang, C#, D i BASIC. W przeciwieństwie do wielu innych bibliotek wieloplatformowych, wxwidgets daje tworzonym aplikacjom natywny interfejs systemu operacyjnego ponieważ używa API systemu operacyjnego zamiast emulować GUI. Jest to biblioteka dojrzała, darmowa i otwartoźródłowa. 2 Rozwijane przez społeczność open source 18
Praca ta obejmuje jedynie opis zagadnień związanych z tworzeniem interfejsu graficznego praktycznie pomijając inne aspekty tworzenia aplikacji. 2.1 Cechy charakterystyczne bibliotek GUI Cechą wspólną wszystkich wymienionych poniżej bibliotek jest język programowania w którym zostały one napisane. Pod uwagę wzięte zostały tylko te biblioteki, które pozwalają na tworzenie aplikacji w języku C++. Obecnie praktycznie każda większa biblioteka GUI napisana jest w metodologii programowania obiektowego, w językach wspierających paradygmat programowania obiektowego. Poniżej przedstawione zostało zestawienie cech, jakie posiadają biblioteki opisane w niniejszej pracy. Zawiera ono najważniejsze aspekty, jakie należy rozważyć przed projektowaniem biblioteki narzędziowej do tworzenia interfejsu graficznego. 2.1.1 Hierarchia komponentów Zarówno jakość tworzonego oprogramowania jak i efektywność samego procesu jego tworzenia zależy bezpośrednio od hierarchii gotowych do użycia klas. Dobra biblioteka GUI powinna zawierać dobrze rozwiniętą hierarchię komponentów interfejsu graficznego, która pozwala na łatwe rozszerzanie o elementy, które potrzebuje programista. Najczęściej spotykane są dwa rozwiązania: Hierarchia klas jest zaprojektowana tak, że dodanie nowej funkcjonalności wymaga zastosowania dziedziczenia i rozszerzenia implementacji o dodatkową funkcjonalność, Podejście komponentowe w którym klasa kontrolki interfejsu graficznego oddelegowuje pewną funkcjonalność obiektom zewnętrznym. Często podejście to jest łączone z wzorcem projektowym Model-Widok-Kontroler. Wszystkie biblioteki GUI na których skupia się niniejszy opis zaprojektowane są z wykorzystaniem połączenia obydwu podejść, tj. posiadają one zorganizowaną i usystematyzowaną hierarchię klas, 19
pozwalają na delegowanie funkcjonalności obiektom wykonawczym. Przykładowo, w architekturze biblioteki Qt możliwe jest rozszerzenie działania standardowej klasy kontrolki QListView poprzez dziedziczenie po tej klasie. Jednak dokumentacja Qt zaleca, aby do wymiany informacji z kontrolką używać zewnętrznego obiektu modelu. 2.1.2 Struktura aplikacji Z punktu widzenia programisty, proces systemowy rozpoczyna swoje działanie w głównej funkcji aplikacji. Wszystkie przedstawione w tym opisie biblioteki rozpoczynają tworzenie struktury całej aplikacji od stworzenia obiektu który ją symbolizował najczęściej będącym instancją klasy typu Application 3. Obiekt aplikacji odpowiada za główny wątek przetwarzania komunikatów. Pozwala to na ukrycie przed programistą implementacji kolejki przetwarzania komunikatów uruchamianej najczęściej pod postacią metody Run (lub podobnej). Typowa aplikacja z graficznym interfejsem użytkownika posiada przynajmniej jedną formatkę, będąca głównym oknem aplikacji. Poprzez nią odbywa się komunikacja z użytkownikiem: zarówno prezentacja wyników działania aplikacji, jak i przyjmowanie poleceń od użytkownika. Biblioteki graficznego interfejsu użytkownika wprowadzają także pojęcie okna dialogowego jako prostszej alternatywy dla formatki okna głównego aplikacji. 2.1.3 Elementy okna głównego W zależności od typu aplikacji, zarówno zachowanie jak i wygląd głównej formatki zmieniają się: SDI dla każdego otwartego dokumentu otwierane 4 jest nowe okno główne. Działanie aplikacji kończy się wraz z zamknięciem ostatniej formatki głównej, 3 Zależnie od biblioteki, posiadała ona przedrostek: C dla MFC, Q dla Qt oraz wx dla wxwidgets 4 Częściej jednak po prostu uruchamiana jest nowa instancja aplikacji 20
MDI okno główne zawiera w sobie tzw. okno przestrzeni roboczej aplikacji (ang. application workspace), w którym wyświetlane są okna dzieci. Aplikacje tego typu przetwarzają wiele dokumentów i widoków równocześnie pozwalając na dowolne ich rozmieszczanie w ramach okna przestrzeni roboczej aplikacji, TDI podejście to jest podobne do aplikacji MDI z tą różnicą, że przestrzeń robocza okna aplikacji traktuje każdy widok dokumentu jako pojedynczą zakładkę. Wszystkie wymienione w tym rozdziale biblioteki wspierają wszystkie trzy style tworzenia aplikacji. Pomimo mnogości rozwiązań znanych z różnych bibliotek, typowymi elementami głównej formatki aplikacji stały się: menu główne menu zawierające wszystkie dostępne dla użytkownika narzędzia, paski narzędzi zestaw pasków grupujących najczęściej używane przez użytkownika narzędzia, pasek stanu pasek umieszczany na dole formatki, zawierający informacje o stanie działania aplikacji, okna dokowalne okna zawierające elementy sterujące aplikacją, z możliwością zmiany ich układu przez użytkownika. Prostsze aplikacje, zamiast stosować różne style obsługi dokumentów (albo w ogóle nie stosować wzorca dokument / widok) wykorzystują zamiast formatki okno dialogowe 5. Rozróżnienie na ramki oraz okna dialogowe wprowadzone zostało ze względu na potrzebę odróżnienia okna głównego aplikacji od okien potomnych 6. 2.1.4 Wsparcie dla architektury Dokument / Widok Kolejnym ważnym aspektem tworzenia aplikacji z wykorzystaniem bibliotek interfejsu użytkownika jest wsparcie dla architektury dokument / widok. Architektura ta dzieli aplikację na logiczne części jakimi są: 5 Niektóre biblioteki, chociażby takie jak MFC, nie pozwalają na wizualną edycję ramek (ang. frame) okna głównego aplikacji w przeciwieństwie do okien dialogowych 6 Ramka aplikacji nie powinna być dzieckiem innego okna niż pulpitu w przeciwieństwie do okna dialogowego, które może (ale nie musi) być dzieckiem okna pulpitu 21
ramka aplikacji jest to główne okno aplikacji. Odpowiada za wykonywanie komend oraz stanowi podstawowe okno komunikacji z użytkownikiem. W architekturze dokument / widok ramka aplikacji zawiera widoki aktualnie otwartych dokumentów, dokument jest to pojęcie abstrakcyjne. Dokumentem można nazwać każde dane, zgrupowane w logiczną całość oraz możliwe do prezentacji użytkownikowi. Dokumentem może być chociażby plik tekstowy lub plik binarny, widok jest to element interfejsu użytkownika, który odpowiada za prezentacje (a niekiedy edycję) treści dokumentu. Jeden dokument może posiadać wiele otwartych widoków. Przykładowo, dokument w postaci pliku wykonywalnego exe, może być przedstawiany poprzez: widok zasobów zawarte w aplikacji zasoby pliku wykonywalnego, widok pliku binarnego surowe dane binarne reprezentujące plik wykonywalny, widok informacji o pliku wykonywalnym wraz z informacjami o zawartych sekcjach, nagłówkach, tablicach symboli itp. Należy jednakże zaznaczyć, iż jeden dokument może posiadać wiele otwartych widoków tego samego typu. Przykładem takiej aplikacji może być aplikacja pakietu Visual Studio Visual C++, która pozwala na otwarcie oraz synchronizację między sobą wielu widoków na dany dokument plik źródłowy. 2.1.5 Mechanizm RTTI Implementacja mechanizmu RTTI jest ważnym elementem każdej biblioteki interfejsu graficznego 7. Wszystkie wspomniane w tej pracy biblioteki korzystały z własnych implementacji tego mechanizmu eliminując potrzebę korzystania z RTTI zdefiniowanego w języku C++. Każda z bibliotek implementowała go w postaci 7 Mimo to, możliwe jest nie korzystanie z niego budując architekturę opartą o model komponentowy, przykładowo COM 22
obiektu klasy opisującej inną klasę wraz z zestawem makr potrzebnych do korzystania z niego. Hierarchia klas tworzona jest podczas statycznej inicjalizacji aplikacji istnieje więc już przed przejściem do kodu aplikacji. Implementacja tego mechanizmu w bibliotekach MFC i wxwidgets bazuje na tych samych założeniach i podobnym zestawie makr. Wyjątkiem jest biblioteka Qt, która do działania mechanizmu RTTI wymaga osobnego narzędzia dołączanego do tzw. toolchain-a tzw. meta object compiler (w skrócie moc). 2.1.6 Wielowątkowość Istotnym aspektem tworzenia aplikacji z interfejsem graficznym jest ich wysoka responsywność. Zlecenie aplikacji przetworzenia większej ilości informacji nie powinno wstrzymywać jej działania (zamrozić). Biblioteka programowania interfejsu graficznego powinna dawać odpowiednie narzędzia, które pozwalałyby tworzyć aplikacje pracujące wielowątkowo zostawiając przynajmniej jeden główny wątek dla interfejsu użytkownika. Najczęściej spotykanym rozwiązaniem jest możliwość tworzenia wątków bezpośrednio przez programistę aplikacji, wraz z odpowiednimi prymitywami synchronizującymi. Jednak niektóre systemy operacyjne udostępniają o wiele bardziej wyrafinowane mechanizmy, chociażby takie jak asynchroniczne wywoływanie procedur 8. W praktyce jednak w bibliotekach wieloplatformowych spotyka się te mechanizmy, które stanowią wspólny mianownik pomiędzy różnymi platformami chociaż często konkretne API konkretnego systemu operacyjnego zostaje zdublowane w bibliotece. Wszystkie opisane w tej pracy biblioteki GUI wykorzystują podstawowy model wielowątkowości oparty na wątkach tworzonych przez aplikację i ręcznej ich synchronizacji. 8 http://msdn.microsoft.com/en-us/library/windows/desktop/ms681951%28v=vs.85% 29.aspx, ostatni dostęp 21 grudnia 2011 23
2.1.7 Obsługa zdarzeń Istniejące na rynku biblioteki GUI implementują mechanizm obsługi zdarzeń w różny sposób. Programiści każdej z nich starają się (bazując na rozwiązaniach w innych bibliotekach) stworzyć oraz ulepszać własną implementację. Niemniej jednak w przypadku aplikacji dla systemu Windows, wszystkie biblioteki bazują na mechanizmie dostarczonym wraz z systemem w postaci pętli obsługi komunikatów. Nomenklatura skojarzona z daną biblioteką może się zmieniać nie ma znaczenia, czy mechanizm korzysta z delegatów, architektury sygnał / slot, bądź też dynamicznych / statycznych map zdarzeń / komunikatów. Są to implementacje dostarczone wraz z biblioteką wewnątrz każdej z nich zaimplementowana została obsługa kolejki komunikatów systemu Windows gdyż stanowi to jedyną drogę w podstawowej komunikacji z systemem operacyjnym. W istniejących implementacjach bibliotek interfejsu graficznego spotyka się obecnie dwie bliskie sobie metody obsługi zdarzeń: Mapy / tabele komunikatów są to statyczne bądź dynamiczne listy wpisów definiujące rodzaj komunikatu, metodę jego obsługi oraz dodatkowe flagi i identyfikatory. W mechanizmie tym komunikaty są obsługiwane hierarchicznie poprzez wywoływanie metod klasy nadrzędnej. Podejście to implementują biblioteki: MFC, WTL oraz wxwidgets. Główną zaletą tej metody jest możliwość dokonania pewnych optymalizacji (z cachowaniem włącznie) wadą zaś możliwość wywołania tylko jednej metody obsługi dla danego komunikatu, zdarzenie delegat / sygnał slot zdarzenie (inaczej: obiekt zdarzenia) jest obiektem, który agreguje metody obsługi zdarzenia tak zwanych delegatów. W przypadku wystąpienia danego komunikatu, wywoływany jest obiekt zdarzenia który automatycznie wywołuje wszystkie dodane do niego metody obsługi. Podobny w strukturze i działaniu jest mechanizm sygnałów slotów. Implementacje tego podejścia obsługi zdarzeń można znaleźć w bibliotekach WinForms oraz Qt. 24
2.1.8 Wspomaganie tworzenia interfejsu graficznego Często decydującym czynnikiem przy wyborze biblioteki do tworzenia interfejsu graficznego aplikacji jest wsparcie dla automatycznego tworzenia struktury interfejsu użytkownika. Praktycznie każda komercyjna biblioteka GUI dostarcza własne narzędzie do prototypowania interfejsu graficznego aplikacji. Narzędzie to praktycznie zawsze tworzone jest przy użyciu biblioteki do której jest dołączane takie ścisłe powiązanie pozwala na łatwe rozszerzanie tego narzędzia o dodatkowe możliwości oraz nowości, które dodane zostały przez programistów biblioteki. Charakterystyczne jest, iż komercyjne biblioteki GUI posiadają bardzo rozbudowane i funkcjonalne narzędzia, niekiedy zintegrowane z środowiskiem tworzenia aplikacji (Microsoft Visual Studio, Qt Creator). Z drugiej strony, proste oraz otwarte biblioteki GUI często nie posiadają narzędzi do tworzenia interfejsu bądź narzędzia te są osobnymi aplikacjami (Dialog Blocks oraz wxformbuilder dla biblioteki wxwidgets). Programiści korzystający z API systemowego posiadają możliwość skorzystania z wielu edytorów zasobów, jakim przykładowo jest ResEdit. 2.2 Podsumowanie Przedstawione powyżej biblioteki mają wiele wad oraz zalet, jednak przedstawienie ich w sposób obiektywny jest zdecydowanie trudnym zadaniem. Często zamiast obiektywnej oceny programiści kierują się własnymi upodobaniami oraz przyzwyczajeniami. Za każdą z przedstawionych powyżej bibliotek stoi inna ideologia praktycznie każda funkcjonalność dostarczana przez bibliotekę obsługiwana jest w nieco odmienny sposób. Autor pracy, poprzez stworzenie własnej biblioteki GUI chciał wypełnić pewnego rodzaju lukę, która znajduje się pomiędzy przedstawionymi powyżej bibliotekami. Stworzona przez autora biblioteka UIX w całości prezentuje wiedzę oraz umiejętności programistyczne autora pracy. 25
26
Rozdział 3 Założenia projektowe Rozdział ten prezentuje założenia projektowe jakie postawił przed sobą autor pracy poprzez stworzenie implementacji biblioteki interfejsu graficznego użytkownika. Prezentuje on te założenia, które autor pracy uznał za kluczowe przed przystąpieniem do implementacji biblioteki UIX. 3.1 Podstawowe założenia Idea projektu zakłada, że będzie to mała biblioteka narzędziowa do tworzenia innych aplikacji narzędziowych. Będzie to biblioteka tylko dla systemów Windows ze względu na znajomość metod i technik programowania w tym środowisku przez autora. Zatem założeniami biblioteki są: stworzenie obiektowej warstwy abstrakcji na strukturalne API systemu Windows, przyspieszenie procesu tworzenia aplikacji przez programistę, ułatwienie integracji języka C++ z API systemowym, usprawnienie działania standardowych mechanizmów systemu Windows, 27
stworzenie prostego w użyciu oraz jednolitego API frameworku, ułatwienie integracji z innymi bibliotekami, stworzenie biblioteki o jak najmniejszym narzucie pamięciowym (ang. memory overhead) zarówno pamięci operacyjnej jak i dyskowej, ograniczenie architektury biblioteki do niezbędnego minimum obsługa interfejsu graficznego aplikacji, implementację standardowych kontrolek interfejsu graficznego dostępnych w systemie Windows. 3.1.1 Licencja Kod źródłowy implementacji biblioteki zostanie opublikowany publicznie na zasadach określonych przez licencję GNU Lesser General Public License v3.0. Więcej o warunkach korzystania z kodu można przeczytać na stronie internetowej licencji: http://www.gnu.org/licenses/lgpl.html 3.1.2 Język programowania Do implementacji biblioteki wybrany został język C++, ze względu na wysoką wydajność oraz możliwość integracji z API systemu operacyjnego. Język C++ jest językiem natywnym (niezarządzanym) co sprawia, że wszelkie optymalizacje wykonywane są już w trakcie kompilacji co odbija się pozytywnie na wydajności tworzonych aplikacji. 3.1.3 Windows API Aby móc tworzyć aplikacje z interfejsem graficznym w środowisku Windows, należy wykorzystać do tego funkcje API systemu Windows. Samo API stworzone jest w języku C i z użyciem zupełnie innych technik programistycznych niż te, które oferuje język C++. Dlatego biblioteka UIX będzie musiała rozwiązać takie problemy jak chociażby zarządzanie zasobami oraz translację pomiędzy uchwytami a wskaźnikami. Sam fakt zastosowania klas języka C++ komplikuje użycie Windows API. 28
3.1.4 Użycie łańcuchów Unicode Systemy Windows z serii NT wewnętrznie wszystkie teksty i ciągi znaków obsługują poprzez łańcuchy Unicode. Łańcuchy Unicode w systemach Windows różnią się od standardowych tym, że na jeden punkt kodowy standardu Unicode przypadają dwa bajty 1. Stosowanie w bibliotece łańcuchów jednobajtowych (tzw. ANSI) w aplikacjach dla systemu Windows traci rację bytu, ze względu na wszelkie konwersje ich do łańcuchów Unicode przez funkcje API. 3.1.5 Narzut pamięciowy Ze względu na samo zastosowanie biblioteki, jednym z głównych jej założeń jest niski narzut pamięciowy. Stworzenie pełnego interfejsu graficznego aplikacji będzie wymagało od biblioteki jak najmniejszego zużycia zasobów pamięciowych pozostawiając je dla tworzonej aplikacji. 3.2 Elementy interfejsu graficznego System Windows dostarcza zbioru kontrolek GUI, które programista może wykorzystać w swoich programach. Aby móc z nich skorzystać biblioteka UIX będzie musiała zaimplementować i rozszerzyć publiczny interfejs klas do istniejących kontrolek. Poniżej przedstawiono zestawienie kontrolek GUI, które autor ma zamiar wdrożyć do biblioteki: Ramka i okna dialogowe kontrolki pozwalające na organizację podstawowej struktury interfejsu graficznego, Pola tekstowe kontrolki pozwalające na wprowadzanie i wyświetlanie tekstu. Grupa ta obejmuje także specjalistyczne kontrolki do wprowadzania takich danych jak adresy IP, czas i datę czy też skróty klawiszowe, Przyciski kontrolki pozwalające na wykonywanie komend przez użytkownika czy też dokonywanie wyboru, 1 Tak naprawdę systemy Windows obsługują jedynie standard UCS-2, który jest podzbiorem standardu UTF-16 ze względu na brak obsługi tzw. par zastępczych (ang. surrogate pairs) 29
Kontrolki widoków 2 takich jak listy czy też drzewa, Paski kontrolki GUI rysowane w postaci prostokątnych pasków. Należą do nich takie kontrolki jak paski postępu, stanu czy też narzędzi. Szczególnym przypadkiem takiej kontrolki jest tzw. zbrojenie (ang. rebar) czyli kontener na inne paski i kontrolki, Inne wszystkie pozostałe kontrolki, takie jak panele, kalendarze, animacje czy też karty zakładek. 3.3 Mechanizmy biblioteki Poza implementacją samych kontrolek, biblioteka ta będzie musiała posiadać kilka mechanizmów, które pozwolą lepsze i szybsze tworzenie aplikacji, a także na jej dalszy rozwój w przyszłości. 3.3.1 RTTI Implementacja mechanizmu dynamicznej informacji o typie rozszerza standardowe zastosowania biblioteki o nowe możliwości, a także ułatwia pracę programisty języka C++. Dzięki zastosowaniu RTTI, implementacja wielu wzorców projektowych staje się o wiele bardziej prostsza i intuicyjna 3. Architektura aplikacji opartej o RTTI różni się od tej, jaką oferuje model komponentowy. 3.3.2 Wielowątkowość Jednym z założeń biblioteki jest możliwość tworzenia aplikacji wielowątkowych. Dlatego biblioteka UIX będzie musiała obsługiwać mechanizmy wielowątkowości, jakie dostarcza system Windows oraz pozwalać na integrację z resztą kodu. Dlatego implementacja takich elementów jak wątki i prymitywy synchronizujące w oparciu o API systemowe stworzy wystarczającą warstwę abstrakcji na model wielowątkowej aplikacji. 2 Bynajmniej nie są to wspomniane wcześniej widoki ze wzorca dokument-widok 3 Mechanizm ten można z powodzeniem wykorzystać we wzorcu Fabryki Abstrakcyjnej 30
3.3.3 Manager zasobów System Windows udostępnia taki mechanizm jak zarządzanie zasobami statycznymi aplikacji takimi jak okna dialogowe czy też obrazy. Manager ten pozwala na dostęp do zasobów zarówno z modułu aplikacji jak i innych modułów biblioteki. Rozszerzenie tego mechanizmu wewnątrz biblioteki pozwoli na automatyzację pewnych operacji (takich jak wyszukiwanie zasobów) oraz wprowadzi nowe możliwości (chociażby takie jak przesłanianie zasobów). 31
32
Rozdział 4 Implementacja Rozdział ten stanowi opis zaimplementowanego frameworku UIX User Interface Framework. Przedstawia on architekturę biblioteki wraz z omówieniem zaimplementowanych mechanizmów. W szczególności, rozdział ten opisuje takie aspekty jak: modularność mechanizm korzystania z wielu modułów aplikacji (takich jak rozszerzenia czy pluginy), zarządzanie zasobami rozszerzenie mechanizmu zarządzania zasobami dostępnego w systemie Windows o modularność, obsługa błędów wykorzystanie wyjątków i asercji do zapewnienia niezawodności oprogramowania, wielowątkowość możliwość tworzenia aplikacji wielowątkowych poprzez użycie wątków oraz obiektów synchronizacji, RTTI autorską implementację mechanizmu RTTI duplikującą i rozszerzającą mechanizm dostępny w języku C++, obsługę zdarzeń schematyczny zarys obsługi zdarzeń, ich przetwarzania i refleksji, 33
Framework UIX jest biblioteką pozwalającą na tworzenie aplikacji z graficznym interfejsem użytkownika w środowisku Microsoft Windows. Napisana została ona w języku C++, wykorzystując API systemowe do tworzenia kontrolek interfejsu graficznego. Implementuje ona mechanizmy, które ułatwiają proces tworzenia i rozwoju tworzonej aplikacji. Autor pracy starał ograniczać implementację biblioteki tylko i wyłącznie do implementacji mechanizmów związanych z zarządzaniem procesem oraz GUI tak, aby pozostawić wybór bibliotek obsługi pozostałych aspektów tworzonej aplikacji programiście. 4.1 Organizacja kodu Kod biblioteki napisany został w języku C++ zgodnie z przyjętymi przez autora zasadami projektowania interfejsów programistycznych: Wszystkie typy i funkcje zdefiniowane są w przestrzeni nazw UIX, Nazwy typów pochodzących z biblioteki rozpoczynają się od przedrostka uix, Nazwy funkcji znajdujących się w przestrzeni nazw UIX rozpoczynają się od przedrostka Uix, Nazwy wartości dla list wyliczeniowych rozpoczynają się od przedrostka będącego nazwą typu wyliczeniowego 1 : Listing 4.1: Przykładowa deklaracja listy wyliczeniowej enum uixhotkeymodifier { uixhotkeymodifier_alt = HOTKEYF_ALT, //!< Alt uixhotkeymodifier_control = HOTKEYF_CONTROL,//!< Ctrl uixhotkeymodifier_ext = HOTKEYF_EXT, //!< Super uixhotkeymodifier_shift = HOTKEYF_SHIFT, //!< Shift }; Każdy plik źródłowy posiada definicję jednej klasy obiektu wraz z typami bezpośrednio od niej zależnymi, 1 stanowi to pewnego rodzaju furtkę dla standardu C++ 11, który dodaje zasięgowe i silnie typowane listy wyliczeniowe enum class 34
Hierarchia klas ogranicza się do pojedynczego dziedziczenia oraz implementacji interfejsów, Aplikacje tworzone przy użyciu biblioteki zawierają się we własnej przestrzeni nazw oraz wykorzystują składnię using namespace UIX do włączania typów i funkcji do kodu aplikacji. Pozwala to na krótsze odwoływanie się do typów z przestrzeni nazw UIX, Kod frameworku korzysta z słowa kluczowego override, oznaczającego wymuszone nadpisywanie metod klasy bazowej w klasie pochodnej 2. Rozszerzenie to znajduje się w oficjalnym standardzie języka C++ 11 oraz dostępne jest dla kompilatora Visual C++. Dla pozostałych kompilatorów definiowany jest pusty symbol preprocesora 3. 4.2 Modułowa organizacja frameworku Cechą wspólną wielu bibliotek służących do tworzenia aplikacji z interfejsem graficznym jest ich podział na logiczne, oddzielne części moduły. Podział taki pozwala programiście wybrać, które moduły chce wykorzystać w tworzonej aplikacji. Przykładowo, do napisania prostej aplikacji kalkulatora nie jest potrzebna obsługa baz danych. Dzięki zastosowaniu podziału na moduły, zmniejsza się rozmiar aplikacji, a także wielkość dodatkowych plików binarnych związanych z aplikacją. Każda aplikacja napisana dla systemu Windows, uruchamiana jest jako proces z odrębną przestrzenią adresową. Zanim zostanie wywołana główna funkcja main (lub też WinMain), wywoływane są funkcje DllMain zależnych bibliotek DLL a także wywoływane konstruktory obiektów statycznych. Moment wywołania funkcji WinMain oraz DllMain można wykorzystać w celu utworzenia listy łączonej załadowanych modułów. 2 Mechanizm ten wymusza na kompilatorze sprawdzenie czy nadpisywana metoda została zdeklarowana wcześniej w hierarchii dziedziczenia. 3 Dla kompilatora GCC dostępna jest opcja -Woverloaded-virtual generująca w opisywanym przypadku jedynie ostrzeżenie. Źródło: http://gcc.gnu.org/onlinedocs/gcc-4.6.2/ gcc/c_002b_002b-dialect-options.html#c_002b_002b-dialect-options ostatni dostęp 27 grudnia 2011 35
Architektura frameworku UIX przewiduje trzy rodzaje modułów: moduł aplikacji moduł ten skojarzony jest bezpośrednio z procesem uruchamianej przez system aplikacji. Odpowiada on za utworzenie wewnętrznej struktury aplikacji, moduł rdzenia ten rodzaj modułów stanowi pewnego rodzaju furtkę pozostawioną przez autora, pozwalającą na dodawanie do biblioteki autoryzowanych rozszerzeń, które mogą osiągnąć status modułów rdzennych. Moduły rdzenia mają przewagę nad zwykłymi modułami rozszerzeń, gdyż w wielu mechanizmach wewnątrz frameworku obsługiwane są jako jedne z pierwszych, moduł rozszerzenia rozszerzenia są dodatkowymi modułami, w których implementowane są klasy oraz mechanizmy, które nie znalazły się w rdzeniu biblioteki. W szczególności, rozszerzenia mogą zawierać zasoby statyczne aplikacji, Relacje pomiędzy tymi modułami w architekturze frameworku UIX przedstawia rysunek 4.1. Application.exe Process State LanguageHandle CoreModule Handle ResourceHandle uixcore10.dll ModuleState LanguageHandle NextModule Handle ResourceHandle NULLPTR Application ModuleListHead uixext10.dll userext.dll Application ModuleState ModuleState LanguageHandle NextModule LanguageHandle NextModule Handle Handle ResourceHandle ResourceHandle Rysunek 4.1: Struktura modułów frameworku 36
4.2.1 uixprocessstate stan procesu Stan procesu jest obiektem, który stanowi trzon uruchomionej aplikacji. Odpowiada on za dostęp do struktury załadowanych przez aplikację modułów i rozszerzeń pozwalając także na bezpośredni dostęp do rdzenia biblioteki. Reprezentuje on także stan modułu aplikacji. Stan procesu należy także utożsamiać z głównym modułem aplikacji 4.2.2 uixmodulestate stan modułu Każdy załadowany przez aplikację moduł, który wchodzi w skład biblioteki UIX, opisany jest poprzez obiekt stanu modułu. Precyzuje on takie informacje jak stan modułu, uchwyty do bibliotek zależnych (m.in. z zasobami), a także tworzy listę łączoną załadowanych modułów. Listing 4.2: Struktura stanu modułu struct UIXAPI uixmodulestate : ui xpro cess Local Obje ct { //... }; // Uchwyt do modu ł u HMODULE m_ Handle ; // Uchwyt do modu ł u ( innego ) z zasobami HMODULE m_ Resource ; // Uchwyt do modu ł u z zasobami jezykowymi HMODULE m_ LangResource ; // Wska ź nik do kolejnego modu ł u uixmodulestate * m_ NextModule ; // Okre ś la, czy modu ł nale ż y do rdzenia frameworku BOOL m_ IsCore ; // Okre ś la, czy modu ł zosta ł zainicjalizowany BOOL m_ Initialized ; Poniższy kod przedstawia główną procedurę rdzenia biblioteki UIX. Odpowiada ona za obsługę inicjalizacji i finalizacji rdzennego modułu podczas dołączania i odłączania go od aplikacji. 37
Listing 4.3: Funkcja DllMain rdzennej biblioteki BOOL APIENTRY DllMain ( HINSTANCE instance, DWORD reason, LPVOID reserved ) { uixunreferenced ( reserved ); switch ( reason ) { case DLL_ PROCESS_ ATTACH : UIX :: UixInitializeCoreModule ( instance ); break ; case DLL_ PROCESS_ DETACH : UIX :: UixShutdownCoreModule (); break ; case DLL_ THREAD_ ATTACH : case DLL_ THREAD_ DETACH : break ; } return TRUE ; } 4.2.3 uixthreadstate stan wątku Stan wątku jest obiektem przechowującym dane specyficzne dla wątku. Wprowadzenie obiektu stanu wątku pozwala uniknąć wielu niepotrzebnych synchronizacji, zwłaszcza wtedy, gdy dane te nie mają charakteru danych globalnych. W frameworku UIX, struktura stanu wątku zdefiniowana została następująco: Listing 4.4: Struktura stanu wątku struct UIXAPI uixthreadstate : ui xpro cess Local Obje ct { //... //! Aktualnie przetwarzany komunikat MSG m_ CurrentMsg ; //! Ostatnia pozycja kursora uixpoint m_ LastCursorPos ; //! Ostatnio przetworzony identyfikator komunikatu UINT m_ LastMsg ; //! Wska ź nik na obiekt aktualnego w ą tku 38
uixuithread * m_ CurrentThread ; //! Ostatnio przes ł any komunikat MSG m_ LastSentMsg ; }; //! Uchwyt do haku tworzenia okna HHOOK m_createwindowhook_handle ; //! Dane dla haku tworzenia okna uixwindow * m_createwindowhook_window ; Wszystkie dane przechowywane w powyższej strukturze, są inne dla każdego wątku. Funkcjonalność tą osiągnięto dzięki mechanizmowi nazwanemu TLS (ang. Thread Local Storage) 4.2.4 uixprocessstatelock globalne blokady procesu Aby móc korzystać z możliwości, jakie niesie ze sobą wielowątkowość, biblioteka UIX definiuje mechanizm globalnych blokad procesu. Jest to mechanizm, który odpowiada za synchronizację dostępu do zasobów biblioteki poprzez sekcje krytyczne. Mechanizm ten nie jest dostępny dla programisty aplikacji bezpośrednio, zaś lista obsługiwanych blokad podlega ciągłemu rozwojowi. W momencie pisania pracy mechanizm ten obsługiwał jedynie możliwość synchronizacji dostępu do wewnętrznej struktury modułów. Poniższy przykład prezentuje sposób, w jaki mechanizm ten jest wykorzystywany w funkcji ładującej biblioteki DLL: Listing 4.5: Implementacja funkcji UixLoadLibrary HINSTANCE UixLoadLibrary ( const uixchar * libfilename ) { // Zablokuj list ę modu ł ó w uixprocessstatelock :: Lock ( uixprocessstatelock :: Lock_ ModuleList ) ; // Za ł aduj bibliotek ę HINSTANCE handle = :: LoadLibrary ( libfilename ); // Odblokuj list ę modu ł ó w 39
uixprocessstatelock :: Unlock ( uixprocessstatelock :: Lock_ ModuleList ); } return handle ; 4.2.5 Aplikacja Każdy proces, który korzysta z biblioteki UIX musi implementować własną klasę aplikacji. Biblioteka definiuje klasę bazową uixapplication, która odpowiada za uruchomienie właściwego mechanizmu przetwarzania komunikatów. W istocie klasa uixapplication rozszerza klasę wątku interfejsu użytkownika uixuithread, pozwalając na sterowanie głównym wątkiem aplikacji 4. Listing 4.6: Definicja klasy aplikacji class UIXAPI uixapplication : public uixuithread { uixdeclare_dynamic_class ( uixapplication ); public : //... // Inicjalizacja instancji aplikacji virtual BOOL InitInstance () override ; // Finalizacja instancji aplikacji virtual LRESULT ExitInstance () override ; // Uruchamia p ę tle komunikat ó w virtual LRESULT Run () override ; // Zdarzenie przechwycenia wyj ą tku przez aplikacj ę virtual void OnException ( const uixexception & e) override ; }; // Globalny dost ę p do utworzonej aplikacji static uixapplication * Get (); //... 4 Nie jest to do końca możliwe ze względu na bezpośrednie skojarzenie głównego wątku (ang. Main Thread) z procesem wykonywanej aplikacji 40
Obiekt aplikacji inicjalizowany jest w funkcji UixMain, która wywoływana jest na samym końcu procesu uruchamiania aplikacji. Od tego momentu wykonywany jest kod napisany przez programistę aplikacji tworzone jest okno główne aplikacji oraz uruchamiana pętla komunikatów wątku 4.2.6 Rozszerzenia Biblioteka dynamiczna rozszerzenia niewiele różni się od zwykłej biblioteki w postaci pliku DLL. Aby zwykła biblioteka stała się rozszerzeniem frameworku UIX, należy w funkcji punktu wejścia biblioteki DLL wywołać dwie funkcje odpowiedzialne za inicjalizację i finalizację modułu rozszerzenia: UixInitializeExtModule inicjalizująca moduł rozszerzenia. Uzupełnia ona informacje w przekazanym obiekcie stanu modułu i dodaje moduł do listy rozszerzeń w strukturze stanu procesu. Wywoływana gdy biblioteka DLL dołączana jest do uruchomionego procesu (argument reason == DLL PROCESS ATTACH), UixShutdownExtModule finalizująca moduł rozszerzenia. Podobnie jak funkcja UixInitializeExtModule, wywoływana jest w funkcji punktu wejścia (dla argumentu reason == DLL PROCESS DETACH) gdy biblioteka DLL jest odłączana od procesu, 4.3 Zarządzanie zasobami Każdy plik wykonywalny w systemie Windows, poza skompilowanym kodem posiada sekcję rsrc. Przechowuje ona statyczne zasoby, skonwertowane do formatu binarnego przez specjalny kompilator (Resource Compiler). Zasoby te obejmują takie dane binarne jak: okna dialogowe zawierają informacje o rozmieszczeniu kontrolek na oknie dialogowym, zasoby graficzne są to obrazy, takie jak bitmapy, kursory czy też ikony, 41
napisy wszystkie łańcuchy tekstowe dostępne dla aplikacji, np. z komunikatami dla użytkownika, akceleratory zawierają informacje o skrótach klawiszowych oraz przypisanych do nich identyfikatorach komend, menu zasób ten opisuje strukturę menu wraz identyfikatorami komend i etykietami tekstowymi, paski zadań podobnie jak menu, zawierają listę przycisków i separatorów, wraz z identyfikatorami komend i etykietami tekstowymi, manifesty zasoby zawierające informacje o pliku wykonywalnym, a także określające wymagania dla komponentów systemu operacyjnego, informacje o wersji zawierające informacje o wersji pliku, wydawcy, nazwie produktu itp, inne dane dane binarne, pliki HTML, animacje. Każdy zasób w aplikacji identyfikowany jest przez swój identyfikator. Należy zaznaczyć, że dwa różne zasoby mogą mieć taki sam identyfikator aplikacja musi wraz z identyfikatorem sprecyzować typ zasobu. W powyższym przykładzie, w module aplikacji istnieją dwa zasoby o identyfikatorze IDR TOOLBAR STD, jednak są to zasoby różnego typu (RT BITMAP bitmapa, oraz RT TOOLBAR pasek narzędziowy). Architektura systemu Windows ogranicza liczbę zasobów danego typu do 65535 różnych identyfikatorów. W praktyce jednak, wewnątrz aplikacji może znajdować się o wiele większa liczba zasobów, ze względu na mechanizm lokalizacji aplikacji pojedynczy zasób może istnieć w wielu wersjach językowych. Korzystanie z zasobów możliwe jest dzięki API systemu Windows. Funkcje te wymagają, aby poza identyfikatorem zasobu podawać także uchwyt do modułu, w którym się ładowany zasób znajduje. Framework UIX odpowiada za automatyczne odnalezienie modułu, w którym dany zasób się znajduje. 42
Application.exe IDR_TOOLBAR_STD = 0x10000 RT_MENU IDR_MAINMENU RT_DIALOG IDD_DLG_ABOUT IDD_DLG_ABOUT = 0x10001 IDR_MAINMENU = 0x10002 RT_BITMAP RT_TOOLBAR IDR_TOOLBAR_STD IDR_TOOLBAR_STD Rysunek 4.2: Zasoby w module aplikacji Automatyczne odnajdywanie zasobów pozwala na przesłanianie zasobów zrealizowane jest to poprzez hierarchiczne przeszukiwanie załadowanych modułów. Manager zasobów, szuka ładowanego zasobu w module aplikacji. W przypadku, gdy poszukiwanego zasobu tam nie ma, manager przechodzi przez listę załadowanych rozszerzeń. Na samym końcu sprawdzane są zasoby rdzenia biblioteki. 4.4 Struktura aplikacji Tak jak każda biblioteka GUI, tak i implementacja biblioteki UIX narzuca programiście pewnego rodzaju strukturę tworzonej aplikacji. Zaprojektowana architektura nie różni się od tych znanych z wymienionych w rozdziale drugim bibliotek. Poniżej przedstawiono zarys tej struktury. 4.4.1 Aplikacja Jest głównym obiektem biblioteki UIX. Od niej rozpoczyna się właściwa faza uruchamiania programowanej aplikacji. Biblioteka UIX zakłada, że istnieje dokładnie 43
jeden obiekt implementujący aplikację dostęp do niej możliwy jest globalnie dzięki użyciu zmodyfikowanej wersji wzorca projektowego Singleton. Z punktu widzenia programisty aplikacji, klasa uixapplication nie różni się od klasy wątku interfejsu graficznego (uixuithread). 4.4.2 Ramki i okna dialogowe Głównym zadaniem metody InitInstance klasy uixapplication jest inicjalizacja aplikacji, a co za tym idzie utworzenie okna głównego aplikacji. Zachowanie tej metody będzie się różniło w zależności od wybranego rodzaju okna głównego: okno dialogowe jest najprostszym rodzajem okna. Okna dialogowe można tworzyć manualnie poprzez rozszerzanie klasy uixdialog lub też automatycznie poprzez ładowanie struktury kontrolek z zasobów statycznych aplikacji. Najczęściej wybierane jest do prostych aplikacji ze względu na możliwość wczytywania okna dialogowego z zasobów. Utworzone okno dialogowe uruchamiane jest w trybie modalnym (uruchamia własną pętlę obsługi komunikatów) dlatego metoda InitInstance zwraca wartość FALSE, informując obiekt aplikacji o zakończeniu jej działania bez uruchamiania głównej pętli komunikatów, ramka aplikacji jest to okno o wiele bardziej zaawansowane w obsłudze od okna dialogowego. Ramka aplikacji z założenia nie może być oknem modalnym najczęściej nie posiada okna nadrzędnego, które mogłaby blokować. W bibliotece UIX zaimplementowana w klasie uixframe. Jego struktura musi być tworzona manualnie przez programistę gdyż nie posiada ono możliwości wczytywania struktury kontrolek z zasobów aplikacji. Metoda InitInstance zwraca wartość TRUE, informując obiekt aplikacji o konieczności uruchomienia pętli przetwarzania komunikatów. 4.4.3 Kontrolki Kontrolki są podstawowymi elementami budowy interfejsu graficznego. Ze względu na praktycznie odmiennie pełnione przez nie funkcje, autor pracy nie starał się grupować kontrolek według żadnego kryterium. Wszystkie klasy implementujące 44
kontrolki systemu Windows posiadają wspólną klasę bazową uixwindow. Definiuje ona podstawowe metody pozwalające na zarządzanie nimi, konfigurowanie ich oraz wymianę komunikatów a także zdeklarowanie metod obsługi standardowych zdarzeń. 4.5 Obsługa błędów asercje i wyjątki Aby ułatwić programiście odnajdywanie błędnie działającego kodu, framework UIX został napisany przy użyciu technik programowania defensywnego[12, str. 246]. Kod biblioteki posiada wiele zabezpieczeń przed błędnym działaniem, a jego trzon stanowi mechanizm asercji i zapewnień. W sekcji tej zostaną przedstawione zaimplementowane mechanizmy, które ułatwiają proces tworzenia programów, jak i samego rozwoju frameworku. 4.5.1 Wyjątki uixexception Mechanizm obsługi wyjątków jest częścią standardu języka C++. Framework UIX definiuje własną hierarchię możliwych wyjątków, dostosowując je do obsługi błędów zaimplementowanej w systemie Windows. uixexception klasa bazowa dla wszystkich wątków definiuje wspólny interfejs, pozwalający na utworzenie komunikatu tekstowego, który można wyświetlić użytkownikowi, uixuserexception klasa wyjątku dostępna dla programisty aplikacji. Programista może używać tej klasy wyjątku do informowania użytkownika o błędach niezwiązanych z frameworkiem UIX, uixinvalidargumentexception klasa wyjątku zrzucanego, gdy nastąpiło przerwanie działania aplikacji spowodowane niespełnieniem warunku zapewnienia (uixensure), uixinvalidoperationexception klasa wyjątku zrzucana gdy ostatnia operacja zakończona została niepowodzeniem. Dodatkowo informuje ona o kodzie błędu wraz z odpowiadającym mu komunikatem systemowym, 45
uixnotimplementedexception klasa wyjątku zrzucana gdy wywoływana metoda nie została zaimplementowana. Wyjątek ten jest użyteczny w trakcie tworzenia aplikacji, gdyż przypomina on programistom pracującym nad aplikacją o braku implementacji pewnej metody. Ze względu jednak na ograniczony zasięg przechwytywania wyjątków, wyjątek ten może zostać złapany albo przez kod aplikacji, albo w procedurze przetwarzania komunikatów. Należy zwrócić jednak uwagę na fakt, że rzucenie wyjątkiem nie dziedziczącym z uixexception nie zostanie złapane programista musi sam zadbać o obsługę wyjątków nienależących do biblioteki UIX. Klasa uixapplication pozwala na zmianę sposobu informowania użytkownika aplikacji o wystąpieniu sytuacji wyjątkowej. Każdy wyjątek przechwycony w procedurze obsługi komunikatu zostaje przekierowany do tej właśnie metody. Standardowa implementacja obejmuje wyświetlenie komunikatu z informacją o wyjątku w postaci okna dialogowego pokazanego na rysunku 4.3. Rysunek 4.3: Okno dialogowe wyświetlające komunikat o nieprzechwyconym wyjątku 4.5.2 Asercje uixassert Asercja jest predykatem, który z założenia ma być spełniony. Umieszczona w kodzie wskazuje, że w danym miejscu sprawdzany warunek ma być prawdziwy. Niespeł- 46
nienie warunku asercji powoduje zatrzymanie działania programu na danej asercji. Działanie wstrzymanego programu można wznowić lub przerwać, bądź też zignorować całą asercję. Rysunek 4.4 przedstawia okno dialogowe, jakie zostaje pokazane, gdy nastąpi przerwanie programu z powodu niespełnienia warunku asercji Rysunek 4.4: Okno dialogowe przerwania asercji Asercje są podstawowym mechanizmem testowania poprawności kodu. Korzystanie z asercji przyspiesza proces tworzenia i testowania kodu; asercje posiadają pewien narzut na prędkość działania w kompilacji debug w przeciwieństwie do kompilacji release, gdzie asercje w ogóle nie są kompilowane. Listing 4.7: Przykładowe użycia asercji void MyMainForm :: OnSize ( uixsizeeventargs & e) { uixassert ( this!= NULLPTR ); uixassert (:: IsWindow ( m_handle )); // Reszta kodu } 4.5.3 Zapewnienia uixensure Podobnie jak asercje, zapewnienia są predykatami sprawdzanymi w trakcie działania programu, z tą jednak różnicą, że działają w trybie kompilacji Release, rzucając 47
wyjątkiem uixinvalidargumentexception. Listing 4.8: Użycie mechanizmu zapewnień try { uixensure ( false == true ); } catch ( uixinvalidargument & e) { // Dzia ł a jak oczekiwano : false!= true } 4.5.4 Walidacje uixvalidate Mechanizm walidacji obiektów dotyczy tylko tych klas, które dziedziczą po głównej klasie obiektu, uixobject. Sama walidacja polega na sprawdzaniu wewnętrznego stanu obiektu, i zwracaniu ogólnego podsumowania (które sprowadza się do wartości prawda/fałsz). Walidacje ze względu na rozmiar generowanego kodu, ignorowane są w kompilacji release przeznaczone są one do działania w trakcie tworzenia i testowania oprogramowania. Możliwe jest jednak ich wykorzystanie w kompilacji wydaniowej. Listing 4.9: Mechanizm walidacji void GenerateDocument ( uixobject * documenttemplate ) { // Asercja uixassert ( documenttemplate!= NULLPTR ); // Walidacja uixvalidate ( documenttemplate ); //... } 4.5.5 Zrzuty uixdump Mechanizm zrzucania stanów obiektów jest uruchamiany w przypadku wystąpienia błędu terminalnego lub nieprawidłowego przerwania działania aplikacji przez 48
użytkownika. Główną ideą tego mechanizmu jest zrzucenie stanu obiektów do specjalnego obiektu, zwanego dalej kontekstem zrzutu. Generowanie zrzutu obejmuje wszystkie obiekty dziedziczące z klasy uixobject. Listing 4.10: Zrzut stanu obiektu void MainForm :: DumpObject ( uixdumpcontext & ctx ) { // Wywo ł anie metody nadrz ę dnej uixframe :: DumpObject ( ctx ); } // Sekcja z dzie ć mi formatki ctx. Begin (" Children "); m_toolbarstd. DumpObject ( ctx ); m_button. DumpObject ( ctx ); ctx. End (); 4.5.6 Śledzenie uixtrace Ostatnim mechanizmem, który ułatwia debugowanie aplikacji jest mechanizm śledzenia. Dzięki wykorzystaniu makra uixtrace, możliwe jest przesyłanie komunikatów tekstowych z aplikacji do debuggera. Makro to pozwala na formatowanie łańcuchów w stylu funkcji sprintf. Mechanizm ten jest wyłączany w kompilacjach release. Listing 4.11: Śledzenie komunikat o rozmiarze okna void MainForm :: OnSize ( uixsizeeventargs & e) { //... uixtrace (" New size : (%d, %d)", e. Width, e. Height ); //... } 4.6 Wielowątkowość Obsługa wielowątkowości jest ważną cechą bibliotek interfejsu graficznego. Dzięki zastosowaniu mechanizmu wątków i obiektów synchronizacji systemu Windows, 49
możliwa jest implementacja aplikacji wielowątkowych. 4.6.1 Wątek Wątek jest podstawową jednostką wykonawczą w systemie Windows. Każdy proces posiada jeden wątek główny. Przy użyciu API systemowego możliwe jest tworzenie wielu wątków oraz synchronizację wykonywania operacji oraz dostępu do zasobów. Framework UIX opakowuje funkcje API systemu Windows w klasie uixthread. 4.6.2 Wątek UI W frameworku UIX, wątek UI jest specjalnym rozszerzeniem standardowego wątku o możliwość przetwarzania komunikatów pochodzących z komponentów interfejsu graficznego. Wątek UI zaimplementowany jest w postaci klasy uixuithread, która synchronicznie przetwarza komunikaty w tzw. pętli komunikatów. Umożliwia to tworzenie aplikacji posiadających wiele okien głównych, a które są wykonywane niezależnie od siebie, w osobnych wątkach. Specjalną implementacją wątku UI jest klasa aplikacji, która to automatycznie przejmuje kontrolę nad wątkiem głównym aplikacji. Architektura systemu Windows ogranicza jednak możliwość przenoszenia utworzonych już okien pomiędzy wątkami. Ograniczenie jest to związane z przypisaniem danego okna do wątku. Jeśli okno zostało utworzone w innym wątku, tylko ten wątek będzie w stanie obsługiwać jego komunikaty. Dodatkowo, nie wolno tworzyć hierarchii okien z elementów utworzonych w różnych wątkach. Pomiędzy wątkami które posiadają pętle komunikatów możliwe jest asynchroniczne przesyłanie komunikatów. 4.6.3 Prymitywy synchronizacyjne Synchronizacja w przetwarzaniu wielowątkowym, w frameworku UIX odbywa się poprzez prymitywy synchronizacji systemu Windows, takie jak: 50
uixcriticalsection sekcja krytyczna [20, str. 238] implementuje systemowy mechanizm sekcji krytycznej, czyli blokowania wykonywania fragmentu kodu, który korzysta z zasobu współdzielonego. Jest obiektem nienazwanym (czyli lokalnym procesu), nie może być współużytkowany przez inne procesy 5, uixsemaphore semafor [20, str. 245] implementuje systemowy obiekt semafora, pozwalający równoległy dostęp do zasobu przez wiele wątków. Podobnie jak sekcja krytyczna, jest obiektem nienazwanym, uixmutex muteks [20, str. 330] obiekt synchronizacji, podobny w działaniu do sekcji krytycznej w przeciwieństwie do niej może być nazwany i w konsekwencji używany pomiędzy wieloma procesami znającymi jego nazwę. Może służyć do blokowania uruchamiania więcej niż jednej instancji aplikacji, Wszystkie te klasy rozszerzają bazową klasę obiektu synchronizacji, zdefiniowaną jako uixthreadsyncobject. Dodatkowo, w celu ułatwienia automatycznego wywoływania metod blokowania i odblokowywania na obiekcie blokady, zdefiniowana została klasa blokady zasięgowej (uixscopedlock), korzystająca z idiomu RAII języka C++. Listing 4.12: Przykład zablokowania dostępu do obiektu sekcja krytyczna class MyResource { public : void Load ( const uixstring & filename ) { // Blokada zasi ę gowa - automatycznie aktywuje i dezaktywuje blokad ę uixscopedlock (& m_lock ); //.. reszta kodu } private : // Obiekt synchronizacji - sekcja krytyczna uixcriticalsection m_ Lock ; }; 5 Ze względu na brak nazwy, którą mógłby znać inny proces 51
4.6.4 Obiekty lokalne wątku i procesu Tworzenie aplikacji wielowątkowych nie zawsze sprowadza się do synchronizacji wątków poprzez sekcje krytyczne czy też semafory. Charakter niektórych danych pozwala na duplikowanie ich w każdym wątku mowa wtedy o danych lokalnych wątku. Obiekty które powinny być lokalnymi wątku bądź też procesu, powinny dziedziczyć po klasie uixprocesslocalobject. Klasa ta implementuje dynamiczną alokację obiektu w stercie aktualnego procesu. Daje to pewność, że obiekt będzie unikalny dla każdej uruchomionej aplikacji. Aby móc skorzystać z obiektów lokalnych wątku lub procesu, należy zdeklarować zmienne dostępu do nich. Do tego celu powinny być wykorzystane klasy szablonowe uixthreadlocal<t> oraz uixprocesslocal<t> odpowiednio obiektu lokalnego wątku i procesu. 4.7 Mechanizm RTTI Język C++ posiada dość ograniczony funkcjonalnie mechanizm dynamicznej informacji o typie (ang. Run Time Type Information). W przeciwieństwie do języków takich jak C# czy też Delphi, ogranicza się jedynie do implementacji operatora dynamic cast oraz udostępniania nazwy typu w trakcie działania programu. W przypadku bibliotek interfejsu graficznego, standardowy mechanizm języka C++ staje się niewystarczający. Dlatego framework UIX implementuje własny, o wiele bardziej funkcjonalny miechanizm zastępczy dla RTTI (dalej nazywany metaklasami). Listing 4.13: Interfejs klasy uixmetaclass class uixmetaclass { public : //! Tworzy nowy obiekt klasy uixobject * Create () const ; //! Czy klasa jest dynamiczna 52
BOOL IsDynamic () const ; //! Sprawdza czy klasa dziedziczy z innej klasy BOOL IsDerivedFrom ( const uixmetaclass * Class ) const ; //! Zwraca nazw ę klasy const char * GetName () const ; //! Zwraca klas ę bazow ą const uixmetaclass * GetBaseClass () const ; //! Zwraca rozmiar obiektu klasy int GetSize () const ; }; //! Zwraca delegata tworz ą cego nowy obiekt klasy uixcreateobjectdelegate GetCreateObjectDelegate () const ; Z punktu widzenia frameworku, metaklasa jest obiektem posiadającym informacje o typie innego obiektu. W przeciwieństwie do RTTI języka C++, obiekt metaklasy można swobodnie przekazywać, jak i dynamicznie tworzyć instancje danej klasy. Zastosowanie metaklas upraszcza implementacje wzorca fabryki abstrakcyjnej. Należy jednak zwrócić uwagę, iż mechanizm ten nie stara się zaimplementować mechanizmu refleksji (znanego z języków takich jak Java czy też C#), ograniczając się jedynie do minimalnych funkcjonalności, niezbędnych do działania biblioteki. Zaimplementowany mechanizm RTTI ogranicza się jednak tylko do klas biblioteki oraz działa równolegle do standardowej implementacji. Obsługa mechanizmu ogranicza się jedynie do klas i struktur, które zdefiniowane zostały z użyciem makr: uixdeclare DYNAMIC CLASS(Class) klasa dynamiczna klasa, której obiekty mogą być tworzone dynamicznie poprzez obiekt uixmetaclass. Odpowiada mu makro uiximplement DYNAMIC CLASS(Class, Base), uixdeclare ABSTRACT CLASS(Class) klasy abstrakcyjne są to klasy które są pewnego rodzaju abstrakcją i mogą stanowić jedynie bazę dla innych klas. Odpowiada mu makro uiximplement ABSTRACT CLASS(Class, Base), 53
Aby móc wykorzystać ten mechanizm w praktyce, należy skorzystać z dodatkowych makr. uixmeta CLASS(Class) zwraca obiekt metaklasy dla zadanej klasy uixdynamic CAST(Class, Object) rzutuje podany obiekt na żądany typ uixis DERIVED FROM(Object, Class) sprawdza, czy typ obiektu należy do hierarchii dziedziczenia Listing 4.14: Przykład użycia mechanizmu uixmetaclass uixwindow * CreateWindow ( uixmetaclass * meta ) { // Je ś li klasa dziedziczy z uixwindow if (meta -> IsDerivedFrom ( uixmeta_class ( uixwindow ))) { // Tworzymy nowy obiekt i rzutujemy go na uixwindow return uixdynamic_cast ( uixwindow, meta -> Create ()); } // Je ś li nie, zwracamy NULLPTR return NULLPTR ; } uixbutton * CreateButton () { // Pobieramy metaklas ę dla uixbutton uixmetaclass * meta = uixmeta_ CLASS ( uixbutton ); // Tworzymy nowe okno ( przycisk ) uixwindow * obj = CreateWindow ( meta ); // Rzutujemy stworzony obiekt na klas ę uixbutton uixbutton * button = uixdynamic_ CAST ( uixbutton, obj ); return button ; } 4.8 Przetwarzanie komunikatów W systemie Windows interakcja programu z otoczeniem odbywa się poprzez mechanizm przesyłania komunikatów. Każda aplikacja która posiada graficzny interfejs użytkownika musi uruchomić tzw. kolejkę przetwarzania komunikatów. Stanowi ją 54
zbiór funkcji API systemu Windows, odpowiedzialnych za pobieranie, translację i rozdysponowanie przetwarzanych komunikatów do odpowiednich okien odbiorców. System Windows stawia istotny wymóg, aby każdy wątek odpowiedzialny za obsługę interfejsu użytkownika posiadał własną kolejkę komunikatów. Komunikaty te mogą być przesyłane pomiędzy wątkami asynchronicznie 6 bądź też w obrębie danego wątku zarówno synchronicznie jak i asychronicznie. Rolą dyspozytora komunikatów jest wywołanie odpowiedniej procedury obsługi komunikatów okna odbiorcy. Stanowi on połączenie pomiędzy kolejką komunikatów a graficznym interfejsem użytkownika. 4.8.1 Rejestrowanie klasy okna W celu tworzenia nowych okien, system Windows musi posiadać kilka informacji o docelowej klasie okna. Informacje te precyzują m.in. procedurę obsługi komunikatów a także dodatkowe właściwości okna, chociażby takie jak sposób automatycznego odrysowywania a także standardowy pędzel, którym będzie odmalowywane okno. Na uproszczenie rejestrowania tych klas (a także sprawdzanie czy klasa została już zarejestrowana) pozwala klasa uixwindowclass. TryRegister metoda próbuje zarejestrować nową klasę okna. W przypadku, gdy klasa już została zarejestrowana, zwraca wartość FALSE. Dodatkowo pozwala na ustawienie pędzla, którym ma być wypełniane okno a także dodatkowe style klasy okna, IsRegistered sprawdza czy w systemie została zarejestrowana klasa o podanej nazwie. Zwraca wartość TRUE, gdy klasa o podanej nazwie istnieje. 4.8.2 Procedura obsługi komunikatów Podczas rejestracji klasy okna w systemie Windows, programista musi podać statyczny wskaźnik do tzw. procedury obsługi komunikatów okna (ang. Window Pro- 6 funkcja PostThreadMessage 55
cedure). Odpowiada ona za przetwarzanie komunikatów przychodzących do danego typu okna. O ile rozwiązanie to sprawdza się w przypadku pisania bardzo prostych aplikacji w języku C, o tyle w przypadku języków obiektowych pojawia się istotna wada takiego rozwiązania: informacja o obiekcie docelowym nie jest powiązana ze wskaźnikiem na funkcję procedury obsługi 7. Biblioteka UIX posiada globalną procedurę obsługi komunikatów, która pełni rolę pośrednika pomiędzy dyspozytorem komunikatów systemu Windows a samą biblioteką. Każde nowo tworzone okno ustawia tą procedurę jako główną procedurę obsługi komunikatów zarówno podczas rejestrowania nowej klasy okna jak i podczas jego subclassingu. Jej działanie polega na wywoływaniu metod obiektów skojarzonych z nadawcą przychodzącego komunikatu Listing 4.15: Procedura dyspozytora komunikatów LRESULT uixwindow :: UixWindowProc ( HWND handle, UINT message, WPARAM wparam, LPARAM lparam ) { // Pobierz okno skojarzone z uchwytem uixwindow * window = uixwindow :: FromHandle ( handle ); // Je ś li okno nie zosta ł o subclassowane if (( window == NULLPTR ) ( window - > m_ Handle!= handle )) { // Oddeleguj obs ł ug ę komunikatu do systemu Windows return DefWindowProc ( handle, message, wparam, lparam ); } uixthreadstate & ts = uixthreadstate :: Get (); //... LRESULT result = 0; try { result = window - > WindowProc ( message, wparam, lparam ); } catch ( uixexception & e) { // Obs łuż wyj ą tek if ( ts. m_ CurrentThread!= NULLPTR ) { 7 rozwiązanie to ominęła firma Borland w swoim produkcie Borland C++ Builder, wprowadzając słowo kluczowe closure umożliwiający tworzenie tzw. domknięć http://docwiki. embarcadero.com/radstudio/en/closure ostatni dostęp 15 grudnia 2011 56
} } //... result = ts. m_currentthread -> ProcessWindowProcException (e, & previousmsg ); } return result ; 4.8.3 Subclassing Aby móc kontrolować proces tworzenia elementów interfejsu graficznego, a także mieć dostęp do szczegółowych informacji o samym procesie tworzenia kontrolki (wraz z jej właściwościami), twórcy systemu Windows wprowadzili dwa mechanizmy: subclassing (ang. rozszerzanie) strukturalny odpowiednik dziedziczenia w programowaniu obiektowym. W przypadku systemów Windows jest to proces podmiany procedury obsługi zdarzeń subklasowanego okna, hookowanie (hook przechwytywanie, podsłuchiwanie, dosł. zakładanie haka) mechanizm systemu Windows, pozwalający programiście na podsłuchiwanie części wywołań funkcji systemowych. Pozwala na podsłuchiwanie m.in. komunikatów pomiędzy oknami, aktywności urządzeń wejścia takich jak mysz czy klawiatura. W pracy zastosowany do przechwycenia wywołania funkcji CreateWindowEx co pozwala na subclassing tworzonego okna tuż przed jego faktycznym utworzeniem. Kod procedury filtrującej dla założonego haka przedstawia listing 4.16. Proces subclassingu tworzonego okna odbywa się wewnątrz funkcji filtrującej wywołania metody CreateWindowEx. Listing 4.16: Kod funkcji filtru procedury przechwytującej :: LRESULT uixcreatewindowhook :: Filter (:: INT code, :: WPARAM wparam, :: LPARAM lparam ) { if ( code!= HCBT_ CREATEWND ) { goto Success ; 57
} // Argumenty funkcji CreateWindowEx CREATESTRUCT * cs = (( CBT_CREATEWND *) lparam ) -> lpcs ; uixassert ( cs!= NULLPTR ); // Aktualny stan wą tku uixthreadstate & ts = uixthreadstate :: Get (); // Wska ź nik do tworzonego okna uixwindow * window = ts. m_createwindowhook_window ; // Jeśli jest dost ę pne if ( window ) { // Subclassing przekazanego uchwytu okna uixassert ( wparam!= 0); window -> Subclass (( HWND ) wparam ); } // Zresetowanie wska ź nika hookowanego okna ts. m_createwindowhook_window = NULLPTR ; Success : // Kontynuacja łań cucha hak ów return CallNextHookEx ( uixthreadstate :: Get (). m_ CreateWindowHook_ Handle, code, wparam, lparam ); } Zaimplementowany mechanizm subclassingu w połączeniu z przechwytywaniem wywoływań funkcji CreateWindowEx pozwolił na rozszerzanie działania takich elementów interfejsu graficznego systemu Windows, do których normalnie programista nie ma dostępu. Przykładowo, możliwy jest subclassing okien dialogowych tworzonych przez funkcję MessageBox. 4.8.4 Mapy zdarzeń Architektura biblioteki UIX pozwala na implementację praktycznie dowolnej metody obsługi komunikatów. Jednym z założeń tworzonej biblioteki była jej transpa- 58
rentność dla rozwiązań stosowanych przez programistę. Zaimplementowane mechanizmy z założenia mają za zadanie wspomagać programistę w pisaniu kodu nie narzucając żadnej organizacji kodu obsługi komunikatów. Autor pracy pozostawił pewnego rodzaju furtkę w mechanizmie obsługi komunikatów. Obecna implementacja opiera się na metodzie OnWindowMessage do której trafiają przetwarzane komunikaty z pętli obsługi komunikatów. Ze względów optymalizacyjnych najlepszym rozwinięciem tego rozwiązania jest ogromna instrukcja switch... case zawierająca wywołania metod obsługi komunikatów. Aby ułatwić oraz zautomatyzować obsługę zdarzeń, do biblioteki dodany został zestaw makr tworzących tzw. mapy zdarzeń. Idea mapy zdarzeń pozwala na skojarzenie ze sobą szczegółowych informacji o zachodzącym zdarzeniu (kod komunikatu, zakresy identyfikatorów itp.) z metodą jego obsługi. Poniżej przedstawiono najważniejsze z nich: uixdeclare EVENT MAP deklaruje mapę zdarzeń. Wymaga późniejszego użycia uixbegin EVENT MAP oraz uixend EVENT MAP, uixbegin EVENT MAP(Class, Base) rozpoczyna definicję mapy zdarzeń. Argument Class określa klasę w której mapa została zdeklarowana, zaś argument Base określa klasę bazową, do której mechanizm przetwarzania komunikatów ma przejść w przypadku braku deklaracji obsługi zdarzenia. Musi być użyte w pliku cpp, uixend EVENT MAP kończy definicję mapy zdarzeń. Podobnie jak uixbegin EVENT MAP, musi być użyte w pliku cpp, uixevt * zestaw makr obsługujących wszystkie możliwe zdarzenia dla wszystkich zaimplementowanych kontrolek. Pozwalają na rozszerzanie istniejących już kontrolek interfejsu graficznego, uixevt REFLECT * zestaw makr obsługujących zdarzenia zreflektowane przez kontrolkę rodzica z powrotem do nadawcy, uixevt COMMAND* makra obsługujące komendy interfejsu użytkownika, 59
uixevt NOTIFY makro deklarujące obsługę komunikatu notyfikacji okna rodzica. 4.8.5 Mechanizm refleksji komunikatów Standardowe elementy interfejsu graficznego w systemie Windows stworzone są tak, aby ułatwić programiście tworzenie aplikacji w jak najprostszy sposób. Programista, pisząc prostą aplikację, nie musi korzystać ze wszystkich mechanizmów dostarczonych wraz z API może ograniczyć się do prostego modelu z scentralizowaną obsługą komunikatów. Takie podejście nie tylko ogranicza i upraszcza korzystanie z API, ale także nie wymaga obiektowości. Wiele kontrolek UI takich jak chociażby przyciski, wysyła dodatkowe komunikaty informujące okno nadrzędne o pewnych zdarzeniach, takich jak: notyfikacje (WM NOTIFY) ogólny mechanizm przesyłania komunikatów do okna rodzica. Komunikaty te zdefiniowane są dla każdej kontrolki niezależnie. Ten rodzaj komunikatów obsługuje mechanizm Owner Draw, w którym okno rodzica odrysowuje okno potomne, komunikaty kontenerów (WM DRAWITEM, WM COMPAREITEM, WM DELETEITEM, WM MEASUREITEM, WM CHARTOITEM, WM VKEYTOITEM) stanowią zestaw komunikatów, które pozwalają na bardziej zaawansowaną obsługę standardowych kontrolek kontenerów, np. ListBox, kolory kontrolek (WM CTLCOLOR*) Pozwalają na zdefiniowanie niestandardowego koloru danej kontrolki. Rodzina tych komunikatów wywodzi się z 16-bitowego API systemu Windows. Odradzane jest korzystanie z tego mechanizmu, ze względu na jego ograniczenia 8. Obecnie zaleca się korzystanie z bardziej zaawansowanych mechanizmów takich jak Owner Draw czy też Subclassing. 8 Działa tylko gdy w systemie wyłączony jest manager kompozycji lub gdy aplikacja nie posiada manifestu Common Controls 60
Wymienione powyżej zestawy komunikatów łamią podstawową zasadę programowania obiektowego, jaką jest hermetyzacja szczegóły implementacji danej kontrolki muszą być znane poza nią. Aby dostosować architekturę przetwarzania komunikatów w aplikacjach systemu Windows, do biblioteki UIX wprowadzony został mechanizm refleksji komunikatów. Działa on bez udziału programisty oraz pozostaje transparentny dla tworzonej aplikacji. Ideą jego działania jest odsyłanie komunikatów od okna dziecka z powrotem do nadawcy zachowując zasadę hermetyzacji. Dzięki temu mechanizmowi możliwa jest implementacja niestandardowych kontrolek, które nie muszą oddelegowywać specyficznych metod i mechanizmów związanych z danym komunikatem. Schematyczne działanie tego mechanizmu przedstawiono na rysunku 4.5 Rysunek 4.5: Diagram sekwencji mechanizmu refleksji komunikatów 4.8.6 Hierarchia zdarzeń W celu ujednolicenia mechanizmu przekazywania argumentów dla zdarzeń, wprowadzona została hierarchia struktur argumentów zdarzeń. Struktury należące do tej 61
hierarchii mają za zadanie zdekodowanie argumentów komunikatu do postaci właściwych danych. Listing 4.17: Przykładowa struktura argumentów zdarzenia /// Stan aktywacji okna enum uixactivatestate { /// Aktywne uixactivatestate_active = WA_ACTIVE, /// Aktywowane przez klikni ę cie uixactivatestate_clickactive = WA_CLICKACTIVE, /// Nieaktywne uixactivatestate_inactive = WA_INACTIVE, }; /// Struktura argument ó w aktywacji okna struct uixactivateeventargs : uixeventargs { uixactivateeventargs ( WPARAM w, LPARAM l) : uixeventargs ( w, l) { State = ( uixactivatestate ) LOWORD (w); Minimized = ( BOOL ) HIWORD (w); PreviousWindow = ( HWND ) l; } }; uixactivatestate State ; //! < Aktualny stan okna BOOL Minimized ; //! < Czy zminimalizowane HWND PreviousWindow ; //! < Uchwyt do poprzedniego okna Do przekazywania argumentów komunikatu system Windows wykorzystuje dwie wartości typu WPARAM i LPARAM. Ich znaczenie różni się w zależności od przesyłanego komunikatu. W wypadku, gdy komunikat przesyła więcej argumentów, pakowane są one do struktury przekazywany jest jedynie wskaźnik do niej. Wszystkie struktury argumentów dziedziczą po bazowej klasie uixeventargs. Umożliwia ona dostęp do surowych wartości argumentów WPARAM i LPARAM. Zadaniem zaś każdej ze struktur dziedziczących po niej jest odpowiednie zdekodowanie argumentów zdarzenia. 62
Wprowadzenie takiej struktury zunifikowało mechanizm przesyłania komunikatów. Programista nie musi pamiętać kolejności ich występowania ani tym bardziej znaczenia argumentów WPARAM i LPARAM dla danego komunikatu. Nazwy struktur argumentów zostały dobrane tak, aby programista już pisząc nazwę metody wiedział jakiego typu argumenty ona przyjmuje. Przykładowo: dla zdarzenia OnInitDialog struktura argumentów nazywa się uixinitdialogeventargs, dla zdarzenia OnClose struktura argumentów nazywa się uixcloseeventargs. Przekazanie argumentów do obiektu argumentów zdarzenia odbywa się w implementacji makr mapy zdarzeń. Listing 4.18 przedstawia częściowe rozwinięcie przykładowego makra obsługi zdarzenia OnActivate 9 Listing 4.18: Makro obsługi komunikatu WM ACTIVATE # define uixevt_ ACTIVATE () \ if ( message == WM_ ACTIVATE ) { \ uixactivateeventargs e( wparam, lparam ); \ ThisClass :: OnActivate (e); \ * result = 0; \ return TRUE ; \ } 4.8.7 Zdarzenia globalne komendy We frameworku UIX obsługa komend jest tranzytywna nieobsłużone komunikaty WM COMMAND przechodzą wyżej w hierarchii obsługi zdarzeń, aż do obiektu aplikacji (wspomniana klasa uixapplication). Możliwe jest zdefiniowanie globalnej mapy zdarzeń aplikacji, w której obsługiwane byłyby komendy globalne dla całej aplikacji. W przeciwieństwie do mechanizmu refleksji komunikatów, komendy nieobsłużone w oknie nadrzędnym wędrowałyby dalej w hierarchii obiektów aż do obiektu aplikacji. Rysunek 4.6 przedstawia mechanizm obsługi komend globalnych. 9 Należy zaznaczyć, że symbole message, result, wparam, lparam oraz ThisClass definiowane są w makrze uixbegin EVENT MAP. 63
Rysunek 4.6: Refleksja komend 4.8.8 Obsługa zdarzeń Aby dane zdarzenie zostało poprawnie obsłużone, musi ono posiadać odpowiedni wpis w mapie zdarzeń oraz odpowiednią metodę jego obsługi. Metody obsługi nie zwracają żadnych wartości oraz posiadają jeden argument w postaci struktury z argumentami zdarzenia. Praktycznie dla każdego zdarzenia występuje osobna struktura definiująca argumenty, jakie to zdarzenie obsługuje oraz zwracane wartości. Wszystkie struktury argumentów zdarzeń wywodzą się z struktury uixeventargs. W praktyce wiele ze zdarzeń nie posiada argumentów, oraz istnieją grupy zdarzeń, które posiadają identyczne argumenty. Przekazywanie zdarzeń odbywa się poprzez wywoływanie metod klasy nadrzędnej (ze względu na optymalizację w postaci niestosowania metod wirtualnych. W przypadku, gdy kod metody obsługi zdarzenia chce je odrzucić nie wywołuje metody klasy nadrzędnej. 64
Rozdział 5 Dokumentacja, testy, przykłady Rozdział ten przedstawia przykładowe aplikacje z GUI, wykorzystujące bibliotekę UIX. Aplikacje te prezentują jedynie interfejs użytkownika, nie posiadając tym samym żadnej funkcjonalności. Założeniem tego rozdziału jest jedynie prezentacja możliwości, jakie autor pracy osiągnął poprzez stworzenie biblioteki UIX wraz z możliwościami rozszerzania jej o dodatkowe moduły. Kody źródłowe wszystkich zaprezentowanych w tym rozdziale aplikacji i komponentów znajdują się na nośniku dołączonym do niniejszej pracy. Szczegółowe omówienie przykładowych aplikacji znajduje się w dokumentacji technicznej projektu. 5.1 Dokumentacja Dokumentacja biblioteki wygenerowana została przy użyciu darmowego narzędzia jakim jest Doxygen. Generuje ono dokumentację w postaci plików HTML, dokumentu PDF lub też w postaci skompilowanych plików pomocy. Dokumentacja generowana jest bezpośrednio z komentarzy opisujących dany fragment kodu. Dokumentacja ta zawiera także krótkie wprowadzenie do programowania w bibliotece UIX, wraz z opisem przedstawionych w niniejszym rozdziale aplikacji. 65
Rysunek 5.1: Dokumentacja biblioteki Indeks klas 5.2 Prezentacja zaimplementowanych kontrolek Autor pracy stworzył przykładową aplikację prezentującą zaimplementowane kontrolki w działaniu. Pokazuje ona możliwości jakie udostępniają zaimplementowane kontrolki GUI implementację aplikacji korzystającej z zasobów. Należy zaznaczyć, iż praktycznie rzadko spotyka się aplikacje, które wykorzystują wszystkie możliwe kontrolki interfejsu graficznego. Aplikacja składa się z typowych elementów: menu głównego, paska narzędziowego zawierającego te same pozycje co menu główne, paska stanu wyświetlającego użytkownikowi aktualny stan aplikacji oraz podpowiedzi, kontrolki z zakładkami zawierającej karty z pogrupowanymi kontrolkami interfejsu graficznego: karta Controls zawierająca kontrolki ogólnego przeznaczenia, 66
karta Drawing zawierająca proste kontrolki do rysowania obramowań oraz wyświetlania bitmap, karta Views prezentująca kontrolki różnych widoków elementów oraz pasków. Rysunek 5.2: Interfejs aplikacji z kartą Controls 67
Rysunek 5.3: Karta Drawing Rysunek 5.4: Karta Views 5.3 Przykładowa aplikacja klon programu notatnik Kolejna aplikacja stworzona w bibliotece UIX to klon programu Notatnik, nazwany 68
++Notepad. Wykorzystuje on do edycji tekstu kontrolkę Scintilla 1. Aplikacja ta pokazuje, w jaki sposób można szybko osiągnąć prostą i działającą aplikację. Rysunek 5.5 prezentuje graficzny interfejs użytkownika aplikacji. Rysunek 5.5: Interfejs programu ++Notepad 1 Strona domowa kontrolki: http://www.scintilla.org/ 69
5.4 Kontrolka niestandardowy panel Celem tego przykładu jest prezentacja możliwości tworzenia nowych kontrolek interfejsu graficznego w bibliotece UIX. Jako przykład zaimplementowano panel z niestandardowym obramowaniem i belką tytułową. Kontrolka ta została dołączona do biblioteki uixext10.dll. Rysunek 5.6 przedstawia okno główne zawierające kilka takich zagnieżdżonych paneli. Rysunek 5.6: Niestandardowy panel Tworzenie kontrolek w bibliotece UIX polega na tworzeniu zupełnie nowych dzięki zastosowaniu klasy uixwindow bądź też rozszerzaniu istniejących już kontrolek. 70
5.5 Kontrolka okno typu Splash Screen Splash screen (inaczej ekran powitalny) jest oknem wyświetlającym na ekranie obraz wraz z logiem aplikacji podczas uruchamiania bądź jej ładowania. Zaimplementowana klasa została dołączona do biblioteki uixext10.dll. Rysunek 5.7 przedstawia przykładowe okno tego typu. Rysunek 5.7: Przykładowe okno splash screen 5.6 Tworzenie prostej aplikacji Ostatni przykład pokazuje, jak w najprostszy sposób stworzyć aplikację korzystającą z biblioteki UIX. Przedstawiona poniżej prosta aplikacja pokazuje, jak w prosty sposób można rozszerzać implementację klasy uixframe oraz jak korzystać z obiektu aplikacji. Zadaniem tej aplikacji jest pokazanie użytkownikowi dwóch przycisków, odpowiadających za: pokazanie prostego komunikatu tekstowego oraz pokazanie okna dialogowego O programie (ang. About Dialog). 5.6.1 Klasa aplikacji Zadaniem klasy aplikacji jest inicjalizacja interfejsu użytkownika. Listing 5.1 pokazuje kod klasy aplikacji, która inicjalizuje onkno główne przykładowej aplikacji. Jej zadaniem jest stworzenie obiektu ramki głównej aplikacji i pokazaniem jej przed przejściem do głównej pętli komunikatów. 71
Listing 5.1: Klasa aplikacji class CApplication : public uixapplication { public : // Inicjalizacja instancji aplikacji virtual BOOL InitInstance () override { if (! uixapplication :: InitInstance ()) return FALSE ; SetMainWindow (& m_ MainFrame ); // Ustawienie okna g ł ó wnego m_ MainFrame. CreateFrame (); // Stworzenie ramki m_ MainFrame. Show (); // Pokazanie jej m_mainframe. Update (); // i odświe ż enie return TRUE ; } private : CMainFrame m_ MainFrame ; }; Za właściwe uruchomienie aplikacji odpowiada makro uiximplement APPLICATION(Application), które uruchamia mechanizm inicjalizacji modułu aplikacji. 5.6.2 Klasa ramki głównej Przykład ten pokazuje, w jaki sposób należy tworzyć nowe kontrolki oraz dodawać metody obsługi komend do klasy ramki głównej aplikacji. Pokazuje także typowy przykład użycia klasy uixaboutdlg w celu pokazania informacji o programie. Kod źródłowy implementacja klasy głównej ramki aplikacji przedstawiona została na listingu 5.2, zaś wygląd interfejsu przykładowej aplikacji przedstawiony jest na rysunku 5.8. Listing 5.2: Klasa ramki głównej // Klasa ramki głó wnej okna class CMainFrame : public uixframe { uixdeclare_ EVENT_ MAP () public : 72
// Deklaracja identyfikator ó w komend enum Command { Command_ Default = uixid_ LAST, // Standardowy przycisk Command_ About, // Okno About }; // Obs ł uga zdarzenia OnCreate - po utworzeniu okna aplikacji void OnCreate ( uixcreateeventargs & e) { uixframe :: OnCreate (e); // Sprawdzenie poprawnego utworzenia podstawy okna if (e. Result == -1) return ; } // Ustawienie tekstu belki tytu ł owej this -> SetText (_(" Example application ")); // Utworzenie przycisku " Click Me" m_button. Create (this, _("& Click Me"), uixpoint (7, 7), uixsize (75, 23), Command_ Default ); // Utworzenie przycisku " About " m_about. Create (this, _("& About "), uixpoint (7, 37), uixsize (75, 23), Command_About ); // Metoda obs ł ugi komendy Command_ Default void OnButtonClick ( uixcommandeventargs & e) { MessageBox ( GetHandle (), _(" You clicked Button "), _(" Information "), MB_ OK MB_ ICONINFORMATION ); } // Metoda obs ł ugi komendy Command_ About void OnAboutClick ( uixcommandeventargs & e) { // Utworzenie okna dialogowego " O programie " uixaboutdlg d; d. Create ( this ); d. SetAppName (_(" Example application ")); d. DoModal (); // Pokazanie go w trybie modalnym } private : uixbutton m_ Button ; 73
}; uixbutton m_ About ; // Deklaracja mapy zdarze ń dla ramki g ł ó wnej aplikacji // wraz z odpowiednimi deklaracjami obs ł ugi zdarze ń uixbegin_ EVENT_ MAP ( CMainFrame, uixframe ) uixevt_ CREATE () uixevt_ COMMAND ( CMainFrame :: Command_ Default, OnButtonClick ) uixevt_ COMMAND ( CMainFrame :: Command_ About, OnAboutClick ) uixend_ EVENT_ MAP () Rysunek 5.8: Przykładowe okno splash screen 74
Rozdział 6 Podsumowanie rezultatów Rozdział ten przedstawia wyniki prac nad biblioteką UIX oraz stanowi próbę ukierunkowania jej dalszego rozwoju, poprzez określenie możliwych zmian. Autor pracy także próbuje porównać ją do istniejących już rozwiązań, oraz pokazać zalety przyjętych na początku założeń. Poprzez stworzenie zaprezentowanej w niniejszej pracy biblioteki GUI oraz samej pracy, autor wykorzystał i poszerzył wiedzę oraz umiejętności z zakresu tworzenia aplikacji z graficznym interfejsem programistycznym. Stworzenie całego systemu (jakim niewątpliwie jest biblioteka programistyczna) wymagało od autora odpowiedniej wiedzy z zakresu programowania zarówno aplikacji GUI, ale i także znajomości wielu wewnętrznych mechanizmów systemu operacyjnego bez których niemożliwe byłoby stworzenie wydajnej, bezpiecznej, wspierającej wielowątkowość a co najważniejsze przezroczystej dla programisty biblioteki. Autor poprzez niniejszą pracę wykazał dogłębną znajomość języka C++ oraz metodologii i paradygmatów programowania, popartą wieloletnim doświadczeniem programisty. 6.1 Charakterystyka biblioteki Stworzona biblioteka w jak najmniejszym stopniu ingeruje w kod aplikacji programisty definiuje jak najmniej symboli preprocesora, cały kod zaś znajduje się wewnątrz przestrzeni nazw UIX. Biblioteka ta jest całkowicie przezroczysta dla in- 75
nych bibliotek gdyż nie ingeruje w mechanizmy kluczowe dla działania aplikacji oraz odpowiada tylko i wyłącznie za graficzny interfejs użytkownika. Rozmiar plików wykonywalnych biblioteki dzięki ukierunkowaniu się na obsługę GUI został zminimalizowany w jak największym stopniu. Główny plik biblioteki Uix10.dll w kompilacji wydaniowej zajmuje niewiele ponad 200 KiB. Co ważne, dzięki zastosowaniu metod inline, wywołania wielu metod obiektów zostały rozwinięte w trakcie kompilacji powodując zmniejszenie pliku wykonywalnego biblioteki DLL. Ważną z punktu widzenia cechą powstałej biblioteki jest jej dokumentacja. Wygenerowana została ona na podstawie komentarzy opisujących typy oraz metody klas stanowiąc źródło pomocy dla programisty aplikacji. Dokumentacja wygenerowana została do postaci skompilowanego pliku pomocy HTML 1. Zestaw zaimplementowanych elementów GUI obejmuje takie kontrolki jak: Pola tekstowe klasy uixcombobox, uixdatetimectrl, uixhotkeyctrl, uixipaddress, uixlabel, uixlinkctrl, uixtextctrl, Przyciski klasy uixbutton, uixcheckbox, uixcommandlink, uixradiobutton, uixupdownctrl, Okna klasy uixframe, uixdialog, uixcolordialog, uixfiledialog, uixfontdialog, Widoki klasy uixlistboxctrl, uixlistviewctrl, uixtreeviewctrl, Paski klasy uixprogressbarctrl, uixrebarctrl, uixscrollbar, uixstatusbarctrl, uixtoolbar, uixtrackbarctrl, Inne klasy uixanimationctrl, uixgrouppane, uixheaderctrl, uixmonthcalendarctrl, uixpagescrollerctrl, uixtabctrl, uixtooltip, 1 pliki o rozszerzeniu *.chm 76
6.2 Bezpieczeństwo W przypadku bibliotek graficznego interfejsu użytkownika, ważne jest aby sama biblioteka pozostawała transparentna dla reszty aplikacji. Oznacza to tyle, że powinna być ona dokładnie przetestowana na długo przed wykorzystywaniem jej w rozbudowanych aplikacjach. Pomimo tego, że framework UIX przeznaczony jest do tworzenia prostych aplikacji, autor musiał zapewnić pewien minimalny poziom bezpieczeństwa. Aby mieć pewność, że kod biblioteki działa poprawnie, autor pracy przyjął metodologię programowania kontraktowego zapewniającą możliwość sprawdzania wszelkich warunków i zależności już w trakcie tworzenia aplikacji. Zwiększony został także główny poziom generowania ostrzeżeń kompilatora z poziomu 3 (Level 3 ) na najwyższy możliwy - 4 (Level 4 ). 6.2.1 Asercje Podstawowym narzędziem projektowania kontraktowego w języku C++ są asercje[13, str. 391]. Ich zadaniem jest zapewnienie poprawności wykonania kodu poprzez sprawdzanie odpowiednich predykatów przed i po jego wykonaniu gwarantując jego poprawność. Mechanizm ten wykorzystywany jest jedynie w kompilacjach debug, ze względu na jego nadmiarowość oraz narzut czasu wykonania. W przypadku niespełnienia warunku asercji, debugger automatycznie wstrzymuję pracę programu. Aby dodatkowo zapewnić sprawdzanie kluczowych warunków dla działania biblioteki oraz aplikacji wprowadzone zostały także zapewnienia (ang. ensure), rozszerzające mechanizm asercji o kompilacje release. W przeciwieństwie do asercji, zapewnienia zgłaszają wyjątek uixinvalidargumentexception dając aplikacji możliwość reakcji. W chwili pisania pracy, w kodzie biblioteki użyto ponad 2900 asercji. Liczba ta wydaje się być niewielka, jednak w porównaniu do ogólnej liczby linii kodu równej około 27 000 linii. Oznacza to, że na 100 linii kodu wypada około 11 asercji. 77
6.2.2 Statyczna analiza kodu źródłowego Dodatkowo autor pracy użył narzędzia pozwalającego na statyczną analizę kodu źródłowego. Rezultaty analizy potwierdziły fakt, że kod został napisany poprawnie. W połączeniu z prostotą napisanego kodu oraz maksymalnym wykorzystaniem podstawowych mechanizmów języka C++, może to stanowić istotny dowód na wysoką jakość stworzonego kodu. 6.2.3 Testy jednostkowe Charakterystyczną cechą bibliotek tworzenia interfejsu graficznego jest istotna trudność w przeprowadzaniu testów jednostkowych. Istotą graficznego interfejsu użytkownika jest interakcja z użytkownikiem aplikacji którą trudno jest wyrazić w postaci testów jednostkowych. Implementacja takich testów możliwa byłaby poprzez symulowanie prawdziwej interakcji z użytkownikiem, co nie stanowi istoty niniejszej pracy. Dlatego autor pracy postanowił zastosować metodologię programowania kontraktowego wszelkie nieprawidłowości w działaniu kodu frameworku objawiłyby się właśnie w trakcie interakcji z użytkownikiem. 6.3 Możliwości rozwoju Pomimo ogromu pracy, jaką autor włożył w implementację biblioteki UIX nie można uznać, że stworzona biblioteka programistyczna może konkurować pod względem funkcjonalności z kompleksowymi rozwiązaniami przedstawionymi w drugim rozdziale niniejszej pracy. W tej części rozdziału, autor próbuje pokazać przyszły kierunek rozwoju biblioteki. 6.3.1 Aplikacje MDI oraz TDI W chwili pisania niniejszej pracy, framework nie pozwala na pisanie aplikacji z interfejsem typu MDI ani TDI. Spowodowane jest to brakiem implementacji zarządzania dokumentami i widokami oraz poziomem skomplikowania takich aplikacji. Stanowi to jednak przyszły kierunek rozwoju frameworku, który pozwalałby na pisanie aplikacji obsługujących wiele dokumentów w postaci kart (TDI ) czy też ramek 78
wewnątrz głównej ramki aplikacji (MDI ). 6.3.2 Implementacja architektury dokument / widok Biblioteka UIX nie wspiera programowania aplikacji korzystających z architektury dokument/widok. Implementacja tej funkcjonalności nie stanowiła istoty niniejszej pracy. Stworzenie takiego mechanizmu wymagałaby rozszerzenia istniejącej obecnie hierarchii o dodatkowe klasy, które implementowałyby działającą architekturę dokument/widok. 6.3.3 Zakładki właściwości Zakładki właściwości (ang. property sheets) są mechanizmem systemu Windows pozwalającym na tworzenie okien dialogowych, które potrafią wyświetlać tak zwane strony właściwości (ang. property page). Pozwalają one także na tworzenie różnego rodzaju kreatorów poprzez zdefiniowanie wyświetlanych kolejno stron oraz logiki przejść pomiędzy nimi. 6.3.4 Rozszerzenie istniejących kontrolek o style renderowania Wszystkie zaimplementowane kontrolki wykorzystują natywne renderowanie ich przez system operacyjny. Dzięki zaimplementowaniu w frameworku mechanizmu subclassingu, możliwe jest zaimplementowanie nowego mechanizmu renderowania. Mechanizm ten pozwalałby na dynamiczną zmianę wyglądu aplikacji, oraz uniezależnienie jej od aktualnych ustawień systemu operacyjnego. 6.3.5 Automatyczne układy kontrolek Jedną z wad obecnej implementacji biblioteki jest brak mechanizmu automatycznego rozmieszczania kontrolek w trakcie zmiany rozmiaru okna rodzica. Programista musi ręcznie obliczać pozycję i rozmiary kontrolek potomnych oraz ręcznie je aktualizować. Rozwiązaniem tego problemu byłoby stworzenie mechanizmu układów (ang. 79
layout), które w zależności od ustawień właściwości, odpowiadałyby za automatyzację zmiany rozmiaru elementów okien. 6.3.6 Mechanizm właściwości Główną ideą właściwości jest wywoływanie metod ustawiania i pobierania właściwości (tzw. setterów i getterów) poprzez implementację operatorów przypisania (operator = (const TypŹródłowy&)) i rzutowania (operator TypDocelowy ()). Implementacja takiego mechanizmu w języku C++ może odbywać się jako rozszerzenie języka bądź też wykorzystanie szablonów. Pomimo faktu iż język C++ nie wspiera właściwości w rozumowaniu czysto obiektowym (chociażby takich jak w językach C# czy też Delphi), możliwa jest implementacja takiego rozwiązania która byłaby zgodna ze standardem języka. Niestety w trakcie wczesnych prac nad prototypami rozwiązań zastosowanych w bibliotece okazało się, że aktualnie dostępne kompilatory języka C++ nie zawsze potrafiły skompilować kod szablonu, który przyjmował wskaźniki do metod jako parametry szablonu. Listing 6.1: Problematyczna deklaracja właściwości class MyClass { // Akcesory właś ciwo ści void set_delay ( int value ); int get_ Delay () const ; }; // Właś ciwo ści uixpropertybyvalue < MyClass, // klasa bazowa int, // typ właś ciwo ści & get_ Delay, // adres metody gettera & set_ Delay, // adres metody settera uixpropertyaccess_readwrite // odczyt / zapis > Delay ; // Właś ciwo ść " Delay " ( dost ęp poprzez warto ść) Problem ten dotyczył zwłaszcza kompilatora C++ od Microsoftu, zarówno z pakietów Visual C++ 2008 jak i Visual C++ 2010. Ze względu na fakt, że biblioteka 80
rozwijana była właśnie w IDE Microsoftu, mechanizm ten nie znalazł się w bibliotece UIX. 6.3.7 Dodatkowe kontrolki biblioteka UixExt W trakcie implementowania kolejnych kontrolek interfejsu użytkownika, autor rozpoczął prace nad modułem rozszerzającym bibliotekę o dodatkowe kontrolki, niedostępne standardowo w systemie Windows. Ze względu na strukturę, kontrolki można podzielić na trzy grupy: nowe Są to kontrolki stworzone całkowicie od podstaw. Nie korzystają one z innych kontrolek (chociażby takich jak paski narzędzi) implementują całkowicie nowe elementy interfejsu graficznego, rozszerzające Kontrolki te najczęściej zmieniają zachowanie lub metodę ich renderowania rozszerzają istniejące już kontrolki o nowe funkcjonalności, standardowo nieudostępniane przez system operacyjny, złożone Kontrolki te wykorzystują inne kontrolki do zapewnienia założonych funkcjonalności. Hermetyzują one wewnętrzne przetwarzanie komunikatów, nie pozwalając na ingerencję z zewnątrz. Dobrym przykładem może być kontrolka typu PropertyGrid, która składa się z kilku innych, oraz obsługuje własne komendy i zdarzenia, W trakcie tworzenia przykładowych aplikacji, autorowi pracy zaczęło brakować kilku zestawów kontrolek, które w znacznym stopniu upraszczają tworzenie zaawansowanych aplikacji. Poniżej przedstawiono ich listę: Property Grid Zadaniem tej kontrolki jest stworzenie dynamicznego interfejsu zmiany właściwości obiektów. Kontrolka wyświetla pogrupowane w kategorie właściwości, które można edytować w trakcie działania aplikacji. Umożliwia ona dodawanie własnych typów właściwości, wraz z odpowiadającymi im edytorami, 81
Rysunek 6.1: Kontrolka Property Grid aplikacji Microsoft Visual Studio Kontrolki powłoki Są to kontrolki pozwalające na przeglądanie struktury katalogów na komputerze. Kontrolki te rozszerzają takie elementy interfejsu graficznego jak pola rozwijane, widoki drzew czy też widoki list. Rysunek 6.2 przedstawia okno programu Microsoft Windows Explorer wraz z kontrolkami powłoki, Ribbon UI Wstążka 2 (ang. Ribbon, Microsoft Fluent UI ) jest elementem interfejsu użytkownika zastępującym tradycyjne menu i paski narzędzi. Ideą wstążki było stworzenie funkcjonalnej części interfejsu użytkownika, która umieściłaby wszystkie funkcje oprogramowania w jednym miejscu, czyniąc je łatwymi do znalezienia i użytkowania. Rysunek 6.3 przedstawia kontrolkę wstążki w interfejsie programu Microsoft Paint dostarczonego wraz z systemem Windows 7, Zaawansowany edytor kodu Framework UIX implementuje dwa rodzaje pól tekstowych: w postaci zwy- 2 Za: Wstążka (komputery) Wikipedia, http://pl.wikipedia.org/wiki/wst%c4%85%c5% BCka_%28komputery%29, ostatni dostęp: 25.11.2011 82
Rysunek 6.2: Kontrolki powłoki w programie Microsoft Windows Explorer Rysunek 6.3: Kontrolka Ribbon w programie Microsoft Paint kłych edytorów tekstowych oraz bogatych edytorów tekstowych (ang. rich edit). Istnieje także trzeci rodzaj wyspecjalizowanych edytorów tzw. edytorów kodu. Zaimplementowanie takiej kontrolki od podstaw byłoby niezwykle trudnym zadaniem (jeśliby wziąć możliwości takich kontrolek). Bibliotekę UIX można by łatwo rozszerzyć o obsługę darmowej kontrolki Scintilla, dostępnej 83