Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

System komunikatów w Delphi

Tekst został importowany z Warsztatowych artykułów. Jego oryginalnym autorem jest Stefan Wyszyński "MadMax". Jeżeli został importowany poprawnie, usuń ten szablon!

UWAGA

Rozpowszechnianie tego artykułu w internecie, bez zgody autora jest zabronione.
Jak mawiali starożytni eskimosi:
Jeśli nie chcesz wjazdu na chatę, to uszanuj moją pracę ;) (ang. Joke).

Wstęp

To jest mój pierwszy artykuł, a temat jaki zamierzam opisać jest rozległy, więc proszę o wyrozmiałość. Chciałbym jedak w niniejszym tekście wyjaśnić jaką rolę odgrywają komunikaty w systemie Windows. Postaram się napisać jak najwięcej szczegółów związanych z tym tematem.

Często będę powtarzał niektóre słowa (np. metoda, obsługa, procedura i inne), gdyż nie sposób znać 30 znaczeń jednego wyrazu. Aby zrozumieć i przyswoić sobię informacje tu opisane, potrzebna jest przynajmniej na poziomie podstawowym wiedza o interfejsie Win32 Api, języku Object Pascal i środowisku Delphi.

Nie znam dobrze języka C, więc przykłady podam w Delphi 4 (co nie znaczy że przdstawione tu kody, nie są kompatybilne z wcześniejszymi wersjami tego produktu). Postaram się chociaż w pkt. "Nagłówki opisanych procedur i funkcji w języku C" umieścić w języku C nagłówki opisanych tu funkcji Win32 Api.

Chciałbym jeszcze dodać, iż schematy przedstawione w niniejszym artykule, przekazują informacje nie w sposób ścisły, a raczej poglądowy, gdyż zostały one wykonane na podstawie mojego doświadczenia. Nie traktuj więc ich, jako swoistego kompedium wiedzy na rozwiązanie problemów związanych z pojmowaniem działania komunikatów w systemie Windows.

Natomiast w przykładach, kluczowe słówka Delphi (takie jak begin, end, for, do, while i inne), nie zostały odznaczone pogrubieniem, ponieważ przygotowanie tego artykułu trwało by za długo. Jedynie komentarze w kodzie zostały pokolorowane na niebiesko, co znacznie zwiększa efekt wizualny i czytelność kodu.

Wykorzystane słowa a ich znaczenie

Dla wygody "Delphi" używam w kontekście Object Pascala i vice versa.

"Rekord" i "struktura" - będę używał do określenia tego samego typu danych.

"Zmienna" - przeznaczę na przedstawienie pola jakiejś struktury, lub samodzielnego wskaźnika do miejsca w pamięci.

Słówko "zdarzenie" - będę używał raz w celu określenia reakcji na wystąpienie komunikatu, a raz jako dosłowe zdarzenie występujące w kontekście Delphi. Myślę, że rozróżnisz, o którą z sytuacji chodzi.

"klauzula" i "słówko" - będę używał do określania słów (np. Inherited), które dla Delphi ma specjalne znaczenie.

"Metoda" - do określenia procedury lub funkcji, która wywołana jest na rzecz konkretnej klasy.

"Windows" i "Win32 Api" - będę używać naprzemiennie.

Spis treści

  1. Po co są komuniaty i do czego mogą się przydać ?
  2. Rodzaje komunikatów,
  3. Postać rekordu komunikatu w systemie Windows 9x,
  4. Kolejka komunikatów i pętla pobierająca - co to jest ?
  5. Jakie aplikacje posiadają kolejkę komunikatów(ang. queue messages),
  6. Przykład wykorzystania pętli w kodzie i opis związanych z tym funkcji Win32 Api,
  7. Obsługa komunikatów w Win32 Api - procedura okienkowa i funkcje z nią związane,
  8. Nazewnictwo komunikatów,
  9. Deklaracja i definicja procedury obsługi komunikatu w Delphi,
  10. Mapowanie własnych lub pomocniczych rekordów na TMessage i ich nazewnictwo,
  11. Obowiązkowa obsługa komunikatu, a nieobowiązkowa zdarzeń w Delphi,
  12. Co kryje się za niewinnym słówkiem "Inherited" ?
  13. SendMessage, PostMessage - jaka jest różnica ?
  14. Tworzenie kmunikatów użytkownika,
  15. Obiekt "TApplication", co przed nami ukrywa ?
  16. Różnica drogi komunikatu między VCL, a interfejsem Win32 API,
  17. VCL czy Win32 Api, pod który z mechanizmów pisać gry w Delphi ?
  18. Przykładowy projekt gry napisany w VCL wykorzystujący komunikaty,
  19. Nagłówki opisanych procedur i funkcji w języku C,
  20. Uwagi końcowe, co dalej ?

Dodatkowe informacje :
A. Słownik ważniejszych pojęć związanych z niniejszym artykułem,
B. Słownik nazw polko-angielskich,

Ad 1. Po co są komuniaty i do czego mogą się przydać?

System Windows bazuję na komunikatach, które zarządzają stanem aplikacji. Komunikaty są więc, aby informować różne aplikcaje o tym co dzieję się aktualnie w systemie.

Są one bardzo potrzebne, ponieważ nawet jeden z nich może nieść ze sobą dość pokaźną informację, która może kryć się za wskaźnikiem do struktury, lub obiektu, znajdujującego się w pamięci.

W systemie komunikaty odbierane i wysyłane są bardzo często, nawed do 100 jak nie więcej razy w ciągu sekundy (trudno określić tą wartość, gdyż jest ona zależna od określonej sytuacji). Komunikaty są też wykorzystywane do komunikacji dwóch lub więcej aplikacji między sobą.

Komunikaty wysyłąne są przez różne funkcje interfejsu Win32 Api, z których sam też możesz korzystać.

Ad 2. Rodzaje komunikatów,

Zazwyczaj komunikaty dzielimy następująco :
- komunikaty poleceń (ang. command messages)
- komunikaty powiadomień (ang. notification messages)

Komunikaty poleceń są wysyłane w celu wykonania jakiejś czynności, natomiast komunikaty powiadomień, jak sama nazwa wskazuje służą do powiadomienia aplikacji lub systemu o wystąpieniu zdarzenia.

Komunikaty można też podzielić na:
- użytkownika (ang. user),
- systemu,
- rozgłaszające (ang. broadcast),

Komunikaty użytkownika to po prostu twoje własne komunikaty, których będziesz używał do informowania okien swojej aplikacji o zajściu określonego zdarzenia.

Komunikaty systemu są wysyłane do okien twoich i innych aplikacji w celu powiadomienia ich o wystąpieniu zdarzenia w systemie (np. o odwzorowaniu palety którejś z aplikacji w logiczną palete systemową, skopiowaniu czegoś do schowka i wiele innych rzeczy), lub polecenia wykonania czynności (np: odświeżenia obszaru płótna).

W VCL Komunikaty rozgłaszające wysyłąne są (przez metodę BroadCast klasy TWinControl) do komponentu w celu powiadomienia wszystkich zawłaszczonych przez niego kontrolek o wystąpieniu określonego zdarzenia.

Według mnie w Delphi jest jeszcze jedna kategoria
- komunikaty wewnętrzne komponentów (ang. component messages)

Takie komunikaty to np. CM_MOUSEENTER, których obsługa wykonana jest wewnątrz komponentów.

Ad 3. Postać rekordu komunikatu w systemie Windows 9x,

W Delphi obiektem który reprezentuje systemową postać komunikatu jest klasa TMSG.

Type TMsg = packed record hwnd: HWND; message: UINT; wParam: WPARAM; lParam: LPARAM; time: DWORD; pt: TPoint; end;

Poniżej wyjaśniam znaczenie ważniejszych pól tej struktury.

"hwnd" - jest to uchwyt okna, do którego są kierowane poszczególne komunikaty.
"message" - w tym polu przesyłana jest stała reprezentująca typ wysyłanego komunikatu

"wParam" i "lParam" - pola te mogą zawierać informacje dodatkowe, np. wskaźnik do struktury, jakąś liczbę i inne dane.

Nazwy pól "wParam" i "lParam" - są pozostałością po 16 bitowym Windows 3.11, ponieważ tam
pola te różniły się wielkością "wParam" było typu Word (16 bitowy - 2 bajtowy), natomiast lParam typu Long (32 bitowy - 4 bajtowy). W obecnej wersji systemu (nowszej niż Win 3.11) są one tej samej wielkości czyli DWord 32 bity (4 bajty).

time - Czas umieszczenia komunikatu w kolejce.
pt - zmienna zawierająca współrzędne myszy.

Korzystając z VCL musimy liczyć się z faktem że komunikat po pobraniu z kolejki przetwarzany jest w strukturę TMsg, a następnie zmieniany jest przez VCL w rekord poniższego typu :

type TMessage = record Msg: Cardinal; case Integer of 0: ( WParam: Longint; LParam: Longint; Result: Longint); 1: ( WParamLo: Word; WParamHi: Word; LParamLo: Word; LParamHi: Word; ResultLo: Word; ResultHi: Word); end;

Zawiera on część zmienną, której odpowiednikiem w języku "C" jest "Unia".
Umożliwia on proste mapowanie własnych rekordów komunikatów na tę strukturę i rozbijanie ich pól na dwu bajtowe słowa (16 bitowe zmienne), co uwalnia nas od bespośredniego operowania funkcjami LoWord i HiWord zwracjącymi mniej/bardziej zanaczące słowo czterobajtowej zmiennej.

W Win32 Api obsługa komunikatu np. WM_MOUSEMOVE w procedurze okna (ang. window procedure) wygląda tak :

function Main_WndProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; var x, y : SmallInt; Key : DWord; begin case Msg.Message do WM_MOUSEMOVE : begin Key := Msg.wParam; // stan kalawiszy x := LoWord(Msg.lParam); // pozycja x kursora y := HiWord(Msg.lParam); // pozycja y kursora Result := 0; end; else // domyślna obsługa komunikatu (nic się nie zdarzyło) Result := DefWindowProc(hWnd, Msg, wParam, lParam); end; end;

zakładając, że rekord przekazywany procedurze obsługi jest typu TWMMouseMove, można zastąpić to przez :

procedure WMMouseMove(var msg : TWMMouseMove); var x, y : SmallInt; Key : DWord; begin Key := Msg.Keys; // stan kalawiszy x := Msg.XPos; // pozycja x kursora y := Msg.YPos; // pozycja y kursora end;

lub przez mapowanie niestandardowych struktur na TMessage.

procedure WMMouseMove(var msg : TMessage); var x, y : SmallInt; Key : DWord; begin with TWMMouseMove(Msg) do begin Key := Keys; // stan kalawiszy x := XPos; // pozycja x kursora y := YPos; // pozycja y kursora end; end;

TMessage zawiera też dodatkowe pole Result (którego próżno szukać w TMsg), przeznaczone do zachowania w nim rezultatu wykonania obsługiwanego komunikatu, a następnie przekazania go systemowi.

Ad 4. Kolejka komunikatów i pętla pobierająca - co to jest?

Kolejka komunikatów to obszar pamięci odwzorowanej w pamięć procesu (aplikacji), w którym są kolejkowane komunikaty.

Istnieją dwa sposoby (jak nie więcej) realizacji systemu komunikatów w Delphi.

- Pierwszy ze sposobów wykonywany w Win32 Api:

  1. gdy wystąpi zdarzenie system przetwarza je w strukturę danych,
  2. następnie umieszcza tą strukturę w kolejce komunikatów,
  3. process pobiera komunikat z kolejki przez pętle pobierającą i tworzy dla niego rekord TMsg,
  4. komunikat jest wysyłany pod adresem okna docelowego,
  5. jeżeli w procedurze okienkowej, tego okna, uwzględniliśmy jego obsługę, wtedy jest ona wywoływana, w przeciwnym wypadku komunikat jest obsługiwany w sposób standardowy dla Win32 Api.
  6. w obu wymienionych sytuacjach z ppkt. 5, komunikat zwraca rezultat swej obsługi do systemu, podając go jako rezultat procedurze okna.

- Drugi ze sposobów jest realizowany przez VCL, ukrywa on szczegóły funckjonowania Win32 Api:

  1. gdy wystąpi zdarzenie, system przetwarza je w strukturę danych,
  2. następnie umieszcza tą strukturę w kolejce komunikatów,
  3. aplikacia pobiera komunikat z kolejki przez pętle pobierającą i tworzy dla niego rekord TMsg,
  4. komunikat jest wysyłany pod adresem okna docelowego i rekord TMsg zmieniany jest w TMessage,
  5. jeżeli okno to zawiera procedurę obsługi tego komunikatu, wtedy jest ona wywoływana, w przeciwnym wypadku komunikat jest obsługiwany w sposób standardowy.
  6. wywołana jest końcowa metoda DefaultHandler.
  7. w obu wymienionych sytuacjach z ppkt 5, rekord TMessage jest przetworzany na strukturę TMsg, a jego pole Result jest wykorzystane w celu przekazania informacji zwrotnej systemowi.

Obydwa sposoby przedstawiam na poniższym rysunku. Wydzielenie z tego rysunku poszczególnego z wymienionych sposobów (VCL lub Win32 Api) jest już twoim zadaniem (możesz to potraktówać jak prace domową).

Rysunek ten ilustruje ogólne funkcjonowanie komunikatów w Win32 Api i VCL. Pozatym nie przedstawia on dokładnego losu komunikatu (np. przez funkcjię DispatchMessage, Dispatch i inne), powinnto to jednak nam narazie wystarczyć do zrozumienia jego drogi w systemie.

Jeśli chcesz dokładniej wiedzieć jak komunikat podróżuję w VCL i Win32 Api zobacz pkt. pod tytułem. : "Różnica drogi komunikatu między VCL, a interfejsem Win32 API".

Istnieją też kounikaty, które nie trafiają do kolejki, lecz bespośrednio do procedury obsługi danego okna. O tym jednak napiszę troche więcej w pkt. "SendMessage, PostMessage - jaka jest różnica?".

Ad 5. Jakie aplikacje posiadają kolejkę komunikatów (ang. queue messages),

Każdy proces z graficznym interfejsem posiada pętlę komunikatów. Nie posiadają jej na przykład aplikacje konsolowe (z oczywistych względów, ponieważ Dos nie bazował na komunikatach).

W Delphi aplikcaje konsolowe oznacza się za pomocą symbolu "{$APPTYPE CONSOLE}". Przykładowe wykorzystanie tego symbolu w kodzie może wyglądać tak : program DelphiWin32TestApp; {$APPTYPE CONSOLE} // Linia ta czyni z programu aplikację konsolową uses windows; var S : string; begin Write('Wstaw linie tekstu : '); Readln(S); Writeln('Walnij klawisz <Enter>'); Readln; end.

Działająca aplikacja konsolowa :

Ad 6. Przykład wykorzystania pętli w kodzie i opis związanych z tym funkcji Win32 Api,

Z wysłąniem i pobieraniem komunikatów lub sprawdzeniem ich istnienia w kolejce, zwązanych jest kilka funkcji, z których ważniejsze zamierzam pokrótce omówić, a oto ich nagłówki:

function GetMessage( var lpMsg : TMsg; // wskaźnik do struktury z komunikatem hWnd : HWND; // uchwyt okna wMsgFilterMin, // pierwszy komunikat wMsgFilterMax : UINT // ostatni komunikat ): BOOL; stdcall; // rezultat informujący o istnieniu komunikatu w kolejce

1. Przeznaczenie :

Jak sama nazwa wskazuje (GetMessage - pol. Pobierz komunikat) funkcja pobiera komunikat z kolejki.

2. Rezultat funkcji :

Zwraca ona niezerową wartość dopóki, dopóty pobranym komunikatem nie będzie WM_QUIT. Może ona też zwrócić "-1" jeżeli uchwyt okna pod jakim skierowany jest komunikat, jest nieprawidłowy, lub okno takie nie istnieje ( spowoduje to oczywiście błąd systemowy ).

3. Opis parametrów :

lpMsg - komunikat pobrany z kolejki jest umieszczany pod adresem przekazanej tu zmiennej TMsg

hWnd - identyfikuje uchwyt okna, które zamierza obsłużyć komunikat pobrany z kolejki. Podając w pole to liczbę zero powodujeż, że komunikat pobierany jest dla jakiego kolwiek okna istniejącego, w kontekście wątku jaki wywołuje tą funkcję.

wMsgFilterMin i wMsgFilterMax - pola typu Integer, określające dolny i górny zakres komunikatu jaki zostanie pobrany ze wszystkich istniejących w kolejce, wszelkie inne wartości przekraczające podane w nich indeksy nie zostaną pobrane. Jężeli pola te są wyzerowane, to zakres pobieranego komunikatu nie jest ograniczony.

{------------------------------------------------------------------------------------} function PeekMessage( var lpMsg: TMsg; // wskaźnik do struktury z komunikatem hWnd: HWND; // uchwyt okna wMsgFilterMin, // pierwszy komunikat wMsgFilterMax, // ostatni komunikat wRemoveMsg: UINT // flagi usunięcia ): BOOL; stdcall; // rezultat informujący o istnieniu komunikatu w kolejce

1. Przeznaczenie :

Pobiera komunikat z kolejki.

2. Rezultat funkcji :

Jeżeli w kolejce istnieje komunikat, zwracaną wartością jest "True" (if PeekMessage.Result > 0), w przeciwnym wypadku zwraca "False".

3. Opis parametrów :

lpMsg - komunikat pobrany z kolejki jest umieszczany pod adresem przekazanej tu zmiennej typu TMsg.

hWnd - identyfikuje uchwyt okna, które zamierza obsłużyć komunikat pobrany z kolejki. Podając w pole to liczbę zero powodujesz, że komunikat pobierany jest dla jakiego kolwiek okna istniejącego w kontekście wątku, jaki wywołuje tą funkcję.

wMsgFilterMin i wMsgFilterMax - pola typu Integer, określające dolny i górny zakres kmunikatu jaki zostanie pobrany ze wszystkich istniejących w kolejce. Wszelkie inne wartości przekraczające podane w nich indeksy nie zostaną pobrane. Jeżeli pola te są wyzerowane, to zakres pobieranego komunikatu nie jest ograniczony.

wRemoveMsg - Ustala czy komunikat będzie usuwany z kolejki podczas pobierania, czy też nie.
Zmienna ta może reprezentować jedną z dwóch wartości:
PM_NOREMOVE - komunikat nie jest usuwany z kolejki.
PM_REMOVE - komunikat jest usuwany z kolejki.

Istnieje jeszcze jedna wartość "PM_NOYIELD", którą można przypisać zmiennej wRemoveMsg wraz z powyżej wymienionymi, lecz straciła ona znaczenie, gdy pojawił się 32 bitowy Windows 95. Dlatego też, nie zamierzam opisywać jej znaczenia.

{------------------------------------------------------------------------------------} // inne funkcje związane z komunikatami : {------------------------------------------------------------------------------------}

Nie znam dokładnego przeznaczenia poniższej funkcji, jednak zamierzam przedstawić informacje jakie posiadam na jej temat.

function TranslateMessage( const lpMsg: TMsg // Adres do struktury z komunikatem ): BOOL; stdcall;

1. Przeznaczenie.

Funkcja obsługuje komunikaty związane z przetważaniem klawiszy.

2. Rezultat funkcji.

jeżeli komunikat jest przetłumaczony, zwracana wartość jest nie zerowa. Jeśli jednak komunikat nie jest przetłumaczony, zwracaną wartością jest "0".

W Windows NT : Funkcja TranslateMessage zwraca wartość niezerową dla klawiszy funkcyjnych i strzałek również jak dla klawiszy znaków i cyfr.

3. Opis parametrów.

lpMsg - wskaźnik do rekordu typu TMsg, który reprezentuje komunikat pobrany z kolejki za pośrednictwem funkcji GetMessage lub PeekMessage.

{------------------------------------------------------------------------------------} function DispatchMessage( const lpMsg: TMsg // wskaźnik do struktury z komunikatem ): Longint; stdcall;

1. Przeznaczenie.

- Funkcja wysyła komunikat pod adresem okna (przekazanego w parametrze TMsg.hwnd) i wywołuję procedure okna z nim powiązaną.

2. Rezultat funkcji.

Zwracaną wartością jest rezultat procedury okna.

3. Opis parametrów.

lpmsg - Wskaźnik do struktury TMsg, która zawiera wysyłany komunikat.

{------------------------------------------------------------------------------------}

Istnieją jeszcze funkcje PostThreadMessage, PostAppMessage, WaitMessage, SendNotifyMessage, SendMessageCallBack, GetMessageTime, GetMessagePos, SetMessageExtraInfo, GetMessageExtraInfo i inne.

Wymieniłem je po to abyś później mógł zobaczyć w pomocy Delphi (najlepiej w pliku "Win32 Programmer's Reference.hlp" lub "Win32.hlp") jekie jest ich wykorzystanie, a nie zamierzam ich tu opisywać, ponieważ wykraczają one poza możliwości objętościowe tego artykułu.

Funkcje wysyłające SendMessage i PostMessage omówię dopiero w pkt. "SendMessage, PostMessage - jaka jest różnica?".

Poniżej przedstawiam wymienione funkcje Win32 Api oraz najprostszy sposób ich wykorzystania w przykładowej pętli obsługi komnikatów :

//... var MyOwnWndClass: TWndClass; HMyOwnWnd : HWND; MyOwnMsg : TMsg; begin { inicjacja struktury TWndClass rejestrowanie i tworzenie okna itd. } // pobieranie przez pętle pojedyńczego komunikatu z kolejki while GetMessage(MyOwnMsg, 0, 0, 0) do begin Translatemessage(MyOwnMsg); // wysyłanie komunikatu pod adresem okna docelowego kryjącego się za // uchwytem przekazanym jako parametr reordu TMsg.hwnd DispatchMessage(MyOwnMsg); end; Halt(MyOwnMessage.WParam); end.

A oto następny przykład (przydatny przy pisaniu gier), korzystający z funkcji PeekMessage :

function WinMain(hInstance : HINST; hPrevInstance : HINST ;lpCmdLine : PChar; nCmdShow : Integer) : Integer; stdcall; var Msg : TMsg; Koniec : Boolean; // tutaj inne potrzebne zmienne begin Koniec := False; { Utwórz i zarejestruj swoje okno } while Koniec do begin // sprawdź, czy jest komunikat dla tego okna if (PeekMessage(Msg, 0, 0, 0, PM_REMOVE)) then begin // jeżeli pobrany komunikat z kolejki to WM_QUIT, wtedy zakończ aplikacje if (Msg.Message = WM_QUIT) then Koniec := True else begin // w przeciwnym wypadku wywołaj TranslateMessage i DispatchMessage dla tego okna TranslateMessage(Msg); DispatchMessage(Msg); end; end else begin { Tutaj wykonaj akcję gry np. oblicz czas każdej klatki, wyświetl animację itd., a później sprawdź czy wciśnięty jest klawisz "Esc"o kodzie VK_ESCAPE i jeśli tak jest, toustaw Koniec := True; jeśli jednak nie toobsłuż klawiaturę } end; end; { Usuń i wyrejestruj okno } //przypisz rezultat Result := Msg.wParam; end;

Ad 7. Obsługa komunikatów w Win32 Api - procedura okienkowa i funkcje z nią związane,

Komunikaty po pobraniu ich z kolejki są wysyłane pod adresem okien, więc ich realizaja musi zostać w nich przeprowadzona.

Z każdym oknem związana jest procedura okna (ang. window procedure), której zadaniem jest obsługa trafiających do niej komunikatów. System Windows rozbija strukturę komunikatu na poszczególne parametry i podaje procedurze okienkowej. Na tej podstawie są w niej wykonane odpowiednie działania i zwracany jest rezultat tych czynności do systemu informujący o powodzeniu, lub niepowodzeniu tej obsługi. Nagłówek tej funkcji przedstawia się następująco :

function WndProc( WndHandle : HWnd; Msg, WParam, LParam : LongInt) : LongInt; stdcall;

Działanie opisanego schematu widać na rysunku.

Projekt zawierający kod realizujący w najprostszy sposób wymienione czyności, wygląda tak :

program DelphiWin32TestApp; uses Windows, Messages; var h_Wnd : HWND; QuitExists : Boolean = False; function WndProc(hwnd: HWND; Msg: UINT; wParam: wParam; lParam: lParam): LRESULT; stdcall; begin case Msg of // Zmienna zawierająca identyfikator komunikatu WM_CLOSE : begin // Obsługa komunikatu WM_CLOSE PostQuitMessage(0); Result := 0; end; WM_KEYDOWN : begin // Obsługa komunikatu WM_KEYDOWN QuitExists := (wParam = VK_ESCAPE); Result := 0; end; else Result := DefWindowProc(hwnd , Msg, wParam, lParam); end; end; function WinMain(hInstance : HINST; hPrevInstance : HINST ;lpCmdLine : PChar; nCmdShow : Integer) : Integer; stdcall; var Msg : TMsg; Koniec : Boolean; // i inne potrzebne zmienne begin Koniec := False; { Utwórz i zarejestruj swoje okno } while not Koniec do begin // sprawdz czy jest komunikat dla tego okna if (PeekMessage(Msg, 0, 0, 0, PM_REMOVE)) then begin // jeżeli pobrany komunikat z kolejki to WM_QUIT wtedy zakończ aplikacje if (Msg.Message = WM_QUIT) then Koniec := True else begin // w przeciwnym wypadku wywołaj TranslateMessage i DispatchMessage dla tego okna TranslateMessage(Msg); DispatchMessage(Msg); end; end else begin Koniec := QuitExists; { Tutaj wykonaj akcję gry np. oblicz czas każdej klatki, wyświetl animację itd., a później sprawdź czy wciśnięty jest klawisz "Esc" o kodzie VK_ESCAPE i jeśli tak jest, toustaw Koniec := True; jeżeli jednak nie toobsłuż klawiaturę } end; end; { Usuń i wyrejestruj okno } //przypisz rezultat Result := Msg.wParam; end; begin // Główny blok rozpoczynający i kończący pracę programu WinMain( hInstance, hPrevInst, CmdLine, CmdShow ); end.

Jak zapewne zauważyłeś, w kodzie tym występuje dotychczas nie omawiana funkcja "DefWindowProc", o której nie wspomniałem, aby nie zrobić zbytniego zamętu, w twojej głowie.

Pisałem w pkt. "Po co są komuniaty i do czego mogą się przydać?", że w systemie występuje bardzo duża liczba różnych komunikatów, a ich sposób realizacji mógłby przyprawić o ból głowy nie jednego programistę, przy tym końcowy kod procedury okienkowej byłby bardzo zagmatwany. Przeznaczeniem funkcji "DefWindowProc" jest obsługa, w sposób domyślny (całkowicie poprawny bo wykonany przez system) komunikatów, których wykonanie nie ma odzwierciedlenia w kodzie zródłowym programisty. Dzięki temu możemy obsługiwać tylko te komunikaty, które nas interesują.

{------------------------------------------------------------------------------------} function DefWindowProc( hWnd: HWND; // uchwyt do okna Msg: UINT; // Identyfiator komunikatu wParam: WPARAM; // pierwszy parametr komunikatu lParam: LPARAM // następny parametr komunikatu ): LRESULT; stdcall;

1. Przeznaczenie.

Wykonuje ona domyślne czynności związane z obsługą podanego jej komunikatu.

2. Rezultat funkcji.

Zwracaną wartością jest rezultat przetworzonego komunikatu.

3. Opis parametrów.

hWnd - Identyfikuje uchwyt okna, dla którego wywoływana jest ta procedura.
Msg - Specyfikuje komuniat.
wParam, lParam - Dodatkowe informacje komunikatu.

Ad 8. Nazewnictwo komunikatów,

Prawie każdy komunikat w różnych językach programowania jest reprezentowany przez stałą, co uwalnia nas od pamiętania identyfikatorów poszczególnych komunikatów np.: WM_PAINT = $000F;

Nazwa komunikatu jest złożona z przedrostka i znaczenia, np. w powyższym przykładzie WM_ to przedrostek, a PAINT (pol. malować) to znaczenie. WM to skrót znaczący Window Messages (pol. komunikaty okien). Istnieją też inne przedrostki, a oto niektóre z nich :

CM - komunikaty komponentu (ang. component messages),
NM - komunikaty powiadomień (ang. notification messages),
TB - komunikaty paska narzędzi (ang. toolbar),
EM - komunikaty pola edycyjnego (ang. edit message),

Teraz wyobraź sobie, że masz 5 komunikatów i chcesz zapamiętać ich liczby identyfikacyjne, a później to obsłużyć w procedurze okienkowej.

case Msg of WM_APP + $000F : // tu obsługa WM_APP + $00AF : // tu obsługa WM_APP + $00FF : // tu obsługa WM_APP + $00EE : // tu obsługa WM_APP + $00AA : // tu obsługa end;

Stałą WM_APP (jak również WM_USER) opiszę dokładniej w pkt. "Komunikaty użytkownika" , natomiast tu trzeba wspomnieć, że wykorzystuje się ją w celu uniknięcia kolizji identyfikatora twojego komunikatu z liczbą przedstawiającą komunikat innej aplikacji, lub komponentów, których używasz tworząc aplikacje przy pomocy VCL.

Gdy powracasz po miesiącu do tego kodu to poprostu nic nie rozumiesz. Dlatego warto dla każdego komunikatu przeznaczyć stałą, wtedy wyglądało by to tak :

const WM_LATANIE = WM_APP + $000F; WM_MYNORMAL = WM_APP + $00AF; WM_UDERZ = WM_APP + $00FF; WM_DEFAULT = WM_APP + $00EE; WM_SKOK = WM_APP + $00AA; //......... case Msg of WM_LATANIE : // tu obsługa WM_MYNORMAL : // tu obsługa WM_UDERZ : // tu obsługa WM_DEFAULT : // tu obsługa WM_SKOK : // tu obsługa end;

Czy ten przykład nie jest łatwiejszy do zrozumienia i zapamiętania.

Ad 9. Deklaracja i definicja procedury obsługi komunikatu w Delphi,

Deklaracja obsługi komunikatu w VCL może być przeprowadzona tylko na rzecz obiektów wywodzących się z klasy okna zarejestrowanej w systemie i posiadającej procedurę okienkową. Taką klasą jest np. TControl i klasy pochodne (np. TWinControl, TGraphicsControl i inne).

Niech za przykład posłuży komunikat WM_CREATE, wysyłany przez funkcje CreateWindowEx, lub CreateWindow każdorazowo, gdy tworzone jest okno. Nagłówek metody obsługi komunikatu w VCL wygląda następująco :

procedure WMKomunikat(var Msg : TMessage); message WM_KOMUNIKAT;

Klauzula "message ", po której występuje identyfikator komunikatu w postaci stałej, mówi że zamierzasz obsłużyć komunikat, którego indeks jest w niej umieszczony. Nazwa procedury obsługi komunikatu może być dowolna, lecz standardowo jest nią odzwierciedlenie nazwy stałej komunikatu, bez podkreślenia po przedrostku, np.:

procedure WMDelete(var Msg : TMessage); message WM_DELETE;

Każda procedura komunikatu w VCL musi posiadać jeden parametr przkazany przez zmienną (np. var Msg : TMessage), reprezentujący komunikat. W bierzącym punkcie parametrem tym będzie TMessage, jednak możesz umieszczać własne lub już istniejące rekordy reprezentujące komunikat. O tym przekonasz się w pkt. "Mapowanie własnych lub pomocniczych rekordów na TMessage i ich nazewnictwo" .

Poniższy przykład przedstawia deklaracje i definicje metody obiektu nie łamiącą żadnych z dotychczas wymienionych zasad.

//..... type TTestComp = class(TWinControl) private // Deklaracja zamiaru obsługi komunikatu WM_CREATE procedure WMCreate(var Msg : TMessage); message WM_CREATE; end; //..... implementation //..... procedure TTestComp.WMCreate(var Msg : TMessage); // metoda obiektu begin inherited; // wywołaj metode nadrzędną obiektu { tu umieść kod przedstawiający twoją reakcje na wystąpienie tego komunikatu } end;

Gdybyśmy teraz chcieli utworzyć formularz i zareagować na to zdarzenie przez komunikat WM_CREATE, należało by wykonać taki kod :

unit MessageTest; interface uses Windows, Messages, Forms, Dialogs; type TFrmMessage = class(TForm) public { Deklaracja komunikatu WM_CREATE } procedure WMCreate(var Msg : TMessage); message WM_CREATE; end; var FrmMessage: TFrmMessage; implementation {$R *.DFM} { Definicja komunikatu WM_CREATE } procedure TFrmMessage.WMCreate(var Msg : TMessage); // metoda obiektu begin inherited; // wywołaj metode nadrzędną obiektu ShowMessage('Witam z komunikatu WM_CREATE'); end; end.

Ad 10. Mapowanie własnych lub pomocniczych rekordów na TMessage i ich nazewnictwo,

Jeśli do tej pory czytałeś uważnie, to wiesz, że mapowanie rekordów komunikatów na strukturę TMessage jest przydatne, ponieważ czyni ono programowanie łatwiejszym (zobacz przykład wykorzystania komunikatu WM_MOUSEMOVE w pkt. 3).

Mapowanie rekordu TWMMouseMove (komunikatu WM_MOUSEMOVE) na strukture TMessage, można zilustrować w następujący sposób :

Rysunek ten przedstawia uproszczony rekord TMessage (nie zawierający części wariantowej), na który mapowana jest struktura TWMMouseMove.

Prawie dla każdego komunikatu w Delphi, istnieje odpowiednia struktura mapująca np.:

WM_ERASEBKGND - TWMEraseBkgnd,
WM_CTLCOLOR - TWMCtlColor,
WM_CREATE - TWMCreate,
WM_MouseMove - TWMMouseMove,

Sam widzisz, że nazwy tych struktur pochodzą od nazwy stałej komunikatu poprzedzonej literą "T" i z usuniętym podkreśleniem po przedrostku "WM". Co prawda można nadać dowolną nazwę dla struktur reprezetujących własne komunikaty, lecz na dłuższą metę niedostosowanie się do standardu nazewnictwa, może przysporzyć ci i ludziom w twoim zespole, dużo kłopotów, wiec własne struktury powinno się tworzyć tak :

WM_GRAJ = TWMGraj,
WM_IDZ = TWMIdz, itd.

Reguły jakie musisz poznać zanim zaczniesz tworzyć własne struktury mapujące, są następujące.

- Rekordy mapujące muszą być równe co do wielkości strukturze TMessage (czyli 16 bajtów),
- Rekordy takie muszą także zawierać pola, "Result" i "Msg" (lub pod innymi nazwami), gdzie pole "Msg" musi być umieszczone w pierwszych 4 bajtach, natomiast "Result" w ostatnich 4 bajtach,

Kompletny moduł przykładowy tworzenia i wykorzystania takiego rekordu prezentuje się następująco:

unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; const // Stała komunikatu WM_POCISK = WM_USER + 100; // Sałe umieszczane w polu "Typ" rekordu TWMPocisk _BOMBA = 0; _RAKIETA = 1; _KULKA = 2; { TMessage = record // Wzorcowy rekord TMessage, dla ułatwienia bez części wariantowej Msg: Cardinal; WParam: Longint; LParam: Longint; Result: Longint); end; } type TWMPocisk = record // Własny rekord mapujący Msg: Cardinal; // Msg Typ: Longint; // WParam PozycjaX: Word; // LOWORD(LParam) PozycjaY: Word; // HIWORD(LParam) Result: Longint; // Result end; TPozycjaMyszy = record X: Word; Y: Word; end; TForm1 = class(TForm) procedure FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); private { Private declarations } public PozycjaMyszy : TPozycjaMyszy; procedure WMPocisk(var Msg : TWMPocisk); message WM_POCISK; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.WMPocisk(var Msg : TWMPocisk); begin with Msg do begin case Typ of _BOMBA : Caption := 'Bomba'; _RAKIETA : Caption := 'Rakieta'; _KULKA : Caption := 'Kula'; end; Caption := Caption + ', X:' + IntToStr(PozycjaX) + ', Y:' + IntToStr(PozycjaY); Result := 0; end; end; procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin PozycjaMyszy.X := X; PozycjaMyszy.Y := Y; Perform(WM_POCISK, Ord(Button), LongInt(PozycjaMyszy)); end; end.

Wystarczy przestudiować powyższy przykład żeby zrozumieć pkt. Mapowanie, lecz dla leniwych pokażę krok po kroku (ang. step by step), jak to się czyni.

- Ustalamy komunikat, w oparciu o który będzie tworzony rekord mapujący

const WM_KOMUNIKAT = WM_USER + 123; // Przykładowy komunikat

- Tworzymy nazwę rekordu, która wzoruje się na nazwie tego komunikatu

type TWMKomunikat = record // Własny rekord mapujący

- Pierwszym polem musi być czterobajtowa zmienna Msg (lub pod inną nazwą)

Msg: Cardinal; // Wymagane pole

- Drugie 4 bajty możesz wykorzystać dowolnie np.:

// dowolna nazwa pola, lub pól, które mieszczą się w granicy 4 bajtów PolePierwsze : Boolean;

- Trzecie 4 bajty możesz wykorzystać dowolnie np.:

// dowolna nazwa pola, lub pól, które mieszczą się w granicy 4 bajtów PoleDrugie : Integer;

- Ostawnim polem musi być czterobajtowa zmienna Msg (lub pod inną nazwą), która przeznaczona jest do zwracania rezultatu systemowi.

Result := LongInt;

Oto nasz rekord mapujący w swej całej okazałości :

type TWMKomunikat = record // Własny rekord mapujący Msg: Cardinal; // Msg PolePierwsze : Boolean; // WParam PoleDrugie : Integer; // LParam Result: Longint; // Result end;

Ad 11. Obowiązkowa obsługa komunikatu, a nieobowiązkowa zdarzeń Delphi,

Zdarzenia w kontekscie Delphi (nie komunikatów), traktuje się jak właściwość (należącą do obiektu) typu wskaźnikowego, która wskazuje pod adres procedury wywołanej, gdy wystąpi sytuacja określona mianem zdarzenia. Tak samo w życiu, np. kopnąłeś z obrotu w dzewo jak "Jaś Klaun Waj Dam" :) i wystąpiły zdarzenia: kopniecie, ból nogi i podróż do szpitala, więc zdarzeniem w Delphi jest np. prosta procedura "OnClick" wywołana, gdy kliknełeś przyciskiem myszy na przycisk (klasy TButton), panel (klasy TPanel), lub inny komponent zawierającey ten typ zdarzenia. Zapewne wiesz że nieobsłużenie tego zdarzenia nie powoduje błędu, co można udowodnić poniższym kodem,

//kod ten jest wywołany gdy komponent zarejestruje kliknięcie w swoim obszarze. //FOnClick jest wywołane tylko wtedy, gdy wskazuje na procedurę obsługi tego zdarzenia. //..... Type TNotifyEvent = procedure (Sender : TObject) of object; FOnClick : TNotifyEvent; //..... if Assigned(FOnClick) then FOnClick(SelF);

czego nie można powiedzieć o komunikatach, gdyż zadeklarowanie i zdefiniowanie procedury obsługi komunikatu i nie obsłużenie go w jej ramach, powoduje błąd, ponieważ system żąda od aplikacji rezultatu tej obsługi. W niektórych przypadkach reakcją systemu na nie zwrócenie przez aplikację rezultatu obsługi komunikatu, jest powtórne wysłanie przez niego komunikatu do aplikacji. Poniżej przedstawiam błędny i prawidłowy sposób obsługi komunikatu WM_POCISK przedstawionego jako przykład w poprzednim punkcie.

procedure TKlasa.WMPocisk(var Msg : TWMPocisk); begin end;

W poprawny sposób należy to zrobić tak :

procedure TKlasa.WMPocisk(var Msg : TWMPocisk); begin inherited; // wywołaj procedurę macierzystą { tutaj kod obsługi komunikatu } end;

lub :

procedure TKlasa.WMPocisk(var Msg : TWMPocisk); begin { tutaj kod obsługi komunikatu } Msg.Result := 0; end;

Stosowanie się do zaleceń tu przedstawionych jest wymogiem, który należy spełnić, aby twoje programy działały prawidłowo. W większości przypadków nie jest wymagane zwócenie rezultatu obsługi do systemu, jednak niektóre komunikaty muszą go zwrócić. Nie jest też wymagane wywołanie metody macierzystej (za pomocą słówka inherited), lecz bez niego sterowanie przepływem komunikatu zakończy się na procedurze, która go nie wywołała.

Klauzula "inherited" wywołuje procedurę nadrzędną PROCEDURY, w której została ona umieszczona. To czy postawimy ją jako pierwszą, czy ostatnią w procedurze obsługi komunikatu (lub zwykłej procedurze), ma niebagatelne znaczenie, o czym przekonasz się w następnym pkt.

Ad 12. Co kryje się za niewinnym słówkiem "Inherited"?

Słówko "inherited", po polsku znaczy odziediczyć. Używa się go na rzecz klasy potomnej, co umożliwia wywołanie metody nadrzędnej, np.:

//... type TBazowa = class // klasa wywodząca się z TObject public //virtual lub dymnamic daje możliwość przedefiniowania w klasie pochodnej function Atakuj : boolean; virtual; end; TPotomna = class(TBazowa) public function Atakuj : boolean; override; //zastąpienie end; implementation function TBazowa.Atakuj : boolean; begin Result := True; end; function TPotomna.Atakuj : boolean; begin Result := False; Result := inherited Atakuj; end; end.

A teraz jak myślisz, co zwróci funkcja "Atakuj" klasy TPotomna. To proste. Najpierw rezultat ustawiany jast na "False", później wywołana jest metoda "Atakuj" klasy, z której dziediczy klasa TPotomna, co możliwe jest za pomocą magicznego słówka "Inherited", po którym występuje nazwa metody nadrzędnej (wraz z parametrami, jeżeli takowe posiada). W przykładzie tym metoda nadrzędna, czyli funkcja "Atakuj" klasy TBazowa, zwraca wartość "True", co powoduję że funkcja "Atakuj" klasy TPotomna zawsze zwracać będzie "True". Jak zapewne zauważyłeś umieszczenie lini : "Result := inherited Atakuj;" na samym począdku zmieniło by chwilowo wartość rezultatu na "True", lecz w następnej lini rezultat byłby równy "False",

Result := inherited Atakuj; //wywołaj funkcje klasy TBazowa - zwróć TRUE Result := False;

więc, wywołanie metody nadrzędnej w niewłaściwym miejscu może zmienić przeznaczenie procedury, która te słówko wywołuje. Pewnie zapytasz, jak to wszystko ma się do komunikatów?. Otóż metoda obsługi komunikatu też możę wywołać procedurę macierzystą, lecz nie musi po tym słówku umieszczać nazwy (i ewentualnie parametów) tej metody, gdyż Delphi wie, która to procedura, np.:

procedure TForm1.WMCreate(var Msg : TMessage); begin inherited; // wywołaj metode nadrzędną obiektu end;

Jak już wspomniałem w poprzednim punkcie, nie jest też wymagane wywołanie metody macierzystej (za pomocą słówka inherited), lecz bez niego sterowanie przepływem komunikatu zakończy się wraz z procedurą, która go nie wywołała, np. posłużmy się komunikatem WM_MOUSEMOVE :

procedure TControl.WindowProc(var Msg : TMessage); begin // słówko inherited nie zostanie wywołane end; procedure TForm1.WMMouseMove(var Msg : TWMMouseMove); begin // metoda obsługi komunikatu end; procedure DefaultHandler(var Msg); begin // końcowa obsługa komunikatu end;

Jsk widać w powyższym przykładzie klauzula inherited nie została wywołana w procedurze okienkowej, więc metoda obsługi WMMouseMove i końcowa procedura DefaultHandler nie są wykonane.

Ad 13. SendMessage, PostMessage - jaka jest różnica?

Jak już wspomniałem w pkt. "Kolejka komunikatów i pętla pobierająca, co to jest?". istnieją komunikaty, które nie trafiają do kolejki, lecz bespośrednio do procedury obsługi danego okna. Komunikaty takie są szybciej przetwarzane ponieważ omijają kilkadzieśiąt lub kilkaset cykli procesora związanych z pobraniem komunikatu z kolejki i zazwyczaj jego wstępnym przetworzeniem. Wysyłanie komunikatów w ten sposób jest możliwe poprzez funkcję "SendMessage", i metodę "Perform" dostępną przez odwołanie kwalifikowane "." do komponentów wywodzących się z klasy "TControl" np.:

//... type TTest = class(TControl); public procedure SendMessageTest; procedure PerformTest; end; implementation procedure TTest.SendMessageTest; begin { Self to wskaźnik do obiektu klasy TTest, z którego ta procedura jest wywoływana } SendMessage(Self.Handle, WM_COŚ_TAM, 0, 0); end; procedure TTest.PerformTest; begin { Self to wskaźnik do obiektu klasy TTest, z którego ta procedura jest wywoływana } Self.Perform(WM_COŚ_TAM, 0, 0); end; end;

funkcja SendMessage została zdeklarowana w module "Windows.pas", natomiast metoda Perform znajduje się w pliku "Controls.pas". To ich nagłówki:

{------------------------------------------------------------------------------------} function SendMessage( hWnd: HWND; // uchwyt docelowego okna Msg: UINT; // komunikat do wysłania wParam: WPARAM; // pierwszy parametr komunikatu lParam: LPARAM // następny parametr komunikatu ) : LRESULT; stdcall;

1. Przeznaczenie :

Wysyła komunikat do okna, lub okien. Funkcja wywołuję procedurę okna i nie zwraca działania dopóki, dopóty procedura okna nie przetworzy komunikatu.

2. Rezultat funkcji :

Zwracaną wartością jest rezultat przetworzonego komunikatu i od niego zależy.

3. Opis parametrów :

hWnd - Identyfikuje okno, którego procedura okna ma otrzymać komunikat.

Jeżeli w parametrze tym podasz stałą HWND_BROADCAST, komunikat będzie wysłany do wszystkich okien na wierzchu w systemie, zawierających stan zablokowanych, lub okien niewidzialnych bez właściciela i okien rozwijanych; lecz komunikat nie jest wysłany do okien potomnych, czyli dzieci (ang. childs).

Msg - komunikat, który będzie wysłany.

wParam i lParam - dodatkowe specyficzne informacje, które niesie komunikat.

{------------------------------------------------------------------------------------} function Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;

- Zobacz opis funkcji SendMessage ponieważ działa ona w podobny sposób, z jedną małą różnicą. Otóż funkcja Perform jest wywoływana na rzecz obiektu klasy TControl (i pochodnych), który posiada uchwyt (jak każda kontrolka w Windows), więc wywołuję się ją bez tego parametru, gdyż jest on znany tej klasie od samego początku.

Przedstawiam przykład :

FResult := TControl.Perform(WM_KOMUNIKAT, 0, 0); FResult := SendMessage(TControl.Handle, WM_KOMUNIKAT, 0, 0); {------------------------------------------------------------------------------------} Funkcją przeznaczoną do umieszczania komunikatów w kolejce jest PostMessage. {------------------------------------------------------------------------------------} function PostMessage( hWnd: HWND; // uchwyt docelowego okna Msg: UINT; // komunikat do umieszczenia w kolejce wParam: WPARAM; // pierwszy parametr komunikatu lParam: LPARAM // następny parametr komunikatu ): BOOL; stdcall;

1. Przeznaczenie :

Umieszcza komunikat w kolejce aplikacji i natychmiast zwraca sterowanie, więc nie czeka ona na zakończenie obsługi wysyłanego przez nią komunikatu (jak robi to funkcja SendMessage). Rezultatem przez nią zwracanym jest wartość rożna od 0 (True), gdy komunikat został poprawnie umieszczony w kolejce, w przeciwnym wypadku zwracaną wartością jest 0 (False).

2. Rezultat funkcji :

- Jeśli wykonanie funkcji zakończyło się sukcesem, zwracana wartość jest niezerowa ( Boolean(PostMessage.Result) = True ). Jeżeli jednak powstał błąd, funkcja zwróci 0. Aby dostać rozszerzoną informację o błędzie, wywołaj funkcję Win32 Api "GetLastError".

3. Opis parametrów :

hWnd - identyfikuje okno, którego procedura okienkowa będzie odbierała komunikaty. Jednak dwie wartości przekazane za pośrednictwem tego parametru, mają specjalne znaczenie.

Znaczenie tych wartości :

HWND_BROADCAST Komunikat jest wysyłany do wszystkich okien znajdujących się na wierzchu (ang. top-level) w systemie, które zawierają stan niedostępne (ang. disabled), lub niewidzialne (ang. invisible) okna bez właściciela (ang. unowned windows). Komunikat nie jest wysyłany do okna, które posiada rodzica (ang. Parent).

Msg - specyfikuję komunikat, który zostanie umieszczony w kolejce.
wParam, lParam - specyfikują dodatkowe indormację o komunikacie.

{------------------------------------------------------------------------------------}

Jak uważnie czytasz ten punkt to zapewne już wiesz, jaka jest różnica między funkcją SendMessage, a PostMessage. Podpowiem, otóż różnią się one rezultatem i sposobem wysyłania komunikatu. Komunikat wysłany za pomocą SendMessage nie przechodzi przez kolejkę komunikatów, tylko trafia bezpośrednio do procedury okna i czeka na zwrócony przez nią rezultat, natomiast PostMessage umieszcza komunikat w kolejce informując przytym o jego poprawnym lub niepoprawnym umieszczeniu, a jego dalszy los już jej nie obchodzi (czyli natychmiast zwraca sterowanie wątkowi wywołującemu tą funkcje).

Jeżeli piszesz w VCL, to sposób wysyłania komunikat może mieć dla ciebie duże znaczenie, ponieważ komunikaty wysłane za pomocą SendMessage nie trafiają do kolejki, a więc nie wywołują zdarzenia OnMessage klasy TApplication - więc nie mogą być w nim obsługiwane.

Ad 14. Tworzenie kmunikatów użytkownika,

Istnieje dużo różnych komunikatów, które są wysyłane przez system, przez komponenty i aktualnie działające aplikacje, a ich obsługa pozwala na załatwienie prawie wszystkich spraw przez ciebie zadanych do wykonania. Są jednak sytuacje, w których należy się posunąć do stworzenia i wysłania własnego komunikatu, np. przy tworzeniu gry, komponentów, lub aplikacji wymagającej specyficznych komunikatów.

Kreując własny komunikat musisz zadbać o to, aby jego identyfikator nie kolidował z liczbami reprezentującymi komunikaty innych aplikacji. np.:

WM_KOMUNIKAT = 54;

Deklarując komunikat w powyższy sposób możesz być pewny, że jego stała WM_KOMUNIKAT zawierająca liczbę 54, koliduje z innym komunikatem w systemie, lub aplikacją nad którą pracujesz. Pytasz jak zmniejszyć, lub zniwelować to ryzyko do 0 ?.

To prostę, a pomoże nam w tym Delphi i jego stała WM_USER, której identyfikator jest górnym zakresem komunikatów, jakie nie są wykorzystane przez system Windows,

WM_KOMUNIKAT = WM_USER + 100;

lecz nie zmniejsza to do końca ryzyka - gdyż niektóre z komponentów lub klas w Delphi używa tego zakresu do nadawania identyfikatorów własnym komunikatom.

Czy istnieje lepsze rozwiązanie ?. Tak, możemy naprzykład zadeklarować swoją stałą komunikatu z wartością WM_USER + 200, lub większą. Co jednak zrobić gdy chcemy mieć pewność, że nie powstanie kolizja komunikatów. Otóż są jeszcze dwie możliwości by temu zapobiec. Pierwszą z nich jest wykorzystanie stałej WM_APP, której wartość powinna wystarczyć do prawidłowego przepływu komunikatów, lecz nadal możliwa jest kolizja.

Drugą możliwością i zdecydowanie najlepszą jest wykorzystanie funkcji,

function RegisterWindowMessage(lpString: PChar); UINT; stdcall;

która zwraca unikalny identyfikator komunikatu - w bieżącej sesji windows - na podstawie podanego wskaźnika do łańcucha znaków (a dokładniej do pierwszego znaku tego łańcucha).

Prawdopodobieństwo kolizji komunikatów, przy wykorzystaniu tej funkcji maleje niemal do zera i pozwala na komunikowanie się między sobą dwóch lub więcej aplikacji. Obsługa takich komunikatów jest trudniejsza, ponieważ może być zrealizowana jedynie, w ramach metody DefaultHandler, która jest końcową procedurą na drodze komunikatu, lub poprzez podmianę procedury okienkowej (za pomocą funkcji SetWindowLong). Wymóg ten może wydawać się nieco dziwny, lecz przestaje być takowy, gdy uświadomimy sobie fakt, że standartowa deklaracja identyfikatora dla stałej następuje na etapie edycji kodu :

const WM_KOMUNIKAT = WM_APP + 10; type TKlasa = class(TWinControl) public procedure WMKomunikat(var Msg : TMessage); message WM_KOMUNIKAT; end;

Gdy jednak używamy tej funkcji, identyfikator jest przydzielany już w czasie działania programu (po kompilacji). Jej rezultat nie może więc być przechowywany w stałej, a jedynie zmiennej.

Ad 15. Obiekt "TApplication", co przed nami ukrywa ?

W Delphi istnieje wielę obiektów, które ułatwiają dostęp do urządzeń i mechanizmów np. klasa TPrinter związana z drukarką, TStream, TScreen, TFileStream, TForm (chyba najbardziej przydatna), TRegistry, TMouse i wiele innych. Jestem wdzięczny (ty też powinieneś) programistom z Borlanda za tak duży wysyp obiektów, bez których programowanie staje się trudniejsze.

Zamierzam pokrótce przedstawić obiekt TApplication, gdyż jest on nierozłącznie związany z komunikatami i ukrywa przed programistą problemy związane z tworzeniem aplikacji, a czyni to w sposób uniwersalny. Jak wiadomo uniwersalność ta niesie ze sobą mniejszą wydajność programu napisanego z jej udziałem, gdyż obługa wyjątków, komunikatów i innych rzeczy realizowanych przez ten obiekt w sposób standardowy, wymaga wielu lini kodu.

TApplication jest odzwieciedleniem prawie całej funkcji WinMain, w której tworzone są okna i znajduje się główna pętla pobierająca komunikaty i wysyłająca je do właściwych okien. Wielu początkujących programistów nawet nie wie, co tak naprawdę dzieje się w gównym pliku projektu (*.dpr). To właśnie zamierzam wyjaśnić na przykładzie.

program Project1; uses Forms, Unit1 in 'Unit1.pas' {Form1}; {$R *.RES} begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.

Jak widzisz w głównym bloku programu wykonywane są linie:

Application.Initialize; - inicjacia aplikacji.

Application.CreateForm(TForm1, Form1); - tworzy formularz o typie podanym w pierwszym parametrze, a jego wskaźnik zwraca pod adresem drugiego prarametru. Wskaźnik do formularza, który został utworzony jako pierwszy, zapamiętany jest pod właściwością MainForm klasy TApplication. Formularz taki jest właścicielem wszystkich po nim utworzonych. Kolejność tworzenia formularzy ma więc niebagatelne znaczenie.

Application.Run - jest to główna procedura tej klasy, która wykonuje wstępne czynności związane z działaniem aplikacji i zawiera w sobie pętlę odpowiedzialną za pobieranie komunikatów z kolejki. Gdy pętla ta nie znajdzie w kolejce komunikatu, natychmiest wywołuje procedurę HandleMessage; która z kolei uruchamia zdarzenie OnIdle. OnIdle jest więc zdarzeniem, którego obsłużenie pozwala na wykonanie kodu w czasie - gdy aplikacja jest bezczynna.

Pętla ta działa dopóty, dopóki właściwość Terminated nie jest ustawiona na "TRUE".
Omawiana klasa zawiera również zdarzenie OnMessage, którego obsługa umożliwia przechwycenie wszystkich komunikatów trafiających do okien. Zły sposób tej obługi może narobić błędów, lub spowolnić pracę aplikacji. Pierwszym parametrem OnMessage jest rekord TMsg (nie posiadający pola Result jak w przypadku TMessage), następnym zaś jest zmienna Handled. Ustawiając Handled na wartość "TRUE" oznajmiasz, iż komunikat zostanie obsłużony w całości; domyślną wartością tego pola jest "FALSE", co powoduje że komunikat ten natrafia na dalsze metody jego obsługi (np. WindowProc, MetodaObiektu, DefaultHandler i dalsze).

Ważna uwaga : Wysłanie komunikatu za pomocą funkcji SendMessage, lub metody Perform (klasy TControl) nie generuje zdarzenia OnMeesage.

//... TFormularz = class(TForm) public procedure MyMessageProc(var Msg : TMsg; var Handled : Boolean); end; //... procedure TFormularz.MyMessageProc(var Msg : TMsg; var Handled : Boolean); begin case Msg of WM_QUIT : //przetworzenie komunikatu WM_NCHITTEST : //przetworzenie komunikatu end; Handled := True; // Obsługa wszystkich komunikatów kończy się na tej procedurze end; begin Application.OnMessage := MyMessageProc; end;

Ad 16. Różnica drogi komunikatu między VCL, a interfejsem Win32 API,

Dotąd nie przedstawiłem dokładniejszego schematu drogi komunikatu w Win32 Api i VCL, co niniejszym czynię. Poniżej znajduje się rysunek, który pokazuję (poglądowo) drogę komunikatu, od pętli pobierającej, aż do końcowej metody DefaultHandler.

[Tu był obrazek, ale niestety zaginął... - przyp. redaktora]

Przyglądając się rysunkowi, można zauważyć, że VCL wykonuję za nas dość dużo pracy, jednak ułatwienia przez niego dostępne wymagają wielu lini kodu, co z oczywistych przyczyn spowalnia pracę aplikacji. Spowolnienie to jest mało znaczące dla zwykłych programów. Jeżeli jednak zamiarem programisty czytającego ten artykuł jest pisanie gier, to radziłbym przesiąść się na Win32 Api.

Ad 17. VCL czy Win32 Api, pod który z nich pisać gry w Delphi ?

Dla niektórych programistów pisanie programów, lub komponentów, nie jest najpszyjemniejszym zadaniem i po pewnym czasie zaczyna nudzić. Co zrobić ?. Myślę, że wskażę proste rozwiązanie, napisz grę !. Gdy pisze się grę jest trudniej, lecz po skończeniu jest większa frajda, niż po napisaniu zwykłego programu.

Jeżeli zamierzasz napisać grę profesjonalną, to lepiej (uzbrój się w cierpliwość) przygotuj dla niej dokument, w którym umieścisz ogólne informacje, oraz opis wszystkich wykorzystanych algorytmów, procedur, stuktur danych i innych potrzebnych rzeczy. W dokumencie tym powinien się też znaleźć opis sposobu obsługi komunkatów z nią związanych (no chyba, że nie korzystasz z komunikatów). Zanim jednak to zrobisz musisz zdecydować, czy grę napiszesz przy pomocy biblioteki komponenów wizualnych (VCL), oferowanych przez Delphi, czy też nieco trudniejszego lecz bardziej wydajnego interfejsu Win32 Api. Musisz ustalić, czy warte będzie wykorzystanie Win32 Api, gdyż w niektórych przypadkach wystarczy VCL. Chciałbym przedstawić jakie (według mnie) są wady i zalety tych dwóch mechanizmów.

Zalety VCL :

- udostępnienie procedur zdarzeniowych, które ukrywają przed nami realizację obsługi komunikatów,
- wykonuje za nas podstawowe czynności związane z tworzeniem i rejestrowaniem okna, obsługą klawiatury itp.,
- udostępnia nam klasy, które wykonują za nas "czarną robotę", dlatego pozwala programiście zająć się ważniejszymi sprawami i szybciej skończyć pracę nad projektem,
- wygląd programu jest nam znany już na etapie projektowania,

Wady VCL :

- mniejsza szybkość wykonania kodu,
- mniejsza od Win32 Api możliwość ingerencji w działanie programu,

Zalety Win32 Api :

- szybkie działanie,
- większa możliwość kontroli nad programem,

Wady Win32 Api :

- wymaga od programisty większej wiedzy na jego temat,
- wygląd programu ujawnia się dopiero podczas działania,

Mimo przeważających zalet VCL, posiada on wadę, która go dyskfalifikuję do pisania gier profesionalnych. Osobiście polecam zrezygnowanie z VCL, gdyż ma on za zadanie ułatwić pisanie programów, w których szybkość nie ma takiego wielkiego znaczenia jak przy pisaniu gier, chociaż dobrze napisana gra pod VCL, może hulać nawet 200 fps (frames per second), wymaga to jednak zastosowania trudnych technik i dodania do jej kodu procedur i funkcji interfejsu Win32 Api.

Ad 18. Przykładowy projekt gry napisany w VCL wkorzystujący komunikaty,

W jednym z punktów, przedstawionych na łamach forum Game Design PL, napisałem że w artykue tym umieszczę kod mojej starej gry opierającej się na komunikatach, lecz po dłuższym zastanowieniu stwierdziłem, że gdybym chciał opisać algorytmy w niej wykorzystane to zajeło by to następne 30 stron, nie wspominając nawet o kodzie, którego jest trzy razy więcej. Postanowiłem więc napisać (od początku do końca) prostą gre i przedstawić jej ważniejsze struktury. Ponieważ nie chciałem przedłużać czasu w jakim ten artykuł ma się ukazać, grę napisałem na bieżąco, w przeciągu 7 godzin. Więc niech nikogo nie dziwi fakt, że nie jest to super produkcja ala "John Carmack". Ma ona jedynie przedstawić działanie niektórych mechanizmów opisanych w tym artykule. Zazwyczaj gry piszę w OpenGL, jednak tą napisłem bez wykorzystnia akceleratorów, ponieważ zajeło by to więcej czasu i miejsca (np. inicjowanie formatu pikseli, obsługa palety, ustawianie świateł i itp.).

Przy jej tworzeniu korzystałem z następujących ułatwień :

- prawie wszystkie zmienne są publiczne,
- statki kosmiczne gracza i przeciwników narysowałem odręcznie,
- nazwy i typy zmiennych nie były przemyślane,
- algorytmy robione w pośpiechu bez żadnych optymalizacji,
- bitmapy reprezętujące statki kosmiczne, nie są wyświetlane z przezroczystym tłem, dlatego też tło planszy jest jednolitego koloru,
- napisana jest przy użyciu VCL połączonego w minimalnym stopniu z Win32 Api,
- szybkość gracza jest iloczynem czasu jaki upływa od początku do końca głównej procedury renderującej, więc nie jest ustawiana dokładnie,
- wyświetlenie statku gracza jest zrobione asynchronicznie,

Resztę niedoróbek sam zdążysz zauważyć, gdyż nie warto było myśleć nad nimi, przy tego typu projekcie.

W grze kierujesz (statkiem kosmicznym) przy pomocy strzałek na klawiaturze, lub myszką i strzelasz w nadlatujące samoloty. Masz do wyboru trzy poziomy trudności i poziomy pasek przewijania, którym regulujesz interwał czasu po jakim następuje ruch nieprzyjacielskich statków.

Wyjaśnię teraz pokrótce znaczenie ważniejszych struktur, procedur i stałych gry :

const CMaxRight = 19; //Szerokość siatki CMaxBottom = 19; //Wysokość siatki CMaxMonsters = 60; //Maksymalna liczba przeciwników w wybranym poziomie truności CGameTimerId = 100; //identyfikator głównego zegara CArrowTimerId = 110; //identyfiator zegara przesuwającego pociski CCanMakeArrowTimerId = 120; //identyfikator zegara pozwalającego wystrzelić pocisk CGAMEOVER = 0; //flaga funkcji GameOver, która ją informuje o przegranej grze CWINNER = 1; //flaga funkcji GameOver, która ją informuje o wygranej grze CMakeArrowSound = 'MakeArrow.wav'; //stała zawierająca nazwę pliku z dźwiękiem CDestroyShipSound = 'Destroy.wav'; //stała zawierająca nazwę pliku z dźwiękiem // komunikat wysłany gdy siatka rząda, by pocisk został odświeżony na płótnie WM_DRAWARROW = WM_APP + 10; // gdy jest ustawione na True gracz ma możliwość wystrzału pocisku CanFire : Boolean = False; // zmienna inicjowana informująca program kiedy ma wywołać procedurę // która sprawdza czy gracz wygrał MakeWinner : Boolean = False; type TLevelDifficulty = (ldEasy, ldMedium, ldHard); //poziom trudności TScreenField = record //rekord przechowujący informację o jednej komórce siatki IsFree : Boolean; //informuję czy komórka nie zawiera w sobie przeciwnika PosX, PosY : Word; //przeliczona na piksele pozycja siatki end; TGameFields = record //rekord zawierający główne pola gry MonsterCount : Word; //liczba przeciwników zależna od poziomu trudności //bitmapy gry w kolejności : przeciwnika, tylnego bufora przeciwników i gracza MBmp, BackBuffer, PlayerBackBuffer : TBitmap; end; //siatka komórek TScreenTable = array [0..CMaxRight, 0..CMaxBottom] of TScreenField; TPlayer = record //rekod zawierający informację o graczu Pos : Integer; //pozioma "X" pozycja gracza wyrażana w pikselach PBmp, PArrowBmp : TBitmap; // bitmapy : gracza i pocisku KeyState : array[0..255] o

Tekst dodał:
Adam Sawicki
01.04.2006 15:36

Ostatnia edycja:
Adam Sawicki
01.04.2006 15:36

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#1 edytuj 01.04.2006 15:36 Adam Sawicki 64 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • ~Lukas 09 czerwca 2007 23:27
    Wspaniały artykuł. Dzięki. Dużo się dowiedziałem.
  • ~Lukas 09 czerwca 2007 23:27
    Wspaniały artykuł. Dzięki. Dużo się dowiedziałem.
  • ~madmax 06 marca 2008 14:14
    dzieki wam za komentarze :)
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)