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

Zdarzenia myszy

Do tej pory mysz służyła tylko do klikania przycisków. Każda kontrolka — także zwykła ramka czy napis — potrafi jednak zareagować na najechanie kursorem, opuszczenie, wciśnięcie lewego lub prawego przycisku, ruch i kółko. Dzięki temu zrobisz podświetlane kafelki, ocenę gwiazdkami, mapę sali czy prosty program do rysowania. To pierwszy krok do gier, które zbudujesz pod koniec bloku.

C# XAML MouseEnter MouseLeftButtonDown GetPosition MouseWheel 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zareagujesz na najechanie kursorem na kontrolkę i na jej opuszczenie.
  • Rozróżnisz lewy i prawy przycisk myszy oraz podwójne kliknięcie.
  • Odczytasz pozycję kursora względem wybranej kontrolki.
  • Obsłużysz kółko myszy i zmienisz wygląd kursora.
  • Podepniesz zdarzenia myszy do kontrolek tworzonych w kodzie.
  • Wyjaśnisz, dlaczego panel bez tła nie reaguje na mysz.
TEORIA

Zdarzenia myszy

ZdarzenieParametr eKiedy
MouseEnterMouseEventArgskursor wjechał nad kontrolkę
MouseLeaveMouseEventArgskursor opuścił kontrolkę
MouseMoveMouseEventArgskażdy ruch kursora nad kontrolką
MouseLeftButtonDown / UpMouseButtonEventArgswciśnięcie / puszczenie lewego przycisku
MouseRightButtonDown / UpMouseButtonEventArgsto samo dla prawego przycisku
MouseDownMouseButtonEventArgsdowolny przycisk; który — mówi e.ChangedButton
MouseWheelMouseWheelEventArgsobrót kółka; kierunek w e.Delta

Typy parametrów i klasa Cursors są w przestrzeni nazw System.Windows.Input — dopisz using System.Windows.Input;. Visual Studio dodaje je automatycznie, gdy tworzysz metodę obsługi z XAML.

TEORIA

Najechanie i opuszczenie

MainWindow.xaml
<Border x:Name="Kafel" Width="160" Height="80" Background="LightGray" CornerRadius="6"
        MouseEnter="Kafel_MouseEnter" MouseLeave="Kafel_MouseLeave">
    <TextBlock Text="Najedź na mnie" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
MainWindow.xaml.cs
private void Kafel_MouseEnter(object sender, MouseEventArgs e)
{
    Kafel.Background = Brushes.Gold;
}

private void Kafel_MouseLeave(object sender, MouseEventArgs e)
{
    Kafel.Background = Brushes.LightGray;
}

MouseEnter i MouseLeave zawsze występują w parze: co włączysz przy wejściu, wyłączasz przy wyjściu. Czy kursor jest teraz nad kontrolką, sprawdzisz też właściwością IsMouseOver.

TEORIA

Przyciski myszy

MainWindow.xaml.cs
// w XAML: MouseDown="Kafel_MouseDown"
private void Kafel_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.ChangedButton == MouseButton.Left)
    {
        Info.Text = "lewy";
    }
    else if (e.ChangedButton == MouseButton.Right)
    {
        Info.Text = "prawy";
    }
    else if (e.ChangedButton == MouseButton.Middle)
    {
        Info.Text = "środkowy (kółko)";
    }
}

Dla jednego przycisku wygodniej podpiąć MouseLeftButtonDown albo MouseRightButtonDown. Czy przycisk jest trzymany w trakcie ruchu, sprawdzisz w MouseMove albo MouseEnter: e.LeftButton == MouseButtonState.Pressed — to podstawa rysowania „przeciąganiem”.

Przycisk „zjada” lewy przycisk myszy

Button sam obsługuje lewy przycisk, żeby wywołać Click, i jego MouseLeftButtonDown nie dociera do Twojej metody. Dla przycisku używaj Click, a jeśli koniecznie potrzebujesz wciśnięcia — zdarzenia PreviewMouseLeftButtonDown. Ramki, napisy i panele tego problemu nie mają.

TEORIA

Pozycja kursora

e.GetPosition(kontrolka) zwraca punkt (Point) z polami X i Y — współrzędnymi kursora liczonymi od lewego górnego rogu podanej kontrolki. Oś Y rośnie w dół.

MainWindow.xaml.cs
private void Obszar_MouseMove(object sender, MouseEventArgs e)
{
    Point p = e.GetPosition(Obszar);       // względem obszaru
    Point w = e.GetPosition(this);         // względem całego okna
    Info.Text = $"w obszarze: {p.X:F0}, {p.Y:F0}   w oknie: {w.X:F0}, {w.Y:F0}";
}
(0, 0) (ActualWidth, ActualHeight) ✚ (170, 60)

x rośnie w prawo, y — w dół

Szerokość i wysokość kontrolki po ułożeniu w oknie podają ActualWidth i ActualHeight — przydają się np. do sprawdzenia, czy kursor jest w lewej, czy prawej połowie.

TEORIA

Kółko, dwuklik i kursor

MainWindow.xaml.cs
// kółko: Delta > 0 — od siebie, Delta < 0 — do siebie (zwykle ±120 na „ząbek”)
private void Podglad_MouseWheel(object sender, MouseWheelEventArgs e)
{
    if (e.Delta > 0 && Podglad.FontSize < 72) Podglad.FontSize += 2;
    if (e.Delta < 0 && Podglad.FontSize > 8) Podglad.FontSize -= 2;
}

// podwójne kliknięcie: liczba kliknięć w serii
private void Kafel_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (e.ClickCount == 2)
    {
        Info.Text = "Dwuklik!";
    }
}

Wygląd kursora nad kontrolką ustawisz właściwością Cursor: w XAML Cursor="Hand", w kodzie Cursors.Hand. Inne przydatne: Cross (celownik), Wait (klepsydra), No (zakaz), SizeAll (przesuwanie).

TEORIA

Tło łapie mysz

WPF przekazuje zdarzenie myszy kontrolce, która jest narysowana pod kursorem. Panel bez Background jest przezroczysty „na wylot” — kliknięcie w puste miejsce panelu go nie dotyczy, a MouseMove działa tylko nad jego dziećmi. Rozwiązanie: nadaj panelowi tło, choćby Background="Transparent" — przezroczyste, ale „klikalne”.

PRZYKŁAD

Stan stanowisk w pracowni

Mapa pracowni z 16 stanowiskami. Najechanie na kafelek podświetla go niebieską ramką i pokazuje informacje po prawej. Lewy przycisk zmienia stan stanowiska w kółko (wolne → zajęte → awaria), prawy przywraca „wolne”. Pasek stanu pokazuje liczby stanowisk w każdym stanie i współrzędne kursora nad salą.

Pracownia 204 — stan stanowisk–□✕
Lewy przycisk: zmień stan (wolne → zajęte → awaria). Prawy przycisk: przywróć „wolne”.
PC-01
PC-02
PC-03
PC-04
PC-05
PC-06
PC-07
PC-08
PC-09
PC-10
PC-11
PC-12
PC-13
PC-14
PC-15
PC-16
PC-06
Stan: awaria
Rząd 2, miejsce 2
Wolne: 12 Zajęte: 3 Awaria: 1x = 162, y = 88

kursor nad stanowiskiem PC-06 — niebieska ramka i informacje po prawej

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja25.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Pracownia 204 — stan stanowisk" Height="460" Width="620"
        MinWidth="500" MinHeight="400" WindowStartupLocation="CenterScreen">

    <DockPanel Margin="12">
        <!-- legenda u góry -->
        <TextBlock DockPanel.Dock="Top" Margin="0,0,0,8" TextWrapping="Wrap"
                   Text="Lewy przycisk: zmień stan (wolne → zajęte → awaria). Prawy przycisk: przywróć „wolne”."/>

        <!-- pasek stanu na dole -->
        <Border DockPanel.Dock="Bottom" Background="#FFE5E7EB" Padding="8,4" Margin="0,8,0,0">
            <DockPanel>
                <TextBlock x:Name="Pozycja" DockPanel.Dock="Right" Foreground="DimGray"/>
                <TextBlock x:Name="Podsumowanie"/>
            </DockPanel>
        </Border>

        <!-- panel informacji po prawej -->
        <Border DockPanel.Dock="Right" Width="170" Margin="10,0,0,0" Padding="10"
                BorderBrush="LightGray" BorderThickness="1" CornerRadius="6">
            <StackPanel>
                <TextBlock x:Name="InfoNazwa" FontSize="18" FontWeight="Bold" Text="—"/>
                <TextBlock x:Name="InfoStan" Margin="0,6,0,0" TextWrapping="Wrap"
                           Text="Najedź myszą na stanowisko."/>
            </StackPanel>
        </Border>

        <!-- sala: 16 kafelków tworzonych w kodzie -->
        <UniformGrid x:Name="Sala" Rows="4" Columns="4" Background="White"
                     MouseMove="Sala_MouseMove" MouseLeave="Sala_MouseLeave"/>
    </DockPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace Lekcja25;

public partial class MainWindow : Window
{
    private readonly string[] stany = new string[16];   // "wolne", "zajęte" albo "awaria"

    public MainWindow()
    {
        InitializeComponent();

        for (int i = 0; i < stany.Length; i++)
        {
            stany[i] = "wolne";

            Border kafel = new Border
            {
                Margin = new Thickness(4),
                CornerRadius = new CornerRadius(6),
                BorderBrush = Brushes.Gray,
                BorderThickness = new Thickness(1),
                Cursor = Cursors.Hand,                     // kursor „łapka” nad kafelkiem
                Tag = i,                                   // numer stanowiska
                Child = new TextBlock
                {
                    Text = $"PC-{i + 1:D2}",
                    HorizontalAlignment = HorizontalAlignment.Center,
                    VerticalAlignment = VerticalAlignment.Center,
                    FontWeight = FontWeights.Bold
                }
            };

            // zdarzenia myszy podpinamy w kodzie, bo kafelki powstają w kodzie
            kafel.MouseEnter += Kafel_MouseEnter;
            kafel.MouseLeave += Kafel_MouseLeave;
            kafel.MouseLeftButtonDown += Kafel_MouseLeftButtonDown;
            kafel.MouseRightButtonDown += Kafel_MouseRightButtonDown;

            Sala.Children.Add(kafel);
            Pokoloruj(kafel);
        }

        PokazPodsumowanie();
    }

    // Kursor wjechał nad kafelek
    private void Kafel_MouseEnter(object sender, MouseEventArgs e)
    {
        Border kafel = (Border)sender;
        kafel.BorderBrush = Brushes.RoyalBlue;
        kafel.BorderThickness = new Thickness(3);
        PokazInformacje(kafel);
    }

    // Kursor opuścił kafelek
    private void Kafel_MouseLeave(object sender, MouseEventArgs e)
    {
        Border kafel = (Border)sender;
        kafel.BorderBrush = Brushes.Gray;
        kafel.BorderThickness = new Thickness(1);
    }

    // Lewy przycisk: wolne → zajęte → awaria → wolne
    private void Kafel_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        Border kafel = (Border)sender;
        int nr = (int)kafel.Tag;

        if (stany[nr] == "wolne") stany[nr] = "zajęte";
        else if (stany[nr] == "zajęte") stany[nr] = "awaria";
        else stany[nr] = "wolne";

        Pokoloruj(kafel);
        PokazInformacje(kafel);
        PokazPodsumowanie();
    }

    // Prawy przycisk: szybki powrót do „wolne”
    private void Kafel_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
        Border kafel = (Border)sender;
        stany[(int)kafel.Tag] = "wolne";

        Pokoloruj(kafel);
        PokazInformacje(kafel);
        PokazPodsumowanie();
    }

    // Pozycja kursora względem sali, przy każdym ruchu myszy
    private void Sala_MouseMove(object sender, MouseEventArgs e)
    {
        Point p = e.GetPosition(Sala);
        Pozycja.Text = $"x = {p.X:F0}, y = {p.Y:F0}";
    }

    private void Sala_MouseLeave(object sender, MouseEventArgs e)
    {
        Pozycja.Text = "";
    }

    private void Pokoloruj(Border kafel)
    {
        string stan = stany[(int)kafel.Tag];

        if (stan == "wolne") kafel.Background = Brushes.Honeydew;
        else if (stan == "zajęte") kafel.Background = Brushes.LightSkyBlue;
        else kafel.Background = Brushes.LightCoral;
    }

    private void PokazInformacje(Border kafel)
    {
        int nr = (int)kafel.Tag;
        InfoNazwa.Text = $"PC-{nr + 1:D2}";
        InfoStan.Text = $"Stan: {stany[nr]}\nRząd {nr / 4 + 1}, miejsce {nr % 4 + 1}";
    }

    private void PokazPodsumowanie()
    {
        int wolne = 0, zajete = 0, awarie = 0;
        foreach (string s in stany)
        {
            if (s == "wolne") wolne++;
            else if (s == "zajęte") zajete++;
            else awarie++;
        }
        Podsumowanie.Text = $"Wolne: {wolne}   Zajęte: {zajete}   Awaria: {awarie}";
    }
}

Co dzieje się po kolei

  • Konstruktor tworzy 16 kafelków (Border z napisem) i wkłada je do UniformGrid. Numer stanowiska jest w Tag, a stan — w tablicy stany pod tym samym numerem.
  • Zdarzenia myszy kafelków są podpinane w kodzie przez +=, tak jak Click w lekcji 04. Wszystkie kafelki mają te same cztery metody, a który kafelek wywołał zdarzenie, mówi sender.
  • Cursor = Cursors.Hand pokazuje „łapkę” — użytkownik widzi, że kafelek jest klikalny.
  • MouseEnter pogrubia ramkę i wypisuje informacje, MouseLeave przywraca zwykłą ramkę. Panel informacji zostaje z danymi ostatniego kafelka.
  • Lewy przycisk przesuwa stan o jeden dalej w kółko, prawy ustawia „wolne”. Po każdej zmianie odświeżamy kolor, informacje i podsumowanie.
  • {i + 1:D2} formatuje liczbę na dwóch cyfrach z zerem z przodu: 1 → „01”.
  • Sala ma Background="White", więc MouseMove działa także w szczelinach między kafelkami. e.GetPosition(Sala) daje współrzędne względem lewego górnego rogu sali.
  • Rząd i miejsce wynikają z numeru: nr / 4 i nr % 4 (jak w grze „Zgaś światła” z lekcji 12).

Sprawdź trzy rzeczy

  1. Usuń z UniformGrid atrybut Background="White". Współrzędne zmieniają się tylko nad kafelkami — w szczelinach zastygają.
  2. Usuń podpięcie MouseLeave. Najechane kafelki zostaną z niebieską ramką na zawsze.
  3. Zmień w Sala_MouseMove GetPosition(Sala) na GetPosition(this). Współrzędne w lewym górnym rogu sali nie będą już równe (0, 0).
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
e.GetPosition(kontrolka)PointPozycja kursora względem kontrolki (X, Y).
e.ChangedButtonMouseButtonLeft, Right, Middle.
e.ClickCountint1 — kliknięcie, 2 — dwuklik.
e.LeftButtonMouseButtonStatePressed, gdy lewy przycisk jest trzymany.
e.DeltaintKierunek obrotu kółka (> 0 od siebie).
IsMouseOverboolCzy kursor jest nad kontrolką.
Cursor, Cursors.HandkursorWygląd kursora nad kontrolką.
ActualWidth, ActualHeightdoubleRzeczywisty rozmiar kontrolki w oknie.
{x:D2}formatowanieLiczba całkowita na co najmniej 2 cyfrach.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
MouseLeftButtonDown na ButtonMetoda się nie wywołuje — przycisk obsługuje lewy przycisk sam. Użyj Click.
panel bez BackgroundKliknięcia i ruch w pustych miejscach nie są zgłaszane. Dodaj Background="Transparent".
MouseEnter z parametrem MouseButtonEventArgsBłąd: sygnatura nie pasuje — parametr to MouseEventArgs.
brak using System.Windows.Input;CS0246 — nie znaleziono MouseEventArgs, Cursors.
podświetlenie w MouseEnter, bez MouseLeaveKontrolka zostaje podświetlona po odjechaniu kursora.
e.GetPosition(this), gdy chodzi o pozycję w obszarzeWspółrzędne liczone od rogu okna, nie obszaru.
ciężkie obliczenia w MouseMoveZdarzenie przychodzi dziesiątki razy na sekundę — program zaczyna „przycinać”.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Kontrolki reagujące na mysz zrób z ramek, napisów i paneli — nie z przycisków. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Menu z podświetleniem★☆☆

Pionowe menu z pięciu kafelków (Start, Plan lekcji, Oceny, Wiadomości, Ustawienia). Najechanie zmienia tło kafelka na jasnoniebieskie i kursor na „łapkę”, a pod menu wyświetla krótki opis sekcji. Kliknięcie wybiera sekcję: wybrany kafelek ma ciemnoniebieskie tło i biały tekst także po odjechaniu kursora, a nagłówek okna pokazuje jej nazwę.

pokaż wskazówkę
  • Wybrany kafelek zapamiętaj w polu klasy typu Border?. W MouseLeave nie zmieniaj tła wybranego kafelka.
ZAD 2Licznik na wszystkie sposoby★☆☆

Duży kafelek z liczbą na środku. Lewy przycisk: +1, prawy: −1, kółko od siebie: +10, do siebie: −10, podwójne kliknięcie lewym: zerowanie. Liczba jest ograniczona do zakresu −100…100, a jej kolor zależy od znaku (zielona, czerwona, czarna dla zera). Pod kafelkiem opis ostatniej akcji: „kółko ↑ (+10)”.

pokaż wskazówkę
  • Dwuklik to dwa zdarzenia MouseLeftButtonDown: przy pierwszym ClickCount = 1 (doda 1), przy drugim = 2. Przy zerowaniu nie przejmuj się tym pierwszym dodaniem — i tak zerujesz.
ZAD 3Ćwiartki układu współrzędnych★★☆

Obszar 400 × 300 z cienką ramką. Środek obszaru to punkt (0, 0) „matematycznego” układu, w którym oś Y rośnie w górę. Podczas ruchu myszy program pokazuje współrzędne w tym układzie (np. „x = −120, y = 45”), numer ćwiartki (I–IV, „na osi”, gdy x albo y wynosi 0) i koloruje tło obszaru inaczej dla każdej ćwiartki. Po wyjechaniu poza obszar napis „Poza obszarem”, tło białe.

pokaż wskazówkę
  • Przeliczenie: x = p.X − szerokość / 2, y = wysokość / 2 − p.Y (minus odwraca oś).
ZAD 4Ocena gwiazdkami★★☆

Pięć dużych gwiazdek (napisy „★”, czcionka 40) w poziomym rzędzie. Najechanie na gwiazdkę pokazuje podgląd: gwiazdki do tej włącznie są złote, dalsze szare, a pod spodem opis („3 — dobry”). Po odjechaniu z całego rzędu wraca wybrana ocena. Kliknięcie ustala ocenę. Przycisk Wyślij opinię jest aktywny dopiero po wyborze oceny.

Oceń lekcję–□✕
★★★★★
3 — dobry
Wyślij opinię
pokaż wskazówkę
  • Numer gwiazdki trzymaj w Tag. Jedna metoda „pokoloruj do n” wystarczy dla podglądu i dla wybranej oceny.
  • MouseLeave podepnij do całego panelu z gwiazdkami — inaczej przejście między gwiazdkami będzie „mrugać”.
ZAD 5Kolorowanka po numerach★★☆

Siatka 8 × 8 pól z numerami 1–4 (wzór zapisz w tablicy, np. prosty domek albo serce). Po lewej paleta czterech kolorów z numerami. Wybrany kolor ma pogrubioną ramkę. Kliknięcie pola maluje je wybranym kolorem; prawy przycisk czyści pole. Pod siatką „Poprawnie pomalowane: 41 / 64”, a po ukończeniu — „Obrazek gotowy!” i znikają numery.

pokaż wskazówkę
  • Wzór: int[] wzor z 64 liczbami; pomalowany kolor każdego pola trzymaj w drugiej tablicy (0 = puste).
ZAD 6Edytor pixel-artu★★★

Siatka 16 × 16 kwadratowych pól i paleta 8 kolorów. Rysowanie „przeciąganiem”: wciśnięcie lewego przycisku i ruch nad polami maluje je wybranym kolorem; prawy przycisk wymazuje. Kółko myszy nad siatką zmienia wybrany kolor na następny/poprzedni w palecie. Pole wyboru Siatka pokazuje/ukrywa linie między polami. Przycisk Wyczyść (z potwierdzeniem) i napis z liczbą pomalowanych pól. Kursor nad siatką to celownik.

pokaż wskazówkę
  • Malowanie przeciąganiem: w MouseEnter pola sprawdź e.LeftButton == MouseButtonState.Pressed. Samo wciśnięcie na pierwszym polu obsłuż w MouseLeftButtonDown.
  • Linie siatki to BorderThickness pól — ustawiasz 0 albo 0,5 w pętli.
PODSUMOWANIE

Co trzeba zapamiętać

  • MouseEnter i MouseLeave w parze — podświetlenie i jego wyłączenie.
  • MouseLeftButtonDown, MouseRightButtonDown albo MouseDown z e.ChangedButton; dwuklik — e.ClickCount == 2.
  • e.GetPosition(kontrolka) — pozycja od lewego górnego rogu kontrolki, y w dół.
  • MouseWheel i e.Delta — kierunek kółka; Cursor — wygląd kursora.
  • Panel reaguje na mysz w pustych miejscach tylko wtedy, gdy ma tło.
  • Przycisk sam obsługuje lewy przycisk myszy — dla niego zostaje Click.

Dokumentacja: Microsoft Learn — dane wejściowe myszy.

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