Dataczwartek, 13 sierpnia 2026 Czas18:57:55
← Aplikacje desktopowe (WPF)

Lekcja 9. Obsługa zdarzeń — mysz, klawiatura i wprowadzenie do Command

Średni

Po co się tego uczymy?

Do tej pory reagowaliśmy głównie na kliknięcie przycisku (Click) i zmianę tekstu (TextChanged). Ale interfejs użytkownika generuje dziesiątki innych zdarzeń — ruch myszy, wciśnięcie konkretnego klawisza, najechanie kursorem. Ta lekcja systematyzuje obsługę zdarzeń w WPF i wprowadza Command/ICommand — nowoczesną alternatywę dla zdarzeń, fundamentalną dla wzorca MVVM, który poznasz w kolejnych lekcjach.

Teoria

Dwie drogi do tego samego celu: eventy i komendy

Windows Forms ma tylko jedną drogę — zdarzenia (eventy). Każdy przycisk ma swój kod w tle, podpięty bezpośrednio, np. button1_Click. WPF pozwala na to samo, ale dodatkowo oferuje komendy (Command) — mechanizm łączący kontrolkę z metodą w logice programu (ViewModelu) bez pisania kodu w code-behind. Dzięki temu UI i logika mogą być całkowicie rozdzielone, co ma znaczenie przy większych projektach i na egzaminie INF.04.

Zdarzenia myszy

Zdarzenie Kiedy się wywołuje
MouseEnter Kursor wjeżdża na obszar kontrolki.
MouseLeave Kursor opuszcza obszar kontrolki.
MouseDown / MouseUp Wciśnięcie / puszczenie dowolnego przycisku myszy nad kontrolką.
MouseMove Ruch kursora nad kontrolką (wywołuje się bardzo często — ostrożnie z ciężkim kodem w środku).
MouseDoubleClick Podwójne kliknięcie.
private void Panel_MouseEnter(object sender, MouseEventArgs e)
{
    PanelBoczny.Background = Brushes.LightBlue;
}
private void Panel_MouseLeave(object sender, MouseEventArgs e)
{
    PanelBoczny.Background = Brushes.White;
}

Efekt: panel podświetla się na niebiesko, gdy najedziesz na niego myszą — klasyczny efekt "hover", znany z CSS (:hover) w dziale HTML/CSS.

Zdarzenia klawiatury

Zdarzenie Kiedy się wywołuje
KeyDown Wciśnięcie klawisza (można sprawdzić e.Key, np. Key.Enter).
KeyUp Puszczenie klawisza.
TextInput Wprowadzenie znaku tekstowego (uwzględnia układ klawiatury, polskie znaki itd.).
private void PoleWyszukaj_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter)
    {
        WykonajWyszukiwanie();
    }
}

Efekt: naciśnięcie Enter w polu wyszukiwania uruchamia tę samą akcję, co kliknięcie przycisku "Szukaj" — bardzo częsty, oczekiwany przez użytkowników skrót.

Command i ICommand — komendy w WPF

Zamiast Click="Przycisk_Click", przycisk może korzystać z właściwości Command, powiązanej przez binding z obiektem implementującym interfejs ICommand w ViewModelu:

<Button Content="Zapisz" Command="{Binding ZapiszCommand}" />

ICommand wymaga dwóch elementów: metody Execute (co ma się wykonać) i CanExecute (czy komenda jest aktualnie dostępna — jeśli zwróci false, przycisk automatycznie staje się wyszarzony, BEZ ręcznego ustawiania IsEnabled!). Poniższa klasa RelayCommand to standardowa, uniwersalna implementacja tego interfejsu, używana w niemal każdym projekcie WPF:

public class RelayCommand : ICommand
{
    private readonly Action _wykonaj;
    private readonly Func<bool>? _czyMozna;

    public RelayCommand(Action wykonaj, Func<bool>? czyMozna = null)
    {
        _wykonaj = wykonaj;
        _czyMozna = czyMozna;
    }

    public bool CanExecute(object? parametr) => _czyMozna?.Invoke() ?? true;
    public void Execute(object? parametr) => _wykonaj();

    public event EventHandler? CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}

Dzięki tej jednej, uniwersalnej klasie, w ViewModelu wystarczy napisać:

public ICommand ZapiszCommand { get; }

public MainViewModel()
{
    ZapiszCommand = new RelayCommand(WykonajZapis, () => !string.IsNullOrEmpty(Imie));
}

private void WykonajZapis()
{
    Wynik = $"Zapisano: {Imie}";
}

Efekt: przycisk "Zapisz" jest AUTOMATYCZNIE nieaktywny, dopóki pole Imie jest puste — bez pisania ani jednej linijki kodu w code-behind okna. Pełne, praktyczne użycie RelayCommand razem z pełnym wzorcem MVVM (w tym INotifyPropertyChanged) poznasz w kolejnych lekcjach tego działu — tu poznajesz sam mechanizm, żeby rozumieć, skąd się bierze i jak działa.

Kiedy zwykły event, a kiedy Command?

Dla prostych, jednorazowych aplikacji (jak większość ćwiczeń w tym dziale) zwykłe zdarzenia (Click, TextChanged) w code-behind są całkowicie wystarczające i szybsze do napisania. Command/ICommand ma sens, gdy: (1) logika ma być testowalna niezależnie od UI, (2) chcesz automatycznego włączania/wyłączania przycisków bez ręcznego ustawiania IsEnabled w kilku miejscach, (3) budujesz aplikację zgodną z pełnym wzorcem MVVM.

Schemat

Dwie drogi obslugi klikniecia przycisku w WPF:

DROGA 1 (zdarzenie, code-behind):
  XAML: <Button Click="Przycisk_Click" />
  C#:   private void Przycisk_Click(object sender, RoutedEventArgs e) { ... }
  -> prostsze, szybsze w pisaniu, logika w oknie

DROGA 2 (Command, ViewModel):
  XAML: <Button Command="{Binding ZapiszCommand}" />
  C#:   ZapiszCommand = new RelayCommand(WykonajZapis, CzyMozna);
  -> logika calkowicie oddzielona od okna, testowalna, podstawa MVVM

Przykład z życia

Skróty klawiszowe w edytorze tekstu (Ctrl+S zapisuje dokument tak samo, jak kliknięcie ikony dyskietki) to praktyczny przykład tego, że TA SAMA akcja może być wywołana na kilka różnych sposobów — kliknięciem myszy albo klawiaturą. W dobrze zaprojektowanej aplikacji obie ścieżki powinny prowadzić do jednej, wspólnej metody (albo jednej komendy), a nie do dwóch osobnych kopii tego samego kodu.

MainWindow.xaml

<Window x:Class="ZdarzeniaApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Wyszukiwarka" Height="220" Width="380">
    <StackPanel Margin="20">
        <TextBlock Text="Szukaj (Enter uruchamia wyszukiwanie):" Margin="0,0,0,5"/>
        <TextBox x:Name="PoleWyszukaj"
                 KeyDown="PoleWyszukaj_KeyDown"/>

        <Border x:Name="PanelPodpowiedzi"
                Background="White"
                BorderBrush="Gray" BorderThickness="1"
                Margin="0,10,0,0" Padding="10"
                MouseEnter="PanelPodpowiedzi_MouseEnter"
                MouseLeave="PanelPodpowiedzi_MouseLeave">
            <TextBlock x:Name="EtykietaWynik" Text="Najedź tu myszą albo wpisz zapytanie i wciśnij Enter." TextWrapping="Wrap"/>
        </Border>

        <Button Content="Szukaj" Click="PrzyciskSzukaj_Click" Margin="0,15,0,0"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace ZdarzeniaApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void PoleWyszukaj_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                WykonajWyszukiwanie();
            }
        }

        private void PrzyciskSzukaj_Click(object sender, RoutedEventArgs e)
        {
            WykonajWyszukiwanie();
        }

        // Wspolna metoda wywolywana ZAROWNO przez Enter, JAK i przez klikniecie -
        // logika wyszukiwania istnieje w kodzie tylko RAZ
        private void WykonajWyszukiwanie()
        {
            string fraza = PoleWyszukaj.Text.Trim();
            EtykietaWynik.Text = string.IsNullOrEmpty(fraza)
                ? "Wpisz coś, żeby wyszukać."
                : $"Szukam frazy: "{fraza}"...";
        }

        private void PanelPodpowiedzi_MouseEnter(object sender, MouseEventArgs e)
        {
            PanelPodpowiedzi.Background = Brushes.LightYellow;
        }

        private void PanelPodpowiedzi_MouseLeave(object sender, MouseEventArgs e)
        {
            PanelPodpowiedzi.Background = Brushes.White;
        }
    }
}

Komentarz i wyjaśnienie kodu

Kluczowy fragment tego przykładu to metoda WykonajWyszukiwanie() — istnieje w kodzie TYLKO RAZ, ale jest wywoływana z DWÓCH różnych miejsc: z obsługi KeyDown (gdy warunek e.Key == Key.Enter jest spełniony) oraz z obsługi Click przycisku "Szukaj". To unikanie duplikacji kodu — gdyby logikę wyszukiwania wkleić osobno w obu miejscach, każda przyszła zmiana wymagałaby poprawki w dwóch miejscach naraz (i łatwo o to, żeby zapomnieć o jednym z nich).

PanelPodpowiedzi_MouseEnter i _MouseLeave realizują efekt "podświetlenia przy najechaniu", zmieniając Background panelu — dokładny odpowiednik :hover z CSS, tylko sterowany jawnie z kodu C#, a nie deklaratywnie ze stylu.

Ćwiczenie samodzielne

Utwórz projekt z powyższym kodem. Sprawdź, że wpisanie tekstu i wciśnięcie Enter daje DOKŁADNIE taki sam efekt, jak wpisanie tekstu i kliknięcie przycisku "Szukaj". Najedź myszą na panel z wynikiem i sprawdź, czy zmienia kolor tła, a po zjechaniu myszą wraca do białego.

Zadania do pracy własnej

  1. Dodaj obsługę zdarzenia MouseEnter/MouseLeave również na przycisku 'Szukaj', zmieniającą jego kolor tła (Background) na jaśniejszy przy najechaniu.

  2. Dodaj obsługę klawisza Escape (Key.Escape) w polu wyszukiwania, która czyści pole tekstowe i przywraca domyślny komunikat w EtykietaWynik.

  3. Przepisz obsługę przycisku 'Szukaj' z podejścia event-based (Click) na Command — zaimplementuj podaną w lekcji klasę RelayCommand, dodaj do okna prosty ViewModel z właściwością SzukajCommand typu ICommand, ustaw DataContext okna na instancję ViewModelu, i podepnij przycisk przez Command="{Binding SzukajCommand}" zamiast Click. Metoda CanExecute powinna zwracać false (blokować przycisk), gdy pole wyszukiwania jest puste.

Typowe błędy

Duplikowanie logiki zamiast wspólnej metody — jeśli ta sama akcja ma być wywoływana z klawiatury i myszy, napisz JEDNĄ metodę i wywołaj ją z obu miejsc, zamiast kopiować kod.

Sprawdzanie e.Key przez porównanie stringów — poprawnie porównuje się e.Key == Key.Enter (wartość enuma), a nie np. e.Key.ToString() == "Enter", co jest niepotrzebnie powolne i podatne na błędy.

Nadużywanie MouseMove — to zdarzenie wywołuje się BARDZO często (przy każdym mikroruchu myszy), więc umieszczanie w nim ciężkich obliczeń może zauważalnie spowolnić aplikację.

Zapominanie o CanExecuteChanged w customowej implementacji ICommand — bez poprawnej obsługi tego zdarzenia (jak w podanej klasie RelayCommand, opartej o CommandManager.RequerySuggested) przycisk może nie odświeżać swojego stanu IsEnabled automatycznie po zmianie warunku w CanExecute.

Nawiązanie do egzaminu zawodowego

To realizacja INF.04.5.3 w zakresie obsługi zdarzeń oraz wprowadzenie do wzorca komend, wykorzystywanego w kolejnych, bardziej zaawansowanych lekcjach o MVVM. Umiejętność unikania duplikacji logiki między różnymi źródłami tego samego zdarzenia (klawiatura/mysz) jest też oceniana przy jakości kodu na egzaminie praktycznym.