Desktop UI

Content Model — GroupBox, Expander, TabControl, StatusBar

Досліджуємо Content Model WPF — фундаментальну архітектурну концепцію, на якій побудовано всі контейнерні контроли. Вивчаємо GroupBox, Expander, TabControl та StatusBar як інструменти організації складних інтерфейсів.
Нові терміни у цій статті:ContentControl, ItemsControl, HeaderedContentControl, GroupBox, Expander, ExpandDirection, TabControl, TabItem, StatusBar, StatusBarItem, Content Model, Logical Tree, Visual Tree.

Content Model WPF: фундаментальна архітектурна концепція

Чому це важливо розуміти

Коли новачок у WPF вперше бачить, що Button може містити StackPanel з Image та TextBlock, — він здивований. У WinForms кнопка — це кнопка: прямокутник із написом. Звідки така гнучкість?

Відповідь — у Content Model (Моделі Вмісту), архітектурному рішенні, яке пронизує весь WPF від самого фундаменту. Розуміння цієї концепції є ключем не просто до групових контролів цієї статті, а й до всього WPF загалом — до DataTemplate, ControlTemplate, стилізації та навіть до MVVM.

У WPF кожен контрол визначає, скількох і яких дочірніх елементів він очікує. Виходячи з цього, все дерево контролів розбивається на дві великі категорії:


ContentControl: один вміст, необмежена складність

ContentControl — клас, для якого вміст означає рівно один об'єкт, але цей об'єкт може бути будь-чим. Властивість Content типу object приймає:

  • рядок — відображається як TextBlock автоматично;
  • будь-який UIElement — відображається як є;
  • будь-який CLR-об'єкт (не UIElement) — відображається через DataTemplate або ToString().

Це означає: якщо Button є ContentControl (а він є), то його Content може бути StackPanel, що містить Image і TextBlock. WPF просто покладає цей StackPanel всередину кнопки і відображає — без жодних обмежень. Саме тому у WPF немає окремого класу ImageButton: будь-яка Button вже є "image button", якщо в неї покласти Image.

Приклади ContentControl-ів у WPF:

КласДе живе Content
Button, RepeatButtonВидима область кнопки
LabelВідображувана частина підпису
CheckBox, RadioButtonТекст праворуч від позначки
GroupBoxВміст рамки
ExpanderВміст, що розкривається
TabItemВміст відповідної вкладки
ScrollViewerПрокручуваний вміст
WindowЄдиний кореневий елемент вікна

ItemsControl: колекція елементів

ItemsControl — клас для контролів, що відображають колекцію елементів. Замість одного Content — властивості Items (колекція ItemCollection) та ItemsSource (прив'язка до зовнішньої колекції).

Приклади ItemsControl-ів:

КласЩо містить
ListBox, ListViewСписок рядків або складних елементів
ComboBoxСписок варіантів для вибору
Menu, ContextMenuПункти меню
TabControlКолекція TabItem-ів
StatusBarКолекція StatusBarItem-ів
TreeViewІєрархічне дерево вузлів
Цей розподіл — не просто класифікація заради класифікації. Він визначає, як контрол взаємодіє з даними. ContentControl прив'язується до одного об'єкта (DataContext). ItemsControl прив'язується до колекції (ItemsSource). Розуміння цієї різниці є передумовою для Data Binding у Блоці 6.

HeaderedContentControl: заголовок плюс вміст

Між ContentControl і конкретними контролами на кшталт GroupBox або Expander стоїть ще один проміжний клас — HeaderedContentControl. Він розширює ContentControl, додаючи друге "місце" для вмісту — Header. Ієрархія:

FrameworkElement
  └── Control
        └── ContentControl
              └── HeaderedContentControl
                    ├── GroupBox
                    └── Expander

Header — так само object, як і Content. Це означає: заголовок GroupBox або стрілка-заголовок Expander можуть містити не лише текст, а й зображення, іконку, кнопку або цілий StackPanel. Саме таку гнучкість ми побачимо у прикладах нижче.


GroupBox: рамка з заголовком

Призначення та архітектура

GroupBox — найнаочніший простий HeaderedContentControl. Він малює видиму рамку (border) навколо свого вмісту і виводить заголовок (Header) у лівій частині верхньої межі рамки. Це класичний UI-паттерн для логічного групування пов'язаних елементів у формах: "Персональні дані", "Адреса", "Налаштування безпеки".

Важливо розуміти: GroupBox — це виключно візуальний та семантичний контейнер. Він не впливає на логіку, не обмежує введення, не об'єднує RadioButton-и в групу (для цього використовується GroupName). Його єдина роль — дати користувачу зрозуміти, що ці елементи пов'язані між собою.

Типова структура GroupBox:

<GroupBox Header="Заголовок групи">
  <!-- Content: будь-який один UIElement -->
  <StackPanel>
    <!-- Довільна кількість дочірніх елементів -->
  </StackPanel>
</GroupBox>

Оскільки Content приймає лише один дочірній елемент, завжди потрібен контейнер-посередник (StackPanel, Grid, WrapPanel) для розміщення кількох полів всередині GroupBox.

Header як довільний XAML

Одна з малопомічних, але потужних можливостей — Header може бути не просто рядком, а повноцінним XAML-елементом. Це дозволяє створювати заголовки з іконками, кольоровим текстом або навіть функціональними елементами (наприклад, CheckBox у заголовку для вмикання/вимикання всієї групи — патерн "Enable Group"):

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

GroupBox із CheckBox у заголовку: патерн "Enable Group"

Один з найпоширеніших прикладних патернів з GroupBox — використання CheckBox безпосередньо у Header. Коли прапорець знятий — вся група налаштувань вимикається (через IsEnabled). Це інтуїтивний UX для "необов'язкових секцій":

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Зверніть на прив'язку IsEnabled="{Binding ElementName=proxyEnabledCheck, Path=IsChecked}". Це той самий ElementName-binding, що ми вивчали у статті про Slider. Властивість IsEnabled на StackPanel успадковується всіма дочірніми елементами — жодного C#-коду не потрібно. Декларативне рішення у чистому XAML.

Expander: секція, що розкривається

Концепція та призначення

ExpanderHeaderedContentControl, що додає до рамки GroupBox одну ключову поведінку: вміст можна приховати та розкрити натисканням на заголовок. У згорнутому стані видно лише Header зі стрілкою-індикатором; в розгорнутому — під заголовком з'являється Content.

Цей патерн повсюдно використовується в:

  • FAQ-секціях: список питань, де відповідь розкривається при кліку.
  • Панелях налаштувань: рідко використовувані опції приховані за замовчуванням (наприклад, "Додаткові параметри").
  • Деревах з групуванням: групи елементів, що можна згорнути для зменшення візуального шуму.
  • Бічних панелях: секції типу "Фільтри" або "Метадані", що розкриваються на вимогу.

Визначальна перевага Expander перед ручним приховуванням через Visibilityанімація. WPF за замовчуванням анімує розгортання та згортання вмісту — елемент плавно з'являється/зникає завдяки вбудованому стану аніматора у ControlTemplate. Жодного C#-коду для анімації не потрібно.

Ключові властивості Expander

IsExpanded
bool
Керує поточним станом. true — вміст видимий, false — прихований. За замовчуванням — false(закрито). Встановіть IsExpanded="True" для автоматичного розгортання при відкритті вікна.
ExpandDirection
ExpandDirection
Напрямок розгортання вмісту відносно заголовка. Down (вміст з'являється знизу, стрілка дивиться вниз — за замовчуванням), Up (вміст вище заголовка), Left (вміст ліворуч), Right (вміст праворуч). Для Left/Right заголовок відображається вертикально.
Expanded / Collapsed
RoutedEvent
Спрацьовують при розгортанні (IsExpanded → true) та згортанні (IsExpanded → false). Підписуйтесь через Expanded="Handler" у XAML або через AddHandler у коді.
Header
object
Заголовок секції — так само object, як у GroupBox. Може бути рядком, TextBlock, StackPanel з іконкою тощо. Поруч із Header завжди відображається стрілка-індикатор поточного стану.

Expander: базове використання

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Expander: XAML-заголовок з іконкою та ExpandDirection

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Превью використовує Avalonia Fluent Theme, яка підтримує базову поведінку Expander. Анімація розгортання у реальному WPF — більш плавна завдяки вбудованому DoubleAnimation у стандартному ControlTemplate. У Avalonia анімація може бути спрощеною або відсутньою залежно від версії.

TabControl: багатосторінковий інтерфейс

Архітектурна роль TabControl

TabControl — це ItemsControl, дочірніми елементами якого є TabItem-и. Кожен TabItem є HeaderedContentControl із двома частинами:

  • Header — заголовок вкладки (видимий завжди, навіть коли вкладка не активна).
  • Content — вміст вкладки (видимий лише для активної вкладки).

Ця архітектура вирішує поширену задачу: відобразити кілька "сторінок" налаштувань або розділів в одному вікні без переходів між вікнами. Типові застосування — вікна налаштувань (General / Appearance / Advanced), майстри (Wizards) з кроками, деталі об'єкта з кількох аспектів (Основне / Контакти / Документи).

Ключові властивості TabControl

SelectedIndex
int
Індекс активної вкладки (0-based). За замовчуванням — 0 (перша вкладка). Програмне перемикання: tabControl.SelectedIndex = 2; — переходить на третю вкладку.
SelectedItem
object
Обраний TabItem у вигляді об'єкта. Зручніший ніж SelectedIndex у прив'язці даних.
TabStripPlacement
Dock
Де розміщуються ярлики вкладок відносно вмісту: Top (за замовчуванням), Bottom, Left, Right.
SelectionChanged
RoutedEvent
Подія зміни активної вкладки. Аргумент SelectionChangedEventArgs містить AddedItems (нова вкладка) та RemovedItems (попередня).

Властивості TabItem

TabItem сам по собі нащадок HeaderedContentControl, тому успадковує логіку Header/Content. Крім того:

IsSelected
bool
true якщо ця вкладка активна. Зазвичай зчитується, не встановлюється — краще використовувати TabControl.SelectedIndex.
IsEnabled
bool
Якщо false — вкладка відображається, але не клікабельна. Корисно для "кроків майстра", де доступ до наступного кроку відкривається після завершення поточного.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Перемикання вкладок програмно

Звичайний сценарій: кнопка "Далі"/"Назад" у майстрі (wizard), що керує TabControl.SelectedIndex:

private void NextStep_Click(object sender, RoutedEventArgs e)
{
    int current = wizardTab.SelectedIndex;
    int total   = wizardTab.Items.Count;

    if (current < total - 1)
    {
        wizardTab.SelectedIndex = current + 1;
    }

    // Вимикаємо "Далі" на останній вкладці
    nextButton.IsEnabled = wizardTab.SelectedIndex < total - 1;
    // Вмикаємо "Назад" якщо не на першій
    prevButton.IsEnabled = wizardTab.SelectedIndex > 0;
}

private void PrevStep_Click(object sender, RoutedEventArgs e)
{
    if (wizardTab.SelectedIndex > 0)
        wizardTab.SelectedIndex--;

    nextButton.IsEnabled = wizardTab.SelectedIndex < wizardTab.Items.Count - 1;
    prevButton.IsEnabled = wizardTab.SelectedIndex > 0;
}
У TabControl з IsEnabled="False" на вкладках — приховайте також стрілки. Перемикайте Visibility кнопок разом з IsEnabled, щоб не заплутати користувача. Для справжнього Wizard-інтерфейсу (з перевіркою даних перед переходом) перевіряйте в SelectionChanged і у разі невалідності повертайте SelectedIndex назад — з виведенням повідомлення.

StatusBar: рядок статусу

Призначення і контекст

StatusBarItemsControl спеціального призначення, що традиційно розміщується у нижній частині вікна і відображає актуальну інформацію про стан застосунку: поточний режим, прогрес фонової операції, координати курсору, час останнього збереження, кількість обраних елементів.

На відміну від кнопок чи форм, StatusBar несе суто інформаційне навантаження — він не призначений для введення даних або ухвалення рішень. Це "пасивна" частина інтерфейсу, яка постійно оновлюється кодом у відповідь на зміни стану застосунку.

StatusBar може містити StatusBarItem-и (аналог ListBoxItem) — кожен є ContentControl, тобто може вміщувати будь-який UIElement. Між ними розміщують Separator для візуального поділу секцій.

StatusBarItem
ContentControl
Базовий елемент StatusBar. Успадковує від ContentControl, тому Content може бути текстом, TextBlock, ProgressBar, StackPanel тощо.
Separator
Separator
Вертикальна роздільна лінія між StatusBarItem-ами. У StatusBar автоматично відображається вертикально (на відміну від горизонтального роздільника в меню).

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Динамічне оновлення StatusBar

У реальних застосунках StatusBar оновлюється у відповідь на дії користувача або фонові операції:

// Оновлення статусу після збереження файлу
private async void SaveFile_Click(object sender, RoutedEventArgs e)
{
    statusText.Text = "Збереження...";
    saveProgressBar.Visibility = Visibility.Visible;
    saveProgressBar.IsIndeterminate = true;

    try
    {
        await Task.Run(() => SaveFileAsync()); // Симуляція збереження

        statusText.Text = $"Збережено: {DateTime.Now:HH:mm:ss}";
        saveProgressBar.IsIndeterminate = false;
        saveProgressBar.Value = 100;
    }
    catch (Exception ex)
    {
        statusText.Text = $"Помилка: {ex.Message}";
        statusText.Foreground = Brushes.OrangeRed;
    }
    finally
    {
        // Ховаємо ProgressBar через 2 секунди
        await Task.Delay(2000);
        saveProgressBar.Visibility = Visibility.Collapsed;
    }
}
У прикладі вище Task.Run і await вже потребують розуміння асинхронного програмування. Для навчальних цілей замінити на синхронний виклик або Thread.Sleep. Важливо: будь-яке оновлення UI з не-UI потоку потребує Dispatcher.Invoke(() => { ... }) — безпосередньо звертатися до WPF-елементів з фонового потоку заборонено.

Практичні завдання


Підсумок

Що ми вивчили у цій статті

Ключове відкриття цієї статті — не самі контроли, а Content Model: архітектурна концепція, що пояснює, чому WPF настільки гнучкий.

Content Model ділить усі контроли на два фундаментальні типи. ContentControl (і його нащадок HeaderedContentControl) приймає рівно один Content — але цей об'єкт може бути будь-якою складністю. Саме тому Button може містити Grid, GroupBoxStackPanel, а заголовок ExpanderStackPanel з іконкою та Border. ItemsControl оперує колекцією елементів — це основа для TabControl, StatusBar, ListBox, ComboBox.

GroupBoxHeaderedContentControl для візуального та семантичного групування форм. Header може бути довільним XAML, що відкриває потужний паттерн "CheckBox у заголовку" для вмикання/вимикання цілих секцій через ElementName-прив'язку.

Expander додає до GroupBox поведінку розгортання/згортання з вбудованою анімацією. IsExpanded, ExpandDirection, XAML-заголовок — основні важелі. Незамінний для FAQ, панелей налаштувань і секцій з рідко потрібними опціями.

TabControlItemsControl із TabItem-ами (HeaderedContentControl). SelectedIndex керує активною вкладкою програмно. TabItem.IsEnabled="False" — зручний механізм для "кроків майстра". TabStripPlacement дозволяє розміщувати ярлики з будь-якого боку.

StatusBarItemsControl для відображення стану застосунку. Може містити TextBlock, ProgressBar, будь-який UIElement всередині StatusBarItem. Оновлюється виключно через код; для фонових операцій — await Task.Run() плюс Dispatcher.Invoke().

Що далі

У наступній статті ми розглянемо панелі прокрутки та навігаціїScrollViewer, Frame та механізм навігації WPF. ScrollViewer — обов'язковий контейнер для будь-якого вмісту, що може виходити за межі видимої області. Frame та Page — основа для навігаційних застосунків із журналом переходів.