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

ZdarzenieKiedy się wywołuje
MouseEnterKursor wjeżdża na obszar kontrolki.
MouseLeaveKursor opuszcza obszar kontrolki.
MouseDown / MouseUpWciśnięcie i puszczenie przycisku myszy nad kontrolką.
MouseMoveRuch kursora nad kontrolką. Wywołuje się bardzo często, więc nie wkładaj do niego ciężkiego kodu.
MouseDoubleClickPodwójne kliknięcie.
Para `MouseEnter` i `MouseLeave` daje efekt podświetlenia znany z CSS jako `:hover`, tylko sterowany z kodu C#.

2 · Klawiatura

Najważniejsze zdarzenia klawiatury

ZdarzenieKiedy się wywołuje
KeyDownWciśnięcie klawisza. Który to klawisz, sprawdzisz w e.Key, np. Key.Enter.
KeyUpPuszczenie klawisza.
TextInputWprowadzenie znaku tekstowego, z uwzględnieniem układu klawiatury i polskich znaków.
Klawisze porównuj z wartościami typu wyliczeniowego (`e.Key == Key.Enter`), a nie z napisami.

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.

Ta interakcja wymaga włączonego JavaScriptu.

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.

MainWindow.xamlxaml
<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>
  1. KeyDown="PoleWyszukaj_KeyDown" Metoda wykona się po wciśnięciu dowolnego klawisza w polu.
  2. Border x:Name="PanelPodpowiedzi" Ramka z tłem, które zmienia się przy najechaniu.
  3. MouseEnter i MouseLeave Dwie 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.

MainWindow.xaml.cscsharp
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;
    }
}
  1. if (e.Key == Key.Enter) Sprawdza, czy wciśnięto Enter. Inne klawisze są ignorowane.
  2. WykonajWyszukiwanie() Wspólna metoda wywoływana z dwóch miejsc.
  3. .Trim() Usuwa spacje z początku i końca frazy.
  4. warunek ? a : b Operator warunkowy wybiera komunikat: prośbę o wpisanie frazy albo informację o wyszukiwaniu.
  5. Brushes.LightYellow i Brushes.White Zmiana 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.

MainWindow.xaml.cscsharp
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ć.";
    }
}
  1. Key.Escape Wartość z wyliczenia Key. Inne klawisze znajdziesz, wpisując Key. i czytając podpowiedzi.
  2. Kolory w zadaniu 1 Jaśniejszy kolor przy najechaniu to Brushes.LightBlue, powrót do Brushes.LightGray. Wymaga using 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.

RelayCommand.cscsharp
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;
    }
}
  1. Action _wykonaj Metoda, którą komenda uruchamia.
  2. Func<bool>? _czyMozna Warunek dostępności. Gdy go nie podasz, komenda jest zawsze dostępna.
  3. CommandManager.RequerySuggested Sprawia, że WPF sam ponownie sprawdza CanExecute i 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.

SzukajViewModel.cscsharp
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}";
    }
}
  1. new RelayCommand(WykonajZapis, () => ...) Pierwszy argument to metoda, drugi to warunek dostępności.
  2. !string.IsNullOrEmpty(Imie) Komenda jest dostępna tylko wtedy, gdy pole Imie nie jest puste.

9 · Czego unikać

Cztery błędy ze zdarzeniami

Ta sama logika wklejona w dwa miejsca
Tak nie
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.

Tak
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
Tak nie
if (e.Key.ToString() == "Enter")

Działa, ale jest wolniejsze i podatne na literówki.

Tak
if (e.Key == Key.Enter)

Porównuj wartości typu wyliczeniowego, a kompilator pilnuje poprawności nazwy.

Ciężki kod w `MouseMove`
Tak nie
private void Panel_MouseMove(object sender, MouseEventArgs e)
{
    PrzeliczCaleDane();
}

Zdarzenie wywołuje się przy każdym mikroruchu, więc aplikacja zwalnia.

Tak
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`
Tak nie
public event EventHandler? CanExecuteChanged;

Przycisk może nie odświeżać stanu po zmianie warunku.

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

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Zdarzenia myszy: MouseEnter, MouseLeave, MouseDown, MouseUp, MouseMove.
  2. Zdarzenia klawiatury: KeyDown, KeyUp, TextInput, a klawisz sprawdzasz w e.Key == Key.Enter.
  3. Tę samą akcję z kilku źródeł obsłuż jedną wspólną metodą.
  4. ICommand ma Execute i CanExecute, a RelayCommand to uniwersalna implementacja.
  5. 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ę →
Źródła i dalsza nauka