Kontrolki standardowe wraz z ''Common Controls'' to może niezbyt pokaźny zbiorek, ale w zupełności wystarczają do stworzenia wielu aplikacji. Jednak często zdarza się, że chcielibyśmy w naszym programie czegoś jeszcze - skoro inne programy to mają, to my też możemy ;-). Jednym ze sposobów rozszerzenia możliwości naszych interfejsów graficznych (może nie najprostszym, ale za to dającym najwięcej satysfakcji) jest tworzenie własnych kontrolek. WinAPI daje tu kilka możliwości:
Pierwszą technikę już omówiliśmy we wcześniejszych odcinkach tego kursu. Pozostałymi zajmiemy się tutaj.
Łączenie istniejących kontrolek
Żeby nie zaczynać z pustymi rękami, warto czasem zastanowić się, co właściwie chcemy osiągnąć i co już zrobiono za nas. Jeśli na przykład chcemy mieć kontrolkę, która wygląda i działa jak zwykły przycisk, ale obok niego wyświetlana jest dodatkowo liczba "wciśnięć", to najwygodniej będzie wykorzystać w tym celu połączone siły kontrolek
BUTTON i
STATIC.
Naszym celem jest stworzenie nowej kontrolki, czyli nowej klasy okien. Tak więc powinniśmy skorzystać z superclassingu. Klasą bazową może być dowolna z predefiniowanych klas kontrolek, na przykład
STATIC (która zresztą chyba najlepiej się nadaje do tego celu). Pobieramy informacje o tej klasie i zmieniamy co trzeba:
WNDCLASS wc;
GetClassInfo( hThisInstance, "STATIC", & wc );
wc.lpszMenuName = NULL;
wc.lpszClassName = "Nasza kontrolka";
wc.hInstance = hThisInstance;
g_OldControlProc = wc.lpfnWndProc;
wc.lpfnWndProc = ControlProc;
Ustaliliśmy między innymi nazwę naszej nowej klasy (
"Nasza kontrolka") oraz wskaźnik na nową procedurę okna. Ten ostatni jest oczywiście sednem całej operacji tworzenia nowej kontrolki. Wskaźnik na starą procedurę zachowujemy w globalnej zmiennej
g_OldControlProc. Pora na napisanie nowej procedury, czyli
ControlProc. Co powinna ona robić? Przede wszystkim - wywoływać starą procedurę okna, tę z klasy
STATIC. Szczegóły omówiliśmy już przy superclassingu, więc już wiesz, o co chodzi.
Drugą ważną rzeczą, którą będzie robić procedura
ControlProc, jest stworzenie przycisku i etykietki (
STATIC), na której będzie wyświetlana liczba kliknięć. Muszą one oczywiście być oknami potomnymi dla naszej nowej kontrolki i dlatego właśnie będą tworzone wewnątrz jej procedury okna. Będziemy potrzebować jakichś identyfikatorów dla przycisku i dla etykietki:
#define IDC_BUTTON 1000
#define IDC_LABEL 1001
Pora napisać procedurę
ControlProc. Tworzenie wspomnianych dwóch kontrolek potomnych umieszczamy w obsłudze komunikatu
WM_CREATE - żeby zostały stworzone tuż po przyjściu na świat swego okna-rodzica.
Warto wspomnieć, że nie będziemy się musieli martwić o zniszczenie tych dwóch okien, które tworzymy w
WM_CREATE. System zrobi to za nas, ponieważ będą to okna potomne. A oto i procedura, o której tyle mówimy:
LRESULT CALLBACK ControlProc( HWND hwnd, UINT mesg, WPARAM wParam, LPARAM lParam )
{
switch( mesg )
{
case WM_CREATE:
{
CreateWindowEx( 0, "BUTTON", "Wciśnij mnie", WS_CHILD | WS_VISIBLE, 0, 0, 100, 30, hwnd,
( HMENU ) IDC_BUTTON, GetModuleHandle( NULL ), NULL );
CreateWindowEx( 0, "STATIC", "0", WS_CHILD | WS_VISIBLE | SS_CENTER, 100, 0, 50, 30, hwnd,
( HMENU ) IDC_LABEL, GetModuleHandle( NULL ), NULL );
}
break;
}
return CallWindowProc( g_OldControlProc, hwnd, mesg, wParam, lParam );
}
Trudno może uwierzyć, ale nasza kontrolka jest już gotowa. Wystarczy teraz tylko wywołanie
CreateWindowEx z nazwą klasy, którą przed chwilą zarejestrowaliśmy:
HWND hControl = CreateWindowEx( 0, "Nasza kontrolka", NULL, WS_CHILD | WS_VISIBLE,
5, 5, 150, 30, hwnd, NULL, hThisInstance, NULL );
Dodatkowa funkcjonalność
Mało się napracowaliśmy, ale i mało w sumie osiągnęliśmy, ponieważ nasza kontrolka jeszcze nie działa dokładnie tak, jak sobie założyliśmy (a mianowicie nie liczy kliknięć). I tu mamy nieco większy problem. Żeby coś liczyć, musimy mieć zmienną. Gdzie ją wpakować? Moglibyśmy oczywiście, zadeklarować ją jako zmienną globalną. Pamiętajmy jednak, że potencjalny użytkownik może stworzyć (a że wredny jest, to i pewnie kiedyś stworzy) kilka instancji naszej kontrolki, a przecież każda powinna sobie liczyć kliknięcia osobno... Wygląda więc na to, że jeśli chcemy przechowywać liczbę kliknięć "na zewnątrz" kontrolki, to musimy mieć do tego celu dynamiczną tablicę.
Możemy też przechowywać te dane "wewnątrz". Będzie to o wiele bardziej eleganckie rozwiązanie, bo "prywatne" dane kontrolki powinny być oddzielone od reszty programu. Nie przypadkiem użyłem słowa "prywatne" - gdyby WinAPI było obiektowe, to nową kontrolkę tworzylibyśmy jako klasę w sensie C++, a licznik kliknięć umieścilibyśmy w sekcji
private tej klasy. Jednak marzenia na bok - WinAPI obiektowe nie jest i musimy się męczyć w inny sposób :-) (chociaż oczywiście nikt ci nie zabroni opakowania tego, o czym tu mówimy, w klasy C++).
Zapewne od pierwszego odcinka kursu WinAPI zastanawiasz się, po co te ''extra bytes'', o które ciągle potykamy się przy rejestracji klasy okna. Otóż jest to miejsce, w którym możemy przechowywać dowolne dane. Coś jakby stworzonego specjalnie dla naszego licznika kliknięć :-). Najpierw musimy powiedzieć systemowi, ile tych dodatkowych bajtów chcemy. Nasze żądania nie mogą być zbyt wygórowane, bo system mamy wyjątkowo skąpy; sam pożera łapczywie RAM w każdej dostępnej ilości, ale jedno okno może mieć najwyżej
40 bajtów na własne potrzeby, w dodatku nie możemy użyć z tego ostatnich
4 bajtów, bo tak sobie życzy Microsoft. Oczywiście jeśli nasza kontrolka wymaga więcej dodatkowej pamięci, to możemy ją sobie zaalokować sami, a do owych
40 bajtów pamięci okna wrzucić sam wskaźnik. Do naszego zadania z liczeniem kliknięć jednak
40 bajtów to będzie aż nadto ;-). Dość gadania - dopisujemy do rejestracji naszej klasy nową linijkę:
wc.cbWndExtra = sizeof( DWORD ) + 4;
Dzięki temu każde okno naszej klasy będzie miało do dyspozycji tyle bajtów, ile wynosi rozmiar typu
DWORD (czyli
4 bajty).
Oprócz dodatkowej pamięci dla poszczególnych okien klasy możemy też zarezerwować dodatkowe bajty wspólne dla wszystkich okien tej klasy; w tym celu ustawiamy pole cbClsExtra.
Mamy już pamięć zarezerwowaną, ale jak się tam dostać? Proste - dzięki funkcji
SetWindowLong. Dotychczas używaliśmy jej do zmiany różnych parametrów okna, podając uchwyt tego okna, nazwę parametru, który chcemy zmienić (np.
GWL_STYLE) oraz nową wartość tego parametru. Tym razem zamiast nazwy parametru podajemy offset. Zamierzamy przechowywać
4 bajty licznika kliknięć na samym początku obszaru dodatkowej pamięci okna, więc nasz offset wynosi
0. Do dzieła więc - najpierw zadbajmy o to, by podczas tworzenia naszej kontrolki licznik był zerowany:
case WM_CREATE:
{
CreateWindowEx( 0, "BUTTON", "Wciśnij mnie", WS_CHILD | WS_VISIBLE, 0, 0, 100, 30,
hwnd,( HMENU ) IDC_BUTTON, GetModuleHandle( NULL ), NULL );
CreateWindowEx( 0, "STATIC", "0", WS_CHILD | WS_VISIBLE | SS_CENTER, 100, 0, 50, 30,
hwnd,( HMENU ) IDC_LABEL, GetModuleHandle( NULL ), NULL );
SetWindowLong( hwnd, 0, 0 ); }
break;
Następnie sprawimy, że nasz licznik nareszcie zacznie działać. Musimy w tym celu obsłużyć komunikat
WM_COMMAND (w procedurze
ControlProc) oraz sprawdzić, jakie wieści niesie nam jego parametr
wParam. Przypominam, że
HIWORD(wParam) zawiera kod powiadomienia (dla kliknięcia przycisku jest on równy
BN_CLICKED), natomiast
LOWORD(wParam) zawiera identyfikator kontrolki, którą kliknięto (u nas powinien on być równy
IDC_BUTTON). Żeby zwiększyć licznik o
1, musimy najpierw pobrać "starą" jego wartość poprzez
GetWindowLong, zwiększyć ją, a na koniec uaktualnić wartość przez
SetWindowLong i wyświetlić. Piszemy:
case WM_COMMAND:
{
if( LOWORD( wParam ) == IDC_BUTTON && HIWORD( wParam ) == BN_CLICKED )
{
DWORD dwCounter = GetWindowLong( hwnd, 0 );
++dwCounter;
SetWindowLong( hwnd, 0,( LONG ) dwCounter );
SetDlgItemInt( hwnd, IDC_LABEL,( UINT ) dwCounter, FALSE );
}
}
break;
Gotowe. Licznik działa. Możemy teraz stworzyć kilka naszych kontrolek jednocześnie i sprawdzić, czy nasze wysiłki się opłaciły:
HWND
hControl1 = CreateWindowEx( 0, "Nasza kontrolka", NULL, WS_CHILD | WS_VISIBLE, 5, 5, 150, 30,
hwnd, NULL, hThisInstance, NULL ),
hControl2 = CreateWindowEx( 0, "Nasza kontrolka", NULL, WS_CHILD | WS_VISIBLE, 5, 40, 150, 30,
hwnd, NULL, hThisInstance, NULL ),
hControl3 = CreateWindowEx( 0, "Nasza kontrolka", NULL, WS_CHILD | WS_VISIBLE, 5, 75, 150, 30,
hwnd, NULL, hThisInstance, NULL ),
Efekt powinien być (po paru kliknięciach na przyciski, rzecz jasna) następujący:
Aha - ktoś mógłby się czepić, że nie sprawdzamy, czy użytkownik nie przekręci przypadkiem licznika, klikając zbyt wiele razy. Otóż szanse są doprawdy znikome :-). Zakładając, że w ciągu
1 sekundy klikniemy
4 razy, czyniąc to przez całą dobę (
86400 sekund) uzyskamy zaledwie
345 600 kliknięć (a nie sądzę, by się komuś chciało ;-)). Żeby przepełnić zmienną typu
DWORD, musielibyśmy tak klikać przez około
12427 dni (
34 lata) bez przerwy. Ale przecież nie ma to jak poświęcić pół życia słusznej sprawie, prawda? ;-)