Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Własne kontrolki, cz. 1

[lekcja]
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:

  • sub- i superclassing
  • kontrolki rysowane przez rodzica (''owner-drawn controls'')
  • łączenie standardowych kontrolek
  • tworzenie własnych kontrolek od podstaw

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:

C/C++
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:

C/C++
#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:

C/C++
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:

C/C++
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ę:

C/C++
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:

C/C++
// to oczywiście powinno się znaleźć w ControlProc :-)
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 ); // inicjalizacja licznika kliknięć
}
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:

C/C++
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:

C/C++
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:

Nasze dzieło, sztuk 3 (Windows 98)
Nasze dzieło, sztuk 3 (Windows 98)

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? ;-)
Poprzedni dokument Następny dokument
Podpowiedzi (Tooltips) Własne kontrolki, cz. 2