Desktop UI

Адаптивний Layout та найкращі практики

Responsive Layout у WPF: Star sizing, Visibility (Collapsed/Hidden), SizeChanged event, MinWidth як responsive tool, WrapPanel для адаптивних сіток, best practices.

Адаптивний Layout та найкращі практики

WPF-застосунок — на відміну від мобільних додатків — запускається у вікні довільного розміру. Користувач може розтягнути вікно на весь екран, стиснути до мінімуму або розмістити поряд з іншими вікнами. Якщо інтерфейс не адаптується до цих змін — він виглядає зламаним.

Хороша новина: WPF-система layout вже побудована з урахуванням адаптивності. *-розміри у Grid, WrapPanel, ScrollViewer — все це автоматично реагує на зміну доступного місця. Ваша задача — правильно скласти ці блоки.

Словник теми:Star sizing (*) — пропорційний розподіл залишкового простору у Grid. Visibility — перерахунок простору при Collapsed (звільняє місце) vs Hidden (зберігає місце). SizeChanged — подія що виникає при зміні розміру елемента. ActualWidth/ActualHeight — реальний розмір елемента після Layout Pass. UseLayoutRounding — вирівнювання до цілих пікселів для чіткості.

Star sizing: основа адаптивних layout

Ви вже знайомі з * з попередньої статті. Тепер розглянемо його як головний інструмент responsive layout.

Чому * кращий за фіксовані пікселі

<!-- ПОГАНО: фіксований розмір — при зменшенні вікна кнопки ховаються -->
<Grid Width="800">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="600"/>  <!-- Завжди 600px -->
        <ColumnDefinition Width="200"/>  <!-- Завжди 200px -->
    </Grid.ColumnDefinitions>
    ...
</Grid>

<!-- ДОБРЕ: пропорційний розподіл — адаптується до будь-якої ширини -->
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="3*"/>  <!-- 75% простору -->
        <ColumnDefinition Width="*"/>   <!-- 25% простору -->
    </Grid.ColumnDefinitions>
    ...
</Grid>

При ширині вікна 800px: 3* → 600px, * → 200px — той самий результат.
При ширині 1200px: 3* → 900px, * → 300px — масштабується пропорційно.
При ширині 400px: 3* → 300px, * → 100px — залишається пропорційним.

Золоте правило комбінування Auto + *

Найефективніша комбінація для адаптивних форм:

<Grid>
    <Grid.ColumnDefinitions>
        <!-- Auto для фіксованих елементів (мітки, іконки) -->
        <ColumnDefinition Width="Auto"/>

        <!-- * для елементів що мають розтягуватись (поля вводу, списки) -->
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <!-- Мітка auto = її природній розмір -->
    <TextBlock Grid.Column="0" Text="Email:" Margin="0,0,8,0"
               VerticalAlignment="Center"/>

    <!-- Поле вводу * = займає весь залишок -->
    <TextBox Grid.Column="1" Text="user@example.com"/>
</Grid>

Цей патерн працює при будь-якій ширині вікна: мітка займає рівно стільки скільки їй треба, поле вводу займає решту.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


Visibility: приховування і перебудова layout

Visibility — властивість що є у кожного UIElement. Вона контролює видимість і участь у layout:

<!-- Visible: елемент видимий і займає місце (за замовчуванням) -->
<Button Visibility="Visible" Content="Видима кнопка"/>

<!-- Collapsed: елемент невидимий і НЕ займає місце (layout перебудовується!) -->
<Button Visibility="Collapsed" Content="Прихована кнопка (0×0 у layout)"/>

<!-- Hidden: елемент невидимий, але ВСЕ ЩЕ займає місце -->
<Button Visibility="Hidden" Content="Невидима, але місце є"/>

Collapsed vs Hidden: практична різниця

[Button 1] [Button 2] [Button 3]

Якщо Button 2 = Collapsed:
[Button 1] [Button 3]   ← Layout перебудувався

Якщо Button 2 = Hidden:
[Button 1] [       ] [Button 3]   ← Місце збереглось

Collapsed — для responsive приховування (sidebar, panel, toolbar).
Hidden — для синхронізованих layout де ви хочете резервувати місце (наприклад, помилки валідації що не повинні зміщувати форму).

<!-- Типовий патерн: помилка валідації що не зміщує форму -->
<TextBlock Text="Поле обов'язкове!" Foreground="Red"
           Visibility="Hidden"  <!-- зберігає місце під повідомлення -->
           x:Name="errorMessage"/>

<!-- На відміну від Collapsed, TextBox не стрибає вгору/вниз при показі помилки -->

Visibility через код C#

// Перемкнути sidebar
private void ToggleSidebar(bool show)
{
    sidebar.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
}

// Реакція на зміну розміру
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
    if (e.NewSize.Width < 800)
    {
        sidebar.Visibility = Visibility.Collapsed;
        sidebarSplitter.Visibility = Visibility.Collapsed;
    }
    else
    {
        sidebar.Visibility = Visibility.Visible;
        sidebarSplitter.Visibility = Visibility.Visible;
    }
}

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


SizeChanged: реакція на зміну розміру

SizeChanged — подія що спрацьовує при будь-якій зміні ActualWidth або ActualHeight елемента. Її можна підписати на Window, Grid, Border чи будь-який інший FrameworkElement.

<!-- XAML: підписуємо Window або Grid -->
<Window SizeChanged="Window_SizeChanged">
<!-- або -->
<Grid SizeChanged="ContentGrid_SizeChanged">
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
    // e.NewSize: новий розмір
    // e.PreviousSize: попередній розмір
    // e.WidthChanged: чи змінилась ширина
    // e.HeightChanged: чи змінилась висота

    double newWidth = e.NewSize.Width;

    // Adaptive breakpoints:
    if (newWidth < 600)
    {
        // Compact mode
        sidebar.Visibility = Visibility.Collapsed;
        splitter.Visibility = Visibility.Collapsed;
        compactToolbar.Visibility = Visibility.Visible;
        fullToolbar.Visibility = Visibility.Collapsed;
    }
    else if (newWidth < 1000)
    {
        // Medium mode
        sidebar.Visibility = Visibility.Collapsed;
        splitter.Visibility = Visibility.Collapsed;
        compactToolbar.Visibility = Visibility.Collapsed;
        fullToolbar.Visibility = Visibility.Visible;
    }
    else
    {
        // Full mode
        sidebar.Visibility = Visibility.Visible;
        splitter.Visibility = Visibility.Visible;
        compactToolbar.Visibility = Visibility.Collapsed;
        fullToolbar.Visibility = Visibility.Visible;
    }
}

ActualWidth та ActualHeight

Width і Height — це бажані розміри, що ви задаєте у XAML. ActualWidth і ActualHeightреальні розміри після Layout Pass. Вони можуть відрізнятися:

// Після завантаження:
Console.WriteLine(myButton.Width);        // NaN (не задано явно)
Console.WriteLine(myButton.ActualWidth);  // 142.5 (реальний розмір після layout)

// ActualWidth доступний тільки після Loaded події:
myButton.Loaded += (s, e) =>
{
    Console.WriteLine(myButton.ActualWidth);  // Коректне значення
};
ActualWidth/ActualHeightнедоступні до того, як елемент пройшов перший Layout Pass. Якщо ви читаєте їх у конструкторі — отримаєте 0. Завжди підписуйтесь на Loaded або SizeChanged.

Адаптивний Dashboard: повний приклад з SizeChanged

<!-- MainWindow.xaml -->
<Window SizeChanged="Window_SizeChanged">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="sidebarColumn" Width="220" MinWidth="180" MaxWidth="320"/>
            <ColumnDefinition Width="4"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- Sidebar -->
        <Border x:Name="sidebar" Grid.Column="0" Background="#1e293b">
            <!-- Навігація -->
        </Border>

        <!-- GridSplitter -->
        <GridSplitter x:Name="splitter" Grid.Column="1" Width="4"
                      HorizontalAlignment="Stretch"/>

        <!-- Content -->
        <Grid Grid.Column="2">
            <!-- Compact toolbar (тільки при малій ширині) -->
            <Button x:Name="menuButton" Content="☰"
                    HorizontalAlignment="Left" Margin="8"
                    Visibility="Collapsed"
                    Click="MenuButton_Click"/>

            <!-- Основний вміст -->
            <ScrollViewer Margin="0,40,0,0" x:Name="mainContent">
                <!-- Dashboard контент -->
            </ScrollViewer>
        </Grid>
    </Grid>
</Window>
// MainWindow.xaml.cs
private bool _sidebarVisible = true;

private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
    bool shouldShowSidebar = e.NewSize.Width >= 800;

    if (shouldShowSidebar != _sidebarVisible)
    {
        _sidebarVisible = shouldShowSidebar;
        UpdateSidebarVisibility(shouldShowSidebar);
    }
}

private void UpdateSidebarVisibility(bool show)
{
    sidebar.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
    splitter.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
    menuButton.Visibility = show ? Visibility.Collapsed : Visibility.Visible;

    if (show)
        sidebarColumn.Width = new GridLength(220);
    else
        sidebarColumn.Width = new GridLength(0);
}

private void MenuButton_Click(object sender, RoutedEventArgs e)
{
    // Показати sidebar як overlay або toggle
    UpdateSidebarVisibility(!_sidebarVisible);
}

MinWidth/MaxWidth як responsive інструмент

MinWidth — це не просто обмеження розміру. Це responsive threshold: коли доступний простір стає менший за MinWidth, елемент зберігає свій мінімальний розмір і батьківський ScrollViewer починає прокручуватись:

<!-- ScrollViewer + Grid з мінімальним розміром: -->
<ScrollViewer HorizontalScrollBarVisibility="Auto">
    <Grid MinWidth="480">  <!-- Grid не буде вужчим за 480px -->
        <!-- Контент форми -->
    </Grid>
</ScrollViewer>

При ширині вікна > 480px — горизонтальний scroll не з'явиться.
При ширині вікна < 480px — з'явиться горизонтальний ScrollBar, Grid залишиться > 480px.

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

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


WrapPanel як responsive grid

WrapPanel — найближчий аналог CSS flex-wrap. Елементи вишиковуються у рядок і автоматично переносяться на наступний при нестачі місця. Без жодного C# коду.

<ScrollViewer>
    <WrapPanel Margin="8" ItemWidth="200" ItemHeight="120">
        <!-- При ширині 800px: 4 карток у рядку -->
        <!-- При ширині 600px: 3 картки у рядку -->
        <!-- При ширині 420px: 2 картки у рядку -->
        <Border Background="White" BorderBrush="#e2e8f0" BorderThickness="1"
                CornerRadius="8" Margin="6"/>
        <!-- Ще картки... -->
    </WrapPanel>
</ScrollViewer>

Переваги такого підходу:

  • Повністю декларативний — жодного C# для адаптивності
  • Автоматичний перенос при зміні ширини вікна
  • Рівномірні розміри через ItemWidth/ItemHeight

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


Best practices Layout

1. Уникайте абсолютних розмірів (крім Min/Max)

<!-- ПОГАНО: Width/Height без Auto або * -->
<TextBox Width="240" Height="32"/>

<!-- ДОБРЕ: тільки Min/Max, сам елемент адаптується -->
<TextBox MinWidth="120" MaxWidth="400"/>

<!-- НОРМАЛЬНО: фіксований Width для sidebar (дизайн-рішення) -->
<Border Width="220"/>  <!-- Sidebar фіксованої ширини — ок -->

2. Мінімізуйте глибину вкладання панелей

Кожен рівень вкладання — додатковий Measure+Arrange Pass. При 10+ рівнях вкладання (що зустрічається у складних UI) — це виразно впливає на продуктивність.

<!-- ПОГАНО: надлишкове вкладання -->
<DockPanel>
    <StackPanel>
        <Border>
            <Grid>
                <StackPanel>
                    <TextBlock Text="Привіт"/>
                </StackPanel>
            </Grid>
        </Border>
    </StackPanel>
</DockPanel>

<!-- ДОБРЕ: тільки необхідні рівні -->
<DockPanel>
    <Border Padding="16">
        <TextBlock Text="Привіт"/>
    </Border>
</DockPanel>

3. UseLayoutRounding="True" для чіткості

При масштабуванні з fractional DPI (96, 120, 144 DPI) координати можуть бути дробовими. UseLayoutRounding вирівнює до цілих пікселів і позбавляє розмитих ліній:

<!-- На рівні вікна (поширюється на всіх дітей): -->
<Window UseLayoutRounding="True">

Або на конкретному елементі:

<Border UseLayoutRounding="True" BorderThickness="1" BorderBrush="#e2e8f0"/>

Без UseLayoutRounding="True" межі Border завтовшки 1px можуть виглядати 2px через SubPixel Rendering.

4. SnapsToDevicePixels для гострих ліній

<!-- Для елементів де важлива чіткість (separator, border, chart lines): -->
<Rectangle SnapsToDevicePixels="True" Fill="#e2e8f0" Height="1"/>

SnapsToDevicePixels — старіший аналог UseLayoutRounding, впливає на рендеринг, а не на layout.

5. Порядок пріоритетів при виборі розміру

WPF застосовує обмеження у такому порядку (від вищого до нижчого пріоритету):

MinWidth ← MaxWidth ← Width ← DesiredSize (Auto/*)

Тобто MinWidth завжди перемагає над Width, а Width перемагає над Auto/DesiredSize. Це дозволяє будувати строгі обмеження:

<!-- Кнопка від 80px до 200px, бажано Auto -->
<Button MinWidth="80" MaxWidth="200" Width="Auto" Content="Адаптивна кнопка"/>

6. Чи потрібен ScrollViewer?

Золоте правило: якщо вміст може бути більшим за контейнер — загорніть у ScrollViewer.

<!-- Сторінка налаштувань — завжди у ScrollViewer: -->
<ScrollViewer VerticalScrollBarVisibility="Auto">
    <StackPanel Margin="24" Spacing="16">
        <!-- Секції налаштувань -->
    </StackPanel>
</ScrollViewer>

<!-- Навігаційна панель — теж у ScrollViewer: -->
<ScrollViewer VerticalScrollBarVisibility="Auto">
    <StackPanel Spacing="4">
        <!-- Пункти меню -->
    </StackPanel>
</ScrollViewer>

Підсумок: чеклист адаптивного layout

Star vs Fixed

Використовуйте * і Auto замість фіксованих пікселів. Фіксовані розміри — тільки для Sidebar та UI-елементів де дизайн вимагає конкретного значення.

Visibility

Collapsed — прибирає елемент з layout (responsive приховування). Hidden — зберігає місце (для валідаційних повідомлень). Ніколи не плутайте ці два режими.

SizeChanged

Підписуйтесь на Window.SizeChanged для breakpoint-логіки. Читайте e.NewSize.Width. Не читайте ActualWidth до Loaded.

WrapPanel

Для адаптивних сіток елементів — WrapPanel + ItemWidth/ItemHeight + ScrollViewer. Переноситься автоматично. Ніякого C#.

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