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 上",而是 三层分包:

在这里插入图片描述

这套三层架构带来三个直接后果:

  1. C# 业务代码与平台代码彻底解耦——同一份 Window 定义,可能在 Windows 上拿到一个 HWND 后端,在 Wayland 上拿到 wl_surface。
  2. 窗口系统特性归平台层——窗口最小尺寸、失去焦点、跨屏拖拽等行为在不同系统上有不同实现,托管层只调用 IPlatformWindow 抽象。
  3. 渲染器可裁剪——同一份 UI 定义,可以走 Vulkan 走 GPU,也可以降级到软件渲染而不需要改业务代码。

Jalium.UI没有站在已有C#资源下.比如Skia.net,Silk.net等常用基础框架.底层的代码,全部C++入手.榨干硬件的极致性能。这一点就比很多套Skia的强太多。

Jalium.UI支持impeller/Impeller渲染方式。这也是很大的亮点,实时切换,似乎有点太强了吧。

1.1 一次跨平台窗口创建的真实路径

下面这张序列图展示了"用户把鼠标移到窗口外再拖回来"这种小事,是怎么在 Jalium.UI 里被几行抽象同时照顾到三个平台的:

mermaid diagram

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

1.2 当前平台矩阵

mermaid diagram

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 概念对照表

mermaid diagram

具体的概念差别

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 历史包袱

mermaid diagram

这是项目方"对 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 解析时做什么

mermaid diagram

更新行为有两套规则:

  • 可观察源(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 在几处设计上主动避免了这一点:

mermaid diagram

注意几个关键的设计选择:

  1. 依赖属性发现按类型缓存

    "Dependency property discovery is cached per type instead of walking the hierarchy by reflection on every patch."
    WPF 里每次属性变更都要走一遍类型层级反射。Jalium.UI 用类型缓存把它拉成 O(1),同时为 AOT 编译期去掉了反射。

  2. 协议文件保持 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 友好——避免引入整个模块初始化链。

  3. Jalium.UI.Xaml.SourceGenerator 是单独的 NuGet 包,承担 XAML → 视觉树 的编译期工作,相当于 Roslyn 增量生成器。

4.2.1 NativeAOT 过程

mermaid diagram

4.3 资源定期回收

WPF 里最阴险的一类 bug:资源字典订阅泄漏。某个视觉元素消失了,但资源字典里仍持有它的弱引用链;过几千次切换后,整棵视觉树被钉在 GC 根上回收不掉。

Jalium.UI 在设计上做了三件事来对抗这种场景:

4.3.1 订阅反向索引 + 字典层提升

mermaid diagram

"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 能正常回收。
  • 暴露回收观测点——开发者可以挂钩到资源字典的反向索引,主动触发清理。

性能数据:

mermaid diagram

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的功能,迁移一个应用还是很容易的,敬请期待。

我是搬砖工。欢迎大家关注,点赞,分享自己最专业的见解。

posted on 2026-09-11 11:46  HelyCheng  阅读(22)  评论(0)    收藏  举报

导航