Files
AtomUI/docs/Icons.Shared/IconPackageCreationGuide.md
T

13 KiB
Raw Blame History

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

生成器会:

  1. 扫描 Assets/Svg/ 下所有 .svg 文件
  2. 在 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" />