Desktop UI

Просунуті техніки Layout

Margin, Padding, Alignment, MinWidth/MaxWidth, ScrollViewer, ViewBox, Border, GridSplitter — повноцінний посібник з просунутих можливостей розташування елементів у WPF і Avalonia.

Просунуті техніки Layout

Попередні статті про панелі (StackPanel, WrapPanel, DockPanel, Grid) охопили основний "каркас" інтерфейсу. Але WPF пропонує набір додаткових інструментів, без яких реальний UI неможливий: коректні відступи, вирівнювання, обмеження розмірів, прокручування, масштабування та інтерактивне перетворення зон.

Ця стаття — про деталі, що відрізняють "робочий" інтерфейс від "гарного" інтерфейсу.

Словник теми:Margin — зовнішні відступи елемента від сусідів і країв батьківського контейнера. Padding — внутрішні відступи між краями контролу та його вмістом. HorizontalAlignment/VerticalAlignment — розміщення елемента у межах виділеного простору. Stretch — режим де елемент займає увесь доступний простір. ScrollViewer — контейнер що додає смуги прокрутки до вмісту. ViewBox — масштабуючий контейнер. GridSplitter — інтерактивний роздільник між зонами Grid.

Margin: зовнішні відступи

Margin — це простір навколо елемента, між його зовнішнім краєм і сусідніми елементами або краями батьківського контейнера. Аналог margin у CSS.

Синтаксис Margin

WPF підтримує три варіанти запису:

<!-- 1. Одне число: однаковий відступ з усіх сторін -->
<Button Margin="8" Content="8px з усіх сторін"/>

<!-- 2. Два числа: horizontal, vertical (Left+Right, Top+Bottom) -->
<Button Margin="16,8" Content="16pxліво/право, 8px верх/низ"/>

<!-- 3. Чотири числа: Left, Top, Right, Bottom -->
<Button Margin="12,8,12,4" Content="12 ліво, 8 верх, 12 право, 4 низ"/>
Порядок у WPF: Left, Top, Right, Bottom — відрізняється від CSS де порядок Top, Right, Bottom, Left (за годинниковою). Це часта помилка навіть у досвідчених WPF-розробників що прийшли з веб.

Margin — це Thickness — структура з чотирма полями. У коді:

// Еквіваленти:
button.Margin = new Thickness(8);             // "8"
button.Margin = new Thickness(16, 8, 16, 8);  // "16,8"
button.Margin = new Thickness(12, 8, 12, 4);  // "12,8,12,4"

Margin зсередини Grid та StackPanel

Margin елемента займає простір у батьківському контейнері — зменшує доступний простір для елемента:

<StackPanel>
    <Button Margin="8" Content="Кнопка 1"/>      <!-- 8px відступ з усіх сторін -->
    <Button Margin="8" Content="Кнопка 2"/>      <!-- Відступ між кнопками: 8 + 8 = 16px -->
    <!-- Ось чому Spacing у StackPanel зручніший: не подвоює відступи -->
</StackPanel>

Краще використовувати Spacing у StackPanel для відступів між елементами і Margin для відступів від країв контейнера:

<!-- Правильно: Spacing між елементами, Margin ззовні -->
<StackPanel Spacing="8" Margin="16">
    <Button Content="Кнопка 1"/>
    <Button Content="Кнопка 2"/>
    <Button Content="Кнопка 3"/>
</StackPanel>

Padding: внутрішні відступи

Padding — простір всередині контролу, між його краями і вмістом. Не всі елементи мають Padding (наприклад, TextBlock підтримує, Rectangle — ні).

Той самий синтаксис що Margin:

<Button Padding="20,8" Content="Більша кнопка"/>
<TextBox Padding="12,6" Text="Текст з відступами від країв"/>
<Border Padding="16">
    <TextBlock Text="Контент з відступом 16px від кожного краю Border"/>
</Border>

Margin proти Padding: ключова різниця

┌─────────────── ParentContainer ─────────────────┐
│         ↑ Margin.Top                            │
│   ┌─────┴──────── Element ──────────────┐       │
│   │      ↑ Padding.Top                 │       │
│M  │  ┌───┴──────  Content  ─────┐     │P  M   │
│.  │  │                         │     │.  .   │
│L  │  └────────────────────────-┘     │R  R   │
│   │      ↓ Padding.Bottom            │       │
│   └───────────────────────────────────┘       │
│         ↓ Margin.Bottom                       │
└────────────────────────────────────────────────┘
  • Margin — простір зовні елемента (між елементом і його сусідами)
  • Padding — простір всередині кордону елемента (між кордоном і вмістом)
  • Background заповнює Padding-область (тобто фон видно за відступами)
  • Border малюється поверх Background, але до Padding

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


Alignment: вирівнювання елементів

Кожен елемент у WPF має дві властивості вирівнювання:

  • HorizontalAlignment — горизонтальне (Left, Center, Right, Stretch)
  • VerticalAlignment — вертикальне (Top, Center, Bottom, Stretch)

За замовчуванням — Stretch: елемент займає весь доступний простір у своєму напрямку.

Вирівнювання в Grid-комірці

<Grid Width="400" Height="200">
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    <!-- Без вирівнювання: Stretch (за замовчуванням) -->
    <Button Grid.Row="0" Grid.Column="0" Content="Stretch" Margin="4"/>

    <!-- Left: прилипає до лівого краю комірки -->
    <Button Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left"
            Content="Left" Margin="4"/>

    <!-- Center: по центру комірки -->
    <Button Grid.Row="0" Grid.Column="2" HorizontalAlignment="Center"
            Content="Center" Margin="4"/>

    <!-- Right: прилипає до правого краю -->
    <Button Grid.Row="0" Grid.Column="3" HorizontalAlignment="Right"
            Content="Right" Margin="4"/>
</Grid>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

HorizontalContentAlignment та VerticalContentAlignment

Це вирівнювання вмісту всередині контролу — наприклад, тексту всередині Button або ListBoxItem. Відрізняється від HorizontalAlignment контролу відносно батьківського контейнера:

<!-- HorizontalAlignment: кнопка у правій частині Grid -->
<Button HorizontalAlignment="Right" Content="Праворуч у Grid"/>

<!-- HorizontalContentAlignment: текст у правій частині кнопки -->
<Button Width="200" HorizontalContentAlignment="Right" Content="Текст у правій частині кнопки"/>

<!-- Поєднання: кнопка по центру Grid, текст по лівому краю всередині кнопки -->
<Button HorizontalAlignment="Center" Width="200"
        HorizontalContentAlignment="Left" Padding="12,0"
        Content="← Текст зліва у кнопці"/>

MinWidth/MaxWidth, MinHeight/MaxHeight

Обмеження розмірів — важливий інструмент для адаптивних інтерфейсів. Без обмежень елементи можуть бути занадто малими або занадто великими.

<!-- TextBox що розтягується, але не менше 100px і не більше 400px -->
<TextBox MinWidth="100" MaxWidth="400" Width="Auto"
         HorizontalAlignment="Stretch" Text="Гнучке текстове поле"/>

<!-- Кнопки мають мінімальну ширину 80px (однаковий вигляд) -->
<StackPanel Orientation="Horizontal" Spacing="8">
    <Button MinWidth="80" Content="OK"/>
    <Button MinWidth="80" Content="Скасувати"/>
    <Button MinWidth="80" Content="Застосувати"/>
</StackPanel>

<!-- Dropdown-список не вище 300px -->
<ComboBox MaxHeight="300">
    <!-- ItemsSource або Items -->
</ComboBox>

MinWidth/MaxWidth взаємодіють із Measure Pass:

  1. Grid/StackPanel питає елемент: "Скільки тобі треба?" (MeasureOverride)
  2. Елемент відповідає DesiredSize — але з урахуванням Min/Max обмежень
  3. Якщо доступне місце менше ніж MinWidth — елемент усе одно займе MinWidth (може виступати за межі батьківського)
  4. Якщо доступне місце більше MaxWidth — елемент обмежується MaxWidth

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


ScrollViewer: прокрутка вмісту

ScrollViewer — контейнер-обгортка, що додає смуги прокрутки до свого вмісту. Коли вміст більший за видиму область — з'являється ScrollBar.

<!-- Базовий ScrollViewer: вертикальна прокрутка за замовчуванням -->
<ScrollViewer>
    <StackPanel>
        <!-- Bagato elementiv -->
    </StackPanel>
</ScrollViewer>

Visibility режими ScrollBar

VerticalScrollBarVisibility і HorizontalScrollBarVisibility мають чотири значення:

ЗначенняПоведінка
AutoСмуга прокрутки з'являється тільки коли вміст не вміщується (за замовчуванням для Vertical)
VisibleСмуга завжди видима (навіть якщо прокрутки немає)
HiddenСмуга прихована, але вміст все одно прокручується (колесо миші)
DisabledСмуга прихована І контент обрізається (прокрутки немає)

За замовчуванням у ScrollViewer:

  • VerticalScrollBarVisibility = Auto (вертикальна прокрутка є)
  • HorizontalScrollBarVisibility = Disabled (горизонтальна прокрутка вимкнена!)
<!-- Вертикальна: Auto (з'являється за потребою) -->
<ScrollViewer VerticalScrollBarVisibility="Auto">...</ScrollViewer>

<!-- Обидва напрямки -->
<ScrollViewer HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto">
    <Image Width="2000" Height="3000" Source="large-map.jpg"/>
</ScrollViewer>

<!-- Завжди видима смуга (виглядає стабільніше) -->
<ScrollViewer VerticalScrollBarVisibility="Visible">...</ScrollViewer>

Типова помилка: ScrollViewer всередині StackPanel

Найпоширеніша помилка початківця:

<!-- ПРОБЛЕМА: ScrollViewer не прокручується! -->
<StackPanel>
    <ScrollViewer>  <!-- ← ScrollViewer отримує нескінченну висоту! -->
        <ListBox ItemsSource="{Binding Items}"/>
    </ScrollViewer>
</StackPanel>

Причина: StackPanel надає своїм дочірнім елементам нескінченний простір по вертикалі (Measure Pass повертає Infinity). Тому ScrollViewer отримує нескінченну висоту, його вміст теж отримує нескінченну висоту і ніколи не "переповнюється" — прокрутки не виникає.

Рішення 1: Дати ScrollViewer фіксовану висоту:

<StackPanel>
    <ScrollViewer Height="300">
        <ListBox ItemsSource="{Binding Items}"/>
    </ScrollViewer>
</StackPanel>

Рішення 2: Замінити StackPanel на Grid з *-рядком (Grid надає скінченну висоту):

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>  <!-- Заголовок -->
        <RowDefinition Height="*"/>     <!-- ScrollViewer отримує скінченну висоту! -->
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0" Text="Список"/>
    <ScrollViewer Grid.Row="1">
        <ListBox ItemsSource="{Binding Items}"/>
    </ScrollViewer>
</Grid>

Рішення 3: Загорнути весь StackPanel у ScrollViewer (часто найлогічніше):

<ScrollViewer>
    <StackPanel>
        <TextBlock Text="Заголовок"/>
        <ListBox Height="300" ItemsSource="{Binding Items}"/>
    </StackPanel>
</ScrollViewer>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


ViewBox: масштабування вмісту

ViewBox — контейнер, що масштабує свій єдиний дочірній елемент, щоб заповнити доступний простір. Він не змінює логічний розмір вмісту — тільки візуально трансформує через ScaleTransform.

<Viewbox Width="200" Height="100">
    <!-- Вміст Viewbox: кнопка 80×30 → буде масштабована до 200×100 -->
    <Button Width="80" Height="30" Content="Масштабована кнопка"/>
</Viewbox>

Режими Stretch

Stretch визначає як саме вміст масштабується:

<!-- Uniform: зберігає пропорції (чорні смуги якщо пропорції різні) -->
<Viewbox Stretch="Uniform" Width="300" Height="100">
    <Button Width="200" Height="200" Content="Квадрат у прямокутнику"/>
</Viewbox>

<!-- Fill: розтягує без збереження пропорцій (все заповнено, але може деформуватись) -->
<Viewbox Stretch="Fill" Width="300" Height="100">
    <Button Width="200" Height="200" Content="Деформований"/>
</Viewbox>

<!-- UniformToFill: зберігає пропорції, обрізає (все заповнено, нічого не деформовано) -->
<Viewbox Stretch="UniformToFill" Width="300" Height="100">
    <TextBlock Text="Обрізаний але не деформований" FontSize="24"/>
</Viewbox>

<!-- None: не масштабує (натуральний розмір) -->
<Viewbox Stretch="None" Width="300" Height="100">
    <Button Width="200" Height="200" Content="Натуральний (може виступати)"/>
</Viewbox>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


Border: візуальний контейнер

Border — один з найуживаніших елементів у WPF. Він надає чотири візуальні можливості: рамку (BorderBrush/BorderThickness), фон (Background), закруглені кути (CornerRadius) та внутрішні відступи (Padding).

<Border Background="#f8fafc"
        BorderBrush="#e2e8f0"
        BorderThickness="1"
        CornerRadius="8"
        Padding="16">
    <!-- Вміст: будь-який один елемент -->
    <TextBlock Text="Картка з рамкою, фоном і закругленнями"/>
</Border>

Border — одно-дочірній контейнер. Щоб помістити кілька елементів — вкладіть StackPanel або Grid:

<Border Background="White" BorderBrush="#e2e8f0" BorderThickness="1"
        CornerRadius="12" Padding="20">
    <StackPanel Spacing="8">
        <TextBlock Text="Заголовок картки" FontSize="16" FontWeight="Bold"/>
        <TextBlock Text="Опис картки з кількома рядками тексту" TextWrapping="Wrap"/>
        <Button Content="Дія" HorizontalAlignment="Right"/>
    </StackPanel>
</Border>

CornerRadius: закруглені кути

<!-- Однакові всі 4 кути -->
<Border CornerRadius="8" Background="#2563eb" Padding="12,6">
    <TextBlock Text="Всі 8px" Foreground="White"/>
</Border>

<!-- Різні кути: TopLeft, TopRight, BottomRight, BottomLeft -->
<Border CornerRadius="16,0,16,0" Background="#7c3aed" Padding="12,6">
    <TextBlock Text="Змінні кути" Foreground="White"/>
</Border>

<!-- Таблетка (pill-форма): велике заокруглення для повністю круглих країв -->
<Border CornerRadius="100" Background="#059669" Padding="16,8">
    <TextBlock Text="Pill badge" Foreground="White"/>
</Border>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


GridSplitter: інтерактивне перетворення зон

GridSplitter — елемент, що дозволяє користувачеві перетягуванням змінювати розміри рядків і колонок у Grid. Це ключовий елемент для IDE-подібних інтерфейсів з регульованими панелями.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200" MinWidth="100" MaxWidth="400"/>
        <ColumnDefinition Width="4"/>    <!-- Колонка для GridSplitter -->
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <!-- Ліва панель -->
    <TreeView Grid.Column="0"/>

    <!-- GridSplitter: займає свою колонку цілком -->
    <GridSplitter Grid.Column="1"
                  Width="4"
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Stretch"
                  Background="#e2e8f0"/>

    <!-- Права панель -->
    <TextBox Grid.Column="2" AcceptsReturn="True"/>
</Grid>

Вертикальний vs горизонтальний GridSplitter

<!-- Вертикальний роздільник (між колонками): -->
<GridSplitter Grid.Column="1"
              Width="4"
              HorizontalAlignment="Stretch"  <!-- займає всю ширину колонки -->
              ResizeDirection="Columns"/>

<!-- Горизонтальний роздільник (між рядками): -->
<GridSplitter Grid.Row="1"
              Height="4"
              VerticalAlignment="Stretch"    <!-- займає всю висоту рядка -->
              HorizontalAlignment="Stretch"  <!-- займає всю ширину -->
              ResizeDirection="Rows"/>

ResizeBehavior

Визначає які сусідні рядки/колонки змінюються при перетягуванні:

  • PreviousAndNext (за замовчуванням): змінює попередню та наступну
  • PreviousAndCurrent: змінює попередню та поточну
  • CurrentAndNext: змінює поточну та наступну
  • BasedOnAlignment: автоматично визначається по HorizontalAlignment

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


Вкладання панелей: комплексний layout

Реальні інтерфейси завжди є комбінацією панелей. Ключове правило:

Grid — основний каркас, StackPanel/WrapPanel — для простих списків всередині комірок.

<!-- Типова архітектура: DockPanel → Grid → StackPanel -->
<DockPanel>
    <!-- Зовнішній рівень: DockPanel для Shell Layout -->
    <Menu DockPanel.Dock="Top"/>
    <StatusBar DockPanel.Dock="Bottom"/>

    <!-- Середній рівень: Grid для основного layout -->
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250"/>
            <ColumnDefinition Width="4"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- Ліва панель: StackPanel для навігації -->
        <ScrollViewer Grid.Column="0">
            <StackPanel Spacing="4" Margin="8">
                <Button Content="Пункт 1"/>
                <Button Content="Пункт 2"/>
                <!-- ... -->
            </StackPanel>
        </ScrollViewer>

        <GridSplitter Grid.Column="1" Width="4" HorizontalAlignment="Stretch"/>

        <!-- Права панель: Grid для форми -->
        <ScrollViewer Grid.Column="2">
            <Grid Margin="16">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <!-- ... -->
                </Grid.RowDefinitions>
                <!-- Поля форми -->
            </Grid>
        </ScrollViewer>
    </Grid>
</DockPanel>

Підсумок

ІнструментПризначення
MarginЗовнішні відступи від сусідів/контейнера
PaddingВнутрішні відступи між кордоном і вмістом
HorizontalAlignmentПозиція у батьківській комірці (Left/Center/Right/Stretch)
HorizontalContentAlignmentПозиція вмісту всередині контролу
MinWidth/MaxWidthОбмеження розмірів (адаптивність)
ScrollViewerПрокрутка — дати Grid/фіксовану висоту, не StackPanel
ViewBoxМасштабування вмісту (Uniform/Fill/UniformToFill/None)
BorderРамка + фон + CornerRadius + Padding
GridSplitterІнтерактивна зміна розмірів зон Grid мишею

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