Desktop UI

Layout в Avalonia: відмінності та нові можливості

Чим Avalonia відрізняється від WPF у layout: Spacing, RelativePanel, SplitView, Expander, ItemsRepeater, Transitions та інші можливості що роблять Avalonia потужнішим у сучасних сценаріях.

Layout в Avalonia: відмінності та нові можливості

Якщо ви вивчали попередні статті блоку Layout — ви вже знаєте Grid, StackPanel, DockPanel, Canvas, Border та ScrollViewer. Все це, разом з Grid.Row, ColumnSpan, Margin, Padding та Alignment — присутнє в Avalonia один-в-один. XAML-код з WPF-статей перенесеться в Avalonia-проєкт з мінімальними змінами.

Але Avalonia — не просто "WPF для Linux". Це самостійна платформа що розвивається з 2013 року, та за цей час накопичила ряд принципових покращень у системі layout. Деякі з них настільки зручні, що WPF-розробники, побачивши їх вперше, запитують: "Чому цього немає у WPF?".

Ця стаття — практичний гід: що залишилось, що покращилось і що з'явилось нового.

Словник теми:Spacing — вбудований відступ між дочірніми елементами панелі (без Margin на кожному). RelativePanel — панель де елементи позиціонуються відносно один одного через Attached Properties. SplitView — панель з висувним Pane та основним Content (hamburger-меню). Expander — контейнер що розгортається/згортається з анімацією. ItemsRepeater — легковісний аналог ListBox для відображення списків через кастомний Layout. Transitions — вбудована в Avalonia система анімації зміни властивостей layout.

Що повністю збігається з WPF

Перш за все — важлива новина: все основне — ідентично. Якщо ви знаєте WPF layout — ви вже знаєте Avalonia layout на 80%.

ЕлементWPFAvaloniaСумісність
GridПовна (+ RowSpacing/ColumnSpacing)
StackPanelПовна (+ Spacing)
DockPanelПовна
WrapPanelПовна
CanvasПовна
UniformGridПовна
ScrollViewerМайже повна
BorderПовна (+ BoxShadow)
ViewBoxПовна
GridSplitterПовна
Panel.ZIndexПовна
Margin, PaddingПовна
HorizontalAlignmentПовна
MinWidth/MaxWidthПовна

Практично весь XAML з попередніх статей запуститься в Avalonia-проєкті після лише одного кроку: замінити xmlns на Avalonia-неймспейси.

<!-- WPF -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<!-- Avalonia — лише це відрізняється: -->
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

Решта XAML всередині — може залишатись без змін. Grid, RowDefinitions, Grid.Row, ColumnSpan, Margin, Border з CornerRadius — все це Avalonia розуміє і рендерить так само.

При міграції WPF-проєкту в Avalonia найбільше часу займає не layout — а прив'язка даних (Compiled Bindings), стилі (Selectors замість Triggers) і обробка подій вводу. Layout-частина мігрує практично без змін.

Spacing: вбудований відступ між елементами

Це перше і найочевидніше покращення, яке одразу помічають WPF-розробники.

Проблема у WPF

У WPF немає вбудованого відступу між дочірніми елементами StackPanel. Щоб додати відступи між кнопками або секціями — треба було або додавати Margin на кожен елемент, або створювати глобальний стиль:

<!-- WPF: Margin на кожному елементі (Margin-хак) -->
<StackPanel>
    <Button Margin="0,0,0,8" Content="Кнопка 1"/>
    <Button Margin="0,0,0,8" Content="Кнопка 2"/>
    <Button Margin="0,0,0,8" Content="Кнопка 3"/>
    <!-- Остання кнопка — зайвий нижній відступ! -->
</StackPanel>

Проблема Margin-хаку: останній елемент отримує зайвий відступ (у прикладі — 8px знизу після "Кнопка 3"). Щоб виправити — треба або не давати Margin останньому, або давати від'ємний Margin контейнеру. Це незручно.

Деякі WPF-розробники пишуть стиль:

<!-- WPF: Style-підхід (краще але все одно обхід) -->
<StackPanel>
    <StackPanel.Resources>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="0,0,0,8"/>
        </Style>
    </StackPanel.Resources>
    <Button Content="Кнопка 1"/>
    <Button Content="Кнопка 2"/>
    <!-- Але стиль застосовується до ВСІХ кнопок включно з останньою -->
</StackPanel>

Рішення в Avalonia: Spacing

Avalonia додала властивість Spacing прямо до StackPanel. Вона додає рівномірний відступ між елементами — але не після останнього:

<!-- Avalonia: просто і елегантно -->
<StackPanel Spacing="8">
    <Button Content="Кнопка 1"/>
    <Button Content="Кнопка 2"/>
    <Button Content="Кнопка 3"/>
    <!-- Відступ тільки між елементами: 8px між 1-2 і між 2-3 -->
    <!-- Після "Кнопка 3" — ЖОДНОГО зайвого відступу -->
</StackPanel>

Результат ідеальний: Spacing додає (n-1) відступів для n елементів. Це точно те, що потрібно у 99% випадків.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Spacing у Grid: RowSpacing та ColumnSpacing

Avalonia також додала відступи між рядками і колонками у Grid:

<!-- WPF: щоб зробити відступ між рядками — потрібен "порожній" рядок висотою 8px -->
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="8"/>    <!-- Штучний відступ! -->
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0" Text="Рядок 1"/>
    <!-- Grid.Row="1" — це порожній рядок-відступ -->
    <TextBlock Grid.Row="2" Text="Рядок 2"/>
</Grid>
<!-- Avalonia: просто RowSpacing -->
<Grid RowSpacing="8" ColumnSpacing="12">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Text="Ім'я:"/>
    <TextBox   Grid.Row="0" Grid.Column="1" Text="Іван"/>

    <TextBlock Grid.Row="1" Grid.Column="0" Text="Email:"/>
    <TextBox   Grid.Row="1" Grid.Column="1" Text="ivan@example.com"/>
    <!-- RowSpacing=8 автоматично між рядками, ColumnSpacing=12 між колонками -->
</Grid>

Результат: жодних фіктивних рядків-роздільників. Grid-структура лишається чистою, а відступи декларуються де належить — на самому Grid.


RelativePanel: позиціонування відносно сусідів

Grid чудовий для структурованих сіток — але іноді вам потрібно позиціонувати елементи відносно один одного: "цей — справа від того", "цей — під тим", "цей — вирівняний по правому краю панелі". Саме це вирішує RelativePanel.

Чому Grid тут не підходить

Уявіть інтерфейс пошуку: поле TextBox по ширині займає весь рядок, але з правого боку — кнопка "×" (очистити). При зміні розміру вікна TextBox розтягується, а кнопка має залишатись у правому куті TextBox. У Grid для цього потрібна складна структура. У RelativePanel — одна рядок.

RelativePanel — це панель де кожен дочірній елемент може декларувати своє розташування через Attached Properties відносно:

  • країв самого RelativePanel
  • або конкретного сусіднього елемента за ім'ям

Attached Properties для позиціонування

RelativePanel має дві категорії Attached Properties:

Відносно панелі:

Attached PropertyЗначення
RelativePanel.AlignLeftWithPanelВирівняти лівий край з панеллю
RelativePanel.AlignRightWithPanelВирівняти правий край з панеллю
RelativePanel.AlignTopWithPanelВирівняти верхній край з панеллю
RelativePanel.AlignBottomWithPanelВирівняти нижній край з панеллю
RelativePanel.AlignHorizontalCenterWithPanelГоризонтальний центр
RelativePanel.AlignVerticalCenterWithPanelВертикальний центр

Відносно іншого елемента ({x:Reference} або x:Name):

Attached PropertyЗначення
RelativePanel.RightOfРозмістити правіше вказаного елемента
RelativePanel.LeftOfРозмістити лівіше вказаного елемента
RelativePanel.BelowРозмістити нижче вказаного елемента
RelativePanel.AboveРозмістити вище вказаного елемента
RelativePanel.AlignLeftWithВирівняти лівий край з вказаним елементом
RelativePanel.AlignRightWithВирівняти правий край з вказаним елементом
RelativePanel.AlignTopWithВирівняти верхній край з вказаним елементом
RelativePanel.AlignBottomWithВирівняти нижній край з вказаним елементом

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

Кожен елемент у RelativePanel декларує своє розміщення через ці Attached Properties:

<RelativePanel>
    <!-- Заголовок: вгорі зліва від панелі -->
    <TextBlock x:Name="title"
               RelativePanel.AlignLeftWithPanel="True"
               RelativePanel.AlignTopWithPanel="True"
               Text="Налаштування профілю"
               FontSize="18" FontWeight="Bold"
               Margin="0,0,0,16"/>

    <!-- Аватар: нижче заголовка, вирівняний зліва -->
    <Border x:Name="avatar"
            RelativePanel.Below="title"
            RelativePanel.AlignLeftWithPanel="True"
            Width="64" Height="64"
            Background="#2563eb" CornerRadius="32"
            Margin="0,0,0,12">
        <TextBlock Text="🧑" FontSize="28" HorizontalAlignment="Center"
                   VerticalAlignment="Center"/>
    </Border>

    <!-- Ім'я: справа від аватара, вирівняний по його верху -->
    <TextBlock x:Name="userName"
               RelativePanel.RightOf="avatar"
               RelativePanel.AlignTopWith="avatar"
               Text="Іван Петренко"
               FontSize="16" FontWeight="SemiBold"
               Margin="12,0,0,0"/>

    <!-- Посада: нижче імені, вирівняна зліва з ім'ям -->
    <TextBlock RelativePanel.Below="userName"
               RelativePanel.AlignLeftWith="userName"
               Text="Junior WPF Developer"
               Foreground="#64748b"
               Margin="12,4,0,0"/>

    <!-- Кнопка "Редагувати": у правому нижньому куті панелі -->
    <Button RelativePanel.AlignRightWithPanel="True"
            RelativePanel.AlignBottomWithPanel="True"
            Content="✏️ Редагувати"
            Padding="12,6" Background="#2563eb" Foreground="White"
            CornerRadius="6"/>
</RelativePanel>

Зверніть увагу: у RelativePanel немає Grid.Row або будь-якого іншого індексного позиціонування. Кожен елемент сам декларує своє місце і відносний елемент-сусід.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Коли RelativePanel кращий за Grid

RelativePanel — не заміна Grid. Це зброя для специфічних сценаріїв:

Overlapping елементів: Бейдж "●" поверх аватара. У Grid — складно, у RelativePanel — AlignRightWith + AlignTopWith.
Float UI: Кнопка в кутку картки незалежно від контенту — AlignRightWithPanel + AlignBottomWithPanel.
Адаптивне перекомпонування: Елементи перебудовуються між режимами без перепису структури Grid.
Liquid layout: Коли елементи "прив'язані" один до одного й мають рухатись разом.

❌ Grid краший для: рівномірних форм (Label + Input), складних таблиць, фіксованих областей з чіткою структурою рядків/колонок.


SplitView: адаптивний каркас застосунку

SplitView — це контрол, призначений спеціально для реалізації патерну "Hamburger Menu" (бічна панель з навігацією). У WPF для створення такого меню доводилося писати складний код управління анімацією, розширенням колонок Grid або використовувати сторонні бібліотеки (наприклад, MahApps.Metro).

В Avalonia SplitView доступний "з коробки". Він ділить доступний простір на дві частини:

  1. Pane — бічна панель (меню).
  2. Content — основний вміст екрану.

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

  • IsPaneOpen: (bool) Відкрита чи закрита бічна панель. Можна легко прив'язати до ToggleButton (гамбургер-кнопки).
  • DisplayMode: Визначає як панель взаємодіє з основним контентом.
    • Inline: Відкриваючись, штовхає контент (змінює його ширину).
    • CompactInline: Закрита панель показує вузьку смужку (наприклад, лише іконки), відкрита — штовхає контент.
    • Overlay: Панель виїжджає поверх контенту (ідеально для мобільних екранів).
    • CompactOverlay: Закрита — іконки, відкрита — виїжджає поверх контенту.
  • OpenPaneLength: Ширина відкритої панелі.
  • CompactPaneLength: Ширина закритої панелі у компактному режимі.
  • PanePlacement: Розташування панелі (Left або Right).

Приклад використання

<SplitView IsPaneOpen="{Binding IsSidebarOpen}"
           DisplayMode="CompactInline"
           CompactPaneLength="48"
           OpenPaneLength="200">
    <SplitView.Pane>
        <!-- Вміст бічної панелі: іконки + текст -->
        <StackPanel Spacing="8" Margin="0,8">
            <Button Command="{Binding ToggleSidebarCommand}" 
                    Content="☰" Width="48" Background="Transparent"/>
            <ListBox>
                <ListBoxItem>
                    <StackPanel Orientation="Horizontal" Spacing="12">
                        <TextBlock Text="🏠" Width="24" TextAlignment="Center"/>
                        <TextBlock Text="Головна"/>
                    </StackPanel>
                </ListBoxItem>
                <ListBoxItem>
                    <StackPanel Orientation="Horizontal" Spacing="12">
                        <TextBlock Text="⚙️" Width="24" TextAlignment="Center"/>
                        <TextBlock Text="Налаштування"/>
                    </StackPanel>
                </ListBoxItem>
            </ListBox>
        </StackPanel>
    </SplitView.Pane>

    <SplitView.Content>
        <!-- Основний контент екрану -->
        <Border Background="#f1f5f9" Padding="24">
            <TextBlock Text="Тут відображається обрана сторінка" 
                       FontSize="20" Foreground="#333"/>
        </Border>
    </SplitView.Content>
</SplitView>

Цей елемент є основою для застосунків, які мають добре працювати як на широких десктопних моніторах (режим Inline), так і на телефонах/планшетах (режим Overlay).


Expander: розгортальні секції

Так, Expander є і у WPF, проте в Avalonia він має деякі переваги у побудові адаптивних layout'ів завдяки вбудованій системі інтеграції з анімаціями (Transitions).

Avalonia дозволяє легко задавати ContentTransition для згладженого переходу між розгорнутим і згорнутим станом, чого завжди не вистачало WPF, де вміст з'являвся стрибком і вимагав складних Storyboard.

<Expander Header="Розгорніть для деталей">
    <!-- Додаємо анімацію вмісту -->
    <Expander.ContentTransition>
        <CrossFade Duration="0:0:0.2" />
    </Expander.ContentTransition>
    
    <StackPanel Spacing="8" Margin="8">
        <TextBlock Text="Цей текст з'являється плавно з ефектом fade." />
        <TextBlock Text="А висота панелі розширюється за допомогою вбудованої анімації Expander." />
    </StackPanel>
</Expander>

ItemsRepeater: легковісний layout для колекцій

У WPF основним контролом для списків є ListBox або ItemsControl. Але коли вам потрібно просто вивести 1000 карток з даними у вигляді сітки — ListBox додає багато зайвого навантаження (виділення, фокус, складні візуальні дерева).

В Avalonia з'явився ItemsRepeater — контрол для високопродуктивного рендерингу прив'язаних даних. Він фокусується лише на Layout та Virtualization (не керує виділенням елементів на відміну від ListBox).

ItemsRepeater підтримує різні алгоритми розташування через властивість Layout:

  • StackLayout — класичний стек (як StackPanel).
  • WrapLayout — перенесення на новий рядок (як WrapPanel).
  • UniformGridLayout — сітка з однаковими комірками, яка автоматично змінює кількість колонок залежно від ширини екрану (ідеально для карток товарів/відео).
  • LinedFlowLayout — розташування як в галереї фотографій (вирівнювання за рядками).

Створення адаптивної сітки карток

Це класичний патерн "Responsive Grid", який переносить картки і заповнює доступний простір екрану:

<ScrollViewer>
    <ItemsRepeater ItemsSource="{Binding Products}">
        <ItemsRepeater.Layout>
            <!-- Адаптивна сітка з відступами 16px і мінімальним розміром комірки 280x320 -->
            <UniformGridLayout MinItemWidth="280" MinItemHeight="320"
                               MinColumnSpacing="16" MinRowSpacing="16" />
        </ItemsRepeater.Layout>
        
        <ItemsRepeater.ItemTemplate>
            <DataTemplate>
                <Border Background="White" CornerRadius="8" Padding="16" BoxShadow="0 4 12 0 #1A000000">
                    <StackPanel Spacing="8">
                        <Image Source="{Binding ContextImage}" Height="150" Stretch="UniformToFill"/>
                        <TextBlock Text="{Binding Title}" FontWeight="Bold" FontSize="16" />
                        <TextBlock Text="{Binding Price}" Foreground="#2563eb" FontWeight="SemiBold"/>
                    </StackPanel>
                </Border>
            </DataTemplate>
        </ItemsRepeater.ItemTemplate>
    </ItemsRepeater>
</ScrollViewer>

Вбудовані анімації Layout (Transitions)

Хоча анімація — це окрема тема, в Avalonia вона настільки глибоко інтегрована безпосередньо в систему Layout, що заслуговує згадки тут.

Там, де WPF вимагає ColorAnimation в EventTrigger або написання коду C#, Avalonia пропонує CSS-подібні Transitions. Будь-яка Panel може плавно анімувати зміну власних властивостей (Width, Height, Margin, Background).

<Border Background="#2563eb" Width="100" Height="40" CornerRadius="4">
    <!-- Реєстрація того, ЯК панель повинна реагувати на зміну її Width -->
    <Border.Transitions>
        <Transitions>
            <DoubleTransition Property="Width" Duration="0:0:0.3"/>
            <BrushTransition Property="Background" Duration="0:0:0.3"/>
        </Transitions>
    </Border.Transitions>

    <!-- Якщо змінити клас (через C# або CSS-стилі в Avalonia), 
         Width зміниться з 100 до 200 ПЛАВНО за 0.3 секунди -->
</Border>

Це дозволяє створювати інтерфейси, які здаються дуже "живими" (fluid interfaces). Змінили властивість розміру або позиції в C# — і елемент не "стрибне" миттєво, а плавно зсуне сусідні елементи у layout.


Висновок до блоку Layout

Avalonia бере все найкраще з WPF Layout (Grid, Margin/Padding патерни, двопрохідну модель вимірювань Measure/Arrange) і додає до неї те, що розробники просили роками:

  1. Spacing на панелях (чистість XAML).
  2. RelativePanel для "плаваючого" позиціонування без глибокого вкладання панелей.
  3. SplitView та UniformGridLayout (через ItemsRepeater) для створення сучасних, "чуйних" (responsive) десктопних застосунків.

Ці інструменти роблять створення високоякісного UI в Avalonia швидшим і виразнішим, залишаючись при цьому у знайомій усім WPF-розробникам XAML-парадигмі.