Naszym pierwszym wielkim wyzwaniem będzie stworzenie i narysowanie zwykłego okienka. Wielkim, bo nie jest to kwestia jednej instrukcji, jak być może sobie wyobrażasz. Trochę trzeba się będzie nagimnastykować... Najpierw musimy stworzyć i zarejestrować klasę okna, następnie stworzyć samo okno, potem wyświetlić je i wreszcie stworzyć tak zwaną pętlę komunikatów.
Zacznijmy od stworzenia nowego projektu. Zakładam, że korzystasz z Dev-C++. W starszych wersjach Deva był odrębny typ projektu - "WinMain project", obecnie jest tylko "Windows Application". Jeśli go wybierzemy, to uzyskamy gotowy szablon windowsowego programu, który będzie potrafił wszystko, co opiszemy sobie za chwilę w tej części kursu. Ale jeśli przeskoczymy ten etap teraz, to później nic a nic nie zakumamy z następnych zagadnień, więc zalecam na razie przeczytać tę część, później przeczytać jeszcze raz i jeszcze raz, aż wreszcie coś zaczniemy łapać ;-).
Tak więc wybieramy typ projektu "Empty Project" i wchodzimy sobie od razu do opcji projektu. W pierwszej zakładce ("General") szukamy ramki "Type" i wybieramy "Win32 GUI". W tej wersji Dev-C++ (kiedy piszę te słowa, aktualna wersja to 4.9.9.2) na tym kończy się ustawianie opcji i możemy od razu przejść do właściwego kodzenia.
Funkcja WinMain
WinMain jest to windowsowy odpowiednik main, jak się zapewne domyślasz. Jej budowa jest następująca:
#include <windows.h>
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )
{
return 0;
}
Plik
windows.h (razem z innymi powiązanymi z nim nagłówkami) zawiera dosłownie wszystko, co będzie nam potrzebne w kursie WinAPI i będziemy go zawsze dołączać, kiedy zechcemy napisać cokolwiek pod windowsa. O funkcji WinMain trochę już mówiłem, ale wypadałoby dokładniej opisać jej argumenty. Parametr hInstance to uchwyt naszej aplikacji. Tłumacząc na ludzki język: numer jej wystąpienia (instance). Oznacza to, że jeśli uruchomimy np. Notatnik, a następnie nie zamykając go otworzymy Notatnik jeszcze raz, to pierwsze okno Notatnika będzie miało inny numer identyfikacyjny, niż drugie.
W WinAPI typy danych zaczynające się przedrostkiem H oznaczają uchwyty. Zmienne takie służą do indentyfikowania rozmaitych obiektów. Obiektem takim może być też menu (typ uchwytu - HMENU), czcionka (typ uchwytu - HFONT), okno (typ uchwytu - HWND), blok pamięci (typ uchwytu - HGLOBAL lub HLOCAL) i wiele, wiele innych.
Argument
hPrevInstance powinien zawierać uchwyt poprzedniego wystąpienia aplikacji, ale od wersji systemu Windows 95 wynosi on zawsze
NULL.
Specjalna wartość NULL jest po prostu równa zero. Zwykle wykorzystuje się ją do oznaczenia, że wskaźnik lub uchwyt jest pusty.
Argument
lpCmdLine zawiera linię poleceń, z jakiej został uruchomiony nasz program. Jest to niestety pojedynczy string, tak więc trzeba go będzie pociachać, jeśli zechcemy wyciągnąć z niego konkretne parametry dla programu i nie ma takiego luksusu, jak w "zwykłej"
main.
Typ LPSTR to po prostu synonim typu char*. Ogólnie wszystkie typy z przedrostkiem P lub LP oznaczają w WinAPI wskaźniki.
Wreszcie
nCmdShow określa, jaki powinien być stan okna naszego programu. Powinniśmy podać ten argument dalej, do funkcji
ShowWindow pokazującej okno - o tym później.
Cóż jeszcze może być tajemniczego w powyższym kodzie? Instrukcji
return 0 chyba nie trzeba omawiać; może najwyżej dziwnie wygląda to WINAPI w deklaracji
WinMain. Określa ono tzw. konwencję wywoływania funkcji, ale o tym na razie nie musisz nic wiedzieć :-).
Funkcja MessageBox
Kod powyżej oczywiście nie robi nic konkretnego, podobnie jak pusta funkcja
main w programie konsolowym. Ale możemy sobie coś dodać. Okna naszej aplikacji jeszcze nie mamy, ale możemy sobie wywalić na ekran jakiś fajny komunikat. Robimy to funkcją
MessageBox.
Składnia:
MessageBox( hWnd, lpText, lpCaption, uType )
A oto i style, określające przyciski, jakie będą widoczne w okienku komunikatu:
Jeśli nie wybierzesz żadnego z tych przycisków, domyślnie zostanie wybrany MB_OK. Dostępne ikonki to:
Jeśli nie wybierzesz żadnej z tych ikon, komunikat będzie bez ikony :-). Żeby wskazać, który przycisk jest domyślny (z grubą, czarną ramką dookoła), dodajesz jeden ze stylów:
MB_DEFBUTTON1, MB_DEFBUTTON2, MB_DEFBUTTON3, MB_DEFBUTTON4. Domyślnie wybierany jest
MB_DEFBUTTON1.
Możesz określić modalność komunikatu:
Istnieje jeszcze kilka możliwych stylów, ale nie będą nam one na razie potrzebne. Zresztą połowy z wymienionych też nie będziesz pewnie zbyt często używał :-). Warto je jednak znać. Resztę doczytasz sobie w innych źródłach, jak mawiają profesorowie :-).
Funkcja
MessageBox może zwrócić następujące wartości:
Często nie musimy w ogóle sprawdzać, co
MessageBox zwraca (np. gdy mamy tylko jeden przycisk :-) ). Tak też będzie w naszym przykładziku, który po prostu wyświetla wiadomość. Poniższy kod wpisujemy oczywiście wewnątrz funkcji
WinMain:
#include <windows.h>
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )
{
MessageBox( NULL, "To jest wiadomość.", "Wiadomość", MB_ICONINFORMATION | MB_OKCANCEL );
return 0;
}
Komunikat z tego przykładu będzie miał przyciski OK i Anuluj, ale nie sprawdzamy, który z nich został naciśnięty. Mogłem nie dodać żadnego przycisku, wtedy byłby tylko OK, ale chciałem pokazać, jak się łączy poszczególne style (operatorek OR, rzecz jasna, aczkolwiek zwykłe dodawanie + też może być).
Klasa okna
Jak już wspomniałem, żeby stworzyć okienko aplikacji, takie z prawdziwego zdarzenia, musimy najpierw zarejestrować jego klasę. W tym celu wypełniamy pola struktury WNDCLASSEX. Jest to dość spora struktura - moglibyśmy skorzystać z prostszej, WNDCLASS, ale tamta nie ma pola na małą ikonkę, a my bardzo chcemy mieć małą ikonkę w programie (proszę nie bić :-)). Oto co wpisujemy w kolejne pola:
A więc ogólnie nasze wypełnianie struktury, której nadaliśmy malowniczą nazwę
wc (niby od ''window class'') będzie wyglądało jakoś tak:
LPSTR NazwaKlasy = "Klasa Okienka";
WNDCLASSEX wc;
wc.cbSize = sizeof( WNDCLASSEX );
wc.style = 0;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hbrBackground =( HBRUSH )( COLOR_WINDOW + 1 );
wc.lpszMenuName = NULL;
wc.lpszClassName = NazwaKlasy;
wc.hIconSm = LoadIcon( NULL, IDI_APPLICATION );
Skoro wypełniliśmy już nasz formularz rejestracyjny, pozostaje tylko wysłać go do Wysokiej Komisji Rejestrującej Klasy :-) i modlić się o pozytywne rozpatrzenie prośby. Należy również liczyć się z możliwością odmowy rejestracji (co w praktyce się nie zdarza, Komisyja łaskawa jest, ale programistyczny savoir vivre wymaga przewidywania absolutnie każdej sytuacji) - wtedy nie pozostanie nam nic innego, jak tylko wywalić nieprzyjemny komunikat i zakończyć program:
if( !RegisterClassEx( & wc ) )
{
MessageBox( NULL, "Wysoka Komisja odmawia rejestracji tego okna!", "Niestety...",
MB_ICONEXCLAMATION | MB_OK );
return 1;
}
Jeżeli jednak komisja w postaci funkcji
RegisterClassEx wyraziła zgodę, zwracając wartość niezerową, to możemy śmiało przystąpić do kroku następnego, a mianowicie budowy naszego okienka. Do tego zaś służy funkcja
CreateWindowEx. Oto jakie są składniki każdego szanującego się okna:
Składnia:
CreateWindowEx( dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam )
Najczęściej używane style okienka, podawane jako dwStyle, to:
Przyjrzyj się kilku przykładom zastosowania powyższych stylów:
Wiemy już prawie wszystko na temat budowy okienka, a więc - do dzieła. Wywołujemy CreateWindowEx, która przy odrobinie szczęścia powinna nam zwrócić uchwyt do nowego okna:
HWND hwnd;
hwnd = CreateWindowEx( WS_EX_CLIENTEDGE, NazwaKlasy, "Oto okienko", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 240, 120, NULL, NULL, hInstance, NULL );
if( hwnd == NULL )
{
MessageBox( NULL, "Okno odmówiło przyjścia na świat!", "Ale kicha...", MB_ICONEXCLAMATION );
return 1;
}
Okno wreszcie gotowe! Jedyne, co nam pozostało, to uczynić je widocznym. A tym zajmie się funkcja ShowWindow, oraz UpdateWindow (ta ostatnia żeby upewnić się, że okno zostało poprawnie narysowane):
ShowWindow( hwnd, nCmdShow );
UpdateWindow( hwnd );
Jak zapewne pamiętasz, parametr
nCmdShow wzięliśmy od funkcji
WinMain, ta zaś otrzymała go od systemu, a konkretnie od użytkownika, który we właściwościach skrótu może określić, czy program ma być uruchamiany w okienku normalnym, zmaksymalizowanym itp. Oczywiście, możesz zamiast przypisywania tego parametru dać cokolwiek innego, wtedy użytkownik nie będzie miał po prostu wpływu na to, jak okno będzie wyglądało tuż po uruchomieniu programu.
Pętla komunikatów
Pewnie nie jesteś zachwycony, że nasz program mimo tylu zaklęć nadal nie działa jak należy (o ile miałeś odwagę go skompilować na tym etapie - ja bym nie miał ;-)). No cóż, brakuje jeszcze najważniejszego - pętli komunikatów. Jest to najzwyklejsza w świecie pętla, która ma za zadanie przechwytywać wszelkie komunikaty, jakie system wyśle do naszej aplikacji. Takim komunikatem może być kliknięcie myszą, wciśnięcie jakiegoś klawisza albo zamknięcie okna. Wszystko zależy od tego, jakie konkretnie komunikaty chcemy przechwytywać.
Najpierw musimy sobie stworzyć globalną (na zewnątrz
WinMain) zmienną do przechowywania komunikatów:
Następnie robimy wspomnianą pętelkę (to już wewnątrz
WinMain):
while( GetMessage( & Komunikat, NULL, 0, 0 ) )
{
TranslateMessage( & Komunikat );
DispatchMessage( & Komunikat );
}
return Komunikat.wParam;
Funkcja
GetMessage, jak sama nazwa wskazuje, pobiera kolejne wiadomości od systemu, a dokładniej z tzw. kolejki wiadomości (''message queue''). Jeśli robimy jakąś czynność, która wiąże się z powstaniem jakiejś wiadomości, np. poruszamy myszą, wiadomość posyłana jest do tej kolejki, skąd zabiera ją
GetMessage. Jeżeli kolejka w danym momencie jest pusta, to
GetMessage czeka na pierwszą lepszą wiadomość, blokując dalsze wykonywanie programu. Może to brzmieć groźnie, ale w okienkowym systemie jest akurat bardzo pożądane.
Funkcja
TranslateMessage wykonuje kilka drobnych operacji z wykorzystaniem naszego komunikatu - szczegóły nie są nam do niczego potrzebne.
DispatchMessage wysyła komunikat do właściwego miejsca przeznaczenia - tutaj jest nim nasze okno. Przy tak prostym programie nie musisz podawać, do którego okna ma trafić komunikat - system "sam się domyśli".
Obsługa komunikatów
Wewnątrz
WinMain mamy już wszystko, czego nam do szczęścia trzeba. Ale rejestrując klasę naszego okienka podaliśmy nazwę funkcji, która jeszcze nie istnieje i którą sami musimy napisać. Mowa o
WndProc (nazwę wymyślasz sam, nie jest ona istotna). To właśnie ta funkcja zajmie się właściwą obsługą komunikatów systemu, czyli np. reakcją na wciskanie różnych klawiszy. Dodajemy tę funkcję zaraz za
WinMain:
LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
case WM_CLOSE:
DestroyWindow( hwnd );
break;
case WM_DESTROY:
PostQuitMessage( 0 );
break;
default:
return DefWindowProc( hwnd, msg, wParam, lParam );
}
return 0;
}
Jeden z argumentów funkcji WndProc to msg - jest to kod komunikatu, który w danej chwili funkcja ma obsłużyć. Żeby nasze okno w ogóle się pokazało, WndProc nie musi obsługiwać żadnego komunikatu. My chcemy jednak, aby nasze śliczne okienko dało się czasami zamknąć, dlatego dodaliśmy reakcję na komunikat WM_CLOSE. Reakcja to wywołanie funkcji niszczącej nasze okno. Przy niszczeniu okna system wysyła do aplikacji komunikat WM_DESTROY, który również obsłużymy - wysyłając z kolei do systemu komunikat, że chcemy już zakończyć działanie naszej aplikacji - PostQuitMessage (nie ma okna - nie ma aplikacji).
Pozostałe komunikaty (np. kliknięcie myszą) nas nic nie obchodzą, więc odsyłamy je dalej - do funkcji DefWindowProc, która zajmie się ich domyślną obsługą. To wszystko, WndProc powinna zwrócić jeszcze 0.
Wielki finisz!
No, nareszcie - mamy nasze okienko! Trzeba się było nieźle nagimnastykować, ale to wszystko to była w sumie najtrudniejsza część programowania w WinAPI. Od tej pory pozwolimy, aby DevC++ generował za nas ten cały szkielet programu, a my tylko ewentualnie będziemy go modyfikować do własnych, niecnych celów ;-).
Na zakończenie tej części przedstawiam (na wszelki wypadek) cały omówiony kod w jednym kawałku:
#include <windows.h>
LPSTR NazwaKlasy = "Klasa Okienka";
MSG Komunikat;
LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam );
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )
{
WNDCLASSEX wc;
wc.cbSize = sizeof( WNDCLASSEX );
wc.style = 0;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hbrBackground =( HBRUSH )( COLOR_WINDOW + 1 );
wc.lpszMenuName = NULL;
wc.lpszClassName = NazwaKlasy;
wc.hIconSm = LoadIcon( NULL, IDI_APPLICATION );
if( !RegisterClassEx( & wc ) )
{
MessageBox( NULL, "Wysoka Komisja odmawia rejestracji tego okna!", "Niestety...",
MB_ICONEXCLAMATION | MB_OK );
return 1;
}
HWND hwnd;
hwnd = CreateWindowEx( WS_EX_CLIENTEDGE, NazwaKlasy, "Oto okienko", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 240, 120, NULL, NULL, hInstance, NULL );
if( hwnd == NULL )
{
MessageBox( NULL, "Okno odmówiło przyjścia na świat!", "Ale kicha...", MB_ICONEXCLAMATION );
return 1;
}
ShowWindow( hwnd, nCmdShow ); UpdateWindow( hwnd );
while( GetMessage( & Komunikat, NULL, 0, 0 ) )
{
TranslateMessage( & Komunikat );
DispatchMessage( & Komunikat );
}
return Komunikat.wParam;
}
LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
case WM_CLOSE:
DestroyWindow( hwnd );
break;
case WM_DESTROY:
PostQuitMessage( 0 );
break;
default:
return DefWindowProc( hwnd, msg, wParam, lParam );
}
return 0;
}