Desktop UI

Панелі Layout: StackPanel, WrapPanel, DockPanel

Вивчаємо основні панелі розташування WPF: двопрохідна модель Measure/Arrange, StackPanel для лінійного вкладання, WrapPanel для автоматичного переносу та DockPanel для класичного інтерфейсу.

Панелі Layout: StackPanel, WrapPanel, DockPanel

У веб-розробці є CSS. Там ви пишете position: absolute; top: 100px; left: 200px і елемент опиниться рівно там, де ви хочете. Можна також написати position: fixed, z-index: 999, margin: auto і отримати бажаний результат.

WPF пропонує інший підхід — і він значно кращий для складних інтерфейсів.

У WPF немає "абсолютного позиціонування за замовчуванням". Якщо ви просто кинете кілька кнопок у Grid — вони всі виявляться одна поверх одної по центру. Щоб розташувати їх правильно — потрібна панель. Панель — це спеціальний контейнер, що знає алгоритм розміщення своїх дочірніх елементів.

Чому ця архітектура краща? Тому що:

  • Інтерфейс адаптується до розміру вікна автоматично. Ви змінюєте розмір вікна — панелі перераховують розміщення.
  • Вам не треба хардкодити координати. StackPanel сам вириховує, де ставити наступну кнопку.
  • Інтерфейс залишається коректним при різних DPI, масштабах, локалізаціях.
Словник теми:Measure Pass — перший прохід layout: батько питає дочірнього "скільки місця тобі треба?", дочірній повертає DesiredSize. Arrange Pass — другий прохід: батько каже "ось тобі ця область (finalRect), розташуйся", дочірній малює себе. StackPanel — панель, що вкладає елементи вертикально або горизонтально один за одним. WrapPanel — як StackPanel, але з Orientation="Horizontal" переносить на новий рядок при нестачі місця. DockPanel — панель з "прикріпленням" елементів до сторін: Top, Bottom, Left, Right, і один (LastChildFill) займає решту.

Модель розташування WPF: Measure → Arrange

Перш ніж говорити про конкретні панелі — треба зрозуміти механізм, що лежить під усіма ними. Це двопрохідний алгоритм layout, який виконується щоразу, коли вікно змінює розмір або змінюється вміст.

Прохід 1: Measure (вимірювання)

На першому проході кожен елемент з'ясовує, скільки місця йому потрібно.

Алгоритм рекурсивний: батьківський елемент викликає Measure(availableSize) для кожного дочірнього. "Available size" — це скільки місця батько може потенційно надати. Дочірній вимірює себе (враховуючи свій вміст, шрифт, зображення) і зберігає результат у властивості DesiredSize.

// Спрощений вигляд того, що відбувається всередині WPF
protected override Size MeasureOverride(Size constraint)
{
    // Запитуємо у дочірніх: "скільки місця вам треба?"
    foreach (UIElement child in InternalChildren)
    {
        child.Measure(constraint); // constraint = скільки є у нас
        // child.DesiredSize — тепер містить відповідь
    }

    // Повертаємо наш власний DesiredSize
    return new Size(totalWidth, totalHeight);
}

Важливо: availableSize може містити double.PositiveInfinity по одній або обох осях. Це означає "місця необмежено — скільки треба". StackPanel у WPF передає нескінченний constraint по своїй осі розкладки: він говорить дочірнім "займайте стільки місця по вертикалі, скільки хочете".

Прохід 2: Arrange (розміщення)

На другому проході батьківський елемент розміщує дочірніх у конкретних прямокутниках.

Батько викликає Arrange(finalRect) для кожного дочірнього, передаючи Rect — точну позицію і розмір. Дочірній зобов'язаний розмістити себе у цій області. Він не може вийти за межі (WPF обріже зображення поза finalRect), але може розміститися менше ніж виділена область (якщо має HorizontalAlignment / VerticalAlignment).

// Спрощений Arrange у StackPanel
protected override Size ArrangeOverride(Size finalSize)
{
    double y = 0; // Поточна вертикальна позиція

    foreach (UIElement child in InternalChildren)
    {
        double childHeight = child.DesiredSize.Height;

        // Ставимо дочірній елемент у прямокутник
        child.Arrange(new Rect(0, y, finalSize.Width, childHeight));

        y += childHeight; // Наступний елемент іде нижче
    }

    return finalSize;
}

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

Розуміння Measure/Arrange пояснює поведінку панелей без магії:

  • Чому StackPanel не скролиться: По осі вкладання він передає дочірнім Infinity як constraint. Дочірні займають стільки, скільки хочуть. Батько StackPanel сам стає нескінченно великим. Якщо вікно менше — елементи просто обрізаються. Щоб мати скролл — треба обгорнути у ScrollViewer.
  • Чому Width="*" у Grid працює: GridColumn у фіналі Arrange розподіляє решту простору між зірочковими стовпцями.
  • Чому елемент із HorizontalAlignment="Center" не займає всю ширину: У Arrange він отримав широкий finalRect, але обрав лише DesiredSize.Width і центрував себе всередині.
Коли ви бачите дивну поведінку layout — запитайте себе: "Що Measure Pass повернув як DesiredSize? Який finalRect отримав цей елемент у Arrange Pass?" Відповіді на ці питання вирішать 90% Layout-загадок.

StackPanel: вертикальний та горизонтальний стек

StackPanel — найпростіша і найчастіше вживана панель. Вона робить одну річ: вкладає дочірні елементи один за одним у вертикальному або горизонтальному напрямку.

Вертикальний StackPanel (за замовчуванням)

За замовчуванням Orientation="Vertical". Кожен елемент іде під попереднім:

<StackPanel>
    <TextBlock Text="Перший рядок"/>
    <TextBlock Text="Другий рядок"/>
    <TextBlock Text="Третій рядок"/>
    <Button Content="Кнопка"/>
</StackPanel>

Що важливо: StackPanel розтягує дочірніх по поперечній осі (горизонталі у вертикальному StackPanel) до свого повного розміру. Тому кожен TextBlock і кожна Button займає повну ширину StackPanel. Але по основній осі (вертикалі) — елемент займає рівно стільки, скільки йому треба.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Горизонтальний StackPanel

Змінюємо Orientation="Horizontal" — і елементи вишиковуються в рядок:

<StackPanel Orientation="Horizontal" Spacing="8">
    <Button Content="Зберегти"/>
    <Button Content="Скасувати"/>
    <Button Content="Попередній перегляд"/>
</StackPanel>

У горизонтальному StackPanel:

  • По основній осі (горизонталі) — елемент займає свій DesiredSize.Width
  • По поперечній осі (вертикалі) — елемент розтягується до висоти StackPanel

Якщо ви хочете, щоб елементи не розтягувалися по висоті — задайте VerticalAlignment:

<StackPanel Orientation="Horizontal" Spacing="8" Height="60"
            VerticalAlignment="Center">
    <Button Content="Маленька" VerticalAlignment="Center" Padding="8,4"/>
    <Button Content="Велика"   Padding="8,12"/>    <!-- Займе всю висоту -->
    <Button Content="Середня"  VerticalAlignment="Center" Padding="8,8"/>
</StackPanel>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Spacing — відступи між елементами

У WPF .NET 6+ у StackPanel доданий атрибут Spacing (спочатку лише в Avalonia, потім портований). Він задає відстань між елементами автоматично — без потреби руками задавати Margin кожному:

<!-- Без Spacing: треба задавати Margin кожному -->
<StackPanel>
    <Button Content="Перша" Margin="0,0,0,8"/>
    <Button Content="Друга" Margin="0,0,0,8"/>
    <Button Content="Третя"/>
</StackPanel>

<!-- Зі Spacing: один атрибут на всіх -->
<StackPanel Spacing="8">
    <Button Content="Перша"/>
    <Button Content="Друга"/>
    <Button Content="Третя"/>
</StackPanel>

Обидва дають однаковий результат — але Spacing значно зручніший і чистіший.

Вкладені StackPanel

StackPanel легко вкладати: вертикальний StackPanel містить горизонтальні рядки, або навпаки:

<StackPanel Spacing="12" Margin="16">

    <!-- Рядок 1: Поле вводу -->
    <StackPanel>
        <TextBlock Text="Ім'я користувача" FontWeight="SemiBold" Margin="0,0,0,4"/>
        <TextBox Padding="8,6" PlaceholderText="Введіть ім'я..."/>
    </StackPanel>

    <!-- Рядок 2: Поле вводу -->
    <StackPanel>
        <TextBlock Text="Email" FontWeight="SemiBold" Margin="0,0,0,4"/>
        <TextBox Padding="8,6" PlaceholderText="email@example.com"/>
    </StackPanel>

    <!-- Рядок 3: Горизонтальна пара кнопок -->
    <StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
        <Button Content="Скасувати" Padding="12,8"/>
        <Button Content="Зберегти"  Padding="12,8" Background="#2563eb" Foreground="White"/>
    </StackPanel>

</StackPanel>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Коли StackPanel — правильний вибір

StackPanel ідеальний для:

Вертикальні списки: меню, списки налаштувань, чекбокси, форми з вертикальним потоком
Горизонтальні панелі: toolbar, рядок кнопок, breadcrumbs, tabs-рядок
Фіксований вміст: коли кількість елементів невелика і не змінюється
Як внутрішній контейнер: всередині Grid-комірок, Border, ScrollViewer

Не підходить для:

  • Складних 2D-сіток (там потрібен Grid)
  • Великих списків з тисячами елементів (там ListBox або ItemsControl з VirtualizingStackPanel)
  • Коли потрібна точна розбивка на рядки/колонки з вирівнюванням між рядками

WrapPanel: стек з автоматичним переносом

WrapPanel — це StackPanel, якому дозволили "переносити рядок". Якщо при горизонтальному вкладанні наступний елемент не вміщається у ширину — він переходить на новий рядок. При вертикальному — на новий стовпець.

Основна поведінка

<!-- Горизонтальний WrapPanel (за замовчуванням) -->
<WrapPanel>
    <Button Content="Елемент А" Margin="4" Padding="12,8"/>
    <Button Content="Елемент Б" Margin="4" Padding="12,8"/>
    <Button Content="Елемент В" Margin="4" Padding="12,8"/>
    <!-- Якщо В не вміщається — переноситься на новий рядок -->
    <Button Content="Елемент Г" Margin="4" Padding="12,8"/>
    <Button Content="Елемент Д" Margin="4" Padding="12,8"/>
</WrapPanel>

Ключова відмінність від StackPanel:

  • StackPanel ніколи не переносить — елементи просто виходять за межі і обрізаються (або розтягують панель нескінченно)
  • WrapPanel відстежує доступну ширину і переносить при нестачі

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

ItemWidth та ItemHeight: рівномірна сітка

За замовчуванням кожен елемент займає стільки місця, скільки йому треба — тому елементи у WrapPanel можуть мати різну ширину. Щоб зробити рівномірну сітку — використовуйте ItemWidth та ItemHeight:

<!-- Всі елементи матимуть ширину 120 і висоту 80 -->
<WrapPanel ItemWidth="120" ItemHeight="80">
    <Border Background="#3b82f6" Margin="4">
        <TextBlock Text="Картка 1" HorizontalAlignment="Center"
                   VerticalAlignment="Center" Foreground="White"/>
    </Border>
    <Border Background="#8b5cf6" Margin="4">
        <TextBlock Text="Картка 2" HorizontalAlignment="Center"
                   VerticalAlignment="Center" Foreground="White"/>
    </Border>
    <Border Background="#10b981" Margin="4">
        <TextBlock Text="Картка 3" HorizontalAlignment="Center"
                   VerticalAlignment="Center" Foreground="White"/>
    </Border>
    <!-- ... -->
</WrapPanel>

Коли заданий ItemWidthWrapPanel ділить доступну ширину на ItemWidth і визначає, скільки елементів поміщається у рядок. Решта переноситься. Отримується поведінка подібна до CSS display: flex; flex-wrap: wrap;.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Вертикальний WrapPanel

Orientation="Vertical" вкладає елементи зверху вниз і переносить на новий стовпець при нестачі висоти:

<WrapPanel Orientation="Vertical" Height="200">
    <Button Content="А" Margin="4" Padding="10,8"/>
    <Button Content="Б" Margin="4" Padding="10,8"/>
    <Button Content="В" Margin="4" Padding="10,8"/>
    <!-- При нестачі висоти — переноситься у другий стовпець -->
    <Button Content="Г" Margin="4" Padding="10,8"/>
    <Button Content="Д" Margin="4" Padding="10,8"/>
</WrapPanel>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

WrapPanel у ScrollViewer: галерея

WrapPanel часто використовують в парі зі ScrollViewer. Так виходить адаптивна галерея: при зменшенні вікна — більше рядків але скролл компенсує:

<ScrollViewer VerticalScrollBarVisibility="Auto">
    <WrapPanel Margin="8">
        <!-- Тут може бути 100 карток/зображень -->
        <Border Width="160" Height="120" Margin="6" Background="#e2e8f0" CornerRadius="8">
            <TextBlock Text="Фото 1" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <!-- ... -->
    </WrapPanel>
</ScrollViewer>
WrapPanel — не найефективніший для великих списків. При 1000+ елементів краще використовувати ItemsControl з WrapPanel як ItemsPanel — тоді WPF застосує віртуалізацію (тільки видимі елементи існують у пам'яті). Але для UI-компонентів (кнопки тегів, набори іконок, менеджер файлів) — WrapPanel абсолютно підходить.

Коли WrapPanel — правильний вибір

Набори тегів або категорій: кнопки-теги, фільтри, чіпи
Файловий менеджер / галерея: іконки файлів що заповнюють вікно
Адаптивні панелі кнопок: toolbar що переносить кнопки при вузькому вікні
Будь-яке розміщення "зліва направо з переносом"

Не підходить для:

  • Таблиць з вирівняними стовпцями (там Grid)
  • Фіксованих сіток де важлива порядність рядків (там UniformGrid або Grid)
  • Великих динамічних списків без віртуалізації

DockPanel: прикріплення до сторін

DockPanel — панель для побудови класичного інтерфейсу настільного застосунку: меню зверху, панель статусу знизу, дерево/навігація зліва, основний вміст у центрі. Цей патерн настільки поширений, що має власну назву — Shell Layout.

Принцип роботи: кожен дочірній елемент "прикріплюється" до однієї зі сторін через Attached Property DockPanel.Dock. Значення: Top, Bottom, Left, Right. Після всіх прикріплених — останній дочірній займає весь решта простору. Ця поведінка регулюється LastChildFill (за замовчуванням True).

Базовий синтаксис

<DockPanel>
    <Menu DockPanel.Dock="Top"/>          <!-- прикріплено зверху -->
    <StatusBar DockPanel.Dock="Bottom"/>   <!-- прикріплено знизу -->
    <TreeView DockPanel.Dock="Left" Width="200"/>  <!-- прикріплено зліва -->
    <TextBox AcceptsReturn="True"/>        <!-- LastChildFill: займає весь центр -->
</DockPanel>

Порядок оголошення має значення: DockPanel обробляє елементи зверху вниз по XML. Кожен наступний отримує залишок простору після попередніх.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Порядок обробки: чому важливий XAML-порядок

Кожен елемент "відрізає" свою частину від загального простору. Наступний отримує залишок:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Кілька елементів на одній стороні

Один Dock може бути у кількох елементів — вони вишиковуються по черзі в тому ж напрямку:

<DockPanel>
    <!-- Обидва зверху: Menu, потім Toolbar під ним -->
    <Menu DockPanel.Dock="Top"/>
    <ToolBar DockPanel.Dock="Top"/>

    <!-- Bottom: перший Bottom буде ближче до краю, другий — вище нього -->
    <StatusBar DockPanel.Dock="Bottom"/>
    <FindBar DockPanel.Dock="Bottom"/>

    <TextBox/>
</DockPanel>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

LastChildFill="False"

Якщо не потрібно, щоб останній елемент займав весь центр:

<!-- Всі елементи з явним Dock: центру немає -->
<DockPanel LastChildFill="False">
    <Button DockPanel.Dock="Top" Content="Верх"/>
    <Button DockPanel.Dock="Left" Content="Ліво"/>
    <Button DockPanel.Dock="Right" Content="Право"/>
    <!-- Без LastChildFill останній також просто прикріплюється зверху -->
    <Button DockPanel.Dock="Top" Content="Ще верх"/>
</DockPanel>
DockPanel є одним з рідкісних випадків у WPF, де порядок дочірніх у XAML прямо впливає на результат. StackPanel і WrapPanel — теж залежать від порядку, але у DockPanel ця залежність найбільш драматична: переставити Menu нижче за StatusBar в XAML — і UI зламається.

Підсумок: яку панель обрати

StackPanel

Для: Вертикальних форм, горизонтальних toolbar, вкладених рядків меню. Елементи займають свій DesiredSize по основній осі і 100% по поперечній.

Використовуйте коли: Потрібен простий лінійний потік елементів без переносу і без прив'язки до сторін.

WrapPanel

Для: Тегів, фільтрів, файлових іконок, адаптивних наборів кнопок. ItemWidth/ItemHeight для рівних клітинок.

Використовуйте коли: Елументи мають переноситися на нові рядки автоматично при нестачі місця.

DockPanel

Для: Shell Layout (Menu + Toolbar + Sidebar + Content + StatusBar). Порядок у XAML = порядок розміщення.

Використовуйте коли: Потрібна типова оболонка застосунку або будь-який layout з "периферійними" зонами та центральним вмістом.

StackPanelWrapPanelDockPanel
Авто-перенос
Рівномірна сіткаItemWidth
Центральний Fill
Вкладеннярідко

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