简介:这份资源是《葵花宝典 WPF自学手册》随书光盘的完整源代码,面向正在学习 WPF 桌面开发、希望对照书中示例动手实践的初中级开发者。它解决的是看书时缺少可运行工程、难以验证控件用法与项目结构的问题,适合边读边调试、按章节复现案例的学习场景。压缩包共 1713 个文件,约 38.54MB,以 655 个 cs 源码、385 个 xaml 界面文件和 109 个 csproj 工程文件为主,另含 sln 解决方案、resx 资源、settings 配置及少量图片、音频与可执行文件,基本覆盖书中各示例的完整工程结构。目前已有 1097 人学习下载。读者可直接打开解决方案运行示例,观察 XAML 布局与后台代码的对应关系,理解依赖属性、数据绑定、样式模板等知识点的落地写法,并借助多工程目录对比不同章节的实现差异,减少自行搭建环境的时间,提升学习效率。
1. 从一份 WPF 自学手册源码说起:为什么我建议你亲手跑一遍
很多人学 WPF 的路径是:看视频、抄代码、遇到绑定不生效就懵了。我见过不少开发者,XAML 写了一大堆,问他DataContext到底怎么传递的、INotifyPropertyChanged为什么必须手动触发,答不上来。问题出在哪?缺一个能跑起来、能改得动的完整项目。
这份《葵花宝典 WPF自学手册》源代码,就是冲着这个痛点来的。它不是零散的知识点堆砌,而是一套跟着章节走的可运行示例集合,覆盖了从布局控件、数据绑定、命令系统到样式模板、动画、自定义控件这些 WPF 核心模块。适合谁?刚接触 WPF 桌面开发、想从 WinForms 转过来、或者已经会写但总觉得“知其然不知其所以然”的开发者。你把它下载下来,用 Visual Studio 打开解决方案,逐个项目跑一遍,比看十篇文章都管用。
2. 环境准备与项目结构:把源码跑起来需要几步
2.1 开发环境与依赖清单
这套源码基于 .NET Framework 和 WPF,不是 .NET Core 那套。所以你的机器上需要装 Visual Studio,版本建议 2019 或 2022 社区版就够,安装时勾选“.NET 桌面开发”工作负载。别用 VS Code 硬扛,WPF 的设计器、XAML 智能提示、调试可视化工具在 VS 里才完整。
具体依赖如下:
| 项目 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Windows 10/11 | WPF 不支持跨平台运行 |
| IDE | VS 2019/2022 | 社区版免费,勾选桌面开发 |
| .NET 版本 | .NET Framework 4.6.1+ | 源码目标框架,别改成 Core |
| 其他 | 无第三方 NuGet 包 | 纯原生 WPF,不依赖外部库 |
注意:如果你打开解决方案后提示“目标框架未安装”,去 Visual Studio Installer 里补装对应版本的 .NET Framework 开发工具包,不要急着改项目属性里的目标框架,改了容易出玄学问题。
2.2 解决方案目录结构拆解
下载解压后,你会看到一个.sln解决方案文件,双击用 VS 打开。整个解决方案按章节或功能模块拆成了多个项目,典型的目录结构长这样:
WPF自学手册源码/ ├── Chapter01_Layout/ # 布局与基础控件 ├── Chapter02_Binding/ # 数据绑定与通知 ├── Chapter03_Command/ # 命令系统与路由事件 ├── Chapter04_StyleTemplate/ # 样式、模板与触发器 ├── Chapter05_Animation/ # 动画与变换 ├── Chapter06_CustomControl/ # 自定义控件与依赖属性 ├── Chapter07_MVVM/ # MVVM 模式实战 └── WPF自学手册.sln每个项目都是独立的可启动应用,右键某个项目设为启动项目,F5 就能跑。这种拆分方式的好处是:你学哪一章就开哪个项目,不会被无关代码干扰。我一般会先把 Chapter02 跑起来,因为数据绑定是 WPF 的分水岭,这块通了后面就顺了。
2.3 首次运行可能遇到的编译问题
直接 F5 不一定一次成功,常见的有两类。第一类是 XAML 命名空间引用报错,比如clr-namespace指向的程序集名称对不上,这通常是因为项目重命名过。解决办法是检查 XAML 头部的xmlns:local是否指向当前项目的命名空间。
第二类是资源字典路径问题。有些项目把样式抽到了独立的.xaml资源字典里,在App.xaml中合并。如果路径写的是绝对路径或者错误的相对路径,运行时会抛IOException。打开App.xaml,确认Source属性用的是相对路径,类似这样:
<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <!-- 用相对路径引用同项目下的资源字典 --> <ResourceDictionary Source="Styles/ButtonStyles.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>如果还是报错,右键资源字典文件看属性,确保“生成操作”是Page,而不是Resource或Content。这个坑我踩过,明明文件在,编译就是找不到,血泪经验。
3. 核心模块拆解:数据绑定、命令与 MVVM 的落地写法
3.1 数据绑定:从 DataContext 到 INotifyPropertyChanged
WPF 数据绑定的核心链路是:DataContext提供数据源,XAML 里用{Binding}标记扩展建立连接,属性值变化时靠INotifyPropertyChanged通知 UI 刷新。源码里 Chapter02 有一个典型的 ViewModel 实现:
// ViewModel 基类,实现属性变更通知 public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; // 调用此方法触发通知,[CallerMemberName] 自动传入属性名 protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // 封装 SetProperty,简化属性定义 protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (EqualityComparer<T>.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(propertyName); return true; } }逻辑说明:SetProperty先比较新旧值,相同就直接返回,避免无意义的 UI 刷新。[CallerMemberName]特性让编译器自动填入调用方的属性名,省去手写字符串的麻烦,也避免了改属性名忘记改通知参数的翻车现场。
参数说明:ref T field是属性的后备字段,T value是新值。返回bool是为了在 setter 里做条件判断,比如值变了才触发某些逻辑。
在 View 里绑定就很简单:
<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"/>UpdateSourceTrigger=PropertyChanged表示每次按键都回写源属性,默认是LostFocus。这个参数在搜索框、实时校验场景下必须改,不然用户输入完点别处才更新,体验很差。
3.2 命令系统:ICommand 与 RelayCommand 的配合
WPF 的命令系统把“操作”从事件处理器里解放出来,让 ViewModel 不依赖 UI 控件。源码里 Chapter03 和 Chapter07 都用了自定义的RelayCommand:
public class RelayCommand : ICommand { private readonly Action<object> _execute; private readonly Predicate<object> _canExecute; public RelayCommand(Action<object> execute, Predicate<object> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } // 命令能否执行,决定按钮是否可用 public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true; public void Execute(object parameter) => _execute(parameter); // 手动触发 CanExecuteChanged,通知 UI 重新评估按钮状态 public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } }逻辑说明:CanExecute返回 false 时,绑定了该命令的按钮会自动变灰。CommandManager.RequerySuggested是 WPF 内置的重新查询机制,当焦点变化、键盘输入等操作发生时,它会自动触发所有命令重新评估状态。如果你发现按钮该亮的时候不亮,多半是CanExecute逻辑写错了,或者没走CommandManager这套。
参数说明:Action<object> execute是命令执行体,Predicate<object> canExecute是可选的可用性判断。在 ViewModel 里这样用:
public ICommand SaveCommand { get; } public MainViewModel() { SaveCommand = new RelayCommand(Save, CanSave); } private void Save(object obj) { /* 保存逻辑 */ } private bool CanSave(object obj) => !string.IsNullOrWhiteSpace(UserName);XAML 里按钮绑定Command="{Binding SaveCommand}",不需要写Click事件。这就是 MVVM 的解耦价值:View 只负责展示,逻辑全在 ViewModel 里,单元测试也好写。
3.3 MVVM 分层:View、ViewModel 与 Model 的职责边界
Chapter07 是一个完整的 MVVM 示例,结构清晰。View 是.xaml文件,只放绑定和样式;ViewModel 继承ViewModelBase,持有命令和属性;Model 是纯数据类,不依赖任何 WPF 类型。
常见错误是把业务逻辑写在 View 的代码隐藏文件里,或者 ViewModel 里直接引用MessageBox。前者导致无法单元测试,后者导致 ViewModel 和 UI 框架耦合。源码里的做法是:需要弹窗时,通过接口IDialogService注入,或者用事件聚合器解耦。我一般会定义一个简单的IDialogService,在 ViewModel 里调用_dialogService.Show("保存成功"),具体实现放在 View 层。这样 ViewModel 就能脱离 WPF 运行,测试起来方便很多。
4. 避坑与排查:WPF 新手最容易翻车的五个场景
4.1 绑定不生效,输出窗口一堆 Binding Error
现象:界面上的值不显示,或者改了数据 UI 不动。原因:DataContext没设、属性名拼错、或者属性没有实现INotifyPropertyChanged。解决:打开 VS 的输出窗口,看有没有System.Windows.Data Error: 40之类的提示,它会告诉你哪个绑定路径找不到。然后检查DataContext是否在构造函数里赋值,XAML 里{Binding}的属性名是否和 ViewModel 里完全一致,大小写敏感。
4.2 命令按钮一直是灰的
现象:按钮绑定了Command,但始终不可点击。原因:CanExecute返回了 false,或者没有触发重新查询。解决:先确认CanExecute逻辑,可以在里面打个断点。如果逻辑没问题,检查是否用了CommandManager.RequerySuggested。有些场景需要手动调用CommandManager.InvalidateRequerySuggested()强制刷新,比如异步操作完成后。
4.3 样式模板改了没反应
现象:在App.xaml里定义了全局样式,但某个控件不生效。原因:控件自己设置了Style属性,局部样式覆盖了全局样式;或者资源字典合并顺序有问题,后面的覆盖前面的。解决:检查控件上有没有显式Style="{StaticResource ...}",有的话会优先用局部的。另外,BasedOn可以继承基础样式,别重复造轮子。
4.4 自定义控件依赖属性不更新
现象:自己写的UserControl暴露了一个依赖属性,绑定后值变了但界面不刷新。原因:依赖属性的元数据没设FrameworkPropertyMetadataOptions.AffectsRender或者回调没写对。解决:注册依赖属性时,用FrameworkPropertyMetadata并指定PropertyChangedCallback,在回调里手动更新内部控件的值。或者更简单,内部用Binding相对绑定到自身属性。
4.5 动画卡顿或内存泄漏
现象:动画跑起来掉帧,或者反复触发后内存一直涨。原因:用了DoubleAnimation但没设Storyboard的FillBehavior,导致动画结束后属性值被锁定;或者事件订阅没取消。解决:动画结束后如果需要保持最终值,设FillBehavior="Stop"并手动赋值;如果不需要保持,用FillBehavior="Stop"让属性回到原值。事件方面,Loaded里订阅的事件记得在Unloaded里取消订阅,不然控件被回收时事件还挂着,内存就泄漏了。
5. 二次开发与改造:把这套源码变成你自己的项目底座
5.1 从示例到产品:需要补哪些工程化能力
这套源码是教学性质的,每个章节独立,没有统一的日志、配置、异常处理。如果你想拿它当项目底座,第一件事是加一个全局异常捕获。在App.xaml.cs里订阅DispatcherUnhandledException:
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 捕获 UI 线程未处理异常,避免直接崩溃 DispatcherUnhandledException += (s, args) => { MessageBox.Show($"发生错误:{args.Exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); args.Handled = true; // 标记已处理,阻止应用退出 }; }参数说明:args.Handled = true表示异常已处理,应用继续运行。生产环境建议记录日志而不是弹窗,这里只是演示。另外,AppDomain.CurrentDomain.UnhandledException可以捕获非 UI 线程的异常,但那个无法阻止进程退出,只能记录。
5.2 用依赖注入管理 ViewModel 和 Service
教学代码里 ViewModel 都是直接new出来的,项目大了不好维护。我一般会引入Microsoft.Extensions.DependencyInjection,在App.xaml.cs里注册:
public partial class App : Application { public IServiceProvider Services { get; } public App() { var services = new ServiceCollection(); services.AddSingleton<IDialogService, DialogService>(); services.AddTransient<MainViewModel>(); services.AddTransient<MainWindow>(); Services = services.BuildServiceProvider(); } protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var mainWindow = Services.GetRequiredService<MainWindow>(); mainWindow.DataContext = Services.GetRequiredService<MainViewModel>(); mainWindow.Show(); } }这样 ViewModel 的依赖通过构造函数注入,测试时可以替换成 Mock 实现。注意 WPF 的App.xaml默认没有StartupUri,需要手动去掉或者改成用代码启动,不然会创建两个窗口。
5.3 改造验证:一个可复用的检查清单
改完之后怎么验证没改坏?我习惯走一遍这个清单:
| 检查项 | 验证方式 | 预期结果 |
|---|---|---|
| 数据绑定 | 修改 ViewModel 属性,看 UI 是否刷新 | 实时更新,无延迟 |
| 命令可用性 | 触发 CanExecute 条件变化 | 按钮状态同步变化 |
| 样式继承 | 修改全局样式,看所有控件 | 统一生效,无遗漏 |
| 异常处理 | 在按钮事件里抛异常 | 弹窗提示,应用不崩 |
| 内存泄漏 | 反复打开关闭窗口,看任务管理器 | 内存稳定,不持续增长 |
提示:内存泄漏检查可以用 VS 的“诊断工具”,拍快照对比对象数量。如果某个 ViewModel 实例数量只增不减,多半是事件没取消订阅。
5.4 一个具体技巧:用 Blend 提取控件模板
WPF 最强大的功能之一是模板化。如果你想改一个Button的样式,不用从头写ControlTemplate。在 VS 的设计视图里右键按钮,选“编辑模板”->“编辑副本”,它会自动生成当前模板的 XAML 代码到资源字典里。然后你在这个基础上改,比凭空写快得多。源码里 Chapter04 的模板示例就是这么来的。我每次做自定义控件,都先用这招把默认模板扒下来,再动刀,省得漏掉VisualState或者Trigger。
从那以后我每次拿到一套 WPF 源码,都强制走一遍“跑通 -> 改绑定 -> 加日志 -> 拆模块”的流程,不跑通不动手改。这套《葵花宝典 WPF自学手册》源代码,我建议你也按这个顺序来,先让 Chapter02 的绑定示例跑起来,再顺着命令、模板、MVVM 一路改下去。希望帮到你。
本文还有配套的精品资源,点击获取