WPF 4 варіант кнопки з іконкою та текстом
Я думаю, що кожен (або майже кожен), з тих, хто приходить у WPF з WinForms, спочатку відчуває розгубленість щодо функціоналу стандартних контролів. Здавалося б - ось він - знайомий контрол. Він дуже схожий на старого знайомого з WinForms. Навіть сигнатура звичайних методів або повністю збігається, або зазнала незначної трансформації (ну, наприклад, властивість Enabled отримала приставку Is). Налаштувань у контролів багато, від параметрів візуалізації рябить в очах. Але при трохи тіснішому знайомстві і спробі натягнути звичні способи побудови інтерфейсу на XAML і приходить та сама розгубленість.
Як же так? Ну невже кнопка не має властивості Image? Ви ж жартуєте, правда? Уся справа в тому, що у WPF (точніше у XAML) зовсім інша ідеологія організації інтерфейсу. Базові контролі представляють лише базовий (вибачте за тавтологію) функціонал. Простота базових контролів компенсується потужними механізмами шаблонів та стилів. Існують і сторонні бібліотеки компонентів, але вони, найчастіше, або марні, або безнадійно застаріли, або дуже платні.
Нещодавно я в черговий раз зіткнувся з необхідністю вирішення цього дуже простого (здавалося б) завдання. Я зневажив весь гугл запитами типу "XAML button with image" "WPF button image text" і т.д.
Але повернімося до способів реалізації кнопки з іконкою.
Перший і найочевидніший спосіб - описати потрібнийContentкнопки прямо в коді форми

Другий очевидний спосіб - успадкування відButton
Третій очевидний спосіб - створимоUserControl.
СтворимоUserControl, на який покладемо одну лише кнопку. УUserControlстворимоDependencyProperty, через яке задаватимемо іконку дляContentControl, який лежить у кнопці. Цей спосіб по праву заслуговує на медаль за максимальну корявість. Він успадковує багато недоліків попередніх методів, і додає безліч своїх. У коді форми ми отримуємоUserControl, але втрачаємо кнопку. Втрачаємо разом з усіма властивостями та подіями. Автор ідеї пропонує витягнути все, що було втрачено, через ті саміDependencyProperty, загалом ви зрозуміли. Стає незрозуміло за що ми боролися.
Четвертий спосіб - AttachedProperty
Отже, зовсім небагато теорії. Що ж таке це самеAttachedProperty
Кожен XAML розробник стикався зAttachedвластивостями коли, наприклад, контролював властивістьGrid.Column. Якщо в трьох словах - то це своїй ідеї трохи схоже на Extension з Linq. Можна зареєструвати властивість, значення якої можна задати будь-якомуDependencyObject. Виглядає це приблизно так (приклад MSDN):
У цьому коді реєструється властивістьIsBubleSource. В результаті будь-якогоDependencyObject, наприклад того жButton, можна задати його значення:
Загальний зміст цього коду – при заданому властивості IsBubbleSource для кнопки ми автоматично потрапляємометодSetIsBubbleSource, який встановлює значення. При отриманні значення відповідно потрапляємо в методGetIsBubbleSource. Це все відбувається автоматично, достатньо лише написати методи з іменами Set та Get, решта – справа платформи.
Незважаючи на те, що написано не так вже й мало коду, самомуButtonвід такої операції ні жарко ні холодно – він просто стає сховищем відокремленого значення, яке можна ставити і питати. Звичайно, можна реалізувати в методахSetIsBubbleSourceіGetIsBubbleSourceхитру логіку, яка буде приводитиelementдоButton, діставати з нього вміст і виробляти з вмістом різні операції, але це знову погано пахне, робити так не потрібно.
Приступаємо до практичної частини
У проект WPF додаємо наступний клас:
Що тут відбувається? Ми зареєструвалиAttachedвластивістьIconтипуFrameworkElementзі стандартним значенням null.
Тепер створимо шаблон нашої кнопки. Я не зупинятимусь на поясненні «що таке шаблони і як вони працюють» – якщо раптом комусь це невідомо – інформації в мережі дуже багато. Отже, додаємо в наш проектResourceDictionaryз ім'ям Styles.xaml (якщо раптом у проекті ще немає ресурсу стилів). У цьомуResourceDictionaryдодамо наступний код:
Коротко про те, що це таке. Цей записResourceDictionaryописує шаблон будь-якої кнопки нашого проекту. У цьому шаблоні задається звичайне оформлення кнопки WPF, але перевизначається її вміст. Як вміст виступаєStackPanel, у якому лежатьContentControlіTextBlock, тобто. так само, як і в першому прикладі. Крім цього у шаблоні я поставив таку поведінку дляіконки – якщо для кнопки встановленоIsEnabled == False, то іконка отримує прозорість 50%, і стає схожою на неактивну.
Додамо на нашу форму 4 прості кнопки. Призначимо кожній кнопці свій текст, наприклад:Content=«Кнопка 1». Запускаємо програму та отримуємо перший сюрприз.

Ідентичні іконки на кожній кнопці програми – це не те, чого ми домагаємось, і саме тут ми пускаємо у справу нашу секретну зброю – клас Ext.E та механізмAttachedProperty. Для непосвячених наступні рядки будуть нагадувати якусь магію (як і для мене, коли я це вперше побачив), але якщо спробувати вникнути, то стає зрозуміло за рахунок чого все це працює.
Отже, йдемо в наш ресурсний файл Styles.xaml і додаємо до нього новийnamespace:
Після цього опускаємося нижче і в шаблоні кнопки знаходимо рядок, в якому створюєтьсяContentControlі задається його вміст:
Змінюємо другий рядок на:
Цей рядок змушуєContentControlзвернутися до властивості Ext.E.Icon у кнопки та отримати з нього свій вміст. Після цього залишається додати код, що встановлює значення якості Ext.E.Icon у саму кнопку. Робиться це у коді форми, де створюється кнопка.
Примітивний варіант кнопки з готовий значок. Змінюючи значенняIconTriangleна інші іменаресурсів, можна встановити різні іконки на кнопках. При цьому, на відміну від перших трьох способів, ми зберігаємо у кнопки всі її вроджені здібності до стилізації (за винятком можливості змінювати структуру Content, само собою). Вміст кнопки визначається не з C#, і всі властивості з подіями залишилися на своєму місці.
Ходімо трохи далі
Якщо ми спробуємо використати цю кнопку в реальному проекті, то зіткнемося ось із чим:
- Розмір іконки не налаштовується.
- Орієнтація кнопки (вертикальна чи горизонтальна) не налаштовується.
Якщо точніше, все налаштовується, але у шаблоні, тобто. для всіх кнопок відразу, але клонування шаблонів – це граблі та суцільна незручність. Ми боролися не за це. Розширимо клас Ext.E. Додамо туди ще дваAttachedProperty
Вихідний код одним архівом буде наприкінці статті, тому я не дублюватиму аналогічні методи класу Ext.E у статті.
Опишу лише зміни, які потрібно зробити у шаблоніButton. РозміриContentControlпов'язуємо зі значеннямIconSize:
ОрієнтаціюStackPanelпов'язуємо зі значеннямOrientation
В результаті кнопка отримала додаткові параметри, і ми можемо написати так:
В результаті нехитрих маніпуляцій можна отримати такий зоопарк (перша кнопкаIsEnabled=«False»):

Ну і насамкінець згадаю про обмеження
Всі вони стосуються процесу та засобів розробки: — XAML дизайнер VisualStudio 2010 реагує на такий опис кнопки якось так:

- Після зміни класу Ext.E краще перезавантажити середовище розробки. Без цієї зміни найчастішене визначаються і дизайнери сваряться те що, що додані чи змінені властивості немає.
- За невстановленим мною алгоритмом значення, задані кнопці за допомогою AttachedProperty, то видно дизайнеру, то ні. Частіше не видно, і форма виглядає якось так:
Але це стосується тільки Designer, в режимі виконання програми все працює як потрібно.
Можливо (і навіть швидше за все) я описав велосипед, але той факт, що за два дні пошуків я не знайшов більш прийнятної безкоштовної реалізації говорить про те, що на цьому фронті не все гаразд. Крім того, я отримав можливість трохи розібратися з механізмом розширення стандартних контролів нестандартним способом і застосування цього механізму можна знайти масу.