Desktop UI

Button, Image, ProgressBar та інші базові контроли

Вивчаємо найбільш вживані контроли WPF — кнопки, зображення, індикатори прогресу, слайдери та спливаючі підказки. Кожен контрол розбирається від базових властивостей до нетривіальних деталей.
Нові терміни у цій статті: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).

У цій статті ми зосередимось на базових контролах — тих, що студент зустрічає у перших же своїх застосунках і без яких неможливо уявити жодну форму: кнопки у різних варіаціях, зображення, індикатори прогресу, слайдери та підказки. Ці контроли прості у використанні, але кожен із них має нюанси, які важливо розуміти, щоб не наштовхуватись на несподівані проблеми пізніше.

Контроли у WPF та Avalonia здебільшого ідентичні за назвами та властивостями. Те, що ви вивчите у цій статті, майже без змін працюватиме і в Avalonia-проєкті. Саме тому для цього блоку не потрібен окремий Avalonia-companion.

Button: набагато більше, ніж кнопка

Основна модель взаємодії

Button є, мабуть, найпершим контролом, з яким стикається будь-який розробник GUI. На перший погляд, він здається тривіальним: є кнопка, є подія Click — що тут складного? Але WPF-реалізація Button приховує у собі чимало деталей, які, якщо їх не знати, можуть спантеличити у найнеочікуваніший момент.

Почнемо з найпростішого. Щоб відреагувати на натискання кнопки у code-behind, ми підписуємось на подію Click:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Зверніть увагу на сигнатуру обробника: 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)...

Content — не лише текст

Одна з найважливіших відмінностей між Button у WPF і кнопкою у WinForms полягає у тому, що властивість Content у WPF може містити будь-який об'єкт — рядок, число, зображення, цілу панель із вкладеними елементами. Це стає можливим завдяки тому, що Button успадковує від ContentControl, а ContentControl відображає своє Content через DataTemplate або напряму, якщо вміст вже є UIElement.

Найпоширеніший практичний приклад — кнопка з іконкою та текстом. У WinForms це потребувало спеціального ImageButton-компонента або хакерських рішень. У WPF — це просто кілька вкладених елементів усередині Button.Content:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Між тегами <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 через рівні інтервали часу. Це робить його ідеальним для сценаріїв, де логічно "прокручувати" значення, поки натиснуто: кнопки збільшення/зменшення числового поля, кнопки ручної прокрутки у кастомних скролбарах тощо.

Дві ключові властивості керують цією поведінкою:

Delay
int
Час у мілісекундах між першим натисканням та початком повторення. За замовчуванням — 250. Це "захист" від випадкових подвійних спрацьовувань — короткий клік не викличе жодного повторення.
Interval
int
Час у мілісекундах між повторними Click-подіями у "безперервному режимі". За замовчуванням — 250. Чим менше значення — тим швидше відбуваються повтори.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

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)...

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.

Найпоширеніша помилка початківців — залишити Build Action = None (значення за замовчуванням для зображень, доданих через Solution Explorer). У такому випадку файл фізично присутній у папці проєкту, але не вбудовується у збірник. Застосунок знайде зображення під час розробки (бо файл лежить поруч із проєктом на диску), але після публікації або встановлення на іншу машину зображення зникне — файлу поруч з .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Шлях до ресурсу всередині збірки, відносно кореня проєкту
У XAML-розмітці можна використовувати скорочений відносний URI — просто /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)...

Властивість 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 (за замовчуванням)

Зображення масштабується пропорційно таким чином, щоб повністю вміститись у відведених розмірах. Пропорції завжди зберігаються. Якщо співвідношення сторін зображення відрізняється від співвідношення сторін контролу — з'являться порожні смуги ("letterboxing"). Це найбезпечніший режим для загального використання.

UniformToFill

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

Щоб різниця між режимами стала наочною, розглянемо кожен у живому прикладі. Зображення оригінального розміру — широке (landscape), контрол — квадратний. Це найкращий тест для спостереження поведінки Stretch:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Для аватарів, фонових зображень та банерів використовуйте Stretch="UniformToFill" у поєднанні з ClipToBounds="True" на Border-контейнері. ClipToBounds гарантує, що частини зображення, які виходять за межі Border, не відображатимуться навіть якщо Image за розміром більший.

Для зручного запам'ятовування — зведена таблиця:

StretchПропорціїЗаповнює весь простірМоже обрізатиТиповий use case
None✅ Зберігає❌ Ні✅ Так (якщо велике)Піксель-перфектні іконки
Fill❌ Порушує✅ Так❌ НіМайже ніколи
Uniform✅ Зберігає❌ Ні (смуги)❌ НіЗагальне відображення
UniformToFill✅ Зберігає✅ Так✅ Так (краї)Аватари, обкладинки
Превью використовує Avalonia Fluent Theme і виглядає як Windows 11. У реальному WPF-проєкті поведінка Stretch ідентична — це властивість самого Image-контролу, незалежна від теми. Зовнішній вигляд Border (рамки контейнера) може незначно відрізнятись.

ProgressBar: індикатор виконання операцій

Роль індикатора прогресу у користувацькому досвіді

Уявіть застосунок, який завантажує великий файл, виконує складний обрахунок або синхронізується з сервером. Що відбувається з інтерфейсом у цей час? Якщо розробник нічого не передбачив — вікно "завмирає", перестає реагувати на дії користувача, і той бачить лише нерухомий екран. Без жодного сигналу про те, що відбувається і скільки це триватиме. Подібний досвід підриває довіру до застосунку та змушує думати про "зависання".

ProgressBar — це саме той контрол, який перетворює невизначеність очікування на зрозумілий, вимірюваний процес. Навіть якщо реальний прогрес виміряти неможливо (наприклад, невідомо, скільки чіпів обробить алгоритм — скористайтеся режимом IsIndeterminate), сам факт анімації повідомляє: "застосунок живий, він працює, зачекайте".

З точки зору архітектури WPF, ProgressBar успадковує від RangeBase — базового класу, що визначає концепцію "значення у діапазоні". Той самий клас є предком Slider та ScrollBar. Це елегантне рішення: незважаючи на зовнішню несхожість, всі три контроли поділяють спільний контракт: є мінімум, є максимум, є поточне значення.

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

Value
double
Поточне значення прогресу. Повинне знаходитись у діапазоні Minimum, Maximum. За замовчуванням — 0. Найчастіше прив'язується через {Binding} до властивості ViewModel або встановлюється у code-behind під час виконання тривалої операції.
Minimum
double
Нижня межа діапазону. За замовчуванням — 0. Звичайно, залишається нульовим, але може бути будь-яким числом. Наприклад, Minimum="-100" і Maximum="100" для індикатора, що показує відхилення від нейтрального стану.
Maximum
double
Верхня межа діапазону. За замовчуванням — 100. Встановлюйте значення, що відповідає загальному обсягу роботи: кількості файлів, кількості записів, загальному розміру у байтах тощо.
IsIndeterminate
bool
Якщо true — контрол переходить у режим невизначеного прогресу: замість фіксованої смуги показується "пульсуюча" анімація ("серпантин"). Властивості Value, Minimum, Maximum у цьому режимі ігноруються. За замовчуванням — false.
Orientation
Orientation
Визначає напрямок заповнення: Horizontal (за замовчуванням, ліворуч направо) або Vertical (знизу вгору). Вертикальний ProgressBar використовується рідко, але буває зручним для відображення рівнів (наприклад, рівень "заряду" батареї або гучності звуку).

Визначений прогрес: класичний ProgressBar

Найпоширеніший сценарій — показ прогресу операції з відомим загальним обсягом. Наприклад, завантаження 150 файлів: Minimum=0, Maximum=150, Value — поточна кількість завантажених файлів.

У реальних застосунках Value рідко встановлюється статично у XAML — зазвичай відбувається прив'язка до властивості ViewModel, яка оновлюється асинхронно. Але для розуміння роботи контролу виведемо статичний приклад:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Зверніть увагу на кілька деталей. По-перше, 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)...

Превью використовує Avalonia Fluent Theme. Анімація 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

TickFrequency
double
З якою частотою розміщувати мітки (tick marks) уздовж доріжки. Якщо Minimum=0, Maximum=100, TickFrequency=10 — мітки будуть на позиціях 0, 10, 20, ... 100. За замовчуванням — 1. Мітки відображаються лише якщо TickPlacementNone.
TickPlacement
TickPlacement
Де відображати мітки відносно доріжки: None (не відображати, за замовчуванням), TopLeft (зверху для горизонтального / ліворуч для вертикального), BottomRight (знизу / праворуч), Both (з обох боків).
IsSnapToTickEnabled
bool
Якщо true — thumb "прилипає" до найближчої мітки при перетягуванні. Значення Value буде завжди кратне TickFrequency, ніколи не буде "між мітками". Зручно для налаштувань з дискретними значеннями (наприклад, вибір роздільної здатності або розміру шрифту).
SmallChange
double
Величина зміни значення при натисканні клавіш стрілок. За замовчуванням — 0.1. Для цілочисельних Slider рекомендується встановити 1.
LargeChange
double
Величина зміни значення при кліку на доріжку або натисканні Page Up / Page Down. За замовчуванням — 1. Зазвичай у 10 разів більше за SmallChange.
Orientation
Orientation
Horizontal (за замовчуванням) або Vertical. Для вертикального значення зростають знизу вгору.

Базовий Slider без прив'язки

Розпочнемо з найпростішого прикладу — горизонтального Slider з відображенням міток:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Прив'язка 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)...

Зверніть на ключові деталі цього прикладу. 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.
Превью використовує Avalonia Fluent Theme. Зовнішній вигляд thumb та track у реальному WPF буде іншим (класична тема Aero). Поведінкові властивості (TickFrequency, TickPlacement, IsSnapToTickEnabled, ElementName-прив'язка) — ідентичні в обох фреймворках.

ToolTip: спливаючі підказки

Навіщо потрібні ToolTip і коли вони доречні

Будь-який добре спроєктований інтерфейс передусім є зрозумілим. Але зрозумілість і стислість — це суперечливі вимоги: якщо пояснювати кожну кнопку написом, інтерфейс стане захаращеним; якщо нічого не пояснювати — незрозумілим. ToolTip вирішує цю дилему елегантно: він показує пояснення лише тоді, коли користувач затримує курсор миші над елементом — тобто тоді, коли він сам сигналізує про зацікавленість.

У WPF ToolTip — це не просто властивість рядка, а повноцінний контрол. Він успадковує від ContentControl, а отже, як і Button, може містити будь-який XAML-вміст. Це відкриває сценарії, недосяжні у WinForms: ToolTip із зображенням, ключами клавіатури, форматованим текстом або навіть цілою мініатюрою форми.

Простий текстовий ToolTip

Найпростіший спосіб додати підказку — встановити рядок у властивість ToolTip будь-якого елемента. WPF автоматично обгортає його у відповідний контрол:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Зверніть: ToolTip="..." — це скорочений запис. WPF за лаштунками перетворює рядок на об'єкт ToolTip з Content = "...". Додаткові параметри (затримка показу, тривалість показу) можна регулювати через прикріплені властивості класу ToolTipService:

Властивість ToolTipServiceЗа замовч.Опис
InitialShowDelay400 мсЧас від наведення курсору до появи підказки
ShowDuration5000 мсЧас відображення підказки
BetweenShowDelay100 мсЗатримка між закриттям одного та відкриттям іншого ToolTip
PlacementMouseВідносно чого розміщувати (аналогічно до Popup.Placement)

Складний структурований ToolTip

Коли простого рядка недостатньо — замінюємо атрибут ToolTip="..." на елемент <X.ToolTip><ToolTip>...</ToolTip></X.ToolTip> і всередині розміщуємо довільний XAML. Це "Typed ToolTip" або "Structured ToolTip" — стандартний підхід у програмних продуктах:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

У реальних застосунках структуровані ToolTip-и часто містять іконку контролу, короткий заголовок і опис. Visual Studio, наприклад, показує ToolTip для кожного елемента IntelliSense із сигнатурою, коротким описом і посиланням на документацію. Це повністю реалізується засобами WPF з DataTemplate та прив'язкою даних.

ToolTip і Popup на перший погляд схожі — обидва показують щось "поверх" основного інтерфейсу. Але їхня природа і призначення кардинально різняться.

ToolTip — суто реактивний: він з'являється автоматично при наведенні курсору і зникає без будь-якого втручання коду. Його поведінкою керує WPF та ToolTipService. Програмного контролю над ToolTip практично немає.

Popup — це будівельний примітив для спливаючих шарів будь-якого типу: випадаючі меню, контекстні панелі, спливаючі форми, нотифікації. Його поява та зникнення повністю підконтрольні коду: властивість IsOpen = true/false вмикає і вимикає Popup. Саме з Popup внутрішньо побудовані ComboBox-список, ContextMenu, Menu-підменю та сам ToolTip.

IsOpen
bool
Ключова властивість. true — Popup відображається. false — прихований. Встановлюється програмно або через прив'язку даних. За замовчуванням — false.
Placement
PlacementMode
Визначає, відносно чого і як розміщувати Popup. Найпоширеніші значення: Bottom (під PlacementTarget), Top (над), Mouse (біля курсору), Center (по центру target), Absolute (у конкретних координатах екрана).
PlacementTarget
UIElement
Елемент, відносно якого розраховується позиція Popup (якщо Placement не Mouse і не Absolute). За замовчуванням — батьківський елемент у дереві.
StaysOpen
bool
Якщо false (за замовчуванням) — Popup автоматично закривається при кліку за його межами. Якщо true — закривається лише програмно. Використовуйте StaysOpen="True" для Popup-форм, що потребують явного підтвердження.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

У превью Popup не відображається автоматично, оскільки Click="..." не виконується у WASM-середовищі (лише Command). У реальному WPF встановлення IsOpen="True" безпосередньо у XAML одразу відображає Popup — спробуйте змінити значення атрибуту, щоб переконатися. Керування через код — єдиний повноцінний спосіб взаємодії з Popup у реальних застосунках.
Popup живе поза межами логічного дерева вікна. Він рендерується у окремому нативному вікні (HWND) поверх усього іншого. Це означає: Popup не успадковує DataContext від батьківського вікна автоматично — потрібно явно передати його через DataContext="{Binding ElementName=mainWindow, Path=DataContext}". Також Popup не обрізається межами вікна і може виходити за його краї на відміну від звичайних панелей.

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

Нижче — три завдання різного рівня складності, що охоплюють матеріал цієї статті. Рекомендується виконувати їх послідовно: кожне наступне спирається на навички, відпрацьовані у попередньому.


Підсумок

Що ми вивчили у цій статті

У цій статті ми пройшли шлях від найпростіших кнопок до програмно керованих спливаючих шарів. Узагальнимо ключові висновки.

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.