Programowanie Aplikacji Lokalnych / Programowanie .NET
Rok akademicki: 2026/27
Instrukcja laboratoryjna: Architektura Frameworka WPF i Język XAML (.NET 8/9/10), Drzewo Wizualne, Data Binding, Dispatcher i P/Invoke
Prowadzący: Tomasz Goluch
Wersja: 1.0
1. Wprowadzenie: Architektura WPF, Język XAML i Kompilacja Częściowa
Cel: Zrozumienie architektury podsystemu graficznego Windows Presentation Foundation (WPF), deklaratywnego języka znaczników XAML oraz mechanizmu kompilacji widoków do klas częściowych C#.
Windows Presentation Foundation (WPF) to
zaawansowany, wektorowy podsystem prezentacji dla aplikacji desktopowych
w systemie Windows, wchodzący w skład platformy .NET (.NET 8 LTS
/ .NET 9 / .NET 10 LTS). W odróżnieniu od klasycznego systemu
Windows Forms (opartego na wiekowym interfejsie GDI/GDI+ oraz uchwytach
okien HWND dla każdej kontrolki), WPF renderuje cały
interfejs w oparciu o potok DirectX z pełną akceleracją
sprzętową karty graficznej (GPU). Oferuje to płynne skalowanie na
ekranach o wysokim DPI (4K+), zaawansowane zagnieżdżanie dowolnych
kontrolek, bogaty system stylów i szablonów oraz natywną obsługę
animacji.
Interfejs użytkownika w WPF definiujemy za pomocą deklaratywnego języka znaczników XAML (Extensible Application Markup Language), co pozwala na czystą separację warstwy wizualnej (widoku) od logiki aplikacyjnej i biznesowej.
Kluczowe zasady języka XAML:
- Wrażliwość na wielkość liter (Case-Sensitivity): XAML jest dialektem XML i rygorystycznie rozróżnia wielkość liter – nazwy znaczników i atrybutów muszą dokładnie odpowiadać nazwom typów i właściwości w C#.
- Mapowanie 1:1 na klasy CLR: Każdy znacznik XML (np.
<Button>) odpowiada utworzeniu instancji klasy w pamięci (new Button()), a każdy atrybut (np.Content="Zatwierdź") odpowiada przypisaniu wartości do właściwości lub subskrypcji zdarzenia tego obiektu. - Składnia elementów właściwości (Property Element
Syntax): Gdy wartość właściwości jest zbyt złożona, aby
zapisać ją jako prosty tekst w atrybucie (np. gradient lub złożona
zawartość), zapisujemy ją jako podznacznik
<NazwaKlasy.NazwaWlasciwosci>.
Proces
kompilacji XAML i klasy częściowe (*.g.cs oraz
*.baml)
Logika okna implementowana jest w plikach code-behind (z
rozszerzeniem .xaml.cs), które łączą się z plikiem
.xaml za pomocą mechanizmu klas częściowych
(partial class).
[ MainWindow.xaml ] [ MainWindow.xaml.cs ]
│ │
Kompilator znaczników (Markup Compiler) │
┌──────────────┴──────────────┐ │
▼ ▼ ▼
[ MainWindow.baml ] [ obj/.../MainWindow.g.cs ] ◄─────┘
(Skompilowany binarny (Wygenerowana partial class:
zasób XAML w .dll) pola kontrolek x:Name + InitializeComponent())
│ │
└──────────────┬──────────────┘
▼
[ Kompilator Roslyn (C#) ] ──► Zestaw wynikowy (.dll / .exe)
Podczas budowania projektu kompilator XAML wykonuje dwa kroki:
- Tłumaczy strukturę XAML do skompilowanego formatu binarnego
BAML (Binary Application Markup Language),
osadzanego w zasobach pliku wynikowego
.dll. - Generuje w katalogu pośrednim
obj\<Konfiguracja>\net8.0-windows\(lubnet9.0-windows/net10.0-windows) plikMainWindow.g.cs. Zawiera on drugą część klasypartial class MainWindowz polami dla wszystkich kontrolek oznaczonych atrybutemx:Nameoraz definicję metodyInitializeComponent(), która w czasie uruchomienia ładuje strumień BAML i buduje drzewo obiektów.
Anatomia podstawowych plików projektu WPF
App.xaml(orazApp.xaml.cs):- Głównym znacznikiem jest
<Application>, definiujący punkt startowy, cykl życia procesu oraz słownik zasobów globalnych (<Application.Resources>). - Atrybut
StartupUri="MainWindow.xaml"wskazuje okno otwierane automatycznie po uruchomieniu programu (w zaawansowanych aplikacjach z kontenerem Dependency Injection często zastępujemy go nadpisaniem metodyOnStartupwApp.xaml.cs). - Nowość w .NET 9 / .NET 10: Atrybut
ThemeMode="System"(lub"Light"/"Dark") w znaczniku<Application>aktywuje natywny styl Windows 11 Fluent Design.
- Głównym znacznikiem jest
MainWindow.xaml(orazMainWindow.xaml.cs):- Głównym znacznikiem jest
<Window>, reprezentujący okno systemowe. - Mapowania przestrzeni nazw XML
(
xmlns):xmlns="[http://schemas.microsoft.com/winfx/2006/xaml/presentation](http://schemas.microsoft.com/winfx/2006/xaml/presentation)"– domyślna przestrzeń nazw zawierająca wszystkie standardowe kontrolki WPF (dzięki atrybutowi[XmlnsDefinition]na poziomie zestawu łączy wiele przestrzeni CLR pod jednym adresem URI).xmlns:x="[http://schemas.microsoft.com/winfx/2006/xaml](http://schemas.microsoft.com/winfx/2006/xaml)"– przestrzeń konstrukcji języka XAML (np.x:Class,x:Name,x:Key,x:Type,x:Static).xmlns:d="[http://schemas.microsoft.com/expression/blend/2008](http://schemas.microsoft.com/expression/blend/2008)"orazmc:Ignorable="d"– przestrzeń czasu projektowania (Design-Time). Atrybuty takie jakd:Width="800",d:Height="450"lubd:DataContextdziałają wyłącznie w podglądzie projektanta Visual Studio i są całkowicie ignorowane podczas kompilacji programu.- Mapowanie własnych przestrzeni nazw C#
(
clr-namespace):
Aby użyć w XAML własnych klas z projektu (np. konwerterów, kontrolek użytkownika lub ViewModeli), deklarujemy prefiks:
(Jeżeli docelowa przestrzeń nazw znajduje się w tym samym projekcie co plik XAML, członxmlns:local="clr-namespace:MojaAplikacja.ViewModels;assembly=MojaAplikacja";assembly=...można pominąć).
- Głównym znacznikiem jest
⚠️ Ograniczenie parsera XAML przy klasach zagnieżdżonych:
Analizator XAML nie obsługuje bezpośredniego tworzenia instancji klas zagnieżdżonych wewnątrz innych klas C# ani automatycznego przechodzenia do podprzestrzeni nazw po kropce (ponieważ kropka w składni<local:Klasa.Wlasciwosc>jest zarezerwowana dla właściwości obiektu). Każda podprzestrzeń nazw wymaga osobnej deklaracjixmlns.
2. Kontenery Układu (Layout Panels) i Pozycjonowanie Elementów
Cel: Opanowanie responsywnego rozmieszczania kontrolek przy użyciu wbudowanych paneli układu oraz systemu pomiaru w pikselach niezależnych od urządzenia (DIP).
W przeciwieństwie do sztywnego pozycjonowania pikselowego, WPF wykorzystuje dwuetapowy silnik układu (Measure Pass – pomiar pożądanego rozmiaru dzieci oraz Arrange Pass – ostateczne rozmieszczenie na ekranie). Wszystkie wymiary w WPF podawane są w pikselach niezależnych od urządzenia (DIP – Device-Independent Pixels), gdzie 1 jednostka odpowiada zawsze \(\frac{1}{96}\) cala fizycznego, co gwarantuje automatyczne skalowanie na monitorach o różnym zagęszczeniu pikseli.
Podstawowe panele układu w WPF
| Panel układu | Charakterystyka i zastosowanie | Kluczowe właściwości dołączone / sterujące |
|---|---|---|
Grid |
Domyślny i najbardziej wszechstronny kontener. Dzieli przestrzeń na siatkę wierszy i kolumn (odpowiednik CSS Grid). | Grid.Row,
Grid.Column, Grid.RowSpan,
Grid.ColumnSpan |
StackPanel |
Układa elementy jeden za drugim w pojedynczej linii (pionowo lub poziomo). Nie zawija elementów po wyczerpaniu miejsca. | Orientation="Vertical"
(domyślnie) lub "Horizontal" |
DockPanel |
Przyciąga (dokuje) kolejne elementy do
krawędzi okna (Top, Bottom, Left,
Right). Idealny jako główny szkielet aplikacji (pasek menu,
pasek stanu, obszar roboczy). |
DockPanel.Dock="Top",
LastChildFill="True" (domyślnie ostatnie dziecko wypełnia
środek) |
WrapPanel |
Układa elementy sekwencyjnie w wierszu lub kolumnie, ale automatycznie zawija je do nowego wiersza/kolumny, gdy brakuje miejsca. | Orientation,
ItemWidth, ItemHeight |
Canvas |
Płótno o bezwzględnych współrzędnych \((X, Y)\). Nie skaluje zawartości automatycznie – stosowany głównie do wykresów, rysunków CAD i gier 2D. | Canvas.Left,
Canvas.Top, Canvas.Right,
Canvas.Bottom, Panel.ZIndex |
Definiowanie
wymiarów w panelu Grid (Auto, *,
px)
W siatce Grid szerokość kolumn i wysokość wierszy można
definiować na trzy sposoby:
Auto– wiersz/kolumna dopasowuje się ściśle do rozmiaru największej umieszczonej w niej kontrolki.*(Star sizing – rozmiar proporcjonalny): dzieli pozostałą wolną przestrzeń okna proporcjonalnie (np. dwie kolumny o szerokości1*oraz2*podzielą wolne miejsce w stosunku \(1:2\)).- Wartość liczbowa (np.
120): sztywny rozmiar wyrażony w jednostkach DIP.
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Wiersz 0: dopasowany do nagłówka -->
<RowDefinition Height="*"/> <!-- Wiersz 1: zajmuje całą pozostałą wysokość -->
<RowDefinition Height="40"/> <!-- Wiersz 2: sztywna stopka 40 DIP -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/> <!-- Kolumna 0: 1/3 szerokości -->
<ColumnDefinition Width="2*"/> <!-- Kolumna 1: 2/3 szerokości -->
</Grid.ColumnDefinitions>
<!-- Nagłówek rozciągnięty na 2 kolumny (ColumnSpan="2") -->
<TextBlock FontSize="18" FontWeight="Bold" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" Margin="0,0,0,10" Text="Panel Sterowania"/>
<ListBox Grid.Column="0" Grid.Row="1" Margin="0,0,5,0"/>
<ContentControl Grid.Column="1" Grid.Row="1" Margin="5,0,0,0"/>
<StatusBar Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2"/>
</Grid>
💡 Kolejność nakładania elementów (
Panel.ZIndex):
Gdy kilka kontrolek zajmuje tę samą komórkę siatkiGridlub nakłada się na siebie wCanvas, o tym, która z nich jest widoczna na wierzchu, decyduje kolejność deklaracji w pliku XAML (późniejsze elementy przykrywają wcześniejsze). Kolejność tę można jawnie nadpisać właściwością dołączonąPanel.ZIndex="10"– element o wyższej wartościZIndexjest zawsze rysowany na pierwszym planie.
3.
Zasoby Logiczne (Resources): StaticResource vs
DynamicResource
Cel: Organizacja reużywalnych obiektów (pędzli, kolorów, stylów, konwerterów) w słownikach zasobów oraz zrozumienie różnicy między wiązaniem statycznym a dynamicznym.
W WPF oprócz zasobów binarnych osadzanych w plikach zestawu (obrazy
.png, ikony, czcionki) istnieją Zasoby Logiczne
(ResourceDictionary). Pozwalają one zdefiniować w
XAML dowolny obiekt .NET pod unikalnym kluczem
x:Key i wielokrotnie używać go w różnych
częściach interfejsu.
Każdy element dziedziczący po FrameworkElement (np.
Window, UserControl, Grid,
Button) oraz klasa Application posiadają
własną właściwość Resources. Wyszukiwanie zasobu o podanym
kluczu odbywa się hierarchicznie w górę drzewa logicznego:
Kontrolka → Panel nadrzędny (np. Grid) →
Okno (Window.Resources) →
Aplikacja (Application.Resources) →
Motyw systemowy.
Porównanie
rozszerzeń znaczników StaticResource i
DynamicResource
| Cecha | {StaticResource Klucz} |
{DynamicResource Klucz} |
|---|---|---|
| Moment rozwiązania | Jednorazowo podczas ładowania i parsowania strumienia XAML (przed wyświetleniem okna). | W locie (w czasie wykonania), za każdym razem gdy obiekt pod danym kluczem w słowniku zostanie podmieniony. |
| Reakcja na podmianę obiektu w słowniku | Brak – przypisanie nowego
obiektu Resources["Klucz"] = nowyPedzel; zostanie
zignorowane. |
Automatyczna aktualizacja UI (np. przy zmianie motywu Jasny/Ciemny lub zmianie kolorów w systemie Windows). |
| Wymagania kolejności w XAML | Zasób musi być zadeklarowany w XAML wcześniej (wyżej) niż miejsce jego użycia. | Zasób może zostać załadowany lub dodany do słownika później (np. w kodzie C#). |
| Wydajność | Wysoka (brak narzutu na śledzenie zmian w słowniku). | Nieco niższa (utrzymuje wyrażenie
nasłuchujące zmian w ResourceDictionary). |
Przykład praktyczny: Reakcja na zmianę koloru systemowego w czasie rzeczywistym
<Window.Resources>
<!-- Własny zasób logiczny pędzla -->
<SolidColorBrush Color="#05E0E9" x:Key="AccentBrush"/>
</Window.Resources>
<StackPanel Margin="20">
<!-- 1. StaticResource: Pobierze kolor pulpitu TYLKO raz przy starcie okna -->
<Button Margin="0,0,0,10" Padding="10">
<Button.Background>
<SolidColorBrush Color="{StaticResource {x:Static SystemColors.DesktopColorKey}}"/>
</Button.Background>
Przycisk StaticResource (stały po uruchomieniu)
</Button>
<!-- 2. DynamicResource: Odświeży kolor natychmiast po zmianie tła w Ustawieniach Windows! -->
<Button Padding="10">
<Button.Background>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.DesktopColorKey}}"/>
</Button.Background>
Przycisk DynamicResource (reaguje na żywo)
</Button>
</StackPanel>
(Aby zaobserwować różnicę podczas działania programu, otwórz w
systemie Windows: Ustawienia → Personalizacja → Tło,
ustaw Jednolity kolor i zmień barwę – przycisk z
DynamicResource natychmiast zmieni swój wygląd).
4.
Kaskadowe Style (Style) i Dziedziczenie Wyglądu
(BasedOn)
Cel: Ujednolicenie wyglądu kontrolek w aplikacji przy użyciu stylów niejawnych, jawnych, dziedziczonych oraz programowych.
Obiekt Style pozwala zgrupować zestaw wartości
właściwości (<Setter>) oraz wyzwalaczy
(<Trigger>) i zaaplikować je do wielu kontrolek
jednocześnie – analogicznie do arkuszy CSS w technologiach webowych.
1. Style
niejawne (Implicit Styles – bez atrybutu x:Key)
Gdy w definicji stylu podamy wyłącznie atrybut
TargetType="TextBox" (pomijając x:Key), styl
zostanie automatycznie nałożony na wszystkie kontrolki
typu TextBox znajdujące się w zasięgu danego słownika
zasobów:
<Window.Resources>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="6,4"/>
</Style>
</Window.Resources>
2. Style jawne
(Explicit Styles – z atrybutem x:Key)
Dodanie atrybutu x:Key="TitleText" sprawia, że styl jest
stosowany wyłącznie do kontrolek, które jawnie się do niego
odwołają:
<Style TargetType="TextBox" x:Key="TitleText">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<!-- Użycie w kodzie XAML: -->
<TextBox Style="{StaticResource TitleText}" Text="Nagłówek formularza"/>
3. Dziedziczenie stylów
(BasedOn)
Style mogą rozszerzać inne style (zarówno jawne, jak i domyślny styl
bazowy kontrolki) za pomocą atrybutu BasedOn:
<!-- Rozszerzenie domyślnego (niejawnego) stylu dla typu TextBox: -->
<Style BasedOn="{StaticResource {x:Type TextBox}}" TargetType="TextBox" x:Key="HighlightedTextBox">
<Setter Property="BorderBrush" Value="DodgerBlue"/>
<Setter Property="BorderThickness" Value="2"/>
</Style>
4. Programowe przypisanie stylu w C#
Aby przypisać styl z poziomu pliku code-behind, kontrolka
musi posiadać nazwę x:Name:
InputNumberTextBox.Style = (Style)FindResource("TitleText");
5.
Szablony Kontrolek (ControlTemplate),
TemplateBinding i Wyzwalacze (Triggers)
Cel: Całkowita przebudowa struktury wizualnej istniejących kontrolek (koncepcja Lookless Controls) z zachowaniem ich wbudowanej funkcjonalności i logiki.
Kontrolki w WPF są tzw. kontrolkami bezpostaciowymi
(Lookless Controls) – klasa Button
definiuje wyłącznie logikę zachowania (obsługę kliknięcia, fokusu
klawiatury, stanów IsPressed / IsMouseOver),
natomiast jej fizyczny wygląd pochodzi w 100% z wymiennego szablonu
ControlTemplate.
Budowa własnego
szablonu ControlTemplate
Definiując nowy szablon dla przycisku, musimy pamiętać o trzech kluczowych elementach:
ContentPresenter: Specjalny znacznik wewnątrz szablonu określający miejsce, w którym ma zostać wyrenderowana zawartość (Content) przekazana przez użytkownika przycisku (np. tekst"STOP"lub ikona).{TemplateBinding Wlasciwosc}: Lekkie wiązanie pobierające wartość właściwości bezpośrednio z szablonowanej kontrolki (np.Background,Foreground,Padding), dzięki czemu nasz szablon pozostaje elastyczny.<ControlTemplate.Triggers>: Ponieważ nadpisanie szablonu usuwa domyślną animację najechania kursorem i wciśnięcia, interakcje wizualne definiujemy za pomocą wyzwalaczy (Trigger) reagujących na właściwości takie jakIsMouseOverczyIsPressed.
<Window.Resources>
<ControlTemplate TargetType="Button" x:Key="RoundedButtonTemplate">
<Grid>
<!-- Element tła narysowany wektorowo za pomocą Path i EllipseGeometry -->
<Path Fill="{TemplateBinding Background}" Stretch="Uniform" Stroke="{TemplateBinding Foreground}" StrokeThickness="2" UseLayoutRounding="False" x:Name="backgroundElement">
<Path.Data>
<EllipseGeometry RadiusX="1" RadiusY="1"/>
</Path.Data>
</Path>
<!-- Miejsce wstrzyknięcia zawartości przycisku (Content) -->
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<!-- Definicja zachowania wizualnego przy interakcji z myszą -->
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" TargetName="backgroundElement" Value="AliceBlue"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="backgroundElement" Value="LightSteelBlue"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Resources>
<!-- Zastosowanie szablonu na przycisku: -->
<Button Background="Crimson" FontWeight="Bold" Foreground="White" Height="100" Template="{StaticResource RoundedButtonTemplate}" Width="100">
STOP
</Button>
6.
Drzewo Logiczne vs Drzewo Wizualne oraz Zdarzenia Trasowane
(Routed Events)
Cel: Zrozumienie dwupoziomowej struktury drzewiastej interfejsu WPF oraz mechanizmu propagacji zdarzeń (bąbelkowania i tunelowania).
1.
Drzewo Logiczne (Logical Tree) a Drzewo Wizualne
(Visual Tree)
W każdej aplikacji WPF współistnieją dwa drzewa obiektów:
- Drzewo Logiczne (
LogicalTreeHelper): Odzwierciedla strukturę znaczników zadeklarowaną przez programistę w pliku XAML (np.Window→Grid→Button). Odpowiada za:- wyszukiwanie zasobów (
StaticResource/DynamicResource), - dziedziczenie wartości
DataContextoraz właściwości zależności (np.FontSizeustawiony na oknie spływa do wszystkich przycisków), - rozwiązywanie nazw elementów (
ElementName).
- wyszukiwanie zasobów (
- Drzewo Wizualne (
VisualTreeHelper): Rozwija każdy element drzewa logicznego o jego wewnętrzną strukturę wynikającą z zaaplikowanego szablonuControlTemplate(np. pojedynczy<Button>rozwija się naBorder→ContentPresenter→TextBlock). Odpowiada za faktyczne renderowanie grafiki oraz trasowanie zdarzeń wejścia.
DRZEWO LOGICZNE (XAML) DRZEWO WIZUALNE (Po rozwinięciu ControlTemplate)
[ Window ] [ Window ]
│ │
[ Grid ] [ Border ]
│ │
[ Button ] ──────(Szablon przycisku)──────► [ Grid ]
│
[ ButtonChrome ]
│
[ ContentPresenter ]
│
[ TextBlock ]
🔍 Diagnostyka w czasie rzeczywistym (Live Visual Tree):
Podczas debugowania aplikacji w Visual Studio nad oknem programu pojawia się czarny pasek narzędziowy. Kliknięcie ikony „Przejdź do dynamicznego drzewa wizualnego” (Go to Live Visual Tree) pozwala na żywo badać rozwinięte drzewo wizualne oraz sprawdzać w oknie Live Property Explorer, skąd pochodzi aktualna wartość każdej właściwości (ze stylu, wiązania czy wartości lokalnej).
2. Zdarzenia Trasowane
(Routed Events)
Wyobraźmy sobie przycisk <Button>, wewnątrz
którego umieszczono złożony układ: <StackPanel> z
obrazkiem <Image> i tekstem
<TextBlock>. Gdy użytkownik kliknie myszą dokładnie
na piksele obrazka, w klasycznych systemach okienkowych zdarzenie
trafiłoby tylko do kontrolki Image, która nie wie, że leży
wewnątrz przycisku.
WPF rozwiązuje ten problem za pomocą Zdarzeń Trasowanych
(Routed Events), które wędrują wzdłuż drzewa
wizualnego według jednej z trzech strategii:
- Zdarzenia bąbelkujące (Bubbling Events):
Zostają wywołane na elemencie źródłowym (np.
Image), a następnie wędrują w górę drzewa przez kolejnych rodziców (StackPanel→Button→Grid→Window). Przykłady:MouseDown,KeyDown,Button.Click,Drop. - Zdarzenia tunelujące (Tunneling Events – z
prefiksem
Preview*): Każde zdarzenie bąbelkujące posiada swój odpowiednik tunelujący, który wyrusza z korzenia okna (Window) w dół drzewa aż do elementu źródłowego, zanim jeszcze wykona się właściwe zdarzenie bąbelkujące. Przykłady:PreviewMouseDown,PreviewKeyDown(idealne do przechwytywania i blokowania klawiszy przed wpisaniem ich doTextBox). - Zdarzenia bezpośrednie (Direct Events):
Wykonywane wyłącznie na samym elemencie źródłowym (jak klasyczne
zdarzenia CLR). Przykład:
MouseEnter,MouseLeave.
// Obsługa zdarzenia Drag & Drop (wymaga ustawienia AllowDrop="True" w XAML):
private void Button_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
// Przetworzenie upuszczonych plików...
}
// Zatrzymanie dalszej propagacji zdarzenia dla pozostałych rodziców w drzewie:
e.Handled = true;
}
Rejestracja procedury obsługi w kodzie C#:
// Wariant 1: Standardowy operator +=
b1.Drop += Button_Drop;
// Wariant 2: Metoda AddHandler (pozwala przechwycić zdarzenie nawet wtedy,
// gdy wcześniejszy element w drzewie ustawił już flagę e.Handled = true!):
b1.AddHandler(UIElement.DropEvent, new DragEventHandler(Button_Drop), handledEventsToo: true);
7.
System Właściwości: DependencyProperty,
DependencyObject i AttachedProperty
Cel: Zrozumienie architektury właściwości zależności
oraz implementacja własnych DependencyProperty i
AttachedProperty.
Zwykła właściwość języka C#
(public string Text { get; set; }) przechowuje wartość w
prywatnym polu obiektu w pamięci RAM. Gdyby każda kontrolka WPF
(posiadająca ponad 100 właściwości: czcionki, marginesy, pędzle, efekty)
alokowała w każdej instancji pamięć na 100 pól – z których 95% ma
wartość domyślną – aplikacja zużywałaby setki megabajtów RAM-u.
Dlatego kontrolki WPF dziedziczą po klasie
DependencyObject, a ich właściwości
budowane są jako Właściwości Zależności
(DependencyProperty):
- Wartości nie są trzymane w polach instancji, lecz w rzadkim słowniku (sparse storage) tylko wtedy, gdy różnią się od wartości domyślnej.
- Obsługują system priorytetów wartości (animacja
> wartość lokalna /
Binding>Triggerszablonu >Setterstylu > dziedziczenie po rodzicu > wartość domyślna metadanych). - Wbudowują automatyczne powiadamianie silnika renderowania oraz mechanizmu Data Binding o każdej zmianie wartości.
1.
Implementacja własnej właściwości zależności
(DependencyProperty)
Tworząc własną właściwość zależności, musimy ściśle przestrzegać
konwencji nazewniczej: statyczne pole readonly typu
DependencyProperty musi nazywać się
<NazwaWlasciwosci>Property (z sufiksem
Property), a właściwość C# pełni jedynie rolę cienkiej
powłoki (wrappera) wywołującej GetValue i
SetValue:
using System.Windows;
namespace MyApp.Controls;
public class ExampleDependencyObject : DependencyObject
{
// 1. Rejestracja statyczna (konwencja nazwy pola: Nazwa + "Property"):
public static readonly DependencyProperty ReadyForBindingProperty =
DependencyProperty.Register(
name: nameof(ReadyForBinding),
propertyType: typeof(string),
ownerType: typeof(ExampleDependencyObject),
typeMetadata: new PropertyMetadata(defaultValue: "Stan początkowy"));
// 2. Wrapper CLR (UWAGA: Wewnątrz get/set NIE WOLNO umieszczać żadnej dodatkowej logiki,
// ponieważ silnik XAML wywołuje bezpośrednio metody GetValue/SetValue!):
public string ReadyForBinding
{
get => (string)GetValue(ReadyForBindingProperty);
set => SetValue(ReadyForBindingProperty, value);
}
}
(W Visual Studio wpisanie skrótu
propdp i dwukrotne naciśnięcie klawisza
Tab automatycznie wygeneruje powyższy szablon).
2.
Implementacja właściwości dołączonej
(AttachedProperty)
Właściwość dołączona (Attached Property) to
specjalny rodzaj DependencyProperty, który jest definiowany
w jednej klasie (np. DockPanel lub Grid), ale
przypisywany dowolnym innym kontrolkom (np.
<CheckBox DockPanel.Dock="Top" />).
Rejestrujemy ją za pomocą metody
DependencyProperty.RegisterAttached oraz
pary publicznych metod statycznych
Get<Nazwa> i
Set<Nazwa>:
using System.Windows;
namespace MyApp.Helpers;
public static class SecurityBadgeHelper
{
public static readonly DependencyProperty AccessLevelProperty =
DependencyProperty.RegisterAttached(
name: "AccessLevel",
propertyType: typeof(int),
ownerType: typeof(SecurityBadgeHelper),
defaultMetadata: new PropertyMetadata(0));
// Wymagana statyczna metoda odczytu dla analizatora XAML:
public static int GetAccessLevel(DependencyObject obj) =>
(int)obj.GetValue(AccessLevelProperty);
// Wymagana statyczna metoda zapisu dla analizatora XAML:
public static void SetAccessLevel(DependencyObject obj, int value) =>
obj.SetValue(AccessLevelProperty, value);
}
Użycie w XAML na dowolnej kontrolce:
<Button Content="Panel Admina" local:SecurityBadgeHelper.AccessLevel="5"/>
(Skrót snippeta w Visual Studio:
propa + Tab
Tab).
8.
Mechanizm Wiązania Danych (Data Binding),
INotifyPropertyChanged i Kolekcje
Cel: Opanowanie deklaratywnego łączenia kontrolek
interfejsu z obiektami danych za pomocą DataContext, trybów
przepływu Binding.Mode, interfejsu
INotifyPropertyChanged oraz kolekcji
ObservableCollection<T>.
Wiązanie danych (Data Binding) tworzy automatyczny most
komunikacyjny między Celem wiązania (Binding
Target) – którym musi być DependencyProperty
kontrolki UI – a Źródłem wiązania (Binding
Source), którym może być dowolny obiekt C# (najczęściej
ViewModel przypisany do właściwości
DataContext) lub inna kontrolka XAML
(ElementName).
[ Cel (Binding Target) ] [ Źródło (Binding Source) ]
┌──────────────────────────┐ Obiekt Binding ┌─────────────────────────────┐
│ Kontrolka UI (np. TextBox│ ◄────────── OneWay ───────── │ Obiekt C# w DataContext │
│ DependencyProperty: Text)│ ◄────────── TwoWay ────────► │ Właściwość + │
│ │ ────── OneWayToSource ─────► │ INotifyPropertyChanged / │
└──────────────────────────┘ OneTime │ ObservableCollection<T> │
└─────────────────────────────┘
1. Cztery
tryby kierunku przepływu danych (BindingMode)
OneWay: Zmiana właściwości w obiekcie C# automatycznie odświeża kontrolkę UI (odpowiedni dla kontrolek tylko do odczytu, jakTextBlockczyProgressBar).TwoWay: Komunikacja dwukierunkowa – zmiana w C# odświeża widok, a edycja wartości przez użytkownika w UI aktualizuje właściwość w C# (domyślny tryb dlaTextBox.Text,CheckBox.IsChecked,Slider.Value).OneWayToSource: OdwrotnośćOneWay– wyłącznie zmiany z kontrolki UI są zapisywane do właściwości w obiekcie źródłowym.OneTime: Wartość jest odczytywana tylko raz podczas inicjalizacji widoku (najwydajniejszy tryb dla danych statycznych, które nie ulegają zmianie).
🧠 Kiedy
TextBoxaktualizuje właściwość w C#? (UpdateSourceTrigger)
Domyślnie kontrolkaTextBoxw trybieTwoWayma ustawiony wyzwalaczUpdateSourceTrigger="LostFocus"– zmiana trafia do obiektu C# dopiero wtedy, gdy użytkownik kliknie poza pole tekstowe lub naciśnieTab. Aby właściwość aktualizowała się na żywo po każdym wciśniętym klawiszu, ustawiamy:
<TextBox Text="{Binding InputNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />.
2.
Powiadamianie widoku o zmianach: Interfejs
INotifyPropertyChanged
Aby zmiana wartości właściwości w kodzie C# w trybach
OneWay oraz TwoWay natychmiast odświeżyła
kontrolkę na ekranie, klasa źródłowa (np. ViewModel lub
okno) musi implementować interfejs
INotifyPropertyChanged:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MyApp.ViewModels;
public class Person : INotifyPropertyChanged
{
private string _name = string.Empty;
private int _age;
public string Name
{
get => _name;
set { if (_name != value) { _name = value; OnPropertyChanged(); } }
}
public int Age
{
get => _age;
set { if (_age != value) { _age = value; OnPropertyChanged(); } }
}
public event PropertyChangedEventHandler? PropertyChanged;
// Atrybut [CallerMemberName] automatycznie podstawia nazwę wywołującej właściwości!
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
3.
Wiązanie między kontrolkami (ElementName oraz
RelativeSource)
Źródłem wiązania może być również inny element XAML (wskazany przez
ElementName) lub sam bieżący obiekt
(RelativeSource Self):
<Window DataContext="{Binding RelativeSource={RelativeSource Self}}" x:Class="MyApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Margin="20">
<TextBox Text="{Binding InputNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="InputNumberTextBox"/>
<Slider Maximum="100" Minimum="0" Value="{Binding Text, ElementName=InputNumberTextBox, Mode=TwoWay}"/>
</StackPanel>
</Window>
4.
Wiązanie kolekcji (ObservableCollection<T>),
DataTemplate oraz ICollectionView
Zwykła lista List<T> nie informuje interfejsu
użytkownika o dodaniu lub usunięciu elementu. W WPF do bindowania list
(ListBox, ComboBox, ListView,
DataGrid) zawsze wykorzystujemy kolekcję
ObservableCollection<T>, która
implementuje interfejs INotifyCollectionChanged:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
namespace MyApp.ViewModels;
public class MainViewModel
{
public ObservableCollection<Person> People { get; } = new()
{
new Person { Name = "Jan", Age = 25 },
new Person { Name = "Anna", Age = 30 },
new Person { Name = "Tomasz", Age = 40 }
};
public void ApplySortAndFilter()
{
// Widok kolekcji pozwala sortować i filtrować dane w UI
// bez modyfikowania oryginalnej kolekcji ObservableCollection!
ICollectionView view = CollectionViewSource.GetDefaultView(People);
view.SortDescriptions.Clear();
view.SortDescriptions.Add(new SortDescription(nameof(Person.Age), ListSortDirection.Ascending));
view.Filter = obj => obj is Person p && p.Age >= 25;
}
}
Wizualizację pojedynczego obiektu na liście definiujemy za pomocą
szablonu danych DataTemplate:
<ListBox ItemsSource="{Binding People}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="2" Orientation="Horizontal">
<TextBlock FontWeight="Bold" Text="{Binding Name}"/>
<TextBlock Foreground="Gray" Text=" – wiek: "/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
9.
Konwertery Wartości (IValueConverter oraz
IMultiValueConverter)
Cel: Transformacja danych w locie pomiędzy typem źródłowym w modelu a typem oczekiwanym przez właściwość kontrolki UI.
Silnik XAML posiada wbudowane konwertery typów
(TypeConverter), które potrafią automatycznie zamienić np.
tekst "Yellow" na pędzel Brushes.Yellow. Gdy
jednak potrzebujemy niestandardowej logiki przeliczania wartości podczas
wiązania (np. skali logarytmicznej dla suwaka, zamiany wartości
bool na widoczność
Visibility.Visible/Collapsed, czy kolorowania
wiersza w zależności od progu liczbowego), tworzymy klasę implementującą
interfejs IValueConverter:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace MyApp.Converters;
public class LogScaleConverter : IValueConverter
{
// Konwersja od Źródła (TextBox.Text) do Celu (Slider.Value):
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is null) return DependencyProperty.UnsetValue;
if (double.TryParse(value.ToString(), NumberStyles.Any, culture, out double numericValue)
&& numericValue > 0)
{
return Math.Log(numericValue);
}
// Binding.DoNothing instruuje WPF, aby nie aktualizował kontrolki docelowej przy błędnym wejściu
return Binding.DoNothing;
}
// Konwersja powrotna od Celu (Slider.Value) do Źródła (TextBox.Text) w trybie TwoWay:
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is double sliderValue)
{
return ((int)Math.Round(Math.Exp(sliderValue))).ToString(culture);
}
return Binding.DoNothing;
}
}
Rejestracja konwertera w zasobach okna i użycie w wiązaniu:
<Window.Resources>
<local:LogScaleConverter x:Key="LogScaleConverterInstance" />
</Window.Resources>
<Slider Maximum="10" Minimum="0" Value="{Binding Text, ElementName=InputNumberTextBox, Mode=TwoWay,
Converter={StaticResource LogScaleConverterInstance}}"/>
(Gdy wartość docelowa musi zostać wyliczona jednocześnie z kilku
różnych właściwości źródłowych, stosujemy znacznik
<MultiBinding> wraz z interfejsem
IMultiValueConverter).
10. Wielowątkowość i
Model STA: Dispatcher
Cel: Bezpieczna aktualizacja interfejsu użytkownika z wątków roboczych tła przy zachowaniu jednowątkowego modelu apartamentowego (Single-Threaded Apartment).
Wszystkie kontrolki WPF dziedziczą na samym dole swojej hierarchii po
klasie DispatcherObject
(Object → DispatcherObject →
DependencyObject → Visual →
UIElement → FrameworkElement →
Control). Narzuca to architekturę Thread Affinity
(Powiązania Wątkowego) w modelu STA:
- Tylko główny wątek interfejsu użytkownika (Wątek UI), który utworzył daną kontrolkę, ma prawo odczytywać i modyfikować jej właściwości.
- Próba bezpośredniego wpisania
progressBar.Value = 50;wewnątrz wątku tła (Task.Run(...)lub timera wątkowego) zakończy się natychmiastowym wyjątkiemSystem.InvalidOperationException(„Wątek wywołujący nie może uzyskać dostępu do tego obiektu, ponieważ należy on do innego wątku”).
Kolejkowanie
zadań do wątku UI przez Dispatcher
Aby zaktualizować interfejs z wątku tła, zlecamy wykonanie delegata
do kolejki komunikatów obiektu Dispatcher:
Dispatcher.Invoke(Action): Wykonuje delegat synchronicznie – wątek tła zostaje zablokowany i czeka, aż wątek UI wykona zlecenie.Dispatcher.BeginInvoke(Action)/Dispatcher.InvokeAsync(Action): Wykonuje delegat asynchronicznie – wątek tła natychmiast wraca do swojej pracy po wstawieniu zadania do kolejki UI.Dispatcher.CheckAccess(): Zwracatrue, jeśli bieżący kod wykonuje się już na właściwym wątku UI, lubfalse, jeśli jesteśmy w wątku roboczym i musimy skorzystać zInvoke.
Uniwersalna
klasa rozszerzająca DispatcherExtensions
using System;
using System.Windows.Threading;
namespace MyApp.Helpers;
public static class DispatcherExtensions
{
// Bezpieczne wywołanie akcji bezparametrowej
public static void InvokeIfRequired(this DispatcherObject obj, Action action)
{
if (!obj.Dispatcher.CheckAccess())
{
obj.Dispatcher.Invoke(action);
}
else
{
action();
}
}
// Bezpieczne wywołanie akcji z parametrem T
public static void InvokeIfRequired<T>(this DispatcherObject obj, Action<T> action, T parameter)
{
if (!obj.Dispatcher.CheckAccess())
{
obj.Dispatcher.Invoke(action, parameter);
}
else
{
action(parameter);
}
}
}
Użycie wewnątrz metody okna uruchamianej w tle:
Task.Run(() =>
{
// Ciężkie obliczenia w wątku puli wątków...
this.InvokeIfRequired(val => progressBar.Value = val, 50.0);
});
11.
Tworzenie Własnych Kontrolek: UserControl vs
Custom Control
Cel: Wybór właściwej strategii tworzenia reużywalnych komponentów wizualnych w WPF.
WPF oferuje trzy poziomy tworzenia własnych kontrolek, różniące się elastycznością i nakładem pracy:
| Klasa bazowa | Charakterystyka | Kiedy stosować? |
|---|---|---|
UserControl |
Kompozycja istniejących kontrolek WPF
(plik .xaml + .xaml.cs). Dziedziczy po
ContentControl. |
Gdy chcemy wydzielić powtarzalny fragment
widoku (np. kartę pacjenta, pasek wyszukiwania) do wielokrotnego użycia
lub osobnej biblioteki DLL. Nie wspiera wymiany wyglądu
przez ControlTemplate. |
Control (Custom
Control) |
Kontrolka bezpostaciowa (Lookless
– sam plik .cs + domyślny szablon w
Themes/Generic.xaml). |
Gdy budujemy uniwersalną kontrolkę
biblioteczną, której strukturę wizualną inni programiści będą mogli
całkowicie podmienić własnym ControlTemplate i
motywami. |
FrameworkElement |
Niskopoziomowe renderowanie bezpośrednie
poprzez nadpisanie metody OnRender(DrawingContext). |
Gdy potrzebujemy maksymalnej wydajności rysowania tysięcy prymitywów geometrycznych (wykresy czasu rzeczywistego, oscyloskopy). |
12.
Konsola Diagnostyczna w Procesie WPF, Mechanizm P/Invoke i Wzorzec
IDisposable
Cel: Dynamiczne dołączanie systemowego okna konsoli
Win32 do aplikacji okienkowej WPF za pomocą wywołań natywnych P/Invoke
(kernel32.dll, user32.dll) oraz bezpieczne
zwalnianie zasobów niezarządzanych.
Projekt WPF kompilowany jest jako aplikacja podsystemu okienkowego
(<OutputType>WinExe</OutputType>), przez co
domyślnie nie posiada przypisanego okna konsoli, a wywołania
Console.WriteLine trafiają w próżnię. Aby w celach
diagnostycznych otworzyć i kontrolować prawdziwe okno konsoli z poziomu
działającej aplikacji WPF, wykorzystujemy mechanizm P/Invoke
(Platform Invocation Services) pozwalający wywoływać
funkcje C/C++ bezpośrednio z bibliotek systemowych Windows.
Wykorzystywane funkcje Win32 API:
- Biblioteka
kernel32.dll(Zarządzanie konsolą procesu):AllocConsole()– tworzy i przydziela nowe okno konsoli dla bieżącego procesu.FreeConsole()– odłącza proces od konsoli i zamyka jej okno.GetConsoleWindow()– zwraca niezarządzany uchwyt okna (IntPtr/HWND) przypisanej konsoli.SetConsoleTitle(string)– ustawia tekst na pasku tytułu konsoli.
- Biblioteka
user32.dll(Manipulacja oknem i menu systemowym):ShowWindow(IntPtr hWnd, int nCmdShow)– pozwala ukrywać (SW_HIDE = 0) i pokazywać (SW_SHOW = 5) okno konsoli.GetSystemMenu(IntPtr hWnd, bool bRevert)orazDeleteMenu(...)– Kluczowe zabezpieczenie: domyślnie kliknięcie przez użytkownika przyciskuX(Zamknij) na oknie konsoli wysyła sygnałCTRL_CLOSE_EVENT, który natychmiast zabija cały proces aplikacji WPF (bez wywołania zdarzeńClosing!). Usunięcie pozycjiSC_CLOSE(0xF060) z menu systemowego konsoli dezaktywuje przyciskXkonsoli, chroniąc aplikację przed przypadkowym zamknięciem.
Kompletna
implementacja klasy ConsoleManager z wzorcem
IDisposable
Ponieważ konsola systemowa jest zasobem niezarządzanym (spoza pamięci
nadzorowanej przez Garbage Collector), klasa zarządzająca nią musi
implementować pełny wzorzec IDisposable
wraz z metodą GC.SuppressFinalize(this):
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MyApp.Diagnostics;
public class ConsoleManager : IDisposable
{
// Stałe Win32 API (User32.dll)
private const int SW_HIDE = 0;
private const int SW_SHOW = 5;
private const int MF_BYCOMMAND = 0x00000000;
private const int SC_CLOSE = 0xF060;
// Importy z biblioteki Kernel32.dll
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
[DllImport("kernel32.dll")]
private static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool SetConsoleTitle(string lpConsoleTitle);
// Importy z biblioteki User32.dll
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
private static extern bool DeleteMenu(IntPtr hMenu, int nPosition, int wFlags);
private bool _isDisposed;
public ConsoleManager(string title = "Debug Console")
{
// 1. Przydzielenie nowej konsoli dla procesu WPF
if (AllocConsole())
{
SetConsoleTitle(title);
// Odświeżenie standardowych strumieni wejścia/wyjścia klasy System.Console
var stdOut = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
Console.SetOut(stdOut);
Console.SetError(stdOut);
Console.SetIn(new StreamReader(Console.OpenStandardInput()));
IntPtr handle = GetConsoleWindow();
if (handle != IntPtr.Zero)
{
// 2. Dezaktywacja przycisku "X" konsoli, aby nie zabić procesu WPF
IntPtr sysMenu = GetSystemMenu(handle, false);
if (sysMenu != IntPtr.Zero)
{
DeleteMenu(sysMenu, SC_CLOSE, MF_BYCOMMAND);
}
// 3. Wyświetlenie okna konsoli
ShowWindow(handle, SW_SHOW);
}
}
}
public void Dispose()
{
Dispose(disposing: true);
// Informujemy Garbage Collector, że zasób został już zwolniony deterministycznie
// i nie trzeba wywoływać finalizatora (~ConsoleManager)
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
FreeConsole();
_isDisposed = true;
}
}
~ConsoleManager()
{
Dispose(disposing: false);
}
}
Integracja
z cyklem życia głównego okna (MainWindow.xaml.cs)
Zwolnienie konsoli umieszczamy w obsłudze zdarzenia
Closing (lub Closed) głównego okna
aplikacji:
using System;
using System.ComponentModel;
using System.Windows;
using MyApp.Diagnostics;
namespace MyApp;
public partial class MainWindow : Window
{
private readonly ConsoleManager _console;
public MainWindow()
{
InitializeComponent();
_console = new ConsoleManager("Konsola Diagnostyczna WPF");
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Konsola została pomyślnie zainicjalizowana.");
}
// Zdarzenie podpięte w XAML: <Window ... Closing="Window_Closing">
private void Window_Closing(object? sender, CancelEventArgs e)
{
_console.Dispose();
}
}