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 є, мабуть, найпершим контролом, з яким стикається будь-який розробник 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: він починається у тому елементі, на якому відбувся клік, і "спливає" вгору по дереву до кореня.
Оскільки 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>
Одна з найважливіших відмінностей між 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 — це спеціалізований нащадок 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 — це ще один нащадок 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 — ви вже розумієте фундамент обох.Якщо ви мали досвід роботи з Windows Forms, то, мабуть, пам'ятаєте, що відображення зображення там зводилося до двох рядків: встановлення властивості Image у компонент PictureBox і вказівка шляху до файлу. WPF виглядає схожим за результатом, але принципово відрізняється за механізмом роботи. Щоб правильно використовувати Image у WPF, потрібно розуміти дві ключові концепції: як WPF посилається на ресурси всередині застосунку (система Pack URIs) та як WPF вписує зображення у відведений простір (режими Stretch). Без розуміння цих двох речей ви неодноразово стикатиметесь із класичними помилками: зображення або не завантажується (неправильний URI), або виглядає розтягнутим та спотвореним (неправильний Stretch).
Перш ніж щось відобразити, потрібно правильно включити графічний файл до складу проєкту. WPF-застосунок — це, по суті, ZIP-архів (.exe або .dll), і всі ресурси, до яких він звертається, мають бути або вбудовані у цей архів, або доступні за абсолютним шляхом файлової системи. Перший підхід значно надійніший і є рекомендованим для більшості сценаріїв.
Процедура підключення зображення виглядає так:
Додайте файл зображення до папки у вашому проєкті (наприклад, Assets/Images/logo.png). Це можна зробити через меню Project → Add Existing Item... або простим перетягуванням файлу у Solution Explorer.
У вікні властивостей файлу (клавіша F4 або правою кнопкою → Properties) знайдіть властивість Build Action і встановіть значення Resource. Саме ця настройка інструктує MSBuild вбудувати файл у зкомпільований збірник (assembly) як binary resource.
Властивість Copy to Output Directory слід залишити значенням Do not copy. Зображення вже "живе" всередині збірника — немає потреби копіювати його ще й поруч із .exe.
.exe більше немає.Система 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" | Онлайн-ресурси (асинхронне завантаження) |
Центральна властивість контролу 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.Уявіть, що у вас є зображення розміром 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 — це саме той контрол, який перетворює невизначеність очікування на зрозумілий, вимірюваний процес. Навіть якщо реальний прогрес виміряти неможливо (наприклад, невідомо, скільки чіпів обробить алгоритм — скористайтеся режимом 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 використовується рідко, але буває зручним для відображення рівнів (наприклад, рівень "заряду" батареї або гучності звуку).Найпоширеніший сценарій — показ прогресу операції з відомим загальним обсягом. Наприклад, завантаження 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="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, є кілька поширених помилок, на які варто звернути увагу ще до того, як ви з ними зіткнетесь на практиці.
Помилка 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 — це контрол, що дозволяє користувачу вибрати числове значення у заданому діапазоні шляхом перетягування повзунка (thumb) уздовж доріжки (track). Подібно до ProgressBar, він успадковує від RangeBase, тому логіка Minimum, Maximum і Value тотожня. Різниця принципова в одному: ProgressBar — пасивний індикатор (не передбачає взаємодії), а Slider — активний елемент введення, що реагує на дії миші та клавіатури.
Аналогія з реального світу: регулятор гучності на підсилювачі або "крутилка" яскравості монітора. Ви бачите поточне положення та можете плавно змінювати значення у визначених межах. Саме такий UX-паттерн реалізує Slider у WPF.
З точки зору взаємодії Slider підтримує кілька способів зміни значення:
LargeChange у відповідному напрямку.SmallChange.LargeChange.Minimum / Maximum.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 з відображенням міток:
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>
Одна з найнаочніших демонстрацій прив'язки даних у 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 вирішує цю дилему елегантно: він показує пояснення лише тоді, коли користувач затримує курсор миші над елементом — тобто тоді, коли він сам сигналізує про зацікавленість.
У WPF ToolTip — це не просто властивість рядка, а повноцінний контрол. Він успадковує від ContentControl, а отже, як і Button, може містити будь-який XAML-вміст. Це відкриває сценарії, недосяжні у WinForms: 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="..." на елемент <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>
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 в єдиному інтерактивному рішенні.
Завдання: Реалізуйте мінімалістичний переглядач зображень із симуляцією завантаження.
picsum.photos). Відображайте їх у Image з Stretch="UniformToFill" у квадратному Border.ProgressBar під зображенням. Зв'яжіть ProgressBar.Value з Slider.Value через {Binding ElementName} — переміщення Slider вручну симулює прогрес завантаження.ProgressBar додайте TextBlock з відсотком (прив'язка через StringFormat={}{0:F0}%).Ціль: Застосувати Popup для реалізації нетривіальної UI-взаємодії.
Завдання: Створіть список елементів, де при натисканні кнопки "ℹ Деталі" поруч з кожним рядком відкривається Popup із розгорнутою інформацією.
Технічні вимоги:
Border з ім'ям і кнопкою "ℹ Деталі".Popup з Placement="Right", прив'язаний до кнопки через PlacementTarget.Popup: заголовок, опис, ProgressBar (наприклад, "Наявність: 75%").StaysOpen="False" — 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.