Na pasku zadań Windowsa jest taki fajny obszar, zwany obszarem statusu (a popularnie - tray'em). Znajdują się tam najczęściej ikonki programów działających w tle. Pewnie nieraz zastanawiało cię, jak się dobrać do tego miejsca:
Dzięki temu artykułowi stanie się to proste ;-).
Ikonka
Wszelkie zadania związane z ikonkami w tray'u (dodawanie, modyfikowanie, usuwanie) wykonuje funkcja
Shell_NotifyIcon. Jej składnia jest bardzo prosta, pobiera ona tylko rodzaj operacji do wykonania oraz adres struktury typu
NOTIFYICONDATA. Struktura ta zawiera wszystkie niezbędne informacje o ikonce. W plikach nagłówkowych naszego SDK zadeklarowana ona jest następująco:
typedef struct _NOTIFYICONDATA
{
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
char szTip[ 64 ];
} NOTIFYICONDATA, * PNOTIFYICONDATA;
Przeznaczenia większości tych pól nietrudno się domyślić. Podobnie jak w przypadku innych struktur charakterystycznych dla WinAPI, pole
cbSize należy ustawić na rozmiar struktury, pobrany operatorem
sizeof. Jako
hWnd podajemy uchwyt okna, które będzie otrzymywało komunikaty związane z ikonką - zazwyczaj jest to główne okno naszego programu. Pole
uID to identyfikator ikonki, zaś
uFlags będzie określało, które z pozostałych trzech pól struktury są w danym momencie aktywne.
Bierzmy się zatem do dzieła, czyli do umieszczenia ikony naszego programu na tray'u. Najpierw, jak zwykle, wypełnimy strukturę:
LPSTR sTip = "Moja własna ikonka";
NOTIFYICONDATA nid;
nid.cbSize = sizeof( NOTIFYICONDATA );
nid.hWnd = hwnd;
nid.uID = ID_TRAY1;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = CMSG_TRAY1;
nid.hIcon = LoadIcon( NULL, IDI_APPLICATION );
lstrcpy( nid.szTip, sTip );
Teraz niezbędne wyjaśnienia. Użyliśmy wszystkich trzech możliwych flag, żeby za jednym zamachem ustawić ikonkę, tooltipa (czyli napis, który się wyświetla po najechaniu na ikonkę myszą) oraz kod specjalnego komunikatu (o nim później). Oczywiście stałe
ID_TRAY1 oraz
CMSG_TRAY1 musimy sobie sami zdefiniować:
#define ID_TRAY1 601
#define CMSG_TRAY1 0x8001
Twórcy Windowsa nie byliby sobą, gdyby od czasu do czasu nie zrobili programistom jakiegoś kawału, i oto mamy go - pole
szTip nie jest wskaźnikiem (jak w przypadku większości tego typu pól w WinAPI), lecz tablicą o stałym rozmiarze
64 znaków, dlatego wypełniamy ją przy pomocy funkcji
lstrcpy.
Wreszcie ostatnia rzecz: zakładam, że powyższa struktura będzie wypełniana już PO utworzeniu głównego okna programu, o uchwycie
hwnd. Zakładam również, że
wincl to nazwa struktury typu
WNDCLASSEX, która była użyta do rejestracji klasy okna i zawiera poprawny uchwyt do ikony.
Po tych przygotowaniach możemy wreszcie dodać ikonę do obszaru statusu:
BOOL r;
r = Shell_NotifyIcon( NIM_ADD, & nid );
if( !r ) MessageBox( hwnd, "No niestety, z ikonki nici...", "Łeeee...", MB_ICONEXCLAMATION );
Wszystko pięknie, ikonka widnieje sobie na tray'u:
Trochę nam się co prawda pokaszaniła ta ikona, ale dzięki temu mamy nauczkę na przyszłość: jeden z kolorów jest najwyraźniej używany jako kolor maski, więc należy się liczyć z tym, że na tray'u nie będzie go widać.
Poza tym drobiazgiem wszystko jest jednak OK. Zadbajmy jeszcze o to (programiści lubią sobie o tym zapominać), żeby nasza ikonka samoczynnie usuwała się w momencie zniszczenia głównego okna:
case WM_DESTROY:
{
NOTIFYICONDATA nid;
nid.cbSize = sizeof( NOTIFYICONDATA );
nid.hWnd = hwnd;
nid.uID = ID_TRAY1;
nid.uFlags = 0;
Shell_NotifyIcon( NIM_DELETE, & nid );
PostQuitMessage( 0 );
}
break;
Oczywiście w przypadku usuwania ikony nie musimy zawracać sobie głowy polami
hIcon, uCallbackMessage i
szTip, dlatego też ustawiliśmy
uFlags na
0. Teraz ikonka będzie znikała po wyjściu z programu (wcześniej działo się to dopiero po najechaniu na nią myszą).
Komunikaty
Teraz pora zająć się praktyczniejszym wykorzystaniem takich ikonek. Jak wiadomo, zwykle kliknięcie prawym przyciskiem myszy na takiej ikonce rozwija menu kontekstowe, z którego można wybrać pewne opcje programu (np. w WinAmp-ie można w ten sposób włączyć lub zatrzymać odtwarzanie, zmieniać skórki i wtyczki oraz wiele innych rzeczy). Zwykle też pojedyncze kliknięcie lewym przyciskiem przywołuje program na pierwszy plan (jeśli wcześniej działał on w tle). My zrobimy sobie to ostatnie; sprawimy, że po zminimalizowaniu naszego głównego okienka będzie ono znikało z głównego obszaru paska zadań i pojawiało się w tray'u.
Przede wszystkim musimy teraz sprawić, żeby po zrzuceniu do paska zadań okienko znikało z niego. Pomoże nam w tym znana nam dobrze funkcja
ShowWindow. Umieścimy ją w części kodu odpowiedzialnej za obsługę komunikatu
WM_SIZE. Tam będziemy sprwdzać, czy okienko jest minimalizowane (wtedy wywołamy
ShowWindow z parametrem
SW_HIDE), czy nie:
case WM_SIZE:
{
if( wParam == SIZE_MINIMIZED )
{
ShowWindow( hwnd, SW_HIDE );
}
else
{
ShowWindow( hwnd, SW_SHOW );
}
}
break;
W miejscach oznaczonych komentarzami wstawiamy ponadto:
1) kod dodający ikonkę do tray'a,
2) kod usuwający tę ikonkę. Warto zwrócić uwagę, że kod usuwający ikonkę z tray'a powinien wystąpić zarówno w powyższym, oznaczonym miejscu, jak i tam, gdzie wstawialiśmy go wcześniej, czyli w obsłudze komunikatu
WM_DESTROY, przy czym w tym drugim miejscu musimy teraz sprawdzić, czy okno jest zminimalizowane (bo tylko wtedy będzie sens usuwania ikony). Tak więc komunikat
WM_DESTROY będziemy teraz obsługiwać tak:
case WM_DESTROY:
{
if( IsIconic( hwnd ) )
{
}
PostQuitMessage( 0 );
}
break;
Nowością jest funkcja
IsIconic, która sprawdza, czy okno jest zminimalizowane - tylko wówczas pokazywana jest w naszym programie ikonka na tray'u. Równie dobrze moglibyśmy w tym przypadku użyć funkcji
IsWindowVisible.
Teraz pora na pozostałą część zadania - obsłużenie kliknięcia na ikonce. Do tej pory bowiem po zminimalizowaniu naszego okna nie dało się już bez stosowania tzw. cudów przywrócić do normalnego stanu - mogliśmy jedynie wcisnąć Ctrl+Alt+Del i wykopać program z pamięci siłą. Teraz sprawimy, by kliknięcie na ikonę sprowadzało okno z powrotem na pulpit.
Wykorzystamy w tym celu zdefiniowany przez nas komunikat
CMSG_TRAY1. Dlaczego nadaliśmy jej akurat taką wartość? Żeby nasz komunikat dało się odróżnić od tych systemowych - wszystkie numery od
0x8000 do
0xBFFF są do dyspozycji użytkownika jako "prywatne" komunikaty do użytku wewnątrz danej aplikacji.
Pozostaje nam teraz jedynie obsłużenie kliknięcia na naszej ikonce. Parametr
wParam komunikatu
CMSG_TRAY1 będzie zawierał identyfikator ikonki (w naszym przypadku
ID_TRAY1), natomiast
lParam - rodzaj komunikatu (w przypadku kliknięcia lewym przyciskiem - np.
WM_LBUTTONDOWN). Obsługa może wyglądać jakoś tak:
case CMSG_TRAY1:
{
if( wParam == ID_TRAY1 )
if( lParam == WM_LBUTTONDOWN )
ShowWindow( hwnd, SW_RESTORE );
}
break;
Ważne jest tutaj sprawdzenie, od której ikonki pochodzi komunikat (jeśli dodaliśmy kilka ikon; od "obcych" ikon komunikaty nie przychodzą) oraz jakie zdarzenie nam oznajmia. Szczególnie na to drugie należy zwrócić uwagę; gdybyśmy nie sprawdzili wartości
lParam, to nasze okno pojawiałoby się nawet po samym najechaniu myszą na ikonę (co jest efektem raczej niepożądanym).
Balonowe Komunikaty
Każdy użytkownik Windowsa zapewne wie, że w tray'u pojawiają się także takie balonowe komunikaty informujące o różnych bzdetach w stylu "Dostępne są nowe aktualizacje". Nic nie stoi jednak na przeszkodzie (przynajmniej w teorii ;-)) by takie komunikaty wykorzystać w naszej aplikacji.
Komunikaty balonowe działają na Windowsie 2000, XP, Vista i Seven czyli praktycznie na każdym w miarę nowym komputerze. Wróćmy do struktury
NOTIFYICONDATA. Przedstawiona ona została już na początku artykułu, jednak jest to wersja dla Windows 98. Nowe Windowsy mają "lepszą" wersję tej struktury. Oto jak mniej więcej jest ona zadeklarowana:
typedef struct _NOTIFYICONDATA {
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
#if _WIN32_IE >= 0x0500
CHAR szTip[ 128 ];
DWORD dwState;
DWORD dwStateMask;
CHAR szInfo[ 256 ];
union {
UINT uTimeout;
UINT uVersion;
} DUMMYUNIONNAME;
CHAR szInfoTitle[ 64 ];
DWORD dwInfoFlags;
#else
CHAR szTip[ 64 ];
#endif
#if _WIN32_IE >= 0x600
HICON hBalloonIcon;
GUID guidItem;
#endif
} NOTIFYICONDATA, * PNOTIFYICONDATA;
Jeżeli na komputerze użytkownika jest Internet Explorer w wersji 5 lub wyższej (czyli praktycznie na każdym komputerze z Windows) to możemy korzystać z balonowych komunikatów. Żeby je uaktywnić, na początku programu definiujemy wersję IE taką dyrektywą:
Teraz mamy do dyspozycji dodatkowe pola struktury
NOTIFYICONDATA. Oto pola, które będą nas interesować:
Struktura
NOTIFYICONDATA:
W parametrze
dwInfoFlags możemy wstawić różne flagi, oto kilka najczęściej używanych:
Flagi
dwInfoFlags:
Niektóre z tych flag są zdefiniowane w zależności od wartości makra
_WIN32_IE, innych w ogóle może nie być w starszych wersjach SDK, można więc na początku programu (przed
#include <windows.h>) dorzucić taki kod:
#define NIIF_NONE 0x00000000
#define NIIF_INFO 0x00000001
#define NIIF_WARNING 0x00000002
#define NIIF_ERROR 0x00000003
#define NIIF_USER 0x00000004
#define NIIF_NOSOUND 0x00000010
#define NIIF_LARGE_ICON 0x00000020
#define NIIF_RESPECT_QUIET_TIME 0x00000080
#define NIF_INFO 0x00000010
Teraz możemy już zrobić nasz balonowy komunikat:
LPSTR sTip = "Moja własna ikonka";
LPSTR sTytul = "Mój własny komunikat";
LPSTR sOpis = "To jest mój własny komunikat balonowy";
NOTIFYICONDATA nid;
nid.cbSize = sizeof( NOTIFYICONDATA );
nid.hWnd = hwnd;
nid.uID = ID_TRAY1;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO;
nid.uCallbackMessage = CMSG_TRAY1;
nid.hIcon = LoadIcon( NULL, IDI_APPLICATION );
nid.dwInfoFlags = NIIF_WARNING; lstrcpy( nid.szTip, sTip );
lstrcpy( nid.szInfoTitle, sTytul );
lstrcpy( nid.szInfo, sOpis );
Kod raczej nie jest skomplikowany. Na początku dorzuciliśmy flagę
NIF_INFO, żeby uaktywnić tą część struktury, która odpowiada za balonowe komunikaty. Do pola
dwInfoFlags wrzucamy flagę
NIFF_WARNING aby nasz balonik miał ikonkę ostrzeżenia. Potem kopiujemy zawartość stringów do struktury. Teraz tylko trzeba włożyć naszą ikonkę do tray'a:
BOOL r;
r = Shell_NotifyIcon( NIM_ADD, & nid );
if( !r ) MessageBox( hwnd, "No niestety, z ikonki nici...", "Łeeee...", MB_ICONEXCLAMATION );
i już możemy podziwiać rezultaty:
Obsługa balonowych komunikatów
Jak zapewne zauważyłeś, naszemu komunikatowi czegoś brakuje. Przecież w "profesjonalnych" aplikacjach, jak choćby Windows Update, po kliknięciu na dymek "Dostępne są nowe aktualizacje" wyświetla się okno, gdzie możemy instalować aktualizacje. Po kliknięciu naszego dymka nie dzieje się nic.
Najwyższy czas, żeby coś zaczęło się dziać. Jeżeli w naszym dymku wystąpi jakieś zdarzenie (np. kliknięcie, zamknięcie, itp.) to system wysyła do naszego programu odpowiednie powiadomienie. Znajdziemy je w komunikacie (zdefiniowanym w polu
uCallbackMessage) w niższym słowie parametru
lParam. Żeby go wydobyć można użyć makra
LOWORD. Najpierw jednak omówimy powiadomienia, które system może do naszego programu wysłać:
Powiadomienia związane z balonowym komunikatem (dymkiem) w trayu:
Możliwe, że w twojej wersji SDK nie będzie definicji tych powiadomień. W takim wypadku powinieneś wstawić taki kod na początku programu:
#define NIN_BALLOONSHOW 0x00000402
#define NIN_BALLOONHIDE 0x00000403
#define NIN_BALLOONTIMEOUT 0x00000404
#define NIN_BALLOONUSERCLICK 0x00000405
Jak już wspomniałem wcześniej, powiadomienie wysyłane jest w niższym słowie parametru
lParam. Oto jak takie powiadomienia obsługiwać (zakładam że pole
uCallbackMessage ma wartość
CMSG_TRAY1 z początku artykułu, a
ID_TRAY1 to ID ikonki):
LRESULT CALLBACK WindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) {
switch( message ) {
case CMSG_TRAY1:
switch( wParam ) {
case ID_TRAY1: switch( LOWORD( lParam ) ) {
case NIN_BALLOONUSERCLICK: MessageBox( hwnd, "Fajnie że kliknąłeś mój balonik.", ";-)", MB_ICONEXCLAMATION );
break;
case NIN_BALLOONTIMEOUT: MessageBox( hwnd, "Dlaczego nie kliknąłeś mojego balonika, co ?!", ";-(", MB_ICONEXCLAMATION );
break;
default:
break;
}
break;
}
break;
}
return 0;
}
W powyższym kodzie mamy podaną przykładową obsługę komunikatu balonowego. W komunikacie
CMSG_TRAY1 w części dotyczącej naszej ikonki (
case ID_TRAY1:) sprawdzamy, jakie powiadomienie zostało wysłane (
switch (LOWORD(lParam)) { ... }). Jeżeli nie wysłano żadnego powiadomienia, to instrukcja
LOWORD(lParam) będzie mieć wartość
0. Wtedy wykona się kod oznaczony "tu instrukcje nie związane z powiadomieniami". Możemy dodać też inne case'y, np.
WM_LBUTTONDOWN do obsługi minimalizacji okienka do tray'a z poprzedniego rozdziału.
W naszym przykładzie po kliknięciu dymka, wyświetlają się głupawe komunikaty. Nic nie stoi na przeszkodzie, by wykorzystać nasz dymek do różnych niecnych celów ;-). W dziale
[[Download|download]]
znajdziesz gotowy nagłówek wypełniający ewentualne braki w twojej wersji SDK.
Kompatybilność pod XP i Vistą
W systemie Windows Vista można umieścić ikonę w balonowym komunikacie niezależnie od ikonki w tray'u, umieszczając uchwyt do ikony w polu
hBalloonIcon. Jeżeli używamy pola
hBallonIcon to nasz komunikat nie wyświetli się pod XP ;-(. Ponadto flaga
NIIF_USER działa dopiero od Windows XP SP2.
Nie wszystkie kompilatory (np. Dev-C++) posiadają strukturę z tym polem. Jeżeli używasz Dev-C++ to ściągnij z
[[Download|downloadu]]
plik związany z tym artykułem, gdzie znajdziesz poprawiony nagłówek
shellapi.h dla kompilatora Dev-C++. Użytkownicy Visuala nie muszą się tym przejmować - u nich to pole jest.
Tak więc co zrobić, żeby nasz balonik działał zarówno pod i XP jak i Vistą? Moglibyśmy po prostu korzystać z wersji struktury
NOTIFYICONDATA pod XP, ale pod Vistą można mieć w balonie niezależną ikonkę, a my bardzo taką chcemy ;-). Żeby to osiągnąć musimy w zależności od wersji DLL shell'a podać różny rozmiar struktury
NOTIFYICONDATA. Najpierw jedna trzeba poznać wersję tej DLL'ki. Robimy to za pomocą funkcji
DllGetVersion. Nie jest ona częścią API więc musimy ją "ręcznie" załadować z DLL'ki, której wersję chcemy poznać. Przyjmuje ona jeden argument, jakim jest wskaźnik na strukturę
DLLVERSIONINFO. Oto ona:
typedef struct _DllVersionInfo {
DWORD cbSize;
DWORD dwMajorVersion;
DWORD dwMinorVersion;
DWORD dwBuildNumber;
DWORD dwPlatformID;
} DLLVERSIONINFO;
Pole
cbSize to rozmiar struktury. Najważniejsze są jednak pola
dwMajorVersion,
dwMiniorVersion i
dwBuildNumber. To one przechowują informacje o wersji. Teraz możemy się zabrać do pobrania informacji o wersji:
DLLVERSIONINFO dvi;
ZeroMemory( & dvi, sizeof( DLLVERSIONINFO ) );
dvi.cbSize = sizeof( DLLVERSIONINFO );
DllGetVersion( & dvi );
Mamy już wersję systemu, teraz w zależności od niej powinniśmy dać rozmiar strukturze
NOTIFYICONDATA. Ale jaki rozmiar? Zostały one zebrane w tabelce:
Jeżeli w twojej wersji SDK te rozmiary nie są zdefiniowane to pobierz z
[[Download|download]]
plik związany z tą częścią kursu. Tam znajdziesz odpowiedni plik nagłówkowy.
To teraz zobaczmy jak to wygląda w praktyce:
#include <shlwapi.h>
HINSTANCE hDll;
LPCTSTR lpszDllName = TEXT( "Shell32.dll" );
hDll = LoadLibrary( lpszDllName );
DLLGETVERSIONPROC DllGetVersion;
if( hDll != NULL ) {
DllGetVersion =( DLLGETVERSIONPROC ) GetProcAddress( hDll,
"DllGetVersion" );
if( DllGetVersion == NULL ) {
MessageBox( hwnd, "Nie można załadować funkcji!", "Bład!", MB_ICONSTOP );
return 0;
}
} else {
MessageBox( hwnd, "Nie można załadować DLL'ki!", "Bład!", MB_ICONSTOP );
return 0;
}
DLLVERSIONINFO dvi;
ZeroMemory( & dvi, sizeof( DLLVERSIONINFO ) );
dvi.cbSize = sizeof( DLLVERSIONINFO );
( * DllGetVersion )( & dvi );
LPSTR sTip = "Moja własna ikonka";
LPSTR sTytul = "Mój własny komunikat";
LPSTR sOpis = "To jest mój własny komunikat balonowy";
NOTIFYICONDATA nid;
ZeroMemory( & nid, sizeof( NOTIFYICONDATA ) );
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO;
if(( dvi.dwMajorVersion == 6 ) &&(( dvi.dwMinorVersion == 0 ) &&( dvi.dwBuildNumber >= 6000 ) ||( dvi.dwMinorVersion >= 1 ) ) ) { nid.cbSize = sizeof( NOTIFYICONDATA );
nid.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON;
nid.hBalloonIcon = LoadIcon( hInstance, MAKEINTRESOURCE( 666 ) );
} else if(( dvi.dwMajorVersion == 6 ) &&( dvi.dwMinorVersion == 0 ) ) { nid.cbSize = NOTIFYICONDATA_V3_SIZE;
if( dvi.dwPlatformID > 2800 ) { nid.dwInfoFlags = NIIF_USER;
} else { nid.dwInfoFlags = NIIF_WARNING;
}
} else if(( dvi.dwMajorVersion == 5 ) &&( dvi.dwMinorVersion == 0 ) ) { nid.cbSize = NOTIFYICONDATA_V2_SIZE;
nid.dwInfoFlags = NIIF_WARNING;
} else { nid.cbSize = NOTIFYICONDATA_V1_SIZE;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; }
nid.hWnd = hwnd;
nid.uID = ID_TRAY1;
nid.uCallbackMessage = CMSG_TRAY1;
nid.hIcon = LoadIcon( NULL, IDI_APPLICATION );
lstrcpy( nid.szTip, sTip );
lstrcpy( nid.szInfoTitle, sTytul );
lstrcpy( nid.szInfo, sOpis );
BOOL r;
r = Shell_NotifyIcon( NIM_ADD, & nid );
if( !r ) MessageBox( hwnd, "No niestety, z ikonki nici...", "Łeeee...", MB_ICONEXCLAMATION );
FreeLibrary( hDll );
Kod jest dość przydługi, ale działa (co w Windowsie wcale nie jest takie oczywiste ;-)). No może nie do końca. Użyliśmy ikonki o id
666 z zasobów. Musimy więc stworzyć odpowiedni plik
*.rc:
Teraz nadszedł czas na wyjaśnienia. Na początku ładujemy funkcję
DllGetVersion z
Shell32.dll. Potem tworzymy strukturę
DLLVERSIONINFO i wczytujemy do niej informacje o wersji funkcją
DllGetVersion. Później przygotowujemy zmienne na napisy w balonie i strukturę
NOTIFYICONDATA. Następnie mamy dużo instrukcji warunkowych, które w zależności od wersji systemu wypełniają strukturę
NOTYFIICONDATA w róźny sposób. By rozpoznać, czy Windows jest SP2 lub nowszy sprawdzamy parametr
dwPlatformID. Liczba
2800 oznacza SP1, stąd każda większa będzie oznaczać kolejne SP (2900 - SP3). Następnie wstawiamy naszą ikonę do tray'a i zwalniamy DLL'kę.
Dzięki naszym staraniom komunikat działa poprawnie pod większością Windowsów (kolejno Seven, Vista, XP SP3, XP SP1):