LEKCJA 40: STRUKTURA PROGRAMU PROCEDURALNO - ZDARZENIOWEGO PRZEZNACZONEGO DLA WINDOWS. ________________________________________________________________ W trakcie tej lekcji poznasz ogólną budowę interfejsu API Windows i dowiesz się, co z tego wynika dla nas - autorów programów przeznaczonych dla Windows. ________________________________________________________________ W przeciwieństwie do długich liniowych programów przeznaczonych dla DOS, w naszych programach dla Windows będziemy pisać coś na kształt krótkich odcinków programu i przekazywać sterowanie Windows. Jest to bardzo ważna cecha - kod programu jest zwykle silnie związany z Windows w taki sposób, że użytkownik może w dużym stopniu decydować o sposobie (kolejności) wykonywania programu. Praktycznie robi to poprzez wybór opcji-klawiszy w dowolnej kolejności. Przy takiej filozofii w dowolnym momencie powinniśmy mieć możliwość przełączenia się do innego programu (innego okna) i nasz program powinien (bez zauważalnej zwłoki) przekazać sterowanie, nie zagarniając i nie marnując czasu CPU. Z tego powodu kod programu powinien być bardzo "zmodularyzowany". Każda sekcja kodu powinna być odseparowana i każda, po wykonaniu powinna przekazywać sterowanie do Windows. NOTACJA WĘGIERSKA I NOWE TYPY DANYCH. Tworzenie zdarzeniowych programów dla Windows wymaga kilku wstępnych uwag na temat nowych typów danych. Okienkowe typy są definiowane w plikach nagłówkowych (WINDOWS.H, WINDOWSX.H, OWL.H itp) i mają postać najczęściej struktury, bądź klasy. Typowe sposoby deklaracji w programach okienkowych są następujące: HWND hWnd - WiNDow Handle - identyfikator okna HWND hWnd - typ (predefiniowany), hWnd - zmienna HINSTANCE hInstance - Instance Handle - identyfikator danego wystąpienia (uruchomienia) programu PAINTSTRUCT - struktura graficzna typu PAINTSTRUCT ps - nasza robocza struktura (zmienna) WNDCLASS - struktura (a nie klasa wbrew mylącej nazwie) POINT - struktura (współrzędne punktu - piksela na ekranie) RECT - struktura (współrzędne prostokąta) BOOL - typ signed int wykorzystywany jako flaga (TRUE/FALSE) WORD - unsigned int DWORD - unsigned long int LONG - long int HANDLE, HWND, HINSTANCE - unsigned int (jako nr - identyfikator) UINT - j. w. - unsigned int. W programach okienkowych stosuje się wiele predefiniowanych stałych, których znaczenie sugeruje przedrostek i nazwa, np: WM_CREATE - Windows Message: Create! - Komunikat Windows: Utworzyć! (np. okno) WS_VISIBLE - Window Style: Visible - Styl Okna: Widoczne ID_... - IDentifier - IDentyfikator MB_... - Message Box - elementy okienka komunikatów W środowisku Windows stosuje się specjalną notację nazwaną od narodowości swojego wynalazcy Karoja Szimoni - notacją węgierską. Sens notacji węgierskiej polega na dodaniu do nazwy zmiennej określonych liter jako przedrostka (prefix). Litery-przedrostki stosowane w notacji węgierskiej zebrano w Tabeli poniżej. Pomiędzy nazewnictwem Microsofta a Borlanda istnieją wprawdzie drobne rozbieżności, ale ogólne zasady można odnieść zarówno do BORLAND C++ 3+...4+, jak i Microsoft C++ 6...7, czy Visual C++. Notacja węgierska ________________________________________________________________ Prefix Skrót ang. Znaczenie ________________________________________________________________ a array tablica b bool zmienna logiczna (0 lub 1) by unsigned char znak (bajt) c char znak cb count of bytes liczba bajtów cr color reference value określenie koloru cx, cy short (count x, y len.) x-ilość, y-długość (short) dw unsigned long liczba długa bez znaku double word podwójne słowo fn function funkcja pfn pointer to function wsk. do funkcji h handle "uchwyt" - identyfikator i integer całkowity id identifier identyfikator n short or int krótki lub całkowity np near pointer wskaźnik bliski p pointer wskaźnik l long długi lp long pointer wskaźnik typu long int lpfn l. p. to function daleki wskaźn. do funkcji s string łańcuch znaków sz string terminated '\0' łańcuch ASCIIZ tm text metric miara tekstowa w unsigned int (word) słowo x,y short x,y coordinate współrzędne x,y (typ: short) ________________________________________________________________ O PROGRAMOWANIU PROCEDURALNO - ZDARZENIOWYM DLA WINDOWS. W proceduralno-sekwencyjnych programach DOS'owskich sterowanie jest przekazywane mniej lub bardziej kolejno kolejnym instrukcjom w taki sposób, jak życzył sobie tego programista. W Windows program-aplikacja prezentuje użytkownikowi wszystkie dostępne opcje w formie widocznych na ekranie obiektów (visual objects) do wyboru przez użytkownika. Program funkcjonuje zatem według zupełnie innej koncepcji nazywanej "programowaniem zdarzeniowym" (ang. event-driven programming). Można powiedzieć, że za przebieg wykonania programu nie jest odpowiedzialny tylko programista lecz część tej odpowiedzialności przejmuje użytkownik i to on decyduje w jaki sposób przebiega wykonanie programu. Użytkownik może wybrać w dowolnym momencie dowolną spośród wszystkich oferowanych mu opcji a program powinien zawsze zareagować poprawnie i równie szybko. Jest oczywiste, że pisząc program nie możemy przewidzieć w jakiej kolejności użytkownik będzie wybierał opcje/rozkazy z menu. Przeciwnie powiniśmy napisać program w taki sposób by dla każdego rozkazu istniał oddzielny kod. Jest to ogólna koncepcja, na której opiera się programowanie zdarzeniowe. W przeciwieństwie do programów proceduralno - sekwencyjnych, które należy czytać od początku do końca, programy dla Windows muszą zostać pocięte na na mniejsze fragmenty - sekcje - na zasadzie jedna sekcja - obsługa jednego zdarzenia. Jeśli zechcesz wyświetlić napis "Hello, World", sekcja zdarzeniowego programu obsługująca takie zdarzenie może wyglądać np. tak: Funkcja_Obsługi_Komunikatów_o_Zdarzeniach(komunikat) { switch (komunikat_od_Windows) { case WM_CREATE: ... TextOut(0, 0, "Napis: np. Hello world.", dlugosc_tekstu); break; ... case WM_CLOSE: // CLOSE - zamknąć okno .... break; ..................... itd. } a w przypadku obiektowego stylu programowania - metoda obsługująca to zdarzenie (należąca np. do obiektu Obiekt_Główne_Okno - TMainWindow) może wyglądać np. tak: void TMainWindow::RysujOkno() { TString Obiekt_napis = "Hello, World"; int dlugosc_tekstu = sizeof(Obiekt_napis); TextOut(DC, 10, 10, Obiekt-napis, dlugosc_tekstu); } Taki fragment kodu programu jest specjalnie przeznaczony do obsługi jednego zdarzenia (ewent-ualności). W okienku wykonuje się operacja PAINT (maluj). "Malowanie" okna może się odbywać albo po raz pierwszy, albo na skutek przesunięcia. Programy zdarzeniowe tworzone w C++ dla Windows będą zbiorem podobnych "kawałków" następujących w tekście programu sekcja za sekcją. Oto jak działa program zdarzeniowy: kod programu, podzielony na sekcje obsługujące poszczególne zdarzenia koncentruje się wokół interfejsu. FUNKCJE WinMain() i WindowProc(). W programach pisanych w standardowym C dla Windows używane są dwie najważniejsze funkcje: WinMain() i WindowProc(). ________________________________________________________________ UWAGA: Funkcji WindowProc() można nadać dowolną nazwę, ale WinMain() musi się zawsze nazywać WinMain(). Jest to nazwa zastrzeżona podobnie jak main() dla aplikacji DOSowskich. ________________________________________________________________ Funkcja WinMain() powoduje utworzenie okna programu umożliwiając zdefiniowanie i zarejestrowanie struktury "okno" (struct WNDCLASS) a następnie powoduje wyświetlenie okna na ekranie. Od tego momentu zarządzanie przejmuje funkcja WindowProc(). W typowej proceduralno - zdarzeniowej aplikacji dla Windows to właśnie funkcja WindowProc() obsługuje pobieranie informacji od użytkownika (np. naciśnięcie klawisza lub wybór z menu). Funkcja WindowProc() robi to dzięki otrzymywaniu tzw. komunikatów (ang. Windows message). W Windows zawsze po wystąpieniu jakiegoś zdarzenia (event) następuje przesłanie komunikatu (message) o tym zdarzeniu do bieżącego aktywnego w danym momencie programu w celu poinformowania go, co się stało. Jeśli został naciśnięty klawisz, komunikat o tym zdarzeniu zostanie przesłany do funkcji WindowProc(). Tak funkcjonuje interfejs pomiędzy aplikacją a Windows. W programach tworzonych w C prototyp funkcji WindowProc() wygląda następująco: LONG FAR PASCAL WindowProc(HWND hWnd, WORD Message, WORD wParam, LONG lParam); Słowa FAR i PASCAL oznaczają, że: FAR - kod funkcji znajduje się w innym segmencie niż kod programu; PASCAL - kolejność odkładania argumentów na stos - odwrotna (jak w Pascalu). ________________________________________________________________ UWAGA: Prototyp funkcji może zostać podany również tak: LONG FAR PASCAL WndProc(HWND, unsigned, WORD, LONG); ________________________________________________________________ Pierwszy parametr hWnd jest to tzw. identyfikator okna (ang. window handle). Ten parametr zawiera informację, dla którego okna przeznaczony jest komunikat. Zastosowanie takiego identyfikatora jest celowe, ponieważ funkcje typu WindowProc() mogą obsługiwać przesyłanie komunikatów do wielu okien. Jeśli okien jest wiele, okno jest identyfikowane przy pomocy tego właśnie identyfikatora (numeru). Następny parametr to sam komunikat o długości jednego słowa (word). Ten parametr przechowuje wartość z zakresu zdefiniowanego w pliku nagłówkowym WINDOWS.H. W zależności od tego co się zdarzyło, Windows mogą nam przekazać ok. 150 różnych komunikatów a w tym np.: WM_CREATE Utworzono okno WM_KEYDOWN Naciśnięto klawisz WM_SIZE Zostały zmienione wymiary okna WM_MOVE Okno zostało przesunięte WM_PAINT Okno należy narysować (powtórnie) - (re)draw WM_QUIT Koniec pracy aplikacji itp. Przedrostek WM_ to skrót od Windows Message - komunikat Windows. Wymiana komunikatów w środowisku Windows może przebiegać w różny sposób - zależnie od źródła wywołującego generację komunikatu i od charakteru zdarzenia. Ze względu na źródło można komuniakty umownie podzielić na następujące grupy: 1. Działanie użytkownika (np. naciśnięcie klawisza) powoduje wygenerowanie komunikatu. 2. Program - aplikacja wywołuje funkcję Windows i powoduje przesłanie komunikatu do aplikacji. 3. Środowisko Windows przesyła komunikat do programu. 4. Dwie aplikacje związane mechanizmem dynamicznej wymiany danych (Dinamic Data Exchange - DDE) wymieniają komunikaty. Komunikaty Windows można także podzielić umownie na następujące kategorie: 1. Komunikaty dotyczące zarządzania oknami (Windows Managenent Msg.): WM_ACTIVATE (zaktywizuj lub zdezaktywizuj okno), WM_PAINT, WM_MOVE, WM_SIZE, WM_CLOSE, WM_QUIT. Bardzo istotnym szczegółem technicznym jest problem przekazywania aktywności pomiędzy oknami. Szczególnie często występuje potrzeba przekazania aktywności do elementu sterującego. Jeśli hEditWnd będzie identyfikatorem (window handle) okienka edycyjnego: case WM_SETFOCUS: SetFocus(hEditWnd); break; funkcja SetFocus() spowoduje, że wszystkie komunikaty dotyczące zdarzeń klawiatury będą kierowane do okna sterującego, jeżeli okno macieżyste jest aktywne. Ponieważ zmiana rozmiaru okna głównego nie pociąga za sobą automatycznej zmiany rozmiaru okna sterującego, potrzebna jest dodatkowo obsługa wiadomości WM_SIZE wobec okna elementu sterującego. 2. Komunikaty inicjacyjne dotyczące konstrukcji np. menu aplikacji: WM_INITMENU - zainicjuj menu (wysyłany przed zainicjowaniem), WM_INITDIALOG - zainicjuj okienko dialogowe. 3. Komunikaty generowane przez Windows w odpowiedzi na wybór rozkazu z menu, zegar, bądź naciśnięcie klawisza: WM_COMMAND - wybrano rozkaz z menu, WM_KEYDOWN - naciśnięto klawisz, WM_MOUSEMOVE - przesunięto myszkę, WM_TIMER - czas minął. 4. Komunikaty systemowe. Aplikacja nie musi odpowiadać na rozkazy obsługiwane przez domyślną procedurę Windows - DefWindowProc(). Szczególnie dotyczy to rozkazów nie odnoszących się do roboczego obszaru okna - Non Client Area Messages. 5. Komunikaty schowka (Clipborad Messages). Sens działania funkcji WindowProc() w C/C++ polega na przeprowadzeniu analizy, co się stało i podjęciu stosownej akcji. Można to realizować przy pomocy drabinki if-else-if, ale najwygodniejsze jest stosowanie instrukcji switch. LONG FAR PASCAL WindowProc(HWND hWnd, WORD Message, WORD wParam, LONG lParam) { switch (Message) { case WM_CREATE: ..... break; /* Koniec obsługi komunikatu WM_CREATE */ case WM_MOVE: .... /* Kod obsługi komunikatu WM_MOVE */ break; /* Koniec obsługi WM_MOVE. */ case WM_SIZE: .... /* Kod obsługi sytuacji WM_SIZE */ break; /* Koniec obsługi WM_SIZE */ .......... /* Inne, pozostałe możliwe sytuacje */ case WM_CLOSE: /* Zamknięcie okna */ .... break; default: /* wariant domyślny: standardowa obsługa .... przez standardową funkcję Windows */ } } ________________________________________________________________ UWAGA: Ponieważ komunikatów "interesujących" daną aplikację może być ponad 100 a sposobów reakcji użytkownika jeszcze więcej, w "poważnych" aplikacjach tworzone są często struktury decyzyjne o większym stopniu złożoności. Jeśli istnieje potrzeba optymalizacji działania programów stosuje się struktury dwu typów: * hierarchia wartości (Value Tree) i * drzewo analizy zdarzeń (Event Tree). Utworzone w taki sposób tzw. "Drzewo decyzyjne" nazywane także "Drzewem analizy zdarzeń" może być wielopoziomowe. Widoczny powyżej pierwszy poziom drzewa (pierwszy przesiew) realizowany jest zwykle przy pomocy instrukcji switch a następne przy pomocy drabinek typu if-else-if-break. Schemat if-else-if-break często bywa zastępowany okienkami dialogowymi. ________________________________________________________________ Parametry wParam i lParam przechowują parametry istotne dla danego komunikatu. wParam ma długość pojedynczego słowa (word) a lParam ma długość podwójnego słowa (long). Jeśli, dla przykładu, okno zostało przesunięte, te parametry zawierają nowe współrzędne okna. Jeżeli program ma być programem zdarzeniowym, powinniśmy przed podjęciem jakiejkolwiek akcji zaczekać aż Windows przyślą nam komunikat o tym, jakie zdarzenie nastąpiło. Wewnątrz Windows tworzona jest dla komunikatów kolejka (ang message queue). Dzięki istnieniu kolejkowania otrzymujemy komunikaty pobierane z kolejki pojedynczo. Jeśli użytkownik przesunie okno a następnie przyciśnie klawisz, to Windows wywołają funkcję WindowProc() najpierw z parametrem WM_MOVE a następnie z parametrem WM_KEYDOWN. Jednym z najważniejszych zadań funkcji WinMain() jest utworzenie kolejki dla komunikatów i poinformowanie Windows, że komunikaty do naszego programu należy kierować pod adresem funkcji WindowProc(). W tym celu stosuje się daleki wskaźnik do procedury okienkowej lpfn (Long Pointer to Function). Poza tym funkcja WinMain() tworzy okno (okna) i wyświetla je na ekranie w pozycji początkowej. Kiedy program zostaje po raz pierwszy załadowany i uruchomiony - Windows najpierw wywołują funkcję WinMain(). Windows manipulują komunikatami posługując się strukturą MSG (od messages - komunikaty). Struktura MSG jest zdefiniowana w pliku WINDOWS.H w następujący sposób: typedef struct tagMSG { HWND hwnd; WORD message; WORD wParam; LONG lParam; DWORD time; POINT pt; } MSG; Na pierwszym polu tej struktury znajduje się "identyfikator" (kod) okna, dla którego przeznaczony jest komunikat (każdy komunikat może być przesłany tylko do jednego okna). Na drugim polu struktury przechowywany jest sam komunikat. Komunikat jest zakodowany przy pomocy predefiniowanych stałych w rodzaju WM_SIZE, WM_PAINT czy WM_MOUSEMOVE. Kolejne dwa pola służą do przechowania danych-parametrów towarzyszących każdemu komunikatowi: wParam i lParam. Na następnym polu przechowywany jest w zakodowanej postaci czas - moment, w którym wystąpiło zdarzenie. Na polu pt przechowywane są współrzędne kursora myszki na ekranie w momencie w którym został wygenerowany komunikat o wystąpieniu zdarzenia. Należy zwrócić tu uwagę, że typ POINT oznacza strukturę. Struktura POINT (punkt) w Windows wygląda tak: typedef struct tagPOINT { int x; int y; } POINT; Aby mieć pewność, że otrzymaliśmy wszystkie komunikaty, które zostały do nas skierowane, w programie wykonywana jest pętla pobierania komunikatów (message loop) wewnątrz funkcji WinMain(). Na początek wywoływana jest zwykle okienkowa (czyli należącą do Windows API) funkcja GetMessage(). Ta funkcja wypełnia strukturę komunikatów i zwraca wartość. Zwracana przez funkcję wartość jest różna od zera, jeżeli otrzymany właśnie komunikat był czymkolwiek za wyjątkiem WM_QUIT. Komunikat WM_QUIT jest komunikatem kończącym pracę każdej aplikacji dla Windows. Jeśli otrzymamy komunikat WM_QUIT powinniśmy przerwać pętlę pobierania komunikatów i zakończyć pracę funkcji WinMain(). Taka sytuacja oznacza, że więcej komunikatów nie będzie. Po uwzględnieniu tych warunków pętla może wyglądać tak: int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, \ LPSTR lpszCmdLine, int nCmdShow) .... while(GetMessage(&msg,NULL,0,0)) //Poki nie otrzymamy WM_QUIT { .... } Po naciśnięciu przez użytkownika klawisza generowany jest komunikat WM_KEYDOWN. Jednakże z faktu otrzymania komunikatu WM_KEYDOWN nie wynika, który klawisz został przyciśnięty, czy była to duża, czy mała litera. Funkcję TranslateMessage() (PrzetłumaczKomunikat) stosuje się do przetłumaczenia komunikatu WM_KEYDOWN na komunikat WM_CHAR. Komunikat WM_CHAR przekazuje przy pomocy parametru wParam kod ASCII naciśniętego klawisza. Funkcję TranslateMessage() stosujemy w pętli pobierania komunikatów tak: int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, \ LPSTR lpszCmdLine, int nCmdShow) .... while(GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); .... } W tym stadium program jest gotów do przesłania komunikatu do funkcji - procedury okienkowej WindowProc(). Posłużymy się w tym celu funkcją DispatchMessage() (ang. dispatch - odpraw, przekaż, DispatchMessage = OtprawKomunikat). Funkcja WinMain() poinformowała wcześniej Windows, że odprawiane komunikaty powinny trafić właśnie do WindowProc(). while(GetMessage(&msg, NULL, NULL, NULL)) { TranslateMessage(&msg); DispatchMessage(&msg); } Tak funkcjonuje pętla pobierająca komunikaty od Windows i przekazująca je funkcji WindowProc(). Pętla działa do momentu pobrania komunikatu WM_QUIT (Koniec!). Otrzymanie komunikatu WM_QUIT powoduje przerwanie pętli i zakończenie pracy programu. Komunikaty systemowe (system messages), które są kierowane do Windows także trafiają do tej pętli i są przekazywane do WindowProc(), ale ich obsługą powinna się zająć specjalna funkcja DefWindowProc() - Default Window Procedure, umieszczona na końcu (wariant default). Jest to standardowa dla aplikacji okienkowych postać pętli pobierania komunikatów. Jak widać, wymiana informacji pomiędzy użytkownikiem, środowiskiem a aplikacją przebiega tu trochę inaczej niż w DOS. Program pracujący w środowisku tekstowym DOS nie musi np. rysować własnego okna. [Z] ________________________________________________________________ 1. Uruchom Windows i popatrz świadomym, fachowym okiem, jak przebiega przekazywanie aktywności (focus) między okienkami aplikacji. ________________________________________________________________