ComboBox — lista rozwijana
Gdy opcji jest kilkanaście albo więcej, przyciski opcji zajęłyby pół okna. Lista rozwijana pokazuje tylko wybraną pozycję i rozwija się po kliknięciu. To najczęstsza kontrolka wyboru w formularzach egzaminacyjnych: klasa, województwo, waluta, rozmiar. Nauczysz się wypełniać ją w XAML i z kodu, odczytywać wybór numerem i treścią oraz budować listy zależne od siebie.
Czego się dziś nauczysz
- Wypełnisz listę rozwijaną pozycjami w XAML i w pętli z tablicy.
- Odczytasz wybór numerem (
SelectedIndex) i treścią (SelectedItem), a dodatkowe dane — zTagpozycji. - Zareagujesz na zmianę wyboru zdarzeniem
SelectionChangedi obsłużysz stan „nic nie wybrano” (−1). - Pozwolisz użytkownikowi wpisać własną wartość (
IsEditable). - Zbudujesz dwie listy, z których druga zależy od wyboru w pierwszej.
Pozycje w XAML
<StackPanel Margin="15" Width="220" HorizontalAlignment="Left">
<Label Content="_Klasa:" Target="{Binding ElementName=Klasa}"/>
<ComboBox x:Name="Klasa" SelectedIndex="0">
<ComboBoxItem Content="1TI"/>
<ComboBoxItem Content="2TI"/>
<ComboBoxItem Content="3TI"/>
<ComboBoxItem Content="4TI" Tag="maturalna"/>
</ComboBox>
</StackPanel>
rozwinięta lista — pierwsza pozycja wybrana przez SelectedIndex="0"
Każda pozycja to ComboBoxItem z tekstem w Content. Jak każda kontrolka, pozycja może mieć Tag z dodatkowymi danymi. SelectedIndex="0" wybiera pierwszą pozycję; bez niego lista startuje pusta.
Pozycje z kodu
Gdy pozycji jest dużo albo pochodzą z danych, dodajesz je z kodu do kolekcji Items. Najprościej dodawać zwykłe napisy:
string[] miesiace = { "styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec",
"lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień" };
foreach (string m in miesiace)
{
Miesiac.Items.Add(m);
}
Miesiac.SelectedIndex = DateTime.Now.Month - 1; // bieżący miesiąc (numeracja od 0!)
// inne operacje na pozycjach
Miesiac.Items.Count; // ile pozycji
Miesiac.Items.Clear(); // usuń wszystkie
Miesiac.Items.Contains("maj") // czy jest taka pozycja
Items działa podobnie jak Children panelu: Add, Insert, Remove, RemoveAt, Clear, Count. Nie mieszaj w jednej liście pozycji z XAML i z kodu — wybierz jeden sposób.
Odczyt wyboru
| Właściwość | Typ | Co zawiera |
|---|---|---|
SelectedIndex | int | Numer wybranej pozycji od 0. −1, gdy nic nie wybrano. Idealny do tablic: kursy[Waluta.SelectedIndex]. |
SelectedItem | object? | Wybrana pozycja: napis (gdy dodawałeś napisy) albo ComboBoxItem (gdy pozycje są w XAML). null, gdy nic nie wybrano. |
Text | string | Tekst widoczny w polu listy — wygodny do wyświetlania. |
if (Klasa.SelectedIndex == -1)
{
Info.Text = "Wybierz klasę.";
return;
}
// pozycje z XAML: SelectedItem to ComboBoxItem
ComboBoxItem pozycja = (ComboBoxItem)Klasa.SelectedItem;
Info.Text = $"Klasa {pozycja.Content}";
if ($"{pozycja.Tag}" == "maturalna")
{
Info.Text += " — w tym roku matura!";
}
// pozycje dodane jako napisy: SelectedItem to string
string miesiac = (string)Miesiac.SelectedItem;
Zdarzenie SelectionChanged
Wywołuje się po każdej zmianie wyboru: kliknięciem, klawiaturą (strzałki na zamkniętej liście też zmieniają wybór!) i z kodu — także przez SelectedIndex="0" w XAML w trakcie InitializeComponent(). Parametr ma typ SelectionChangedEventArgs.
private void Klasa_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (Info == null || Klasa.SelectedIndex == -1)
{
return; // okno jeszcze się buduje albo lista została wyczyszczona
}
Info.Text = $"Wybrano: {Klasa.Text}"; // uwaga: Text może mieć jeszcze starą wartość
}
Text w SelectionChanged
W chwili zdarzenia SelectedIndex i SelectedItem mają już nową wartość, ale Text bywa jeszcze stary. W SelectionChanged odczytuj wybór przez SelectedIndex lub SelectedItem.
Lista z wpisywaniem
Z IsEditable="True" lista staje się połączeniem pola tekstowego i listy: użytkownik może wybrać pozycję albo wpisać własny tekst. Wpisaną wartość odczytujesz z Text, a SelectedIndex wynosi wtedy −1 (chyba że tekst pasuje do którejś pozycji). Podczas pisania lista podpowiada pasujące pozycje.
<ComboBox x:Name="Miasto" IsEditable="True" Width="200">
<ComboBoxItem Content="Lębork"/>
<ComboBoxItem Content="Słupsk"/>
<ComboBoxItem Content="Gdańsk"/>
</ComboBox>
Listy zależne
Wybór w pierwszej liście zmienia zawartość drugiej — np. województwo i miasto. W SelectionChanged pierwszej listy czyścisz drugą i wypełniasz ją od nowa:
private readonly string[] pomorskie = { "Gdańsk", "Lębork", "Słupsk" };
private readonly string[] zachodniopomorskie = { "Koszalin", "Szczecin" };
private void Wojewodztwo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Miasto.Items.Clear(); // SelectedIndex miasta wraca do -1
string[] miasta = Wojewodztwo.SelectedIndex == 0 ? pomorskie : zachodniopomorskie;
foreach (string m in miasta)
{
Miasto.Items.Add(m);
}
Miasto.IsEnabled = true;
}
Kantor walut
Użytkownik wpisuje kwotę i wybiera walutę źródłową i docelową. Wynik i kurs przeliczają się przy każdej zmianie, a przycisk ⇄ zamienia waluty miejscami. Waluty i kursy są w trzech tablicach, a listy wypełnia pętla w konstruktorze.
wymiana 250 zł na euro
<Window x:Class="Lekcja16.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Kantor szkolny" Height="300" Width="520"
ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
<Grid Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="_Mam:" Target="{Binding ElementName=Kwota}"/>
<Label Grid.Row="0" Grid.Column="2" Content="_Chcę otrzymać:" Target="{Binding ElementName=WalutaNa}"/>
<!-- lewa kolumna: kwota i waluta źródłowa -->
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBox x:Name="Kwota" Text="100" FontSize="16" Margin="0,0,0,6"
TextChanged="Kwota_TextChanged"/>
<ComboBox x:Name="WalutaZ" SelectionChanged="Waluta_SelectionChanged"/>
</StackPanel>
<Button Grid.Row="1" Grid.Column="1" Content="⇄" Width="40" Height="40"
FontSize="18" Margin="10,0" VerticalAlignment="Bottom"
ToolTip="Zamień waluty" Click="Zamien_Click"/>
<!-- prawa kolumna: wynik i waluta docelowa -->
<StackPanel Grid.Row="1" Grid.Column="2">
<TextBox x:Name="Wynik" IsReadOnly="True" IsTabStop="False" FontSize="16"
Background="#FFF5F5F5" Margin="0,0,0,6"/>
<ComboBox x:Name="WalutaNa" SelectionChanged="Waluta_SelectionChanged"/>
</StackPanel>
<TextBlock x:Name="Kurs" Grid.Row="2" Grid.ColumnSpan="3" Margin="0,15,0,0"
Foreground="DimGray"/>
<TextBlock Grid.Row="3" Grid.ColumnSpan="3" VerticalAlignment="Bottom"
FontSize="11" Foreground="Gray"
Text="Kursy przykładowe — nie są kursami NBP."/>
</Grid>
</Window>
using System.Windows;
using System.Windows.Controls;
namespace Lekcja16;
public partial class MainWindow : Window
{
// trzy tablice tej samej długości: element i opisuje tę samą walutę
private readonly string[] kody = { "PLN", "EUR", "USD", "GBP", "CHF", "CZK" };
private readonly string[] nazwy = { "złoty", "euro", "dolar amerykański", "funt brytyjski", "frank szwajcarski", "korona czeska" };
private readonly decimal[] kursy = { 1m, 4.30m, 3.95m, 5.10m, 4.55m, 0.17m }; // ile złotych za 1 jednostkę
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < kody.Length; i++)
{
WalutaZ.Items.Add($"{kody[i]} — {nazwy[i]}");
WalutaNa.Items.Add($"{kody[i]} — {nazwy[i]}");
}
WalutaZ.SelectedIndex = 0; // PLN
WalutaNa.SelectedIndex = 1; // EUR
}
private void Waluta_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Przelicz();
}
private void Kwota_TextChanged(object sender, TextChangedEventArgs e)
{
Przelicz();
}
private void Zamien_Click(object sender, RoutedEventArgs e)
{
int z = WalutaZ.SelectedIndex;
WalutaZ.SelectedIndex = WalutaNa.SelectedIndex;
WalutaNa.SelectedIndex = z;
}
private void Przelicz()
{
// zdarzenia przychodzą też w trakcie InitializeComponent i wypełniania list
if (Wynik == null || Kurs == null || WalutaZ.SelectedIndex < 0 || WalutaNa.SelectedIndex < 0)
{
return;
}
int z = WalutaZ.SelectedIndex;
int na = WalutaNa.SelectedIndex;
if (!decimal.TryParse(Kwota.Text, out decimal kwota) || kwota < 0)
{
Wynik.Text = "—";
Kurs.Text = "Wpisz kwotę (liczba nieujemna).";
return;
}
decimal wynik = kwota * kursy[z] / kursy[na];
Wynik.Text = $"{wynik:F2} {kody[na]}";
Kurs.Text = $"1 {kody[z]} = {kursy[z] / kursy[na]:F4} {kody[na]}";
}
}
Co dzieje się po kolei
- Trzy tablice —
kody,nazwy,kursy— mają tę samą długość. Element o numerzeiw każdej z nich opisuje tę samą walutę. - Konstruktor dodaje do obu list napisy „EUR — euro”. Kolejność pozycji jest taka sama jak w tablicach, więc
SelectedIndexlisty jest jednocześnie numerem w tablicach. - Kurs to „ile złotych za 1 jednostkę”. Kwota w złotych to
kwota × kursy[z]; podzielona przezkursy[na]daje kwotę w walucie docelowej. Dzięki temu da się wymieniać dowolną parę walut, także EUR na USD. Przeliczwychodzi od razu, gdy okno jeszcze się buduje (Wynik == null) albo któraś lista nie ma wyboru (−1). PoleKwotama w XAMLText="100", więc jegoTextChangedprzychodzi wcześnie.- Obie listy mają tę samą metodę
Waluta_SelectionChanged— i tak przeliczamy wszystko. Zamien_Clickzamienia numery wybranych pozycji przez zmienną pomocniczą, jak przy zamianie dwóch liczb. Każda zmianaSelectedIndexwywołuje przeliczenie.- Kurs w napisie ma 4 miejsca po przecinku (
F4), bo dla walut o małej wartości (CZK) dwa miejsca to za mało.
Sprawdź trzy rzeczy
- Kliknij listę walut (bez rozwijania) i naciskaj strzałki ↑ ↓. Wybór i wynik zmieniają się bez rozwijania listy.
- Usuń z
PrzeliczwarunekWalutaZ.SelectedIndex < 0. Przy starciekursy[-1]zakończy program wyjątkiemIndexOutOfRangeException. - Dodaj na końcu tablic walutę „NOK”, „korona norweska”, 0.37. Pojawi się w obu listach bez zmian w XAML.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
ComboBoxItem | kontrolka | Pozycja listy w XAML; tekst w Content, dane w Tag. |
Items | kolekcja | Add, Insert, Remove, RemoveAt, Clear, Count, Contains. |
SelectedIndex | int | Numer wybranej pozycji; −1 = brak wyboru. |
SelectedItem | object? | Wybrana pozycja (napis albo ComboBoxItem). |
Text | string | Widoczny tekst; przy IsEditable — tekst wpisany. |
IsEditable | bool | Pozwala wpisać własny tekst. |
SelectionChanged | zdarzenie | Zmiana wyboru; parametr SelectionChangedEventArgs. |
IsDropDownOpen | bool | Czy lista jest rozwinięta; można ją rozwinąć z kodu. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
tablica[Lista.SelectedIndex] bez sprawdzenia | Przy braku wyboru indeks −1 → IndexOutOfRangeException. |
(string)Klasa.SelectedItem dla pozycji z XAML | InvalidCastException — to ComboBoxItem, nie napis. |
Klasa.SelectedItem.ToString() dla pozycji z XAML | Daje „System.Windows.Controls.ComboBoxItem: 3TI”. Użyj Content. |
Klasa.Text w SelectionChanged | Może mieć jeszcze poprzednią wartość. |
pozycje w XAML i Items.Add w kodzie w jednej liście | Mieszanka napisów i ComboBoxItem — trudne do odczytu. |
SelectedIndex="0" + SelectionChanged zmieniające kontrolkę niżej w pliku | NullReferenceException przy starcie — sprawdź null. |
Items.Clear() w drugiej liście i od razu (string)Miasto.SelectedItem | SelectedItem to null — lista po wyczyszczeniu nie ma wyboru. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Zawsze obsłuż sytuację „nic nie wybrano”. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Lista rozwijana z dniami od poniedziałku do piątku. Po wyborze dnia pod listą pojawia się plan lekcji tego dnia: „1. matematyka”, „2. j. polski”… (wymyśl plan klasy). Po uruchomieniu wybrany jest dzisiejszy dzień, a w weekend — poniedziałek z dopiskiem „Najbliższy dzień nauki”.
pokaż wskazówkę
- Plan zapisz jako tablicę napisów, po jednym na dzień, z lekcjami rozdzielonymi przecinkiem —
Split(',')zrobi z nich listę. DayOfWeek: poniedziałek = 1 … piątek = 5, sobota = 6, niedziela = 0.
Trzy listy rozwijane: Napój (espresso 6 zł, latte 9 zł, kakao 8 zł, herbata 5 zł), Rozmiar (mały ×1, średni ×1,3, duży ×1,6), Mleko (krowie +0 zł, owsiane +2 zł, bez mleka +0 zł). Herbata i espresso nie mają mleka — po ich wyborze lista mleka jest nieaktywna i ustawiona na „bez mleka”. Cena przelicza się na żywo i jest zaokrąglana do 10 groszy w górę.
pokaż wskazówkę
- Ceny i mnożniki trzymaj w tablicach równoległych do pozycji list — jak kursy w przykładzie.
- Zaokrąglenie do 10 groszy w górę:
Math.Ceiling(cena * 10) / 10.
Trzy listy zależne: województwo (3 pozycje), powiat (po 2–3 dla każdego województwa), szkoła (po 2 dla każdego powiatu — wymyśl nazwy). Druga lista jest nieaktywna, dopóki nie wybrano województwa, trzecia — dopóki nie wybrano powiatu. Zmiana województwa czyści i blokuje listę szkół. Pod listami pełny adres wyboru: „pomorskie › lęborski › ZSMI Lębork”.
pokaż wskazówkę
- Powiaty możesz zapisać jako tablicę tablic:
string[][] powiaty, gdziepowiaty[0]to powiaty pierwszego województwa. - Po
Items.Clear()druga lista wywołaSelectionChangedz wyborem −1 — obsłuż to na początku metody.
Lista rozwijana z możliwością wpisywania, z kilkoma przedmiotami na start. Przycisk Dodaj dopisuje wpisany przedmiot do listy (bez spacji na brzegach, z wielkiej litery), jeśli jeszcze go nie ma — bez względu na wielkość liter. Przycisk Usuń usuwa wybrany przedmiot po potwierdzeniu. Napis pod spodem: „Przedmiotów: 7”. Nowy przedmiot zostaje od razu wybrany.
pokaż wskazówkę
- Pozycje dodawaj jako napisy (także te startowe — w konstruktorze), wtedy porównujesz zwykłe
string. - Sprawdzenie bez względu na wielkość liter: przejdź pętlą po
Itemsi porównaj wersjeToLower().
Pole na liczbę i dwie listy: z systemu i na system (dwójkowy, ósemkowy, dziesiętny, szesnastkowy). Wynik pojawia się na żywo. Liczby szesnastkowe wyświetlaj wielkimi literami, dwójkowe — w grupach po 4 cyfry od prawej („1010 0110”). Gdy wpis nie jest poprawną liczbą w wybranym systemie (np. cyfra 2 w systemie dwójkowym), pokaż komunikat zamiast wyniku.
pokaż wskazówkę
Convert.ToInt32(tekst, 2)zamienia tekst w podanym systemie na liczbę, aConvert.ToString(liczba, 16)— liczbę na tekst w systemie. Obsługiwane podstawy to 2, 8, 10 i 16 — zapisz je w tablicy.Convert.ToInt32przy złym wpisie rzuca wyjątekFormatException. Zanim zamienisz, sprawdź każdy znak, np. czy występuje w napisie"0123456789ABCDEF"obciętym do liczby znaków równej podstawie.
Nauczyciel rezerwuje jedną z trzech pracowni (204, 205, 210) na wybrany dzień z najbliższych 7 dni (pozycje w stylu „czw 08.10”) i numer lekcji (1–8, z godzinami w treści, np. „3 — 9:50–10:35”). Pod listami napis „Wolna” na zielono albo „Zajęta przez: Kowalski” na czerwono. Pole Nazwisko i przycisk Rezerwuj (aktywny tylko dla wolnego terminu z wpisanym nazwiskiem). Na start kilka terminów jest już zajętych.
pokaż wskazówkę
- Rezerwacje trzymaj w tablicy trójwymiarowej
string[,,] rezerwacje = new string[3, 7, 8]; pusty element (null) to wolny termin. - Dni wygeneruj w pętli z
DateTime.Today.AddDays(i)i formatu"ddd dd.MM". - Indeksy trzech list to od razu indeksy tablicy.
Co trzeba zapamiętać
- Pozycje listy:
ComboBoxItemw XAML alboItems.Addz kodu — nie mieszaj obu sposobów. SelectedIndex(od 0, −1 = brak) świetnie łączy się z tablicami równoległymi.SelectedItemto napis alboComboBoxItem— zależnie od tego, jak dodałeś pozycje.SelectionChangedprzychodzi także przy starcie okna i poItems.Clear()— sprawdzajnulli −1.IsEditable="True"pozwala wpisać własną wartość; odczytujesz ją zText.- Listy zależne: w
SelectionChangedpierwszej listy wyczyść i wypełnij drugą.
Dokumentacja: Microsoft Learn — klasa ComboBox.