Desktop UI

Grid, Canvas, UniformGrid

Вивчаємо Grid — найпотужнішу панель WPF: типи розмірів Auto/**/fixed, Grid.Row/Column, RowSpan/ColumnSpan, SharedSizeGroup. Розбираємо Canvas та UniformGrid.

Grid, Canvas, UniformGrid

У попередній статті ми розібрали три панелі: StackPanel, WrapPanel, DockPanel. Всі вони вирішують конкретні завдання, але є принципово лінійними: елементи вишиковуються в один рядок, стовпець або прикріплюються до сторін.

Настав час головної панелі WPF. Якщо б вам довелося залишити тільки одну панель — це був би Grid. Він вирішує будь-яке завдання верстки: форми, діалоги, дашборди, складні сітки, повноекранні лейаути. Professional WPF-розробник використовує Grid у 70-80% випадків верстки.

Словник теми:Grid — двовимірна панель з рядками та колонками; кожен дочірній елемент розміщується у конкретній комірці. Auto — розмір рядка/колонки по вмісту. Star (*) — пропорційний розмір: решта місця поділяється між усіма *-рядками/колонками у їх пропорції. RowSpan / ColumnSpan — об'єднання кількох рядків/колонок для одного елемента. SharedSizeGroup — механізм синхронізації ширини колонок між різними Grid-ами. Canvas — панель абсолютного позиціонування: Canvas.Left/Top/Right/Bottom. UniformGrid — спрощений Grid з однаковими комірками.

Grid: двовимірна панель

Grid — це таблиця. Ви визначаєте структуру рядків та колонок, а потім розміщуєте елементи у конкретних комірках. На відміну від HTML-таблиць, Grid у WPF не потребує вкладання <tr><td> — будь-який елемент може бути розміщений у будь-якій комірці через Attached Properties.

Мінімальний Grid

Навіть без оголошення RowDefinitions та ColumnDefinitions Grid вже є: він має 1 рядок і 1 колонку за замовчуванням. Тому будь-який <Grid> без визначень — це просто контейнер де всі елементи ставляться одне на одне по центру:

<!-- Один рядок, одна колонка — все по центру один на одному -->
<Grid>
    <Rectangle Fill="#3b82f6" Width="200" Height="100"/>
    <TextBlock Text="Поверх прямокутника" HorizontalAlignment="Center"
               VerticalAlignment="Center" Foreground="White"/>
</Grid>

Це корисно коли вам потрібно накласти елементи один на одного (наприклад, Loading Spinner поверх контенту) — для цього Grid є кращим контейнером ніж Canvas.

Оголошення рядків та колонок

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>   <!-- рядок по вмісту -->
        <RowDefinition Height="*"/>      <!-- рядок займає решту -->
        <RowDefinition Height="60"/>     <!-- рядок рівно 60px -->
    </Grid.RowDefinitions>

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200"/>  <!-- колонка рівно 200px -->
        <ColumnDefinition Width="*"/>    <!-- колонка займає решту -->
    </Grid.ColumnDefinitions>

    <!-- Тепер розміщуємо елементи через Grid.Row і Grid.Column -->
    <TextBlock Grid.Row="0" Grid.Column="0" Text="Рядок 0, Колонка 0"/>
    <TextBox   Grid.Row="0" Grid.Column="1" Text="Рядок 0, Колонка 1"/>
    <ListBox   Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
    <Button    Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Content="OK"/>
</Grid>

Якщо Grid.Row або Grid.Column не вказані — за замовчуванням 0. Це корисно: перший елемент без атрибутів потрапляє у верхній лівий кут.


Типи розмірів: Auto, *, fixed

Це найважливіша концепція Grid. Кожен RowDefinition.Height та ColumnDefinition.Width може мати один з трьох типів розміру. Їх правильне поєднання — ключ до елегантного layout.

Fixed: фіксовані пікселі

Просто число — означає конкретну кількість device-independent pixels (DIP). Рядок або колонка завжди цього розміру, незалежно від вмісту чи розміру вікна:

<ColumnDefinition Width="200"/>  <!-- завжди 200px -->
<RowDefinition Height="48"/>     <!-- завжди 48px -->

Коли використовувати: Sidebar фіксованої ширини, header фіксованої висоти, toolbar. Загалом — коли розмір не має змінюватися з вмістом.

Коли НЕ використовувати: Основний вміст, форми де текст може бути різної довжини (через локалізацію тощо).

Auto: розмір по вмісту

Auto — The Grid просить дочірній елемент визначити потрібний розмір (Measure Pass), і потім встановлює розмір рядка/колонки рівним максимальному DesiredSize серед усіх елементів у цьому рядку/колонці:

<ColumnDefinition Width="Auto"/>  <!-- ширина = ширина найшироішого елемента у цій колонці -->
<RowDefinition Height="Auto"/>    <!-- висота = висота найвищого елемента у цьому рядку -->

Коли використовувати: Label-колонки у формах (мітки різної довжини — Auto підлаштовується), рядки з контентом різної висоти.

Важливо: Auto-колонка або рядок ніколи не займе більше ніж треба. Але й не менше.

<!-- Класичний Layout форми: Label Auto, Input * -->
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/> <!-- Колонка міток -->
        <ColumnDefinition Width="*"/>    <!-- Колонка полів вводу -->
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <Label Grid.Row="0" Grid.Column="0" Content="Ім'я:"/>
    <TextBox Grid.Row="0" Grid.Column="1" Margin="4"/>

    <Label Grid.Row="1" Grid.Column="0" Content="Email:"/>
    <TextBox Grid.Row="1" Grid.Column="1" Margin="4"/>

    <Label Grid.Row="2" Grid.Column="0" Content="Телефон:"/>
    <TextBox Grid.Row="2" Grid.Column="1" Margin="4"/>
</Grid>

Мітка "Телефон:" трохи довша за "Ім'я:" та "Email:" — але тому що перша колонка Auto, вся колонка автоматично розшириться до ширини найдовшої мітки. Всі поля вводу (у *-колонці) при цьому залишаться вирівняними.

Star (*): пропорційний розподіл

Зірочкові рядки/колонки ділять між собою весь залишковий простір після фіксованих і Auto-розмірів. За замовчуванням зірочки рівноправні:

<Grid Width="300">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>  <!-- 150px: половина -->
        <ColumnDefinition Width="*"/>  <!-- 150px: половина -->
    </Grid.ColumnDefinitions>
</Grid>

Числовий коефіцієнт перед зірочкою задає пропорцію. 2* означає "вдвічі більше ніж 1*":

<Grid Width="600">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>   <!-- 150px: 1 частина з 4 -->
        <ColumnDefinition Width="2*"/>  <!-- 300px: 2 частини з 4 -->
        <ColumnDefinition Width="*"/>   <!-- 150px: 1 частина з 4 -->
    </Grid.ColumnDefinitions>
    <!-- Загалом: 1+2+1 = 4 частини. 600 / 4 = 150px на частину -->
</Grid>

Алгоритм:

  1. Відніміть всі Fixed-розміри від доступного простору
  2. Для Auto — виміряйте вміст
  3. Відніміть Auto-розміри від залишку
  4. Порахуйте суму коефіцієнтів усіх *-визначень
  5. Розподіліть залишок пропорційно коефіцієнтам
<!-- Приклад: Total width = 800px -->
<Grid Width="800">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200"/>   <!-- fixed: 200px -->
        <ColumnDefinition Width="Auto"/>  <!-- auto: припустимо 80px (по вмісту) -->
        <ColumnDefinition Width="2*"/>    <!-- star: (800-200-80) * 2/3 = 346.67px -->
        <ColumnDefinition Width="*"/>     <!-- star: (800-200-80) * 1/3 = 173.33px -->
    </Grid.ColumnDefinitions>
</Grid>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

ShowGridLines="True" для відлагодження

ShowGridLines — корисний атрибут при розробці: він малює пунктирні лінії між рядками та колонками. У готовому застосунку завжди видаляйте або ставте False.

<!-- Тільки для розробки: -->
<Grid ShowGridLines="True">

Grid.Row, Grid.Column: розміщення елементів

Без Attached Properties Grid.Row і Grid.Column усі дочірні елементи потрапляють у комірку [0, 0]. Значення індексовані з нуля:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>  <!-- Row 0 -->
        <RowDefinition Height="Auto"/>  <!-- Row 1 -->
        <RowDefinition Height="*"/>     <!-- Row 2 -->
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>  <!-- Column 0 -->
        <ColumnDefinition Width="*"/>     <!-- Column 1 -->
        <ColumnDefinition Width="Auto"/>  <!-- Column 2 -->
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Text="Ім'я:"/>
    <TextBox   Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"/>

    <TextBlock Grid.Row="1" Grid.Column="0" Text="Країна:"/>
    <ComboBox  Grid.Row="1" Grid.Column="1"/>
    <Button    Grid.Row="1" Grid.Column="2" Content="..."/>

    <TextBox   Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3"
               AcceptsReturn="True" TextWrapping="Wrap"/>
</Grid>

Відступи в Grid: Margin на дочірніх елементах

Grid не має вбудованого Gap між комірками (на відміну від CSS Grid). Відступи задаються Margin на дочірньому елементі:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Column="0" Text="Email:" VerticalAlignment="Center"
               Margin="0,0,8,0"/>  <!-- 8px правого відступу між міткою і полем -->
    <TextBox Grid.Column="1" Margin="0,4"/>  <!-- 4px зверху і знизу -->
</Grid>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


RowSpan та ColumnSpan: об'єднання комірок

Grid.RowSpan та Grid.ColumnSpan дозволяють елементу займати кілька суміжних рядків або колонок:

<Button Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="Розтягнута кнопка"/>
<ListBox Grid.Row="0" Grid.Column="0" Grid.RowSpan="3"/>
<Image Grid.Row="1" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="2"/>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...


SharedSizeGroup: синхронізація між Grid-ами

Уявіть список налаштувань де кожен рядок — окремий маленький Grid з міткою і полем вводу. Мітки мають різну ширину (бо Auto), але ви хочете щоб усі вони вирівнялись — як у таблиці.

SharedSizeGroup дозволяє колонкам у різних Grid ділити спільну ширину.

<!-- Grid.IsSharedSizeScope="True" на батьківському контейнері -->
<StackPanel Grid.IsSharedSizeScope="True">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" SharedSizeGroup="LabelColumn"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="0" Text="Ім'я:"/>
        <TextBox   Grid.Column="1"/>
    </Grid>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" SharedSizeGroup="LabelColumn"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <!-- Ця мітка довша — але ширина першої колонки синхронізується між обома Grid -->
        <TextBlock Grid.Column="0" Text="Довга назва поля:"/>
        <TextBox   Grid.Column="1"/>
    </Grid>
</StackPanel>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Grid.IsSharedSizeScope="True" встановлюється на батьківський контейнер (StackPanel, ScrollViewer тощо) — не на самі Grid. Назва SharedSizeGroup — довільний рядок; головне щоб він збігався у всіх колонках що мають синхронізуватися.

Canvas: абсолютне позиціонування

Canvas — це панель де ви самі вказуєте точну позицію кожного елемента у пікселях. Вона принципово відрізняється від усіх попередніх панелей.

<Canvas>
    <Button Canvas.Left="50"  Canvas.Top="20"  Content="Кнопка 1"/>
    <Button Canvas.Left="200" Canvas.Top="80"  Content="Кнопка 2"/>
    <Button Canvas.Right="20" Canvas.Bottom="20" Content="Правий низ"/>
</Canvas>

Чотири Attached Properties для позиціонування:

  • Canvas.Left — відстань від лівого краю Canvas
  • Canvas.Top — відстань від верхнього краю Canvas
  • Canvas.Right — відстань від правого краю (потребує відомих розмірів Canvas)
  • Canvas.Bottom — відстань від нижнього краю

Важливо: Canvas.Left і Canvas.Right не взаємовиключають — якщо задані обидва, а ширина Canvas відома, елемент буде розтягнутий. Зазвичай використовують або Left+Top, або Right+Bottom.

Canvas — антипатерн для звичайних форм

Canvas виглядає привабливо для початківців — "задам координати і готово". Але у реальних застосунках це антипатерн:

  • Не адаптивний: При зміні розміру вікна елементи залишаються на своїх фіксованих координатах, вилазять за межі або перекриваються
  • Не враховує локалізацію: Текст "Зберегти" і "Save" мають різну ширину — кнопка фіксованих розмірів обріже текст
  • Не реагує на системні параметри: Збільшений шрифт у системних налаштуваннях — кнопки залишаться на місці але текст не поміститься
  • Важко підтримувати: Зміна одного елемента вимагає оновлення координат усіх сусідніх вручну

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

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

Canvas є правильним інструментом у специфічних сценаріях:

  • Ігри та анімації: Спрайти рухаються по конкретних координатах — саме те, для чого Canvas
  • Drag-and-Drop: Перетягуваний елемент потрібно позиціонувати в довільній точці
  • Діаграми та схеми: Вузли графа, з'єднувальні лінії, mermaid-подібні схеми
  • Примітивна векторна графіка: Line, Ellipse, Polygon — малювання фігур по координатах
  • Floating UI: Tooltip-и, pop-up меню, overlay-елементи що "плавають" над основним контентом (як у прикладі вище)
<!-- Canvas для простої векторної графіки -->
<Canvas Width="300" Height="200">
    <Line X1="0" Y1="100" X2="300" Y2="100" Stroke="Gray" StrokeThickness="1"/>
    <Line X1="150" Y1="0" X2="150" Y2="200" Stroke="Gray" StrokeThickness="1"/>
    <Ellipse Canvas.Left="50" Canvas.Top="50" Width="80" Height="80"
             Fill="#3b82f6" Opacity="0.7"/>
    <Ellipse Canvas.Left="120" Canvas.Top="70" Width="100" Height="100"
             Fill="#ef4444" Opacity="0.5"/>
</Canvas>

Z-Index у Canvas

Коли елементи перекриваються — Panel.ZIndex визначає хто зверху. Більше значення = поверх:

<Canvas>
    <Rectangle Canvas.Left="20" Canvas.Top="20" Width="100" Height="100"
               Fill="Blue" Panel.ZIndex="1"/>
    <Rectangle Canvas.Left="60" Canvas.Top="60" Width="100" Height="100"
               Fill="Red" Panel.ZIndex="2"/>  <!-- Червоний поверх синього -->
</Canvas>

З-Index також працює в Grid при накладенні елементів.


UniformGrid: рівномірна сітка без зайвої конфігурації

UniformGrid — спрощений варіант Grid де всі комірки однакового розміру. Ніяких RowDefinitions, ніяких Grid.Row атрибутів — елементи заповнюються зліва направо, рядок за рядком автоматично.

<!-- Задаємо тільки кількість рядків або колонок -->
<UniformGrid Rows="2" Columns="3">
    <Button Content="1"/>
    <Button Content="2"/>
    <Button Content="3"/>
    <!-- Автоматично переноситься на рядок 2 -->
    <Button Content="4"/>
    <Button Content="5"/>
    <Button Content="6"/>
</UniformGrid>

Rows і Columns

  • Якщо задати тільки Columns — кількість рядків визначається автоматично по кількості елементів
  • Якщо задати тільки Rows — кількість колонок визначається автоматично
  • Якщо задати обидва — залишкові комірки будуть порожніми
<!-- 3 колонки, кількість рядків = ceil(n / 3) -->
<UniformGrid Columns="3">
    <Button Content="A"/>
    <Button Content="B"/>
    <Button Content="C"/>
    <Button Content="D"/>
    <Button Content="E"/>
    <!-- 5 елементів / 3 = 2 рядки (6 комірок, одна порожня) -->
</UniformGrid>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

UniformGrid vs Grid: коли що обрати

СитуаціяРекомендація
Всі комірки однакового розміруUniformGrid — простіше
Різні розміри рядків/колонокGrid
Auto або * розміриGrid
RowSpan / ColumnSpanGrid
Заповнення по кількостіUniformGrid Columns="N"
SharedSizeGroup між Grid-амиGrid

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

Grid

Найпотужніша панель. Auto/Star/Fixed розміри, RowSpan/ColumnSpan, SharedSizeGroup. 70-80% усіх layout-задач у реальних застосунках. Потребує явного оголошення структури.

Canvas

Для спеціальних задач: ігри, діаграми, drag-and-drop, векторна графіка, floating UI. Антипатерн для звичайних форм і діалогів. Canvas.Left/Top/Right/Bottom.

UniformGrid

Спрощений Grid з рівними комірками. Без RowDefinitions, без Grid.Row атрибутів. Елементи заповнюються автоматично. Rows і/або Columns. Для клавіатур, карток, іконок.

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