Button, Image, ProgressBar та інші базові контроли
Button, RepeatButton, ToggleButton, Image, ProgressBar, Slider, ToolTip, Popup, Pack URI, IsDefault, IsCancel, IsIndeterminate, TickPlacement, IsSnapToTickEnabled, Placement.Бібліотека контролів: перший погляд
Після того як ви опанували систему розташування елементів — Grid, StackPanel, DockPanel та інші панелі — і вже добре розумієте, як будується структура вікна, настав час перейти до наступного шару: самих контролів, тих інтерактивних будівельних блоків, з яких складається будь-який WPF-застосунок.
WPF постачається з багатою бібліотекою стандартних контролів, які разюче відрізняються від того, що міг запропонувати WinForms. Причина цієї відмінності криється у фундаментальному архітектурному рішенні: у WPF контрол — це поведінка, а не зовнішність. Кнопка вміє реагувати на натискання, але те, як вона виглядає — це лише ControlTemplate, який можна замінити повністю. Цей принцип називається Lookless Controls і є одним із найпотужніших аспектів платформи (ControlTemplate і стилізацію ми детально розбиратимемо у Блоці 8).
У цій статті ми зосередимось на базових контролах — тих, що студент зустрічає у перших же своїх застосунках і без яких неможливо уявити жодну форму: кнопки у різних варіаціях, зображення, індикатори прогресу, слайдери та підказки. Ці контроли прості у використанні, але кожен із них має нюанси, які важливо розуміти, щоб не наштовхуватись на несподівані проблеми пізніше.
Button: набагато більше, ніж кнопка
Основна модель взаємодії
Button є, мабуть, найпершим контролом, з яким стикається будь-який розробник GUI. На перший погляд, він здається тривіальним: є кнопка, є подія Click — що тут складного? Але WPF-реалізація Button приховує у собі чимало деталей, які, якщо їх не знати, можуть спантеличити у найнеочікуваніший момент.
Почнемо з найпростішого. Щоб відреагувати на натискання кнопки у code-behind, ми підписуємось на подію Click:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<Button x:Name="greetButton"
Content="Привітати!"
Click="GreetButton_Click"
HorizontalAlignment="Left"
Padding="12,6"/>
<TextBlock x:Name="resultText"
FontSize="16"
Foreground="Gray"
Text="Натисніть кнопку..."/>
</StackPanel>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void GreetButton_Click(object sender, RoutedEventArgs e)
{
resultText.Text = "Привіт, WPF! Кнопку натиснуто.";
}
}
Зверніть увагу на сигнатуру обробника: sender — це об'єкт, що ініціював подію (у цьому випадку сам Button), а RoutedEventArgs — аргументи маршрутизованої події. Клас RoutedEventArgs містить властивість Handled, яка дозволяє зупинити подальше поширення події по дереву елементів. Систему маршрутизованих подій ми детально розглянемо у статті про Routed Events, але вже зараз важливо знати, що Click — це bubbling event: він починається у тому елементі, на якому відбувся клік, і "спливає" вгору по дереву до кореня.
IsDefault та IsCancel: клавіатурна взаємодія
Оскільки WPF орієнтований на створення повноцінних настільних застосунків, надзвичайно важливою є підтримка клавіатурної навігації. Дві властивості Button безпосередньо пов'язані з цим:
IsDefault — коли встановлено в true, кнопка автоматично спрацьовує при натисканні Enter, незалежно від того, який елемент має фокус у цей момент (за умови, що поточний елемент не перехоплює Enter для власних потреб, як це робить, наприклад, TextBox з AcceptsReturn="True"). Таку кнопку прийнято малювати з потовщеною рамкою — це стандартна UX-конвенція, яка сигналізує користувачу: "Enter виконає саме цю дію".
IsCancel — аналогічно, але для клавіші Escape. Кнопка "Скасувати" у діалогових вікнах майже завжди матиме IsCancel="True". Крім того, якщо вікно відкрито через метод ShowDialog(), натискання цієї кнопки автоматично закриває діалог зі значенням DialogResult = false.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<TextBlock Text="Заповніть форму та натисніть Enter або Escape:"
Foreground="Gray"/>
<TextBox Width="240" HorizontalAlignment="Left" Padding="8,4"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="✔ Підтвердити"
IsDefault="True"
Padding="12,6"
Command="{Binding ShowMessageCommand}"
CommandParameter="Форму підтверджено (Enter або клік)!"/>
<Button Content="✖ Скасувати"
IsCancel="True"
Padding="12,6"
Command="{Binding ShowMessageCommand}"
CommandParameter="Скасовано (Escape або клік)."/>
</StackPanel>
</StackPanel>
Content — не лише текст
Одна з найважливіших відмінностей між Button у WPF і кнопкою у WinForms полягає у тому, що властивість Content у WPF може містити будь-який об'єкт — рядок, число, зображення, цілу панель із вкладеними елементами. Це стає можливим завдяки тому, що Button успадковує від ContentControl, а ContentControl відображає своє Content через DataTemplate або напряму, якщо вміст вже є UIElement.
Найпоширеніший практичний приклад — кнопка з іконкою та текстом. У WinForms це потребувало спеціального ImageButton-компонента або хакерських рішень. У WPF — це просто кілька вкладених елементів усередині Button.Content:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<Button HorizontalAlignment="Left" Padding="12,8">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="💾" FontSize="18" VerticalAlignment="Center"/>
<TextBlock Text="Зберегти файл"
FontSize="14"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button HorizontalAlignment="Left" Padding="12,8">
<StackPanel Orientation="Horizontal" Spacing="8">
<Ellipse Width="12" Height="12"
Fill="LimeGreen"
VerticalAlignment="Center"/>
<TextBlock Text="З'єднати"
FontSize="14"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<Button> і </Button> ми розмістили StackPanel, а не Content="..." у вигляді атрибуту. Це можливо завдяки тому, що Content є Content Property для ContentControl — найперший прямий дочірній елемент у XAML автоматично стає значенням Content. Докладніше про Content Property ми говорили у статті про XAML.Важлива деталь: у XAML можна написати Content="Натисни", якщо вміст — рядок, але написати Content="<StackPanel>..." у вигляді атрибута неможливо — значення атрибута є рядком. Для складного вмісту ми або використовуємо дочірній елемент, або <Button.Content> (Property Element Syntax). Також варто знати про Command як альтернативу Click: замість обробника події у code-behind, Button підтримує властивість Command, яка приймає реалізацію ICommand. Це основний механізм у MVVM-архітектурі, і ми повернемося до нього детально у Блоці 7. Поки достатньо знати, що він існує.
RepeatButton: кнопка, що "тисне" безупинно
Коли одного кліку недостатньо
RepeatButton — це спеціалізований нащадок ButtonBase, поведінка якого відрізняється від звичайного Button в одному принциповому аспекті: поки користувач утримує кнопку миші натисненою, контрол безперервно генерує події Click через рівні інтервали часу. Це робить його ідеальним для сценаріїв, де логічно "прокручувати" значення, поки натиснуто: кнопки збільшення/зменшення числового поля, кнопки ручної прокрутки у кастомних скролбарах тощо.
Дві ключові властивості керують цією поведінкою:
250. Це "захист" від випадкових подвійних спрацьовувань — короткий клік не викличе жодного повторення.Click-подіями у "безперервному режимі". За замовчуванням — 250. Чим менше значення — тим швидше відбуваються повтори.Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16" HorizontalAlignment="Left">
<TextBlock Text="Утримуйте кнопки — вони генерують Click безперервно."
Foreground="Gray" FontSize="12"/>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Left">
<RepeatButton Content="▼"
Delay="300"
Interval="80"
Width="48" Height="36"
FontSize="18"
Command="{Binding ShowMessageCommand}"
CommandParameter="Зменшити!"/>
<TextBlock Text="42"
FontSize="32" FontWeight="Bold"
VerticalAlignment="Center"
MinWidth="60"
TextAlignment="Center"/>
<RepeatButton Content="▲"
Delay="300"
Interval="80"
Width="48" Height="36"
FontSize="18"
Command="{Binding ShowMessageCommand}"
CommandParameter="Збільшити!"/>
</StackPanel>
</StackPanel>
RepeatButton — це саме той компонент, зі якого внутрішньо побудований стандартний ScrollBar. Якщо ви колись розглядали ControlTemplate контролу ScrollBar, то знаходили там два RepeatButton-и (для прокрутки вгору та вниз) та Thumb (перетягувальний повзунок). Тепер ви знаєте будівельний матеріал, з якого зроблені ці повсякденні UI-елементи.ToggleButton: кнопка з пам'яттю
Три стани замість двох
ToggleButton — це ще один нащадок ButtonBase, але з принципово іншою моделлю стану. Якщо звичайний Button — це миттєвий імпульс ("натиснули → відбулась дія"), то ToggleButton — це перемикач, який зберігає свій стан між натисканнями. Уявіть кнопку "Напівжирний" у текстовому редакторі: вона або активна (текст жирний), або неактивна. Це — ToggleButton.
Ключова властивість — IsChecked, яка має тип bool? (nullable bool). Це не помилка — ToggleButton підтримує три стани:
Стан IsChecked | Значення | Відповідна подія |
|---|---|---|
true | Кнопку "натиснуто" / увімкнено | Checked |
false | Кнопку "відпущено" / вимкнено | Unchecked |
null | Невизначений (проміжний) стан | Indeterminate |
Для активації тристанового режиму потрібно встановити IsThreeState="True". За замовчуванням ToggleButton перемикається лише між true та false.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12" HorizontalAlignment="Left">
<TextBlock Text="Оберіть режим:"
Foreground="Gray" FontSize="13"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<ToggleButton Content="🌙 Темна тема"
Padding="12,6"
Command="{Binding ShowMessageCommand}"
CommandParameter="Тему перемкнуто!"/>
<ToggleButton Content="🔔 Сповіщення"
IsChecked="True"
Padding="12,6"/>
<ToggleButton Content="📌 Закріпити"
IsThreeState="True"
Padding="12,6"/>
</StackPanel>
<TextBlock Text="Третя кнопка має три стани — клікайте кілька разів."
Foreground="Gray" FontSize="12"/>
</StackPanel>
ToggleButton є базовим класом для CheckBox та RadioButton. Ці два контроли, які ми розглянемо у наступних статтях, насправді наслідують від ToggleButton і лише додають власну угоду щодо зовнішнього вигляду та логіки групування. Знаючи ToggleButton — ви вже розумієте фундамент обох.Image: відображення зображень у WPF
Чому Image у WPF — це окрема тема
Якщо ви мали досвід роботи з Windows Forms, то, мабуть, пам'ятаєте, що відображення зображення там зводилося до двох рядків: встановлення властивості Image у компонент PictureBox і вказівка шляху до файлу. WPF виглядає схожим за результатом, але принципово відрізняється за механізмом роботи. Щоб правильно використовувати Image у WPF, потрібно розуміти дві ключові концепції: як WPF посилається на ресурси всередині застосунку (система Pack URIs) та як WPF вписує зображення у відведений простір (режими Stretch). Без розуміння цих двох речей ви неодноразово стикатиметесь із класичними помилками: зображення або не завантажується (неправильний URI), або виглядає розтягнутим та спотвореним (неправильний Stretch).
Як додати зображення до проєкту WPF
Перш ніж щось відобразити, потрібно правильно включити графічний файл до складу проєкту. WPF-застосунок — це, по суті, ZIP-архів (.exe або .dll), і всі ресурси, до яких він звертається, мають бути або вбудовані у цей архів, або доступні за абсолютним шляхом файлової системи. Перший підхід значно надійніший і є рекомендованим для більшості сценаріїв.
Процедура підключення зображення виглядає так:
Крок 1: Розмістіть файл у проєкті
Додайте файл зображення до папки у вашому проєкті (наприклад, Assets/Images/logo.png). Це можна зробити через меню Project → Add Existing Item... або простим перетягуванням файлу у Solution Explorer.
Крок 2: Встановіть Build Action = Resource
У вікні властивостей файлу (клавіша F4 або правою кнопкою → Properties) знайдіть властивість Build Action і встановіть значення Resource. Саме ця настройка інструктує MSBuild вбудувати файл у зкомпільований збірник (assembly) як binary resource.
Крок 3: Copy to Output Directory = Do not copy
Властивість Copy to Output Directory слід залишити значенням Do not copy. Зображення вже "живе" всередині збірника — немає потреби копіювати його ще й поруч із .exe.
.exe більше немає.Pack URIs: адресація ресурсів у WPF
Система Pack URIs — це стандарт адресації ресурсів, який WPF успадкував від специфікації XPS (XML Paper Specification). Pack URI виглядає незвично і на перший погляд справляє враження надлишково складного синтаксису, але він вирішує реальну проблему: однозначна адресація ресурсу незалежно від того, де він фізично знаходиться — вбудований у поточну збірку, чи розміщений у зовнішній сателітній збірці.
Найпоширеніша форма Pack URI для ресурсів поточної збірки виглядає так:
pack://application:,,,/Assets/Images/logo.png
Розберемо цей URI по частинах:
| Частина | Значення |
|---|---|
pack:// | Схема URI, специфічна для WPF |
application:,,, | Означає "поточна збірка застосунку" (три коми — це закодовані слеші ///) |
/Assets/Images/logo.png | Шлях до ресурсу всередині збірки, відносно кореня проєкту |
/Assets/Images/logo.png або навіть Assets/Images/logo.png. WPF автоматично розгортає його до повного Pack URI. Повний запис pack://application:,,,/... потрібен лише тоді, коли ви формуєте URI у C#-коді або посилаєтесь на ресурс з іншої збірки.Подивимось на усі варіанти адресації у порівняльній таблиці:
| Тип джерела | Приклад URI | Коли використовувати |
|---|---|---|
| Ресурс поточної збірки (XAML) | Source="/Assets/logo.png" | У більшості XAML-випадків |
| Ресурс поточної збірки (C#) | new Uri("pack://application:,,,/Assets/logo.png") | Динамічне завантаження у коді |
| Ресурс іншої збірки | pack://application:,,,/MyLib;component/Images/icon.png | Реюзабельні бібліотеки контролів |
| Абсолютний шлях файлової системи | Source="C:/Users/user/Pictures/photo.jpg" | Зовнішні файли (наприклад, вибрані користувачем) |
| URL зображення в Інтернеті | Source="https://picsum.photos/320/200" | Онлайн-ресурси (асинхронне завантаження) |
Властивість Source та ImageSource
Центральна властивість контролу Image — це Source, що має тип ImageSource. BitmapImage — найпоширеніша реалізація ImageSource для растрових зображень (PNG, JPEG, BMP, GIF). У XAML перетворення рядка (Pack URI) на BitmapImage відбувається автоматично завдяки вбудованому TypeConverter. У C#-коді доведеться створити об'єкт вручну:
// Завантаження зображення з ресурсів у коді
var image = new Image();
image.Source = new BitmapImage(
new Uri("pack://application:,,,/Assets/Images/logo.png")
);
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<TextBlock Text="Аватар користувача (зовнішній URL):"
Foreground="Gray" FontSize="13"/>
<Image Source="https://dummyimage.com/640x360/000/fff.png&text=Image"
Width="120" Height="120"
RenderOptions.BitmapScalingMode="HighQuality"/>
<TextBlock Text="RenderOptions.BitmapScalingMode=HighQuality"
Foreground="Gray" FontSize="11"/>
</StackPanel>
RenderOptions.BitmapScalingMode — це прикріплена властивість (Attached Property), що керує алгоритмом масштабування растрового зображення при відображенні у розмірах, відмінних від оригінальних. Значення HighQuality (або Fant) дає найкращу якість, але коштує трохи продуктивності; LowQuality (або NearestNeighbor) — максимально швидке, але "пікселізоване" масштабування. Детальніше про Attached Properties — у Блоці 5.Режими Stretch: як WPF вписує зображення у контейнер
Уявіть, що у вас є зображення розміром 800×600 пікселів, а Image-контрол займає лише 200×200 логічних одиниць. Що має зробити WPF? Залишити зображення оригінального розміру і обрізати зайве? Стиснути пропорційно? Стиснути з порушенням пропорцій, аби заповнити весь простір? Саме ці сценарії регулює властивість Stretch — один із найбільш нерозуміних аспектів Image у початківців.
Можливих значень чотири, і їх варто знати напам'ять:
None
Fill
Image-контролу стає шириною зображення, висота — висотою. Якщо пропорції розрізняються — зображення буде спотворено. Майже ніколи не є правильним вибором для фотографій і логотипів.Uniform (за замовчуванням)
UniformToFill
Щоб різниця між режимами стала наочною, розглянемо кожен у живому прикладі. Зображення оригінального розміру — широке (landscape), контрол — квадратний. Це найкращий тест для спостереження поведінки Stretch:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="8">
<TextBlock Text="Stretch.None" FontWeight="Bold"/>
<TextBlock Text="Зображення відображається у оригінальному розмірі. Якщо воно більше за контрол — обрізається."
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
<Border Width="160" Height="160" Background="#1A1A2E"
BorderBrush="#3A3A5C" BorderThickness="1">
<Image Source="https://dummyimage.com/640x360/000/fff.png&text=Stretch.Fill"
Stretch="None"/>
</Border>
</StackPanel>
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="8">
<TextBlock Text="Stretch.Fill" FontWeight="Bold"/>
<TextBlock Text="Зображення розтягується на весь контрол. Пропорції порушуються — зображення спотворюється."
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
<Border Width="160" Height="160" Background="#1A1A2E"
BorderBrush="#3A3A5C" BorderThickness="1">
<Image Source="https://dummyimage.com/640x360/000/fff.png&text=Stretch.Uniform"
Stretch="Fill"/>
</Border>
</StackPanel>
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="8">
<TextBlock Text="Stretch.Uniform (за замовчуванням)" FontWeight="Bold"/>
<TextBlock Text="Зображення вміщується повністю, пропорції збережено. Можливі порожні смуги збоку або зверху."
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
<Border Width="160" Height="160" Background="#1A1A2E"
BorderBrush="#3A3A5C" BorderThickness="1">
<Image Source="https://dummyimage.com/640x360/000/fff.png&text=Stretch.UniformToFill"
Stretch="Uniform"/>
</Border>
</StackPanel>
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="8">
<TextBlock Text="Stretch.UniformToFill" FontWeight="Bold"/>
<TextBlock Text="Зображення заповнює весь контрол, пропорції збережено. Частина зображення обрізається."
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
<Border Width="160" Height="160" Background="#1A1A2E"
BorderBrush="#3A3A5C" BorderThickness="1">
<Image Source="https://dummyimage.com/640x360/000/fff.png&text=Stretch.None"
Stretch="UniformToFill"/>
</Border>
</StackPanel>
Stretch="UniformToFill" у поєднанні з ClipToBounds="True" на Border-контейнері. ClipToBounds гарантує, що частини зображення, які виходять за межі Border, не відображатимуться навіть якщо Image за розміром більший.Для зручного запам'ятовування — зведена таблиця:
| Stretch | Пропорції | Заповнює весь простір | Може обрізати | Типовий use case |
|---|---|---|---|---|
None | ✅ Зберігає | ❌ Ні | ✅ Так (якщо велике) | Піксель-перфектні іконки |
Fill | ❌ Порушує | ✅ Так | ❌ Ні | Майже ніколи |
Uniform | ✅ Зберігає | ❌ Ні (смуги) | ❌ Ні | Загальне відображення |
UniformToFill | ✅ Зберігає | ✅ Так | ✅ Так (краї) | Аватари, обкладинки |
Stretch ідентична — це властивість самого Image-контролу, незалежна від теми. Зовнішній вигляд Border (рамки контейнера) може незначно відрізнятись.ProgressBar: індикатор виконання операцій
Роль індикатора прогресу у користувацькому досвіді
Уявіть застосунок, який завантажує великий файл, виконує складний обрахунок або синхронізується з сервером. Що відбувається з інтерфейсом у цей час? Якщо розробник нічого не передбачив — вікно "завмирає", перестає реагувати на дії користувача, і той бачить лише нерухомий екран. Без жодного сигналу про те, що відбувається і скільки це триватиме. Подібний досвід підриває довіру до застосунку та змушує думати про "зависання".
ProgressBar — це саме той контрол, який перетворює невизначеність очікування на зрозумілий, вимірюваний процес. Навіть якщо реальний прогрес виміряти неможливо (наприклад, невідомо, скільки чіпів обробить алгоритм — скористайтеся режимом IsIndeterminate), сам факт анімації повідомляє: "застосунок живий, він працює, зачекайте".
З точки зору архітектури WPF, ProgressBar успадковує від RangeBase — базового класу, що визначає концепцію "значення у діапазоні". Той самий клас є предком Slider та ScrollBar. Це елегантне рішення: незважаючи на зовнішню несхожість, всі три контроли поділяють спільний контракт: є мінімум, є максимум, є поточне значення.
Ключові властивості
Minimum, Maximum. За замовчуванням — 0. Найчастіше прив'язується через {Binding} до властивості ViewModel або встановлюється у code-behind під час виконання тривалої операції.0. Звичайно, залишається нульовим, але може бути будь-яким числом. Наприклад, Minimum="-100" і Maximum="100" для індикатора, що показує відхилення від нейтрального стану.100. Встановлюйте значення, що відповідає загальному обсягу роботи: кількості файлів, кількості записів, загальному розміру у байтах тощо.true — контрол переходить у режим невизначеного прогресу: замість фіксованої смуги показується "пульсуюча" анімація ("серпантин"). Властивості Value, Minimum, Maximum у цьому режимі ігноруються. За замовчуванням — false.Horizontal (за замовчуванням, ліворуч направо) або Vertical (знизу вгору). Вертикальний ProgressBar використовується рідко, але буває зручним для відображення рівнів (наприклад, рівень "заряду" батареї або гучності звуку).Визначений прогрес: класичний ProgressBar
Найпоширеніший сценарій — показ прогресу операції з відомим загальним обсягом. Наприклад, завантаження 150 файлів: Minimum=0, Maximum=150, Value — поточна кількість завантажених файлів.
У реальних застосунках Value рідко встановлюється статично у XAML — зазвичай відбувається прив'язка до властивості ViewModel, яка оновлюється асинхронно. Але для розуміння роботи контролу виведемо статичний приклад:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Стан 1: Початок операції (0%)" Foreground="Gray" FontSize="12"/>
<ProgressBar Minimum="0" Maximum="100" Value="0" Height="8"/>
<TextBlock Text="Стан 2: Частковий прогрес (35%)" Foreground="Gray" FontSize="12"/>
<ProgressBar Minimum="0" Maximum="100" Value="35" Height="8"/>
<TextBlock Text="Стан 3: Майже завершено (80%)" Foreground="Gray" FontSize="12"/>
<ProgressBar Minimum="0" Maximum="100" Value="80" Height="8"/>
<TextBlock Text="Стан 4: Завершено (100%)" Foreground="#4CAF50" FontSize="12"/>
<ProgressBar Minimum="0" Maximum="100" Value="100" Height="8"/>
</StackPanel>
Зверніть увагу на кілька деталей. По-перше, Height="8" — власний розмір ProgressBar доволі великий (близько 20 логічних одиниць за замовчуванням), тому для "тонкої" смуги прогресу потрібно явно задавати висоту. По-друге, властивість Value у межах Minimum, Maximum ніколи не потрібно нормалізовувати вручну — WPF сам обраховує відсоткове заповнення. Якщо Minimum=0, Maximum=500, Value=250 — смуга заповниться рівно вдвічі.
Невизначений прогрес: IsIndeterminate
Іноді заздалегідь неможливо знати, яку частку роботи вже виконано. Підключення до бази даних, очікування відповіді сервера, перший запуск алгоритму з невідомою складністю — це типові сценарії для IsIndeterminate="True". У цьому режимі ProgressBar показує анімацію, що безперервно рухається від краю до краю (або пульсуючу підсвітку у Fluent Theme), сигналізуючи: "застосунок зайнятий, але ми не знаємо коли закінчимо".
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Визначений прогрес (завjs 60%):" Foreground="Gray" FontSize="12"/>
<ProgressBar Minimum="0" Maximum="100" Value="60" Height="10"/>
<TextBlock Text="Невизначений прогрес (IsIndeterminate=True):"
Foreground="Gray" FontSize="12"/>
<ProgressBar IsIndeterminate="True" Height="10"/>
<TextBlock Text="Вертикальний (Orientation=Vertical, 70%):"
Foreground="Gray" FontSize="12"/>
<ProgressBar Minimum="0" Maximum="100" Value="70"
Orientation="Vertical"
Width="20" Height="100"
HorizontalAlignment="Left"/>
</StackPanel>
IsIndeterminate може виглядати інакше, ніж у реальному WPF — Avalonia використовує "pulse" (пульсуючий блок), тоді як стандартний WPF показує рухому смугу типу "серпантин". У WPF з підключеними бібліотеками тем (наприклад, MahApps.Metro або ModernWPF) анімація також відрізняється. Поведінковий контракт — однаковий.Типові помилки при роботі з ProgressBar
Попри очевидну простоту ProgressBar, є кілька поширених помилок, на які варто звернути увагу ще до того, як ви з ними зіткнетесь на практиці.
Помилка 1: оновлення Value у головному потоці під час тривалої операції. Якщо ви запускаєте важку обробку безпосередньо у обробнику події Button_Click, не в Task.Run, то і UI-потік зайнятий, і ProgressBar не встигає перерисуватись — ви побачите або "заморожений" прогрес, або взагалі нічого. Правильне рішення — виконувати важку роботу у фоновому потоці (async/await + Task.Run), а оновлення Value — через Dispatcher.InvokeAsync або через прив'язку даних до ViewModel.
Помилка 2: встановлення Value поза межами Minimum, Maximum. Якщо Value > Maximum — WPF автоматично "зафіксує" значення на Maximum, прогресбар буде показувати 100%, але виключення кинуто не буде. Якщо Value < Minimum — аналогічно, фіксується на Minimum. Це може замаскувати логічні помилки у коді підрахунку прогресу.
Помилка 3: недострілений розмір. ProgressBar за замовчуванням розтягується на всю ширину батьківського контейнера (HorizontalAlignment="Stretch"). У StackPanel зі скромними розмірами це призводить до несподівано вузького прогрес-бару. Встановіть явний Width або розташуйте ProgressBar у Grid-ячейці.
Slider: повзунок для вибору числового значення
Концептуальна модель Slider
Slider — це контрол, що дозволяє користувачу вибрати числове значення у заданому діапазоні шляхом перетягування повзунка (thumb) уздовж доріжки (track). Подібно до ProgressBar, він успадковує від RangeBase, тому логіка Minimum, Maximum і Value тотожня. Різниця принципова в одному: ProgressBar — пасивний індикатор (не передбачає взаємодії), а Slider — активний елемент введення, що реагує на дії миші та клавіатури.
Аналогія з реального світу: регулятор гучності на підсилювачі або "крутилка" яскравості монітора. Ви бачите поточне положення та можете плавно змінювати значення у визначених межах. Саме такий UX-паттерн реалізує Slider у WPF.
З точки зору взаємодії Slider підтримує кілька способів зміни значення:
- Перетягування thumb мишею — зміна на будь-яку величину у межах діапазону.
- Клік на доріжку — стрибок на значення
LargeChangeу відповідному напрямку. - Клавіші стрілок ← → — зміна на значення
SmallChange. - Клавіші Page Up / Page Down — зміна на значення
LargeChange. - Клавіші Home / End — стрибок до
Minimum/Maximum.
Ключові властивості Slider
Minimum=0, Maximum=100, TickFrequency=10 — мітки будуть на позиціях 0, 10, 20, ... 100. За замовчуванням — 1. Мітки відображаються лише якщо TickPlacement ≠ None.None (не відображати, за замовчуванням), TopLeft (зверху для горизонтального / ліворуч для вертикального), BottomRight (знизу / праворуч), Both (з обох боків).true — thumb "прилипає" до найближчої мітки при перетягуванні. Значення Value буде завжди кратне TickFrequency, ніколи не буде "між мітками". Зручно для налаштувань з дискретними значеннями (наприклад, вибір роздільної здатності або розміру шрифту).0.1. Для цілочисельних Slider рекомендується встановити 1.1. Зазвичай у 10 разів більше за SmallChange.Horizontal (за замовчуванням) або Vertical. Для вертикального значення зростають знизу вгору.Базовий Slider без прив'язки
Розпочнемо з найпростішого прикладу — горизонтального Slider з відображенням міток:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="20">
<TextBlock Text="Основний Slider з мітками:" Foreground="Gray" FontSize="13"/>
<Slider Minimum="0" Maximum="100"
Value="40"
TickFrequency="10"
TickPlacement="BottomRight"
SmallChange="1"
LargeChange="10"
Width="300"
HorizontalAlignment="Left"/>
<TextBlock Text="IsSnapToTickEnabled=True (кроки по 25):" Foreground="Gray" FontSize="13"/>
<Slider Minimum="0" Maximum="100"
Value="50"
TickFrequency="25"
TickPlacement="Both"
IsSnapToTickEnabled="True"
SmallChange="25"
LargeChange="25"
Width="300"
HorizontalAlignment="Left"/>
<TextBlock Text="Вертикальний Slider:" Foreground="Gray" FontSize="13"/>
<Slider Minimum="0" Maximum="100"
Value="65"
Orientation="Vertical"
TickFrequency="20"
TickPlacement="BottomRight"
Height="150"
HorizontalAlignment="Left"/>
</StackPanel>
Прив'язка Slider до TextBlock через ElementName
Одна з найнаочніших демонстрацій прив'язки даних у WPF — синхронізація Slider з TextBlock без жодного рядка C#-коду. Ми вже познайомились із {Binding ElementName} у статті про Markup Extensions — тепер застосуємо його на практиці.
Ідея проста: TextBlock.Text прив'язується до властивості Value іменованого Slider. Коли користувач переміщує повзун — TextBlock миттєво оновлюється. Це двонаправлена синхронізація лише через XAML, без code-behind:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Оберіть розмір шрифту:" Foreground="Gray" FontSize="13"/>
<Slider x:Name="fontSizeSlider"
Minimum="8" Maximum="48"
Value="16"
TickFrequency="4"
TickPlacement="BottomRight"
IsSnapToTickEnabled="True"
SmallChange="4"
LargeChange="8"
Width="300"
HorizontalAlignment="Left"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Поточний розмір: " Foreground="Gray"/>
<TextBlock Text="{Binding ElementName=fontSizeSlider, Path=Value}"
FontWeight="Bold"/>
<TextBlock Text="px" Foreground="Gray"/>
</StackPanel>
<TextBlock Text="Зразок тексту"
FontSize="{Binding ElementName=fontSizeSlider, Path=Value}"
FontWeight="Medium"
Foreground="#6366F1"/>
</StackPanel>
Зверніть на ключові деталі цього прикладу. x:Name="fontSizeSlider" — присвоює іменований ідентифікатор Slider-у в межах поточного XAML-дерева. Саме за цим ім'ям {Binding ElementName=fontSizeSlider, Path=Value} знаходить джерело прив'язки. Прив'язка FontSize до Value (double до double) не потребує жодного конвертера — типи збігаються. А ось прив'язка Text до Value неявно застосовує стандартний ToString(), який перетворює double на рядок. Це автоматично.
Slider.Value зберігає значення типу double з десятковою частиною (наприклад, 16.0). Щоб TextBlock показував 16 замість 16,0 — скористайтесь StringFormat:Text="{Binding ElementName=fontSizeSlider, Path=Value, StringFormat={}{0:F0}}"
{0:F0} — формат з нульовою кількістю знаків після крапки. Або використовуйте IsSnapToTickEnabled="True" — тоді значення завжди буде цілим згідно з TickFrequency.TickFrequency, TickPlacement, IsSnapToTickEnabled, ElementName-прив'язка) — ідентичні в обох фреймворках.ToolTip: спливаючі підказки
Навіщо потрібні ToolTip і коли вони доречні
Будь-який добре спроєктований інтерфейс передусім є зрозумілим. Але зрозумілість і стислість — це суперечливі вимоги: якщо пояснювати кожну кнопку написом, інтерфейс стане захаращеним; якщо нічого не пояснювати — незрозумілим. ToolTip вирішує цю дилему елегантно: він показує пояснення лише тоді, коли користувач затримує курсор миші над елементом — тобто тоді, коли він сам сигналізує про зацікавленість.
У WPF ToolTip — це не просто властивість рядка, а повноцінний контрол. Він успадковує від ContentControl, а отже, як і Button, може містити будь-який XAML-вміст. Це відкриває сценарії, недосяжні у WinForms: ToolTip із зображенням, ключами клавіатури, форматованим текстом або навіть цілою мініатюрою форми.
Простий текстовий ToolTip
Найпростіший спосіб додати підказку — встановити рядок у властивість ToolTip будь-якого елемента. WPF автоматично обгортає його у відповідний контрол:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Наведіть курсор на кнопки, щоб побачити підказки:"
Foreground="Gray" FontSize="13"/>
<WrapPanel ItemSpacing="8">
<Button Content="💾 Зберегти"
Padding="12 6"
ToolTip="Зберегти поточний документ (Ctrl+S)"/>
<Button Content="📂 Відкрити"
Padding="12 6"
ToolTip="Відкрити файл з диску (Ctrl+O)"/>
<Button Content="🖨 Друк"
Padding="12 6"
ToolTip="Надіслати документ на принтер (Ctrl+P)"/>
</WrapPanel>
</StackPanel>
Зверніть: ToolTip="..." — це скорочений запис. WPF за лаштунками перетворює рядок на об'єкт ToolTip з Content = "...". Додаткові параметри (затримка показу, тривалість показу) можна регулювати через прикріплені властивості класу ToolTipService:
Властивість ToolTipService | За замовч. | Опис |
|---|---|---|
InitialShowDelay | 400 мс | Час від наведення курсору до появи підказки |
ShowDuration | 5000 мс | Час відображення підказки |
BetweenShowDelay | 100 мс | Затримка між закриттям одного та відкриттям іншого ToolTip |
Placement | Mouse | Відносно чого розміщувати (аналогічно до Popup.Placement) |
Складний структурований ToolTip
Коли простого рядка недостатньо — замінюємо атрибут ToolTip="..." на елемент <X.ToolTip><ToolTip>...</ToolTip></X.ToolTip> і всередині розміщуємо довільний XAML. Це "Typed ToolTip" або "Structured ToolTip" — стандартний підхід у програмних продуктах:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Наведіть курсор на кнопку для складної підказки:"
Foreground="Gray" FontSize="13"/>
<Button Content="⚙ Параметри" Padding="12,8" HorizontalAlignment="Left">
<Button.ToolTip>
<ToolTip MaxWidth="280">
<StackPanel Spacing="6">
<TextBlock Text="Параметри застосунку"
FontWeight="Bold" FontSize="13"/>
<Separator/>
<TextBlock Text="Відкриває вікно з налаштуваннями виду, мови та з'єднання з базою даних."
TextWrapping="Wrap"
Foreground="Gray" FontSize="12"/>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="Клавіша:" Foreground="Gray" FontSize="11"/>
<Border Background="#3C3C3C" CornerRadius="3" Padding="4,1">
<TextBlock Text="Ctrl+," FontSize="11" Foreground="#E0E0E0"/>
</Border>
</StackPanel>
</StackPanel>
</ToolTip>
</Button.ToolTip>
</Button>
</StackPanel>
Popup: програмно керований спливаючий шар
Popup проти ToolTip: у чому різниця
ToolTip і Popup на перший погляд схожі — обидва показують щось "поверх" основного інтерфейсу. Але їхня природа і призначення кардинально різняться.
ToolTip — суто реактивний: він з'являється автоматично при наведенні курсору і зникає без будь-якого втручання коду. Його поведінкою керує WPF та ToolTipService. Програмного контролю над ToolTip практично немає.
Popup — це будівельний примітив для спливаючих шарів будь-якого типу: випадаючі меню, контекстні панелі, спливаючі форми, нотифікації. Його поява та зникнення повністю підконтрольні коду: властивість IsOpen = true/false вмикає і вимикає Popup. Саме з Popup внутрішньо побудовані ComboBox-список, ContextMenu, Menu-підменю та сам ToolTip.
true — Popup відображається. false — прихований. Встановлюється програмно або через прив'язку даних. За замовчуванням — false.Bottom (під PlacementTarget), Top (над), Mouse (біля курсору), Center (по центру target), Absolute (у конкретних координатах екрана).Placement не Mouse і не Absolute). За замовчуванням — батьківський елемент у дереві.false (за замовчуванням) — Popup автоматично закривається при кліку за його межами. Якщо true — закривається лише програмно. Використовуйте StaysOpen="True" для Popup-форм, що потребують явного підтвердження.Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<TextBlock Text="Натисніть кнопку, щоб відкрити Popup:"
Foreground="Gray" FontSize="13"/>
<Button x:Name="popupToggleBtn"
Content="📋 Показати деталі"
Padding="12,6"
HorizontalAlignment="Left"
Command="{Binding ShowMessageCommand}"
CommandParameter="Popup відкрито!"/>
<Popup x:Name="infoPopup"
PlacementTarget="{Binding ElementName=popupToggleBtn}"
Placement="Bottom"
StaysOpen="False"
IsOpen="False">
<Border Background="#1E1E2E"
BorderBrush="#6366F1"
BorderThickness="1"
CornerRadius="8"
Padding="16">
<StackPanel Spacing="8" Width="220">
<TextBlock Text="Детальна інформація"
FontWeight="Bold" FontSize="13"
Foreground="White"/>
<TextBlock Text="Цей Popup з'являється безпосередньо під кнопкою. Клікніть будь-де поза ним — він закриється автоматично (StaysOpen=False)."
TextWrapping="Wrap"
FontSize="11"
Foreground="#A0A0C0"/>
</StackPanel>
</Border>
</Popup>
</StackPanel>
// У реальному WPF: відкриваємо Popup у обробнику Click кнопки
private void PopupToggleBtn_Click(object sender, RoutedEventArgs e)
{
infoPopup.IsOpen = !infoPopup.IsOpen; // Перемикаємо стан
}
Popup не відображається автоматично, оскільки Click="..." не виконується у WASM-середовищі (лише Command). У реальному WPF встановлення IsOpen="True" безпосередньо у XAML одразу відображає Popup — спробуйте змінити значення атрибуту, щоб переконатися. Керування через код — єдиний повноцінний спосіб взаємодії з Popup у реальних застосунках.Popup живе поза межами логічного дерева вікна. Він рендерується у окремому нативному вікні (HWND) поверх усього іншого. Це означає: Popup не успадковує DataContext від батьківського вікна автоматично — потрібно явно передати його через DataContext="{Binding ElementName=mainWindow, Path=DataContext}". Також Popup не обрізається межами вікна і може виходити за його краї на відміну від звичайних панелей.Практичні завдання
Нижче — три завдання різного рівня складності, що охоплюють матеріал цієї статті. Рекомендується виконувати їх послідовно: кожне наступне спирається на навички, відпрацьовані у попередньому.
Ціль: Закріпити розуміння Button.Content як довільного XAML-вмісту.
Завдання: Створіть панель інструментів зі щонайменше чотирма кнопками. Кожна кнопка повинна містити іконку (емодзі або Path-геометрію) та текстовий підпис, розташовані горизонтально через StackPanel. Кожна кнопка має ToolTip із коротким описом дії та клавіатурним скороченням.
Вимоги до реалізації:
- Кнопки: "Новий файл", "Відкрити", "Зберегти", "Видалити".
- Одна кнопка —
IsDefault="True"(Зберегти). - Одна кнопка —
IsCancel="True"(Скасувати/Закрити). - Використовувати
StackPanel Orientation="Horizontal"для іконки та тексту всередині кнопки. - Усі кнопки — у
WrapPanelабо горизонтальномуStackPanelз зовнішнього боку.
Що перевірити: Натисніть Enter — спрацьовує "Зберегти". Натисніть Escape — спрацьовує "Скасувати".
Ціль: Поєднати Image, ProgressBar та Slider в єдиному інтерактивному рішенні.
Завдання: Реалізуйте мінімалістичний переглядач зображень із симуляцією завантаження.
- Підготуйте 3–5 зображень (можна використовувати URL з
picsum.photos). Відображайте їх уImageзStretch="UniformToFill"у квадратномуBorder. - Додайте
ProgressBarпід зображенням. Зв'яжітьProgressBar.ValueзSlider.Valueчерез{Binding ElementName}— переміщення Slider вручну симулює прогрес завантаження. - Поруч з
ProgressBarдодайтеTextBlockз відсотком (прив'язка черезStringFormat={}{0:F0}%). - Додайте кнопки "Попереднє" та "Наступне" для перемикання між зображеннями у code-behind.
Ціль: Застосувати Popup для реалізації нетривіальної UI-взаємодії.
Завдання: Створіть список елементів, де при натисканні кнопки "ℹ Деталі" поруч з кожним рядком відкривається Popup із розгорнутою інформацією.
Технічні вимоги:
- Список з щонайменше 4 елементами; кожен елемент —
Borderз ім'ям і кнопкою "ℹ Деталі". - При натисканні "ℹ Деталі" — відкривається
PopupзPlacement="Right", прив'язаний до кнопки черезPlacementTarget. - Вміст
Popup: заголовок, опис,ProgressBar(наприклад, "Наявність: 75%"). StaysOpen="False"— Popup закривається при кліку поза ним.- При відкритті другого Popup перший автоматично закривається (відстежуйте
IsOpenпоточного у code-behind).
Ускладнення (необов'язково): замість кнопки "Деталі" — реагувати на MouseEnter елемента, відкриваючи Popup з невеликою затримкою через DispatcherTimer.
Підсумок
Що ми вивчили у цій статті
У цій статті ми пройшли шлях від найпростіших кнопок до програмно керованих спливаючих шарів. Узагальнимо ключові висновки.
Button, RepeatButton, ToggleButton формують сімейство кнопок у WPF. Усі успадковують від ButtonBase, але відрізняються семантикою: Button — миттєвий імпульс; RepeatButton — безперервний потік подій; ToggleButton — бінарний (або тристановий) перемикач зі збереженням стану. Властивість Content може містити довільний XAML — це принципово відрізняє WPF від WinForms.
Image потребує розуміння двох речей: Pack URI для адресації ресурсів (правило Build Action = Resource) та режимів Stretch (None, Fill, Uniform, UniformToFill). Для аватарів і банерів — UniformToFill; для загального перегляду — Uniform.
ProgressBar успадковує від RangeBase і ділить концепцію Minimum/Maximum/Value із Slider та ScrollBar. Режим IsIndeterminate перетворює його на аніматований індикатор зайнятості. Головна пастка — оновлення Value безпосередньо у головному потоці блокує перерисовку.
Slider — активний аналог ProgressBar. Властивості TickFrequency, TickPlacement та IsSnapToTickEnabled обмежують вибір дискретними значеннями. Прив'язка через {Binding ElementName} — одна з найнаочніших демонстрацій потужності WPF Binding без жодного рядка C#.
ToolTip — спливаюча підказка, що може бути як простим рядком, так і складним XAML-вмістом. Поведінку (затримки, розташування) регулює ToolTipService.
Popup — примітив спливаючих шарів, поверх якого побудовано ComboBox, ContextMenu та сам ToolTip. Живе поза деревом вікна — DataContext треба передавати явно. Керується через IsOpen.
Що далі
У наступній статті ми розглянемо текстові контроли — TextBox, PasswordBox та RichTextBox. Ці контроли мають значно глибшу модель введення, аніж здається на перший погляд: підтримка undo/redo, виділення тексту, форматування та події зміни. Окрема тема — FlowDocument та RichTextBox, специфічні для WPF і не мають прямого аналога в Avalonia.
Layout в Avalonia: відмінності та нові можливості
Чим Avalonia відрізняється від WPF у layout: Spacing, RelativePanel, SplitView, Expander, ItemsRepeater, Transitions та інші можливості що роблять Avalonia потужнішим у сучасних сценаріях.
Контроли в Avalonia: відмінності від WPF
Детальний порівняльний огляд стандартних контролів Avalonia та WPF: що повністю збігається, що відрізняється синтаксисом, що є в Avalonia але відсутнє у WPF — і навпаки. Практичний гід для тих, хто знає WPF і переходить на Avalonia.