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

Zdarzenia klawiatury

Strzałki przesuwające postać, Ctrl + S zapisujący plik, Esc zamykający okno — klawiatura to drugi, obok myszy, sposób sterowania programem. W tej lekcji nauczysz się odczytywać naciśnięty klawisz, obsługiwać skróty z Ctrl i Shift i zrozumiesz, dlaczego czasem okno „nie słyszy” klawiatury. Na koniec przesuniesz pionek po planszy zbudowanej w Grid.

C# XAML KeyDown Key Keyboard.Modifiers PreviewKeyDown 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zareagujesz na naciśnięcie klawisza w całym oknie i rozpoznasz, który to klawisz.
  • Obsłużysz skróty z Ctrl, Shift i Alt.
  • Wyjaśnisz, jak klawisz wędruje od kontrolki z fokusem do okna i do czego służą PreviewKeyDown i e.Handled.
  • Rozróżnisz pojedyncze naciśnięcie od przytrzymania (IsRepeat, KeyUp).
  • Zbudujesz w kodzie siatkę o zadanej liczbie wierszy i kolumn i przesuniesz po niej kontrolkę.
TEORIA

KeyDown i e.Key

MainWindow.xaml
<Window ... KeyDown="Window_KeyDown">
MainWindow.xaml.cs
using System.Windows.Input;     // KeyEventArgs, Key

private void Window_KeyDown(object sender, KeyEventArgs e)
{
    Info.Text = $"Naciśnięto: {e.Key}";      // np. „Left”, „A”, „Space”, „D7”

    if (e.Key == Key.Escape)
    {
        Close();
    }
}

e.Key ma typ Key — wyliczenie z nazwą każdego klawisza. Kilka nazw jest nieoczywistych:

KlawiszWartość Key
strzałkiKey.Left, Key.Right, Key.Up, Key.Down
cyfry w górnym rzędzieKey.D0 … Key.D9
cyfry na klawiaturze numerycznejKey.NumPad0 … Key.NumPad9
Enter, Spacja, BackspaceKey.Enter, Key.Space, Key.Back
literyKey.A … Key.Z — bez względu na wielkość litery i Caps Lock
F1–F12Key.F1 … Key.F12

Gdy kilka klawiszy robi to samo, wygodny jest switch z kilkoma case nad jednym blokiem — tak jak w przykładzie, gdzie strzałka w górę i W działają tak samo.

TEORIA

Kto dostaje klawisz

Naciśnięcie klawisza trafia najpierw do kontrolki, która ma fokus (np. pola tekstowego z migającym kursorem), a potem „wędruje w górę” przez kolejne panele aż do okna. Dlatego KeyDown podpięte do okna działa, niezależnie od tego, która kontrolka ma fokus — z dwoma wyjątkami:

  • niektóre kontrolki zużywają klawisz: pole tekstowe obsługuje strzałki (przesuwa kursor), lista — strzałki i Enter. Zużyty klawisz nie dociera do okna;
  • pozostałe klawisze docierają do okna, także litery wpisywane w pole tekstowe — a więc litera W wpisana w polu przesunie też pionek.

Oba problemy rozwiązuje para:

ElementCo robi
PreviewKeyDownZdarzenie wędrujące w drugą stronę: od okna w dół do kontrolki z fokusem. Okno dostaje klawisz jako pierwsze — także strzałki, które pole tekstowe by zużyło.
e.Handled = true;„Obsłużyłem ten klawisz” — zatrzymuje dalszą wędrówkę. Litera nie trafi już do pola tekstowego.

Gra bez pól tekstowych

W oknie gry najprościej nie mieć żadnej kontrolki, która przechwytuje klawisze — jak w przykładzie. Jeśli okno ma przyciski, kliknięty przycisk dostaje fokus i Spacja czy Enter będą go „klikać”. Przyciskom w grach daj Focusable="False".

TEORIA

Skróty z Ctrl i Shift

Wciśnięcie Ctrl + S to dla WPF klawisz Key.S plus informacja, że trzymany jest Ctrl. Tę informację daje Keyboard.Modifiers:

MainWindow.xaml.cs
private void Window_KeyDown(object sender, KeyEventArgs e)
{
    if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.S)
    {
        Zapisz();                         // Ctrl+S
        e.Handled = true;
    }
    else if (Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.S)
    {
        ZapiszJako();                     // Ctrl+Shift+S
        e.Handled = true;
    }
    else if (e.Key == Key.F1)
    {
        MessageBox.Show("Pomoc programu…");
    }
}

Operator | łączy kilka modyfikatorów. Porównanie == wymaga dokładnie tej kombinacji — Ctrl + Shift + S nie uruchomi zwykłego „Zapisz”. Uwaga na Alt: z nim e.Key ma wartość Key.System, a prawdziwy klawisz jest w e.SystemKey.

TEORIA

Przytrzymanie i KeyUp

Przytrzymany klawisz po chwili zaczyna się „powtarzać” — KeyDown przychodzi wiele razy na sekundę, jak przy przytrzymaniu litery w edytorze. Pierwsze naciśnięcie od powtórzeń odróżnisz przez e.IsRepeat. Zdarzenie KeyUp przychodzi raz, przy puszczeniu klawisza.

MainWindow.xaml.cs
private void Window_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Space && !e.IsRepeat)
    {
        Info.Text = "Spacja wciśnięta";      // tylko raz, bez powtórzeń
    }
}

private void Window_KeyUp(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Space)
    {
        Info.Text = "Spacja puszczona";
    }
}

Płynny ruch przy przytrzymanym klawiszu robi się z czasomierzem: KeyDown i KeyUp tylko zapamiętują, czy klawisz jest wciśnięty, a ruch wykonuje zegar. Poznasz go w następnej lekcji.

TEORIA

Plansza z kodu

W lekcji 09 wiersze i kolumny Grid były zapisane w XAML. Gdy wymiary planszy wynikają z danych, dodajesz je w kodzie. Rozmiar wiersza to obiekt GridLength:

MainWindow.xaml.cs
Plansza.RowDefinitions.Add(new RowDefinition { Height = new GridLength(40) });     // 40 px
Plansza.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // *

// przesunięcie kontrolki na inne pole = zmiana jej wiersza i kolumny
Grid.SetRow(Pionek, 3);
Grid.SetColumn(Pionek, 5);
PRZYKŁAD

Labirynt z gwiazdkami

Gracz (złote kółko) chodzi po labiryncie strzałkami albo klawiszami W A S D i zbiera gwiazdki. Ściany zatrzymują ruch. Pod planszą liczba gwiazdek i ruchów, a po zebraniu wszystkich — gratulacje. R zaczyna od nowa. Labirynt jest zapisany jako tablica napisów — zmiana mapy to zmiana kilku znaków.

Labirynt — zbierz gwiazdki–✕
Strzałki lub W, A, S, D — ruch. R — od nowa.
★
★
Gwiazdki: 1 / 3 Ruchy: 13

po zebraniu pierwszej gwiazdki (w lewym dolnym rogu)

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja26.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Labirynt — zbierz gwiazdki" SizeToContent="WidthAndHeight"
        ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen"
        KeyDown="Window_KeyDown">

    <StackPanel Margin="12">
        <TextBlock Text="Strzałki lub W, A, S, D — ruch.  R — od nowa." Margin="0,0,0,8"/>

        <!-- plansza: wiersze, kolumny i pola tworzy konstruktor -->
        <Grid x:Name="Plansza" Background="#FF1F2937"/>

        <TextBlock x:Name="Stan" FontSize="16" FontWeight="Bold" Margin="0,8,0,0"/>
    </StackPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace Lekcja26;

public partial class MainWindow : Window
{
    // # — ściana, . — korytarz, * — gwiazdka, S — start
    private readonly string[] mapa =
    {
        "##########",
        "#S..#...*#",
        "#.#.#.##.#",
        "#.#...#..#",
        "#.###.#.##",
        "#...#...*#",
        "#*#...#..#",
        "##########"
    };

    private const int Rozmiar = 40;                  // bok pola w pikselach

    private readonly Border gracz = new Border
    {
        Background = Brushes.Gold,
        CornerRadius = new CornerRadius(20),         // kwadrat z mocno zaokrąglonymi rogami = koło
        Margin = new Thickness(6)
    };

    private TextBlock?[,] gwiazdki = new TextBlock?[0, 0];
    private int wiersz, kolumna;                     // pozycja gracza
    private int zebrane, ruchy, wszystkich;

    public MainWindow()
    {
        InitializeComponent();
        ZbudujPlansze();
    }

    private void ZbudujPlansze()
    {
        Plansza.Children.Clear();
        Plansza.RowDefinitions.Clear();
        Plansza.ColumnDefinitions.Clear();

        int wiersze = mapa.Length;
        int kolumny = mapa[0].Length;
        gwiazdki = new TextBlock?[wiersze, kolumny];
        zebrane = 0;
        ruchy = 0;
        wszystkich = 0;

        for (int w = 0; w < wiersze; w++)
        {
            Plansza.RowDefinitions.Add(new RowDefinition { Height = new GridLength(Rozmiar) });
        }
        for (int k = 0; k < kolumny; k++)
        {
            Plansza.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(Rozmiar) });
        }

        for (int w = 0; w < wiersze; w++)
        {
            for (int k = 0; k < kolumny; k++)
            {
                char znak = mapa[w][k];

                if (znak == '#')
                {
                    Border sciana = new Border { Background = Brushes.SlateGray, Margin = new Thickness(1) };
                    Grid.SetRow(sciana, w);
                    Grid.SetColumn(sciana, k);
                    Plansza.Children.Add(sciana);
                }
                else if (znak == '*')
                {
                    TextBlock gwiazdka = new TextBlock
                    {
                        Text = "★",
                        FontSize = 24,
                        Foreground = Brushes.Orange,
                        HorizontalAlignment = HorizontalAlignment.Center,
                        VerticalAlignment = VerticalAlignment.Center
                    };
                    Grid.SetRow(gwiazdka, w);
                    Grid.SetColumn(gwiazdka, k);
                    Plansza.Children.Add(gwiazdka);
                    gwiazdki[w, k] = gwiazdka;
                    wszystkich++;
                }
                else if (znak == 'S')
                {
                    wiersz = w;
                    kolumna = k;
                }
            }
        }

        Plansza.Children.Add(gracz);   // gracz dodany na końcu — rysuje się na wierzchu
        UstawGracza();
        PokazStan();
    }

    private void Window_KeyDown(object sender, KeyEventArgs e)
    {
        int dw = 0, dk = 0;          // o ile zmienić wiersz i kolumnę

        switch (e.Key)
        {
            case Key.Up:
            case Key.W:
                dw = -1;
                break;
            case Key.Down:
            case Key.S:
                dw = 1;
                break;
            case Key.Left:
            case Key.A:
                dk = -1;
                break;
            case Key.Right:
            case Key.D:
                dk = 1;
                break;
            case Key.R:
                ZbudujPlansze();
                return;
            default:
                return;              // inne klawisze nas nie interesują
        }

        if (zebrane == wszystkich)
        {
            return;                  // gra skończona — czekamy na R
        }

        int nowyW = wiersz + dw;
        int nowaK = kolumna + dk;

        if (mapa[nowyW][nowaK] == '#')
        {
            return;                  // ściana — nie ruszamy się
        }

        wiersz = nowyW;
        kolumna = nowaK;
        ruchy++;
        UstawGracza();

        // zbieranie gwiazdki
        TextBlock? gwiazdka = gwiazdki[wiersz, kolumna];
        if (gwiazdka != null)
        {
            Plansza.Children.Remove(gwiazdka);
            gwiazdki[wiersz, kolumna] = null;
            zebrane++;
        }

        PokazStan();
    }

    private void UstawGracza()
    {
        Grid.SetRow(gracz, wiersz);
        Grid.SetColumn(gracz, kolumna);
    }

    private void PokazStan()
    {
        Stan.Text = zebrane == wszystkich
            ? $"Brawo! Wszystkie gwiazdki w {ruchy} ruchach. R — jeszcze raz."
            : $"Gwiazdki: {zebrane} / {wszystkich}   Ruchy: {ruchy}";
    }
}

Co dzieje się po kolei

  • Okno ma KeyDown="Window_KeyDown", a w środku nie ma żadnych pól ani przycisków — każdy klawisz trafia prosto do okna. SizeToContent="WidthAndHeight" dopasowuje okno do planszy.
  • Mapa to tablica napisów: mapa[w][k] to znak w wierszu w i kolumnie k. Ściany na brzegach sprawiają, że gracz nigdy nie wyjdzie poza tablicę.
  • ZbudujPlansze czyści siatkę, dodaje wiersze i kolumny po 40 px, a potem wstawia ściany i gwiazdki w odpowiednie komórki. Gracz jest dodawany na końcu, więc rysuje się na wierzchu.
  • Gwiazdki są zapamiętane w tablicy dwuwymiarowej gwiazdki[w, k] — dzięki temu po wejściu na pole od razu wiadomo, czy jest tam gwiazdka do zebrania. TextBlock? oznacza, że element może być pusty (null).
  • switch zamienia klawisz na przesunięcie dw, dk. R buduje planszę od nowa, a default kończy metodę dla wszystkich innych klawiszy.
  • Ruch jest wykonywany tylko wtedy, gdy na nowym polu nie ma ściany. Sam ruch to Grid.SetRow i Grid.SetColumn dla gracza.
  • Gwiazdkę zbiera Children.Remove i wpis null w tablicy.

Sprawdź trzy rzeczy

  1. Przytrzymaj strzałkę — gracz zacznie biec, bo KeyDown się powtarza. Dopisz na początku metody if (e.IsRepeat) return; i porównaj.
  2. Dodaj nad planszą TextBox, kliknij w niego i naciskaj strzałki, a potem W. Strzałki przesuwają kursor w polu, a W rusza graczem i wpisuje literę. Zamień KeyDown okna na PreviewKeyDown i dopisz e.Handled = true; po ruchu.
  3. Zmień w mapie jeden znak . na *. Liczba gwiazdek do zebrania zwiększy się sama.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
KeyDown, KeyUpzdarzeniaNaciśnięcie / puszczenie klawisza; parametr KeyEventArgs.
PreviewKeyDownzdarzenieJak KeyDown, ale najpierw w oknie, potem w kontrolce z fokusem.
e.KeyKeyKtóry klawisz.
e.IsRepeatboolPowtórzenie przy przytrzymaniu.
e.Handledbooltrue zatrzymuje dalszą wędrówkę zdarzenia.
Keyboard.ModifiersModifierKeysTrzymane Control, Shift, Alt.
FocusableboolFalse — kontrolka nie przejmuje fokusu.
new GridLength(40)GridLengthWysokość / szerokość wiersza lub kolumny tworzonej w kodzie.
RowDefinitions.Add, ColumnDefinitions.AddmetodyDodają wiersz / kolumnę siatki z kodu.
CZĘSTE BŁĘDY

Zanim utkniesz

SytuacjaProblem
strzałki nie działają, gdy w oknie jest pole tekstowe lub listaKontrolka z fokusem zużywa strzałki. Użyj PreviewKeyDown.
po kliknięciu przycisku Spacja „klika” go zamiast skakaćPrzycisk ma fokus. Daj mu Focusable="False".
e.Key == Key.1Błąd — cyfry to Key.D1 albo Key.NumPad1.
e.Key == Key.Return i Key.EnterTo ta sama wartość — wystarczy jedna.
skrót z Alt sprawdzany przez e.KeyPrzy Alt e.Key to Key.System; sprawdź e.SystemKey.
wyjście poza tablicę mapyIndexOutOfRangeException. Otocz mapę ścianami albo sprawdzaj granice.
brak using System.Windows.Input;CS0246 — nie znaleziono KeyEventArgs, Key.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Opisz w oknie, jakie klawisze działają. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Tester klawiatury★☆☆

Okno pokazuje dużą czcionką nazwę ostatnio naciśniętego klawisza, a pod nią: trzymane modyfikatory („Ctrl + Shift”), czy to powtórzenie, liczbę wszystkich naciśnięć (bez powtórzeń) i listę ostatnich 10 klawiszy (najnowszy na górze). Esc czyści statystyki. Okno nie zawiera kontrolek, które mogłyby przejąć fokus.

pokaż wskazówkę
  • Modyfikatory zamienisz na tekst przez Keyboard.Modifiers.ToString() — wypisze np. „Control, Shift”.
  • Lista 10 ostatnich: Items.Insert(0, …) i RemoveAt ostatniego, gdy jest ich więcej.
ZAD 2Notatnik ze skrótami★☆☆

Notatnik z wielowierszowym polem i paskiem stanu. Skróty: Ctrl + N — nowy (z pytaniem, gdy tekst nie jest pusty), Ctrl + B — pogrubienie całego tekstu włącz/wyłącz, Ctrl + + i Ctrl + − — rozmiar czcionki, F1 — okno z listą skrótów. Pasek stanu pokazuje rozmiar czcionki i ostatni użyty skrót.

pokaż wskazówkę
  • Pole tekstowe ma fokus, więc skróty obsłuż w PreviewKeyDown okna i ustaw e.Handled = true — inaczej Ctrl + B mógłby trafić do pola.
  • Klawisze plus i minus: Key.OemPlus, Key.OemMinus, Key.Add, Key.Subtract (numeryczne).
ZAD 3Kalkulator z klawiatury★★☆

Prosty kalkulator: wyświetlacz, przyciski cyfr 0–9, + − × ÷, =, C i ⌫ w siatce. Oprócz myszy działa klawiatura: cyfry z górnego rzędu i numerycznej, + - * /, Enter jako „=”, Backspace jako ⌫ i Esc jako C. Dzielenie przez zero pokazuje „Błąd”. Przyciski nie przejmują fokusu.

pokaż wskazówkę
  • Zrób jedną metodę „wciśnięto znak” (np. "7", "+") i wywołuj ją zarówno z Click przycisków, jak i z KeyDown.
  • Cyfrę z klawisza: e.Key >= Key.D0 && e.Key <= Key.D9, a wartość to e.Key - Key.D0.
ZAD 4Przesuwanka „15”★★☆

Plansza 4 × 4 z kafelkami 1–15 i jednym pustym polem. Strzałki przesuwają na puste pole sąsiedni kafelek (strzałka w lewo przesuwa w lewo kafelek stojący na prawo od pustego pola). Na start planszę miesza stała sekwencja 40 ruchów zapisana w kodzie, żeby układ zawsze był rozwiązywalny. Licznik ruchów i komunikat po ułożeniu kafelków po kolei. R miesza od nowa.

pokaż wskazówkę
  • Trzymaj planszę w tablicy int[16] (0 = puste pole) i po każdym ruchu przepisuj napisy kafelków w UniformGrid.
  • Mieszanie: wykonaj w pętli te same ruchy co strzałki, np. z napisu "LLUURRDD…".
ZAD 5Trener pisania★★☆

Program pokazuje zdanie do przepisania. Każda poprawnie wpisana litera robi się zielona, błędna — czerwona (zdanie zbuduj z osobnych Run w jednym TextBlock). Backspace cofa ostatni znak. Po przepisaniu całego zdania program pokazuje liczbę błędów i tempo w znakach na minutę (czas od pierwszego klawisza do ostatniego). Esc zaczyna od nowa z kolejnym zdaniem z tablicy.

pokaż wskazówkę
  • Wpisywane znaki odbieraj zdarzeniem TextInput okna — e.Text zawiera gotowy znak z uwzględnieniem Shift i polskich liter. Backspace i Esc obsłuż w KeyDown.
  • Czas: zapamiętaj DateTime.Now przy pierwszym znaku i odejmij przy ostatnim; .TotalMinutes daje minuty.
ZAD 6Sokoban★★★

Klasyczna łamigłówka magazyniera na planszy z mapy tekstowej: ściany #, skrzynie $, miejsca docelowe ., gracz @. Gracz chodzi strzałkami i pcha skrzynię, jeśli za nią jest wolne pole (nie ściana ani druga skrzynia). Skrzynia na miejscu docelowym zmienia kolor. Poziom kończy się, gdy wszystkie skrzynie stoją na miejscach. Z cofa ostatni ruch (jeden krok wstecz), R zaczyna poziom od nowa. Licznik ruchów i pchnięć.

pokaż wskazówkę
  • Stan gry trzymaj w tablicy char[,], a planszę rysuj od nowa po każdym ruchu — przy kilkudziesięciu polach to szybkie.
  • Cofnięcie: przed ruchem zapamiętaj kopię tablicy ((char[,])plansza.Clone()) i pozycję gracza.
PODSUMOWANIE

Co trzeba zapamiętać

  • KeyDown okna i e.Key — który klawisz; cyfry to Key.D0–Key.D9.
  • Klawisz trafia najpierw do kontrolki z fokusem; pola i listy zużywają strzałki. PreviewKeyDown + e.Handled = true dają oknu pierwszeństwo.
  • Skróty: Keyboard.Modifiers == ModifierKeys.Control i klawisz.
  • e.IsRepeat odróżnia powtórzenia przy przytrzymaniu; KeyUp — puszczenie.
  • Siatkę o dowolnym rozmiarze zbudujesz w kodzie (RowDefinitions.Add), a kontrolkę przesuniesz przez Grid.SetRow/SetColumn.

Dokumentacja: Microsoft Learn — omówienie danych wejściowych.

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