2 PODSTAWY
2.1 Typy zmiennych Windows API
Omawianie API Windows'a niewątpliwie należy rozpocząć od zapoznania się z nowymi zdefiniowanymi typami. Wielu początkujących programistów widząc pierwszy raz kod stworzenia okna w Windows API, otwiera szeroko oczy i zastanawia się czy to co widzą to C++, który do tej pory znali? Jedną z przyczyn takiego stanu rzeczy jest właśnie fakt istnienia własnych typów Windows API. Z definicji w WinAPI wszystkie typy mają mieć nazwy dużymi literami, dlatego nazwom typów z C/C++ nadano nazwy z dużych liter. W plikach nagłówkowych Windows API będzie można odnaleźć takie deklaracje:
typedef int INT;
typedef short SHORT;
typedef long LONG;
typedef float FLOAT;
typedef double DOUBLE;
typedef char CHAR;//znak kodowania ASCII
typedef wchar_t WCHAR;//znak kdowoania UNICODE-16
#define VOID void //w przypadku typu void i const użyta jest makrodefinicja
#define CONST const
Dodatkowo dla wygody programistów C i C++ zdefiniowano krótsze nazwy dla typów bez znaku(unsigned):
typedef unsigned int UINT; //unsigned jest krótkim „U”
typedef unsigned short WORD; //wartość 16-bitowa, tzw. „słowo”
typedef unsigned short USHORT;
typedef unsigned long DWORD; //ten typ nazywa się „dwusłowo”(double word)
typedef unsigned long ULONG;
typedef unsigned char BYTE; //tutaj użyto słowa „bajt”
typedef unsigned char UCHAR;
W odmienny sposób w WinAPI zdefiniowany jest typ logiczny „bool”:
typedef int BOOL;
#define TRUE 1
#define FALSE 0
typedef BYTE BOOLEAN;
Jak widać typ BOOL to wcale nie bool z C++. Dlaczego tak? Prawdopodobnie w celach optymalizacyjnych, procesor będzie szybciej pracował na typie int niż jednobajtowym bool. Natomiast w celu użycia jednobajtowego bool zdefiniowano typ BOOLEAN. Oczywiście „true” i „false” także mogą być pisane z dużych liter.
#define NULL 0
Wartości 0 nadano także charakterystyczną nazwę NULL. Jest ona symbolicznie(umownie) używana jako wartość pustego wskaźnika, oraz pustego uchwytu(co to jest będzie za chwilę).
W Windows API zdefiniowano całą gamę nazw dla słówka kluczowego C++ „__stdcall”, czyli pascalowej konwencji wywołania funkcji. Wszystkie funkcje WinAPI używają właśnie tej konwencji, a oto definicje __stdcall, które można napotkać w Windows API:
#define WINAPI __stdcall
#define CALLBACK __stdcall
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define APIENTRY WINAPI
#define WINAPI_INLINE WINAPI
#define WINAPIV __cdecl //definicja konwencji C
W WinAPI zdefiniowano także typy wskaźnikowe. Przyjęło się zasadę, że typy wskaźnikowe zaczynają się na „P”(pointer - wskaźnik) lub „LP”:
typedef int* PINT;
typedef int* LPINT;
typedef unsigned int* PUINT;
typedef UINT* LPUINT;
typedef long* LPLONG;
typedef LONG* PLONG;
typedef FLOAT* PFLOAT;
typedef CHAR *PCHAR, *LPCH, *PCH; //LPCH i PCH rzadko się spotyka
typedef CHAR *LPSTR, *PSTR, *NPSTR; //wskaźniki są umownie buforami tekstu
typedef CONST CHAR *LPCSTR, *PCSTR; //wskaźniki na stałe bufory tekstu
typedef WCHAR *PWCHAR, *LPWCH, *PWCH; //odpowiedniki UNICODE
typedef WCHAR *LPWSTR, *PWSTR, *NWPSTR;
typedef CONST WCHAR *LPCWCHAR, *PCWCHAR;
typedef BOOL* PBOOL;
typedef BOOL* LPBOOL;
typedef BOOLEAN* PBOOLEAN;
typedef DWORD* PDWORD;
typedef DWORD* LPDWORD;
typedef WORD* PWORD;
typedef WORD* LPWORD;
typedef BYTE* PBYTE;
typedef BYTE* LPBYTE;
typedef void* PVOID;
typedef void* LPVOID;
typedef CONST void* LPCVOID;
Osobiście, jak i pewnie większość programistów nie preferuję używania typów wskaźnikowych z WinAPI, używając dobrze nam znanego * jako typu wskaźnikowego, niemniej jednak warto wiedzieć o typach wskaźnikowych WinAPI.
Kolejną grupą typów są tzw. „uchwyty”. W Windows API tworzymy pewne abstrakcyjne obiekty, do których odwołujemy się przez uchwyty do nich. Wspomniane uchwyty to po prostu wartości(identyfikatory) tych obiektów. W nazewnictwie uchwytów panuje zasada, że typy uchwytowe rozpoczynają się na „H”(handle - uchwyt). Poznawanie WinAPI to nic innego jak nauka posługiwania się takimi obiektami.
typedef void* HANDLE; //uchwyt do nieokreślonego obiektu
Każdy rodzaj obiektu, posiada swój typ uchwytu. W trakcie poznawania obiektów Windows API, poznasz kolejne typy uchwytów.
Obiekty Windows API o których mowa, nie mają nic wspólnego z obiektami klas języka C++. Windows API napisane jest w języku C, więc nie będzie mowy o obiektowych klasach w stylu C++.
Używanie typów z Windows API nie jest przymusowe, możemy cały czas używać typów z C++, gdyż dopiero poznane typy WinAPI, są tak naprawdę tylko nowymi nazwami, a kompilatorowi jest kompletnie obojętne czy użyjemy typu int(z C/C++) czy INT(z WinAPI). Niemniej jednak należy zapoznać się z powyższymi definicjami typów Windows API, aby wiedzieć jak wyglądają ich odpowiedniki w C/C++.
2.2 Makra Windows API
W plikach nagłówkowych WinAPI zdefiniowane są makra, które chcę omówić już na samym początku nauki.
Pierwsza makrodefinicja, którą przedstawię to WIN32_LEAN_AND_MEAN. Należy ją definiować przed dołączeniem nagłówka „windows.h”. Definicja ta skraca czas kompilacji projektu z „windows.h”, przez nie dołączanie rzadziej używanych plików nagłówkowych.
#define WIN32_LEAN_AND_MEAN //musi być przed windows.h
#include <windows.h>
WIN32_LEAN_AND_MEAN daje nieduże przyspieszenie. Obecnie istnieją inne, dużo skuteczniejsze sposoby na przyspieszanie kompilacji, np. technika użycia „precompiled headers”, która zmniejsza czas kompilacji prawie do minimum. Aczkolwiek nadal warto stosować definicję WIN32_LEAN_AND_MEAN.
W Windows API istnieją również makra definiujące wersje systemu pod jakim domyślnie ma działać nasz program. Definicje te należy umieszczać również przed dołączeniem plików WinAPI(windows.h). Dla przykładu jeśli zdefiniujmy sobie makro, które określi wersje dla Windows 98, to jeżeli funkcja której chcemy użyć nie istnieje jeszcze w Win98, spowoduje to, że nie będzie ona dostępna także w definicjach plików Windows API, w skutek czego dostaniemy błąd debbugera, mówiący, że nie może znaleźć nazwy(funkcji której chcemy). Makrodefinicje te powinny określać wersję minimalną, na jakiej powinien pracować program.
Makro WINVER określa wersję systemu Windows, natomiast makro _WIN32_IE określa wersję zainstalowanego Internet Explorer.
Oto wartości, które odpowiadają poszczególnym wersjom systemu Windows:
wersja systemu Windows |
wartość WINVER |
Windows 95 / 98 / ME |
0x400 |
Windows 2000 |
0x500 |
Windows XP / Server 2003 |
0x501 |
Windows XP SP2 / Server 2003 SP1 |
0x502 |
Windows Vista / Server 2008 |
0x600 |
A oto wartości _WIN32_IE dla wersji Internet Explorer:
wersja Internet Explorer |
wartość _WIN32_IE |
Internet Explorer 5.0 |
0x500 |
Internet Explorer 5.01 |
0x501 |
Internet Explorer 5.5 |
0x550 |
Internet Explorer 6.0 |
0x600 |
Internet Explorer 6.0 SP1 |
0x601 |
Internet Explorer 6.0 SP2 |
0x603 |
Internet Explorer 7.0 |
0x700 |
W ramach przykładu załóżmy, że chcemy aby aplikacja działała na minimalnym systemie Windows 2000, oraz Internet Explorer 6.0 SP1:
#define WINVER 0x500
#define _WIN32_IE 0x601
//makrodefinicje muszą być przed dołączeniem windows.h
#include <windows.h>
Ostatnią grupą pożytecznych makr, o których należy wiedzieć, są makra operujące na zmiennych.
Pierwsze makra, które przedstawię to: MAKEWORD i MAKELONG. Służą one do tworzenia wartości zmiennej z dwóch innych mniejszych zakresem wartości. Na pierwszy ogień weźmy MAKEWORD.
BYTE byte1, byte2; //dwie bajtowe liczby
WORD word=MAKEWORD(byte1,byte2); //połączenie dwóch bajtów w jedną wartość
Załóżmy, że mamy dowolne dwie liczby, typu jedno-bajtowego (BYTE = unsigned char). MAKEWORD możemy użyć jak funkcji. Jego działanie spowoduje połączenie dwóch wartości jedno-bajtowych w jedną dwu-bajtową, w ten sposób, że bajt odpowiadający mniejszym wartościom jest podawany jako pierwszym(byte1), natomiast bajt w WORD odpowiadający większym wartościom jako drugi parametr makra.
W rzeczywistości MAKEWORD jest jedynie makrodefinicją odpowiednich przesunięć bitowych. Drugim niemal identycznym makrem jest MAKELONG. MAKELONG różni się od MAKEWORD jedynie tym, że operuje na 2x większych wartościach. Jako argumenty przyjmuje dwie wartości 16-bitowe, zwracając wartość 32-bitową.
WORD word1=0x0566, word2=0x1233;
LONG lng=MAKELONG(word1,word2); //wartość lng będzie wynosić 0x12330566
Istnieje grupa makr działających odwrotnie do MAKEWORD i MAKELONG. Gdy te makra tworzą wartość z dwóch mniejszych wartości, inne z większej wartości wyciągają mniejsze porcje danych.
WORD word=0x1428;
BYTE lobyte=LOBYTE(word); //lobyte == 0x28
BYTE hibyte=HIBYTE(word); //hibyte == 0x14
Mając wartość 16-bitową(2-bajtową) makro LOBYTE zwróci wartość pierwszego bajta, opisującego niższe wartości, natomiast HIBYTE zwróci wartość drugiego bajta, opisującego wyższe wartości.
Z wartości 32-bitowej można także wyciągnąć dwie wartości 16-bitowe makrami: LOWORD i HIWORD.
DWORD dword=0xf0ac1428;
WORD loword=LOWORD(dword);//loword == 0x1428
WORD hiword=HIWORD(dword);//hiword == 0xf0ac
Makra te okazują się być przydatne w wielu zastosowaniach.
2.3 Funkcja główna
Pisząc w C++ z pewnością przyzwyczailiśmy się już do funkcji main(), wejściowej i wyjściowej w naszym programie. Windows API posiada własną funkcję główną, zastępującą main() z konsoli. Jej nazwa to WinMain(). Powinna być zbudowana w ten sposób:
int WINAPI WinMain(HINSTANCE hInst,HINSTANCE hPInst,LPSTR lpCmd,int nShow)
{
//kod programu
return 0;
}
Na pierwszy rzut oka wygląda dość skomplikowanie, ale zapewniam, że taka nie jest. Pierwszą rzeczą, która nam się rzuca w oczy, to większa ilość parametrów niż main().
Na pierwszy ogień bierzemy typ HINSTANCE. Cóż to za tajemniczy typ? Jego definicja:
typedef HANDLE HINSTANCE;
Typ ten zaczyna się na H, więc jest to uchwyt do obiektu, który tytułuje się „INSTANCE”(instancja). W roli wyjaśnień instancja aplikacji(modułu) to jej identyfikator w systemie Windows. Każdy uruchomiony program(moduł) dostaje swój oddzielny numer. Można więc powiedzieć, ze HINSTANCE to uchwyt aplikacji uruchomionej w systemie Windows. Jak widać uruchomione aplikacje(programy) w systemie z poziomu kodu to zwykłe obiekty Windows API.
Ale wróćmy do naszego pierwszego parametru hInst typu HINSTANCE w funkcji WinMain(), otóż jest uchwytem do naszej aplikacji, która właśnie została uruchomiona. Z parametru tego możemy odczytać uchwyt(identyfikator) naszej funkcji WinMain(programu) w systemie. Po co nam uchwyt naszej własnej aplikacji? Otóż czasami jest wymagany jako parametr do innych funkcji, dlatego warto go zachować jako zmienna globalna(lub przekazywać go do funkcji przez parametr).
Uchwyt naszego programu(HISTANCE) w każdej chwili możemy także uzyskać wywołując funkcję GetModuleHandle() z parametrem NULL. hInst=GetModuleHandle(NULL);
Ale to nie koniec, drugi parametr WinMain() jest także typu HINSTANCE, ale obecnie się go nie używa. Jest on pozostałością z czasów Windows'ów 16-bitowych. Obecnie ten parametr zawsze powinien wynosić NULL.
Trzeci parametr WinMain() jest typu LPSTR. Czy pamiętasz co to za typ? Jego odpowiednik C++ to nic innego jak: char*, wskaźnik na tablice znaków(tekst). Tekst z tego parametru jest ciągiem znaków wprowadzonym jeszcze przed uruchomieniem naszego programu. W innym rozdziale dokładniej wyjaśnię jak się posługiwać w praktyce tym parametrem. Jeśli znasz znaczenie parametrów konsolowej funkcji main(), zdradzę, że ten parametr pełni identyczna rolę.
Ostatni parametr WinMain() jest typu int. Argument ten określa sposób wyświetlania okna, zdefiniowany przez użytkownika. Jak wykorzystywać ten parametr zobaczymy już niebawem. Na razie może zobaczmy jak użytkownik komputera może wpływać na ten parametr. Mając skrót na aplikację, we właściwościach, na zakładce „Skrót” znajduje się pole „Uruchom”, to pole nie robi nic innego, jak określa wartość ostatniego parametru WinMain() podczas uruchamiania programu.
Zwróć uwagę jeszcze na słówko WINAPI, pomiędzy int, a WinMain. Już o nim wspominałem, gdy mówiłem o typach, jest to definicja pascalowej konwencji wywołania funkcji(odpowiednik z C++: __stdcall). Funkcje Windows API pracują właśnie w tej konwencji i system spodziewa się, że wejściowa WinMain() także będzie używać pascalowego wywołania funkcji, dlatego WinMain() musi być poprzedzone WINAPI(__stdcall).
Jeśli chodzi o wartość zwracaną przez WinMain() sytuacja tutaj się nie zmieniła w stosunku do main(), powinniśmy zwrócić 0, gdy nasz program zakończy się powodzeniem.
Tym sposobem przebrnęliśmy przez nową funkcję główną programu. Jak widzisz jej budowa nie jest tak skomplikowana, jakby się mogło wydawać na początku. Po prostu WinMain() przekazuje nam jeszcze oprócz tego co main(), identyfikator naszego programu w systemie Windows, oraz sposób pokazania okna i to cała tajemnica WinMain().
2.4 Budowa okna Windows
Czym byłby Windows bez swoich okien? Na czym byśmy dziaj klikali? Nie da się ukryć, że okno Windows jest podstawowym obiektem każdej aplikacji.
Jako użytkownik systemu Windows nie sposób nie spotkać się z oknami i ich obsługą. Działające programy tworzą swoje okna, które wyglądają inaczej, pomimo tego, wszystkie one mają cechy wspólne, pochodzące z systemu. Dla użytkownika programu okno to główny element, dzięki któremu, komunikuje się z większością aplikacji. Natomiast z punktu widzenia programisty okno to kolejny obiekt systemu Windows, pracujący w programie.
Wygląd okna Windows:
Pasek tytułu - charakterystyczny element okien. Jego wygląd zależy od ustawienia stylu graficznego w systemie. Pasek umożliwia przesuwanie okna użytkownikowi za pomocą myszki.
Ikona - grafika indywidualnie ustawiana z programu o wymiarach 16x16 pikseli. Zawsze znajduje się z lewej strony paska tytułu.
Tytuł - każde okno posiada swój własny ciąg znaków, określany jako tytuł okna, gdyż wyświetlany jest na pasku tytułu. Tytuł pełni także funkcję identyfikacyjną w systemie, posiadając jego tytuł, możemy odnaleźć go w systemie.
Przyciski nawigacji - przyciski znajdujące się na pasku tytułu, zawsze po jego prawej stronie. Rozróżniamy 3 podstawowe przyciski: minimalizacji, maksymalizacji i zamknięcia. Służą do modyfikowania stanu okna. Opcjonalnie istnieje jeszcze przycisk pomocy oznaczony jako „?”.
Menu - okno może być wyposażone w menu, z reguły tylko okna główne programu posiadają menu.
Krawędź - okno może posiadać krawędzie boczne, krawędź dolną, oraz jeżeli okno nie posiada paska tytułu, także górną. Krawędź może być jedynie elementem graficznym, jak również może posiadać właściwość zmiany rozmiarów okna.
Powierzchnia robocza - to powierzchnia przeznaczona dla programisty. Na niej powinien utworzyć resztę elementów przeznaczonych do pracy na oknie. Powierzchnia robocza często jest nazywana „obszarem klienta”.
Programista podczas tworzenia(z późniejszą możliwością zmian) określa cechy tworzonego okna, sposób jego działania i elementy z których będzie się składać. Najprostsze graficznie okno nie musi mieć żadnych elementów, może to być pusta przestrzeń robocza.
2.5 Hierarchizacja okien
Niemal zawsze w systemie istnieje jednocześnie wiele okien. Wskutek tego musi istnieć pewna ich hierarchia dziedziczenia, nie każdy użytkownik komputera zdaje sobie sprawę z tego faktu, ale nieświadomie na pewno się z nim spotkał. Każde okno posiada swojego rodzica, czyli inne okno, które jest nad nim nadrzędne. Zależność pomiędzy rodzicem, a jego potomkiem polega na sposobie wyświetlania okna potomnego. Otóż tylko powierzchnia okna rodzica(nadrzędnego) jest miejscem w którym może być wyświetlane okno potomne.
„Okno główne” posiada okno potomne o tytule „Okno potomne”, natomiast okno potomne posiada kolejne okno potomne . Jak widać na rysunku powierzchnia okna rodzica ogranicza obszar wyświetlania okna potomnego. Obrazowo można opisać to tak, że okno potomne będzie znajdować się tylko „w środku”, „we wnętrzu” okna rodzica.
Oknem głównym w systemie jest sam pulpit. Jest to okno najniżej w hierarchii, po którym mogą dziedziczyć kolejne okna. Okno główne programu zwykle jest właśnie potomkiem pulpitu, tzn. można przesuwać je po pulpicie.
W systemie istnieje jeszcze inna hierarchia wynikająca z istnienia pierwszej, otóż jest to hierarchia okien równorzędnych. Ponieważ istnieje możliwość(co zdarza się często) utworzenia kilku okien potomnych z jednego okna rodzica, okna te w hierarchii dziedziczenia są równe, dlatego trzeba je, także ułożyć w hierarchii kolejności wyświetlania na ich wspólnym oknie rodzica. Hierarchia ta określa, które okna mają być najpierw wyświetlane. Dzięki tej hierarchii ułożenie okien wygląda bardziej trójwymiarowo, dlatego mówi się o niej jak o trzecim wymiarze „Z” okien.
Modyfikując poprzedni rysunek hierarchii według dziedziczenia, trzeba dodać hierarchię wyświetlania dla okien równorzędnych(mających tego samego rodzica).
Samo ułożenie tej hierarchii, można porównywać do stosu kartek leżących na sobie, każda kartka ma swoja pozycję w tym stosie, kartka na samej górze(najwyżej w hierarchii) jest widoczna w całości przed innymi kartkami, natomiast na samym dole(najniżej) będzie przykryta kartkami przed nią, tak samo zachowują się okna.
Hierarchię dziedziczenia okien można modyfikować tylko podczas tworzenia i niszczenia okien, natomiast hierarchia wymiaru „Z” okien, zmienia się nieustannie. Wystarczy kliknąć na okno, a przeskoczy ono na najwyższe miejsce, stając się tym najbardziej widocznym, w swojej grupie.
Hierarchię wymiaru „Z” okien można jeszcze rozdzielić na dwie oddzielne grupy, otóż jest podział na okna zwykłe, oraz okna ze statusem „zawsze na wierzchu”. Okna „zawsze na wierzchu” są zawsze przed oknami zwykłymi.
Okno zwykłe może maksymalnie przejść na najwyższe miejsce w ustawieniu okien zwykłych, okno zwykłe nie może nigdy przejść przed jakiekolwiek okno „zawsze na wierzchu”, natomiast okna „zawsze na wierzchu”, nigdy nie mogą schować się za oknami zwykłymi.
2.6 Klasa okna
Po porcji teorii, przyszedł czas na rozpoczęcie tworzenia kodu pierwszego prawdziwego okna. Tworzenie okna nie jest rzeczą skomplikowaną, ale dość długą w zapisie.
Otóż istnieją w systemie wirtualne klasy okien. Taka klasa określa wbudowane cechy okna, które z reguły podczas działania programu nie powinny się zmieniać. Jest ona w pewnym sensie szablonem okna. Tworząc taką klasę, może ona stać się klasą widzianą globalnie w całym systemie, dzięki czemu inne programy będą mogły także z niej korzystać, ale zwykle tworzy się ją jedynie o zasięgu lokalnym, czyli będzie widziana tylko w naszym programie.
Programowo w Windows API taką klasę reprezentuje struktura WNDCLASSEX.
struct {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX, *PWNDCLASSEX;
UINT cbSize
Pole cbSize określa rozmiar tej struktury w bajtach. Zawsze należy przypisać tam wartość zwracaną przez operator sizeof() dla tej struktury. Takie pole określające rozmiar, występuje bardzo często w strukturach Windows API. Istnieje ono dlatego, żeby ustalić z ilu pól zbudowana struktura, ponieważ WNDCLASSEX nie zawsze miało te wszystkie pola. Wraz z postępem, ewolucją systemu Windows, dochodzą nowe pola do różnych struktur, wskutek czego program może być kompilowany dla Windows, w którym jeszcze nie było tych wszystkich pól, stąd jest potrzeba określenia długości struktury.
UINT style
W tym polu określa się style okien zbudowanych z tej klasy. Style te dotyczą działania okna. Każdy styl zapisuje się za pomocą odpowiedniej flagi(mówiłem o nich wcześniej), a flagi(style) można łączyć operatorem sumy bitowej „|”. Style zostaną omówione w następnym dziale.
WNDPROC lpfnWndProc;
Jest to wskaźnik na funkcję, obsługującą zdarzenia okien zbudowanych z tej klasy. Działanie tego pola jest kluczowe dla całej klasy okna. Domyślną funkcją w systemie obsługującą każdy komunikat w sposób domyślny, jest funkcja o nazwie DefWindowProc.
int cbClsExtra;
W tym polu określa się wielkość dodatkowych danych dla tworzonej klasy okna. W rozdziale o zaawansowanej pracy z oknami nauczymy się jak się z tego pola korzysta. Na razie będziemy przypisywać mu zawsze wartość 0.
int cbWndExtra;
To pole jest podobne w działaniu do powyższego, ale okno określa ilość dodatkowych danych dla każdego okna utworzonego z tej klasy. Dopóki nie będziemy wiedzieli jak z niego korzystać, przypisywać mu będziemy wartość 0.
HINSTANCE hInstance;
Ten typ mam nadzieję jest już doskonale znany, to uchwyt do programu. To pole określa w jakim programie znajduje się funkcja obsługująca zdarzenia. Powinno podawać się uchwyt do własnego programu, pobrany z parametru z WinMain() lub pobrany funkcją GetModuleHandle().
HICON hIcon;
HICON to uchwyt do obiektu ikony. To pole określa ikonę okien utworzonych z tej klasy. Ustawiając na NULL system automatycznie użyje domyślnej ikony. Dopóki nie poznamy obsługi zasobów, będziemy tu przypisywać NULL.
HCURSOR hCursor;
HCURSOR jest uchwytem do obiektu kursora. Każda klasa okien może dysponować swoim własnym kursorem. Będziemy tu przypisywać wartość zwróconą przez LoadCursor(NULL,IDC_ARROW), dokładniej funkcję poznamy przy omawianiu zasobów.
HBRUSH hbrBackground;
HBRUSH jest kolejnym uchwytem, tym razem do graficznego obiektu pędzla. Z tym obiektem zapoznamy się dopiero podczas omawiania graficznej części Windows API. Pole to określa pędzel jakim będzie rysowane tło okna utworzone z tej klasy. Można także użyć domyślnych wartości pochodzących z systemu. Domyślną wartością systemowego tła jest flaga: COLOR_BACKGROUND(flagę tą trzeba rzutować na HBRUSH).
LPCSTR lpszMenuName;
Ten parametr to nazwa menu, które będzie pokazywane na oknach utworzonych z tej klasy. Na razie będziemy podawali NULL, co oznacza, że nie wiążemy żadnego menu z klasą okna.
LPCSTR lpszClassName;
Ten parametr określa nazwę tworzonej klasy okna. Klasy okna nie są typowymi obiektami Windows API, więc nie posiadają swoich uchwytów, a do identyfikacji w systemie używa się ich nazw.
HICON hIconSm;
Opcjonalnie oprócz dużej ikony z pola hIcon, można także używać także małej ikony, lub podać NULL, wtedy wszędzie zostanie użyta ikona z pola hIcon.
Po wypełnieniu tej struktury. Należy ją zarejestrować w systemie, czyli zgłosić istnienie takiej klasy okien. Robi się to funkcją RegisterClassEx():
ATOM RegisterClassEx(CONST WNDCLASSEX *lpwcx);
Funkcja przyjmuje wskaźnik na strukturę WNDCLASSEX, którą rejestruje w systemie. Zwracany ATOM jest identyfikatorem(innym niż uchwyt) klasy okna. Jeśli nie wynosi NULL, rejestracja okna przebiegła poprawnie, nasza klasa okna istnieje w systemie. Od odrejestrowywania zarejestrowanej już klasy w systemie, służy funkcja UnregisterClass(), jednak nie ma potrzeby jej wywoływania, gdyż wraz z wyjściem z programu system automatycznie odrejestruje zarejestrowane klasy.
BOOL UnregisterClass(LPCTSTR lpClassName,HINSTANCE hInstance);
Parametr lpClassName to nazwa zarejestrowanej klasy, a hInstance to uchwyt do programu, który zarejestrował klasę, czyli parametr podany w polu hInstance struktury WNDCLASSEX.
A oto przykładowy kod rejestracji klasy w systemie:
//rejestracja klasy okna
WNDCLASSEX wndcls;//struktura z klasą okna
wndcls.cbSize=sizeof(WNDCLASSEX);//rozmiar struktury
wndcls.style=0;//bez styli
wndcls.lpfnWndProc=DefWindowProc;//domyślna obsługa zdarzeń okna
wndcls.cbClsExtra=0;//brak dodatkowych danych
wndcls.cbWndExtra=0;//tu także
wndcls.hInstance=hInst;//uchwyt programu, pobrany z parametru WinMain()
wndcls.hIcon=NULL;//ikona domyślna z systemu
wndcls.hCursor=LoadCursor(NULL,IDC_ARROW);//kursor także
wndcls.hbrBackground=(HBRUSH)COLOR_BACKGROUND;//domyslne tło okien
wndcls.lpszMenuName=NULL;//brak menu
wndcls.lpszClassName="Example_ClassName";//nazwa klasy okna
wndcls.hIconSm=NULL;//brak oddzielnej małej ikony
RegisterClassEx(&wndcls);//rejestracja klasy
2.7 Tworzenie okna
Mając zarejestrowaną klasę, okna można utworzyć systemowy obiekt okna. Jak wcześniej powiedziałem sama klasa okna jest jedynie szablonem, natomiast dopiero okno Windows jest obiektem. Ale okno powstaje na podstawie klasy okien. Porównując tworzenie okna z programowaniem obiektowym w C++, można powiedzieć, że klasa okna Windows jest tylko deklaracją klasy C++, a okno Windows jest jej obiektem utworzonym w pamięci.
Funkcja tworząca okno to CreateWindowEx(). Przyjmuje aż 12 parametrów:
HWND CreateWindowEx(DWORD dwExStyle,LPCSTR lpClassName,LPCSTR lpWindowName, DWORD dwStyle, int x,int y,int nWidth, int nHeight,HWND hWndParent, HMENU hMenu,HINSTANCE hInstance,LPVOID lpParam);
DWORD dwExStyle
Pierwszy parametr to rozszerzone style tworzonego okna. O stylach będzie w następnym rozdziale.
LPCSTR lpClassName
Ten parametr to nazwa klasy okna, z której zostanie utworzone okno. To ta klasa, którą wcześniej rejestrowaliśmy. Z takiej klasy możemy utworzyć dowolną ilość okien.
LPCSTR lpWindowName
Trzeci parametr określa nazwę(tytuł) okna. Tytuł ten będzie wyświetlany na pasku tytułu.
DWORD dwStyle
Ten parametr jest kombinacją flag określających podstawowe style wyglądu okna. Style będą omówione w następnym rozdziale.
int x, int y
Te 2 parametry określają miejsce w którym ma pojawić się okno. Wartości x i y są współrzędnymi układu pikseli na oknie rodzica(pulpicie), w którym będzie lewy-górny róg tworzonego okna. Wartości te mają znaczenie tylko w momencie stworzenia okna, wystarczy, aby użytkownik przesunął okno, a współrzędne te zmienią się. Domyślną wartością jest CW_USEDEFAULT, wtedy system wybierze miejsce pojawienia się okna.
int nWidth, int nHeight
Te wartości określają wymiary całego okna, szerokość(nWidth) i wysokość(nHeight) w pikselach.
HWND hWndParent
Jest to uchwyt do okna rodzica tworzonego okna. Podanie wartości NULL lub HWND_DESKTOP, spowoduje, że okno będzie potomkiem pulpitu.
HMENU hMenu
Uchwyt do menu okna. Wartość NULL oznacza brak menu.
HINSTANCE hInstance
Uchwyt programu do którego ma należeć okno.
LPVOID lpParam
Dane które może przekazać programista. NULL oznacza brak takich danych.
Zwracana wartość HWND
Funkcja zwraca uchwyt do utworzonego okna. Jeśli zostanie zwrócone NULL, okno nie zostało poprawnie utworzone.
Funkcja CreateWindowEx() posiada makro o nazwie CreateWindow(). Obecnie funkcja CreateWindow() w plikach nagłówkowych WinAPI nie istnieje i jest tylko makrem na CreateWindowEx() bez pierwszego parametru(jest ustawiony na 0).
Okno można po utworzeniu w dowolnym momencie zniszczyć(usunąć z pamięci). Robi to funkcja o nazwie DestroyWindow().
BOOL DestroyWindow(HWND hWnd);
HWND hWnd
Uchwyt do niszczonego okna. Jeśli okno posiada okna potomne, najpierw one zostaną zniszczone.
Zwracana wartość BOOL
Jeśli okno zostało usunięte poprawnie zwraca TRUE, jeśli wystąpił błąd FALSE.
2.8 Pętla zdarzeniowa
We wprowadzeniu do tematu modelu zdarzeniowego aplikacji, zaprezentowałem schemat budowy aplikacji w modelu zdarzeniowym, podzieliłem go na 3 części: inicjalizację, przetwarzanie zdarzeń i zwalnianie zasobów. W części inicjalizacji nalezy tworzyć okna i to co jest niezbędne do dalszego działania programu. Następnie następuje część zdarzeniowa, i tutaj zatrzymamy się na dłużej. Jak to działa? Dobre pytanie. Otóż nie jest to żadna magia. Istnieje pewna pętla, która przetwarza wszystkie zdarzenia, które wystąpią w naszym programie. Może ona wyglądać różnie, klasycznie konstruuje się ją w ten sposób:
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Pierwsze co widzimy to typ MSG. Otóż jest to struktura, która przechowuje informacje o zdarzeniu(komunikacie), który wystąpił w danej chwili.
typedef struct {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG;
HWND hwnd
Każdy komunikat(zdarzenie) jest adresowany do konkretnego okna. To pole określa adresata danego komunikatu. Gdy zrobimy w naszym programie kilka okien, przetwarzane komunikaty będą wędrować do różnych okien, dlatego potrzebne jest to pole, aby było wiadomo do jakiego okna ma wędrować komunikat.
UINT message
Pole to określa rodzaj zdarzenia(komunikatu). Każdy komunikat(zdarzenie) ma swój własny numer, który jest zdefiniowany odpowiednia flagą. W systemie istnieją setki rodzajów komunikatów, np. kliknięcie na okno, naciśniecie przycisku, poruszenie myszką, i mnóstwo innych, podczas poznawania Windows API, część z nich poznasz.
WPARAM wParam, LPARAM lParam
WPARAM i LPARAM to typy używane umownie przy komunikatach, są zdefiniowane z typu unsigned int, ale z uwzględnieniem wielkości architektury procesora, w ten sposób:
typedef _w64 unsigned int WPARAM;
typedef _w64 unsigned int LPARAM;
Dzięki temu wielkości zmiennych zależą od architektury procesora pod jaką pisany jest program, tak jak wielkości wskaźników i można na nich wykonywać działania. Jeśli chodzi o przeznaczenie tych zmiennych w komunikatach, to są to jakieś dodatkowe dane, informacje dla danego komunikatu, zależne od rodzaju komunikatu. Np. wartość informacji lParam komunikatu o kliknięciu na oknie będzie zawierała informacje o współrzędnych kursora w chwili kliknięcia. Pole wParam nazywa się „parametrem o wyższym znaczeniu” zdarzenia, a lParam nazywa się „parametrem o niższym znaczeniu”.
DWORD time
Czas w momencie, którym zdarzenie miało miejsce. Pole mało istotne, rzadko używane.
POINT pt;
A to pole przechowuje, współrzędne kursora w momencie zaistnienia zdarzenia. To pole także, rzadko jest używane. POINT jest struktura, która opisuje punkt. Jej prostą budowę przedstawię przy innej okazji.
Zatem po zapoznaniu się z budową komunikatu(zdarzenia) w systemie Windows, można zapoznać się z pętlą zdarzeniową. Otóż jak już wcześniej wspominałem odpowiedzialna jest ona za przetwarzanie komunikatów w programie. Jak to działa? Otóż każdy program w systemie posiada swoją własną kolejkę komunikatów. Cóż to takiego ta kolejka? Gdy wystąpi jakieś zdarzenie w systemie, np. użytkownik komputera kliknie myszką, system ustala na jakim oknie kliknął użytkownik, jak już to wie, ustala do jakiego programu należy to okno, następnie do kolejki komunikatów tego programu wysyła komunikat o kliknięciu na danym oknie. Więc co to ta kolejka komunikatów? Otóż wirtualne miejsce gdzie przechowywane są komunikaty, które wysłał do programu system lub jakiś inny program. Kolejka komunikatów należy do systemu i to on ją obsługuje. Ale dlaczego to kolejka? Dlatego, że gdy system wyśle więcej komunikatów, ustawią się one w pewnej kolejności, można to porównać do ludzi stojących w kolejce. Naszym zadanie, a właściwie zadaniem naszej pętli zdarzeniowej, jest pobieranie komunikatów z tej kolejki, ściślej dokładnie tym zajmuje się funkcja GetMessage(). Wyciąga ona z kolejki komunikat i umieszcza go w strukturze komunikatu MSG. W ten sposób zdarzenie z kolejki komunikatów, trafiło do struktury MSG, czyli znajduje się już w programie.
BOOL GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax);
LPMSG lpMsg
Pierwszy parametr to wskaźnik na strukturę MSG, do której zostanie zapisany komunikat świeżo wyciągnięty z kolejki.
HWND hWnd
Tutaj możemy ustawić, że funkcja będzie pobierać tylko komunikaty adresowane do konkretnego okna, którego należy podać uchwyt w tym parametrze. Podanie NULL, spowoduje, że funkcja będzie pobierać wszystkie komunikaty, bez względu na okno do, którego jest adresowany.
UINT wMsgFilterMin, UINT wMsgFilterMax
Te wartości określają przedział liczbowy komunikatów, które będzie pobierała funkcja. Funkcjonalność rzadko stosowana.
Zwracana wartość BOOL
Warto zwrócić uwagę, że wartość którą zwróci funkcja, będzie warunkiem, który zakończy, bądź będzie kontynuował pętlę przetwarzającą zdarzenia. Zwrócenie wartości FALSE, przerwie działanie pętli, w ten sposób rozpoczynając ostatni etap życia programu, czyli zwalnianie zasobów, zwykle przerwanie pętli powoduje, już rozpoczęcie zamykania programu. Wartość TRUE będzie kontynuować przetwarzanie zdarzeń. Otóż istnieje specjalny komunikat, który pobrany przez GetMessage() powoduje, że zwraca FALSE. Komunikat ten służy właśnie do zatrzymywania pętli komunikatów, a tym samym do zamykania programu.
W przypadku, gdy w kolejce komunikatów nie ma żadnego zdarzenia, GetMessage() zatrzymuje wykonywanie programu i czeka aż jakiś komunikat się pojawi() oszczędzając zasoby procesora. Istnieje funkcja PeekMessage(), która jedynie sprawdza, czy w kolejce komunikatów jest komunikat, jeżeli jest pobiera go, ale jeżeli nie ma, nie zatrzymuje wykonywania programu, tak jak robi GetMessage(). To czy jest czy nie, sygnalizuje wartością jaką zwraca. Funkcja ta używana jest głównie w grach, gdyż nie zatrzymuje wykonywania kodu gry, a gra może zająć się rysowaniem grafiki, czy generowaniem kolejnej klatki.
Po pobraniu komunikatu przez GetMessage(), należy go przetworzyć. Tę robotę odwalają funkcje: TranslateMessage() i DispatchMessage(). Ta pierwsza ma za zadanie odpowiednio przetworzyć komunikat, jeżeli tego wymaga. Istnieje pewna grupa zdarzeń, które są generowane na podstawie innych, właśnie przez TranslateMessage(). Funkcja ta nie jest niezbędna do poprawnego działania pętli zdarzeniowej, ale bez niej mogą nie być generowane wszystkie zdarzenia, o których będzie mowa podczas nauki Windows API.
BOOL TranslateMessage(const MSG* lpMsg);
const MSG* lpMsg
Jedyny parametr to wskaźnik na strukturę komunikatu MSG, na której ma dokonać operacji. Zdarzenie powinno być wyciągnięte z kolejki komunikatów, funkcją GetMessage() lub PeekMessage().
Zwracana wartość BOOL
Jeżeli komunikat należał do grupy komunikatów, z których należało wygenerować kolejny komunikat, funkcja zwróci TRUE. Jeżeli komunikat nie wymagał żadnej akcji ze strony TranslateMessage() funkcja zwróci FALSE.
Druga funkcją, mającą ważniejsze zadanie w procesie przetwarzania zdarzeń jest funkcja DispatchMessage(). Jej zadaniem jest już wykonanie kodu obsługującego dane zdarzenie. Jej zadanie polega na tym, że według pola hwnd w strukturze komunikatu, szuka okna do którego wędruje komunikat, a gdy znajdzie wywołuje funkcję tego okna(właściwie to klasy okna) odpowiedzialną za wykonanie kodu zdarzenia dla danego okna. Każda klasa okna posiada funkcję, która jest odpowiedzialna za przetwarzanie komunikatów. Po czym pętla wraca do początku, czyli pobiera kolejny komunikat i tym sposobem „pompuje” komunikaty przez cały czas istnienia naszego programu.
LRESULT DispatchMessage(const MSG* lpmsg);
const MSG* lpmsg
Podobnie jak TranslateMessage() parametrem jest struktura komunikatu, który należy przetworzyć.
Zwracana wartość LRESULT
Typ LRESULT zwracają funkcje, które wykonują kody zdarzeń(funkcje zdarzeniowe). W przypadku DispatchMessage(), zwraca ona wartość, którą zwróciła funkcja wykonująca kod zdarzenia.
2.9 Procedura zdarzeniowa okna
Tworząc własne okno, przydało by się mieć nad nim kontrolę, a dokładniej, móc obsługiwać zdarzenia, które będą związane z oknem, tzn. które będą adresowane do okna, które zostało stworzone w programie, a wcześniej jego klasa zarejestrowana. Obsługiwanie zdarzeń polega na napisaniu własnej funkcji, która będzie przetwarzać komunikaty. Otóż DispatchMessage(), jak wiemy wywołuje funkcję okna, odpowiedzialną za przetworzenie komunikatów wędrujących do tego okna, w tym podrozdziale napiszemy sobie właśnie taką funkcję, którą będzie wywoływała funkcja DispatchMessage().
W tym momencie warto sobie przypomnieć o jednym z pól struktury klasy okna WNDCLASSEX, a mianowicie o polu: WNDPROC lpfnWndProc. Podczas wypełniania tego pola definiuje się funkcję, jaka będzie odpowiedzialna za przetwarzanie komunikatów wędrujących do okien utworzonych z tej klasy. Poprzednio w przykładzie podałem tutaj funkcję o nazwie DefWindowProc(). Jest to systemowa funkcja, która zajmuje się przetwarzaniem komunikatów w sposób domyślny dla systemu. Naszym zadaniem będzie napisanie własnej funkcji, w której sami umieścimy kod jaki ma się wykonać podczas zaistnienia określonego zdarzenia. W naszej funkcji możemy się skupić na obsłudze kilku, kilkunastu, co najwyżej kilkudziesięciu rodzajów komunikatów, ale do naszej funkcji mogą wędrować setki różnych komunikatów, dlatego w przypadku, gdy nasza funkcja nie obsługuje danego komunikatu, musimy go nadal przetworzyć przez DefWindowProc(), niech system się martwi co robić z komunikatami nieobsługiwanymi przez naszą funkcję zdarzeniową.
Funkcja zdarzeniowa musi mieć zawszę taką samą postać. Jej typ to LRESULT, musi posiadać 4 ściśle określone parametry, oraz rzecz jasna być w konwencji wywołania funkcji pascalowych. Załóżmy, że nazwiemy funkcję zdarzeniową „WndEventsProc”, wtedy funkcja będzie wyglądać w ten sposób(nazwy parametrów, oczywiście także mogą być dowolne):
LRESULT CALLBACK WndEventsProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
return DefWindowProc(hwnd,msg,wParam,lParam);
}
Parametrami są właściwie wartości ze struktury komunikatu MSG. DispatchMessage() w momencie wywołania funkcji zdarzeniowej, w parametrach przekazuje kolejno pierwsze 4 wartości pól ze struktury komunikatu, który przetwarza. W roli przypomnienia:
HWND hwnd
Uchwyt okna, do którego adresowany był komunikat, czyli uchwyt okna do którego odnosi się dane zdarzenie.
UINT msg
Rodzaj zdarzenia.
WPARAM wParam, LPARAM lParam
Dodatkowe parametry określające zdarzenie.
Zwracana wartość LRESULT
W przypadku zwracanych wartości, gdy zdarzenie obsługuje ta funkcja, w większości komunikatów powinno się zwrócić wartość 0, w przypadku gdy funkcja nie obsługuje zdarzenia, należy zwrócić to co zwróciła DefWindowProc().
Wróćmy, do obsługi konkretnych zdarzeń. Parametr UINT msg, określa rodzaj zdarzenia. Najwygodniej jest rozbić wykonywanie kodu poszczególnych zdarzeń przy pomocy switch().
LRESULT CALLBACK WndEventProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
switch(msg)
{
case WM_ZDARZENIE_1:
//obsługa zdarzenia typu 1
break;
case WM_ZDARZENIE_2:
//obsługa zdarzenia typu 2
break;
default://obsługa pozostałych zdarzeń przez DefWindowProc()
return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;//po wykoaniu zdarzenia 1 lub 2 zwracane jest 0
}
W powyższej funkcji zdarzeniowej obsługiwane są 2 rodzaje komunikatów: WM_ZDARZENIE_1 i WM_ZDARZENIE_2. Gdy przetwarzany komunikat będzie typu WM_ZDARZENIE_1 lub WM_ZDARZENIE_2, zostanie wykonany kod, któregoś z tych zdarzeń i zwrócona do DispatchMessage() wartość 0. Natomiast jeżeli zdarzenie nie będzie obsługiwane przez funkcję zdarzeniową, czyli nie będzie uwzględnione w możliwości switch(), zostanie wykonany sposób domyślny instrukcji, czyli zdarzenie zostanie obsłużone przez funkcję systemową DefWindowProc() w sposób domyślny.
Każdy nieobsługiwany typ zdarzenia(komunikatu) przez funkcję zdarzeniową musi być obsłużony przez funkcję obsługi domyślnej. Nie może dojść do sytuacji, w której komunikaty nieuwzględnione w funkcji zdarzeniowej nie będą w ogóle obsługiwane.
Po napisaniu funkcji zdarzeniowej dla okien klasy, należy ją powiązać z odpowiednią klasą okna WNDCLASSEX, w polu WNDPROC lpfnWndProc, przypisując nazwę własnej funkcji zdarzeniowej.
wndcls.lpfnWndProc=WndEventsProc;
2.10 Zdarzenie przerwania pętli
Pierwszym rodzajem komunikatu(zdarzenia) jaki przedstawię, będzie zdarzenie, o którym już wcześniej wspominałem: komunikat przerwania pętli zdarzeniowej. Jego flaga kodowa to: WM_QUIT(wszystkie flagi zdarzeń, komunikatów zaczynają się na WM_).
Gdy GetMessage() pobierze ten komunikat zwraca wartość FALSE, zaleca się wtedy przerwanie wykonywania pętli, gdyż ten komunikat ma służyć wyjściu z programu, czyli wejściu w ostatnią fazę życia programu, przygotowania do zamknięcia.
Jednak ten komunikat, nigdy sam nie zostanie wygenerowany. Interfejs użytkownika nie wysyła tego typu komunikatu, jeśli chcemy zakończyć pętlę zdarzeniową musimy sami umieścić ten komunikat w kolejce komunikatów. Najprostszym rozwiązaniem i najsłuszniejszym jest posłużenie się funkcją PostQuitMessage(), która właśnie to służy do umieszczania WM_QUIT, w kolejce komunikatów w programie, w którym została wywołana.
void PostQuitMessage(int nExitCode);
int nExitCode
Parametr ten powinien określać wartość, którą zwraca WinMain(). Dokładniej wartość tego parametru zostanie przypisana polu: WPARAM wParam, w komunikacie WM_QUIT. Zaleca się także, aby w tym wypadku WinMain() zwracała tę wartość wParam struktury MSG.
Najczęstszym miejscem, w którym umieszcza się przerwanie pętli, czyli wywołanie tej funkcji, jest komunikat zamknięcia głównego okna w programie. W momencie gdy użytkownik zamknie okno główne w programie, zaleca się wysłać WM_QUIT, aby przerwać działanie pętli zdarzeniowej.
2.11 Zdarzenia życia okna
Istnieją kolejne zdarzenia, które wysyłane są w kluczowych momentach życia okna. Te zdarzenia informują o:
Stworzeniu okna (WM_CREATE)
Próby zamknięcia okna (WM_CLOSE)
Niszczeniu okna, usuwaniu z pamięci (WM_DESTROY)
WM_CREATE
Zdarzenie WM_CREATE jest wysyłane do okna podczas tworzenia go, jest to prawdopodobnie pierwsze zdarzenie, które dociera do okna. Dokładnie zdarzenie to wykona się w momencie trwania CreateWindowEx(), funkcja ta tworzy okno w pamięci, a następnie właśnie wykonuje kod tego zdarzenia. W obsłudze tego zdarzenia umieszcza się kod, który tworzy kolejne elementy po stworzeniu okna, np. zawartość tworzonego okna. Funkcja domyślna DefWindowProc() nie wykonuje żadnych czynności podczas obsługi tego zdarzenia. Porównując ten komunikat do programowania obiektowego w C++, zdarzenie to ma dokładnie takie samo zadanie jak konstruktor klasy C++. Można powiedzieć, że zdarzenie to jest konstruktorem obiektu okna w Windows API.
wParam - w tym komunikacie wParam nie posiada żadnych informacji.
lParam - ten parametr to wskaźnik na strukturę CREATESTRUCT, w której są umieszczone informacje na temat stworzonego okna. Informacje te to wartości podane podczas wywołania CreateWindowEx().
typedef struct tagCREATESTRUCTA {
LPVOID lpCreateParams;
HINSTANCE hInstance;
HMENU hMenu;
HWND hwndParent;
int cy;
int cx;
int y;
int x;
LONG style;
LPCSTR lpszName;
LPCSTR lpszClass;
DWORD dwExStyle;
} CREATESTRUCTA, *LPCREATESTRUCTA;
LPVOID lpCreateParams
Jest to wartość, która została podana w ostatnim parametrze funkcji CreateWindowEx(), podczas tworzenia okna, do którego należy ten komunikat. Dzięki temu polu można przekazać jakąś wartość wartość.
HINSTANCE hInstacne
Uchwyt do programu, który został podany podczas tworzenia okna w CreateWindowEx().
HMENU hMenu
Uchwyt do menu podany jako parametr do CreateWindowEx().
HWND hwndParent
Okno rodzica tworzonego okna, wartość podana do CreateWindowEx().
int cy,int cx,int x,int y
cy - szerokość, cx - wysokość utworzonego okna. x i y określa współrzędną położenia okna na oknie rodzica.
LONG style
Style podstawowe okna. Wartość z CreateWindowEx().
LPCSTR lpszName
Wskaźnik na tekst z tytułem okna. Wartość z CreateWindowEx().
LPCSTR lpszClass
Wskaźnik na tekst z nazwą klasy okna, z której zostało utworzone okno. Wartość z CreateWindowEx().
DWORD dwExStyle
Style rozszerzone okna. Wartość z CreateWindowEx().
WM_CLOSE
Zdarzenie WM_CLOSE oznacza, że okno powinno zostać zamknięte. Wysyłane jest, gdy użytkownik kliknie na przycisk [X] na pasku tytułowym okna, lub wciśnie kombinację Alt+F4. Funkcja DefWindowProc() domyślnie wywołuje funkcję DestroyWindow() w obsłudze tego zdarzenia, usuwając okno. Obsługując sami to zdarzenie, możemy umieścić w nim kod odpowiedzialny za akcję, który ma się wykonać gdy użytkownik chce zamknąć okno. Większość edytorów(np. Word lub Notatnik) w obsłudze tego komunikatu pyta się użytkownika „czy zapisać zamiany w dokumencie przed zamknięciem”, a dopiero po odpowiedniej akcji wywołują DestroyWindow(). W przypadku, gdy w obsłudze tego komunikatu nie umieścimy możliwości wywołania DestroyWindow(), zabierzemy użytkownikowi możliwość zamknięcia okna(nie będzie mógł zamknąć okna z poziomu elementów systemowych okna).
wParam - ten parametr, w tym zdarzeniu nie posiada żadnych informacji.
lParam - ten parametr również nie przekazuje żadnej informacji.
WM_DESTROY
Zdarzenie WM_DESTORY jest przeciwieństwem WM_CREATE. Zostaje ono wywołane w momencie niszczenia okna, podczas wywołania funkcji DestroyWindow(). Funkcja DestroyWindow() najpierw wykonuje kod tego zdarzenia, następnie usuwa obiekt okna z systemu. W momencie wykonywania WM_DESTROY nie można już zatrzymać usunięcia okna, w obsłudze tego komunikatu powinny znaleźć się czynności odpowiedzialne, za zwolnienie zasobów używanych przez okno. Porównując ten komunikat do programowania obiektowego w C++, zdarzenie to ma dokładnie takie samo zadanie jak destruktor klasy C++. Można powiedzieć, że zdarzenie to jest destruktorem obiektu okna w Windows API.
wParam - ten parametr, w tym zdarzeniu nie posiada żadnych informacji.
lParam - ten parametr również nie przekazuje żadnej informacji.
W tym momencie nauki Windows API, mogę już przedstawić kod stworzenia okna głównego w programie.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
BOOL InitWindow(HINSTANCE);//rejestruje klasę okna i tworzy je
//funkcja zdarzeniowa okna, odpowiedzialna za przetwarzanie zdarzeń:
LRESULT CALLBACK WndEventProc(HWND,UINT,WPARAM,LPARAM);
//funkcja główna WinMain()
int WINAPI WinMain(HINSTANCE hInst,HINSTANCE,LPSTR lpCmd,int nShow)
{
//etap inicjalizacyjny programu - tworzenie okna głównego
if(InitWindow(hInst)==FALSE) return 0;
//pętla zdzrzeniowa - rozpoczęcie etapu zdarzeniowego
MSG msg;//struktura zdarzenia(komunikatu)
while(GetMessage(&msg,NULL,0,0))//pobranie zdarzenia z kolejki
{
TranslateMessage(&msg);//przetworzenie komunikatu (zdarzenia)
DispatchMessage(&msg);//wykonanie kodu zdarzneia (komunikatu)
//w odpowiedniej funkcji zdarzeniowej okna do którego jest wysłany
}
return msg.wParam;//wyjście w programu(zamknięcie procesu)
//msg.wParam jest wartością parametru z PostQuitMessage()
}
//funkcja rejestruje i tworzy okno
BOOL InitWindow(HINSTANCE hInstance)
//parametr - uchwyt programu przekazany z WinMain()
{
//ZNACZENIE UŻYTYCH STYLI WYJAŚNIONE JEST W ROZDZIALE O OKNACH
WNDCLASSEX wndcls;//klasa okna
char strClassName[]="Klasa okna programu";//nazwa klasy okna
wndcls.cbSize=sizeof(WNDCLASSEX);//rozmiar struktury
wndcls.hInstance=hInstance;//uchwyt programu rejestrującego klasę
wndcls.lpszClassName=strClassName;//przypisujemy nazwę klasy
wndcls.lpfnWndProc=WndEventProc;//własna funckja zdarzeniowa okna
//style:
wndcls.style=CS_BYTEALIGNCLIENT | CS_BYTEALIGNWINDOW | CS_DBLCLKS |
CS_DROPSHADOW | CS_HREDRAW | CS_VREDRAW;
wndcls.hIcon=NULL;//ikona główna (NULL-domyślna)
wndcls.hIconSm=NULL;//ikona mniejsza
wndcls.hCursor=LoadCursor(NULL,IDC_ARROW);//kursor(domyslna strzałka)
wndcls.hbrBackground=(HBRUSH)COLOR_BACKGROUND;//kolor tła okien tej klasy
wndcls.lpszMenuName=NULL;//menu (NULL-brak)
//dodatkowe dane, których nie używamy
wndcls.cbClsExtra=0;
wndcls.cbWndExtra=0;
if(RegisterClassEx(&wndcls))//rejestracja klasy okna
{
//stworzenie obiektu okna (uchwyt - HWND)
HWND hwnd=CreateWindowEx(WS_EX_CLIENTEDGE | WS_EX_COMPOSITED/*od XP*/,
strClassName,"Example",WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,CW_USEDEFAULT,640,480,NULL,NULL,hInstance,NULL);
//parametry okna: 640x480 tytuł: "Example"
if(hwnd!=NULL) return TRUE;//jeśli okno jest stworzone zwracam powodzenie
}
return FALSE;//jesli coś poszło nie tak, zwracam niepowodzenie
}
//funkcja zdarzeniowa okna(funkcja jest ustawiona w polu lpfnWndProc WNDCLASSEX)
LRESULT CALLBACK WndEventProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
//parametry:
//HWND hwnd - uchwyt okna adresata komunikatu, czyli zawsze uchwyt okna głównego
//UINT msg - typ zdarzenia, flagi typów zdarzeń zaczynają się na WM_
//WPARAM wParam, LPARAM lPara - informacje dodatkowe
{
switch(msg)//rozdzielnie poszczeglnych typów zdarzeń
{
case WM_CREATE://WM_CREATE - zdarzenie utworzenia okna (konstruktor okna)
//własna obsługa tego zdarzenia
break;
case WM_DESTROY://WM_DESTROY - zdarzenie zniszczenia okna (destruktor okna)
PostQuitMessage(0);//w obsłudze zniszczenia okna głównego, wywołuję
//funckję przerywającą pętlę zdarzeniową, aby przerwać wykonywanie progrmu
break;
default://gdy funkcja ta nie obsługuje przysłanego typu komunikatu, nalezy go
return DefWindowProc(hwnd,msg,wParam,lParam);//obsłużyć funkcją domyślną
}
return 0;//po obsłudze komunikatu zdefiniwanego w tej funkcji zwracam 0
}
2.12 Symulowanie zdarzeń
W tym podrozdziale chciałbym przedstawić dwie podobne funkcje Windows API, które służą do tego samego, ale realizują to w trochę inny sposób. Otóż mowa o bardzo przydatnej czynności, jaką jest wykonanie kodu zdarzenia, czy zasymulowanie jakiegoś zdarzenia. Funkcje o których mowa to SendMessage() i PostMessage().
LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
Co powinno być widać na pierwszy rzut okna, to budowa tej funkcji, otóż jej parametry i zwracana wartość są dokładnie takie same jak w funkcjach zdarzeniowych. Jest to spowodowane tym, że funkcja w pewnym sensie spełnia właśnie rolę funkcji zdarzeniowej. Jak ona dokładnie działa? Otóż wywołując ją, to tak jakbyśmy wywoływali funkcję zdarzeniową okna, którego uchwyt podaje się jako pierwszy parametr. Funkcja wykona kod zdarzenia i zwróci wartość wykonania tego zdarzenia.
HWND hWnd
Uchwyt adresata zdarzenia, czyli uchwyt okna, którego wykona się funkcja zdarzeniowa.
UINT Msg
Typ zdarzenia, np. WM_CREATE, WM_DESTORY, WM_CLOSE lub WM_QUIT.
WPARAM wParam, LPARAM lParam
Informacje, parametry dodatkowe dla komunikatu, zdarzenia.
Zwracana wartość LRESULT
Zwraca wartość zwróconą przez funkcję zdarzeniową wykonującą kod zdarzenia.
Działanie PostMessage() trochę się różni od SendMessage(). Otóż PostMessage() nie wykonuje zdarzenia od razu. Funkcja ta jedynie wysyła zdarzenie do kolejki komunikatów, dlatego, aby zdarzenie się wykonało trzeba poczekać, aż GetMessage() lub PeekMessage() pobierze to zdarzenie z kolejki komunikatów. PostMessage() sprawdza się tam, gdzie nie trzeba zdarzenia wykonywać od razu, a wystarczy je jedynie umieścić w kolejce komunikatów. PostMessage() nie czeka na przetworzenie komunikatu, jedynie wstawia zdarzenie do kolejki i kończy swoje działanie.
BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
HWND hWnd
Okno do, którego adresowany jest komunikat. Podanie wartości HWND_BROADCAST, spowoduje wysłanie komunikatu do wszystkich okien typu „zawsze na wirzchu”. Podanie wartości NULL, spowoduje nieokreślenie adresata komunikatu, np. WM_QUIT nie wymaga podawania okna, do którego adresuje się komunikat, wystarczy, że pobierze je GetMessage().
UINT Msg
Typ zdarzenia.
WPARAM wParam, LPARAM lParam
Dodatkowe parametru komunikatu.
Zwracana wartość BOOL
Jeśli komunikat zostanie poprawnie umieszczony w kolejce komunikatów, funkcja zwróci TRUE, jeśli wystąpi błąd FALSE.
Co daje wykonywanie zdarzeń przez SendMessage() i PostMessage()? Otóż bardzo dużo, przede wszystkim można symulować dowolne zdarzenie w systemie, np. udawać że użytkownik kliknął w jakieś miejsce lub nacisnął jakiś przycisk.
2.13 Komunikat okienkowy
Czasem w programie zachodzi potrzeba szybkiego wyświetlenia pewnej informacji lub zapytania się użytkownika o jakąś akcję. Windows API oferuje bardzo przydatną funkcję, pokazującą okienko. Takie niewielkie okienko wyświetla określony tekst, posiada określony tytuł, może mieć obrazek ikony, oraz pewne określone przyciski.
Wykonywanie kodu zatrzymuje się na funkcji MessageBox() dopóki nie zostanie naciśnięty przycisk na wyświetlonym okienku komunikatu. Dopiero po zniknięciu okienka kod programu jest dalej wykonywany. Okno z tą właściwością nazywa się oknem dialogowym.
int MessageBox(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType);
HWND hWnd
Uchwyt do okna rodzica komunikatu okienkowego. Można podać NULL, wtedy rodzicem będzie pulpit. Gdy podamy uchwyt do okna(parametr inny niż NULL), podczas wyświetlania okienka komunikatu, nie będzie można powrócić do okna rodzica, gdyż na czas wyświetlania okienka stanie się ono nieaktywne.
LPCSTR lpText
Wskaźnik na bufor z tekstem wiadomości na okienku.
LPCSTR lpCaption
Wskaźnik na bufor z tekstem tytułu okienka, wyświetlanym na pasku tytułu.
UINT uType
Kombinacja flag określających działanie okienka i jego wyglądu. Flagi dla tego parametru zaczynają się na „MB_”.
Zwracana wartość int
Funkcja zwraca identyfikator przycisku, który nacisnął użytkownik.
Przykład programu, który składa się z wywołania okienka komunikatu:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInst,HINSTANCE,LPSTR lpCmd,int nShow)
{
MessageBox(NULL,"Treść komunikatu tekstowego Windows API","Wiadomość!",0);
return 0;
}
MessageBox() w ostatnim parametrze ustala różne właściwości, poprzez ustawienie jednej z dostępnych flag dla tego parametru.
Okienko może posiadać ikonę wyświetlaną z lewej strony, każda ikona posiada własną flagę.
W zależności od wersji i ustawień systemu Windows, graficzny wygląd ikon jest różny.
Na okienku możemy także ustawić rodzaje przycisków jakie użytkownik będzie miał do wyboru. Mamy do dyspozycji 7 możliwych kombinacji. Niezdefiniowanie żadnej z flag przycisków, oznacza sam przycisk OK.
Flaga kombinacji |
Przyciski |
MB_OK |
„OK” |
MB_OKCANCEL |
„OK” i „Anuluj” |
MB_RETRYCANCEL |
„Ponów próbę” i „Anuluj” |
MB_YESNO |
„Tak” i „Nie” |
MB_YESNOCANCEL |
„Tak”, „Nie” i „Anuluj” |
MB_ABORTRETRYIGNORE |
„Przerwij”, „Ponów próbę” i „Ignoruj” |
MB_CANCELTRYCONTINUE |
„Anuluj”, „Ponów próbę”, „Kontynuuj” (flaga dostępna od Windows 2000) |
Funkcja zwraca jaki przycisk nacisnął użytkownik, oto flagi zwracanych przycisków.
Flaga przycisku |
Przycisk |
IDOK |
OK |
IDCANCEL |
Anuluj |
IDRETRY |
Ponów próbę |
IDYES |
Tak |
IDNO |
Nie |
IDABORT |
Przerwij |
IDIGNORE |
Ignoruj |
IDTRYAGAIN |
Ponów próbę z MB_CANCELTRYCONTINUE |
IDCONTINUE |
Kontynuuj |
Są także flagi określające, który z przycisków ma być domyślnie aktywny, tzn. który będzie posiadał pole, którym nawiguje się przyciskiem TAB(fokus). Domyślnie jest to zawsze pierwszy przycisk(od lewej strony).
Flaga aktywnego przycisku |
Numer przycisku od lewej |
MB_DEFBUTTON1 |
Pierwszy jest aktywny |
MB_DEFBUTTON2 |
Drugi jest aktywny |
MB_DEFBUTTON3 |
Trzeci jest aktywny |
Kolejną grupą flag dla MessageBox() to flagi określające siłę modalności okna.
Flaga modalności |
Efekt |
MB_APPLMODAL |
Domyślny efekt(gdy nie zdefiniujemy żadnej z flag modalności). Okienko zablokuje okno rodzica(nie będzie można kliknąć na nie). |
MB_SYSTEMMODAL |
Oprócz efektu z MB_APPLMODAL, okienko komunikatu będzie typu „zawsze na wierzchu”, więc nie będzie się go dało przesłonić zwykłymi oknami. |
MB_TASKMODAL |
Gdy pierwszy parametr będzie wynosił NULL, okienko zablokuje wszystkie okna w programie. Jeśli pierwszy parametr będzie określał okno, flaga będzie miała taką samą właściwość jak MB_APPLMODAL. |
Ostatnią grupą flag są flagi o różnym znaczeniu.
Flaga MessageBox() |
Znaczenie |
MB_DEFAULT_DESKTOP_ONLY |
Flaga ma znaczenie od Windows 2000. Jeśli program został uruchomiony na koncie innego użytkownika, niż obecnie jest wyświetlany, MessageBox() poczeka, aż z powrotem nie zostanie wyświetlony pulpit użytkownika, na którym został uruchomiony program, żeby wyświetlić okienko wiadomości. |
MB_RIGHT |
Tekst jest wyświetlany od prawej do lewej strony. |
MB_RTLREADING |
Przycisk zamknięcia jest z lewej strony, a tytuł z prawej na pasku tytułu. |
MB_TOPMOST |
Okienko będzie posiadać stan „zawsze na wierzchu”. |
MB_SETFOREGROUND |
Okno komunikatu jest oknem pierwszoplanowym. |
MB_SERVICE_NOTIFICATION |
Wyświetla okienko nawet jeśli żaden użytkownik się nie zalogował(z poziomu poza użytkownika). Pierwszy parametr musi być ustawiony na NULL. |
W ramach przykładu, można się spytać czy użytkownik chce na pewno zamknąć program. W obsłudze komunikatu WM_CLOSE należy umieścić kod wyświetlający komunikat okienkowy, z odpowiednim pytaniem, oraz według przycisku, który wybierze użytkownik podjąć odpowiednią akcję.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
BOOL InitWindow();
LRESULT CALLBACK WndEventProc(HWND,UINT,WPARAM,LPARAM);
LRESULT CloseWnd(HWND hwnd);//funkcja pyta się czy zamknąc program
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
if(InitWindow()==FALSE) return 0;
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
BOOL InitWindow()
{
//uchwyt programu pobrany za pomocą funkcji GetModuleHandle()
HINSTANCE hInstance=GetModuleHandle(0);
WNDCLASSEX wndcls;
char strClassName[]="Klasa okna programu";
wndcls.cbSize=sizeof(WNDCLASSEX);
wndcls.hInstance=hInstance;
wndcls.lpszClassName=strClassName;
wndcls.lpfnWndProc=WndEventProc;
wndcls.style=CS_BYTEALIGNCLIENT | CS_BYTEALIGNWINDOW | CS_DBLCLKS |
CS_DROPSHADOW | CS_HREDRAW | CS_VREDRAW;
wndcls.hIcon=NULL;
wndcls.hIconSm=NULL;
wndcls.hCursor=LoadCursor(NULL,IDC_ARROW);
wndcls.hbrBackground=(HBRUSH)COLOR_BACKGROUND;
wndcls.lpszMenuName=NULL;
wndcls.cbClsExtra=0;
wndcls.cbWndExtra=0;
if(RegisterClassEx(&wndcls))
{
HWND hwnd=CreateWindowEx(WS_EX_CLIENTEDGE | WS_EX_COMPOSITED,
strClassName,"Example",WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,CW_USEDEFAULT,640,480,NULL,NULL,hInstance,NULL);
if(hwnd!=NULL) return TRUE;
//w przypadku błędu utworzenia okna, wyświetlenie treści błędu
else MessageBox(NULL,"Błąd tworzenia okna.",NULL,MB_OK | MB_ICONERROR);
}
//w przypadku błędu rejestarcji klasy okna, wyświetlenie treści błędu
else MessageBox(NULL,"Błąd rejestarcji klasy.",NULL,MB_OK | MB_ICONERROR);
return FALSE;
}
LRESULT CALLBACK WndEventProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
switch(msg)
{
case WM_CLOSE://obsługa komunikatu WM_CLOSE
return CloseWnd(hwnd);//obsługa tego zdarzenia-wykonuje się funckja CloseWnd
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;
}
LRESULT CloseWnd(HWND hwnd)
{
int UserResult=MessageBox(hwnd,"Zamknąć?",":(",MB_ICONQUESTION | MB_YESNO);
if(UserResult==IDYES) DestroyWindow(hwnd);// użytkownik wybrał tak - zamykam
return 0;
}
33