CheckBox, RadioButton, GroupName, ComboBox, ListBox, SelectionMode, DatePicker, Calendar, IsThreeState, IsEditable, DisplayMemberPath, SelectedItem, SelectedIndex, BlackoutDates, CalendarDateRange.Надати користувачу можливість обрати щось — одне з найфундаментальніших завдань у проєктуванні інтерфейсів. Але форматів, у яких реалізується цей вибір, чимало: чи вибирається одне значення чи декілька? Чи видимий список одразу, чи розкривається на вимогу? Чи значення відомі заздалегідь, чи вводяться вільно? Від відповіді на ці питання залежить, який саме контрол підходить для завдання.
WPF пропонує спеціалізований набір контролів вибору, кожен з яких оптимізований для конкретного UX-сценарію:
CheckBox
CheckBox-ів діють незалежно один від одного.RadioButton
ComboBox
ListBox
DatePicker / Calendar
DatePicker — компактне поле з випадаючим календарем; Calendar — повноцінний календар завжди видимий на екрані.CheckBox успадковує від ToggleButton (який ми розглядали у статті про базові контроли), тому вся логіка IsChecked (nullable bool) і три стани вже знайомі. Різниця між ToggleButton і CheckBox — суто у зовнішньому вигляді та семантиці: ToggleButton виглядає як кнопка, CheckBox — як прапорець з підписом. Поведінка — ідентична.
Ключова властивість — IsChecked типу bool?:
Значення IsChecked | Стан | Подія |
|---|---|---|
true | Позначено (✓) | Checked |
false | Не позначено (□) | Unchecked |
null | Невизначений (─) | Indeterminate |
Тристановий режим (null) активується властивістю IsThreeState="True". У звичайному режимі — лише true та false.
CheckBox має чітке UX-призначення: "батьківський" прапорець, що представляє групу дочірніх. Якщо частина дочірніх позначена — батьківський у стані null (Indeterminate). Якщо всі позначені — true. Якщо жодного — false. Цей паттерн широко використовується у деревах налаштувань (наприклад, вибір файлів для оновлення, де можна виділити групу чи окремі елементи).Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<TextBlock Text="Налаштування сповіщень:" FontWeight="SemiBold" FontSize="14"/>
<CheckBox Content="Отримувати email-сповіщення" IsChecked="True"/>
<CheckBox Content="Отримувати SMS-сповіщення"/>
<CheckBox Content="Маркетингові листи" IsChecked="False"/>
<Separator Margin="0,4"/>
<TextBlock Text="Тристановий режим (IsThreeState=True):"
Foreground="Gray" FontSize="12"/>
<CheckBox Content="Вибрати всі (частково обрано)"
IsThreeState="True"
IsChecked="{x:Null}"/>
</StackPanel>
Підписуватись на окремі події Checked/Unchecked/Indeterminate або читати IsChecked у будь-який момент — обидва підходи правомірні. Для простих форм зручніше зчитувати IsChecked у момент підтвердження (натискання "Зберегти"), ніж стежити за кожною зміною окремо:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<TextBlock Text="Параметри експорту:" FontWeight="SemiBold"/>
<CheckBox x:Name="chkIncludeImages" Content="Включити зображення" IsChecked="True"/>
<CheckBox x:Name="chkIncludeStyles" Content="Включити стилі CSS" IsChecked="True"/>
<CheckBox x:Name="chkMinifyOutput" Content="Мінімізувати вивід"/>
<CheckBox x:Name="chkOpenAfter" Content="Відкрити після експорту"/>
<Button Content="Експортувати"
Padding="12,6"
HorizontalAlignment="Left"
Margin="0,8,0,0"
Command="{Binding ShowMessageCommand}"
CommandParameter="Параметри зчитано!"/>
</StackPanel>
private void ExportButton_Click(object sender, RoutedEventArgs e)
{
bool includeImages = chkIncludeImages.IsChecked == true;
bool includeStyles = chkIncludeStyles.IsChecked == true;
bool minify = chkMinifyOutput.IsChecked == true;
bool openAfter = chkOpenAfter.IsChecked == true;
// Важлива деталь: IsChecked має тип bool? (nullable).
// Пряме порівняння "== true" повертає false для null та false.
// Не використовуйте (bool)checkbox.IsChecked — кине InvalidCastException якщо null.
var options = $"""
Зображення: {includeImages}
Стилі: {includeStyles}
Мінімізація: {minify}
Відкрити: {openAfter}
""";
MessageBox.Show(options, "Параметри експорту");
}
IsChecked в bool потребує обережності. (bool)checkBox.IsChecked — кине InvalidCastException, якщо значення null (тристановий режим). Безпечні варіанти: checkBox.IsChecked == true, checkBox.IsChecked ?? false, або checkBox.IsChecked.GetValueOrDefault().RadioButton — ще один нащадок ToggleButton, але зі специфічною поведінкою: вибір одного RadioButton у групі автоматично скасовує усі інші у тій самій групі. Це реалізується через механізм GroupName.
Правила групування:
RadioButton-и без GroupName, що знаходяться в одному батьківському контейнері, утворюють одну групу автоматично.RadioButton-и з однаковим GroupName утворюють групу незалежно від їхнього розташування у дереві елементів — навіть якщо вони знаходяться у різних StackPanel-ах чи Grid-ах.RadioButton має GroupName, а інший — ні, вони не є частиною спільної групи.Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Оберіть план підписки:" FontWeight="SemiBold" FontSize="14"/>
<!-- Група 1: тарифний план (GroupName="plan") -->
<StackPanel Spacing="8">
<RadioButton Content="🆓 Безкоштовний — до 3 проєктів"
GroupName="plan"
IsChecked="True"/>
<RadioButton Content="⭐ Стартер — до 10 проєктів, $9/міс"
GroupName="plan"/>
<RadioButton Content="🚀 Про — необмежено, $29/міс"
GroupName="plan"/>
</StackPanel>
<Separator/>
<TextBlock Text="Цикл білінгу:" FontWeight="SemiBold"/>
<!-- Група 2: цикл білінгу (GroupName="billing") — незалежна від першої -->
<StackPanel Orientation="Horizontal" Spacing="20">
<RadioButton Content="Щомісяця"
GroupName="billing"
IsChecked="True"/>
<RadioButton Content="Щороку (знижка 20%)"
GroupName="billing"/>
</StackPanel>
</StackPanel>
GroupName, якщо у вікні є кілька незалежних груп RadioButton-ів. Покладатись на автоматичне групування за батьківським контейнером — це джерело важкозрозумілих помилок, особливо коли в майбутньому структура розкладки змінюється.Оскільки RadioButton-и не мають єдиної властивості "обране значення" (на відміну від ComboBox.SelectedItem), у code-behind доводиться перебирати їх або перевіряти IsChecked кожного окремо:
private string GetSelectedPlan()
{
if (rbFree.IsChecked == true) return "Безкоштовний";
if (rbStarter.IsChecked == true) return "Стартер";
if (rbPro.IsChecked == true) return "Про";
return "Нічого не обрано";
}
IsChecked кожного RadioButton до окремого булевого поля ViewModel. Але це тема Блоку 6–7. Поки — code-behind цілком достатній.ComboBox — це контрол, що у звичайному стані займає місце одного рядка і показує лише поточний обраний елемент. При натисканні — розкривається drop-down список з усіма варіантами. Після вибору — знову згортається. Ця модель ідеальна для вибору з довгого списку варіантів, коли показувати всі одразу займало б забагато місця.
Під капотом ComboBox успадковує від Selector → ItemsControl. Це важлива деталь: він, як і ListBox, побудований на концепції Items та ItemsSource. У цій статті ми заповнюємо Items статично (у XAML або у code-behind) — прив'язку до колекцій через ItemsSource розглянемо у Блоці 6.
ComboBoxItem-ів повертає сам ComboBoxItem. При прив'язці колекції — повертає об'єкт з колекції. За замовчуванням — null (нічого не обрано).-1 — нічого не обрано. Зручний для встановлення вибору за замовчуванням: SelectedIndex="0" обере перший елемент.SelectedValuePath. Якщо SelectedValuePath="Id", то SelectedValue повертає поле Id обраного об'єкта — без необхідності кастувати SelectedItem.true — у закритому стані замість статичного тексту відображається TextBox для ручного введення. Корисно для "combobox з автодоповненням". За замовчуванням — false.ItemsSource), яку потрібно відображати у списку. Без цієї властивості — відображається результат ToString(). Докладніше — у Блоці 6.IsEditable="True" — містить поточний текст у редагованому полі (введений вручну або відповідний обраному елементу).Найпростіший спосіб заповнити ComboBox — додати ComboBoxItem-и безпосередньо у XAML:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Оберіть мову інтерфейсу:" Foreground="Gray" FontSize="13"/>
<ComboBox SelectedIndex="0" Width="240" HorizontalAlignment="Left">
<ComboBoxItem Content="🇺🇦 Українська"/>
<ComboBoxItem Content="🇬🇧 English"/>
<ComboBoxItem Content="🇩🇪 Deutsch"/>
<ComboBoxItem Content="🇫🇷 Français"/>
<ComboBoxItem Content="🇵🇱 Polski"/>
</ComboBox>
<TextBlock Text="Оберіть або введіть місто (IsEditable=True):"
Foreground="Gray" FontSize="13"/>
<ComboBox IsEditable="True"
Width="240"
HorizontalAlignment="Left"
Text="Київ">
<ComboBoxItem Content="Київ"/>
<ComboBoxItem Content="Харків"/>
<ComboBoxItem Content="Одеса"/>
<ComboBoxItem Content="Дніпро"/>
<ComboBoxItem Content="Львів"/>
</ComboBox>
<TextBlock Text="Вимкнений ComboBox (IsEnabled=False):"
Foreground="Gray" FontSize="13"/>
<ComboBox Width="240" HorizontalAlignment="Left"
IsEnabled="False" SelectedIndex="0">
<ComboBoxItem Content="Недоступний варіант"/>
</ComboBox>
</StackPanel>
Якщо список формується динамічно (наприклад, за результатами запиту, але ще без Binding), можна додати рядки безпосередньо у C#:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var countries = new[] { "Україна", "Польща", "Німеччина", "Франція", "США" };
foreach (var country in countries)
{
countryCombo.Items.Add(country);
}
countryCombo.SelectedIndex = 0; // Обираємо перший за замовчуванням
}
private void CountryCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// SelectedItem для рядків — це сам рядок
string selected = countryCombo.SelectedItem?.ToString() ?? "нічого";
statusLabel.Content = $"Обрано: {selected}";
}
ComboBox.Items лежать прості рядки — SelectedItem повертає string, кастування не потрібне. Якщо — ComboBoxItem-и — тоді (comboBox.SelectedItem as ComboBoxItem)?.Content?.ToString(). При прив'язці до колекції об'єктів — SelectedItem повертає сам об'єкт: (comboBox.SelectedItem as MyClass)?.Name. В усіх цих випадках SelectedValuePath суттєво спрощує роботу.Якщо ComboBox — це "акордеон" (вибір прихований до натискання), то ListBox — це "таблиця" (всі рядки видимі одразу). Обирайте ListBox, коли:
ComboBox це не підтримує.Ключова властивість, що відрізняє ListBox від ComboBox — SelectionMode:
| Значення | Поведінка |
|---|---|
Single | Вибір лише одного елемента (за замовчуванням) |
Multiple | Клік на елемент перемикає його виділення незалежно (без Ctrl) |
Extended | Виділення з Shift (діапазон) та Ctrl (окремі елементи), як у провіднику |
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="ListBox (Single — за замовчуванням):"
Foreground="Gray" FontSize="13"/>
<ListBox Width="220" HorizontalAlignment="Left" Height="120">
<ListBoxItem Content="🎨 Дизайн"/>
<ListBoxItem Content="💻 Розробка"/>
<ListBoxItem Content="📊 Аналітика"/>
<ListBoxItem Content="📣 Маркетинг"/>
<ListBoxItem Content="🤝 Підтримка"/>
</ListBox>
<TextBlock Text="ListBox (SelectionMode=Multiple — без Ctrl):"
Foreground="Gray" FontSize="13"/>
<ListBox SelectionMode="Multiple"
Width="220" HorizontalAlignment="Left" Height="120">
<ListBoxItem Content="✅ JavaScript"/>
<ListBoxItem Content="✅ TypeScript"/>
<ListBoxItem Content="C#"/>
<ListBoxItem Content="Python"/>
<ListBoxItem Content="Go"/>
</ListBox>
</StackPanel>
Для Single-режиму — SelectedItem та SelectedIndex, як у ComboBox. Для Multiple/Extended — колекція SelectedItems:
// Single
private void SingleListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var item = singleListBox.SelectedItem as ListBoxItem;
statusText.Text = $"Обрано: {item?.Content}";
}
// Multiple / Extended
private void MultiListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selected = multiListBox.SelectedItems
.Cast<ListBoxItem>()
.Select(i => i.Content?.ToString())
.ToList();
statusText.Text = selected.Count > 0
? $"Обрано: {string.Join(", ", selected)}"
: "Нічого не обрано";
}
SelectedItems повертає IList, не IList<T>. Для роботи з ним потрібне приведення через Cast<T>() або перебір у циклі. При прив'язці до типізованих колекцій через ItemsSource — Cast<MyClass>() повертає об'єкти потрібного типу без обхідних рукоділь.Здається, введення дати через звичайний TextBox — очевидне рішення. Але це рішення, що плодить проблеми: різні формати дат у різних культурах (dd.MM.yyyy в Україні, MM/dd/yyyy в США), помилки введення, неможливість одразу побачити день тижня, необхідність самостійно валідувати рядок. DatePicker вирішує всі ці проблеми — він дає користувачу зручний календар і повертає розробнику готовий DateTime?.
DatePicker — компактний контрол: у нормальному стані показує поточну обрану дату (або порожнє поле), при натисканні на іконку — розкривається мінікалендар. Після вибору — знову згортається. Це аналог ComboBox, але для дат.
null — нічого не обрано. Найчастіше прив'язується до DateTime?-властивості ViewModel. Встановлення через XAML: SelectedDate="{x:Static sys:DateTime.Today}" (потребує xmlns:sys="clr-namespace:System;assembly=mscorlib").DisplayDateStart="{x:Static sys:DateTime.Today}".Short (стислий, відповідно до культури) або Long (розгорнутий). За замовчуванням — Short.Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Звичайний DatePicker:" Foreground="Gray" FontSize="13"/>
<DatePicker Width="180" HorizontalAlignment="Left"/>
<TextBlock Text="Тільки майбутні дати (from today):"
Foreground="Gray" FontSize="13"/>
<DatePicker x:Name="futureDatePicker"
Width="180"
HorizontalAlignment="Left"
SelectedDateChanged="FutureDatePicker_Changed"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Обрана дата:" Foreground="Gray"/>
<TextBlock x:Name="selectedDateLabel"
Text="—"
FontWeight="Bold"
Foreground="#6366F1"/>
</StackPanel>
</StackPanel>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Встановлюємо мінімальну відображувану дату — сьогодні
futureDatePicker.DisplayDateStart = DateTime.Today;
// Додаємо вихідні методом BlackoutDates.AddDatesInPast()
// (блокує всі минулі дати)
futureDatePicker.BlackoutDates.AddDatesInPast();
}
private void FutureDatePicker_Changed(object sender,
SelectionChangedEventArgs e)
{
if (futureDatePicker.SelectedDate is DateTime date)
{
selectedDateLabel.Text = date.ToString("dd MMMM yyyy, dddd",
new System.Globalization.CultureInfo("uk-UA"));
}
else
{
selectedDateLabel.Text = "—";
}
}
Для форм бронювання типовий сценарій — заблокувати вже зайняті дати або вихідні. BlackoutDates приймає CalendarDateRange:
private void SetupBookingCalendar()
{
var picker = bookingDatePicker;
// Блокуємо всі минулі дати разом з сьогодні
picker.BlackoutDates.AddDatesInPast();
// Блокуємо конкретний діапазон (наприклад, технічна перерва)
picker.BlackoutDates.Add(
new CalendarDateRange(
new DateTime(2026, 4, 14),
new DateTime(2026, 4, 18)
)
);
// Блокуємо всі вихідні (суботи та неділі)
DateTime current = DateTime.Today;
DateTime endDate = current.AddYears(1);
while (current <= endDate)
{
if (current.DayOfWeek == DayOfWeek.Saturday
|| current.DayOfWeek == DayOfWeek.Sunday)
{
picker.BlackoutDates.Add(new CalendarDateRange(current));
}
current = current.AddDays(1);
}
}
DisplayDateChanged-подію Calendar і перефарбовують осередки через кастомний CalendarDayButtonStyle (це вже тема стилізації, Блок 8). Для навчальних цілей — цикл цілком прийнятний.Calendar — це той самий "мінікалендар", що розкривається у DatePicker, але виведений як самостійний завжди видимий контрол. Використовуйте його, коли:
SelectionMode="MultipleRange" — DatePicker не підтримує множинний вибір.SingleDate та SingleRange).Multiple та MultipleRange). Доступна лише для читання, але можна викликати Add() для програматичного вибору.SingleDate (один день), SingleRange (суцільний діапазон), MultipleRange (кілька не суміжних діапазонів), None (вибір заблоковано — Calendar тільки для відображення).Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="20">
<TextBlock Text="Calendar (SingleDate, за замовчуванням):"
Foreground="Gray" FontSize="13"/>
<Calendar SelectionMode="SingleDate"
HorizontalAlignment="Left"/>
<TextBlock Text="Calendar (SingleRange — Shift+клік для діапазону):"
Foreground="Gray" FontSize="13"/>
<Calendar SelectionMode="SingleRange"
HorizontalAlignment="Left"/>
</StackPanel>
Calendar та DatePicker у реальному WPF буде помітно іншим (класична Aero-тема із сірими кнопками). Поведінка (SelectionMode, BlackoutDates, SelectedDate) — ідентична. Для стилізованого вигляду у WPF використовують бібліотеки тем (MahApps.Metro, ModernWPF).Ціль: Практика CheckBox, IsThreeState, RadioButton, GroupName, зчитування стану.
Завдання: Реалізуйте форму опитування "Ваш профіль розробника":
CheckBox-ів (C#, Python, JavaScript, Go, Rust, Java). Вгорі — CheckBox "Вибрати всі" з IsThreeState="True". При зміні дочірніх — батьківський автоматично переходить у відповідний стан (true/false/null) у code-behind.RadioButton-и у GroupName="experience": "< 1 року", "1–3 роки", "3–5 років", "5+ років". Один обрано за замовчуванням.RadioButton-и у GroupName="employment": "Найманий", "Фрілансер", "Власник бізнесу".MessageBox з усіма обраними значеннями.Ціль: Практика ComboBox, SelectionChanged, динамічне оновлення UI.
Завдання: Реалізуйте форму вибору країни доставки:
ComboBox з 6–8 країнами (заповнити через code-behind у Window_Loaded). Перша обрана за замовчуванням.ComboBox — TextBlock із прапорцем та назвою країни (оновлюється через SelectionChanged). Мапи прапорців: словник Dictionary<string, string> у code-behind.ComboBox — місто для обраної країни. При зміні країни — список міст у другому ComboBox перезаповнюється (через Items.Clear() → Items.Add()).MessageBox із "Обрано: країна, місто".Ціль: Практика DatePicker, BlackoutDates, SelectedDateChanged, валідація діапазону.
Завдання: Форма бронювання номеру:
DatePicker — "Дата заїзду" та "Дата виїзду". Обидва мають DisplayDateStart = DateTime.Today та BlackoutDates.AddDatesInPast().дата заїзду + 1 день (через checkOutPicker.DisplayDateStart).DatePicker-ах через BlackoutDates (цикл по всіх вихідних протягом наступного року).TextBlock із "Кількість ночей: X" (обраховується як різниця дат у днях).IsEnabled="False".Розширення: додати ListBox (SelectionMode=Multiple) зі списком додаткових послуг: "Сніданок", "Паркінг", "Прибирання", "Спа". Підсумок вартості оновлюється динамічно.
Ця стаття охопила шість контролів вибору — від найпростіших прапорців до повноцінного датапікера. Узагальнимо ключові висновки.
CheckBox — незалежний прапорець із трьома станами (true/false/null). Тристановий режим (IsThreeState="True") використовується для "батьківських" прапорців, що представляють групу. При зчитуванні IsChecked завжди використовуйте == true або ?? false — ніколи пряме приведення.
RadioButton — виключний вибір одного варіанту з групи. Групування через GroupName — єдиний надійний спосіб, якщо у вікні кілька незалежних груп. Для зчитування обраного у code-behind — перевірка IsChecked кожного окремо; в MVVM — прив'язка до bool-властивостей ViewModel.
ComboBox — drop-down список з підтримкою редагованого введення (IsEditable). Статичне наповнення — через ComboBoxItem-и у XAML або Items.Add() у code-behind. SelectedItem/SelectedIndex/SelectedValue — три способи зчитати обраний елемент. ItemsSource для прив'язки до колекцій — у Блоці 6.
ListBox — видимий список з підтримкою множинного вибору (SelectionMode). Режим Extended відтворює поведінку провідника Windows (Shift + Ctrl). SelectedItems для множинного вибору повертає нетипізований IList — потрібен Cast<T>().
DatePicker — компактний вибір дати з калькулятором через BlackoutDates. DisplayDateStart/DisplayDateEnd ховають недоступні дати; BlackoutDates — блокують конкретні. AddDatesInPast() — зручний метод для "тільки майбутнє".
Calendar — повноцінний завжди видимий календар. SelectionMode="MultipleRange" дозволяє обирати кілька несуміжних діапазонів — можливість, недоступна у DatePicker.
У наступній статті ми розглянемо контроли-контейнери вмісту — GroupBox, Expander, ScrollViewer, TabControl, Frame. Ці контроли не відображають дані самі по собі, а організовують інші контроли у структуровані групи, розділи та вкладки — будівельні блоки складних багатосторінкових форм.
Текстові контроли — TextBlock, TextBox, RichTextBox
Детально розглядаємо контроли для відображення та введення тексту у WPF — від легковісного TextBlock до повноцінного RichTextBox із FlowDocument. Розуміємо різницю між відображенням і введенням, форматованим і неформатованим текстом, захищеним введенням через PasswordBox.
Content Model — GroupBox, Expander, TabControl, StatusBar
Досліджуємо Content Model WPF — фундаментальну архітектурну концепцію, на якій побудовано всі контейнерні контроли. Вивчаємо GroupBox, Expander, TabControl та StatusBar як інструменти організації складних інтерфейсів.