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

Menu

[lekcja]

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:

C/C++
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:

C/C++
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:

Nasze pierwsze menu - ciasne ale własne ;-) (Windows 98)
Nasze pierwsze menu - ciasne ale własne ;-) (Windows 98)

 
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.:

C/C++
#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:

C/C++
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:

C/C++
MENUITEM "&JPEG", 105, GRAYED

Teraz menu zaczyna wyglądać 'profesjonalnie' ;-) (Windows 98)
Teraz menu zaczyna wyglądać 'profesjonalnie' ;-) (Windows 98)

Obydwu "efektów" możemy użyć jednocześnie:

C/C++
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:

C/C++
MENUITEM "&Otwórz", 101, MENUBARBREAK

Trochę śmiesznie, ale gdyby było więcej pozycji w tym menu... (Windows 98)
Trochę śmiesznie, ale gdyby było więcej pozycji w tym menu... (Windows 98)


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:

C/C++
MENUITEM "&Nowy\tCtrl+N", 100
MENUITEM "&Otwórz\tCtrl+O", 101
MENUITEM "&Zapisz\tCtrl+Z", 102

Skróty niby są, chociaż nie działają :-/ (Windows 98)
Skróty niby są, chociaż nie działają :-/ (Windows 98)

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:

C/C++
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:
 
C/C++
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:

W menu cosik jakby przybyło ;-) (Windows 98)
W menu cosik jakby przybyło ;-) (Windows 98)

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:

C/C++
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:

C/C++
mii.fType = MFT_STRING | MFT_RADIOCHECK;
mii.fState = MFS_CHECKED;

Fajne te kółeczka, nie? ;-) (Windows 98)
Fajne te kółeczka, nie? ;-) (Windows 98)

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ę:

C/C++
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:

Robi się kolorowo i wesoło ;-) (Windows 98)
Robi się kolorowo i wesoło ;-) (Windows 98)

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:

C/C++
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):

C/C++
BOOL g_Checked = FALSE;

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:
 
C/C++
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:

C/C++
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 ;-).

Menu kontekstowe (Windows 98)
Menu kontekstowe (Windows 98)
Poprzedni dokument Następny dokument
Okna dialogowe, cz. 8 Akceleratory