写在前面
版本说明:基于 .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);
}
|
八、性能诊断工具
1
2
3
| CPU Usage:找 CPU 热点
Memory Usage:找内存泄漏
Application Timeline:找 UI 卡顿(WPF 专用)
|
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/msi | MSIX |
|---|
| 安装 | 注册表、Program Files | 沙盒 |
| 卸载 | 可能残留 | 干净 |
| 更新 | 手动 | 自动 |
| 权限 | 用户 / 管理员 | UAC 友好 |
| 兼容 | 全部 Windows | Win10 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 篇到这里全部完成:
- 架构总览与 XAML 原理
- 依赖属性
- 路由事件
- 布局系统
- 数据绑定
- 命令与 CommunityToolkit.Mvvm
- MVVM 工程化与依赖注入
- 样式与模板
- 多线程与 Dispatcher
- 动画与图形
- 自定义控件
- 性能与发布
希望这份笔记能帮到你建立系统的 WPF 知识体系。WPF 是一个"老但深"的框架——表面 API 简单,底层机制复杂。理解了底层,写代码就不再迷茫;理解了机制,调性能就有了方向。
后端工程师学 WPF 的最大障碍不是 API,而是思维模式——从"命令式"切换到"声明式"。一旦完成这个转变,WPF 反而是 .NET 生态里最优雅的 UI 框架。
继续探索,继续精进。完。