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

Podpowiedzi (Tooltips)

[lekcja]

Tooltipy


My, programiści, uwielbiamy tworzyć skomplikowane rzeczy. Wszak im więcej przycisków i innych dobrodziejstw interfejsu użytkownika ma program, tym bardziej "pro" wygląda ;-). Niestety, gdy już użytkownik naszego dzieła otrząśnie się z szoku, podniesie szczękę z podłogi i wepchnie wytrzeszczone z wrażenia gałki oczne na miejsce, wówczas zwykle dochodzi do wniosku, że warto by spróbować tego cuda... używać. Tylko jak?

Najbardziej pospolity tooltip (Windows XP)
Najbardziej pospolity tooltip (Windows XP)

Zwykle w takich sytuacjach przychodzi z pomocą instrukcja obsługi, ale kto by tam tracił czas na jej czytanie. Rozpieszczeni użytkownicy zdążyli się już przyzwyczaić do luksusu, że gdy wycelują kursor myszy w jakiś przycisk czy inną kontrolkę, to pokaże się małe żółte okienko z podpowiedzią. I takim właśnie okienkiem będziemy się w tym odcinku zajmować.
 
Zaczynamy - a jakże - od stworzenia okna, które będzie reprezentowało naszego tooltipa. Ponieważ jest to kontrolka należąca do grupy Common Controls, musimy najpierw upewnić się, czy odpowiedni plik DLL jest załadowany. Zapewne robiliśmy już to w jednym z poprzednich odcinków, ale dla przypomnienia:
 
C/C++
INITCOMMONCONTROLSEX iccex;
iccex.dwICC = ICC_WIN95_CLASSES;
iccex.dwSize = sizeof( INITCOMMONCONTROLSEX );
InitCommonControlsEx( & iccex );

Jeden tooltip na cały program raczej nam nie wystarczy (chyba, że się uprzemy; znacznie wygodniej jednak tworzyć po jednym tooltipie dla każdej kontrolki w programie), więc napiszemy sobie uniwersalną funkcję CreateTooltip:
 
C/C++
void CreateTooltip( HWND hParent, LPCSTR Text )
{
   
HWND hwndTT = CreateWindowEx( WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL, WS_POPUP |
   
TTS_NOPREFIX | TTS_ALWAYSTIP,
   
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hParent, NULL,
   
g_hThisInstance, NULL );
   

Na razie mamy tylko fragment naszej funkcji - stworzyliśmy kontrolkę, ale w tej chwili jest ona póki co bezużyteczna. Jako klasę okna podaliśmy stałą TOOLTIPS_CLASS. Łatwo też zauważyć, że dołączyliśmy "nowe" style - TTS_NOPREFIX i TTS_ALWAYSTIP. Pierwszy z nich powoduje, że znaczek '&' nie jest zamieniany na podkreślenie (w przypadku tooltipów jest to mało przydatne), drugi - że tooltip będzie się pojawiał nawet wtedy, gdy kontrolka-rodzic zostanie wyłączona. Nie raczej ma sensu wyłączać tooltipów w takiej sytuacji - użytkownik często chce się dowiedzieć, co robi nieaktywny przycisk.
 
Nasz tooltip jest dzieckiem dla kontrolki o uchwycie hParent, więc pojawi się nad nią. Ale jeśli będziemy mieć w programie inną kontrolkę, która w hierarchii okien jest nad naszym hParent (np. przycisk znajdujący się tuż obok), to będzie ona przysłaniać tooltipa. Tego oczywiście zazwyczaj sobie nie życzymy, dlatego warto zadbać, by tooltip pojawiał się zawsze na wierzchu. Robimy to tak:
 
C/C++
SetWindowPos( hwndTT, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );

Funkcję SetWindowPos zapewne już znamy, ale na wszelki wypadek w skrócie powiemy sobie, że oprócz zastosowania sugerowanego w nazwie potrafi ona zrobić z oknem kilka innych rzeczy - między innymi ustalić dokładne miejsce okna w osi Z, o co nam właśnie chodzi. Stała HWND_TOPMOST oznacza, że okienko powędruje na pierwszy plan i żadne inne okno go nie przysłoni (chyba, że też ma HWND_TOPMOST). Pozostałe flagi, mówiąc w skrócie, powodują, że nic innego z oknem się nie dzieje :-).
 
Krok trzeci (i najtrudniejszy, ale zarazem też ostatni) to wysłanie komunikatu TTM_ADDTOOL. Tutaj ustalimy pozostałe parametry tooltipa i przypiszemy mu wreszcie tekst podpowiedzi. Wszystko to czynimy, wypełniając strukturę TOOLINFO:
 
C/C++
TOOLINFO ti;
ti.cbSize = sizeof( TOOLINFO );
ti.uFlags = TTF_SUBCLASS | TTF_IDISHWND;
ti.hwnd = g_hWnd;
ti.hinst = NULL;
ti.uId =( UINT_PTR ) hParent;
ti.lpszText =( LPSTR ) Text;

Pole hwnd to uchwyt do okna - najlepiej do głównego okna programu. Słowo "najlepiej" każe się domyślać, że to pole nie jest obowiązkowe - rzeczywiście, w tej sytuacji podany uchwyt będzie zignorowany, ale przyda nam się później w nieco innym celu i wtedy też pomówimy dokładniej o jego znaczeniu. Ignorujemy też pole hinst. Natomiast uId to identyfikator kontrolki, z którą wiążemy tooltipa. Zamiast identyfikatora możemy podać tutaj uchwyt okna, ale wtedy musimy do flag dołączyć TTF_IDISHWND, co też i czynimy.
 
Flaga TTF_SUBCLASS to polecenie dla WinAPI, by okno-rodzic tooltipa zostało poddane subclassingowi. Dlaczego? Tylko wtedy tooltip będzie się mógł dobrać do komunikatów myszy, które są potrzebne, by cały system zadziałał. Inaczej mielibyśmy dużo radości z ręcznym obsługiwaniem tych komunikatów, tymczasem dzięki podaniu tej flagi wszystko dzieje się automatycznie.
 
Musimy jeszcze wypełnić pole rect. Oznacza ono obszar "aktywności" tooltipa, który zwykle jest równy obszarowi kontrolki-rodzica. Tak więc robimy następująco:
 
C/C++
RECT rect;
GetClientRect( hParent, & rect );

ti.rect.left = rect.left;
ti.rect.top = rect.top;
ti.rect.right = rect.right;
ti.rect.bottom = rect.bottom;

Znaczenie pozostałych pól, użytych w powyższym przykładzie, jest chyba oczywiste.
 
Mając strukturę, możemy przesłać wskaźnik do niej przez wspomniany już komunikat i to będzie już wszystko, by nasz tooltip mógł zadziałać:
 
C/C++
SendMessage( hwndTT, TTM_ADDTOOL, 0,( LPARAM ) & ti );
} // koniec funkcji CreateTooltip()

Mamy teraz bardzo użyteczną funkcję, za pomocą której w prosty sposób dodajemy podpowiedzi do dowolnych kontrolek - tutaj do naszego fikcyjnego przycisku 'OK':
 
C/C++
CreateTooltip( GetDlgItem( IDC_BUTTON_OK ), "Ten przycisk wykonuje jakieś podejrzane operacje" );

Kolory, kolory...


Większość tooltipów jest obrzydliwie pastelowo żółta, ale jeśli ktoś akurat nie lubi tego koloru, może ustawić sobie dowolny inny. Służą do tego dwa z kilkunastu dostępnych komunikatów "tooltipowych": TTM_SETTIPBKCOLOR i TTM_SETTIPTEXTCOLOR. Jak się domyślamy, ustawiają odpowiednio: kolor tła i kolor tekstu. Ich użycie jest bardzo proste, by nie powiedzieć banalne:
 
C/C++
SendMessage( hwndTT, TTM_SETTIPBKCOLOR, RGB( 0, 0, 0 ), 0 );
SendMessage( hwndTT, TTM_SETTIPTEXTCOLOR, RGB( 0, 255, 0 ), 0 );

Najlepiej umieścić te wywołania w naszej funkcji CreateTooltip, żebyśmy nie musieli się zastanawiać, skąd wziąć uchwyt hwndTT. Jeśli tak zrobimy, tooltipy zaczną wyglądać mniej więcej tak:

Taki sam tooltip jak poprzednio, tylko w innych kolorach (Windows XP)
Taki sam tooltip jak poprzednio, tylko w innych kolorach (Windows XP)

Balonowe tooltipy


Jeśli obleśne tooltipy w kształcie prostokąta rażą artystyczną stronę twej duszy, masz niezłą alternatywę w postaci eleganckich dymków komiksowych, w kręgach Microsoftu znanych jako baloniki. W dodatku ich stworzenie jest bardzo proste: wystarczy dodać flagę TTS_BALLOON podczas tworzenia tooltipa przez CreateWindowEx(). Jeśli tylko mamy bibliotekę CommonControls w wersji 5.80 lub nowszej (a prawdopodobnie mamy, bowiem jest ona dostarczana razem z Internet Explorerem 5.0, czyli w przypadku nowszych Windowsów razem z systemem), to po takim zabiegu nasze tipy będą takie:

Faceci kochają krągłości... Balonowy tooltip (Windows XP)
Faceci kochają krągłości... Balonowy tooltip (Windows XP)

Tooltipy wieloliniowe


Tooltipy świetnie nadają się do operacji, które da się opisać jednym, dwoma słowami. Od biedy można wcisnąć nawet całe zdanie. Ale na pewno mamy w naszym programie jakieś tajemnicze przyciski, o których działaniu można by napisać jeśli nie poemat, to przynajmniej kilka zdań. I tutaj pojawia się problem, gdyż kontrolka ToolTip za nic nie chce nam podzielić takiego długiego tekstu na linie.
 
Możemy wymusić wieloliniowość, stosując alternatywną metodę ustawiania tekstu tooltipa. Nie jest ona wygodna (jak całe WinAPI zresztą), ale przynajmniej działa (co w Windows wcale nie jest takie oczywiste). Polega na odpowiadaniu na powiadomienie TTN_GETDISPINFO. Jak większość powiadomień, otrzymujemy je jako komunikat WM_NOTIFY i jak w większości powiadomień, przychodzi ono razem ze strukturą pełną rozmaitych ciekawych pól, które aż się proszą, by je wykorzystać:
 
C/C++
case WM_NOTIFY: {
   
if((( LPNMHDR ) lParam )->code == TTN_GETDISPINFO ) {
       
LPNMTTDISPINFO lpnmtdi =( LPNMTTDISPINFO ) lParam;
       
       
lpnmtdi->lpszText = g_TooltipText;
       
lpnmtdi->hinst = NULL;
       
lpnmtdi->uFlags = TTF_DI_SETITEM;
       
       
SendMessage((( LPNMHDR ) lParam )->hwndFrom, TTM_SETMAXTIPWIDTH, 0, 300 );
   
}
}
break;

W tym przykładzie dostaliśmy powiadomienie TTN_GETDISPINFO, co oznacza, że tooltip właśnie ma zamiar się wyświetlić i prosi o podanie tekstu. Struktura NMTTDISPINFO zawiera dwa pola związane z tekstem. Pole szText to bufor, natomiast lpszText to wskaźnik. Mamy więc do wyboru: skopiować tekst do bufora lub ustawić wskaźnik na nasz własny bufor. Można też do lpszText wpisać identyfikator tekstu w zasobach, jeśli dysponujemy takowym. W tym ostatnim przypadku pole hinst to uchwyt instancji modułu z zasobami. My zdecydowaliśmy się na użycie wskaźnika. Całkiem mądrze, gdyż bufor struktury NMTTDISPINFO ma tylko 80 znaków długości, więc nie nadaje się do długich tekstów. Ustawiamy również hinst na NULL, gdyż jest to praktycznie jedyna wskazówka dla systemu, żeby nie potraktował lpszText jako identyfikatora zasobu.
 
Trzecim i ostatnim polem, które ustawiamy, jest uFlags. Wpisujemy tam TTF_DI_SETITEM. Ta podejrzanie brzmiąca flaga oznacza ni mniej, ni więcej, tylko "masz i się wypchaj" – jeśli ją ustawimy, system nie poprosi nas więcej o tekst do tego tooltipa. W przeciwnym razie powiadomienie TTN_GETDISPINFO dostaniemy za każdym razem, gdy tooltip pokaże się na ekranie.
 
Następnie wysyłamy komunikat TTM_SETMAXTIPWIDTH do tooltipa, żeby określić jego maksymalną szerokość - inaczej system spróbuje wyświetlić go na całej dostępnej szerokości ekranu, co nie wygląda zbyt ładnie, zwłaszcza na panoramicznych monitorach. 300 pikseli to optymalna szerokość.
 
Teraz musimy jeszcze tylko poprosić ładnie system, żeby był tak miły i wysłał nam powiadomienie TTN_GETDISPINFO, zamiast zadowolić się tekstem podanym przy tworzeniu tooltipa. Wracamy zatem do miejsca, gdzie wypełnialiśmy strukturę TOOLINFO i jej pole lpszText wypełniamy następująco:
 
C/C++
ti.lpszText = LPSTR_TEXTCALLBACK;

Pozostaje tylko podziwiać nasze dzieło:

A tak wygląda tooltip z bardzo długim tekstem podzielonym na wiersze (Windows XP)
A tak wygląda tooltip z bardzo długim tekstem podzielonym na wiersze (Windows XP)

Oczywiście ustawianie textu tooltipów w powiadomieniu TTN_GETDISPINFO jest bardzo niewygodne. Znacznie łatwiej by było, gdyby ustawianie textu odbywało się tylko przez wywołanie naszej funkcji CreateTooltip. Z pomocą przychodzi nam pole lParam struktury TOOLINFO, które może przechowywać wskaźnik na stringa, strukturę, klasę, itd. Możemy w nim umieścić wskaźnik do naszego stringa, a następnie odczytać go i ustawić w powiadomieniu TTN_GETDISPINFO. Z przykrego doświadczenia wiem, że niektóre kontrolki z grupy ''Common Controls'' także wykorzystują to powiadomienie wraz z lParam i jeśli je obsłużymy, to tooltipy w tych kontrolkach przestaną działać, czego nie chcemy :-). Dlatego też trzeba znaleźć sposób na rozróżnienie naszych tooltipów od "cudzych" tooltipów. A można to zrobić bardzo prosto. Stworzymy strukturę, która będzie zawierać wskaźnik do stringa, oraz specjalne pole o ustalonej przez nas wartości, na podstawie którego dowiemy się, które tooltipy są nasze. Brzmi to dość zawile, ale jest dość proste. Zobacz na przykład takiej struktury:

C/C++
struct MYTOOLTIP {
   
DWORD dwTest;
   
LPSTR szTip;
};

Musimy jeszcze wymyślić unikalną wartość, którą będzie miało pole dwTest:

C/C++
#define MYTOOLTIPTEST 0x00DEAD00

Musimy teraz zmodyfikować funkcję CreateTooltip. Musimy utworzyć strukturę, wypełnić ją danymi i zapisać w lParam:

C/C++
MYTOOLTIP * mtl = new MYTOOLTIP;
mtl->dwTest = MYTOOLTIPTEST;
mtl->szTip = Text;
ti.lParam =( LPARAM ) mtl;

Do utworzenia struktury posłużyliśmy się operatorem new. Musimy utworzyć zmienną w ten sposób, inaczej została by ona skasowana bo zakończeniu funkcji. Teraz modyfikujemy obsługę powiadomienia TTN_GETDISPINFO:

C/C++
case WM_NOTIFY: {
   
if((( LPNMHDR ) lParam )->code == TTN_GETDISPINFO ) {
       
LPNMTTDISPINFO lpnmtdi =( LPNMTTDISPINFO ) lParam;
       
       
if( lpnmtdi->lParam != NULL ) {
           
DWORD dwValue;
           
memcpy( & dwValue,( const void * ) lpnmtdi->lParam, 4 );
           
           
if( dwValue == MYTOOLTIPTEST ) {
               
MYTOOLTIP * mtl =( MYTOOLTIP * )( lpnmtdi->lParam );
               
               
lpnmtdi->lpszText = mtl->szTip;
               
lpnmtdi->hinst = NULL;
               
lpnmtdi->uFlags = TTF_DI_SETITEM;
               
               
SendMessage((( LPNMHDR ) lParam )->hwndFrom, TTM_SETMAXTIPWIDTH, 0, 300 );
               
               
delete mtl;
           
}
        }
    }
}
break;

Teraz tooltipy wieloliniowe będzie można tworzyć tak jak jednoliniowe funkcją CreateTooltip. Pewnie niepokoi cię, dlaczego użyliśmy funkcji memcpy zamiast zwykłego przypisania. To także wynik moich nieprzyjemnych doświadczeń, z których wynika, że przy obsłudze "cudzych" tooltipów program wiesza się przy przypisaniu. Inną ważną rzeczą jest skasowanie struktury utworzonej przez operator new w funkcji CreateTooltip. Robimy to już po wszystkich operacjach na tej strukturze przy użyciu delete.

Tooltipy z tytułem


Użytkownicy systemów XP i Vista mieli niejedną okazję zaobserwowania jeszcze jednego rodzaju tooltipów - przypominające bardziej "normalne" okna, z paskiem tytułowym, ikonką i nawet przyciskiem do ich zamykania (Vista). System wykorzystuje je do powiadamiania nas o różnych bzdetach, na przykład że instaluje się aktualizacja. Nic nie stoi na przeszkodzie, by zaprząc je do jakichś sensowniejszych celów. Jest to bardzo proste:
 
C/C++
SendMessage( hwndTT, TTM_SETTITLE, 1,( LPARAM ) "Ważna informacja" );

Parametr lParam to oczywiście tekst tytułu (nie może być dłuższy niż 100 znaków), natomiast wParam oznacza ikonę. 0 to brak ikony, 1 - informacja, 2 - ostrzeżenie, 3 - błąd. Jeśli podamy wyższą wartość, system przyjmie, że wParam zawiera uchwyt typu HICON - w ten sposób możemy wyświetlić dowolną posiadaną ikonkę. Ten ostatni bajer dostępny jest tylko w Windows XP SP2 i nowszych. A oto efekt naszej ciężkiej pracy:

I na koniec elegancki tooltip z tytułem (Windows XP)
I na koniec elegancki tooltip z tytułem (Windows XP)

Na koniec warto wspomnieć, że tu i ówdzie system wyręcza nas w tworzeniu tooltipów. Kontrolka Toolbar z ustawionym stylem TBSTYLE_TOOLTIPS będzie automatycznie wyświetlać tooltipy dla poszczególnych przycisków - wystarczy podać tekst przy tworzeniu przycisku. Kontrolka Edit wyświetli tooltip po wysłaniu komunikatu EM_SHOWBALLOONTIP (działa tylko pod XP). Obszar statusu (tray) również automatyzuje tooltipy - wystarczy odpowiednio wypełnić strukturę NOTIFYICONDATA. We wszystkich pozostałych przypadkach możemy natomiast z powodzeniem korzystać ze sposobów opisanych w tym artykule.
Poprzedni dokument Następny dokument
Obszar statusu (Tray) Własne kontrolki, cz. 1