Menu i pliki zasobów
Program windowsowy bez menu to jak komputer bez klawiatury i myszki - niby działa, ale nijak się nie można do niego dobrać ;-). Menu jest zwykłą kontrolką, jednak traktowaną w specjalny sposób. Najprostszą metodą stworzenia menu jest napisanie skryptu *.rc. Oto przykładowy plik:
200 MENU
{
POPUP "&Plik"
{
MENUITEM "&Nowy", 100
MENUITEM "&Otwórz", 101
MENUITEM "&Zapisz", 102
MENUITEM SEPARATOR
POPUP "&Importuj"
{
MENUITEM "&Tekst", 103
POPUP "O&braz"
{
MENUITEM "Bit&mapa", 104
MENUITEM "&JPEG", 105
}
}
MENUITEM SEPARATOR
MENUITEM "&Koniec", 106
}
MENUITEM "&Edycja", 108
}
Jedyne, co musimy jeszcze zrobić, aby nasze pierwsze menu pojawiło się w okienku, to załadować je funkcją
LoadMenu, a następnie przypisać do konkretnego okna. Funkcja
LoadMenu ma dwa parametry: uchwyt programu (typ
HINSTANCE), oraz identyfikator menu w pliku (u nas jest nim liczba
200, którą "przerabiamy" na identyfikator makrem
MAKEINTRESOURCE). Zwraca natomiast uchwyt do menu (jak nietrudno się domyślić, uchwyt ten jest typu
HMENU). Oto jak to wygląda:
HMENU hMenu = LoadMenu( hThisInstance, MAKEINTRESOURCE( 200 ) );
Powyższą linijkę kodu powinieneś wstawić PRZED instrukcją tworzącą okno (
CreateWindowEx), ponieważ jak już okno będzie gotowe, trudniej nam będzie wstawić do niego menu ;-). Oczywiście w miejsce
hThisInstance wstawiasz swój własny uchwyt do aplikacji.
Mamy więc uchwyt do nowego menu, teraz tylko wstawimy go do funkcji
CreateWindowEx, żeby menu pojawiło się w głównym oknie naszego programiku. Pamiętasz trzeci od końca parametr funkcji
CreateWindowEx? Zwykle ustawialiśmy go na
NULL, ewentualnie gdy używaliśmy tej funkcji do tworzenia kontrolek, to wstawialiśmy tam identyfikator kontrolki. Teraz po prostu wpisujemy w to miejsce
hMenu, czyli nasz uchwyt do menu i już możemy podziwiać efekty:
Teraz warto by sobie omówić te wszystkie zaklęcia, których użyliśmy w skrypcie ;-). Każda pozycja menu odpowiada jednemu słowu
MENUITEM, po którym następuje etykieta menu oraz identyfikator danej pozycji. W etykiecie mogą wystąpić symbole
&, które oznaczają literkę, która będzie w danym menu podkreślona. Chyba wiesz, o co biega - dzięki temu będzie można obsługiwać twoje menu z klawiatury (prawy ALT + litera) i w dodatku będzie się to działo automatycznie, bez konieczności pisania dodatkowego kodu (!).
Identyfikatory kolejnych pozycji menu muszą być unikalne w skali programu (tak jak wszystkie inne zasoby w plikach *.rc, rzecz jasna). Oczywiście zamiast "gołych" liczb warto stosować "stałe", np.:
#define MNU_PLIK_NOWY 100
Zamiast
MENUITEM możemy użyć
POPUP. Jak już pewnie się domyślasz, dzięki temu dana pozycja menu będzie rozwijalna, czyli najechanie na nią myszą doprowadzi do pojawienia się kolejnego poziomu menu, tak jak na obrazku powyżej. Pozycja menu zdefiniowana przez
POPUP nie może zostać wybrana (jest tylko, hmm, etapem przejściowym w drodze do kolejnej pozycji ;-)), toteż nie posiada własnego identyfikatora.
Specjalnym rodzajem pozycji menu jest
MENUITEM SEPARATOR - czyli kreski rozdzielające poszczególne grupy pozycji menu, żeby menu wyglądało estetyczniej. Tutaj chyba nie potrzeba dalszych komentarzy.
Jako stary windowsowy wyjadacz na pewno niejedno menu już widziałeś ;-). Tak więc pewnie czujesz, że czegoś ci jeszcze brak. Nawet taka prosta aplikacyjka jak Notatnik, w którym piszę właśnie ten kurs, posiada zwykle dodatkowe drobne usprawnienia w swoim menu - niektóre pozycje są "zafajkowane" (''checked''), inne są nieaktywne, czyli nie można ich chwilowo wybrać i są wypisane szarym kolorem. Pierwszy efekt osiągamy przez dodanie (po przecinku) słowa
CHECKED do konkretnej pozycji menu:
MENUITEM "Bit&mapa", 104, CHECKED
Niestety, "fajeczka" nie zniknie automatycznie po kliknięciu na zafajkowaną pozycję :-(. Musimy o to zadbać sami (o tym później). Aby zaś chwilowo wyłączyć daną pozycję, dodajemy w ten sam sposób słowo
GRAYED:
MENUITEM "&JPEG", 105, GRAYED
Obydwu "efektów" możemy użyć jednocześnie:
MENUITEM "Bit&mapa", 104, CHECKED, GRAYED
Warto wiedzieć o parametrach
MENUBREAK i
MENUBARBREAK. Pozwalają one podzielić dane podmenu na kilka kolumn, przy czym
MENUBARBREAK dodatkowo wstawia pionową linię w miejscu podziału, a
MENUBREAK - nie. I tak na przykład gdybyśmy dodali
MENUBARBREAK do pozycji 'Otwórz', to orzymalibyśmy taki efekt:
MENUITEM "&Otwórz", 101, MENUBARBREAK
Niezbyt ładne, ale gdy mamy w menu więcej pozycji, rozwiązanie takie bywa całkiem przydatne. Ciekawe rezultaty może dać użycie tych dwóch parametrów wobec pozycji z głównej belki menu (np. Plik, Edycja itp.). Kiedy tak zrobimy, będą one ułożone jedna pod drugą, zamiast obok siebie. W tym przypadku nie ma żadnej różnicy między
MENUBREAK i
MENUBARBREAK.
Większość komend menu posiada klawiaturowe skróty, umożliwiające wykonanie tych komend bez konieczności rozwijania menu. Skróty wyświetlane są po prawej stronie menu i zazwyczaj są to kombinacje Ctrl+litera. Możemy sobie utworzyć takie skróty, coby jeszcze bardziej uzywilizować nasz prześliczne menu:
MENUITEM "&Nowy\tCtrl+N", 100
MENUITEM "&Otwórz\tCtrl+O", 101
MENUITEM "&Zapisz\tCtrl+Z", 102
Jak widać, polega to po prostu na dodaniu do etykiety tabulacji (symbol
\t) i wpisaniu skrótu. Niestety, automatycznie ten system skrótów nie działa, musimy skorzystać z tzw. '''akceleratorów''', o których dowiemy się w osobnej części tego kursu.
Modyfikacja menu w fazie wykonywania
Czasami możemy chcieć zmienić menu w trakcie wykonywania programu, np. gdy dajemy użytkownikowi możliwość dostosowania menu do własnych potrzeb. W takich wypadkach możemy użyć funkcji
CreateMenu i
CreatePopupMenu do stworzenia nowego, pustego menu, po czym wypełnić je przy pomocy funkcji
InsertMenuItem. Oczywiście nic nie stoi na przeszkodzie, by dodać nową pozycję do gotowego menu stworzonego z pliku zasobów, co za moment sobie zrobimy.
Słusznie się obawiasz - czeka nas teraz pracowite wypełnianie struktury. Zowie się ona
MENUITEMINFO i jak zwykle ma sporo pól, ale na szczęście nie wszystkie musimy wypełniać (też jak zwykle). Robiliśmy już takie rzeczy nieraz, więc szczegóły pomijam. Oto kompletna struktura, gotowa do użycia:
MENUITEMINFO mii;
ZeroMemory( & mii, sizeof( mii ) );
mii.cbSize = sizeof( mii );
mii.fMask = MIIM_ID | MIIM_TYPE;
mii.fType = MFT_STRING;
mii.wID = 110;
mii.dwTypeData = "Reset";
Pole
fMask określa, jakie z pozostałych pól będziemy wypełniać. Potrzebujemy tylko dwóch:
fType i
wID, co widać po doborze flag (
MIIM_TYPE i
MIIM_ID). W polu
fType dajemy stałą
MFT_STRING, co oznacza, że chcemy podać etykietę dla naszej nowej pozycji menu. Etykietę tę wrzucamy żywcem do pola
dwTypeData. Pozostaje tylko ustalić identyfikator dla nowej pozycji menu - niech to będzie liczba
110.
Nowa pozycja menu gotowa - zrobiliśmy polecenie do resetowania kompa, ale z nas wredne istoty ;-). Teraz korzystamy ze wspomnianej już funkcji
InsertMenuItem. Podajemy jej kolejno: uchwyt do menu, identyfikator pozycji PRZED którą chcemy umieścić naszą nową pozycję (tutaj wstawiamy 'Reset' przed pole 'Koniec', więc musimy podać identyfikator tego ostatniego -
106), wartość
FALSE (lub
TRUE, jeśli zamiast identyfikatora podaliśmy indeks pozycji), wreszcie adres naszej wypełnionej struktury:
InsertMenuItem( hMenu, 106, FALSE, & mii );
Powyższą instrukcję możemy umieścić zarówno przed utworzeniem okna zawierającego nasze menu, jak i po nim. Tadaaa! Gotowe:
Na razie umiemy tylko dodawać "zwykłe" pozycje menu - a jak dodać pozycje nieaktywne lub zafajkowane? Otóż musimy się dodatkowo zainteresować polem
fState naszej struktury. Jeśli wpisać do tego pola
MFS_GRAYED - nowa pozycja menu będzie "szara", jeśli damy
MFS_CHECKED - zafajkujemy ją :-). Oczywiście musimy uwzględnić pole
fState przy wypełnianiu
fMask:
mii.fMask = MIIM_ID | MIIM_TYPE | MIIM_STATE;
mii.fState = MFS_CHECKED | MFS_ GRAYED;
...i już mamy naszą pozycję 'Reset' oznaczoną fają i nieaktywną w dodatku.
Aby wstawić separator, w miejsce
MFT_STRING dajemy
MFT_SEPARATOR. Oczywiście wtedy ustawianie etykiety ani identyfikatora nie jest konieczne.
Jak być może zauważyłeś, w różnych programach oprócz "fajek" występują też kółeczka. Możemy też sobie takie zrobić. Wystarczy do
fType dorzucić stałą
MFT_RADIOCHECK:
mii.fType = MFT_STRING | MFT_RADIOCHECK;
mii.fState = MFS_CHECKED;
Jak widzimy, "ręczne" dodawanie menu jest zbyt pracochłonne, żeby korzystać z tej możliwości do stworzenia całego menu, więc cieszmy się, że dobrzy ludzie wynaleźli pliki skryptowe ;-).
Ikonki menu
Naprawdę wypasione programy, jak choćby Dev-C++, mają w menu ikonki. My też takie chcemy, a dla chcącego jak wiadomo nie ma nic trudnego ;-). Możemy wykorzystać fakt, że domyślne fajki i kółeczka dla oznaczenia stanu
CHECKED mogą zostać wymienione na własne obrazki, niekoniecznie przedstawiające fajki i kółka ;-).
Do wyboru mamy: wypełnienie pól
hbmpChecked i
hbmpUnchecked naszej kochanej struktury uchwytami do bitmap, które musimy sobie sami wczytać, oraz skorzystanie z funkcji
SetMenuItemBitmaps, która zrobi to samo przy mniejszej ilości pisaniny ;-). Oczywiście wybieramy tę drugą opcję:
HBITMAP hbmMorda =( HBITMAP ) LoadImage( hThisInstance, "morda.bmp",
IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_LOADTRANSPARENT );
SetMenuItemBitmaps( hMenu, 110, MF_BYCOMMAND, hbmMorda, hbmMorda );
Jeśli pamiętasz jeszcze kurs grafiki w WinAPI, to pewnie wiesz, jak korzystać z funkcji
LoadImage. Dodaliśmy do niej jedną flagę -
LR_LOADTRANSPARENT. Dzięki niej funkcja zastępuje kolor pierwszego piksela naszego obrazka w całej bitmapie na domyślny kolor systemowy menu, dzięki czemu nasz obrazek ma "przezroczyste" tło.
Zanim przystąpisz do wybierania obrazka, zadbaj o jego wymiary. Domyślne wymiary zależą od wersji systemu, zwykle jest to 16x16. Aby wszystko było cacy, możesz pobrać te wymiary funkcją
GetSystemMetrics, stosując flagi
CXMENUCHECK i
CYMENUCHECK, a następnie odpowiednio wyskalować wczytaną bitmapę (najlepiej zrobić to od razu w wywołaniu funkcji
LoadImage).
Funkcja
SetMenuItemBitmaps pobiera kolejno: uchwyt menu, identyfikator pozycji, której dorabiamy ikonkę, flagę
MF_BYCOMMAND (oznacza, że poprzedni parametr jest identyfikatorem pozycji menu), wreszcie najważniejsze - dwa uchwyty; bitmapa "zafajkowana" i bitmapa "odfajkowana". U nas daliśmy do obu identyczną bitmapę, co zresztą robi się prawie zawsze przy korzystaniu z ikon menu. Efekt? Proszę bardzo:
Menu i komunikaty
To naprawdę fajnie, że umiemy już zrobić menu ze wszelkimi bajerami, ale co nam po nim, skoro klikanie na tym menu nic nie daje? Trzeba zrobić obsługę komunikatów. Kliknięcie na wybranej pozycji menu powoduje wysłanie komunikatu
WM_COMMAND. Jak nietrudno się domyślić, parametr
wParam (a ściślej: jego dolne słowo) zawiera identyfikator pozycji menu, która została kliknięta. Tak więc nietrudno będzie przypisać odpowiednim pozycjom menu jakieś działanie:
case WM_COMMAND:
{
if( LOWORD( wParam ) == 106 )
PostQuitMessage( 0 );
else
MessageBox( hwnd, "Dziękujemy za skorzystanie z naszego menu ;-P",
"Mesydż", MB_ICONINFORMATION );
}
break;
Tutaj poszliśmy sobie na łatwiznę i dodaliśmy działanie menu 'Koniec', natomiast wybranie jakiejkolwiek innej pozycji spowoduje wyświetlenie okna z wiadomością.
Skoro mamy już możliwości, to nauczmy się wreszcie, co zrobić z tymi cholernymi fajeczkami, żeby działały jak Pan Bóg przykazał. Wszystkie atrybuty menu można zmienić funkcją
SetMenuItemInfo, która korzysta z omówionej już struktury
MENUITEMINFO. Nie przepadamy jednak za tą strukturą i korzystamy z niej tylko wtedy, gdy już naprawdę nie da się inaczej. A w przypadku fajeczek się da, bo mamy w zanadrzu całkiem miłą funkcyjkę
CheckMenuItem. Wywołuje się ją bardzo podobnie do
SetMenuItemBitmaps, tyle że do flag (trzeci argument) dodajemy dodatkowo
MF_CHECKED lub
MF_UNCHECKED (no i oczywiście nie podajemy uchwytów do bitmap, bo nie do tego ta funkcja służy).
Przechodząc do praktyki - potrzebować będziemy globalnej zmiennej, przechowującej stan danej pozycji menu (zafajkowana/odfajkowana):
Następnie wybieramy sobie jakąś pozycję menu, która ma być fajkowana - niech to będzie ta z plikiem JPEG w etykiecie (identyfikator:
105). W obsłudze
WM_COMMAND piszemy:
case WM_COMMAND:
{
if( LOWORD( wParam ) == 105 )
{
g_Checked = ~g_Checked;
CheckMenuItem( hMenu, 105, MF_BYCOMMAND |
( g_Checked ) ? MF_CHECKED
: MF_UNCHECKED );
}
}
break;
Jeśli korzystasz raczej z kółeczek, niż z fajeczek, czyli masz grupę pozycji menu, z których tylko jedna może być w danym momencie zaznaczona, to przydatniejsza będzie dla ciebie funkcja
CheckMenuRadioItem, której dodatkowo podajemy identyfikatory pierwszej i ostatniej pozycji w grupie, dzięki czemu może ona odznaczyć wcześniej zaznaczoną pozycję i zaznaczyć nową (heh ;-)).
Menu kontekstowe
Ostatnią rzeczą związaną z menu, jaką sobie omówimy, będą menu kontekstowe/podręczne/skrótowe/wyskakujące czy jak je tam sobie nazwiemy (po angielsku: ''popup menus'' lub ''shortcut menus''), w każdym razie chodzi o menu, pojawiające się zwykle po naciśnięciu prawego przycisku myszy. Możemy sobie takowe utworzyć całkiem oddzielnie od istniejącego już paska menu albo też skorzystać z tego, co już mamy zrobione. Bardzo często menu kontekstowe jest właśnie wycinkiem "właściwego" menu (zwłaszcza menu 'Edycja').
Oto co trzeba zrobić, żeby prawy przycisk gryzonia pokazywał nam menu 'Plik' w dowolnym miejscu naszego okienka:
POINT point;
point.x = LOWORD( lParam );
point.y = HIWORD( lParam );
ClientToScreen( hwnd, & point );
TrackPopupMenu( GetSubMenu( hMenu, 0 ), 0, point.x, point.y, 0, hwnd, NULL );
Pierwszy argument tej fajnej funkcji służy, jak sama nazwa wskazuje, do pobrania podmenu. Podmenu to takie menu, które rozwija się w dół lub w bok. Aby pobrać uchwyt podmenu 'Plik', musimy podać funkcji
GetSubMenu wartość zero jako drugi argument, ponieważ nasze menu 'Plik' jest na samym poczatku całego menu.
Następne dwa atrybuty to współrzędne, gdzie ma się pojawić menu kontekstowe - dajemy tu współrzędne kliknięcia. Muszą być to współrzędnych ekranowe, a współrzędne w parametrze
lParam to współrzędne w oknie, dlatego trzeba dokonać odpowiedniej konwersji przy użyciu funkcji
ClientToScreen. Następny argument jest zarezerwowany tylko-dla-Microsoftu i ma być równy
0. Wreszcie dajemy uchwyt do okna i
NULL. W tym ostatnim możemy podać adres do prostokąta, w którym kliknięcie nie powoduje schowania menu kontekstowego. Jeśli zaś pójdziemy na łatwiznę i wrzucimy tam
NULL, to menu będzie chowane zawsze wtedy, gdy klikniemy na zewnątrz niego, co w większości przypadków chcemy właśnie osiągnąć. I gotowe ;-).