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.
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żą
PreviewKeyDownie.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ę.
KeyDown i e.Key
<Window ... KeyDown="Window_KeyDown">
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:
| Klawisz | Wartość Key |
|---|---|
| strzałki | Key.Left, Key.Right, Key.Up, Key.Down |
| cyfry w górnym rzędzie | Key.D0 … Key.D9 |
| cyfry na klawiaturze numerycznej | Key.NumPad0 … Key.NumPad9 |
| Enter, Spacja, Backspace | Key.Enter, Key.Space, Key.Back |
| litery | Key.A … Key.Z — bez względu na wielkość litery i Caps Lock |
| F1–F12 | Key.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.
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:
| Element | Co robi |
|---|---|
PreviewKeyDown | Zdarzenie 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".
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:
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.
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.
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.
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:
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);
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.
po zebraniu pierwszej gwiazdki (w lewym dolnym rogu)
<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>
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 wierszuwi kolumniek. Ściany na brzegach sprawiają, że gracz nigdy nie wyjdzie poza tablicę. ZbudujPlanszeczyś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). switchzamienia klawisz na przesunięciedw,dk. R buduje planszę od nowa, adefaultkończy metodę dla wszystkich innych klawiszy.- Ruch jest wykonywany tylko wtedy, gdy na nowym polu nie ma ściany. Sam ruch to
Grid.SetRowiGrid.SetColumndla gracza. - Gwiazdkę zbiera
Children.Removei wpisnullw tablicy.
Sprawdź trzy rzeczy
- Przytrzymaj strzałkę — gracz zacznie biec, bo
KeyDownsię powtarza. Dopisz na początku metodyif (e.IsRepeat) return;i porównaj. - 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ńKeyDownokna naPreviewKeyDowni dopisze.Handled = true;po ruchu. - Zmień w mapie jeden znak
.na*. Liczba gwiazdek do zebrania zwiększy się sama.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
KeyDown, KeyUp | zdarzenia | Naciśnięcie / puszczenie klawisza; parametr KeyEventArgs. |
PreviewKeyDown | zdarzenie | Jak KeyDown, ale najpierw w oknie, potem w kontrolce z fokusem. |
e.Key | Key | Który klawisz. |
e.IsRepeat | bool | Powtórzenie przy przytrzymaniu. |
e.Handled | bool | true zatrzymuje dalszą wędrówkę zdarzenia. |
Keyboard.Modifiers | ModifierKeys | Trzymane Control, Shift, Alt. |
Focusable | bool | False — kontrolka nie przejmuje fokusu. |
new GridLength(40) | GridLength | Wysokość / szerokość wiersza lub kolumny tworzonej w kodzie. |
RowDefinitions.Add, ColumnDefinitions.Add | metody | Dodają wiersz / kolumnę siatki z kodu. |
Zanim utkniesz
| Sytuacja | Problem |
|---|---|
| strzałki nie działają, gdy w oknie jest pole tekstowe lub lista | Kontrolka 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.1 | Błąd — cyfry to Key.D1 albo Key.NumPad1. |
e.Key == Key.Return i Key.Enter | To ta sama wartość — wystarczy jedna. |
skrót z Alt sprawdzany przez e.Key | Przy Alt e.Key to Key.System; sprawdź e.SystemKey. |
| wyjście poza tablicę mapy | IndexOutOfRangeException. Otocz mapę ścianami albo sprawdzaj granice. |
brak using System.Windows.Input; | CS0246 — nie znaleziono KeyEventArgs, Key. |
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.
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, …)iRemoveAtostatniego, gdy jest ich więcej.
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
PreviewKeyDownokna i ustawe.Handled = true— inaczej Ctrl + B mógłby trafić do pola. - Klawisze plus i minus:
Key.OemPlus,Key.OemMinus,Key.Add,Key.Subtract(numeryczne).
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 zClickprzycisków, jak i zKeyDown. - Cyfrę z klawisza:
e.Key >= Key.D0 && e.Key <= Key.D9, a wartość toe.Key - Key.D0.
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 wUniformGrid. - Mieszanie: wykonaj w pętli te same ruchy co strzałki, np. z napisu
"LLUURRDD…".
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
TextInputokna —e.Textzawiera gotowy znak z uwzględnieniem Shift i polskich liter. Backspace i Esc obsłuż wKeyDown. - Czas: zapamiętaj
DateTime.Nowprzy pierwszym znaku i odejmij przy ostatnim;.TotalMinutesdaje minuty.
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.
Co trzeba zapamiętać
KeyDownokna ie.Key— który klawisz; cyfry toKey.D0–Key.D9.- Klawisz trafia najpierw do kontrolki z fokusem; pola i listy zużywają strzałki.
PreviewKeyDown+e.Handled = truedają oknu pierwszeństwo. - Skróty:
Keyboard.Modifiers == ModifierKeys.Controli klawisz. e.IsRepeatodróżnia powtórzenia przy przytrzymaniu;KeyUp— puszczenie.- Siatkę o dowolnym rozmiarze zbudujesz w kodzie (
RowDefinitions.Add), a kontrolkę przesuniesz przezGrid.SetRow/SetColumn.
Dokumentacja: Microsoft Learn — omówienie danych wejściowych.