Klasa w osobnym pliku i okno
W kilku ostatnich lekcjach dane trzymałeś w tablicach równoległych: nazwy w jednej, ceny w drugiej, pliki w trzeciej. Działa, ale łatwo o pomyłkę — wystarczy dodać element do jednej tablicy i zapomnieć o drugiej. Z kursu C# znasz lepsze rozwiązanie: klasę. W tej lekcji opiszesz dane klasą w osobnym pliku, będziesz tworzyć obiekty z formularza, trzymać je w List<T> i pokazywać w oknie.
Czego się dziś nauczysz
- Dodasz do projektu WPF nową klasę w osobnym pliku.
- Utworzysz obiekt z danych wpisanych w formularz.
- Przechowasz obiekty w
List<T>i przejdziesz po nich pętlą. - Pokażesz obiekty w
ListBoxdziękiToString()i odczytasz zaznaczony obiekt przezas. - Rozdzielisz dane (model) od tego, co widać w oknie (widok), i odświeżysz widok po zmianie danych.
Tablice równoległe a klasa
string[] tytuly = { "Lalka", "Solaris" };
string[] autorzy = { "Bolesław Prus", "Stanisław Lem" };
int[] lata = { 1890, 1961 };
// tytuly[1], autorzy[1] i lata[1] opisują tę samą książkę — pilnujesz tego sam
List<Ksiazka> ksiazki = new List<Ksiazka>();
ksiazki.Add(new Ksiazka("Lalka", "Bolesław Prus", 1890));
ksiazki.Add(new Ksiazka("Solaris", "Stanisław Lem", 1961));
// ksiazki[1] to cała książka: Tytul, Autor, Rok razem
Obiekt trzyma wszystkie dane jednej rzeczy razem. Lista może rosnąć i maleć — tablica ma stały rozmiar. A gdy książka dostanie nową cechę (np. „kto wypożyczył”), dopisujesz jedną właściwość w klasie zamiast nowej tablicy.
Klasa w osobnym pliku
W Eksploratorze rozwiązań kliknij projekt prawym przyciskiem → Dodaj → Klasa…, wpisz nazwę Ksiazka.cs. Visual Studio utworzy plik z pustą klasą w przestrzeni nazw projektu — tej samej co MainWindow, więc okno „widzi” klasę bez dodatkowego using. Dopisz public przed class.
├── App.xaml
├── Ksiazka.csmodel: dane jednej książki
├── MainWindow.xamlwidok: jak wygląda okno
├── MainWindow.xaml.csobsługa zdarzeń, lista książek
└── Lekcja23.csproj
namespace Lekcja23;
// Model danych: jedna książka z biblioteczki klasowej.
// Klasa nic nie wie o oknie — można jej użyć w dowolnym programie.
public class Ksiazka
{
public string Tytul { get; set; }
public string Autor { get; set; }
public int Rok { get; set; }
public string Wypozyczajacy { get; set; } = ""; // pusty napis = książka na półce
public Ksiazka(string tytul, string autor, int rok)
{
Tytul = tytul;
Autor = autor;
Rok = rok;
}
// właściwość tylko do odczytu, wyliczana z innej
public bool Dostepna => Wypozyczajacy == "";
// tak obiekt wyświetli się na liście
public override string ToString()
{
string stan = Dostepna ? "" : " [wypożyczona]";
return $"{Tytul} — {Autor} ({Rok}){stan}";
}
}
Wszystko w tej klasie znasz z kursu C#: właściwości z get; set;, konstruktor z parametrami, nadpisanie ToString(). Nowy może być tylko zapis public bool Dostepna => Wypozyczajacy == ""; — to właściwość tylko do odczytu, której wartość jest wyliczana przy każdym odczycie. Dzięki niej w kodzie piszesz k.Dostepna zamiast powtarzać warunek.
List<T>
| Zapis | Co robi |
|---|---|
new List<Ksiazka>() | pusta lista obiektów typu Ksiazka |
lista.Add(obiekt) | dodaje na koniec |
lista.Remove(obiekt) | usuwa wskazany obiekt |
lista.RemoveAt(i) | usuwa element o numerze i |
lista.Count | liczba elementów (w tablicy było Length) |
lista[i] | element o numerze i |
lista.Contains(obiekt) | czy obiekt jest na liście |
foreach (Ksiazka k in lista) | przejście po wszystkich elementach |
Lista jest polem klasy okna (private readonly List<Ksiazka> ksiazki), więc żyje tak długo jak okno. readonly oznacza, że zmienna zawsze wskazuje tę samą listę — elementy możesz dodawać i usuwać.
Obiekty w ListBox
Do Items można dodać dowolny obiekt, nie tylko napis. Lista wyświetla wtedy wynik jego metody ToString(). Bez nadpisania zobaczysz nazwę klasy: „Lekcja23.Ksiazka”.
foreach (Ksiazka k in ksiazki)
{
Lista.Items.Add(k); // na liście: k.ToString()
}
// zaznaczona pozycja to CAŁY obiekt, nie napis
Ksiazka? zaznaczona = Lista.SelectedItem as Ksiazka;
if (zaznaczona != null)
{
Info.Text = $"{zaznaczona.Tytul}, wydana w {zaznaczona.Rok}";
}
Operator as działa jak rzutowanie, ale zamiast wyjątku zwraca null, gdy obiekt nie jest danego typu albo gdy nic nie zaznaczono. Dlatego po as zawsze sprawdzasz != null.
Model i widok
Lista ksiazki to model — prawdziwe dane. ListBox to widok — to, co aktualnie widać. Mogą się różnić: filtr „tylko dostępne” chowa część książek, ale nie usuwa ich z modelu. Zasada pracy:
- każda zmiana dotyczy modelu (
ksiazki.Add, zmiana właściwości obiektu,ksiazki.Remove); - po zmianie wywołujesz jedną metodę, która buduje widok od nowa z modelu (
PokazListe).
Zmiana właściwości obiektu nie odświeża sama tekstu na liście — ListBox nie wie, że ToString() zwróciłby teraz coś innego. Przebudowanie widoku rozwiązuje problem. W bloku o danych (lekcje 41–43) poznasz mechanizm, który odświeża widok automatycznie.
Biblioteczka klasowa
Klasa prowadzi biblioteczkę. Po lewej formularz nowej książki, w środku lista wszystkich książek, po prawej szczegóły zaznaczonej z możliwością wypożyczenia, zwrotu i usunięcia. Na dole filtr „tylko dostępne” i statystyka. Dane książki opisuje klasa Ksiazka z pliku powyżej.
zaznaczona wypożyczona książka — aktywny jest tylko „Zwróć” i „Usuń”
<Window x:Class="Lekcja23.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Biblioteczka klasy 3TI" Height="440" Width="820"
MinWidth="700" MinHeight="380" WindowStartupLocation="CenterScreen">
<DockPanel Margin="12">
<!-- lewa kolumna: nowa książka -->
<Border DockPanel.Dock="Left" Width="200" Padding="10" Margin="0,0,10,0"
BorderBrush="LightGray" BorderThickness="1" CornerRadius="6">
<StackPanel>
<TextBlock Text="Nowa książka" FontWeight="Bold" Margin="0,0,0,6"/>
<Label Content="_Tytuł:" Padding="0,2" Target="{Binding ElementName=Tytul}"/>
<TextBox x:Name="Tytul" Margin="0,0,0,6"/>
<Label Content="_Autor:" Padding="0,2" Target="{Binding ElementName=Autor}"/>
<TextBox x:Name="Autor" Margin="0,0,0,6"/>
<Label Content="_Rok wydania:" Padding="0,2" Target="{Binding ElementName=Rok}"/>
<TextBox x:Name="Rok" Width="60" HorizontalAlignment="Left" MaxLength="4"/>
<Button Content="Dodaj" Margin="0,10,0,0" Height="28" Click="Dodaj_Click"/>
</StackPanel>
</Border>
<!-- prawa kolumna: szczegóły zaznaczonej -->
<Border DockPanel.Dock="Right" Width="230" Padding="10" Margin="10,0,0,0"
BorderBrush="LightGray" BorderThickness="1" CornerRadius="6">
<StackPanel x:Name="Szczegoly" IsEnabled="False">
<TextBlock x:Name="SzTytul" FontWeight="Bold" FontSize="15" TextWrapping="Wrap"/>
<TextBlock x:Name="SzAutor" Margin="0,2,0,0"/>
<TextBlock x:Name="SzStatus" Margin="0,8,0,8" TextWrapping="Wrap"/>
<Label Content="_Kto wypożycza:" Padding="0,2" Target="{Binding ElementName=Kto}"/>
<TextBox x:Name="Kto" Margin="0,0,0,6"/>
<Button x:Name="Wypozycz" Content="Wypożycz" Height="28" Margin="0,0,0,6"
Click="Wypozycz_Click"/>
<Button x:Name="Zwroc" Content="Zwróć" Height="28" Margin="0,0,0,6"
Click="Zwroc_Click"/>
<Button Content="Usuń z biblioteczki" Height="28" Click="Usun_Click"/>
</StackPanel>
</Border>
<!-- dół: filtr i statystyka -->
<DockPanel DockPanel.Dock="Bottom" Margin="0,8,0,0">
<CheckBox x:Name="TylkoDostepne" DockPanel.Dock="Left" Content="tylko dostępne"
Checked="Filtr_Zmiana" Unchecked="Filtr_Zmiana"/>
<TextBlock x:Name="Statystyka" TextAlignment="Right"/>
</DockPanel>
<!-- środek: lista obiektów -->
<ListBox x:Name="Lista" SelectionChanged="Lista_SelectionChanged"/>
</DockPanel>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Lekcja23;
public partial class MainWindow : Window
{
// wszystkie książki — niezależnie od tego, co akurat widać na liście
private readonly List<Ksiazka> ksiazki = new List<Ksiazka>();
public MainWindow()
{
InitializeComponent();
ksiazki.Add(new Ksiazka("Lalka", "Bolesław Prus", 1890));
ksiazki.Add(new Ksiazka("Solaris", "Stanisław Lem", 1961));
ksiazki.Add(new Ksiazka("Wiedźmin. Ostatnie życzenie", "Andrzej Sapkowski", 1993));
ksiazki.Add(new Ksiazka("C# 12. Programowanie", "Mark J. Price", 2023));
ksiazki[1].Wypozyczajacy = "Ola Nowak"; // Solaris jest wypożyczone
PokazListe();
}
private void Dodaj_Click(object sender, RoutedEventArgs e)
{
string tytul = Tytul.Text.Trim();
string autor = Autor.Text.Trim();
if (tytul == "" || autor == "" ||
!int.TryParse(Rok.Text, out int rok) || rok < 1450 || rok > DateTime.Today.Year)
{
MessageBox.Show("Podaj tytuł, autora i poprawny rok wydania.", "Nowa książka",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Ksiazka nowa = new Ksiazka(tytul, autor, rok); // obiekt z danych formularza
ksiazki.Add(nowa);
Tytul.Clear();
Autor.Clear();
Rok.Clear();
Tytul.Focus();
PokazListe();
Lista.SelectedItem = nowa; // zaznacz dodaną książkę
}
private void Wypozycz_Click(object sender, RoutedEventArgs e)
{
Ksiazka? k = Lista.SelectedItem as Ksiazka;
if (k == null || Kto.Text.Trim() == "")
{
Kto.Focus();
return;
}
k.Wypozyczajacy = Kto.Text.Trim();
Kto.Clear();
PokazListe();
}
private void Zwroc_Click(object sender, RoutedEventArgs e)
{
Ksiazka? k = Lista.SelectedItem as Ksiazka;
if (k != null)
{
k.Wypozyczajacy = "";
PokazListe();
}
}
private void Usun_Click(object sender, RoutedEventArgs e)
{
Ksiazka? k = Lista.SelectedItem as Ksiazka;
if (k == null)
{
return;
}
MessageBoxResult odp = MessageBox.Show($"Usunąć „{k.Tytul}”?", "Usuwanie",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (odp == MessageBoxResult.Yes)
{
ksiazki.Remove(k);
PokazListe();
}
}
private void Filtr_Zmiana(object sender, RoutedEventArgs e)
{
PokazListe();
}
private void Lista_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
PokazSzczegoly();
}
// Lista w oknie to tylko WIDOK listy ksiazki — budujemy ją od nowa
private void PokazListe()
{
object? zaznaczona = Lista.SelectedItem;
Lista.Items.Clear();
int wypozyczone = 0;
foreach (Ksiazka k in ksiazki)
{
if (!k.Dostepna)
{
wypozyczone++;
}
if (TylkoDostepne.IsChecked == true && !k.Dostepna)
{
continue; // filtr: pomijamy wypożyczone
}
Lista.Items.Add(k); // na liście widać wynik ToString()
}
if (zaznaczona != null && Lista.Items.Contains(zaznaczona))
{
Lista.SelectedItem = zaznaczona;
}
Statystyka.Text = $"Książek: {ksiazki.Count}, wypożyczonych: {wypozyczone}";
PokazSzczegoly();
}
private void PokazSzczegoly()
{
Ksiazka? k = Lista.SelectedItem as Ksiazka;
if (k == null)
{
Szczegoly.IsEnabled = false;
SzTytul.Text = "Wybierz książkę z listy";
SzAutor.Text = "";
SzStatus.Text = "";
return;
}
Szczegoly.IsEnabled = true;
SzTytul.Text = k.Tytul;
SzAutor.Text = $"{k.Autor}, {k.Rok}";
SzStatus.Text = k.Dostepna ? "Na półce" : $"Wypożyczona: {k.Wypozyczajacy}";
SzStatus.Foreground = k.Dostepna ? Brushes.Green : Brushes.Firebrick;
Wypozycz.IsEnabled = k.Dostepna;
Kto.IsEnabled = k.Dostepna;
Zwroc.IsEnabled = !k.Dostepna;
}
}
Co dzieje się po kolei
- Okno to
DockPanel: formularz po lewej, szczegóły po prawej, filtr na dole, lista jako ostatnie dziecko wypełnia środek. - Konstruktor tworzy cztery obiekty
Ksiazkai dodaje je do listyksiazki.ksiazki[1].Wypozyczajacy = …zmienia właściwość drugiej książki. Dodaj_Clicksprawdza pola, tworzy obiektnew Ksiazka(tytul, autor, rok)z danych formularza i dodaje go do modelu. Potem przebudowuje widok i zaznacza nową książkę —SelectedItemprzyjmuje obiekt.Lista.SelectedItem as Ksiazkadaje zaznaczoną książkę albonull. Wypożyczenie i zwrot zmieniają tylko właściwośćWypozyczajacyobiektu.PokazListezapamiętuje zaznaczenie, czyściListBox, dodaje obiekty z modelu (pomijając wypożyczone przy włączonym filtrze), przywraca zaznaczenie i liczy statystykę. Statystyka liczy całą bibliotekę, także ukryte pozycje.continuew pętli przechodzi od razu do następnej książki — tak działa filtr.PokazSzczegolywypełnia prawy panel i włącza tylko pasujące przyciski. Bez zaznaczenia cały panel jest nieaktywny.- Klasa
Ksiazkanie ma ani jednej linii związanej z oknem — mógłbyś jej użyć w programie konsolowym.
Sprawdź trzy rzeczy
- Usuń z klasy
KsiazkametodęToString(). Lista pokaże cztery razy „Lekcja23.Ksiazka”. - Usuń z
Wypozycz_ClickwywołaniePokazListe(). Po wypożyczeniu tekst na liście się nie zmieni, choć obiekt już się zmienił — zmienią się dopiero po kliknięciu filtra. - Zaznacz filtr „tylko dostępne” i wypożycz zaznaczoną książkę. Zniknie z listy, ale statystyka nadal ją liczy.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
List<T> | kolekcja | Add, Remove, RemoveAt, Count, [i], Contains. |
public override string ToString() | metoda | Tekst obiektu — także na liście w oknie. |
public bool X => warunek; | właściwość | Wyliczana przy każdym odczycie, tylko do odczytu. |
obiekt as Typ | operator | Rzutowanie, które daje null zamiast wyjątku. |
Items.Add(obiekt) | metoda | Dodaje obiekt do listy w oknie. |
SelectedItem = obiekt | przypisanie | Zaznacza wskazany obiekt. |
continue | instrukcja | Przejście do następnego obiegu pętli. |
readonly | modyfikator | Pole przypisane raz — zawsze ta sama lista. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
class Ksiazka bez public, użyta w publicznym polu | Błąd CS0052 o niespójnej dostępności. Dodaj public. |
| inna przestrzeń nazw w pliku klasy | CS0246 — okno nie widzi klasy. Ustaw namespace Lekcja23; albo dodaj using. |
| na liście widać „Lekcja23.Ksiazka” | Brak override ToString(). |
(Ksiazka)Lista.SelectedItem bez zaznaczenia | NullReferenceException przy pierwszym użyciu. Użyj as i sprawdź null. |
usuwanie z ListBox, a nie z listy ksiazki | Po przebudowie widoku „usunięta” książka wraca. |
| zmiana właściwości bez odświeżenia widoku | Na liście widać stary tekst. |
ksiazki.Length | CS1061 — lista ma Count. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Dane opisz klasą w osobnym pliku, przechowuj w List<T>, a widok buduj z modelu jedną metodą. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Klasa Uczen z właściwościami Imie, Nazwisko, Klasa i konstruktorem. Na liście uczeń wyświetla się jako „Nowak Jan (3TI)”. Formularz dodaje ucznia (wszystkie pola wymagane), przycisk Usuń usuwa zaznaczonego. Pod listą: „Uczniów: 12”. Kliknięcie ucznia pokazuje pod listą jego inicjały dużą czcionką („J.N.”) — z metody Inicjaly() w klasie.
pokaż wskazówkę
- Pierwsza litera napisu to
Imie[0].
Klasa Produkt: nazwa, cena (decimal), ilość w magazynie i metoda Wartosc() zwracająca cenę × ilość. Na liście: „Drożdżówka — 3,50 zł × 24 szt.”. Formularz dodaje produkt, przyciski +1 i −1 zmieniają ilość zaznaczonego (nie poniżej 0). Pod listą łączna wartość magazynu, a produkty z ilością 0 mają na liście dopisek „BRAK”.
pokaż wskazówkę
- Po zmianie ilości przebuduj widok i przywróć zaznaczenie — inaczej po każdym „+1” trzeba klikać produkt od nowa.
Klasa Ocena: przedmiot, wartość (1–6), waga (1–3), opis (np. „sprawdzian z WPF”), data dodania. Formularz: przedmiot (lista rozwijana), ocena (przyciski opcji 1–6), waga (lista), opis. Nad listą ocen lista rozwijana filtra przedmiotu z pozycją „wszystkie”. Pod listą średnia ważona widocznych ocen z dwoma miejscami po przecinku i przewidywana ocena końcowa (od 4,75 — celujący, od 4,5 — bardzo dobry itd., progi ustal sam).
pokaż wskazówkę
- Średnia ważona = suma (ocena × waga) / suma wag — licz w tej samej pętli, która buduje widok.
- Gdy po filtrze nie ma ocen, nie dziel przez zero.
Przepisz zadanie „Plan wycieczki do Gdańska” z lekcji 17 tak, żeby zamiast tablic równoległych używało klasy Atrakcja (nazwa, koszt, czas w godzinach) i dwóch list: List<Atrakcja> dostepne i List<Atrakcja> plan. Na obu listach w oknie atrakcja wyświetla się jako „Rejs statkiem (45 zł, 2 h)”. Działanie programu ma się nie zmienić. W komentarzu na początku pliku okna wypisz trzy miejsca, w których kod stał się prostszy.
pokaż wskazówkę
- Przeniesienie atrakcji:
dostepne.Remove(a); plan.Add(a);i przebudowa obu widoków.Array.IndexOfnie jest już potrzebne.
Klasa Kontakt: imię, nazwisko, telefon, e-mail, Ulubiony (bool). Ulubione kontakty na liście mają gwiazdkę „★ ” przed nazwiskiem i są wyświetlane na początku. Kliknięcie kontaktu wypełnia formularz jego danymi; Zapisz zmiany zmienia zaznaczony obiekt, Dodaj jako nowy tworzy nowy. Pole wyszukiwania filtruje po imieniu, nazwisku i telefonie.
pokaż wskazówkę
- Ulubione na początku: dwie pętle w metodzie budującej widok — najpierw ulubione, potem pozostałe.
- „Zapisz zmiany” nie tworzy nowego obiektu — ustawia właściwości tego, który jest zaznaczony.
Dwie klasy: Sprzet (numer, nazwa, rodzaj — piłka, rakieta, rower…) i Wypozyczenie (wypożyczony Sprzet, imię i nazwisko ucznia, data wypożyczenia, termin zwrotu, data zwrotu albo brak). Okno: lista sprzętu (dostępny / wypożyczony), formularz wypożyczenia zaznaczonego sprzętu na 1–14 dni, lista aktywnych wypożyczeń z wyróżnieniem przeterminowanych („PO TERMINIE: 3 dni”) i przycisk Zwrot. Pod spodem historia zwrotów. Sprzęt wypożyczony nie może zostać wypożyczony drugi raz.
pokaż wskazówkę
- Właściwość
Sprzetw klasieWypozyczenieto odwołanie do obiektu z listy sprzętu — nie kopia. Dostępność sprzętu możesz wyliczać, sprawdzając, czy istnieje aktywne wypożyczenie z tym obiektem. - Dni po terminie:
(DateTime.Today - TerminZwrotu).Days. Data zwrotu „brak” to typDateTime?z wartościąnull.
Co trzeba zapamiętać
- Dane opisuj klasą w osobnym pliku (Dodaj → Klasa…) — zamiast tablic równoległych.
- Obiekty trzymaj w
List<T>jako polu okna; tworzysz je z danych formularza przeznew. ListBoxmoże przechowywać obiekty i wyświetla ichToString().- Zaznaczony obiekt:
Lista.SelectedItem as Typ+ sprawdzenienull. - Model (lista obiektów) zmieniasz, widok (
ListBox) budujesz od nowa jedną metodą.
Dokumentacja: Microsoft Learn — List<T>.