Aplikacje desktopowe · Zdarzenia
Zdarzenia myszy i klawiatury oraz pierwsze spojrzenie na Command
Do tej pory reagowałeś na kliknięcie i zmianę tekstu. Interfejs generuje jednak dziesiątki innych zdarzeń: ruch myszy, wciśnięcie klawisza, najechanie kursorem. Nauczysz się je obsługiwać i poznasz komendy, czyli nowoczesną alternatywę dla zdarzeń.
- około 45 minut
- WPF · zdarzenia · ICommand
- INF.04.5.3
Po tej lekcji: obsłużyć zdarzenia myszy i klawiatury, wywołać jedną metodę z kilku źródeł oraz wyjaśnić, czym jest ICommand i kiedy zastępuje zdarzenie Click.
Zacznijmy od skrótów
Ctrl+S i ikona dyskietki robią to samo
W edytorze tekstu zapisujesz dokument kliknięciem ikony albo skrótem Ctrl+S. Dwie drogi, jeden efekt. W dobrze napisanej aplikacji oba zdarzenia prowadzą do jednej wspólnej metody, a nie do dwóch kopii tego samego kodu.
W tej lekcji zobaczysz to w praktyce na wyszukiwarce: Enter w polu tekstowym i przycisk „Szukaj” uruchamiają to samo wyszukiwanie.
1 · Mysz
Najważniejsze 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 i puszczenie przycisku myszy nad kontrolką. |
MouseMove | Ruch kursora nad kontrolką. Wywołuje się bardzo często, więc nie wkładaj do niego ciężkiego kodu. |
MouseDoubleClick | Podwójne kliknięcie. |
2 · Klawiatura
Najważniejsze zdarzenia klawiatury
| Zdarzenie | Kiedy się wywołuje |
|---|---|
KeyDown | Wciśnięcie klawisza. Który to klawisz, sprawdzisz w e.Key, np. Key.Enter. |
KeyUp | Puszczenie klawisza. |
TextInput | Wprowadzenie znaku tekstowego, z uwzględnieniem układu klawiatury i polskich znaków. |
Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji
3 · Sprawdź w działaniu
Enter i przycisk, jedna metoda
Wpisz frazę i naciśnij Enter, a potem wyczyść pole i kliknij „Szukaj”. Najedź myszą na żółtawy panel i zjedź z niego. W zakładce .cs zobaczysz, że WykonajWyszukiwanie() jest wywoływana z dwóch miejsc.
Logika istnieje w kodzie raz, a zdarzenia tylko ją wywołują. Dzięki temu każda zmiana wymaga poprawki w jednym miejscu.
4 · Wygląd
MainWindow.xaml
Zdarzenia podpinasz atrybutami. KeyDown należy do pola tekstowego, a MouseEnter i MouseLeave do panelu Border, który ma nazwę, bo zmieniamy jego tło.
<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="260" 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>
KeyDown="PoleWyszukaj_KeyDown"Metoda wykona się po wciśnięciu dowolnego klawisza w polu.Border x:Name="PanelPodpowiedzi"Ramka z tłem, które zmienia się przy najechaniu.MouseEnteriMouseLeaveDwie metody: jedna podświetla panel, druga przywraca kolor.
5 · Działanie
MainWindow.xaml.cs
Dwa zdarzenia (Enter i Click) wołają tę samą metodę. Dwa inne zmieniają tło panelu.
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();
}
// Wspólna metoda wywoływana zarówno przez Enter, jak i przez kliknięcie:
// 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;
}
}
if (e.Key == Key.Enter)Sprawdza, czy wciśnięto Enter. Inne klawisze są ignorowane.WykonajWyszukiwanie()Wspólna metoda wywoływana z dwóch miejsc..Trim()Usuwa spacje z początku i końca frazy.warunek ? a : bOperator warunkowy wybiera komunikat: prośbę o wpisanie frazy albo informację o wyszukiwaniu.Brushes.LightYellowiBrushes.WhiteZmiana tła przy wjechaniu i zjechaniu kursora.
Do zadań
Obsługa klawisza Escape
Escape obsługujesz tak samo jak Enter, w tej samej metodzie KeyDown. Dopisz drugą gałąź else if.
private void PoleWyszukaj_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
WykonajWyszukiwanie();
}
else if (e.Key == Key.Escape)
{
PoleWyszukaj.Clear();
EtykietaWynik.Text = "Wpisz coś, żeby wyszukać.";
}
}
Key.EscapeWartość z wyliczeniaKey. Inne klawisze znajdziesz, wpisującKey.i czytając podpowiedzi.- Kolory w zadaniu 1 Jaśniejszy kolor przy najechaniu to
Brushes.LightBlue, powrót doBrushes.LightGray. Wymagausing System.Windows.Media;.
6 · Druga droga
Zdarzenie czy komenda
Windows Forms zna tylko zdarzenia. WPF dodaje komendy: zamiast Click="Przycisk_Click" przycisk ma właściwość Command, powiązaną z obiektem w ViewModelu:
<Button Content="Zapisz" Command="{Binding ZapiszCommand}" />
Komenda implementuje interfejs **ICommand**, który wymaga dwóch metod: Execute (co zrobić) oraz CanExecute (czy akcja jest teraz dostępna). Gdy CanExecute zwraca false, przycisk sam się wyszarza, bez ręcznego ustawiania IsEnabled.
Dla prostych aplikacji zwykłe zdarzenia w code-behind w zupełności wystarczą. Komenda ma sens, gdy logika ma być niezależna od okna i testowalna albo gdy budujesz aplikację w pełnym wzorcu MVVM, który poznasz w jednej z kolejnych lekcji.
7 · Uniwersalna komenda
RelayCommand: jedna klasa, której użyjesz wszędzie
Ta klasa to standardowa, uniwersalna implementacja ICommand. Przyjmuje metodę do wykonania i opcjonalny warunek dostępności.
using System;
using System.Windows.Input;
namespace ZdarzeniaApp;
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;
}
}
Action _wykonajMetoda, którą komenda uruchamia.Func<bool>? _czyMoznaWarunek dostępności. Gdy go nie podasz, komenda jest zawsze dostępna.CommandManager.RequerySuggestedSprawia, że WPF sam ponownie sprawdzaCanExecutei odświeża stan przycisku.
8 · Jak jej użyć
Komenda zablokowana, dopóki pole jest puste
W ViewModelu wystarczy jedna linia, aby powstała komenda z warunkiem. Pełne użycie razem z INotifyPropertyChanged poznasz przy okazji MVVM.
using System.Windows.Input;
namespace ZdarzeniaApp;
public class SzukajViewModel
{
public string Imie { get; set; } = "";
public string Wynik { get; set; } = "";
public ICommand ZapiszCommand { get; }
public SzukajViewModel()
{
ZapiszCommand = new RelayCommand(WykonajZapis, () => !string.IsNullOrEmpty(Imie));
}
private void WykonajZapis()
{
Wynik = $"Zapisano: {Imie}";
}
}
new RelayCommand(WykonajZapis, () => ...)Pierwszy argument to metoda, drugi to warunek dostępności.!string.IsNullOrEmpty(Imie)Komenda jest dostępna tylko wtedy, gdy poleImienie jest puste.
9 · Czego unikać
Cztery błędy ze zdarzeniami
Ta sama logika wklejona w dwa miejsca
private void Pole_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
Wynik.Text = "Szukam " + Pole.Text;
}
private void Szukaj_Click(object sender, RoutedEventArgs e)
{
Wynik.Text = "Szukam " + Pole.Text;
}Każda zmiana trzeba poprawić dwa razy i łatwo o rozjazd.
private void Pole_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter) Szukaj();
}
private void Szukaj_Click(object sender, RoutedEventArgs e) => Szukaj();Jedna metoda wywoływana z obu miejsc to jedno miejsce do poprawek.
Porównanie klawisza przez napis
if (e.Key.ToString() == "Enter")Działa, ale jest wolniejsze i podatne na literówki.
if (e.Key == Key.Enter)Porównuj wartości typu wyliczeniowego, a kompilator pilnuje poprawności nazwy.
Ciężki kod w `MouseMove`
private void Panel_MouseMove(object sender, MouseEventArgs e)
{
PrzeliczCaleDane();
}Zdarzenie wywołuje się przy każdym mikroruchu, więc aplikacja zwalnia.
private void Panel_MouseEnter(object sender, MouseEventArgs e)
{
PanelPodpowiedzi.Background = Brushes.LightYellow;
}Do prostych efektów użyj MouseEnter i MouseLeave, a MouseMove zostaw na lekki kod.
Niepoprawne `CanExecuteChanged`
public event EventHandler? CanExecuteChanged;Przycisk może nie odświeżać stanu po zmianie warunku.
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}Powiązanie z RequerySuggested sprawia, że WPF sam odświeża stan komendy.
10 · Sprawdź się
Zdarzenie, metoda czy komenda?
Cztery sytuacje do rozstrzygnięcia.
Podsumowanie
Co warto zapamiętać
- Zdarzenia myszy:
MouseEnter,MouseLeave,MouseDown,MouseUp,MouseMove. - Zdarzenia klawiatury:
KeyDown,KeyUp,TextInput, a klawisz sprawdzasz we.Key == Key.Enter. - Tę samą akcję z kilku źródeł obsłuż jedną wspólną metodą.
ICommandmaExecuteiCanExecute, aRelayCommandto uniwersalna implementacja.- Zdarzenia wystarczą w prostych aplikacjach, komendy mają sens w MVVM.
Dalej na swoim koncie
Rozgrzewki, quiz i zadania są w biurku ucznia.
Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.
Zaloguj się →