Введение

В мире разработки приложений, будь то веб или десктоп, использование айконок является неотъемлемой частью пользовательского интерфейса. Векторные айконки предпочтительнее растровых, так как они масштабируются без потери качества. Одной из популярных коллекций векторных айконок является Bootstrap Icons, содержащая более 2000 готовых айконок. Хотя коллекция Bootstrap Icons доступна как npm-пакет bootstrap-icons и ориентирована на веб-разработку, её можно эффективно использовать в десктопных приложениях.
Создадим с нуля контрол BootstrapIcon
для удобного использования двухцветных векторных айконок в приложениях на Avalonia/WPF. Сами изображения, в основном берем из SVG-файлов библиотеки bootstrap-icons
, отсюда и название нашего контрола.
Туториал ориентирован на разработчиков, знакомых с Avalonia на базовом уровне. Основной упор в реализации контрола делается на Avalonia. Вариант для WPF, надеюсь, будет полезен для тех, кто переходит с WPF на Avalonia.
? Полученные контролы BootstrapIcon
для Avalonia и WPF с примерами использования размещены на GitHub.
? Продолжение следует...
Планируется публикация ещё пары туториалов, в которых будет пошаговое руководство для создания главного меню приложения и аналога ToolBar с использованиемBootstrapIcon
.
Содержание руководства
Концепция и структура
Прежде чем погрузиться в пошаговую реализацию контрола BootstrapIcon
, важно понять, что лежит в основе нашего подхода. В этом разделе мы рассмотрим:
причины выбора коллекции Bootstrap Icons в качестве источника иконок;
существенные аспекты реализации;
создание прототипа контрола;
реализацию хранения данных об айконках в кросс-платформенной библиотеке;
общую структуру нашего решения для поддержки Avalonia и WPF.
Почему именно Bootstrap Icons?

В мире векторных айконок существует множество отличных коллекций, таких как Google Fonts Icons, Feather Icons, Font Awesome и другие. Однако мы используем именно Bootstrap Icons по нескольким причинам:
Полнота и стиль: Коллекция содержит более 2000 айконок, охватывающих большинство областей применения; выдержана в едином современном стиле.
Лицензия: Bootstrap Icons распространяются под лицензией MIT, что позволяет свободно использовать их в личных и коммерческих проектах.
Единообразие дизайна: Если вы уже используете фреймворк Bootstrap и его айконки для фронтенда веб-приложений, выбор Bootstrap Icons для приложений Avalonia/WPF позволит добиться единообразия в дизайне между разными платформами.
-
Особенности SVG-структуры: простота и логичность использования элементов SVG:
На момент написания статьи большинство (2070 из 2074) Bootstrap Icons используют для отрисовки исключительно элементы
<path>
(не задействуют<circle>
,<rect>
), кроме того, не применяют атрибуты вродеstroke-width
. Это значительно упрощает обработку SVG-файла: мы можем просто взять строку из атрибутаd
элемента<path>
и скопировать в определение айконки.Многие Bootstrap Icons состоят из нескольких логических частей (например, рамка и значок внутри рамки), каждая из которых определена отдельным элементом
<path>
в исходном SVG. Это является ключевым преимуществом, так как дает возможность назначать индивидуальный цвет каждому такому фрагменту изображения с минимальными затратами.
Возможность автоматизации: Особенности структуры SVG-файлов Bootstrap Icons также упрощают создание средств автоматизации процесса определения набора айконок приложения. Например, можно разработать генератор, который по JSON-файлу с перечнем иконок и их цветами создаёт код перечисления
BootstrapSymbol
(о нём — ниже).
В приложениях Avalonia/WPF для эффективного рендеринга векторных айконок предпочтительнее работать не с целыми SVG-файлами, а с объектами типа StreamGeometry
, которые можно создать из строк атрибута d
элемента <path>
. Особенности структуры SVG-файлов bootstrap-icons
делают процесс определения набора айконок приложения достаточно простым, в том числе для автоматизации.
Подход к реализации

Программную реализацию BootstrapIcon
основываем на исходных кодах PathIcon
- штатного контрола фреймворка Avalonia. Штатный PathIcon
прекрасно справляется с отображением одного StreamGeometry
(векторного изображения, упакованного в один path
). Наш BootstrapIcon
расширяет эти возможности, позволяя отображать до двух StreamGeometry
с независимым управлением цветом для каждого.
Определения двухцветных векторных айконок помещаем не в ресурсы (xaml/axaml-файлы), а в библиотеку классов, которая не зависит от фреймворка Avalonia или WPF.
В итоге, из штатного PathIcon
создаем свой BootstrapIcon
для удобного отображения двухцветных векторных айконок на основе данных из SVG-файлов популярной коллекции bootstrap-icons
. Если не хватает айконок из этой коллекции, можно добавить свои определения или позаимствовать из других источников (после приведения к стандартному размеру).
Для начала создадим прототип нашего контрола, чтобы лучше понимать, что мы собираемся сделать.
Создание прототипа

Для понимания принципов работы BootstrapIcon
, создадим прототип — тестовое приложение на Avalonia, демонстрирующее отображение иконки тремя способами:
<PathIcon>
без указания цвета (используется цвет по умолчанию).<PathIcon>
с заданным цветом заливки.<Viewbox>
с двумя элементами<Path>
, каждый из которых имеет свой цвет, — это будет основа нашего будущего контролаBootstrapIcon
.
Убедитесь, что у вас установлены .NET SDK 8.0 (или новее) и шаблоны Avalonia. Если шаблоны Avalonia отсутствуют, установите их с помощью команды:
dotnet new install Avalonia.Templates
Создайте новое приложение Avalonia в любом каталоге при помощи команды:
dotnet new avalonia.app -n TryIcon -f net8.0
Реализацию прототипа поместим в MainWindow.axaml
, где отобразим айконку "database-x" тремя различными способами:
Подготовка геометрии: На сайте Bootstrap Icons возьмем из SVG-файла айконки строковое значение атрибутов
d
каждого из двух элементов<path>
.-
Определение ресурсов: В
<Window.Resources>
создадим:Два ресурса
<StreamGeometry x:Key="PrimaryGeometry"/>
и<StreamGeometry x:Key="SecondaryGeometry"/>
для геометрии каждой части двухцветной айконки (значениями из атрибутовpath.d
).Ресурс
<StreamGeometry x:Key="IconGeometry"/>
как результат объединения этих двух строк геометрии (через пробел) для отображения монохромной айконки штатнымPathIcon
.Три ресурса
<SolidColorBrush>
(с ключамиIconBrush
,PrimaryForeground
,SecondaryForeground
) для задания цветов.
-
Размещение элементов: В корневом элементе окна создадим
<StackPanel>
с тремя визуальными элементами:Два
PathIcon
, ссылающихся на объединеннуюIconGeometry
(один без цвета, второй сIconBrush
).Один
<Viewbox>
, который станет нашим прототипомBootstrapIcon
.
Структура <Viewbox>
для прототипа BootstrapIcon
:
<Viewbox Width="32" Height="32" Stretch="Uniform">
<Panel>
<Path Width="16" Height="16" Stretch="None"
Data="{StaticResource PrimaryGeometry}"
Fill="{StaticResource PrimaryForeground}"/>
<Path Width="16" Height="16" Stretch="None"
Data="{StaticResource SecondaryGeometry}"
Fill="{StaticResource SecondaryForeground}"/>
</Panel>
</Viewbox>
Здесь два элемента <Path>
размещены внутри <Panel>
, позволяя им накладываться друг на друга. Первый <Path>
отображает геометрию из свойства PrimaryGeometry
и заливается цветом из PrimaryForeground
. Второй <Path>
аналогично использует SecondaryGeometry
и SecondaryForeground
.
Оба элемента <Path>
имеют фиксированные размеры 16x16 (исходный размер айконок Bootstrap) с запретом масштабирования фрагмента изображения (Stretch="None"
). Элемент <Viewbox>
обеспечивает масштабирование <Panel>
до размеров, заданных для самого контрола BootstrapIcon
(Stretch="Uniform"
).
Полный код MainWindow.axaml
находится ниже.
скрытый текст MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="TryIcon.MainWindow"
Title="TryIcon">
<Window.Resources>
<StreamGeometry x:Key="IconGeometry">M12.096 6.223A5 5 0 0 0 13 5.698V7c0 .289-.213.654-.753 1.007a4.5 4.5 0 0 1 1.753.25V4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16c.536 0 1.058-.034 1.555-.097a4.5 4.5 0 0 1-.813-.927Q8.378 15 8 15c-1.464 0-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13h.027a4.6 4.6 0 0 1 0-1H8c-1.464 0-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10q.393 0 .774-.024a4.5 4.5 0 0 1 1.102-1.132C9.298 8.944 8.666 9 8 9c-1.464 0-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777M3 4c0-.374.356-.875 1.318-1.313C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4 M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7m-.646-4.854.646.647.646-.647a.5.5 0 0 1 .708.708l-.647.646.647.646a.5.5 0 0 1-.708.708l-.646-.647-.646.647a.5.5 0 0 1-.708-.708l.647-.646-.647-.646a.5.5 0 0 1 .708-.708</StreamGeometry>
<StreamGeometry x:Key="PrimaryGeometry">M12.096 6.223A5 5 0 0 0 13 5.698V7c0 .289-.213.654-.753 1.007a4.5 4.5 0 0 1 1.753.25V4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16c.536 0 1.058-.034 1.555-.097a4.5 4.5 0 0 1-.813-.927Q8.378 15 8 15c-1.464 0-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13h.027a4.6 4.6 0 0 1 0-1H8c-1.464 0-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10q.393 0 .774-.024a4.5 4.5 0 0 1 1.102-1.132C9.298 8.944 8.666 9 8 9c-1.464 0-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777M3 4c0-.374.356-.875 1.318-1.313C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4</StreamGeometry>
<StreamGeometry x:Key="SecondaryGeometry">M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7m-.646-4.854.646.647.646-.647a.5.5 0 0 1 .708.708l-.647.646.647.646a.5.5 0 0 1-.708.708l-.646-.647-.646.647a.5.5 0 0 1-.708-.708l.647-.646-.647-.646a.5.5 0 0 1 .708-.708</StreamGeometry>
<SolidColorBrush x:Key="IconBrush" Color="#7c3aed"/>
<SolidColorBrush x:Key="PrimaryForeground" Color="#7c3aed"/>
<SolidColorBrush x:Key="SecondaryForeground" Color="Red"/>
</Window.Resources>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Spacing="8" Margin="4">
<PathIcon Width="32" Height="32"
Data="{StaticResource IconGeometry}"/>
<PathIcon Width="32" Height="32"
Data="{StaticResource IconGeometry}"
Foreground="{StaticResource IconBrush}" />
<Viewbox Width="32" Height="32" Stretch="Uniform">
<Panel>
<Path Width="16" Height="16" Stretch="None"
Data="{StaticResource PrimaryGeometry}"
Fill="{StaticResource PrimaryForeground}"/>
<Path Width="16" Height="16" Stretch="None"
Data="{StaticResource SecondaryGeometry}"
Fill="{StaticResource SecondaryForeground}"/>
</Panel>
</Viewbox>
</StackPanel>
</Window>
Запустив приложение, вы увидите айконку в трех вариантах: два монохромных и один двухцветный.
Этот прототип демонстрирует возможность рендеринга двухцветной векторной айконки стандартными средствами Avalonia, определяя ее геометрию и цвета частей как ресурсы. Однако, при таком подходе для каждой айконки требуется до четырех отдельных значений в словаре ресурсов. Это усложняет использование и управление айконками, если они не представлены как единый элемент.
В следующем разделе мы представим альтернативный подход, позволяющий определять айконки не как XAML-ресурсы.
Перечисление BootstrapSymbol
Чтобы удобно работать с айконками в коде как с именованными сущностями (своего рода символами), мы вводим специальное перечисление BootstrapSymbol
. Каждое значение этого перечисления соответствует одной айконке нашего приложения (взятой из коллекции Bootstrap Icons или определенной дополнительно).
Для хранения данных об айконке (символе) мы используем атрибуты SymbolPath
, связанные со значениями BootstrapSymbol
. Этот подход позволяет держать определение айконки (ее имя в коде, геометрию и цвет фрагментов изображения) в общей библиотеке классов.
Пример определения айконок в BootstrapSymbol
:
public enum BootstrapSymbol
{
...
// window-sidebar
[SymbolPath("M2.5 4...", 0xffffc107)]
[SymbolPath("M2 1...z", 0xffffc107)]
WindowSidebar,
// funnel
[SymbolPath("M1.5 1.5...z", KnownColor.RoyalBlue)]
Funnel,
// sort-alpha-down
[SymbolPath("M10.082 5.629z", KnownColor.RoyalBlue)]
[SymbolPath("M12.96 14H9.028...z", KnownColor.RoyalBlue)]
SortAlphaDown,
...
}
Каждый атрибут SymbolPath
хранит строку геометрии (значение атрибута d
из SVG-элемента <path>
) и опционально цвет. Для задания цвета используется uint
(трактуется как ARGB) или значение из System.Drawing.KnownColor
(набор цветов этого системного enum во многом совпадает с названиями HTML-цветов: Aqua, Aquamarine, Gray, Cyan, DarkBlue, Blue и т.д.).
Количество атрибутов SymbolPath
для значения перечисления, как правило, соответствует количеству элементов <path>
в исходном SVG-файле айконки. Цвет фрагмента изображения может задаваться для первого и второго атрибута SymbolPath
(для остальных используется цвет второго атрибута).
Вспомогательная логика в SharedLib
(реализованная через функцию расширения, как мы увидим далее) позволяет по значению BootstrapSymbol
получить все связанные с ним атрибуты SymbolPath
и извлечь из них необходимые данные для отрисовки. Именно эту логику и будет использовать наш контрол BootstrapIcon
.
Структура нашего решения
Наше решение будет состоять из трех проектов для использования кода айконок:
SharedLib: Кросс-платформенная (не зависящая от Avalonia/WPF) библиотека, содержащая определения айконок в виде перечисления
BootstrapSymbol
и вспомогательный код для работы с ними.TryAvalonia: Проект приложения на Avalonia UI, содержащий реализацию контрола
BootstrapIcon
для Avalonia и примеры его использования. Он ссылается наSharedLib
.TryWpf: Проект приложения на WPF, содержащий реализацию контрола
BootstrapIcon
для WPF и примеры его использования. Он также ссылается наSharedLib
.
Такая структура позволяет централизованно хранить определения айконок в SharedLib
и использовать их в UI-специфичных реализациях контрола в проектах TryAvalonia
и TryWpf
.
❕️ В реальных приложениях контролы типа
BootstrapIcon
правильнее размещать в специальных библиотеках (например,SharedLib.Ava
иSharedLib.Wpf
), но мы упростили структуру тестового решения, чтобы сократить описание.
Шаг 1: Создание решения и проектов
Убедитесь, что у вас установлены .NET SDK (рекомендуется .NET 8.0 или новее) и шаблоны Avalonia для dotnet new
. Если шаблоны Avalonia не установлены, выполните команду:
dotnet new install Avalonia.Templates
Затем выполните следующий скрипт в терминале: он создаст директорию AppIcons
, настроит решение и добавит в него три проекта с необходимыми зависимостями:
скрытый текст скрипта
# Создаем корневую директорию, переходим в нее и создаем решение
mkdir AppIcons
cd AppIcons
dotnet new sln -n AppIcons
# Создаем общую библиотеку SharedLib, включаем в решение
dotnet new classlib -n SharedLib -f net8.0
dotnet sln add SharedLib/SharedLib.csproj
# Создаем проект для Avalonia, добавляем ссылку на SharedLib, включаем в решение
dotnet new avalonia.app -n TryAvalonia -f net8.0
dotnet add TryAvalonia/TryAvalonia.csproj reference SharedLib/SharedLib.csproj
dotnet sln add TryAvalonia/TryAvalonia.csproj
# Создаем проект для WPF (требуется Windows), добавляем ссылку на SharedLib, включаем в решение
dotnet new wpf -n TryWpf -f net8.0
dotnet add TryWpf/TryWpf.csproj reference SharedLib/SharedLib.csproj
dotnet sln add TryWpf/TryWpf.csproj
dotnet new -f net8.0
# можете убрать этот параметр или указать другую версию .NET
После выполнения скрипта у вас будет готова структура решения AppIcons.sln
с тремя проектами.
Далее можно проверить корректность создания тестовых проектов, запустив их из терминала:
# Запуск Avalonia приложения
dotnet run --project TryAvalonia/TryAvalonia.csproj
# Запуск WPF приложения (требуется Windows)
dotnet run --project TryWpf/TryWpf.csproj
Шаг 2: Определение айконок в SharedLib
На этом шаге реализуем кросс-платформенную библиотеку SharedLib
для централизованного определения набора айконок, которые используют приложения Avalonia/WPF. В проекте создадим папку Controls
с тремя файлами:
SymbolPathAttribute.cs: Атрибут для связывания геометрии (
path.d
из SVG) и цвета с элементамиBootstrapSymbol
.BootstrapSymbol.cs: Перечисление, содержащее определения всех используемых айконок с помощью атрибутов
SymbolPath
.BootstrapSymbolExtensions.cs: Методы расширения для
BootstrapSymbol
, облегчающие доступ к данным атрибутовSymbolPath
из контроловBootstrapIcon
.
Класс SymbolPathAttribute
Класс SymbolPathAttribute
предназначен для определения строки геометрии одного SVG <path>
и опционально его цвета (в форматах uint
ARGB или System.Drawing.KnownColor
). Если цвет в SymbolPath
не указан, то для отображения геометрии будет использован системный цвет для текстовых элементов по умолчанию.
скрытый текст фрагмента SymbolPathAttribute.cs
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
internal class SymbolPathAttribute : Attribute
{
// Получает SVG path data для отрисовки айконки.
public string PathData { get; init; }
// Получает цвет заливки для SVG path.
public Color FillColor { get; init; }
// Инициализирует атрибут только с path data (без цвета)
public SymbolPathAttribute(string pathData)
{
PathData = pathData;
FillColor = Color.Empty;
}
// Инициализирует атрибут с path data и цветом в формате ARGB uint.
public SymbolPathAttribute(string pathData, uint ardb)
{
PathData = pathData;
FillColor = Color.FromArgb((int)ardb);
}
// Инициализирует атрибут с path data и цветом из System.Drawing.KnownColor.
public SymbolPathAttribute(string pathData, KnownColor knownColor)
{
PathData = pathData;
FillColor = Color.FromKnownColor(knownColor);
}
}
Наличие [AttributeUsage(AllowMultiple = true)]
позволяет применять SymbolPath
несколько раз к конкретному значению BootstrapSymbol
. Это необходимо для айконок, состоящих из нескольких <path>
. При ручном определении айконок рекомендуется на каждый <path>
из SVG-файла создавать соответствующий SymbolPath
(даже если их больше двух), учитывая следующую логику формирования данных для контрола BootstrapIcon
:
PrimaryGeometry
: используем геометрию из первогоSymbolPath
;PrimaryForeground
: используем цвет из первогоSymbolPath
;SecondaryGeometry
: используем геометрию из остальныхSymbolPath
(после слияния строк через пробел);SecondaryForeground
: используем цвет из второгоSymbolPath
(цвет остальных игнорируется).
Перечисление BootstrapSymbol
Перечисление BootstrapSymbol
в проекте SharedLib
определяет все айконки, которые использует приложение. Каждое его значение аннотируется одним или несколькими атрибутами SymbolPath
, хранящими геометрию (path.d
), взятую из исходных SVG-файлов bootstrap-icons или определенную вручную. Для первого и второго SymbolPath
можно определить цвет.
Например, рассмотрим SVG-файл айконки file-earmark-arrow-down:
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>
В этом SVG два элемента <path>
. Соответственно, в нашем перечислении для значения FileEarmarkArrowDown
мы добавим два атрибута SymbolPath
, скопировав содержимое атрибутов d
.
// Export, file-earmark-arrow-down
[SymbolPath("M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293z", 0xff0d6efd)]
[SymbolPath("M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z", KnownColor.Green)]
FileEarmarkArrowDown,
Мы также можем добавить собственные айконки (custom1, custom2), определив их геометрию с помощью редактора SVG-путей (например, используя svg-path-editor). При необходимости определяем цвет фрагментов изображения.
// custom1
[SymbolPath("M3,3 8,6 13,3 10,8 13,13 8,10 3,13 6,8z", KnownColor.Red)]
Custom1,
// custom2
[SymbolPath("M0,0H16V16H0z", KnownColor.Green)]
[SymbolPath("M5,3 13,11 11,13 3,5z M13,5 5,13 3,11 11,3z", KnownColor.Red)]
Custom2,
Таким образом, перечисление BootstrapSymbol
содержит определения всех необходимых для приложения айконок в достаточно компактном и обозримом виде. Обычное приложение редко использует большое количество уникальных айконок, поэтому создание и модификация BootstrapSymbol
даже в ручном режиме, задача не из самых сложных.
? Автоматизация.
Когда мне надоело копировать строки из SVG-файлов в код своего приложения, написал Node.js-скрипт, который упрощает этот процесс. Генератор кода перечисленияBootstrapSymbol
оформил и опубликовал как npm-пакет: path-icons. Можете воспользоваться или автоматизировать этот процесс самостоятельно.
Класс BootstrapSymbolExtensions
Класс BootstrapSymbolExtensions
содержит вспомогательные методы для работы с перечислением BootstrapSymbol
. Его основная задача — извлекать данные из атрибутов SymbolPath
в формате, необходимом контролу BootstrapIcon
.
Класс предоставляет два ключевых метода:
GetGeometryPath()
: ОбъединяетPathData
всех атрибутов в одну строку геометрии (полезно для монохромногоPathIcon
).GetGeometryDefinition()
: Извлекает данные из атрибутов и возвращает кортеж(string primaryPath, uint primaryArgb, string secondaryPath, uint secondaryArgb)
, реализуя логику выбора данных для первичной и вторичной частей, как требуется контролуBootstrapIcon
.
Данные иконок (строки геометрии и цвета в формате string
, uint
), получаемые через GetGeometryDefinition
, являются кросс-платформенными. Контрол BootstrapIcon
для конкретного фреймворка преобразует их в свои типы:
Avalonia.Media.Geometry
,Avalonia.Media.Color
(для Avalonia);System.Windows.Media.Geometry
,System.Windows.Media.Color
(для WPF).
Полный код классов библиотеки SharedLib на GitHub:
На этом Шаг 2 завершен. Мы создали кросс-платформенную библиотеку SharedLib
для определения айконок в виде перечисления BootstrapSymbol
, а также с функциями доступа к данным айконки (символа). Теперь можем перейти к реализации контрола BootstrapIcon
для Avalonia.
Шаг 3: Создание BootstrapIcon для Avalonia

На этом этапе мы реализуем контрол BootstrapIcon
непосредственно в проекте TryAvalonia
. Мы создадим его "с нуля", постепенно добавляя необходимую функциональность, используя как основу исходный код штатного контрола PathIcon
.
Структура раздела
Шаг 3.1. Создание копии PathIcon
-
Шаг 3.2. Переход на использование BootstrapSymbol
Добавление новых свойств
Модификация шаблона
Конвертация данных
Кэширование данных
Логика обработки Symbol
Шаг 3.1. Создание копии PathIcon
Сначала создадим базовые файлы для нашего контрола и сделаем его функциональной копией стандартного PathIcon
. В проекте TryAvalonia
создайте новую папку с именем Controls
, перейдите в нее в терминале и выполните команду:
dotnet new avalonia.templatedcontrol -n BootstrapIcon -p:n TryAvalonia.Controls
Эта команда создаст в папке Controls
два файла BootstrapIcon.axaml
и BootstrapIcon.axaml.cs
, используя namespace TryAvalonia.Controls
.
Если вы предпочитаете работать через Rider, Visual Studio или VS Code, используйте соответствующие шаблоны "Avalonia Templated Control", указав папку Controls
и имя BootstrapIcon
. Затем надо изменить namespace
на TryAvalonia.Controls
(по умолчанию, будет TryAvalonia
).
Теперь необходимо включить тему BootstrapIcon.axaml
в ресурсы приложения. Откройте файл App.axaml
в корне проекта TryAvalonia
и добавьте ссылку на файл темы в секцию <Application.Resources>
:
скрытый текст App.xaml
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="TryAvalonia.App"
RequestedThemeVariant="Default">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="/Controls/BootstrapIcon.axaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>
Проверьте, что проект компилируется без ошибок и что контрол BootstrapIcon
отображается в дизайнере файла BootstrapIcon.axaml
(на этом этапе вы должны увидеть стандартный текст "Templated Control").
Теперь превратим наш контрол BootstrapIcon
в полный аналог штатного контрола PathIcon
, перенеся необходимый исходный код из репозитория Avalonia. Это даст нам рабочую основу для дальнейших доработок.
Исходный код стандартного PathIcon (Avalonia):
Класс
PathIcon
: src/Avalonia.Controls/PathIcon.csТема для
PathIcon
(Fluent Theme): src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml
После переноса кода из PathIcon
получим BootstrapIcon.axaml.cs
с учетом переименования. Используйте этот код для проверки того, что выполнено:
скрытый текст BootstrapIcon.axaml.cs
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace TryAvalonia.Controls;
public class BootstrapIcon : IconElement
{
static BootstrapIcon()
{
AffectsRender<BootstrapIcon>(DataProperty);
}
public static readonly StyledProperty<Geometry?> DataProperty =
AvaloniaProperty.Register<BootstrapIcon, Geometry?>(nameof(Data));
public Geometry? Data
{
get => GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
}
После переноса кода из PathIcon
получим BootstrapIcon.axaml
с учетом переименования, использования xmlns:ui="using:TryAvalonia"
и замены тестовой геометрии. Используйте этот код для проверки того, что сделали на этом шаге:
скрытый текст BootstrapIcon.axaml
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:TryAvalonia.Controls">
<Design.PreviewWith>
<StackPanel Width="32" Spacing="10">
<StackPanel.Resources>
<StreamGeometry x:Key="test_geometry">M3,3 8,6 13,3 10,8 13,13 8,10 3,13 6,8z</StreamGeometry>
</StackPanel.Resources>
<StackPanel Background="{DynamicResource SystemRegionBrush}">
<ui:BootstrapIcon Data="{StaticResource test_geometry}" />
</StackPanel>
</StackPanel>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type ui:BootstrapIcon}" TargetType="ui:BootstrapIcon">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{DynamicResource IconElementThemeHeight}" />
<Setter Property="Width" Value="{DynamicResource IconElementThemeWidth}" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<Viewbox Height="{TemplateBinding Height}" Width="{TemplateBinding Width}">
<Path Fill="{TemplateBinding Foreground}" Data="{TemplateBinding Data}" Stretch="Uniform" />
</Viewbox>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>
После этих изменений контрол BootstrapIcon
по функциональности станет копией PathIcon
. Снова проверьте компиляцию и отображение в дизайнере. Теперь контрол способен отображать простую геометрию, заданную через свойство Data
.
Шаг 3.2. Переход на использование BootstrapSymbol
В текущей реализации (PathIcon
-копии) геометрия задается через одно свойство Data
(типа Geometry
), а цвет — через унаследованный Foreground
(типа Brush
). Наш BootstrapIcon
должен уметь отображать двухцветную айконку, значение которой задается через основное свойство Symbol
(типа BootstrapSymbol
из SharedLib
). Значение из перечисления BootstrapSymbol
определяет геометрии и цвета фрагментов изображения.
Объем кода BootstrapIcon
после всех доработок значительно увеличится, но логика реализации достаточно простая и мы её реализуем в следующей последовательности:
Добавление свойства
Symbol
и производных от него внутренних свойств для двух частей геометрии и цветов.Модификация шаблона (
BootstrapIcon.axaml
) для использования новых внутренних свойств (вместо свойстваData
).Реализация конвертации данных: получение данных для
Symbol
, парсинг, прописывание во внутренние свойства.Реализация механизма кэширования данных.
Реализация логики обработки свойства
Symbol
: подписка на изменение значения и обработка изменений (конвертация значения в требуемый формат, использование кэширования).
Далее по шагам опишем эту трансформацию.
Добавление новых свойств
На этом шаге изменим файл BootstrapIcon.axaml.cs
: добавим основное свойство Symbol
и четыре производных от него (именно эти свойства будут использоваться для отрисовки). Устаревшее свойство Data
пока не удаляем (иначе будем получать ошибку компиляции до обновления шаблона BootstrapIcon.axaml
). В статическом конструкторе меняем параметр у вызова функции AffectsRender<BootstrapIcon>()
:
скрытый текст фрагмента BootstrapIcon.axaml.cs (свойства)
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using SharedLib.Controls;
namespace TryAvalonia.Controls;
public class BootstrapIcon : IconElement
{
static BootstrapIcon()
{
AffectsRender<BootstrapIcon>(SymbolProperty);
}
//...
#region // Properties
public static readonly StyledProperty<BootstrapSymbol> SymbolProperty =
AvaloniaProperty.Register<BootstrapIcon, BootstrapSymbol>(nameof(Symbol), BootstrapSymbol.None);
public BootstrapSymbol Symbol
{
get => GetValue(SymbolProperty);
set => SetValue(SymbolProperty, value);
}
// Производные внутренние свойства для отрисовки
internal static readonly StyledProperty<Geometry?> PrimaryGeometryProperty =
AvaloniaProperty.Register<BootstrapIcon, Geometry?>(nameof(PrimaryGeometry));
internal Geometry? PrimaryGeometry { get => GetValue(PrimaryGeometryProperty); set => SetValue(PrimaryGeometryProperty, value); }
internal static readonly StyledProperty<IBrush?> PrimaryForegroundProperty =
AvaloniaProperty.Register<BootstrapIcon, IBrush?>(nameof(PrimaryForeground));
internal IBrush? PrimaryForeground { get => GetValue(PrimaryForegroundProperty); set => SetValue(PrimaryForegroundProperty, value); }
internal static readonly StyledProperty<Geometry?> SecondaryGeometryProperty =
AvaloniaProperty.Register<BootstrapIcon, Geometry?>(nameof(SecondaryGeometry));
internal Geometry? SecondaryGeometry { get => GetValue(SecondaryGeometryProperty); set => SetValue(SecondaryGeometryProperty, value); }
internal static readonly StyledProperty<IBrush?> SecondaryForegroundProperty =
AvaloniaProperty.Register<BootstrapIcon, IBrush?>(nameof(SecondaryForeground));
internal IBrush? SecondaryForeground { get => GetValue(SecondaryForegroundProperty); set => SetValue(SecondaryForegroundProperty, value); }
#endregion
// ...
}
Теперь класс имеет необходимые свойства, но еще не обновляет их на основе Symbol
.
Модификация шаблона
Измените шаблон контрола в BootstrapIcon.axaml
, чтобы он использовал новые внутренние свойства и отображал два элемента Path
.
Для начала заменим <Design.PreviewWith>
, чтобы визуально проверять корректность работы XAML-шаблона в дизайн-режиме. Можно напрямую прописать PrimaryGeometry
и PrimaryForeground
для целей тестирования.
<Design.PreviewWith>
<StackPanel Margin="4" Spacing="8" Orientation="Horizontal">
<ui:BootstrapIcon PrimaryGeometry="M3,3 8,6 13,3 10,8 13,13 8,10 3,13 6,8z"
PrimaryForeground="BlueViolet"/>
<ui:BootstrapIcon Symbol="Bootstrap"/>
<ui:BootstrapIcon Symbol="PathIcons" />
<ui:BootstrapIcon Symbol="Custom1" Width="32" Height="32" />
<ui:BootstrapIcon Symbol="Custom2" Width="32" Height="32" />
</StackPanel>
</Design.PreviewWith>
Добавим в словарь ресурсов BootstrapIcon.axaml
определение значения BootstrapIconSize
(этот значение будет использоваться в сеттерах <ControlTheme>
и позволит централизованно управлять размером контрола). Текущее значение BootstrapIconSize
задает размер BootstrapIcon
по умолчанию.
<x:Double x:Key="BootstrapIconSize">20</x:Double>
BootstrapIconSize
- это замена определений IconElementThemeWidth
и IconElementThemeHeight
из общих ресурсов Avalonia, которые делает то же самое для PathIcon
.
Изменим блок сеттеров в <ControlTheme>
для свойств BootstrapIcon
: сеттер Foreground
заменим на два сеттера PrimaryForeground
и SecondaryForeground
, а вместо {DynamicResource IconElementThemeWidth}
и {DynamicResource IconElementThemeHeight}
теперь используем {DynamicResource BootstrapIconSize}
:
<ControlTheme x:Key="{x:Type ui:BootstrapIcon}" TargetType="ui:BootstrapIcon">
<Setter Property="PrimaryForeground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="SecondaryForeground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{DynamicResource BootstrapIconSize}" />
<Setter Property="Width" Value="{DynamicResource BootstrapIconSize}" />
...
</ControlTheme>
Использование {DynamicResource TextControlForeground}
означает, что в случае неопределенного цвета для фрагментов изображения (PrimaryForeground
, SecondaryForeground
) будет использован системный цвет для текстовых контролов (поведение BootstrapIcon
аналогично PathIcon
).
Заменим содержимое <ControlTemplate>
:
<ControlTheme x:Key="{x:Type ui:BootstrapIcon}" TargetType="ui:BootstrapIcon">
...
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<Viewbox Stretch="Uniform">
<Panel>
<Path Width="16" Height="16" Stretch="None"
Fill="{TemplateBinding PrimaryForeground}"
Data="{TemplateBinding PrimaryGeometry}" />
<Path Width="16" Height="16" Stretch="None"
Fill="{TemplateBinding SecondaryForeground}"
Data="{TemplateBinding SecondaryGeometry}"/>
</Panel>
</Viewbox>
</Border>
</ControlTemplate>
</Setter>
...
</ControlTheme>
Как работает содержимое <Viewbox>
мы уже описывали во "Введении" при создании прототипа BootstrapIcon
.
Добавим ещё в <ControlTheme>
триггер на изменение стиля для :disabled
, чтобы отличать запрещенные элементы интерфейса от нормальных (уменьшаем Opacity
).
<ControlTheme x:Key="{x:Type ui:BootstrapIcon}" TargetType="ui:BootstrapIcon">
...
<!-- Style to reduce opacity of the icon's panel when the control is disabled. -->
<Style Selector="^:disabled /template/ Panel">
<Setter Property="Opacity" Value="0.3" />
</Style>
</ControlTheme>
В классе BootstrapIcon.axaml.cs
удалите свойство DataProperty
и его обертку Data
, т.к. шаблон уже не использует это свойство.
Теперь шаблон готов отображать две части айконки, как только внутренние свойства будут заполнены логикой класса. Разработка шаблона завершена, больше изменений в файле BootstrapIcon.axaml
не потребуется.
Конвертация данных
Вспомним про функцию расширения GetGeometryDefinition()
, которая по текущему значению Symbol
(имеющему тип BootstrapSymbol
) может извлечь данные из атрибутов и вернуть кортеж (string primaryPath, uint primaryArgb, string secondaryPath, uint secondaryArgb)
. Нам надо обеспечить конвертацию этих данных в удобную форму для хранения (кэширования) и отображения (прописывания во внутренние свойства).
Для этого в класс BootstrapIcon
добавьте структуру SymbolParsed
, которая будет хранить распарсенные данные с UI-специфичными типами Geometry
и Brush
. Именно в эту структуру конвертируются полученные данные Symbol
для отображения, сохраняются в кэш, передаются в качестве параметров внутренних функций:
скрытый текст фрагмента BootstrapIcon.axaml.cs (структура SymbolParsed)
public class BootstrapIcon : IconElement
{
// ...
private readonly struct SymbolParsed(Geometry? primaryGeo, Brush? primaryBrush, Geometry? secondaryGeo, Brush? secondaryBrush)
{
public Geometry? PrimaryGeometry { get; } = primaryGeo;
public Brush? PrimaryForeground { get; } = primaryBrush;
public Geometry? SecondaryGeometry { get; } = secondaryGeo;
public Brush? SecondaryForeground { get; } = secondaryBrush;
// An empty SymbolParsed instance for error cases.
public static SymbolParsed Empty => new(null, null, null, null);
}
}
В класс BootstrapIcon
добавьте пару функций конвертации: uint
в Brush
и BootstrapSymbol
в структуру SymbolParsed
.
скрытый текст фрагмента BootstrapIcon.axaml.cs (конвертация)
public class BootstrapIcon : IconElement
{
// ...
// Converts an ARGB uint value into a SolidColorBrush, or null if the value is 0.
private static SolidColorBrush? CreateBrushFromArgb(uint ardb)
=> ardb == 0 ? null : new SolidColorBrush(ardb);
// Converts a BootstrapSymbol into parsed geometry and brush data for rendering.
private static SymbolParsed CreateSymbolParsed(BootstrapSymbol symb)
{
try
{
var (primaryPath, primaryArgb, secondaryPath, secondaryArgb) = symb.GetGeometryDefinition();
Geometry? primaryGeo = Geometry.Parse(primaryPath ?? "");
Brush? primaryBrush = CreateBrushFromArgb(primaryArgb);
Geometry? secondaryGeo = Geometry.Parse(secondaryPath ?? "");
Brush? secondaryBrush = CreateBrushFromArgb(secondaryArgb);
// Return parsed data as a SymbolParsed struct.
return new SymbolParsed(primaryGeo, primaryBrush, secondaryGeo, secondaryBrush);
}
catch
{
// Return empty data if parsing fails.
return SymbolParsed.Empty;
}
}
//...
}
Осталось добавить в класс BootstrapIcon
простую функцию, которая применяет (конвертирует) значение SymbolParsed
во внутренние свойства PrimaryGeometry
, PrimaryForeground
, SecondaryGeometry
, SecondaryForeground
.
скрытый текст фрагмента BootstrapIcon.axaml.cs (применение свойств)
public class BootstrapIcon : IconElement
{
//...
private void ApplySymbolData(SymbolParsed data)
{
PrimaryGeometry = data.PrimaryGeometry;
SecondaryGeometry = data.SecondaryGeometry;
if (data.PrimaryForeground != null)
{
PrimaryForeground = data.PrimaryForeground;
}
else
{
ClearValue(PrimaryForegroundProperty);
}
if (data.SecondaryForeground != null)
{
SecondaryForeground = data.SecondaryForeground;
}
else
{
ClearValue(SecondaryForegroundProperty);
}
}
//...
}
Обратите внимание на использование ClearValue()
для свойств контрола. Если цвет фрагмента изображения не определен (например, в атрибуте SymbolPath
не был указан цвет, или указан цвет с ARGB=0), то вместо прописывания null
в свойства PrimaryForeground
и SecondaryForeground
мы их очищаем (делаем неопределенными). В этом случае, Avalonia при отрисовке будет использовать системный цвет для текстовых контролов (значение {DynamicResource TextControlForeground}
).
Кэширование данных
Чтобы избежать повторного парсинга геометрии и создания кистей каждый раз при использовании одного и того же значения Symbol
, реализуем простой механизм кэширования. Для этого в класс BootstrapIcon
достаточно добавить статическое поле словаря для хранения кэша:
private static readonly Dictionary<BootstrapSymbol, SymbolParsed> _symbolDataCache = [];
Этот статический словарь _symbolDataCache
будет хранить структуру SymbolParsed
для каждого значения BootstrapSymbol
, с которым мы уже работали, обеспечивая быстрый доступ к уже готовым объектам Geometry
и Brush
. Механизм доступа к данным кэша обычный, например, используя _symbolDataCache.TryGetValue()
.
Логика обработки Symbol
Реализуем основную логику класса BootstrapIcon.axaml.cs
: обработку изменения свойства Symbol
, получение данных из SharedLib
(через локальный кэш), преобразование их в формат Avalonia и обновление внутренних свойств.
Для этого в статическом конструкторе класса настройте подписку на событие изменения SymbolProperty
и реализуйте логику получения данных (с проверкой кэша) и вызовом вспомогательных методов для парсинга и применения данных в функции OnSymbolChanged()
:
скрытый текст фрагмента BootstrapIcon.axaml.cs (изменение свойств)
public class BootstrapIcon : IconElement
{
// A cache to store parsed symbol data for each BootstrapSymbol to improve performance.
private static readonly Dictionary<BootstrapSymbol, SymbolParsed> _symbolDataCache = [];
// Static constructor to set up property change handling for the Symbol property.
static BootstrapIcon()
{
AffectsRender<BootstrapIcon>(SymbolProperty);
SymbolProperty.Changed.AddClassHandler<BootstrapIcon>(OnSymbolChanged);
}
// Handles changes to the Symbol property, updating the control's geometries and colors.
private static void OnSymbolChanged(BootstrapIcon icon, AvaloniaPropertyChangedEventArgs e)
{
var symb = e.GetNewValue<BootstrapSymbol>();
if (!_symbolDataCache.TryGetValue(symb, out var pathData))
{
// Parse and cache new symbol data
pathData = CreateSymbolParsed(symb);
_symbolDataCache.Add(symb, pathData);
}
icon.ApplySymbolData(pathData);
}
//...
}
После реализации этих методов ваш класс BootstrapIcon.axaml.cs
будет содержать всю логику, необходимую для загрузки, кэширования и применения данных иконок из SharedLib
, которые затем будут отображены шаблоном.
Ссылки на полные исходники BootstrapIcon для Avalonia:
На этом Шаг 3 завершен. Мы полностью реализовали контрол BootstrapIcon
для Avalonia. Теперь можем перейти к его использованию в интерфейсе приложения (Шаг 4).
Шаг 4: Использование BootstrapIcon для Avalonia
На этом шаге мы продемонстрируем использование контрола BootstrapIcon
в интерфейсе нашего Avalonia приложения (TryAvalonia
). Для этого мы модифицируем файл главного окна MainWindow.axaml
, чтобы создать простой псевдотулбар с кнопками, использующими наши цветные айконки.
Начнем со стандартной структуры окна Avalonia. Добавим пространство имен xmlns:ui="using:TryAvalonia.Controls"
. Создадим базовый <DockPanel>
с верхней областью для кнопок <StackPanel Classes="toolbar">
и центральной областью <TextBlock>
:
скрытый текст MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:TryAvalonia.Controls"
Width="650" Height="150"
mc:Ignorable="d" x:Class="TryAvalonia.MainWindow" Title="TryAvalonia">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Classes="toolbar" Orientation="Horizontal">
<!-- место размещения кнопок-->
</StackPanel>
<TextBlock Text="Welcome to Avalonia!" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</DockPanel>
</Window>
Внутри <StackPanel>
с DockPanel.Dock="Top"
мы будем размещать наши кнопки содержащие <ui:BootstrapIcon>
и <TextBlock>
. Пример определения одной такой кнопки:
<Button>
<StackPanel>
<ui:BootstrapIcon Symbol="SortAlphaDown" />
<TextBlock Text="Sort" />
</StackPanel>
</Button>
Здесь мы используем контрол <ui:BootstrapIcon>
и задаем его свойство Symbol
значением из нашего перечисления BootstrapSymbol
(например, "SortAlphaDown"
). Благодаря логике контрола, он получит геометрию и цвета для данной айконки из SharedLib
и отобразит ее.
Можно задать свои размеры ui:BootstrapIcon
по умолчанию (например, 24x24), для этого достаточно переопределить значение BootstrapIconSize
в ресурсах окна:
<Window.Resources>
<x:Double x:Key="BootstrapIconSize">24</x:Double>
</Window.Resources>
Для придания кнопкам вида панели инструментов мы можем задать для всех единый стиль прямо в окне:
<Window.Styles>
<Style Selector="StackPanel.toolbar">
<Setter Property="Background" Value="#EEF5FD"/>
</Style>
<Style Selector="StackPanel.toolbar Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="MinWidth" Value="60"/>
</Style>
<Style Selector="StackPanel.toolbar Button TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
</Style>
</Window.Styles>
Теперь, собрав все эти части вместе и добавив больше кнопок по примеру выше в верхний StackPanel
, вы получите что-то похожее на тулбар с разнообразными цветными айконками BootstrapIcon
.
Полный код AppIcons/TryAvalonia/MainWindow.axaml на GitHub.
Запустите проект TryAvalonia
, чтобы увидеть результат.
На этом Шаг 4 завершен. Мы успешно использовали наш контрол в Avalonia приложении.
Шаг 5: Создание BootstrapIcon для WPF

На этом этапе мы реализуем контрол BootstrapIcon
для WPF-приложения в проекте TryWpf
.
Структура раздела
Шаг 5.1. Создание заготовки для BootstrapIcon
Шаг 5.2. Адаптация класса BootstrapIcon для WPF
Шаг 5.3. Адаптация темы BootstrapIcon для WPF
Создавать BootstrapIcon
для WPF полностью "с нуля" не будем, адаптируем созданный BootstrapIcon
для Avalonia с учетом особенностей реализации пользовательских контролов для WPF. Как и в Avalonia-версии, наш контрол будет использовать определения айконок из кросс-платформенной библиотеки SharedLib
.
Шаг 5.1. Создание заготовки для BootstrapIcon
В отличие от Avalonia, WPF не предоставляет удобный шаблон через dotnet CLI
для создания пустого пользовательского контрола с темой, а также не имеет штатного аналога PathIcon
.
Поэтому создадим необходимые файлы вручную. В проекте TryWpf
создайте новую папку Controls
и в ней два файла: BootstrapIcon.cs
и BootstrapIcon.xaml
.
скрытый текст BootstrapIcon.cs
using System.Windows.Controls;
namespace TryWpf.Controls;
public class BootstrapIcon : Control
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(BootstrapIcon), new FrameworkPropertyMetadata(typeof(BootstrapIcon)));
}
скрытый текст BootstrapIcon.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:TryWpf.Controls">
<Style TargetType="{x:Type ui:BootstrapIcon}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:BootstrapIcon}">
<TextBlock Text="BootstrapIcon WPF Placeholder" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Теперь необходимо включить стиль нашего контрола, определенный в BootstrapIcon.xaml
, в ресурсы приложения. Откройте файл App.xaml
в корне проекта TryWpf
и добавьте ссылку на файл в секцию <Application.Resources>
:
скрытый текст App.xaml
<Application x:Class="TryWpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TryWpf"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/TryWpf;component/Controls/BootstrapIcon.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Проверьте, что проект компилируется без ошибок. На данном этапе у вас есть пустой WPF контрол с базовым стилем и шаблоном-заглушкой.
Шаг 5.2. Адаптация класса BootstrapIcon для WPF
Переносим и адаптируем логику класса BootstrapIcon.axaml.cs
(из Шага 3) для использования в WPF. Основные отличия в классе BootstrapIcon.cs
для WPF будут связаны с моделью свойств и системой рендеринга, наиболее существенные из них:
Класс наследуется от
System.Windows.Controls.Control
(вместоIconElement
).Для определения свойств используется
System.Windows.DependencyProperty
(вместоStyledProperty
).Регистрация и подписка на изменения свойств реализуется по-другому.
Логика получения данных из SharedLib
через GetGeometryDefinition()
и кэширования аналогична Avalonia-версии.
Шаг 5.3. Адаптация темы BootstrapIcon для WPF
Адаптируем XAML-тему BootstrapIcon.axaml
(из Шага 3) для использования в WPF. Ключевые моменты адаптации BootstrapIcon.xaml
:
Используется
<Style>
вместо<ControlTheme>
.Используется
<Grid>
вместо<Panel>
.Используется
<ControlTemplate.Triggers>
вместо<Style Selector="...">
.
В целом, XAML-структура и принципы привязки данных очень схожи, но требуется адаптация синтаксиса и специфичных для WPF элементов/свойств.
Ссылки на полные исходники BootstrapIcon для WPF:
Код класса: AppIcons/TryWpf/Controls/BootstrapIcon.cs
На этом Шаг 5 завершен. Мы подготовили и адаптировали код контрола BootstrapIcon
для использования в WPF-приложении. В следующем шаге продемонстрируем его использование.
Шаг 6: Использование BootstrapIcon для WPF
На этом шаге продемонстрируем использование контрола BootstrapIcon
в интерфейсе WPF-приложения TryWpf
. Для этого модифицируем файл главного окна MainWindow.xaml
, задействовав штатный ToolBar
.
Добавьте в корневой элемент окна пространство имен нашего контрола: xmlns:ui="clr-namespace:TryWpf.Controls"
.
Внутри <DockPanel>
добавляем <ToolBarTray DockPanel.Dock="Top">
с единственным <ToolBar>
. На этом тулбаре мы будем размещать наши кнопки с экземпляром <ui:BootstrapIcon>
и <TextBlock>
для подписи. Кроме того (в отличии от Avalonia), у нас появилась возможность использовать <Separator>
. Пример определения одной такой кнопки и разделителя:
<Button>
<StackPanel>
<ui:BootstrapIcon Symbol="WindowSidebar" />
<TextBlock Text="Sidebar" />
</StackPanel>
</Button>
<Separator/>
скрытый текст MainWindow.xaml (пример с одной кнопкой и разделителем)
<Window x:Class="TryWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:TryWpf.Controls"
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
mc:Ignorable="d"
Title="TryWpf" Height="150" Width="600">
<DockPanel>
<ToolBarTray DockPanel.Dock="Top" IsLocked="True">
<ToolBar>
<Button>
<StackPanel>
<ui:BootstrapIcon Symbol="WindowSidebar"/>
<TextBlock Text="Sidebar"/>
</StackPanel>
</Button>
<Separator/>
</ToolBar>
</ToolBarTray>
<TextBlock Text="Welcome to Wpf!" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</DockPanel>
</Window>
Добавьте нужный набор кнопок на тулбар, определите подходящие стили. Здесь размер айконок также можно менять при помощи значения ресурса <sys:Double x:Key="BootstrapIconSize">32</sys:Double>
.
Полный код AppIcons/TryWpf/MainWindow.xaml на GitHub.
Запустите проект TryWpf
, чтобы увидеть результат.
На этом Шаг 6 завершен. Мы успешно использовали наш контрол BootstrapIcon
в WPF-приложении.
Заключение
В этом туториале мы прошли путь от выбора коллекции векторных иконок Bootstrap Icons до реализации готового к использованию контрола BootstrapIcon
для Avalonia и WPF. Мы разработали кросс-платформенную библиотеку SharedLib
для централизованного хранения определений иконок и реализовали UI-специфические контролы, способные отображать двухцветные векторные иконки на основе этих данных.
Полученное решение позволяет эффективно использовать цветные, масштабируемые векторные айконки в десктопных приложениях на обеих платформах, используя единую базу определений. Вы можете легко расширить набор иконок, добавив свои или адаптировав данные из других источников, а также реализовать дополнительные возможности по управлению внешним видом контрола.
Этот туториал поможет вам создавать более привлекательные и функциональные пользовательские интерфейсы в ваших приложениях на Avalonia/WPF.
? Полезные ссылки
Исходный код AvaloniaUI (версия 11.3.0) на GitHub: ссылка
Официальный сайт Bootstrap Icons: icons.getbootstrap.com
Онлайн-редактор SVG-путей: SvgPathEditor
Генератор кода перечисления
BootstrapSymbol
(npm пакет): path-iconsИсходный код примера из этого туториала на GitHub: ссылка
? Продолжение следует...
Не удаляйте созданные тестовые проекты, они могут пригодится. В следующих туториалах будет продолжена их доработка: будут добавлены примеры использованияBootstrapIcon
в главном меню приложения и аналоге ToolBar.
Комментарии (10)
xtraroman
09.06.2025 07:15Мы решали подобную задачку в EMXControls. Сделали сборку для векторных иконок. На source generator создаем из названий файлов специальный класс свойства которого называются так же как иконки. За счет этого работают подсказки в VisualStudio и невозможно использовать иконку которой нет в наборе. В коде получается вот так
Eremex.AvaloniaUI.Icons.Basic.Question
в xaml
Glyph="{x:Static icons:Basic.Question}"
для раскраски иконок используем css
Как нибудь напишем статью об этом.
edward_nsk Автор
09.06.2025 07:15для раскраски иконок используем css
А это как сделать в Avalonia?
xtraroman
09.06.2025 07:15посмотрите этот пример
https://github.com/wieslawsoltes/Svg.Skia/tree/master/samples/AvaloniaSvgSkiaStylingSample
Svg.Css - это attached inherited свойство. Работает на поддерево.
edward_nsk Автор
09.06.2025 07:15Svg.Css - это attached inherited свойство. Работает на поддерево.
Спасибо, посмотрю как они это реализовали. Только для нашего случая, наверно, будет тяжеловато. И по рендерингу, и по программной реализации.
edward_nsk Автор
09.06.2025 07:15Пример подсказок Visual Studio Согласен, при помощи класса тоже хороший вариант. Для моей реализации подсказки в дизайнере Visual Studio тоже работают, причем без
{x:Static}
и, что удивительно, даже namespace не надо указывать.upd: Компилятор тоже не даст указать несуществующую айконку.
Colt045
09.06.2025 07:15На момент написания статьи большинство (2070 из 2074) Bootstrap Icons используют для отрисовки исключительно элементы
<path>
(не задействуют<circle>
,<rect>
)Если кому интересно, на сейчас из 2078 иконок, богопротивные элементы circle и rect есть в следующих файлах:
align-bottom
align-top
circle-fill
dice-1ПыСы Извините, немного вопрос не по окладу: а почему "айконка", а не "иконка"?
edward_nsk Автор
09.06.2025 07:15Абсолютно точно, именно эти 4 штуки мне кровь портили.
Поэтому я в своем генераторе их переопределяю.{ "align-bottom": [ "M6 1h4a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z", "M1.5 14a.5.5 0 0 0 0 1zm13 1a.5.5 0 0 0 0-1zm-13 0h13v-1h-13z" ], "align-top": [ "M6 15h4a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1z", "M1.5 2a.5.5 0 0 1 0-1zm13-1a.5.5 0 0 1 0 1zm-13 0h13v1h-13z" ], "circle-fill": [ "M16 8A8 8 0 1 1 0 8A8 8 0 1 1 16 8Z" ], "dice-1": [ "M9.5 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z", "M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3z" ] }
Результат работы генератора:
> path-icons@0.0.13 update-bi D:\Git\path-icons > node build/update-bi-json.mjs --verbose Starting Bootstrap Icons JSON build... Found 2078 SVG file(s) to process from D:\Git\path-icons\node_modules\bootstrap-icons\icons WARN: Skipping align-bottom.svg. Contains disallowed element type: <rect>. WARN: Skipping align-top.svg. Contains disallowed element type: <rect>. WARN: Skipping circle-fill.svg. Contains disallowed element type: <circle>. WARN: Skipping dice-1.svg. Contains disallowed element type: <circle>. --- Build Summary --- Total SVG files found: 2078 - 2074 successfully processed and included - 4 skipped (invalid size/content/no paths/error) Path Count Distribution (for processed icons): - 1217 icon(s) with 1 path - 753 icon(s) with 2 paths - 92 icon(s) with 3 paths - 9 icon(s) with 4 paths - 3 icon(s) with 5 paths Output JSON written to: D:\Git\path-icons\src\bi.json Build completed: 112.916ms
PS: простите за мой француcкий, проверявший меня на ошибки и опечатки ИИ не ругался.
Ydav359
Супер, но разве хранить геометрию в атрибуте не излишне? Это же рефлексия для извлечения. Просто вопрос, если что
edward_nsk Автор
Это один из вариантов хранения в C#-коде, когда всё в одном месте (обозримость, легкая правка). Для меня было главное - уйти от ресурсов в XAML и как-то автоматизировать включение фрагментов SVG-файла в свой код. Возможно, не самый оптимальный вариант. Что можете предложить взамен?
Ydav359
Эти вопросы я больше для себя задаю, чтобы лучше понимать тему, спасибо за ответ!