mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-22 09:23:53 +08:00
13 KiB
13 KiB
Icon 包制作指导手册
本文档指导如何基于 AtomUI Icon 基础设施创建自定义 Icon 包。
1. 前提条件
- 准备好 SVG 图标源文件,按主题分目录存放(如
Filled/、Outlined/、TwoTone/) - SVG 文件命名使用 kebab-case(如
check-circle.svg),生成器会自动转为 PascalCase 类名 - SVG 中应使用标准图形元素:
<path>、<rect>、<circle>、<ellipse>、<line>、<polyline>、<polygon>,避免使用<use>、<symbol>、<defs>等引用元素
2. 创建步骤
步骤 1: 创建 Icon 包项目(运行时类库)
创建一个新的类库项目,如 MyCompany.Icons.MyIconSet:
<!-- MyCompany.Icons.MyIconSet.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(AtomUITargetFrameworks)</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<RootNamespace>MyCompany.Icons.MyIconSet</RootNamespace>
<OutputType>library</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AtomUI.Core/AtomUI.Core.csproj" />
<!-- 或者使用 NuGet 包引用 -->
<!-- <PackageReference Include="AtomUI.Core" /> -->
</ItemGroup>
</Project>
步骤 2: 创建包级 Icon 基类
如果你的图标包需要特殊的渲染逻辑(如 Ant Design 的 ZoomToFit),创建一个包级基类;否则可以直接继承 Icon。
// MyIconSetIcon.cs
using AtomUI.Controls;
using Avalonia;
namespace MyCompany.Icons.MyIconSet;
public class MyIconSetIcon : Icon
{
// 如果需要自定义全局几何变换,覆写此方法
// 否则使用默认的 Matrix.Identity
protected override Matrix CalculateGlobalGeometryMatrix()
{
// 可参考 AntDesignIcon 的 ZoomToFit 实现
return base.CalculateGlobalGeometryMatrix();
}
}
步骤 3: 创建 IconProvider
// MyIconSetIconProvider.cs
using System.Reflection;
using AtomUI.Controls;
namespace MyCompany.Icons.MyIconSet;
public class MyIconSetIconProvider : IconProvider<MyIconSetIconKind>
{
public MyIconSetIconProvider()
{
}
public MyIconSetIconProvider(MyIconSetIconKind kind)
: base(kind)
{
}
protected override Type GetTypeForKind(MyIconSetIconKind kind)
{
var typeName = $"MyCompany.Icons.MyIconSet.{kind.ToString()}";
var type = Type.GetType(typeName)
?? Assembly.GetExecutingAssembly().GetType(typeName);
if (type == null)
{
throw new InvalidOperationException($"Type {typeName} does not exist");
}
return type;
}
}
步骤 4: 注册 AXAML 命名空间
创建 Properties/AssemblyInfo.cs:
using Avalonia.Metadata;
[assembly: XmlnsPrefix("https://mycompany.com/icons/myiconset", "myicons")]
[assembly: XmlnsDefinition("https://mycompany.com/icons/myiconset", "MyCompany.Icons.MyIconSet")]
步骤 5: 创建生成器项目
创建一个控制台项目,如 MyCompany.Icons.MyIconSet.Generator:
<!-- MyCompany.Icons.MyIconSet.Generator.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(AtomUITargetFrameworks)</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AtomUI.Icons.Shared/AtomUI.Icons.Shared.csproj" />
<!-- 或者使用 NuGet 包引用 -->
<!-- <PackageReference Include="AtomUI.Icons.Shared" /> -->
</ItemGroup>
</Project>
步骤 6: 实现生成器
// MyIconSetGenerator.cs
using System.Text;
using AtomUI.Controls;
using AtomUI.Icons;
namespace MyCompany.Icons.MyIconSet.Generator;
public class MyIconSetGenerator : DefaultIconPackageGenerator
{
public MyIconSetGenerator(string sourcePath, string targetPath)
: base(sourcePath, targetPath)
{
PackageName = "MyIconSet";
PackageNamespace = "MyCompany.Icons.MyIconSet";
}
public static async Task<int> Main(string[] args)
{
try
{
var targetProjectPath = Path.GetFullPath(
Path.Combine(Directory.GetCurrentDirectory(),
"../../../../src/MyCompany.Icons.MyIconSet"));
var sourceProjectPath = Path.GetFullPath(
Path.Combine(Directory.GetCurrentDirectory(),
"../../../../src/MyCompany.Icons.MyIconSet.Generator"));
var sourcePath = Path.Combine(sourceProjectPath, "Assets/Svg");
var generator = new MyIconSetGenerator(sourcePath, targetProjectPath);
await generator.GenerateAsync();
return 0;
}
catch (Exception e)
{
Console.Error.WriteLine($"Generate error: {e.Message}");
return 1;
}
}
protected override async Task GenerateIconPackageClass(
IconFileInfo iconFileInfo, Stream output)
{
var sourceText = new StringBuilder();
sourceText.AppendLine("// This code is auto generated. Do not modify.");
sourceText.AppendLine("using Avalonia;");
sourceText.AppendLine("using System;");
sourceText.AppendLine("using Avalonia.Media;");
sourceText.AppendLine("using AtomUI.Controls;");
sourceText.AppendLine("using AtomUI.Media;");
sourceText.AppendLine($"namespace {PackageNamespace};");
sourceText.AppendLine();
var svgSource = await File.ReadAllTextAsync(iconFileInfo.FilePath);
var svgParsedInfo = SvgParser.Parse(svgSource);
var viewBox = svgParsedInfo.ViewBox;
var className = $"{iconFileInfo.Name}{iconFileInfo.ThemeType}";
// 类声明 - 继承你的包级基类
sourceText.AppendLine($"public class {className} : MyIconSetIcon");
sourceText.AppendLine("{");
// 构造函数
sourceText.AppendLine($" public {className}()");
sourceText.AppendLine(" {");
sourceText.AppendLine($" IconTheme = IconThemeType.{iconFileInfo.ThemeType};");
sourceText.AppendLine($" ViewBox = new Rect({viewBox.X}, {viewBox.Y}, {viewBox.Width}, {viewBox.Height});");
sourceText.AppendLine(" }");
sourceText.AppendLine();
// 绘图指令
sourceText.AppendLine(" private static readonly DrawingInstruction[] StaticInstructions = [");
for (var i = 0; i < svgParsedInfo.GraphicElements.Count; i++)
{
var element = svgParsedInfo.GraphicElements[i];
if (element is PathElement pathElement)
{
sourceText.AppendLine(" new PathDrawingInstruction()");
sourceText.AppendLine(" {");
sourceText.AppendLine($" Data = StreamGeometry.Parse(\"{pathElement.Data}\"),");
// 根据主题类型决定画刷
switch (iconFileInfo.ThemeType)
{
case IconThemeType.Filled:
sourceText.AppendLine(" FillBrush = IconBrushType.Fill,");
break;
case IconThemeType.Outlined:
sourceText.AppendLine(" FillBrush = IconBrushType.Stroke,");
break;
default:
sourceText.AppendLine(" FillBrush = IconBrushType.Fill,");
break;
}
if (!string.IsNullOrEmpty(pathElement.Transform))
{
sourceText.AppendLine($" Transform = TransformParser.Parse(\"{pathElement.Transform}\").Value");
}
sourceText.AppendLine(" }");
}
// 类似地处理 RectElement, CircleElement 等...
if (i != svgParsedInfo.GraphicElements.Count - 1)
{
sourceText.Append(", ");
}
}
sourceText.AppendLine(" ];");
sourceText.AppendLine();
sourceText.AppendLine(" protected override IList<DrawingInstruction> DrawingInstructions => StaticInstructions;");
sourceText.AppendLine("}");
await output.WriteAsync(Encoding.UTF8.GetBytes(sourceText.ToString()));
}
}
步骤 7: 放置 SVG 资源
MyCompany.Icons.MyIconSet.Generator/
└── Assets/
└── Svg/
├── Filled/
│ ├── home.svg
│ ├── user.svg
│ └── ...
├── Outlined/
│ ├── home.svg
│ ├── user.svg
│ └── ...
└── TwoTone/ (可选)
└── ...
步骤 8: 运行生成器
cd src/MyCompany.Icons.MyIconSet.Generator
dotnet run
生成器会:
- 扫描
Assets/Svg/下所有.svg文件 - 在
src/MyCompany.Icons.MyIconSet/GeneratedIcons/下生成:MyIconSetIconKind.g.cs— 枚举HomeFilled.g.cs、HomeOutlined.g.cs、UserFilled.g.cs... — 图标类
3. 项目最终结构
MyCompany.Icons.MyIconSet/
├── MyIconSetIcon.cs ← 包级基类
├── MyIconSetIconProvider.cs ← MarkupExtension
├── MyCompany.Icons.MyIconSet.csproj
├── Properties/
│ └── AssemblyInfo.cs ← AXAML 命名空间注册
└── GeneratedIcons/ ← 自动生成(不手动编辑)
├── MyIconSetIconKind.g.cs
├── HomeFilled.g.cs
├── HomeOutlined.g.cs
└── ...
MyCompany.Icons.MyIconSet.Generator/
├── MyIconSetGenerator.cs
├── MyCompany.Icons.MyIconSet.Generator.csproj
└── Assets/
└── Svg/
├── Filled/
├── Outlined/
└── TwoTone/
4. 注意事项与最佳实践
4.1 SVG 文件要求
- ViewBox:SVG 根元素必须包含
viewBox属性(如viewBox="0 0 1024 1024") - 图形元素:只使用
<path>、<rect>、<circle>、<ellipse>、<line>、<polyline>、<polygon> - 避免的元素:
<use>、<symbol>、<defs>、<filter>、<mask>、<clipPath>(clipPath 会被跳过) <g>分组:支持一层<g>分组,子元素会继承<g>的fill、stroke、transform等属性- 命名规范:文件名使用
kebab-case(如check-circle.svg),自动转为CheckCircle类名
4.2 主题目录命名
SVG 文件的父目录名必须与 IconThemeType 枚举值匹配(不区分大小写):
| 目录名 | IconThemeType |
|---|---|
Filled/ |
Filled |
Outlined/ |
Outlined |
TwoTone/ |
TwoTone |
Rounded/ |
Rounded |
Sharp/ |
Sharp |
MultiColor/ |
MultiColor |
如果目录名不匹配任何枚举值,默认为 Filled。
4.3 生成代码管理
GeneratedIcons/目录中的文件全部为自动生成,请勿手动编辑- 每次运行生成器会完全清空并重新生成该目录
- 建议将生成代码纳入版本管理(而非
.gitignore),以便 CI/CD 环境无需运行生成器
4.4 枚举值命名
生成的枚举值名称 = {PascalCaseName}{ThemeType},必须与生成的类名一致,IconProvider 通过枚举名查找类型。
4.5 非 <path> 元素处理
DefaultIconPackageGenerator 默认只生成了枚举和扫描逻辑,具体图形元素的代码生成由 GenerateIconPackageClass 方法负责。如果你的图标包含非 <path> 元素(<rect>、<circle> 等),需要在 GenerateIconPackageClass 中添加对应的代码生成逻辑:
// 处理 <rect> 元素示例
if (element is RectElement rectElement)
{
sourceText.AppendLine(" new RectDrawingInstruction()");
sourceText.AppendLine(" {");
sourceText.AppendLine($" Rect = new Rect({rectElement.X}, {rectElement.Y}, {rectElement.Width}, {rectElement.Height}),");
sourceText.AppendLine($" RadiusX = {rectElement.RadiusX},");
sourceText.AppendLine($" RadiusY = {rectElement.RadiusY},");
sourceText.AppendLine(" FillBrush = IconBrushType.Fill,");
sourceText.AppendLine(" }");
}
// 处理 <circle> 元素示例
if (element is CircleElement circleElement)
{
sourceText.AppendLine(" new CircleDrawingInstruction()");
sourceText.AppendLine(" {");
sourceText.AppendLine($" Radius = {circleElement.Radius},");
sourceText.AppendLine($" Center = new Point({circleElement.CenterX}, {circleElement.CenterY}),");
sourceText.AppendLine(" FillBrush = IconBrushType.Fill,");
sourceText.AppendLine(" }");
}
4.6 自定义 TwoTone 颜色判断
如果你的图标包有 TwoTone 图标,需要根据图标源的颜色约定实现主/次色判断逻辑。参考 AntDesignGenerator 中的实现:通过维护一个次色模板颜色列表来区分主色和次色区域。
4.7 使用已发布的 NuGet 包
如果是在 AtomUI 仓库外部创建 Icon 包,将项目引用替换为 NuGet 包引用:
<!-- 生成器项目 -->
<PackageReference Include="AtomUI.Icons.Shared" Version="x.x.x" />
<!-- Icon 包项目 -->
<PackageReference Include="AtomUI.Core" Version="x.x.x" />