WPF 学习笔记(十二):性能与发布

写在前面

版本说明:基于 .NET 8 LTS。

最后一篇。前面 11 篇讲了 WPF 的原理和写法,本篇关注两件事——让应用跑得快、让应用能上线。这两件事在生产环境比"功能正确"更重要。

本篇之后,WPF 系列就完结。最后会给一个系列知识图谱,方便后续回顾。

本文要回答:

Freezable 冻结到底解决了什么?WPF 为什么不能 AOT / Trimming?self-contained 和 framework-dependent 怎么选?


一、性能优化的总思路

1.1 WPF 性能瓶颈的三个层级

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
1. 渲染层(GPU)
   - 复杂模板、过多视觉树节点
   - 高频动画、过度绘制
   - 大图、缓存失效

2. 布局层(CPU)
   - 复杂 Grid 嵌套
   - 频繁 InvalidateMeasure
   - 大数据未虚拟化

3. 绑定层(CPU)
   - 大量绑定、复杂 Path
   - 频繁 PropertyChanged
   - Converter 滥用

1.2 性能优化的优先级

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
高收益、低改动:
  ✅ 大数据用虚拟化
  ✅ Freeze 资源
  ✅ 避免 StackPanel 装大数据
  ✅ 优先用 RenderTransform 而不是 Width/Height 动画

中等收益:
  - 简化 ControlTemplate 嵌套
  - 减少绑定 Converter
  - 异步加载(不阻塞 UI 线程)

低收益、复杂改动:
  - 自定义 OnRender 替代控件
  - 用 SkiaSharp / Win2D
  - 手动管视觉树

二、Freezable:冻结资源

2.1 Freezable 是什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Freezable 是 WPF 一种特殊类型:
  - 可变状态(默认):可以改属性
  - 冻结状态:不可变,性能更好

继承 Freezable 的类型:
  - Brush(SolidColorBrush、LinearGradientBrush...)
  - Geometry
  - Transform
  - Pen、DashStyle
  - ImageSource(部分)
  - 3D Material

2.2 为什么冻结更快

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var brush = new SolidColorBrush(Colors.Red);

// 不冻:WPF 必须监听 brush 的变化(万一你改 Color)
// 每次渲染都要检查
button.Background = brush;

// 冻:声明"我不会改"
brush.Freeze();
button.Background = brush;
// WPF 知道不可变 → 跳过监听 → 渲染更快
// 而且多个线程可以安全访问(线程安全)
1
2
3
4
5
6
7
8
9
性能差异:
  Freeze 一个常用 Brush,渲染频繁场景
  → 减少 10~30% 渲染开销
  → 频繁改色的场景效果更明显

附加好处:
  - 冻结对象线程安全(可跨线程访问)
  - 不会触发变更通知
  - 适合做资源(Resources 字典里都该 Freeze)

2.3 在 XAML 里冻结

1
2
3
4
5
6
7
8
<!-- XAML 里写的资源会自动 Freeze -->
<Application.Resources>
    <!-- 这是 Frozen 的 -->
    <SolidColorBrush x:Key="bg" Color="Red"/>
</Application.Resources>

<!-- 但动态创建的不会 -->
<!-- C# 里 new 的 Brush 不会自动冻 -->

2.4 在 C# 里冻结

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static readonly Brush WarningBrush = CreateFrozenBrush();

private static Brush CreateFrozenBrush()
{
    var brush = new LinearGradientBrush
    {
        StartPoint = new Point(0, 0),
        EndPoint = new Point(1, 0)
    };
    brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0));
    brush.GradientStops.Add(new GradientStop(Colors.Red, 1));
    brush.Freeze();   // ← 关键
    return brush;
}

2.5 检查与解冻

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (brush.CanFreeze)
    brush.Freeze();

if (brush.IsFrozen)
{
    // 解冻 = 创建可变副本
    var mutable = brush.Clone();
    mutable.Opacity = 0.5;
    mutable.Freeze();
}

三、虚拟化与滚动性能

3.1 启用虚拟化

1
2
3
4
<ListBox ItemsSource="{Binding Items}"
         VirtualizingPanel.IsVirtualizing="True"
         VirtualizingPanel.VirtualizationMode="Recycling"
         VirtualizingPanel.ScrollUnit="Pixel"/>
1
2
3
4
5
6
7
8
关键属性:
  IsVirtualizing:开/关虚拟化(默认 True)
  VirtualizationMode:
    Standard:每次创建新容器
    Recycling:复用容器(推荐,性能好)
  ScrollUnit:
    Item:按 Item 滚动(默认,可能卡)
    Pixel:按像素滚动(流畅)

3.2 虚拟化的"陷阱"

嵌套破坏虚拟化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- ❌ 外层 ScrollViewer + 内层 ListBox -->
<ScrollViewer>
    <StackPanel>  <!-- StackPanel 给子无限高度 -->
        <ListBox ItemsSource="{Binding Items}"/>
        <!-- ListBox 高度被拉伸到所有 Item → 虚拟化失效 -->
    </StackPanel>
</ScrollViewer>

<!-- ✅ ListBox 自己滚动 -->
<Grid>
    <ListBox ItemsSource="{Binding Items}"/>
</Grid>

ItemsControl 默认不虚拟化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- ❌ ItemsControl 默认用 StackPanel -->
<ItemsControl ItemsSource="{Binding Items}"/>

<!-- ✅ 改用 VirtualizingStackPanel -->
<ItemsControl ItemsSource="{Binding Items}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

<!-- ✅ 或直接用 ListBox -->
<ListBox ItemsSource="{Binding Items}"/>

设固定高度也破坏虚拟化

1
2
3
4
5
6
7
8
<!-- ❌ 固定高度 = 没有滚动 = 虚拟化没用 -->
<ListBox Height="500" ItemsSource="{Binding Items}"/>
<!-- 1000 个 Item 还是全部实例化 -->

<!-- ✅ 让 ListBox 在容器内 Stretch -->
<Grid>
    <ListBox ItemsSource="{Binding Items}"/>
</Grid>

3.3 ContainerGenerator 性能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 虚拟化时,容器在后台生成
// 监听 ItemContainerGenerator.StatusChanged
listBox.ItemContainerGenerator.StatusChanged += (s, e) =>
{
    var status = listBox.ItemContainerGenerator.Status;
    if (status == GeneratorStatus.ContainersGenerated)
    {
        // 容器生成完
    }
};

四、绑定优化

4.1 减少 Converter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- ❌ Converter 滥用 -->
<TextBlock Visibility="{Binding IsVisible, Converter={StaticResource BoolToVis}}"/>

<!-- ✅ 用 DataTrigger(在 Style 里) -->
<Style TargetType="TextBlock">
    <Style.Triggers>
        <DataTrigger Binding="{Binding IsVisible}" Value="False">
            <Setter Property="Visibility" Value="Collapsed"/>
        </DataTrigger>
    </Style.Triggers>
</Style>
1
2
3
4
5
6
7
8
9
Converter 的代价:
  每次属性变化都调用 Convert
  跨语言边界(托管 → 反射)
  高频更新场景拖慢

替代方案:
  - 简单逻辑用 DataTrigger
  - 复杂逻辑用 ViewModel 暴露"加工后"的属性
    (ViewModel.IsVisibleText、ViewModel.StatusColor)

4.2 避免复杂 Path

1
2
3
4
5
<!-- ❌ 深层路径,每次都反射 -->
<TextBlock Text="{Binding User.Address.City.ZipCode}"/>

<!-- ✅ ViewModel 直接暴露扁平属性 -->
<TextBlock Text="{Binding ZipCode}"/>

4.3 OneTime 绑定

1
2
3
4
<!-- 数据几乎不变 → OneTime 性能最好 -->
<TextBlock Text="{Binding Name, Mode=OneTime}"/>

<!-- 不订阅 INPC,只绑定一次 -->

4.4 避免字符串格式化

1
2
3
4
5
<!-- ❌ 每次都格式化 -->
<TextBlock Text="{Binding Price, StringFormat=C}"/>

<!-- ✅ ViewModel 直接给字符串 -->
<TextBlock Text="{Binding PriceDisplay}"/>

4.5 异步绑定

1
2
<!-- .NET 4.5+ 异步绑定 -->
<TextBlock Text="{Binding ExpensiveProperty, IsAsync=True}"/>
1
2
3
4
5
6
7
IsAsync 的含义:
  - 在后台线程读属性
  - 不阻塞 UI
  - 适合"很慢的属性"

注意:本质还是同步读取,只是不在 UI 线程
真正的异步要用 Task + NotifyTask(第三方库)

五、Layout 优化

5.1 简化视觉树

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- ❌ 过度嵌套 -->
<Border>
    <Grid>
        <Border>
            <StackPanel>
                <Border>
                    <Button/>
                </Border>
            </StackPanel>
        </Border>
    </Grid>
</Border>

<!-- ✅ 扁平 -->
<Grid>
    <Button/>
</Grid>

5.2 用 Canvas 处理固定布局

1
2
3
4
5
6
<!-- 固定布局(不用 measure) -->
<Canvas>
    <Button Canvas.Left="10" Canvas.Top="10"/>
</Canvas>

<!-- 比 Grid 快得多 -->

5.3 延迟创建(Lazy)

1
2
3
4
5
6
7
<TabControl>
    <TabItem Header="Tab 1">
        <local:HeavyView/>   <!-- TabItem 默认全部创建 -->
    </TabItem>
</TabControl>

<!-- 改:每个 Tab 内容用 ContentPresenter + 触发器延迟 -->

六、RenderOptions

6.1 CachingHint

1
2
3
4
<!-- 复杂静态内容缓存 -->
<Path Data="..." RenderOptions.CachingHint="Cache"
      RenderOptions.CacheInvalidationThresholdMaximum="2"
      RenderOptions.CacheInvalidationThresholdMinimum="0.5"/>
1
2
3
4
5
6
7
8
9
默认(CachingHint=Default):
  小元素:不缓存
  大元素:缓存

显式 Cache:
  强制缓存(适合重复绘制且不变的元素)

显式 NoCache:
  频繁变化的元素不缓存

6.2 EdgeMode

1
2
3
4
5
6
<!-- 边缘模式(位图) -->
<Image Source="photo.jpg" RenderOptions.BitmapScalingMode="HighQuality"/>
<!-- HighQuality(默认):双线性,慢但好 -->
<!-- LowQuality:最近邻,快但糙 -->
<!-- NearestNeighbor:最近邻(像素艺术) -->
<!-- Fant:高质量下采样 -->

6.3 ProcessRenderMode

1
2
3
4
// 强制软件渲染(GPU 渲染反而慢的场景)
RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;

// 默认 HardwareOnly(GPU 渲染)
1
2
3
4
5
6
7
8
何时用 SoftwareOnly:
  - 远程桌面(RDP)场景
  - 某些 GPU 驱动有 bug
  - 复杂 3D 在弱 GPU 上

何时不用:
  - 普通桌面应用
  - 复杂动画、特效

七、内存优化

7.1 弱事件模式

1
2
3
4
5
6
7
8
9
// ❌ 短生命周期对象订阅长生命周期事件
Application.Current.MainWindow.Closing += shortLived.Handler;
// shortLived 被持有,泄漏

// ✅ 弱事件
PropertyChangedEventManager.AddHandler(source, handler, nameof(source.Property));

// 或 Toolkit 的 WeakReferenceMessenger
WeakReferenceMessenger.Default.Register<MyMessage>(this, (r, m) => { ... });

7.2 大对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ❌ 一次性加载大列表
public ObservableCollection<Item> Items { get; } = new();
foreach (var item in Enumerable.Range(0, 100000))
    Items.Add(new Item());  // 大量分配

// ✅ 分页 / 增量加载
public ObservableCollection<Item> Items { get; } = new();
private async Task LoadMoreAsync()
{
    var batch = await _service.GetItems(skip: Items.Count, take: 100);
    foreach (var item in batch) Items.Add(item);
}

7.3 释放资源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ❌ 忘记 Dispose
public MainWindow()
{
    var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
    timer.Tick += Timer_Tick;
    timer.Start();
    // 窗口关闭,timer 仍持有窗口引用 → 泄漏
}

// ✅ 关闭时取消订阅
protected override void OnClosed(EventArgs e)
{
    _timer.Stop();
    _timer.Tick -= Timer_Tick;
    base.OnClosed(e);
}

八、性能诊断工具

8.1 Visual Studio Performance Profiler

1
2
3
CPU Usage:找 CPU 热点
Memory Usage:找内存泄漏
Application Timeline:找 UI 卡顿(WPF 专用)

8.2 WPF Performance Suite

1
2
3
4
5
6
旧工具(SDK 自带):
  - Perforator:渲染统计
  - TraceProfiler:事件追踪
  - Visual Profiler:视觉树性能

新版本在 Windows SDK / Store 找

8.3 代码计时

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var sw = Stopwatch.StartNew();
// ... 操作
sw.Stop();
Debug.WriteLine($"耗时:{sw.ElapsedMilliseconds} ms");

// 计算渲染帧
CompositionTarget.Rendering += (s, e) =>
{
    _frameCount++;
};

九、发布:基础概念

9.1 部署模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Framework-Dependent(依赖框架):
  - 假设用户机器已装 .NET Runtime
  - 发布包小(几 MB)
  - 用户必须自己装 .NET

Self-Contained(自包含):
  - 把 .NET Runtime 打包进去
  - 发布包大(60~150 MB)
  - 用户无需装 .NET

Single-File(单文件):
  - 所有 DLL 打成一个 exe
  - 用户体验好(一个文件)
  - 启动稍慢(首次解压)

9.2 csproj 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<PropertyGroup>
  <OutputType>WinExe</OutputType>
  <TargetFramework>net8.0-windows</TargetFramework>
  <UseWPF>true</UseWPF>

  <!-- 发布配置 -->
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  <SelfContained>true</SelfContained>
  <PublishSingleFile>true</PublishSingleFile>
  <PublishReadyToRun>true</PublishReadyToRun>
  <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
  <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
</PropertyGroup>
1
2
3
4
5
6
RuntimeIdentifier:必须指定(win-x64 / win-x86 / win-arm64)
SelfContained:true 自包含 / false 框架依赖
PublishSingleFile:单文件发布
PublishReadyToRun:AOT 预编译(提前编 IL)
IncludeNativeLibrariesForSelfExtract:native 库也打进单文件
EnableCompressionInSingleFile:压缩单文件(体积更小,启动稍慢)

9.3 发布命令

1
2
3
4
5
6
7
8
# 自包含 + 单文件 + AOT
dotnet publish -c Release -r win-x64 --self-contained true \
    /p:PublishSingleFile=true \
    /p:PublishReadyToRun=true \
    /p:IncludeNativeLibrariesForSelfExtract=true

# 框架依赖(最小包)
dotnet publish -c Release -r win-x64 --self-contained false

9.4 体积对比

1
2
3
4
5
6
7
8
配置                                体积
─────────────────────────────────────────
Framework-Dependent                 ~5 MB
Self-Contained                      ~80 MB
Self-Contained + SingleFile         ~80 MB(一个文件)
Self-Contained + SingleFile + 压缩  ~50 MB
+ ReadyToRun                        +10 MB(提前编 IL)
+ Trimming                          ⚠️ WPF 不支持(见下节)

十、ReadyToRun 与 Trimming

10.1 ReadyToRun(R2R)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ReadyToRun = 提前编译为 native 代码(部分)

优点:
  - 启动更快(JIT 不用从 IL 编译)
  - 仍兼容反射(不是完全 AOT)

代价:
  - 体积增加 10~30%
  - 跨平台弱(必须指定 RID)

WPF 支持:
  ✅ 完全支持
  建议生产应用开启
1
2
<PublishReadyToRun>true</PublishReadyToRun>
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>

10.2 Trimming

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Trimming = 移除未使用的代码(减小体积)

WPF 的硬限制:
  ❌ WPF 不能 Trim(WPF 大量用反射)
  ❌ DependencyProperty.Register 反射注册
  ❌ XAML/BAML 解析反射
  ❌ Binding Path 反射

.NET 6+ 部分支持:
  ⚠️ 可以 trim 应用层代码(不影响 WPF 核心)
  ⚠️ 但 trim WPF 自己的代码会运行时崩

实践:
  Trimming 在 WPF 不推荐
  体积优化靠 SingleFile + 压缩
1
2
3
<!-- 不推荐 -->
<PublishTrimmed>true</PublishTrimmed>
<!-- 即使配 TrimMode=partial,风险也高 -->

10.3 NativeAOT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.NET 8+ 的 NativeAOT:
  - 完全 AOT 编译
  - 体积小、启动极快
  - 无 JIT、无反射(部分)

WPF 的现状:
  ❌ WPF 不支持 NativeAOT
  - DependencyProperty 注册依赖反射
  - XAML 解析依赖反射
  - 类型系统深度依赖运行时

替代方案:
  - WinUI 3:部分支持
  - Avalonia:实验性支持
  - MAUI:iOS / Android 用 AOT(不是 WPF)

  WPF 工程师:放弃 NativeAOT,用 R2R + SingleFile

十一、MSIX 打包

11.1 MSIX 是什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
MSIX = Windows 现代应用打包格式
  - 替代传统 MSI
  - 沙盒运行(隔离)
  - 自动更新(从 Microsoft Store)
  - 干净卸载(不残留注册表)

适用:
  - Microsoft Store 发布
  - 企业内部署
  - 自动更新场景

11.2 打包步骤

1
2
3
4
5
1. 用 Visual Studio 的 "Windows Application Packaging Project"
2. 引用 WPF 项目
3. 配置 Package.appxmanifest
4. 生成 .msix 包
5. 签名(测试或正式证书)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!-- Package.appxmanifest 关键部分 -->
<Identity Name="MyApp"
          Publisher="CN=MyCompany"
          Version="1.0.0.0"/>

<Applications>
  <Application Id="App"
               Executable="$targetnametoken$.exe"
               EntryPoint="$targetentrypoint$">
    <uap:VisualElements
        DisplayName="MyApp"
        Description="My WPF Application"
        BackgroundColor="transparent"
        Square150x150Logo="Assets\Logo.png"/>
  </Application>
</Applications>

11.3 MSIX vs 传统安装包

维度传统 exe/msiMSIX
安装注册表、Program Files沙盒
卸载可能残留干净
更新手动自动
权限用户 / 管理员UAC 友好
兼容全部 WindowsWin10 1709+
适合老应用现代应用
1
2
3
4
实践建议:
  - 桌面应用首选 MSIX(如果支持)
  - 老系统兼容用传统 exe
  - 内部工具直接 dotnet publish 单文件

十二、应用清单与兼容性

12.1 app.manifest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApp"/>

  <!-- DPI 感知(详见第 4 篇) -->
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
    </windowsSettings>
  </application>

  <!-- 兼容性(声明支持的 Windows 版本) -->
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <supportedOS Id="8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a"/>  <!-- Windows 10/11 -->
    </application>
  </compatibility>
</assembly>

12.2 管理员权限

1
2
<!-- 请求管理员权限 -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>

十三、自动更新

13.1 ClickOnce(旧)

1
2
3
4
5
6
7
8
特点:
  - .NET 早期技术
  - 自动更新简单
  - 但限制多(不能改注册表、沙盒)

适用:
  - 内部工具
  - 简单应用

13.2 现代:Velopack / Squirrel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Velopack(推荐):
  - 现代化、跨平台
  - 增量更新
  - GitHub Release 集成
  - 替代 Squirrel

Squirrel:
  - 经典方案
  - GitHub 出品
  - 已基本停更

使用:
  Velopack 在 csproj 集成
  发布到 GitHub Release / 自建服务器
  应用启动检查更新
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Velopack 示例
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        VelopackApp.Build().Run();
        base.OnStartup(e);

        // 检查更新
        var mgr = new UpdateManager("https://myserver/releases");
        if (mgr.CheckForUpdates() is not null)
        {
            var upd = mgr.UpdateAppAsync();
            // 更新完成后重启
        }
    }
}

十四、WPF 知识图谱(系列总结)

经过 12 篇深入,WPF 的核心知识已经覆盖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────────────┐
│                    WPF 知识图谱                          │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  基础机制(第 1~3 篇)                                   │
│    ├─ 架构 + XAML 编译流水线                            │
│    ├─ 依赖属性(11 级优先级 + 三回调链)                │
│    └─ 路由事件(Tunneling/Bubbling/Direct)             │
│                                                          │
│  UI 与数据(第 4~7 篇)                                  │
│    ├─ 布局系统(Measure/Arrange + Panel 全家桶)        │
│    ├─ 数据绑定(INPC + Binding + CollectionView)       │
│    ├─ 命令(ICommand + CommunityToolkit.Mvvm)          │
│    └─ MVVM 工程化(DI + 导航 + 对话框)                │
│                                                          │
│  外观与并发(第 8~10 篇)                                │
│    ├─ 样式与模板(Style/ControlTemplate/DataTemplate)  │
│    ├─ Dispatcher 与 async/await                         │
│    └─ 动画与图形(保留模式 + Storyboard)               │
│                                                          │
│  扩展与上线(第 11~12 篇)                               │
│    ├─ 自定义控件(UserControl/CustomControl/OnRender)  │
│    └─ 性能与发布(Freezable/虚拟化/MSIX)              │
│                                                          │
└─────────────────────────────────────────────────────────┘

14.1 进阶路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
学完本系列,可以继续探索:

UI 框架对比:
  - WinUI 3(现代 Windows UI)
  - MAUI(跨平台)
  - Avalonia(跨平台、类 WPF)
  - Blazor Hybrid(Web 技术做桌面)

进阶主题:
  - SkiaSharp(高性能绘图)
  - Helix Toolkit(3D)
  - WebView2(嵌入式浏览器)
  - WPF + Blazor Hybrid(Razor 组件做桌面)

工程实践:
  - Prism(重量级 MVVM 框架)
  - ReactiveUI(响应式 MVVM)
  - StyleCop + Roslyn 分析器
  - XAML 热重载 + 设计时数据

发布生态:
  - Velopack(自动更新)
  - MSIX(现代打包)
  - Windows App SDK

14.2 后端工程师学 WPF 的"思维转变"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
从命令式 → 声明式
  传统:手动改 UI(textBox.Text = ...)
  WPF:声明绑定({Binding}),数据驱动 UI

从事件 → 命令
  传统:button.Click += handler
  WPF:Command + ViewModel

从单线程 → UI 单线程 + Dispatcher
  传统:随便多线程
  WPF:UI 必须单线程,跨线程用 Dispatcher

从绝对定位 → 两阶段布局
  传统:控件 Location
  WPF:父容器 Measure/Arrange

从渲染即画 → 保留模式
  传统:OnPaint 自己画
  WPF:描述视觉树,框架渲染

十五、小结

本文是 WPF 系列收尾,聚焦生产环境:

  • 性能优化的三层瓶颈(渲染/布局/绑定)
  • Freezable 冻结(10~30% 渲染提升)
  • 虚拟化(大数据列表必备,注意嵌套陷阱)
  • 绑定优化(少 Converter、扁平 Path、OneTime)
  • Layout 优化(简化视觉树、Canvas、延迟创建)
  • RenderOptions(CachingHint、ProcessRenderMode)
  • 内存优化(弱事件、分页加载、释放资源)
  • 发布模式(Framework-Dependent / Self-Contained / Single-File)
  • ReadyToRun(WPF 完全支持)
  • Trimming / NativeAOT(WPF 不支持,原因:反射依赖)
  • MSIX 打包(现代部署)
  • app.manifest(DPI 感知、权限、兼容性)
  • 自动更新(Velopack)
  • WPF 系列知识图谱
1
2
3
4
记住三句话:
  1. 性能优化的第一刀是虚拟化 + Freeze——简单收益大
  2. WPF 不能 AOT/Trim——历史包袱(反射 + XAML 解析)
  3. 发布默认 Self-Contained + SingleFile + ReadyToRun——用户体验最好

系列完结

WPF 学习笔记系列 12 篇到这里全部完成:

  1. 架构总览与 XAML 原理
  2. 依赖属性
  3. 路由事件
  4. 布局系统
  5. 数据绑定
  6. 命令与 CommunityToolkit.Mvvm
  7. MVVM 工程化与依赖注入
  8. 样式与模板
  9. 多线程与 Dispatcher
  10. 动画与图形
  11. 自定义控件
  12. 性能与发布

希望这份笔记能帮到你建立系统的 WPF 知识体系。WPF 是一个"老但深"的框架——表面 API 简单,底层机制复杂。理解了底层,写代码就不再迷茫;理解了机制,调性能就有了方向。

后端工程师学 WPF 的最大障碍不是 API,而是思维模式——从"命令式"切换到"声明式"。一旦完成这个转变,WPF 反而是 .NET 生态里最优雅的 UI 框架。

继续探索,继续精进。完。

Licensed under CC BY-NC-SA 4.0