Menu i ContextMenu
Każdy „poważny” program ma u góry pasek menu: Plik, Edycja, Widok, Pomoc. A po kliknięciu prawym przyciskiem myszy pokazuje krótkie menu z poleceniami dla klikniętego elementu. W tej lekcji dodasz oba rodzaje menu do listy zadań, wyłączysz polecenia, które w danej chwili nie mają sensu, i obsłużysz skróty klawiszowe.
Czego się dziś nauczysz
- Zbudujesz pasek menu z podmenu i separatorami.
- Dodasz klawisze dostępu (Alt + litera) i obsłużysz skróty typu Ctrl + N.
- Użyjesz pozycji menu, które można zaznaczyć jak pole wyboru.
- Dodasz menu kontekstowe do listy.
- Wyłączysz pozycje menu tuż przed jego otwarciem, gdy polecenie nie ma sensu.
Klawisz dostępu i skróty
Podkreślnik przed literą w Header ustawia klawisz dostępu — tak jak w Label z lekcji 10. _Plik otwiera się przez Alt + P, a w otwartym menu wystarczy już sama litera.
Skrót (np. Ctrl + N) działa bez otwierania menu. InputGestureText tylko wypisuje skrót obok pozycji — samo naciśnięcie klawiszy trzeba obsłużyć w KeyDown okna, jak w lekcji 26:
<Window ... KeyDown="Window_KeyDown">
...
<MenuItem Header="_Nowy" InputGestureText="Ctrl+N" Click="Nowy_Click"/>
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.N)
{
Nowy_Click(sender, e); // ta sama metoda co kliknięcie w menu
e.Handled = true;
}
}
Metodę obsługi zdarzenia można wywołać jak każdą inną. KeyEventArgs jest rodzajem RoutedEventArgs, więc pasuje jako drugi argument. Dzięki temu menu, skrót i ewentualny przycisk robią dokładnie to samo.
Dlaczego tak?
W WPF istnieje mechanizm poleceń, w którym skrót i pozycja menu są połączone automatycznie. Poznasz go w lekcji 53 (ICommand). Na razie wystarczy KeyDown.
Pozycje zaznaczane i wyłączane
<MenuItem Header="_Widok" SubmenuOpened="Widok_SubmenuOpened">
<MenuItem x:Name="PasekStanu" Header="Pasek stanu" IsCheckable="True" IsChecked="True"
Click="PasekStanu_Click"/>
<MenuItem x:Name="Powieksz" Header="Powiększ" Click="Powieksz_Click"/>
</MenuItem>
private void PasekStanu_Click(object sender, RoutedEventArgs e)
{
// IsChecked zmieniło się już samo — tylko z niego korzystamy
Stan.Visibility = PasekStanu.IsChecked ? Visibility.Visible : Visibility.Collapsed;
}
// tuż przed pokazaniem podmenu — dobry moment, żeby wyłączyć zbędne pozycje
private void Widok_SubmenuOpened(object sender, RoutedEventArgs e)
{
Powieksz.IsEnabled = Tekst.FontSize < 30;
}
IsCheckable="True" sprawia, że kliknięcie przełącza znaczek ✓ przy pozycji. MenuItem.IsChecked to zwykły bool (nie bool? jak w CheckBox), więc nie trzeba porównywać z true. Bez IsCheckable znaczek można ustawić z kodu przez IsChecked, ale kliknięcie go nie zmienia.
Wyłączona pozycja (IsEnabled = false) jest wyszarzona i nie da się jej kliknąć. To lepsze niż komunikat „najpierw coś zaznacz” po kliknięciu.
ContextMenu
Menu kontekstowe otwiera się po kliknięciu prawym przyciskiem. Przypina się je do konkretnej kontrolki przez właściwość ContextMenu, zapisaną jako element:
<ListBox x:Name="Uczniowie">
<ListBox.ContextMenu>
<ContextMenu Opened="Kontekst_Opened">
<MenuItem x:Name="KontekstObecny" Header="Obecny" Click="Obecny_Click"/>
<Separator/>
<MenuItem Header="Usuń z listy" Click="Usun_Click"/>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
Kliknięcie prawym przyciskiem na element listy najpierw go zaznacza, a potem otwiera menu — więc w metodach wystarczy odczytać SelectedItem. Zdarzenie Opened zachodzi przy każdym otwarciu i służy do tego samego co SubmenuOpened: dopasowania pozycji do sytuacji.
prawy przycisk na „Borkowski Igor” — element jest zaznaczony, menu otwarte obok kursora
Każda kontrolka może mieć własne menu kontekstowe: pole tekstowe ma nawet wbudowane (Wytnij, Kopiuj, Wklej), a przypięcie własnego je zastępuje.
Lista „Do zrobienia”
Prosty menedżer zadań z pełnym menu. Zadania są obiektami klasy Zadanie (jak książki w lekcji 23). Polecenia z menu „Edycja”, menu kontekstowego i skróty klawiszowe korzystają z tych samych metod. Menu „Widok” ma pozycje zaznaczane, które filtrują i porządkują listę, oraz podmenu z rozmiarem tekstu.
otwarte menu „Edycja” — skróty wypisane z prawej strony, podkreślone litery to klawisze dostępu
namespace Lekcja35;
// Jedno zadanie z listy rzeczy do zrobienia
public class Zadanie
{
public string Tresc { get; set; }
public bool Zrobione { get; set; }
public bool Wazne { get; set; }
public Zadanie(string tresc)
{
Tresc = tresc;
}
public override string ToString()
{
string pole = Zrobione ? "☑" : "☐";
string gwiazdka = Wazne ? " ★" : "";
return $"{pole} {Tresc}{gwiazdka}";
}
}
<Window x:Class="Lekcja35.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Do zrobienia" Width="420" Height="420" MinWidth="320" MinHeight="300"
WindowStartupLocation="CenterScreen" KeyDown="Window_KeyDown">
<DockPanel>
<!-- menu główne zawsze u góry okna -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_Plik">
<MenuItem Header="_Nowa lista" InputGestureText="Ctrl+N" Click="Nowa_Click"/>
<Separator/>
<MenuItem Header="_Zakończ" InputGestureText="Alt+F4" Click="Zakoncz_Click"/>
</MenuItem>
<MenuItem Header="_Edycja" SubmenuOpened="Edycja_SubmenuOpened">
<MenuItem x:Name="MenuZrobione" Header="Oznacz jako _zrobione"
InputGestureText="Ctrl+D" Click="Zrobione_Click"/>
<MenuItem x:Name="MenuWazne" Header="_Ważne" InputGestureText="Ctrl+W"
Click="Wazne_Click"/>
<MenuItem x:Name="MenuUsun" Header="_Usuń" InputGestureText="Del"
Click="Usun_Click"/>
<Separator/>
<MenuItem x:Name="MenuUsunZrobione" Header="Usuń wszystkie zrobione"
Click="UsunZrobione_Click"/>
</MenuItem>
<MenuItem Header="_Widok">
<MenuItem x:Name="UkryjZrobione" Header="_Ukryj zrobione"
IsCheckable="True" Click="Widok_Click"/>
<MenuItem x:Name="WazneNaGorze" Header="Ważne na _górze"
IsCheckable="True" IsChecked="True" Click="Widok_Click"/>
<MenuItem Header="_Rozmiar tekstu">
<MenuItem Header="Mały" Tag="12" Click="Rozmiar_Click"/>
<MenuItem Header="Średni" Tag="14" Click="Rozmiar_Click"/>
<MenuItem Header="Duży" Tag="18" Click="Rozmiar_Click"/>
</MenuItem>
</MenuItem>
<MenuItem Header="Pomo_c">
<MenuItem Header="_O programie" Click="Info_Click"/>
</MenuItem>
</Menu>
<TextBlock x:Name="Stan" DockPanel.Dock="Bottom" Margin="10,4" Foreground="Gray"/>
<DockPanel DockPanel.Dock="Top" Margin="10,10,10,6">
<Button Content="Dodaj" DockPanel.Dock="Right" Width="70" Margin="6,0,0,0"
IsDefault="True" Click="Dodaj_Click"/>
<TextBox x:Name="Nowe"/>
</DockPanel>
<!-- menu kontekstowe: prawy przycisk myszy na liście -->
<ListBox x:Name="Lista" Margin="10,0,10,0" FontSize="14" KeyDown="Lista_KeyDown">
<ListBox.ContextMenu>
<ContextMenu Opened="Kontekst_Opened">
<MenuItem x:Name="KontekstZrobione" Header="Zrobione" Click="Zrobione_Click"/>
<MenuItem x:Name="KontekstWazne" Header="Ważne" Click="Wazne_Click"/>
<MenuItem Header="Przenieś na górę" Click="NaGore_Click"/>
<Separator/>
<MenuItem Header="Usuń" Click="Usun_Click"/>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</DockPanel>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Lekcja35;
public partial class MainWindow : Window
{
private List<Zadanie> zadania = new List<Zadanie>();
public MainWindow()
{
InitializeComponent();
zadania.Add(new Zadanie("Oddać projekt z WPF"));
zadania.Add(new Zadanie("Powtórzyć SQL na egzamin") { Wazne = true });
zadania.Add(new Zadanie("Kupić zeszyt") { Zrobione = true });
PokazListe();
Nowe.Focus();
}
// ---------- menu Plik ----------
private void Nowa_Click(object sender, RoutedEventArgs e)
{
if (zadania.Count > 0)
{
MessageBoxResult odp = MessageBox.Show("Usunąć wszystkie zadania i zacząć nową listę?",
"Nowa lista", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (odp != MessageBoxResult.Yes)
{
return;
}
}
zadania.Clear();
PokazListe();
}
private void Zakoncz_Click(object sender, RoutedEventArgs e)
{
Close();
}
// ---------- menu Edycja i menu kontekstowe ----------
// Zanim menu się pokaże, włączamy tylko te pozycje, które mają sens
private void Edycja_SubmenuOpened(object sender, RoutedEventArgs e)
{
bool wybrane = Lista.SelectedItem != null;
MenuZrobione.IsEnabled = wybrane;
MenuWazne.IsEnabled = wybrane;
MenuUsun.IsEnabled = wybrane;
MenuUsunZrobione.IsEnabled = LiczbaZrobionych() > 0;
}
private void Kontekst_Opened(object sender, RoutedEventArgs e)
{
Zadanie? z = Lista.SelectedItem as Zadanie;
ContextMenu menu = (ContextMenu)sender;
// bez zaznaczonego zadania wszystkie pozycje są wyszarzone
foreach (object element in menu.Items)
{
MenuItem? pozycja = element as MenuItem; // Separator nie jest MenuItem
if (pozycja != null)
{
pozycja.IsEnabled = z != null;
}
}
if (z == null)
{
return;
}
KontekstZrobione.Header = z.Zrobione ? "Cofnij „zrobione”" : "Zrobione";
KontekstWazne.IsChecked = z.Wazne; // znaczek przy pozycji menu
}
// Te same metody obsługują pozycje menu głównego, menu kontekstowego i skróty
private void Zrobione_Click(object sender, RoutedEventArgs e)
{
Zadanie? z = Lista.SelectedItem as Zadanie;
if (z != null)
{
z.Zrobione = !z.Zrobione;
PokazListe();
}
}
private void Wazne_Click(object sender, RoutedEventArgs e)
{
Zadanie? z = Lista.SelectedItem as Zadanie;
if (z != null)
{
z.Wazne = !z.Wazne;
PokazListe();
}
}
private void NaGore_Click(object sender, RoutedEventArgs e)
{
Zadanie? z = Lista.SelectedItem as Zadanie;
if (z != null)
{
zadania.Remove(z);
zadania.Insert(0, z);
PokazListe();
}
}
private void Usun_Click(object sender, RoutedEventArgs e)
{
Zadanie? z = Lista.SelectedItem as Zadanie;
if (z != null)
{
zadania.Remove(z);
PokazListe();
}
}
private void UsunZrobione_Click(object sender, RoutedEventArgs e)
{
// od końca, żeby usuwanie nie przesuwało jeszcze nie sprawdzonych elementów
for (int i = zadania.Count - 1; i >= 0; i--)
{
if (zadania[i].Zrobione)
{
zadania.RemoveAt(i);
}
}
PokazListe();
}
// ---------- menu Widok i Pomoc ----------
private void Widok_Click(object sender, RoutedEventArgs e)
{
PokazListe(); // IsChecked zmieniło się samo, bo pozycje mają IsCheckable
}
private void Rozmiar_Click(object sender, RoutedEventArgs e)
{
MenuItem pozycja = (MenuItem)sender;
Lista.FontSize = double.Parse($"{pozycja.Tag}");
}
private void Info_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Do zrobienia 1.0\nLekcja 35 kursu WPF — Menu i ContextMenu",
"O programie", MessageBoxButton.OK, MessageBoxImage.Information);
}
// ---------- dodawanie i klawiatura ----------
private void Dodaj_Click(object sender, RoutedEventArgs e)
{
string tekst = Nowe.Text.Trim();
if (tekst != "")
{
zadania.Add(new Zadanie(tekst));
Nowe.Clear();
PokazListe();
}
Nowe.Focus();
}
// Skróty z InputGestureText trzeba obsłużyć samemu
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.Modifiers != ModifierKeys.Control)
{
return;
}
if (e.Key == Key.N)
{
Nowa_Click(sender, e);
e.Handled = true;
}
else if (e.Key == Key.D)
{
Zrobione_Click(sender, e);
e.Handled = true;
}
else if (e.Key == Key.W)
{
Wazne_Click(sender, e);
e.Handled = true;
}
}
// Del tylko na liście — w polu tekstowym ma kasować znaki
private void Lista_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Delete)
{
Usun_Click(sender, e);
}
}
// ---------- wyświetlanie ----------
private void PokazListe()
{
object? zaznaczone = Lista.SelectedItem;
Lista.Items.Clear();
// przy włączonej opcji najpierw ważne, potem pozostałe
if (WazneNaGorze.IsChecked)
{
DodajDoListy(true);
DodajDoListy(false);
}
else
{
foreach (Zadanie z in zadania)
{
DodajJesliWidoczne(z);
}
}
if (zaznaczone != null && Lista.Items.Contains(zaznaczone))
{
Lista.SelectedItem = zaznaczone;
}
Stan.Text = $"Zadań: {zadania.Count}, zrobione: {LiczbaZrobionych()}" +
(UkryjZrobione.IsChecked ? " (ukryte)" : "");
}
private void DodajDoListy(bool wazne)
{
foreach (Zadanie z in zadania)
{
if (z.Wazne == wazne)
{
DodajJesliWidoczne(z);
}
}
}
private void DodajJesliWidoczne(Zadanie z)
{
if (UkryjZrobione.IsChecked && z.Zrobione)
{
return;
}
Lista.Items.Add(z);
}
private int LiczbaZrobionych()
{
int ile = 0;
foreach (Zadanie z in zadania)
{
if (z.Zrobione)
{
ile++;
}
}
return ile;
}
}
Co dzieje się po kolei
DockPanelukłada kolejno: menu u góry, wiersz stanu na dole, pole z przyciskiem pod menu, a listę — w pozostałym miejscu.- Pozycje „Zrobione”, „Ważne” i „Usuń” w obu menu wywołują te same metody. Każda odczytuje zaznaczone zadanie przez
asi nic nie robi, gdy go brak. Edycja_SubmenuOpenediKontekst_Openedwyłączają pozycje, gdy nic nie jest zaznaczone. Menu kontekstowe zmienia też tekst pozycji („Cofnij „zrobione””) i znaczek przy „Ważne”.- W
Kontekst_Openedpętla przechodzi pomenu.Items.Separatornie jestMenuItem, więcaszwraca dla niegonulli jest pomijany. - Pozycje „Ukryj zrobione” i „Ważne na górze” mają
IsCheckable— kliknięcie samo przełączaIsChecked, aPokazListetylko je czyta. Window_KeyDownobsługuje skróty z Ctrl. Del jest obsłużony tylko w liście — w polu tekstowym ma zwyczajnie kasować znaki.UsunZrobione_Clickusuwa elementy w pętli od końca, tak jak spadające owoce w lekcji 31.
Sprawdź trzy rzeczy
- Naciśnij Alt, potem W i U — bez myszy włączysz ukrywanie zrobionych.
- Kliknij prawym przyciskiem w puste miejsce pod zadaniami, gdy nic nie jest zaznaczone. Wszystkie pozycje menu są wyszarzone.
- Przenieś obsługę Del do
Window_KeyDown, bez sprawdzania Ctrl. Wpisz tekst w pole i spróbuj skasować znak klawiszem Del — zniknie zadanie z listy.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
Menu | kontrolka | Pasek menu; zwykle DockPanel.Dock="Top". |
MenuItem | kontrolka | Pozycja menu; z pozycjami w środku — podmenu. |
Separator | kontrolka | Kreska oddzielająca grupy pozycji. |
Header | object | Tekst pozycji; _ przed literą — klawisz dostępu. |
InputGestureText | string | Opis skrótu po prawej; skrót obsługujesz sam. |
IsCheckable, IsChecked | bool | Pozycja przełączana kliknięciem; znaczek ✓. |
SubmenuOpened | zdarzenie | Tuż przed pokazaniem podmenu. |
ContextMenu | kontrolka i właściwość | Menu pod prawym przyciskiem dla danej kontrolki. |
Opened | zdarzenie | Otwarcie menu kontekstowego. |
Items | kolekcja | Pozycje menu (także separatory). |
Zanim utkniesz
| Zapis | Problem |
|---|---|
InputGestureText="Ctrl+S" i nic więcej | Skrót tylko się wyświetla. Obsłuż go w KeyDown okna. |
if (Pozycja.IsChecked == true) | Działa, ale niepotrzebnie — MenuItem.IsChecked to bool. |
ręczne Pozycja.IsChecked = !Pozycja.IsChecked przy IsCheckable="True" | Podwójne przełączenie — znaczek wraca do stanu sprzed kliknięcia. |
foreach (MenuItem m in menu.Items) | InvalidCastException na Separator. Użyj as i sprawdź null. |
menu bez DockPanel.Dock="Top" w DockPanel | Menu ląduje z lewej strony i rozciąga się na całą wysokość okna. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Wielowierszowe pole tekstowe i menu: „Plik” (Nowy z pytaniem o porzucenie tekstu, Zakończ), „Edycja” (Wstaw datę i godzinę w miejscu kursora, Zaznacz wszystko), „Format” (Zawijanie wierszy — pozycja zaznaczana, Rozmiar czcionki — podmenu 12/16/20), „Pomoc” (O programie). Każda pozycja ma klawisz dostępu, a „Nowy” — skrót Ctrl + N.
pokaż wskazówkę
- Wstawianie w miejscu kursora:
SelectedTextz lekcji 13.
Lista uczniów klasy (wpisana w kodzie). Menu kontekstowe: „Obecny”, „Nieobecny”, „Spóźniony”, „Usprawiedliwiony” — wybrana pozycja dopisuje status do ucznia na liście, np. „Cieślak Maja — spóźniona”. Pozycja odpowiadająca aktualnemu statusowi ma znaczek ✓. Na dole licznik: obecni, nieobecni, spóźnieni.
pokaż wskazówkę
- Klasa
Uczenz polem statusu iToString. Znaczki ustaw wOpened, a status zapisz wTagkażdej pozycji — jedna metodaClickwystarczy.
Kalkulator z polem i przyciskami cyfr oraz działań. Menu „Widok” ma dwie pozycje zaznaczane działające jak przyciski opcji: „Standardowy” i „Naukowy” (dodatkowe przyciski: √, x², 1/x — ukryte w trybie standardowym). Menu „Edycja”: „Kopiuj wynik” (Ctrl + C) i „Wyczyść” (Esc). Menu „Historia” buduje się w kodzie — ostatnie 5 działań jako pozycje; kliknięcie wstawia wynik do pola.
pokaż wskazówkę
- Przy zaznaczaniu jednej z dwóch pozycji drugą odznacz w kodzie (
IsChecked = false). - Historię odtwarzaj w
SubmenuOpened:Historia.Items.Clear()iItems.Add(new MenuItem { ... }). Kopiowanie:Clipboard.SetText(...).
Rozbuduj pomysł z lekcji 25: siatka 4 × 4 kafelków-stanowisk w pracowni. Każdy kafelek ma to samo menu kontekstowe: „Sprawny”, „Awaria”, „W naprawie”, „Opis usterki…” (otwiera okno komunikatu z zapisanym opisem). Kolor kafelka zależy od stanu. W menu głównym „Pracownia” → „Wszystkie sprawne” i „Raport awarii” (lista stanowisk z awarią).
pokaż wskazówkę
- Menu kontekstowe twórz w kodzie, w tej samej pętli co kafelki — każdy kafelek dostaje własne menu, a w
Tagjego pozycji zapisz numer kafelka. Wtedy jedna metodaClickwie, którego kafelka dotyczy polecenie.
Siatka 5 dni × 8 godzin z polami tekstowymi. Menu kontekstowe każdego pola: podmenu „Wstaw przedmiot” (lista przedmiotów z tablicy), „Kopiuj do całego tygodnia” (ta sama godzina we wszystkie dni), „Wyczyść”. Menu główne „Plan” → „Wyczyść wszystko” i pozycja zaznaczana „Podświetl okienka” — puste pola między lekcjami tego samego dnia dostają żółte tło.
pokaż wskazówkę
- Pola trzymaj w tablicy
TextBox[5, 8], jak komórki labiryntu w lekcji 26. „Okienko” to puste pole, które ma niepusty wiersz wyżej i niżej (nie musi być bezpośrednio).
Plansza 9 × 9 z przycisków i 10 losowych min. Lewy przycisk odkrywa pole (liczba min dookoła; pole z zerem odkrywa sąsiadów), menu kontekstowe pola: „Postaw flagę”, „Znak zapytania”, „Usuń oznaczenie”. Menu główne „Gra”: „Nowa gra” (F2), podmenu „Poziom” z trzema pozycjami zaznaczanymi (9 × 9 / 10 min, 12 × 12 / 20 min, 16 × 16 / 40 min), „Zakończ”. Licznik flag i czas gry na dole.
pokaż wskazówkę
- Odkrywanie zer najprościej zrobić metodą, która wywołuje samą siebie dla sąsiadów — ale tylko dla pól jeszcze nieodkrytych.
- Planszę buduj w kodzie w
UniformGrid, czas —DispatcherTimerz lekcji 27.
Co trzeba zapamiętać
Menu+MenuItem; pozycje w pozycji tworzą podmenu,Separatordzieli grupy._wHeader— klawisz dostępu;InputGestureTexttylko opisuje skrót.- Skróty obsługujesz w
KeyDownokna, wywołując tę samą metodę co menu. IsCheckable— pozycja przełączana;IsCheckedtobool.ContextMenuprzypinasz do kontrolki;Opened/SubmenuOpenedpozwalają wyłączyć zbędne pozycje.
Dokumentacja: Microsoft Learn — Menu, ContextMenu.