C#跨平台开发的最新选择-Jalium.UI
VeryJokerJal/Jalium.UI 一个崭新的跨平台框架
大家好,我是搬砖工呀。
长久以来,C#跨平台开发有AvaloinaUI和Uno.都是类WPF而不是.都束缚在WPF的圈子内,并没有大胆的突破创新。
搬砖工卧底开发群数月,研究学习,今天觉得总算有资格可以介绍下这个最新的跨平台UI框架了.
引言
Jalium.UI 是一个 API 形态刻意贴近 WPF、但内核完全独立重写的 GPU 加速跨平台 .NET 10 UI 框架。
它不是 WPF 的 fork,也不是在 WPF 上套壳。它的设计者在 WPF 历史包袱(TextBox+ScrollViewer 度量死循环、winmm!PlaySound、DirectX 绑定等)面前做了一个大胆的决定:与 WPF 看起来像,但能跑在 Windows / Linux / Android 上,设计之初就支持NativeAOT。
1. 跨平台原理
Jalium.UI 的跨平台不是"用 Electron 套个 Webview",也不是"模拟一个 Win32 在另一台 OS 上",而是 三层分包:

这套三层架构带来三个直接后果:
- C# 业务代码与平台代码彻底解耦——同一份
Window定义,可能在 Windows 上拿到一个HWND后端,在 Wayland 上拿到wl_surface。 - 窗口系统特性归平台层——窗口最小尺寸、失去焦点、跨屏拖拽等行为在不同系统上有不同实现,托管层只调用
IPlatformWindow抽象。 - 渲染器可裁剪——同一份 UI 定义,可以走 Vulkan 走 GPU,也可以降级到软件渲染而不需要改业务代码。
Jalium.UI没有站在已有C#资源下.比如Skia.net,Silk.net等常用基础框架.底层的代码,全部C++入手.榨干硬件的极致性能。这一点就比很多套Skia的强太多。
Jalium.UI支持impeller/Impeller渲染方式。这也是很大的亮点,实时切换,似乎有点太强了吧。
1.1 一次跨平台窗口创建的真实路径
下面这张序列图展示了"用户把鼠标移到窗口外再拖回来"这种小事,是怎么在 Jalium.UI 里被几行抽象同时照顾到三个平台的:

关键点:C# 端只看到统一接口。所有 TryGetCursorScreenPoint、TryGetWindowScreenRect、TryGetClientOriginOnScreen、TryMoveWindowToScreenPoint 都在 jalium.native.platform ABI 内按平台路由,Windows 上是 user32,Linux 上是 XQueryPointer / wl_pointer / xdg_toplevel.move。
1.2 当前平台矩阵

C# 读者注意:Jalium.UI 目前不是 WPF 的"直接替代品"。它的 API 有意贴近 WPF,且完备度在快速逼近中,但官方 README 也明确写了"APIs can still evolve between minor versions, keep every
Jalium.UI.*package on the same version"。
1.3 文本栈:自托管引擎,不是 WPF 那一套
WPF 的文本底层依赖 DirectWrite + Uniscribe。Jalium.UI 直接把 FreeType + HarfBuzz 这条传统路径也换掉了:
后果是:
- 跨平台开箱即用——不再依赖平台自带的 OpenType 排版引擎。
- 字素簇(grapheme cluster)按 UAX#29 处理——emoji、ZWJ 序列、肤色修饰符、国旗都不会被错误拆开。
- CJK 与 IME 也走自研通道——LinuxDemo 在 X11 + Wayland 上都验证了 CJK 输入。
2. Jalium.UI 与 WPF 的相同与不同
2.1 概念对照表

具体的概念差别
| WPF 概念 | Jalium.UI 对应 | 备注 |
|---|---|---|
DependencyObject / DependencyProperty |
同名 | 依赖属性发现按类型缓存,避免 WPF 里"每次属性变更都走反射遍历"的成本 |
FrameworkElement / UIElement |
同名 | 概念几乎一一对应 |
Window / Application |
同名 | 入口模式由 AppBuilder : IHostApplicationBuilder 提供 |
{Binding} |
同名 + @Path / $.Path / #.Path |
见 §3 |
{StaticResource} / {DynamicResource} |
同名 | 资源字典"层提升时重新注册订阅",保证不会"丢订阅" |
PART_* 模板部件 |
同名 | PART_ContentHost 等概念完全一致 |
RoutedEvent / Command |
同名 | 路由事件、命令系统均保留 |
ScrollViewer / TextBox / ComboBox / Popup / ToolTip / Slider / ProgressBar / ContextMenu / Menu / Image |
同名 | 130+ 控件,命名沿用 WPF |
UIA 自动化 peer |
Windows 走 UIA,Linux 走 AT-SPI | 双桥接 |
Dispatcher / DispatcherFrame |
同名 | 在非 Windows 上 PushFrame 也会 pump 平台事件,避免把 X11/Wayland 冻住 |
2.2 关键差异速览
| 维度 | WPF | Jalium.UI |
|---|---|---|
| 标记语言 | XAML({Binding ...} 标记扩展) |
JALXAML(XAML 语法 + 内嵌 Razor 风格扩展) |
| 跨平台 | 仅 Windows | Windows / Linux (X11+Wayland) / Android |
| 渲染后端 | DirectX(MilCore) | D3D12 / Vulkan / 软件渲染(同时支持) |
| 文本栈 | DirectWrite(Windows 平台自带) | 自托管引擎 + fontconfig(仅用于字体发现) |
| 资源回收 | 单订阅 + 静态分析 | 反向索引 + 字典层提升时重新注册 |
| NativeAOT | 不支持 | 形态上 AOT 友好(依赖属性按类型缓存,源生成器) |
| 控件数量 | ~60 | 130+(含 Ribbon / Charts / InkCanvas / WebView / Terminal / MapView / Markdown / QRCode…) |
2.3 一个最小 Hello World:两边对照
WPF(XAML)
<Window x:Class="HelloWpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Hello WPF" Width="400" Height="200">
<StackPanel Margin="20">
<TextBlock Text="Hello from WPF" FontSize="24"/>
<Button Content="Click" Margin="0,12,0,0"/>
</StackPanel>
</Window>
Jalium.UI(JALXAML)
<Window xmlns="http://schemas.jalium.ui/2024""
Title="JALXAML Window" Width="800" Height="500">
<Grid>
<StackPanel Margin="20">
<TextBlock Text="Hello from JALXAML" FontSize="24"/>
<Button Content="Click" Margin="0,12,0,0"/>
</StackPanel>
</Grid>
</Window>
注意 XAML 命名空间区别——Jalium.UI 的 JALXAML.同时,它还新增了一类语法:@。
2.4 Jalium.UI 主动修复的 WPF 历史包袱

这是项目方"对 WPF 历史包袱的清醒认知"。Jalium.UI 不仅能在代码里模仿 WPF,还能在内部悄悄地避开 WPF 的几个著名坑。作者功底还是很深厚的。
3. JALXAML 内嵌的 Razor 语法
JalXaml支持Razor.虽然是局部支持。但是功能也足够强大。
3.1 Razor
Razor 语法完全服务于"在标记里写控制流"这一件事:
| 指令 | 含义 | 在 WPF XAML 中如何做 |
|---|---|---|
@Path |
取当前数据上下文路径上的值 | {Binding Path} |
$(expr) |
内联表达式(字符串/对象目标) | {Binding ...} + IValueConverter |
@{ ... } |
多语句代码块 | 只能放进 code-behind |
@* ... *@ |
注释 | <!-- --> |
$.Path |
元素自身属性路径 | 没有,code-behind 才能写 |
#.Path |
仅 DataContext 路径 | 没有 |
@if(expr){ <Element/> } |
条件渲染 | Style.Triggers 或 code-behind 改 Visibility |
@for / @foreach / @while / @switch |
循环与分支 | ItemsControl + ItemsSource 绑定 |
@using |
引入命名空间 | xmlns: 前缀 |
@lock |
线程互斥(标记里就有) | 不存在,要么 BindingOperations.EnableCollectionSynchronization,要么自己写 |
@virtualize |
虚拟化数据循环 | VirtualizingStackPanel,但不能跟 if 混 |
@section / @RenderSection |
模板片段插入 | ContentControl + ContentPresenter,绕一圈 |
@@ / \@ |
转义 | 没有 |
WPF 的 XAML 是声明式的,不允许命令式控制流。 如果你要根据条件渲染一组控件,XAML 只能走
DataTemplate选择、Style.Triggers、Visibility绑定,永远不能让if/foreach直接出现在标记里。JALXAML 允许。
3.2 一个真实的 JALXAML 片段
@using Jalium.UI.Controls
@using System.Collections.ObjectModel
<Window xmlns="http://schemas.jalium.ui/2024""
xmlns:jal="clr-namespace:Jalium.UI.Controls;assembly=Jalium.UI"
Title="@Model.WindowTitle"
Width="640" Height="400">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
@if (Model.IsAdmin)
{
<jal:InfoBar Grid.Row="0"
Severity="Warning"
Title="@Model.AdminBannerTitle"/>
}
@foreach (var item in Model.Items)
{
<jal:TextBlock Grid.Row="1"
Text="@item.Name"
FontSize="14"/>
}
@section Footer {
<StackPanel Grid.Row="2" Orientation="Horizontal">
<Button Content="保存" Click="@(s,e)=>Model.Save()"/>
<Button Content="取消" Margin="8,0,0,0"
Click="@(s,e)=>Model.Cancel()"/>
</StackPanel>
}
</Grid>
</Window>
WPF 程序员看到这个会本能地皱眉:"标记里能写 foreach?那 iOS 上是不是要重新编译模板?"——不会。JALXAML 不是字面替换文本的字符串模板,而是一个解析器:在加载时把 @xxx 编译成等价的 Binding / DataTrigger / Style 子树。在运行时它走的就是和普通 XAML 等价的视觉树,没有引入新的解释器。
3.3 解析时做什么

更新行为有两套规则:
- 可观察源(
INotifyPropertyChanged或依赖属性):实时更新,和{Binding}一样。 - 不可观察的 CLR 源:加载时一次性求值,固化进视觉树。
绑定源解析顺序也与 WPF 同:先 DataContext,再 code-behind 回退。
3.4 @virtualize 的特别之处
WPF 里要虚拟化一个长列表,必须用 ItemsControl + VirtualizingStackPanel,它和"折叠行"不兼容——你不能在虚拟化的同时对某些行做条件渲染。
JALXAML 的 @virtualize 是一类专门给数据绑定循环用的指令,允许你声明虚拟化行为,但仍然能和 @if 一起工作。这相当于把 ItemsControl + 自定义 DataTemplateSelector + VirtualizingPanel 三件套合并为一个声明。
3.5 @section:WPF 没有的"片段插槽"
WPF 里要表达"父模板里有一个插槽给子页面填",标准答案是 ContentControl + ContentPresenter,再加一连串 DataTemplate 链接。代码量不小。
@section / @RenderSection 让这件事变成两行:
<!-- 父模板 Parent.jalxaml -->
<Window>
<DockPanel>
<ContentArea/>
@RenderSection("Body")
</DockPanel>
</Window>
<!-- 子页面 Child.jalxaml -->
@section Body {
<MyPage Content="..."/>
}
这让 JALXAML 拥有 Razor Pages 的"母版页 + 子页"语义——但所有这些仍然产出同一个视觉树,没有引入 Web 概念。
4. 相比 WPF 的三个硬优势
4.1 跨平台
这一点是最直觉的。WPF 只能在 Windows 上跑,Jalium.UI 把同样的概念带到了 Linux(X11 + Wayland)和 Android 7.0+。对一个 C# 老兵来说意味着:
- 团队不再需要在 macOS 上给设计师重写一套 UI 给"跨平台预览"用。
- 内部工具可以一次写,CI 上直接跑在 Ubuntu 容器里。
- 服务端 + 客户端可以共享同一份业务模型类(同一份
INotifyPropertyChanged)。
但要注意:macOS / iOS / WebAssembly 仍不在发布名单上。源码里有 Metal 渲染器,但官方未打包。如果你只能选 macOS,请继续等。
4.2 NativeAOT 友好
WPF 与 NativeAOT 是不兼容的——这是由它的历史设计决定的(深度的运行时反射、MarkupExtension、动态 XamlReader 等)。Jalium.UI 在几处设计上主动避免了这一点:

注意几个关键的设计选择:
-
依赖属性发现按类型缓存
"Dependency property discovery is cached per type instead of walking the hierarchy by reflection on every patch."
WPF 里每次属性变更都要走一遍类型层级反射。Jalium.UI 用类型缓存把它拉成 O(1),同时为 AOT 编译期去掉了反射。 -
协议文件保持 BCL-only
"The protocol file stays BCL-only: the out-of-process watcher compiles it through a source link rather than referencing the framework, and one framework type here would drag the whole module-initializer chain into that process."
AOT 友好——避免引入整个模块初始化链。 -
Jalium.UI.Xaml.SourceGenerator是单独的 NuGet 包,承担XAML → 视觉树的编译期工作,相当于 Roslyn 增量生成器。
4.2.1 NativeAOT 过程

4.3 资源定期回收
WPF 里最阴险的一类 bug:资源字典订阅泄漏。某个视觉元素消失了,但资源字典里仍持有它的弱引用链;过几千次切换后,整棵视觉树被钉在 GC 根上回收不掉。
Jalium.UI 在设计上做了三件事来对抗这种场景:
4.3.1 订阅反向索引 + 字典层提升

"Promoting a dictionary between layers re-registers it in that index — without it a subscription drops out permanently."
4.3.2 Brush 缓存基于实例身份
如果主题色切换每帧都 new SolidColorBrush(...),会在几秒内触发大量 GC。
Jalium.UI 的策略:Brush 缓存键由"实例身份"决定,高频控件(光标闪烁、湿墨水、minimap、媒体字幕、Dock chrome、颜色选择器)直接使用静态只读 brush 或 RenderPenCache。这是它能在"主画廊拖动主题色"这种操作里把延迟从 2.3 ms 降到 0.02 ms 的关键。
4.3.3 WeakReference 与显式"定期回收"触发器
Jalium.UI 内部仍然依赖 C# 的分代式标记-压缩 GC(这也是它跑在 .NET 上的天然好处),但显式做了两件事:
- 订阅路径全部走
WeakReference,让 GC 能正常回收。 - 暴露回收观测点——开发者可以挂钩到资源字典的反向索引,主动触发清理。
性能数据:

WPF 程序员最直观的感受:WPF 里"换一个主题色帧率掉一半"的事,在 Jalium.UI 里掉不到 1%。
5. 如何开始
# 主入口包(按平台挑一个)
dotnet add package Jalium.UI.Desktop # Windows
dotnet add package Jalium.UI.Linux # Linux X11/Wayland
dotnet add package Jalium.UI.Android # Android
# 或者共享元包,由平台包自行引入
dotnet add package Jalium.UI
最小可运行程序(C# 优先,不写 XAML 也行):
using Jalium.UI;
using Jalium.UI.Controls;
var builder = AppBuilder.CreateBuilder(args);
builder.ConfigureApplication(app =>
{
app.MainWindow = new Window
{
Title = "Hello Jalium.UI",
Width = 960,
Height = 640,
Content = new StackPanel
{
Margin = new Thickness(24),
Children =
{
new TextBlock { Text = "Jalium.UI", FontSize = 28 },
new TextBlock
{
Text = "GPU-accelerated .NET UI framework",
Margin = new Thickness(0, 8, 0, 16)
},
new Button { Content = "Start" }
}
}
};
});
using var jalium = builder.Build();
return jalium.Run();
AppBuilder 实现 IHostApplicationBuilder(Microsoft.Extensions.Hosting),所以可以直接接 DI / Options / Logging / Metrics / Jalium MVVM。如果你写过 ASP.NET Core / Worker Service,几乎无缝。
6. 总结
WPF 的 API 形状 + 跨平台的骨架 + JALXAML 让 Razor 控制流走进桌面 UI 标记 + NativeAOT 友好的资源回收机制。
Jalium.UI 不是要取代 WPF 现有的代码,而是给"今天还没启动、或者想走出 Windows"的 C# 桌面团队一个新选项。
后面我们会带来JalUI的实战。它支持了几乎所有WPF的功能,迁移一个应用还是很容易的,敬请期待。
我是搬砖工。欢迎大家关注,点赞,分享自己最专业的见解。
浙公网安备 33010602011771号