Niezaprzeczalnie toolbary to fajna sprawa. Ale w większych aplikacjach (jak np. Visual C++), gdzie do dyspozycji jest kilkadziesiąt czy nawet kilkaset poleceń menu, wstawienie ich wszystkich do toolbaru jest fizycznie niemożliwe - w końcu na ekranie musi zostać jeszcze trochę miejsca na pozostałe elementy aplikacji ;-). Z drugiej strony ograniczanie użytkownika do kilku najczęściej stosowanych poleceń kłóci się z polityką maksymalnej przyjazności interfejsu, zresztą programiści może i są inteligentni, ale na pewno żaden z nich nie jest jasnowidzem i nie potrafi na 100% stwierdzić, czy każdy użytkownik będzie korzystał z danego przycisku na toolbarze czy też nie.
Z powyższych powodów (a także kilku innych) każdy cywilizowany toolbar powinien dać się dowolnie modyfikować. Użytkownik musi mieć możliwość dodawania i usuwania przycisków, grupowania ich według własnego uznania, wstawiania separatorów, przenoszenia poszczególnych grup przycisków w inne miejsce (także poza obszar toolbaru, jako osobne okienko), a nawet definiowania własnych przycisków, których przeznaczenie nie zostało przewidziane przez programistów albo przewidziane tylko częściowo. Wszystkich tych cudów (może za wyjątkiem ostatniego) za moment się nauczymy. Brzmi to może dość kosmicznie, ale jest całkiem proste w implementacji - większość akcji wykonuje za nas automatycznie biblioteka Comctl32.dll.
Rebar
Pod względem wizualnym najwięcej nowości oferuje nam specjalny rodzaj kontrolki, zwany '''rebarem'''. Jest to rodzaj '''kontenera''' - można na nim umieszczać inne kontrolki (mniej więcej tak, jak to robiliśmy z ComboBoxem w poprzedniej części kursu). Kontrolki te mogą być samodzielnymi toolbarami lub też czymkolwiek innym, przy czym każda kontrolka musi występować na oddzielnej bandzie (należy ten termin kojarzyć raczej z obręczą, niż szajką zbirów ;-) ). Zresztą zobacz, jak to jest zrobione np. w Dev-C++ - kilka band zawiera toolbary, a w jednej są dwa ComboBoxy (co prawdopodobnie uzyskano w ten sposób, że banda zawiera toolbar, a ten z kolei ComboBoxy).
No to od czego zaczynamy? Pewnie od stworzenia rebaru. Mamy do dyspozycji klasę
REBARCLASSNAME... Mamy? Nie mamy. Ale możemy mieć, jeśli ją zarejestrujemy. W tym celu zamieniamy wywołanie naszej funkcji
InitCommonControls na jej rozszerzoną wersję -
InitCommonControlsEx (znaną wśród programistów z poczuciem humoru jako InitCommonControlSex ;-)). Różni się ona od "zwykłej" wersji tym, że korzysta dodatkowo z pewnej struktury (już widzę, jak podskakujecie z radości ;-)). Struktura ta to
INITCOMMONCONTROLSEX i omawiać jej nie będziemy, a przynajmniej nie tutaj. Aby ta struktura była dostępna musimy określić wersję IE taką dyrektywą:
Jedyna interesujące nas w tym momencie zastosowanie jest następujące:
INITCOMMONCONTROLSEX icex;
icex.dwSize = sizeof( INITCOMMONCONTROLSEX );
icex.dwICC = ICC_COOL_CLASSES | ICC_BAR_CLASSES;
InitCommonControlsEx( & icex );
W ten sposób zapewniliśmy sobie dostęp do "fajnych klas", w tym do rebaru. Teraz możemy spokojnie przystąpić sobie do jego tworzenia:
hRebar = CreateWindowEx( WS_EX_TOOLWINDOW, REBARCLASSNAME, NULL, WS_CHILD |
WS_VISIBLE | WS_CLIPSIBLINGS | WS_BORDER |
WS_CLIPCHILDREN | RBS_VARHEIGHT | RBS_BANDBORDERS,
0, 0, 0, 0, hwnd, NULL, hInstance, NULL );
Coś tam sobie namodziliśmy w programie, ino efektów nie widać... I w sumie nic dziwnego - nie dodaliśmy przecież żadnych band. Dopiero kiedy to zrobimy, efekty naszych starań staną się widoczne. Do dodawania band, a także kilku innych zastosowań przydatna jest... o radości, kolejna nowa struktura. Tym razem nazywa się toto
REBARBANDINFO.
Ustalamy, że chcemy sobie wyczarować dwie bandy - jedną z toolbarem, jedną na przykład z ComboBoxem. Struktura
REBARBANDINFO jest dość obszerna, więc warto na początek zastanowić się, które z pól tej struktury będą użyte przez obie bandy, a które trzeba będzie ustawiać oddzielnie. Na pewno obydwie wymagają ustawienia pola
cbSize (analogicznie jak w przypadku większości struktur w WinAPI) i
fMask (też analogicznie). Nie zaszkodzi też zaliczyć do tej grupy pola
dwStyle, bowiem styl obu band powinien być taki sam. Deklarujemy więc zmienną i ustawiamy wspólne pola:
REBARBANDINFO rbbi;
rbbi.cbSize = sizeof( REBARBANDINFO );
rbbi.fMask = RBBIM_TEXT | RBBIM_BACKGROUND | RBBIM_STYLE | RBBIM_CHILD |
RBBIM_CHILDSIZE | RBBIM_SIZE;
rbbi.fStyle = RBBS_CHILDEDGE | RBBS_FIXEDBMP | RBBS_GRIPPERALWAYS;
rbbi.hbmBack = LoadBitmap( hThisInstance, MAKEINTRESOURCE( IDB_BACKGRND ) );
Warto zwrócić uwagę, że wczytaliśmy tu sobie z pliku zasobów bitmapkę - możemy bowiem ustawić bitmapę tła dla kazdej bandy, co poprawia jej walory wizualne ;-) (jak na rysunku powyżej).
Teraz tworzymy kontrolki do poszczególnych band. Najpierw toolbar - procedurę jego tworzenia już znamy - wypełniamy strukturę z przyciskami i wywołujemy
CreateToolbarEx:
TBBUTTON tbb[ 3 ];
ZeroMemory( tbb, sizeof( tbb ) );
for( int i = 0; i < 3; ++i ) {
tbb[ i ].idCommand = i;
tbb[ i ].fsState = TBSTATE_ENABLED;
tbb[ i ].fsStyle = TBSTYLE_BUTTON;
}
tbb[ 0 ].iBitmap = STD_FILENEW;
tbb[ 1 ].iBitmap = STD_FILEOPEN;
tbb[ 2 ].iBitmap = STD_FILESAVE;
hToolbar = CreateToolbarEx( hRebar, WS_CHILD | WS_VISIBLE | CCS_NOPARENTALIGN |
CCS_NORESIZE | CCS_NODIVIDER | TBSTYLE_FLAT,
500, 3, HINST_COMMCTRL, IDB_STD_SMALL_COLOR, tbb, 3, 16, 16, 16, 16, sizeof( TBBUTTON ) );
Teraz wypełniamy te pola struktury
REBARBANDINFO, które będą wykorzystane tylko do toolbaru, a później będą oddzielnie wypełnione dla ComboBoxa. Następnie dodajemy gotową bandę do rebaru, korzystając z odpowiedniego komunikatu:
rbbi.lpText = "";
rbbi.hwndChild = hToolbar;
rbbi.cxMinChild = 100;
rbbi.cyMinChild = 22;
rbbi.cx = 100;
SendMessage( hRebar, RB_INSERTBAND,( WPARAM ) - 1,( LPARAM ) & rbbi );
Teraz kolej na ComboBox - używamy oczywiście funkcji
CreateWindowEx:
hCombo = CreateWindowEx( 0, "COMBOBOX", NULL, WS_CHILD | WS_BORDER | WS_VISIBLE |
WS_CLIPCHILDREN | WS_CLIPSIBLINGS | CBS_DROPDOWN, 0, 0, 70, 200,
hRebar,( HMENU ) IDC_COMBOBOX, hThisInstance, 0 );
Będziemy potrzebowali zmiennej do zapamiętania wymiarów ComboBoxa - przydadzą się one za chwilę do określenia wymiarów całej bandy:
RECT rc;
GetWindowRect( hCombo, & rc );
Wypełniamy strukturę informacjami o naszej drugiej bandzie, po czym dodajemy ją do rebaru:
rbbi.lpText = "ComboBox";
rbbi.hwndChild = hCombo;
rbbi.cxMinChild = rc.right - rc.left;
rbbi.cyMinChild = rc.bottom - rc.top;
rbbi.cx = 200;
SendMessage( hRebar, RB_INSERTBAND,( WPARAM ) - 1,( LPARAM ) & rbbi );
To wszystko, możemy się już delektować widokiem, który będzie przypominał coś z tego screena. Zmieniłem tutaj kolor tła na ciemnoszary, żeby rebar był lepiej widoczny:
Powyższy przykład pomija oczywiście całe mnóstwo "nikomu niepotrzebnych" informacji o rebarze, ale gdybym zaczął opisywać wszystkie możliwości tej kontrolki, to wyszłoby z dziesięć dodatkowych odcinków tego kursu, a przecież do wszystkiego możesz sobie w miarę potrzeb dojść sam, wspierając się tym, co już się dowiedziałeś w tej części kursu oraz MSDN.
Dostosowywanie toolbaru
Rebar, nawet tak prosty jak ten przedstawiony powyżej, daje nam już pewne możliwości dopasowania go do naszych wyśrubowanych potrzeb. Możemy sobie zmieniać szerokość poszczególnych band, zamieniać je miejscami, a nawe umieścić każdą bandę w osobnym wierszu. Ale to dziabkę za mało. Potrzebna jeszcze możliwość manipulowania rozmieszczeniem przycisków w obrębie każdej bandy.
Zadanie jest całkiem proste. Na poczatek wystarczy ustawić toolbarowi styl
CCS_ADJUSTABLE. Wtedy zacznie od wysyłać do swego okna rodzicielskiego rozmaite ciekawe powiadomienia w momencie, gdy użytkownik kliknie na ten toolbar dwukrotnie. Te powiadomienia mają służyć do pobrania od programisty informacji, jak dokładnie użytkownik może zmodyfikować toolbar (tj. jakie przyciski może dodać, jakie usunąć, jakie przesunąć na inną pozycję itp.). Jeśli odpowiemy przynajmniej na obowiązkowe powiadomienia, wyświetlony będzie dialog dostosowywania toolbaru:
Nie będziemy się bawili w przydługie wstępy, od razu na sam początek zobaczymy, w jaki sposób uzyskać "na skróty" możliwość modyfikowania paska w fazie run-time:
case WM_NOTIFY: {
LPNMHDR lpn =( LPNMHDR ) lParam;
switch( lpn->code ) {
case TBN_QUERYINSERT:
{ return TRUE; }
case TBN_QUERYDELETE:
{ return TRUE; }
case TBN_GETBUTTONINFO:
{ return FALSE; }
default: break;
}
}
break;
Jak widać, te "obowiązkowe" powiadomienia (jeśli na nie nie odpowiemy, to dialog dostosowywania się w ogóle nie ukaże) to
TBN_QUERYINSERT, TBN_QUERYDELETE i
TBN_GETBUTTONINFO, a odpowiadanie na nie polega po prostu na zwróceniu odpowiedniej wartości. Teraz pora na szczegóły. Po wspomnianym już dwukrotnym kliknięciu na toolbar wysyłane są następujące powiadomienia:
TBN_BEGINADJUST po prostu zawiadamia aplikację, że użytkownik chce dostosowywać toolbar i tego powiadomienia obsługiwać nie musimy. Podobnie jest z
TBN_INITCUSTOMIZE, które służy praktycznie tylko do ukrywania przycisku Pomoc, jeśli programista ma taką chęć (wówczas musi zwrócić
TBNRF_HIDEHELP).
Na następne wymienione powiadomienia odpowiedzieć już trzeba.
TBN_QUERYINSERT wysyłane jest dla każdego przycisku na toolbarze osobno i jeśli odpowiemy na nie
FALSE, to zablokujemy możliwość wstawienia innego przycisku po lewej stronie od tego, dla którego aktualnie wysyłane jest powiadomienie (a jak to z kolei sprawdzić, już wiemy - struktura
NMHDR będzie bardzo pomocna ;-)). Przynajmniej na jedno powiadomienie
TBN_QUERYINSERT musimy odpowiedzieć
TRUE, inaczej dialog się nie wyświetli. W powyższym przykładzie odpowiadamy
TRUE na wszystkie powiadomienia tego rodzaju (nie sprawdzamy nawet, od którego przycisku pochodzą), dając tym samym możliwość wstawiania dodatkowych przycisków gdzie popadnie ;-).
Powiadomienia
TBN_QUERYDELETE również wysyłane są dla każdego przycisku z osobna i dzięki nim możemy poinformować system, które przyciski użytkownik może sobie usunąć z toolbaru, a które pozostają na nim dożywotnio. W przykładzie powyżej użytkownik może usunąć wszystkie przyciski (a niech się cieszy, bestyja ;-)).
Wreszcie -
TBN_GETBUTTONINFO służy do tego, byśmy mogli wypełnić listę dostępnych przycisków do dodania na toolbar (lista ta jest wyświetlana po lewej stronie dialogu dostosowywania). Wypełnianie polega na wypełnieniu struktury
NMTOOLBAR (do której wskaźnik otrzymujemy wraz z powiadomieniem
TBN_GETBUTTONINFO) oraz zwróceniu
TRUE. Operację taką należy powtórzyć dla każdego przycisku, który chcemy dodać na tę listę. Na koniec powinniśmy zaś zwrócić
FALSE, co przerwie wysyłanie powiadomień
TBN_GETBUTTONINFO. Powyżej zrobiliśmy to na samym początku, toteż na liście dostępnych przycisków do dodania mamy tylko domyśly separator (za to możemy ich sobie na toolbar wstawić od groma ;-)).
Nie będę tu się rozpisywał na temat wypełniania
NMTOOLBAR, gdyż nie zawiera ona w zasadzie nic, o czym do tej pory byśmy nie mówili, więc jest to praktycznie tylko kwestia wypróbowania tego na własną rękę. Poza tym nie mam pomysłu, jakie by tu jeszcze przyciski dodać do toolbaru w naszym przykładzie ;-).
Podczas gdy użytkownik wyczynia z toolbarem cuda za pośrednictwem wspomnianego dialogu, jego niecne czyny oczywiście nie przechodzą niezauważone. Za każdym razem, gdy drań coś miesza, wysyłane są odpowiednie donosy, tak że w razie czego możemy zapobiec dalszym szkodom, a nawet ukarać łobuza ;-). Gdy wstawiany jest przycisk, system wysyła
TBN_QUERYINSERT i możemy zwrócić
FALSE - wtedy przycisk nie zostanie wstawiony i mroczne plany użytkownika spełzną na niczym. Gdy użytkownik usuwa przycisk, wysyłane jest powiadomienie
TBN_DELETINGBUTTON. Jeśli użytkownik zmienia cokolwiek, przychodzi powiadomienie
TBN_TOLBARCHANGE (takie uogólnienie dwóch poprzednich, obejmujące dodatkowo przeniesienie przycisku). Wreszcie
TBN_CUSTHELP oznacza wciśnięcie przez usera przycisku Pomoc, a
TBN_RESET - wciśnięcie przycisku Resetuj.
Na samym końcu, gdy użytkownikowi zabawa się już znudzi albo sumienie go ruszy, że za dużo napsuł, zamyka on dialog dostosowywania i wówczas dostajemy powiadomienie
TBN_ENDADJUST (ufff...).
To, o czym tutaj napisałem, nie wyczerpuje oczywiście tematu nawet w połowie, ale jakieś tam wytyczne już mamy, reszta to już tylko praktyka...