Wiele okien – dodawanie i wymiana danych
Nauczysz się dodawać do projektu kolejne okna, otwierać je na dwa różne sposoby oraz przekazywać dane w obie strony – z okna głównego do nowego i z powrotem. Wszystko bez MVVM, prostym stylem code-behind.
Po co wiele okien?
Do tej pory Twoje aplikacje miały jedno okno – MainWindow.
W prawdziwych programach często potrzeba więcej okien:
okno logowania, okno „O programie”, formularz dodawania nowego
rekordu, okno ustawień, okno ze szczegółami wybranego elementu.
Pomyśl o dowolnym programie, którego używasz na co dzień –
np. edytorze zdjęć. Główne okno pokazuje zdjęcie, ale kliknięcie
„Ustawienia” otwiera osobne okienko z opcjami.
Kliknięcie „Eksportuj” może otworzyć kolejne okno z wyborem formatu.
Każde z tych okien to osobny plik .xaml w Twoim projekcie –
dokładnie tak samo jak MainWindow.xaml.
Typowe zastosowania dodatkowych okien:
| Okno | Przykład |
|---|---|
| Formularz dodawania danych | „Dodaj nowego ucznia”, „Nowe zamówienie” |
| Okno szczegółów | Kliknięcie na wiersz w tabeli pokazuje pełne dane |
| Okno ustawień | Konfiguracja motywu, języka, opcji |
| Okno „O programie” | Informacje o autorze, wersji |
| Okno logowania | Ekran przed wejściem do głównej aplikacji |
Dodawanie nowego okna do projektu
W Visual Studio kliknij prawym przyciskiem na nazwę projektu
w Eksploratorze rozwiązań → Dodaj → Nowy element…
→ wybierz szablon „Okno (WPF)” → nadaj nazwę,
np. OknoSzczegoly.xaml → Dodaj.
Visual Studio utworzy dla Ciebie parę plików –
dokładnie tak samo jak przy MainWindow:
| Plik | Do czego służy |
|---|---|
OknoSzczegoly.xaml | Wygląd nowego okna (kontrolki, layout) |
OknoSzczegoly.xaml.cs | Kod C# tego okna (logika, obsługa zdarzeń) |
Tak jak MainWindow jest klasą dziedziczącą po
Window, tak samo OknoSzczegoly
też jest klasą dziedziczącą po Window. Możesz mieć
w projekcie dowolnie wiele takich klas-okien.
Każde jest niezależne – ma własne kontrolki, własny kod,
własne zdarzenia.
<Window x:Class="MojaAplikacja.OknoSzczegoly" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="OknoSzczegoly" Height="300" Width="400"> <Grid> </Grid> </Window>
Otwieranie okna – Show()
Żeby otworzyć nowe okno z poziomu MainWindow,
tworzysz jego obiekt (tak jak każdy inny obiekt w C#) i wywołujesz
metodę Show().
<Button Content="Pokaż szczegóły" Click="btnPokazSzczegoly_Click" />
private void btnPokazSzczegoly_Click(object sender, RoutedEventArgs e) { // 1. Tworzymy obiekt nowego okna (tak jak każdy inny obiekt) OknoSzczegoly okno = new OknoSzczegoly(); // 2. Pokazujemy je na ekranie okno.Show(); }
Show() otwiera okno niemodalnie –
oba okna (główne i nowe) są aktywne jednocześnie.
Użytkownik może dowolnie przełączać się między nimi,
klikać w oba, przesuwać oba. To jak dwa oddzielne programy
działające naraz.
Okno modalne – ShowDialog()
Czasem chcesz, żeby użytkownik musiał obsłużyć nowe okno,
zanim wróci do reszty aplikacji – np. formularz logowania,
okno z pytaniem „czy na pewno usunąć?”. Do tego służy
ShowDialog().
private void btnDodajUcznia_Click(object sender, RoutedEventArgs e) { OknoDodawania okno = new OknoDodawania(); okno.ShowDialog(); // Ten kod wykona się DOPIERO po zamknięciu okna "okno"! MessageBox.Show("Okno dodawania zostało zamknięte"); }
Show() vs ShowDialog() – porównanie
| Cecha | Show() | ShowDialog() |
|---|---|---|
| Okno główne dostępne w tym czasie? | ✅ Tak | ❌ Nie – zablokowane |
| Kod po Show()/ShowDialog() czeka? | ❌ Nie – wykonuje się od razu | ✅ Tak – czeka na zamknięcie okna |
| Może zwrócić wynik (true/false)? | ❌ Nie | ✅ Tak (DialogResult) |
| Typowe zastosowanie | Panel narzędzi, okno pomocnicze obok głównego | Formularz, okno z pytaniem, logowanie |
To najważniejsza różnica: linia kodu zaraz po
okno.ShowDialog(); nie wykona się,
dopóki użytkownik nie zamknie tego okna. Dzięki temu możesz
napisać kod, który „na pewno” wykonuje się już po
zakończeniu pracy z oknem podrzędnym – bardzo przydatne przy
odczytywaniu danych, które użytkownik tam wprowadził
(zobacz sekcję 6).
Przekazywanie danych DO nowego okna
Często nowe okno potrzebuje jakichś danych z okna głównego – np. okno szczegółów ucznia potrzebuje wiedzieć, którego ucznia pokazać. Są dwa sposoby.
Sposób 1 – przez konstruktor
Najczystszy sposób – dodajesz do klasy okna własny konstruktor, który przyjmuje dane jako parametry.
public partial class OknoSzczegoly : Window { // Domyślny konstruktor – WYMAGANY, zostaje bez zmian public OknoSzczegoly() { InitializeComponent(); } // Własny, dodatkowy konstruktor – przyjmuje dane public OknoSzczegoly(string imie, int wiek) { InitializeComponent(); // Od razu wypełniamy kontrolki przekazanymi danymi lblImie.Text = $"Imię: {imie}"; lblWiek.Text = $"Wiek: {wiek} lat"; } }
private void btnPokazSzczegoly_Click(object sender, RoutedEventArgs e) { // Wywołujemy NASZ konstruktor, podając dane OknoSzczegoly okno = new OknoSzczegoly("Anna Kowalska", 17); okno.Show(); }
Klasa może mieć wiele konstruktorów o różnych parametrach (to się nazywa przeciążanie). Domyślny (bez parametrów) zostaw bez zmian – jest generowany automatycznie i czasem przydatny. Dodaj obok niego własny, z parametrami, którego użyjesz do przekazania danych.
Sposób 2 – przez właściwość publiczną
Alternatywa: zamiast konstruktora, dodajesz publiczną właściwość i ustawiasz ją po utworzeniu okna, ale przed jego pokazaniem.
public partial class OknoSzczegoly : Window { // Publiczna właściwość – dostępna z zewnątrz klasy public string ImieUcznia { get; set; } public int WiekUcznia { get; set; } public OknoSzczegoly() { InitializeComponent(); } // Zdarzenie Loaded – wykonuje się TUŻ PRZED wyświetleniem okna, gdy właściwości są już ustawione private void Window_Loaded(object sender, RoutedEventArgs e) { lblImie.Text = $"Imię: {ImieUcznia}"; lblWiek.Text = $"Wiek: {WiekUcznia} lat"; } }
private void btnPokazSzczegoly_Click(object sender, RoutedEventArgs e) { OknoSzczegoly okno = new OknoSzczegoly(); // Ustawiamy właściwości ZANIM pokażemy okno okno.ImieUcznia = "Anna Kowalska"; okno.WiekUcznia = 17; okno.Show(); }
Konstruktor – gdy dane są zawsze wymagane
do sensownego działania okna (bez nich okno „nie ma sensu”).
Wymusza podanie danych już przy tworzeniu obiektu – trudniej o pomyłkę.
Właściwość publiczna – gdy dane są opcjonalne
albo chcesz mieć elastyczność (np. czasem otwierasz okno puste,
czasem z danymi). Pamiętaj tylko, żeby ustawić ją
przed wywołaniem Show().
Przekazywanie danych Z POWROTEM do okna głównego
To odwrotna sytuacja: użytkownik wypełnia formularz w nowym oknie
(np. „Dodaj ucznia”), klika „Zapisz”, i okno główne musi
odebrać te dane. Tutaj właśnie przydaje się
ShowDialog() z sekcji 4.
Sposób 1 – właściwość publiczna (najprostszy)
<Window x:Class="MojaAplikacja.OknoDodawania" ... Title="Dodaj ucznia" Width="300" Height="200"> <StackPanel Margin="15"> <TextBlock Text="Imię i nazwisko:" /> <TextBox x:Name="txtImie" Margin="0,4,0,12" /> <Button Content="Zapisz" Click="btnZapisz_Click" /> </StackPanel> </Window>
public partial class OknoDodawania : Window { // Tu "wystawiamy" wynik na zewnątrz public string WpisaneImie { get; private set; } public OknoDodawania() { InitializeComponent(); } private void btnZapisz_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrWhiteSpace(txtImie.Text)) { MessageBox.Show("Podaj imię!"); return; } WpisaneImie = txtImie.Text; // zapisujemy wynik do właściwości Close(); // zamykamy to okno } }
private void btnDodajUcznia_Click(object sender, RoutedEventArgs e) { OknoDodawania okno = new OknoDodawania(); // ShowDialog() CZEKA aż okno się zamknie – dopiero potem lecimy dalej okno.ShowDialog(); // Tutaj okno jest już zamknięte, możemy bezpiecznie odczytać dane if (!string.IsNullOrEmpty(okno.WpisaneImie)) { lstUczniowie.Items.Add(okno.WpisaneImie); } }
Sposób 2 – DialogResult (informacja OK/Anuluj)
DialogResult to specjalna właściwość wbudowana
w każde okno – mówi „czy użytkownik zatwierdził, czy anulował”.
Świetnie współpracuje z właściwościami publicznymi z poprzedniego przykładu.
public partial class OknoDodawania : Window { public string WpisaneImie { get; private set; } public OknoDodawania() { InitializeComponent(); } private void btnZapisz_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrWhiteSpace(txtImie.Text)) { MessageBox.Show("Podaj imię!"); return; } WpisaneImie = txtImie.Text; DialogResult = true; // ← oznacza "zatwierdzono" i AUTOMATYCZNIE zamyka okno! } private void btnAnuluj_Click(object sender, RoutedEventArgs e) { DialogResult = false; // ← oznacza "anulowano" i zamyka okno } }
private void btnDodajUcznia_Click(object sender, RoutedEventArgs e) { OknoDodawania okno = new OknoDodawania(); // ShowDialog() zwraca bool? – to co ustawiliśmy w DialogResult bool? wynik = okno.ShowDialog(); if (wynik == true) // użytkownik kliknął "Zapisz", nie "Anuluj" { lstUczniowie.Items.Add(okno.WpisaneImie); } // jeśli wynik == false lub null – użytkownik anulował, nic nie robimy }
Ustawienie DialogResult = true; (lub false)
automatycznie wywołuje Close() –
nie musisz pisać tego osobno. To wygodny skrót, który jednocześnie
zamyka okno i „zapamiętuje” czy była to akcja pozytywna, czy negatywna.
Działa tylko gdy okno zostało otwarte przez
ShowDialog() – przy zwykłym Show()
nie ma to sensu (nikt nie czeka na wynik).
| Metoda | Kiedy używać |
|---|---|
| Sama właściwość publiczna | Proste okna, gdzie zawsze jest jeden „dobry” wynik |
| Właściwość + DialogResult | Formularze z opcją Anuluj – wzorcowe rozwiązanie w WPF |
Zamykanie okna z kodu
Każde okno (również MainWindow) ma metodę
Close(), którą wywołujesz, żeby je zamknąć programowo –
np. po kliknięciu przycisku „Anuluj” albo „Zamknij”.
private void btnZamknij_Click(object sender, RoutedEventArgs e) { Close(); // zamyka OKNO, w którym jest ta metoda }
Domyślnie WPF kończy działanie programu, gdy zamknięte zostanie
główne okno (to, które ustawione jest w
App.xaml jako StartupUri).
Zamknięcie okna pomocniczego (np. OknoDodawania)
nie kończy całej aplikacji – reszta programu
działa dalej normalnie.
// Jeśli mamy referencję do otwartego okna, możemy je zamknąć z innego miejsca OknoSzczegoly okno = new OknoSzczegoly(); okno.Show(); // ... później, gdzie indziej w kodzie: okno.Close();
Zdarzenia Closing i Closed
Czasem chcesz coś zrobić tuż przed zamknięciem okna (np. zapytać „czy na pewno?”) albo tuż po jego zamknięciu. Do tego służą dwa zdarzenia.
| Zdarzenie | Kiedy się odpala | Można anulować zamknięcie? |
|---|---|---|
Closing | Tuż PRZED zamknięciem okna | ✅ Tak |
Closed | Tuż PO zamknięciu okna | ❌ Nie – za późno |
<!-- XAML: <Window ... Closing="Window_Closing"> --> private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { MessageBoxResult wynik = MessageBox.Show( "Czy na pewno chcesz zamknąć okno? Niezapisane zmiany zostaną utracone.", "Potwierdzenie", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (wynik == MessageBoxResult.No) { e.Cancel = true; // anulujemy zamknięcie okna! } }
W zdarzeniu Closing masz dostęp do e.Cancel.
Ustawienie go na true zatrzymuje
proces zamykania okna – okno zostaje otwarte, jakby użytkownik
nic nie kliknął. To ta sama mechanika co e.Handled
przy zdarzeniach myszy w Canvas.
private void Window_Closed(object sender, EventArgs e) { // Okno jest już zamknięte - tutaj można np. zwolnić zasoby, zatrzymać timer, wyrejestrować się z jakiegoś zdarzenia _timer?.Stop(); }
Dostęp do okna głównego z okna podrzędnego
Czasem okno podrzędne chce bezpośrednio „dotknąć” kontrolki
w oknie głównym – np. odświeżyć listę bez czekania na
ShowDialog(). Można to zrobić na dwa sposoby.
Sposób 1 – Application.Current.MainWindow
WPF zawsze wie, które okno jest „głównym oknem” aplikacji
(to ustawione w App.xaml). Możesz się do niego
odwołać z dowolnego miejsca w programie.
private void btnOdswiez_Click(object sender, RoutedEventArgs e) { // Rzutujemy Application.Current.MainWindow na nasz konkretny typ MainWindow glowne = (MainWindow)Application.Current.MainWindow; // Teraz mamy dostęp do jego publicznych metod i kontrolek glowne.OdswiezListe(); }
Domyślnie kontrolki z x:Name są dostępne tylko
wewnątrz danej klasy okna. Żeby okno podrzędne
mogło np. odświeżyć listę w oknie głównym, potrzebujesz w
MainWindow publicznej metody
(jak OdswiezListe() powyżej), która robi to za nie –
zamiast bezpośrednio grzebać w prywatnych kontrolkach z zewnątrz.
Sposób 2 – przekazanie referencji przez konstruktor
Alternatywnie, przy tworzeniu okna podrzędnego możesz od razu
przekazać mu referencję this (czyli „samego siebie”) –
okno podrzędne zapamiętuje, kto je otworzył.
private void btnOtworz_Click(object sender, RoutedEventArgs e) { // "this" to referencja do aktualnego okna (MainWindow) OknoPodrzedne okno = new OknoPodrzedne(this); okno.Show(); }
public partial class OknoPodrzedne : Window { private readonly MainWindow _oknoGlowne; public OknoPodrzedne(MainWindow oknoGlowne) { InitializeComponent(); _oknoGlowne = oknoGlowne; } private void btnOdswiez_Click(object sender, RoutedEventArgs e) { _oknoGlowne.OdswiezListe(); } }
Kompletny przykład – lista uczniów z dodawaniem
Połączmy wszystko: okno główne z listą, przycisk otwierający
formularz modalny, przekazanie danych z powrotem przez
DialogResult.
<Window x:Class="MojaAplikacja.MainWindow" ... Title="Lista uczniów" Width="350" Height="400"> <DockPanel Margin="10"> <Button DockPanel.Dock="Top" Content="➕ Dodaj ucznia" Margin="0,0,0,10" Click="btnDodaj_Click" /> <ListBox x:Name="lstUczniowie" /> </DockPanel> </Window>
private void btnDodaj_Click(object sender, RoutedEventArgs e) { OknoDodawania okno = new OknoDodawania(); bool? wynik = okno.ShowDialog(); if (wynik == true) { lstUczniowie.Items.Add(okno.WpisaneImie); } }
<Window x:Class="MojaAplikacja.OknoDodawania" ... Title="Dodaj ucznia" Width="300" Height="180" WindowStartupLocation="CenterOwner" ResizeMode="NoResize"> <StackPanel Margin="15"> <TextBlock Text="Imię i nazwisko ucznia:" /> <TextBox x:Name="txtImie" Margin="0,4,0,16" /> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> <Button Content="Anuluj" Width="80" Margin="0,0,8,0" IsCancel="True" Click="btnAnuluj_Click" /> <Button Content="Zapisz" Width="80" IsDefault="True" Click="btnZapisz_Click" /> </StackPanel> </StackPanel> </Window>
public partial class OknoDodawania : Window { public string WpisaneImie { get; private set; } public OknoDodawania() { InitializeComponent(); } private void btnZapisz_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrWhiteSpace(txtImie.Text)) { MessageBox.Show("Podaj imię i nazwisko!"); return; } WpisaneImie = txtImie.Text; DialogResult = true; } private void btnAnuluj_Click(object sender, RoutedEventArgs e) { DialogResult = false; } }
Ta właściwość otwiera nowe okno wyśrodkowane względem okna, które je otworzyło (a nie względem całego ekranu). Wygląda to bardziej naturalnie niż okno pojawiające się w przypadkowym miejscu. Działa tylko jeśli okno zostało otwarte z poziomu innego okna (nie przy starcie aplikacji).
Częste błędy
❌ Błąd 1: Odczyt danych przed zamknięciem okna (Show zamiast ShowDialog)
❌ Dane jeszcze nie istnieją!
OknoDodawania okno = new OknoDodawania(); okno.Show(); // NIE czeka! // To wykona się NATYCHMIAST, // zanim użytkownik cokolwiek wpisze! lstUczniowie.Items.Add(okno.WpisaneImie);
✅ ShowDialog czeka na zamknięcie
OknoDodawania okno = new OknoDodawania();
bool? wynik = okno.ShowDialog(); // czeka!
if (wynik == true)
lstUczniowie.Items.Add(okno.WpisaneImie);
❌ Błąd 2: Ustawienie właściwości po Show()/ShowDialog()
❌ Za późno – okno już się otworzyło puste
OknoSzczegoly okno = new OknoSzczegoly(); okno.Show(); okno.ImieUcznia = "Anna"; // za późno! // Window_Loaded już się wykonał
✅ Ustaw PRZED pokazaniem okna
OknoSzczegoly okno = new OknoSzczegoly(); okno.ImieUcznia = "Anna"; // najpierw dane okno.Show(); // potem pokazujemy
❌ Błąd 3: Zapomniany domyślny konstruktor
❌ Błąd kompilacji XAML
public partial class OknoSzczegoly : Window
{
// Tylko własny konstruktor –
// brak konstruktora bez parametrów!
public OknoSzczegoly(string imie)
{
InitializeComponent();
}
}
// Visual Studio / XAML designer
// oczekuje domyślnego konstruktora!
✅ Zostaw oba konstruktory
public partial class OknoSzczegoly : Window
{
public OknoSzczegoly() // domyślny – zostaw!
{
InitializeComponent();
}
public OknoSzczegoly(string imie) : this()
{
lblImie.Text = imie;
}
}
❌ Błąd 4: DialogResult ustawione bez ShowDialog()
❌ Wyjątek w czasie działania
// W MainWindow: OknoDodawania okno = new OknoDodawania(); okno.Show(); // Show(), nie ShowDialog()! // W OknoDodawania, przy kliknięciu Zapisz: DialogResult = true; // Wyjątek! DialogResult działa TYLKO // z ShowDialog()
✅ DialogResult tylko z ShowDialog
// Jeśli chcesz używać DialogResult, // zawsze otwieraj okno przez: okno.ShowDialog();
Zadania do wykonania
Dodaj do projektu drugie okno o nazwie OknoInfo
zawierające TextBlock z tekstem „Aplikacja stworzona przez [Twoje imię]”
i przycisk „Zamknij”, który zamyka to okno. W MainWindow dodaj
przycisk „O programie” otwierający OknoInfo przez Show().
Sprawdź, że możesz mieć oba okna otwarte jednocześnie.
Zmień okno z zadania 1 tak, żeby otwierało się przez
ShowDialog(). Dodaj do MainWindow zdarzenie
Closing, które pyta „Czy na pewno chcesz zamknąć
aplikację?” i pozwala anulować zamknięcie (e.Cancel = true).
Stwórz okno OknoKalkulatora przyjmujące przez konstruktor
dwie liczby (int a, int b). Okno wyświetla wynik dodawania,
odejmowania, mnożenia i dzielenia tych liczb w czterech TextBlockach.
W MainWindow dodaj dwa pola TextBox na liczby i przycisk „Oblicz”,
który otwiera OknoKalkulatora z wpisanymi wartościami.
Zbuduj kompletną listę produktów (jak w przykładzie z sekcji 10),
ale rozbuduj formularz dodawania o dwa pola: nazwę produktu (TextBox)
i cenę (TextBox z walidacją – tylko liczby, PreviewTextInput).
Użyj DialogResult i właściwości publicznych
NazwaProduktu oraz CenaProduktu.
W ListBox wyświetlaj produkty w formacie „Nazwa – 12.50 zł”.
Dodaj też przycisk „Usuń zaznaczony”, który usuwa wybrany
element z listy w MainWindow (bez otwierania żadnego okna).