
这是一个删除动画:选中的内容被吸进黑洞,周围的内容跟着弯曲,点撤销再转出来。它是一个 Avalonia 控件,写在 C# 里。
TKCode.Arc 1.0.0 今天正式发布。这是我做的一套 Avalonia 12 组件库,MIT 协议。这篇不讲情怀,主要讲它怎么搭起来的:设计令牌、弹簧动效、性能规则,以及用 Claude Code 写这么大一个项目的真实账单。
先说清楚:视觉语言和动效来自 Arc(uiarc.dev),我把它的 112 个免费组件移植成了 Avalonia 原生控件,这是我的个人项目,不是 Arc 官方出品。黑洞、全息卡这类特效组件是原创设计,交互和动效参考了 Arc Pro。
先跑起来
需要 .NET 10 SDK。这三条命令会生成一个带对话列表和流式回复的 AI 聊天桌面应用:
bash
dotnet new install TKCode.Arc.Templates
dotnet new tkcode-arc-ai-chat -n Assistant
dotnet run --project Assistant
回复默认是预设的示例内容,换成你自己的模型客户端就能接真模型。把模板名换成 tkcode-arc-admin 是管理后台,换成 tkcode-arc-devtool 是 IDE 式开发工具,tkcode-arc-lib 用来起一个你自己的控件库。
已有项目也可以直接加包,Application 类和 App.axaml 都不用为它改动:
csharp
AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.WithArcFonts()
.UseArc(arc => arc
.UseArcControls()
.WithTheme(t => t.Appearance(ArcAppearance.System)
.Accent(ArcAccentPreset.Neutral)));
里面有什么
| 包 | 内容 |
|---|---|
TKCode.Arc |
112 个 Arc 组件,以及布局面板和 Markdown、PropertyGrid、Cascader、TransferBox 等扩展控件 |
TKCode.Arc.Blocks |
页面级组合:AI 对话、定价与结账、账单、仪表盘、团队管理 |
TKCode.Arc.AdvancedCharts |
DataGrid、SankeyFlow、Sunburst、RaceBarChart 等 |
TKCode.Arc.Special |
特效组件:表面、加载、语音、指针特效 |
TKCode.Arc.Workflow / .Dock / .Terminal |
工作流画布、IDE 式停靠布局、基于 libghostty-vt 的终端 |
TKCode.Arc.Pdf / .Audio |
PDF 查看器(桌面 PDFium,浏览器 pdf.js)、音频播放 |
TKCode.Arc.Foundation |
令牌、主题、语言和包注册运行时,只依赖 Avalonia |
一共 21 个 NuGet 包,同一个版本一起发布,面向 net8.0、net10.0 和 net11.0(终端包只有后两个),基于 Avalonia 12.1.4。300 多个文档页都有在线示例和自动生成的 API 表格。
设计令牌:控件不写死任何颜色
颜色、圆角、阴影和字体全部来自全局令牌,主题文件里这样取:
xml
<Setter Property="Background" Value="{arc:ArcDesignTokenResource Surface}" />
单个控件的几何参数放在控件令牌里,和控件放在一起:
csharp
[ControlDesignToken]
internal sealed class ButtonToken : AbstractControlDesignToken
{
public Thickness InlinePaddingMD { get; set; }
public double HoverOpacity { get; set; }
public override void CalculateTokenValues(bool isDarkMode)
{
base.CalculateTokenValues(isDarkMode);
InlinePaddingMD = new Thickness(16, 0);
HoverOpacity = 0.91;
}
}
切主题只有一个入口,每个令牌按外观和强调色重新计算,只写入变化的值,控件原地更新:
csharp
ArcTheme.Apply(t => t.Appearance(ArcAppearance.Dark));
这里有个坑:ArcThemeManager.ApplyAsync 的发布步骤要回到 UI 线程,在 UI 线程上 .Wait() 或 .Result 会死锁。要么 await,要么调用 Apply。
规则靠源生成器兜底。比如每个公开控件必须有对应的令牌类,令牌属性类型有限制,同一个 ControlTheme 的 Key 只能由一个文件定义,违反了会直接报 ARCF 开头的诊断,不用等到运行时才发现。主题文件也由生成器加载,不需要手工合并 *Themes.axaml。
动效:写在主题里,不写在代码里

大部分动效声明在 ControlTheme 里,按动效级别分开:
xml
<ControlTheme x:Key="{x:Type arc:Foo}" TargetType="arc:Foo">
<Setter Property="arc:ArcMotion.Tracks" Value="True" />
<Style Selector="^:motion-full">
<Setter Property="Transitions">
<Transitions>
<arc:ArcBrushTransition Property="Background" />
<arc:SpringTransformOperationsTransition
Property="RenderTransform" Spring="Snappy" />
</Transitions>
</Setter>
</Style>
<Style Selector="^:motion-reduced">
<Setter Property="Transitions">
<Transitions>
<arc:ArcBrushTransition Property="Background" Tween="Instant" />
</Transitions>
</Setter>
</Style>
</ControlTheme>
ArcMotion.Tracks 让控件在 Loaded 之后才拿到这两个伪类,所以首帧不会播动画。动效级别有 Full、Reduced、Off 三档,是可继承的附加属性,Off 下两个伪类都不存在。
弹簧按呈现时长和回弹定义,不用手调物理常数,比如 Snappy 是 0.26 秒、回弹 0.12。声明式弹簧被打断时从当前值以零速度重新开始。需要保留速度的跟手动画才用代码:
csharp
_progress ??= new MotionValue<double>(this, 0, MotionVectors.Double);
_progress.Changed += _ => InvalidateVisual();
await _progress.AnimateTo(1, MotionSpec.Spring(ArcSpring.Snappy));
AnimateTo 返回 Task<bool>,到达目标为 true,被打断为 false。帧由统一的调度器通过 TopLevel.RequestAnimationFrame 驱动,空闲时不请求帧。
性能:几条踩出来的规则

动画组件一多,性能问题就很具体。这些写进了项目的实现规则:
- 逐帧绘制的文本缓存
TextLayout,不要缓存FormattedText。 Avalonia 12 的FormattedText.Draw每次都重新分行和塑形,项目记录的开销是拉丁文约 3 KB / 0.3 ms,中文约 22 KB。TextLayout保留排好的行,绘制时只输出字形。 - 枚举状态用伪类,不用属性选择器。
^[Variant=Primary]每个实例都会建一个激活器、订阅属性并装箱比较。把枚举映射成:primary再用^:primary,按钮、列表项这类重复出现的控件优先改。 - 少用的部件按需创建。 每个模板节点挂载约 5 到 10 KB,隐藏的节点照样付费。焦点环、加载转圈、悬停气泡都在第一次需要时才创建。
- 重复项自绘,动画不触发布局。 图表的点和格子用一个控件的
Render画,动画只改变换、透明度和自绘状态。 - 循环动画要有边界。 只在可见、已挂载、在视口内且动效级别允许时运行。
效果举个例子:GanttTimeline 缩放 60 个任务,每帧分配从约 1.8 MB 降到约 100 KB,视图外的行、条和箭头也不再绘制。
AOT 和按需编译
库不使用反射,按兼容裁剪和 NativeAOT 的方式构建:
bash
dotnet publish -c Release -p:PublishAot=true
如果用 .NET 11 SDK 做 AOT 发布要注意:它的优化发布会丢掉 Avalonia 12.1 编译好的 XAML,启动时报 No precompiled XAML found。在项目文件里加上 <UseOptimizedPublish>false</UseOptimizedPublish>。
Lucide 图标也不用全部打进包里。源生成器会扫描 C# 和 XAML,只编译用到的图标,运行时才决定的图标在项目文件里声明:
xml
<ItemGroup>
<LucideIcon Include="Bell;ArrowRight" />
</ItemGroup>
枚举选择器 EnumSelector 也是同样的思路:成员和显示文字由生成器产出,不靠反射。
也能干正经活

PdfViewer 有缩略图、目录、搜索、文本选择和链接。没有后端时文档会报错并给出提示,不会显示假页面。

DataGrid 支持区域选择、行内编辑、填充柄、剪贴板、分组和导出。工作流画布支持平移、缩放、自动排列和撤销,停靠布局可以存成 JSON。
这套东西花了多少钱
从第一次提交到 1.0 用了 19 天,282 次提交,其中 263 次带着 Claude 的共同署名。Claude 订阅加 API 费用近 2 万元,都是我自己付的,不含我自己的时间。
我扫了一遍本机的 Claude Code 会话记录,这个项目大约用了:
- 请求:约 9 万次
- Token 总量:约 243 亿
- 其中缓存读取:约 237 亿,占 97.6%
- 模型实际输出:约 9300 万
项目的约束尽量写成了能被机器检查的东西:AGENTS.md 里的硬规则、生成器的诊断、缺文档页就失败的测试,以及令牌和预设的基线快照。仓库里现在有 1 万多个测试方法。
能不能用,还是请你自己判断。代码和提交记录都是公开的,每个版本修了哪些 bug 都列在 CHANGELOG 里。
还没做好的
- 移动端组件只在桌面的手机外框里预览过,还没上过 Android 和 iOS 真机。
- 视频组件还在仓库里,没有发布到 NuGet。
- PDF 查看器不支持触屏拖选文字;没有内嵌中日韩字体的文档,显示效果取决于机器上的字体。
从 1.0 起遵循语义化版本,破坏性 API 变更只在新的主版本出现。和上一个预览版对比了 11 个包的 API,没有发现破坏性变更。不过很多不常显示的部件已经移出模板,如果你的自定义 ControlTheme 或代码查找这些 PART_ 名称,要处理它们不存在的情况。
地址
- GitHub:https://github.com/AIDotNet/TKCode.Arc
- 在线文档和示例:https://docs.tkcode.si/(跑在 WebAssembly 上,性能不代表桌面端)
- QQ 群:922379598(Avalonia入门交流群)
有问题欢迎提 Issue。如果你正好在用 Avalonia,我最想听的是哪个控件用起来不顺手。