Płaskie toolbary
Pewnie nie jesteś zachwycony dotychczasowymi efektami? Toolbary w "profesjonalnych" aplikacjach wyglądały jakoś inaczej, no nie? Zgadza się, w dzisiejszych czasach modne są płaskie przyciski na toolbarach, trójwymiarowe nie są już trendi ;-P. Zrobienie "płaskiego" toolbaru jest wrecz banalne, wystarczy dodać do stylów toolbaru stałą
TBSTYLE_FLAT. Wypuszcza ona powietrze z tych paskudnych, nadętych przycisków i już wyglądają one całkiem przyzwoicie:
Niestety, w Dev-ie jest drobna przeszkoda, która może nieco utrudnić ustawienie wspomnianego stylu. Mianowicie plik nagłówkowy
commctrl.h z tego pakietu jest kompletnie skopany i praktycznie nie da się go używać bez wprowadzenia do niego kilku własnoręcznych modyfikacji. Nie ma się co łamać, można dodać na poczatku programu jakieś zaklęcie w rodzaju:
#ifndef TBSTYLE_FLAT
#define TBSTYLE_FLAT 2048
#endif
Ponieważ jednak nie jest to jedyna stała, której brakuje lub która jest źle zdefiniowana, musimy przerobić cały nagłówek. Nie martw się, nie będziesz musiał się tym na razie męczyć - w dziale Download znajdziesz spakowany nagłówek w wersji poprawionej przeze mnie. Nadal nie jest on do końca sprawny, ale przynajmniej wszystkie rzeczy opisane w tym kursie będziesz mógł skompilować bez problemów.
Wracając do toolbaru... Istnieje możliwość utworzenia przezroczystego toolbaru, tj. samych przycisków, bez tła. Tym razem ilustracji nie będzie, gdyż na szarym okienku taki toolbar wygląda dokładnie tak samo, jak zwykły, ale gdybyś kiedyś robił program z pięknym bitmapowym tłem, może ci się ten przezroczysty toolbarek przydać ;-). Wystarczy zamiast
TBSTYLE_FLAT dać
TBSTYLE_TRANSPARENT.
Ramka
Toolbar dość paskudnie wygląda, jeśli pozbawiony jest ramki. Możesz ją dodać, ustawiając mu styl
WS_BORDER. Powyższy screen przedstawia właśnie toolbar z ramką.
Ikony + tekst
Przedstawiony powyżej toolbar wyświetla same ikony, ale w większości współczesnych aplikacji (w tym Dev-C++) możesz się spotkać również z przyciskami składającymi się i z tekstu, i z ikony. Zmajstrowanie takiego cuda nie jest szczególnie trudne. Wystarczy dodać odpowiednie napisy do wewnętrznej listy stringów toolbaru, wysyłając komunikat
TB_ADDSTRING:
SendMessage( hToolbar, TB_ADDSTRING, 0,( LPARAM ) "Nowy\0Otwórz\0Zapisz\0-\0Koniec\0" );
Jak widać, poszczególne napisy muszą być rozdzielone znakami zerowymi, a na ich końcu powinien znajdować się podwójny znak zerowy (tutaj widać tylko jeden, zgadnij dlaczego ;-)). Napisy te przekazujemy w parametrze
lParam. Można również wykorzystać napisy zawarte w pliku zasobów, wtedy parametr
wParam określa uchwyt programu, a
lParam - identyfikator napisu w pliku. Teraz musimy jeszcze oczywiście poprzydzielać indeksy napisów na świeżo utworzonej liście odpowiednim przyciskom (czyli wypełnić pole
iString), co jednak w naszym przykładzie już zapobiegawczo zrobiliśmy na samym początku ;-).
Istnieje możliwość przypisywania pojedynczych stringów do przycisku bez konieczności dodawania ich do wewnętrznej listy. Wystarczy zrobić takie małe "oszustwo" i podać adres bufora ze stringiem, zamiast indeksu do
iString, np:
LPSTR buf =( LPSTR ) GlobalAlloc( GMEM_FIXED, 100 );
LoadString( hThisInstance, IDS_NAPIS, buf, 100 );
tbb[ 0 ].iString =( int ) buf;
Ta metoda oczywiście pozwala tylko na dodawanie nowych przycisków. Co zrobić, jeśli chcemy zmienić napis już istniejącego przycisku? Otóż nowsze wersje biblioteki dają nam w prezencie przydatny komunikat
TB_SETBUTTONINFO. Pozwala on zmienić atrybuty przycisku w dowolnym momencie. Niestety, korzysta on z nieco innej struktury, niż dotychczas omawiana
TBBUTTON. Zmiana etykiety przycisku przy pomocy tego komunikatu będzie wyglądała tak:
TBBUTTONINFO tbbi;
ZeroMemory( & tbbi, sizeof( tbbi ) );
tbbi.cbSize = sizeof( tbbi );
tbbi.dwMask = TBIF_TEXT;
tbbi.pszText = "To już jest koniec";
SendMessage( hToolbar, TB_SETBUTTONINFO, TOOL_KONIEC,( LPARAM ) & tbbi );
W sumie nie ma tutaj co omawiać; przerabialiśmy podobne rzeczy już tysiące razy. Pole
dwMask określa, których z pozostałych pól struktury używamy. Potrzebujemy tylko ustawić pole
pszText, więc do
dwMask wpisujemy stałą
TBIF_TEXT. W samym komunikacie
TB_SETBUTTONINFO przekazujemy wskaźnik do struktury jako
lParam, oraz identyfikator zmienianego przycisku jako
wParam. My zmieniamy przycisk
TOOL_KONIEC. Oczywiście tak długi tekst, jak tutaj podaliśmy, nie zmieści się na przycisku, więc będziemy musieli go jeszcze rozszerzyć (ustawiając dodatkowo pole
cx struktury
TBBUTTONINFO), ale to już twoja praca domowa (albo zajrzyj na koniec tej strony ;-) ).
Możesz również sprawić, żeby tekst wyświetlany był nie pod spodem, ale obok ikony. Odpowiada za to styl
TBSTYLE_LIST.
Przycisk + rozwijalna lista
Wiele programów (np. Internet Explorer) ma takie fajne przyciski ze strzałeczką obok. Jeśli ją nacisnąć, pojawia się dodatkowe menu. My też możemy sobie taki przycisk zrobić, a co. Niech będzie to przycisk Importuj (jeśli nie wiesz o co chodzi - zajrzyj do odcinka o menu). Będzie on rozwijał to samo podmenu, które pojawia się po najechaniu na pozycję menu
"Importuj".
Przede wszystkim potrzebny nam przycisk ze stylem
TBSTYLE_DROPDOWN (lub
BTNS_DROPDOWN, dla wyższych wersji biblioteki). Tak więc wypełnianie odpowiedniego pola struktury
TBBUTTON (całości nie podaję, bo już to przerabialiśmy ;-)) powinno wyglądać mniej więcej w ten sposób:
tbb[ 1 ].fsStyle = TBSTYLE_BUTTON | TBSTYLE_DROPDOWN;
Samo to szczęścia nam nie da, bo trzeba jeszcze wyprodukować strzałkę. W tym celu musimy ustawić rozszerzony styl
TBSTYLE_EX_DRAWDDARROWS. Niestety, nie jest to takie proste jak w przypadku "zwykłych" stylów - robi się to przez oddzielny komunikat,
TB_SETEXTENDEDSTYLE:
SendMessage( hToolbar, TB_SETEXTENDEDSTYLE, 0, TBSTYLE_EX_DRAWDDARROWS );
Dzięki temu eksperymentowi powinniśmy już otrzymać gotowy przycisk ze strzałką. Teraz trzeba tak pokombinować, żeby po kliknięciu na strzałkę pojawiało się menu. Tutaj czeka nas dłuższa gimnastyka. Zakładamy, że główne menu programu już mamy gotowe (ze wspomnianego odcinka kursu...), więc możemy skorzystać z funkcji
GetSubMenu, aby pobrać uchwyt do tego fragmentu menu, który nas interesuje, a następnie wyświetlić je za pomocą
TrackPopupMenu.
Jak wykryć, że naciśnięto strzałkę obok przycisku, a nie sam przycisk? Otóż wysyłane jest wówczas powiadomienie
TBN_DROPDOWN. '''Powiadomienie''', a nie komunikat! I tutaj przydałaby się mała dygresja na temat powiadomień. Każde powiadomienie wysyłane jest jako komunikat
WM_NOTIFY. Żeby rozróżnić, jakie konkretnie jest to powiadomienie (jest ich wiele rodzajów, podobnie jak ze "zwykłymi" komunikatami) i od jakiej kontrolki pochodzi, musimy się bliżej zainteresować parametrem
lParam komunikatu
WM_NOTIFY. Jest w nim przekazywany wskaźnik do struktury, która zawiera wszystkie potrzebne nam informacje. Typ struktury zależy od rodzaju powiadomienia, np. w przypadku toolbaru będzie to struktura
NMTOOLBAR. Generalnie obsługa powiadomień może wyglądać mniej więcej tak:
case WM_NOTIFY:
{
LPNMHDR lpn =( LPNMHDR ) lParam;
LPNMTOOLBAR lpnTB =( LPNMTOOLBAR ) lParam;
switch( lpn->code )
{
case TBN_DROPDOWN:
{
return FALSE;
}
default: break;
}
}
break;
Typ
LPNMTOOLBAR to wskaźnik na strukturę typu
NMTOOLBAR. Jedno z pól tej struktury (a konkretnie jej pierwsze pole) jest typu
NMHDR (skrót od ''Notification Message Header''). Pełni ona rolę nagłówka powiadomienia. Pole
code tej struktury zawiera kod powiadomienia i w naszym przypadku powinno być równe
TBN_DROPDOWN. Na razie nie będziemy obsługiwali innych powiadomień. Moglibyśmy jeszcze wykorzystać pola
hwndFrom i
idFrom struktury
NMHDR, żeby sprawdzić, czy powiadomienie faktycznie pochodzi od toolbaru, ale odpuśćmy sobie takie detale ;-).
Pewnie się zastanawiasz, dlaczego jeden parametr
lParam przekonwertowaliśmy na dwa rodzaje struktur i czy to przypadkiem nie pomyłka. Bynajmniej. Wskaźnik zawarty w
lParam wskazuje na strukturę typu
NMTOOLBAR, ale na samym poczatku tej struktury znajduje się (jak już wspomnieliśmy) nagłówek typu
NMHDR, więc możemy wskaźnik ten przerobić na
LPNMHDR. Robimy to dla czystej wygody, dzięki temu piszemy po prostu
lpn->code zamiast
lpnTB->hdr.code, co przy pisaniu bardziej skomplikowanej obsługi powiadomieć może się okazać błogosławionym rozwiązaniem ;-).
Zajmijmy się teraz wyświetlaniem naszego menu. Najpierw pobieramy i zapamiętujemy w jakiejś zmiennej uchwyt do podmenu Importuj (pamiętając o tym, żeby sobie takie menu utworzyć wcześniej w pliku zasobów):
HMENU hPopupMenu;
hPopupMenu = GetSubMenu( hMenu, 0 );
Mamy już odpowiedni uchwyt, pozostaje się tylko zastanowić, w którym miejscu wyświetlić. Najlepiej byłoby tuż pod przyciskiem. Tak więc musimy pobrać współrzędne tegoż przycisku:
RECT rc;
SendMessage( lpn->hwndFrom, TB_GETRECT,( WPARAM ) lpnTB->iItem,( LPARAM ) & rc );
Otrzymane współrzędne są względne - punkt (
0,0) to lewy górny róg obszaru klienta. Musimy je zatem przekonwertować na współrzędne ekranowe. Robiliśmy to już we wcześniejszych odcinkach kursu z punktami, ale teraz mamy cały prostokąt, więc najprościej będzie użyć funkcji
MapWindowPoints (która potrafi konwertować całą tablicę punktów na raz, a przecież prostokąt to właśnie tablica 2 punktów). Nie będziemy jej dokładniej omawiać, zobaczymy tylko gotowy przykład:
MapWindowPoints( lpn->hwndFrom, HWND_DESKTOP,( LPPOINT ) & rc, 2 );
Teraz tylko wyświetlić menu. Możemy to zrobić funkcją
TrackPopupMenu albo
TrackPopupMenuEx. Ponieważ zaś lubimy wyzwania... No dobra, nie bij, ta druga funkcja ma pewną zaletę - możemy jej podać obszar ekranu, którego menu nie powinno przysłonić. Jak się pewnie domyślasz, obszarem tym będzie nasz przycisk. W dodatku
TrackPopupMenuEx przyjmuje mniej argumentów (!).
TPMPARAMS tpm;
tpm.cbSize = sizeof( TPMPARAMS );
tpm.rcExclude = rc;
TrackPopupMenuEx( hPopupMenu, TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_VERTICAL,
rc.left, rc.bottom, hwnd, & tpm );
Jak widzimy, funkcja korzysta z nowej struktury (hurrra :-]) -
TPMPARAMS, określającej rozszerzone parametry. Właśnie do tej struktury wpisujemy współrzędne naszego prostokąta. Oprócz tego wyświetlamy menu w ten sposób, że jego lewy górny róg będzie się znajdował tuż pod przyciskiem. W porównaniu do
TrackPopupMenu, w "wersji rozszerzonej" możemy jeszcze użyć (i używamy ;-) ) dodatkowej flagi -
TBM_VERTICAL, która podpowiada systemowi, co zrobić w sytuacji, gdyby zabrakło miejsca na ekranie do wyświetlenia menu.
Hot-tracking
Cóż to znowu za czort? Ano, ''hot-tracking'' oznacza podświetlanie (lub oznaczanie w innych sposób) kontrolek, na które najedziemy myszą. Co prawda w naszym toolbarze mamy to już zaimplementowane automatycznie (odkąd go spłaszczyliśmy ;-) ), ale teraz dowiemy się jeszcze, co zrobić by dodatkowo po najechaniu zmieniała się bitmapa przycisku.
Toolbar może miec przypisane trzy listy obrazków. Jedna z nich odpowiada za domyślny wygląd ikon, druga - za wygląd ikon, na które najechano strzałką, trzecia - za wygląd nieaktywnych (wyłączonych) przycisków. Do przypisywania toolbarowi tych list służą specjalne komunikaty, odpowiednio:
TB_SETIMAGELIST, TB_SETHOTIMAGELIST, TB_SETDISABLEDIMAGELIST. W parametrze
lParam tych komunikatów powinniśmy przekazać uchwyt do listy obrazków.
Zanim zastanowimy się, jak uzyskać ten uchwyt, musimy dokonać drobnego spostrzeżenia: toolbar może mieć aż trzy listy obrazków na raz, ale każdy przycisk może mieć przypisany tylko jeden indeks bitmapy w danym momencie. Oznacza to, że nie możemy robić hot-trackingu wybiórczo - albo wszystkie przyciski na toolbarze go obsługują, albo żaden. Jeśli zdecydujemy się na to pierwsze, to musimy zrobić po dwie (lub trzy) bitmapy dla każdego przycisku, tak aby ikony odpowiednich przycisków znajdowały się w obu (lub trzech) bitmapach w TEJ SAMEJ KOLEJNOŚCI, a następnie dodać te bitmapy do dwóch (trzech) list i przypisać listy do toolbaru.
Jak tworzymy listę obrazków? Mamy do tego funkcję
ImageList_Create. Bliższego omawiania nie będzie; zbyt leniwy jestem na to ;-). Pierwsze dwa argumenty to wymiary pojedynczej ikony, a trzeci oznacza głębokość koloru (wspólna dla całej listy), np.
ILC_COLOR8, ILC_COLOR24. Połączony z flagą
ILC_MASK oznacza, że dla każdego obrazka zostanie stworzona mapa bitowa przeźroczystości obrazka. Pozostałe argumenty nie mają dla nas większego znaczenia. Zwracana wartość jest typu
HIMAGELIST, więc powinniśmy sobie wcześniej utworzyć zmienną tego typu:
HIMAGELIST himlDef, himlHot;
himlDef = ImageList_Create( 16, 16, ILC_COLOR24 | ILC_MASK, 0, 1 );
himlHot = ImageList_Create( 16, 16, ILC_COLOR24 | ILC_MASK, 0, 1 );
Wczytywać bitmapy już dawno umiemy (prawda?), więc nie będziemy się powtarzać; zakładamy, że mamy już gotowe uchwyty do bitmap
hbmDef i
hbmHot, które możemy teraz dodać do odpowiednich list. Bitmapa o uchwycie
hbmDef powinna zawierać czarno-białe obrazki,
hbmHot - w kolorze. Do dodawania służy funkcja
ImageList_Add, ale jeszcze lepiej jest użyć
ImageList_AddMasked, która przy okazji wykona za nas "usuwanie" niepotrzebnego tła z bitmapy:
ImageList_AddMasked( himlDef, hbmDef, RGB( 192, 192, 192 ) );
ImageList_AddMasked( himlHot, hbmHot, RGB( 192, 192, 192 ) );
Teraz możemy za pomocą wspomnianych już wyżej komunikatów przypisać stworzone i wypełnione przed chwilą listy obrazków do odpowiednich toolbarów:
SendMessage( hToolbar, TB_SETIMAGELIST, 0,( LPARAM ) himlDef );
SendMessage( hToolbar, TB_SETHOTIMAGELIST, 0,( LPARAM ) himlHot );
Zakładając, że już wcześniej ponadawaliśmy przyciskom odpowiednie indeksy, otrzymujemy toolbar z czarno-białymi przyciskami, które dopiero po "podświetleniu" stają się kolorowe:
Nie zapomnij o zwolnieniu zasobów. Listy obrazków niszczymy za pomocą ImageList_Destroy, podając jako argument uchwyt do niszczonej listy. Bitmapy, przypominam, usuwamy za pomocą DeleteObject.
Tooltipy
Podpowiedzi, wskazówki, z angielska ''tooltips'', to te śmieszne etykietki, zwykle na żółtym tle, które pojawiają się po najechaniu kursorem myszy na jakiś istotny element interfejsu i zatrzymaniu go przez jakiś czas (zwykle około sekundy). Są one przydatne w przypadku każdej kontrolki, ale największe chyba znaczenie mają właśnie przy toolbarach, których przyciski często nie posiadają widocznych podpisów, a nie zawsze przecież ikonka potrafi nam zasugerować dokładne znaczenie danego przycisku ;-).
Zanim zaczniesz się biedzić nad tooltipami, ponadawaj przyciskom unikalne identyfikatory (idCommand), w przeciwnym razie tooltipy będą błędnie wyświetlane.
Najprostszym sposobem na ustawienie tooltipów jest... ustawienie tekstu na przycisku, co już zrobiliśmy. Jeśli ustawimy tekst, ale jednocześnie go ukryjemy, to będzie on wyświetlany jako tooltip. Tylko jak to zrobić? Jedna z metod (chyba najprostsza) to ustawienie toolbarowi stylów
TBSTYLE_TOOLTIP i
TBSTYLE_LIST (koniecznie obydwa!). Ponadto ustawiamy jeszcze rozszerzony styl
TBSTYLE_EX_MIXEDBUTTONS (ten ostatni oczywiście przez wysłanie odpowiedniego komunikatu - patrz wyżej).
Jeśli toolbar ma ustawiony styl
TBSTYLE_EX_MIXEDBUTTONS, to tekst na przyciskach nie jest wyświetlany jako etykieta (chyba, że dany przycisk ma ustawiony styl
BTNS_SHOWTEXT), a najwyżej jako tooltip:
A co zrobić, żeby wyświetlić w tooltipie inny tekst, niż etykieta przycisku? To już wymaga nieco więcej zachodu. Trzeba odpowiedzieć na powiadomienie
TTN_GETDISPINFO, które wysyłane jest przez toolbar do okna rodzicielskiego. W parametrze
lParam dostajemy wtedy wskaźnik do struktury typu
TOOLTIPTEXT, która zawiera nagłówek (typu
NMHDR, oczywiście), pozwalający nam zorientować się, którego dokładnie przycisku dotyczy dane powiadomienie. W strukturze tej występuje również pole
lpszText, do którego musimy wpisać tekst tooltipa. Oto przykład odpowiedzi na to powiadomienie, w której to odpowiedzi ustawiamy tekst dwóch z naszych przycisków:
case TTN_GETDISPINFO:
{
LPTOOLTIPTEXT lpttt =( LPTOOLTIPTEXT ) lParam;
switch( lpttt->hdr.idFrom )
{
case TOOL_NOWY:
lpttt->lpszText = "Tworzy nowy plik";
break;
case TOOL_OTWORZ:
lpttt->lpszText = "Otwiera istniejący plik";
break;
}
}
break;
Dzięki tej pisaninie możemy wyświetlać w tooltipach co chcemy, a etykiety zostawić w spokoju. Wtedy możliwe jest ustawienie krótkich etykiet typu Nowy, Zapisz itp., oraz nieco dłuższych tooltipów, dokładniej objaśniających funkcję danego przycisku:
Kontrolki na toolbarze
Na koniec dowiemy się, jak wstawić do toolbaru inną kontrolkę. Mogłeś się z tym spotkać np. w edytorach tekstu, gdzie na toolbarach jest zwykle ComboBox z wyborem czcionek; zaraz zajmiemy się wstawieniem takiego właśnie ComboBoxa (ale bez czcionek ;-)).
Najpierw trzeba na toolbarze zrobić trochę miejsca. Możemy wstawić do niego nowy separator i rozszerzyć go do tylu pikseli, ile ma mieć nasz ComboBox. Separator ten nie będzie i tak widoczny zza ComboBoxa. Zdefiniujemy też sobie identyfikator dla tego separatora, a także dla ComboBoxa. No to do dzieła:
#define TOOL_PLACEHOLDER 7
#define IDC_COMBOBOX 501
HWND g_hCombo;
int szerokosc = 100;
TBBUTTON tbb3[ 1 ];
ZeroMemory( tbb3, sizeof( tbb3 ) );
tbb3[ 0 ].idCommand = TOOL_PLACEHOLDER;
tbb3[ 0 ].fsState = TBSTATE_ENABLED;
tbb3[ 0 ].fsStyle = TBSTYLE_SEP;
SendMessage( hToolbar, TB_ADDBUTTONS, 1,( LPARAM ) & tbb3 );
"Separator" już jest, ale ma za małe wymiary - kontrolka nam się nie zmieści. Trzeba rozszerzyć. Wykorzystamy omówioną niedawno strukturę
TBBUTTONINFO, by dzieła tego dokonać:
TBBUTTONINFO tbbi;
ZeroMemory( & tbbi, sizeof( tbbi ) );
tbbi.cbSize = sizeof( tbbi );
tbbi.dwMask = TBIF_SIZE;
tbbi.cx = szerokosc;
SendMessage( hToolbar, TB_SETBUTTONINFO, TOOL_PLACEHOLDER,( LPARAM ) & tbbi );
Potrzebujemy teraz wymiarów rozciągniętego właśnie "separatora". Pobierzemy je znanym już komunikatem
TB_GETITEMRECT i wykorzystamy do stworzenia ComboBoxa. Aby ComboBox został utworzony na toolbarze, toolbar musi być rodzicem ComboBoxa. Jednak na razie jako uchwyt rodzica podamy
hwnd, czyli nasze główne okno - dlaczego, zaraz się wyjaśni.
RECT rc;
SendMessage( hToolbar, TB_GETITEMRECT, TOOL_PLACEHOLDER,( LPARAM ) & rc );
g_hCombo = CreateWindowEx( 0L, "COMBOBOX", NULL,
WS_CHILD | WS_BORDER | WS_VISIBLE | CBS_DROPDOWN,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hwnd,( HMENU ) IDC_COMBOBOX, hThisInstance, 0 );
ComboBox już gotowy, ale aby pojawił się na toolbarze, musimy mu jeszcze zmienić rodzica. Proces adopcyjny jest bardzo prosty i nie będziemy musieli nawet biegać po sądach, wystarczy wywołać
SetParent:
SetParent( g_hCombo, hToolbar );
Dlaczego stworzyliśmy bachora, a potem przenieśliśmy go do rodziny zastępczej? Nie wygodniej by było od razu utworzyć go z rodzicem hToolbar? Otóż na pewno byłoby mniej roboty, ale wtedy drań wysyłałby wszystkie komunikaty do toolbaru, a nie do okna głównego, czyli bez zastosowania subclassingu byłyby one dla nas nieprzydatne (nie moglibyśmy obsługiwać zdarzeń związanych z ComboBoxem). Tymczasem dzięki tej sztuczce wszystko gra jak w zegarku: