Przejdź do treści
School IT / Przestrzeń do nauki

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.

XAML C# Menu MenuItem ContextMenu IsCheckable skróty 90 min
CEL LEKCJI

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

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:

MainWindow.xaml
<Window ... KeyDown="Window_KeyDown">
    ...
    <MenuItem Header="_Nowy" InputGestureText="Ctrl+N" Click="Nowy_Click"/>
MainWindow.xaml.cs
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.

TEORIA

Pozycje zaznaczane i wyłączane

MainWindow.xaml
<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>
MainWindow.xaml.cs
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.

TEORIA

ContextMenu

Menu kontekstowe otwiera się po kliknięciu prawym przyciskiem. Przypina się je do konkretnej kontrolki przez właściwość ContextMenu, zapisaną jako element:

MainWindow.xaml
<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.

Lista obecności–□✕
Adamczyk Julia
Borkowski Igor
Cieślak Maja
Obecny
Usuń z listy

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.

PRZYKŁAD

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.

Do zrobienia–□✕
Plik Edycja Widok Pomoc
Dodaj
☐ Powtórzyć SQL na egzamin ★
☐ Oddać projekt z WPF
☑ Kupić zeszyt
Zadań: 3, zrobione: 1
Oznacz jako zrobioneCtrl+D
WażneCtrl+W
UsuńDel
Usuń wszystkie zrobione

otwarte menu „Edycja” — skróty wypisane z prawej strony, podkreślone litery to klawisze dostępu

Zadanie.cs — pełny plik
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}";
    }
}
MainWindow.xaml — pełny plik
<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>
MainWindow.xaml.cs — pełny plik
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

  • DockPanel ukł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 as i nic nie robi, gdy go brak.
  • Edycja_SubmenuOpened i Kontekst_Opened wyłączają pozycje, gdy nic nie jest zaznaczone. Menu kontekstowe zmienia też tekst pozycji („Cofnij „zrobione””) i znaczek przy „Ważne”.
  • W Kontekst_Opened pętla przechodzi po menu.Items. Separator nie jest MenuItem, więc as zwraca dla niego null i jest pomijany.
  • Pozycje „Ukryj zrobione” i „Ważne na górze” mają IsCheckable — kliknięcie samo przełącza IsChecked, a PokazListe tylko je czyta.
  • Window_KeyDown obsługuje skróty z Ctrl. Del jest obsłużony tylko w liście — w polu tekstowym ma zwyczajnie kasować znaki.
  • UsunZrobione_Click usuwa elementy w pętli od końca, tak jak spadające owoce w lekcji 31.

Sprawdź trzy rzeczy

  1. Naciśnij Alt, potem W i U — bez myszy włączysz ukrywanie zrobionych.
  2. Kliknij prawym przyciskiem w puste miejsce pod zadaniami, gdy nic nie jest zaznaczone. Wszystkie pozycje menu są wyszarzone.
  3. 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.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
MenukontrolkaPasek menu; zwykle DockPanel.Dock="Top".
MenuItemkontrolkaPozycja menu; z pozycjami w środku — podmenu.
SeparatorkontrolkaKreska oddzielająca grupy pozycji.
HeaderobjectTekst pozycji; _ przed literą — klawisz dostępu.
InputGestureTextstringOpis skrótu po prawej; skrót obsługujesz sam.
IsCheckable, IsCheckedboolPozycja przełączana kliknięciem; znaczek ✓.
SubmenuOpenedzdarzenieTuż przed pokazaniem podmenu.
ContextMenukontrolka i właściwośćMenu pod prawym przyciskiem dla danej kontrolki.
OpenedzdarzenieOtwarcie menu kontekstowego.
ItemskolekcjaPozycje menu (także separatory).
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
InputGestureText="Ctrl+S" i nic więcejSkró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 DockPanelMenu ląduje z lewej strony i rozciąga się na całą wysokość okna.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Notatnik z menu★☆☆

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: SelectedText z lekcji 13.
ZAD 2Lista obecności z menu kontekstowym★☆☆

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 Uczen z polem statusu i ToString. Znaczki ustaw w Opened, a status zapisz w Tag każdej pozycji — jedna metoda Click wystarczy.
ZAD 3Kalkulator z menu trybów★★☆

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() i Items.Add(new MenuItem { ... }). Kopiowanie: Clipboard.SetText(...).
ZAD 4Kafelki stanowisk z menu kontekstowym★★☆

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 Tag jego pozycji zapisz numer kafelka. Wtedy jedna metoda Click wie, którego kafelka dotyczy polecenie.
ZAD 5Edytor planu lekcji★★☆

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).
ZAD 6Saper★★★

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 — DispatcherTimer z lekcji 27.
PODSUMOWANIE

Co trzeba zapamiętać

  • Menu + MenuItem; pozycje w pozycji tworzą podmenu, Separator dzieli grupy.
  • _ w Header — klawisz dostępu; InputGestureText tylko opisuje skrót.
  • Skróty obsługujesz w KeyDown okna, wywołując tę samą metodę co menu.
  • IsCheckable — pozycja przełączana; IsChecked to bool.
  • ContextMenu przypinasz do kontrolki; Opened/SubmenuOpened pozwalają wyłączyć zbędne pozycje.

Dokumentacja: Microsoft Learn — Menu, ContextMenu.

Postęp zapisuje się w tej przeglądarce.