Lekcja 9. Obsługa zdarzeń — mysz, klawiatura i wprowadzenie do Command
ŚredniPo 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
Dodaj obsługę zdarzenia MouseEnter/MouseLeave również na przycisku 'Szukaj', zmieniającą jego kolor tła (Background) na jaśniejszy przy najechaniu.
Dodaj obsługę klawisza Escape (Key.Escape) w polu wyszukiwania, która czyści pole tekstowe i przywraca domyślny komunikat w EtykietaWynik.
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.