
简介本资源是《葵花宝典 WPF自学手册》配套的完整源代码工程包面向WPF初学者与界面开发实践者旨在通过真实项目案例辅助理解XAML布局、数据绑定、MVVM模式、资源字典、样式模板等核心机制。压缩包共1713个文件涵盖655个C#逻辑文件、385个XAML界面定义、109个csproj工程配置及105个sln解决方案文件辅以resx本地化资源、settings配置项、exe可执行示例和jpg/png图标素材结构完整覆盖从基础控件到多媒体WMA/WMV集成的典型WPF应用场景。包体大小为38.54MB目录组织清晰含build.bat构建脚本与多套APS/BAML编译中间产物体现典型Visual Studio WPF项目生成流程。已有1097人学习下载读者可直接加载sln工程运行全部示例深入理解书中知识点的代码实现细节并复用模块化UI组件与配置结构开展自主开发。1. 这不是一本“秘籍”而是一套能让你在三天内跑通第一个 WPF 桌面应用的实战源码包你有没有试过打开某本号称“零基础学WPF”的书翻到第3章就卡在App.xaml的StartupUri报错上或者照着教程写完MainWindow.xaml编译通过了但窗口一弹出来就是空白——连按钮都看不见这不是你手残是 WPF 的初始化链路太“诚实”它不隐藏任何细节也不替你兜底。而这份《葵花宝典 WPF自学手册 源代码》压根没打算当教科书它是一整套可直接双击.sln文件运行、每个项目都带完整 UI 交互、所有 XAML 和后台逻辑都经过 VS2022 .NET 6 双验证的工程集合。它覆盖了从最基础的TextBox绑定、DataGrid动态加载到MVVM模式下ICommand手动实现、ObservableCollection实时刷新再到UserControl封装复用和简单样式资源字典管理——全都是真实开发中你会立刻用上的最小可行模块。适合刚写完控制台程序、想真正做出一个“有窗体、能点、会响应”的桌面程序的开发者也适合被 MVVM 概念绕晕、需要先看到“命令怎么触发、数据怎么动起来”的转岗前端或后端同学。它不讲“WPF 是什么”只回答“我改哪一行按钮就能变颜色”。2. 项目结构与技术栈看清这 5 个核心工程你就摸到了 WPF 开发的主干道这份源码不是单个解决方案而是由5 个独立、递进、功能明确的 Visual Studio 解决方案.sln组成每个都对应 WPF 学习路径上的一个关键关卡。它们全部基于.NET 6 SDK构建不依赖 .NET Framework避免了旧版 GAC 注册、运行时版本冲突等历史包袱。所有项目均使用C# 10 语法如顶层语句、文件范围命名空间XAML 编写严格遵循 WPF 原生规范未引入第三方 UI 框架如 MahApps.Metro 或 HandyControl确保你学到的是纯正、可迁移的 WPF 底层能力。提示不要试图在一个解决方案里找“全部功能”。每个.sln都是一个独立可运行的最小闭环。这是刻意设计——降低认知负荷避免新手在上百个文件里迷失。2.1 工程清单与定位说明从“Hello World”到“可维护界面”工程名称对应.sln文件核心目标关键技术点典型文件数WpfBasicsDemoWpfBasicsDemo.sln理解 WPF 生命周期与基础控件Application,Window,Button,TextBox,Event Handler8 个含 App.xaml, MainWindow.xamlWpfBindingDemoWpfBindingDemo.sln掌握数据绑定机制DataContext,Binding,INotifyPropertyChanged,TwoWay模式12 个含 ViewModel 类、Model 类WpfMvvmDemoWpfMvvmDemo.sln实现轻量级 MVVM 模式ICommand,RelayCommand,ObservableCollectionT,ResourceDictionary19 个含ViewModelBase,MainViewModel,Styles.xamlWpfUserControlDemoWpfUserControlDemo.sln封装可复用 UI 组件UserControl,DependencyProperty,TemplateBinding,ContentPresenter15 个含SearchBox.xaml,DataCard.xamlWpfThemeDemoWpfThemeDemo.sln管理主题与样式资源StaticResource,DynamicResource,ThemeDictionary,BasedOn继承11 个含LightTheme.xaml,DarkTheme.xaml,App.xaml资源合并这些工程不是按“难度”线性排列而是按“职责分离”逐步展开WpfBasicsDemo让你确认环境没问题WpfBindingDemo揭示数据如何驱动 UIWpfMvvmDemo把逻辑和视图彻底拆开后两个则解决“怎么让代码不越写越乱”的工程问题。我第一次带新人时强制要求他们必须先完整跑通WpfBasicsDemo并修改其标题栏文字再进入下一个——看似简单实则是过滤掉环境配置失败、SDK 版本不匹配等底层问题的关键闸口。2.2 目录结构解析为什么App.xaml.cs里只有一行InitializeComponent()以WpfMvvmDemo.sln为例其典型目录结构如下精简后WpfMvvmDemo/ ├── App.xaml # 应用入口定义全局资源和 StartupUri ├── App.xaml.cs # 仅含 InitializeComponent()无业务逻辑 ├── MainWindow.xaml # 主窗口仅声明 Grid 和 ContentControl ├── MainWindow.xaml.cs # 空文件UI 逻辑完全交由 ViewModel ├── Views/ │ ├── MainView.xaml # 真正的 UI 定义按钮、列表、输入框 │ └── MainView.xaml.cs # 空纯粹容器 ├── ViewModels/ │ ├── ViewModelBase.cs # 实现 INotifyPropertyChanged 的基类 │ ├── MainViewModel.cs # 核心业务逻辑属性、命令、集合 │ └── RelayCommand.cs # 手写 ICommand 实现非 Prism/MvvmLight ├── Models/ │ └── Person.cs # 简单数据模型含属性通知 ├── Resources/ │ ├── Styles.xaml # 全局样式如 Button 默认模板 │ └── Brushes.xaml # 颜色资源供多处引用 └── Properties/ └── AssemblyInfo.cs # .NET 6 中已简化通常为空这个结构的关键在于MainWindow.xaml不再承载任何 UI 控件它只是一个壳真正的界面定义下沉到Views/MainView.xaml所有状态和行为封装在ViewModels/下。这种分层不是为了炫技而是为了解决一个实际问题当你需要把“用户列表页”改成“订单列表页”时你只需替换MainView.xaml的内容和MainViewModel.cs的数据源MainWindow本身完全不用动。我在某高校实验室带学生做课程设计时就靠这套结构让 3 个小组并行开发不同模块最后一天才合并——零冲突。2.3 技术选型深挖为什么坚持手写RelayCommand而不引入 NuGet 包WpfMvvmDemo中的RelayCommand.cs是全手动实现代码仅 40 行却精准覆盖了 WPF MVVM 的核心契约// RelayCommand.cs public class RelayCommand : ICommand { private readonly Action _execute; private readonly Funcbool _canExecute; public RelayCommand(Action execute, Funcbool canExecute null) { _execute execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute canExecute; } public bool CanExecute(object parameter) _canExecute?.Invoke() ?? true; public void Execute(object parameter) _execute(); // WPF 要求实现的事件用于通知 UI 重绘 CanExecute 状态 public event EventHandler CanExecuteChanged { add CommandManager.RequerySuggested value; remove CommandManager.RequerySuggested - value; } }这段代码的价值不在“多高级”而在可调试、可理解、可修改。对比Prism.Commands或CommunityToolkit.Mvvm它们封装太深一旦CanExecute返回false导致按钮灰掉你得层层跳进反编译代码才能定位是CanExecute方法抛异常还是CommandManager.RequerySuggested没触发。而手写版你加个断点一眼看到_canExecute?.Invoke()返回了什么。更关键的是它强制你理解CommandManager.RequerySuggested—— 这个 WPF 内部的“重查询建议”机制是很多教程里一笔带过的黑匣子。我见过太多人把CanExecute写成return false;后疯狂刷新界面却不知道要调用CommandManager.InvalidateRequerySuggested()才能强制重算。这份源码里每个RelayCommand实例创建后都紧跟着一句// 在 MainViewModel.cs 构造函数中 SaveCommand new RelayCommand(Save, CanSave); // 紧跟其后显式触发一次重查询模拟用户输入后状态变化 CommandManager.InvalidateRequerySuggested();这就是“血泪经验”WPF 不会自动监听你的私有字段变化来重算CanExecute你得亲手告诉它“该重新问一遍了”。3. 快速运行指南从解压到第一个可交互窗口三步到位附常见报错直击别急着看代码。先确保你能把它跑起来——这是建立信心的第一步。整个过程不需要安装额外 SDK只要你的 VS2022 是最新稳定版也不需要改任何配置。以下是我在三台不同配置电脑Win10/Win11VS2022 17.3/17.5/17.7上反复验证的流程。3.1 环境准备检查这三项省去 80% 的“打不开”问题Visual Studio 2022推荐 17.4必须安装“.NET 桌面开发” 工作负载。打开 VS Installer → 修改 → 勾选此项。若只装了“ASP.NET 和 Web 开发”WPF 项目将无法加载。.NET 6 SDKx64访问 https://dotnet.microsoft.com/download/dotnet/6.0 下载并安装。注意.NET 6 Runtime不够必须是SDK含编译器。解压路径不含中文或空格这是 Windows 下 MSBuild 的经典玄学。请将源码包解压到类似D:\WpfDemo\这样的纯英文路径。曾有学员解压到D:\我的文档\葵花宝典\结果MSB4019错误满屏飞——不是代码问题是路径编码惹的祸。注意无需安装 .NET Framework 4.x所有工程 TargetFramework 均为net6.0-windows走的是 .NET Core 的现代运行时。3.2 运行第一个工程WpfBasicsDemo的完整操作链进入解压目录找到WpfBasicsDemo/WpfBasicsDemo.sln双击该文件VS2022 自动启动并加载解决方案在“解决方案资源管理器”中右键WpfBasicsDemo项目 → “设为启动项目”按CtrlF5不调试启动等待几秒一个标题为 “WPF 基础演示” 的窗口弹出点击窗口中的 “点击我” 按钮下方TextBox显示 “按钮被点击了”。✅ 成功标志窗口正常显示按钮可点击文本实时更新。此时你已越过 WPF 最高门槛——环境通了。3.3 避坑 / 常见问题 / 排查那些让你怀疑人生的报错其实都有固定解法现象打开.sln后项目名旁显示(不支持)且“解决方案资源管理器”中所有文件呈灰色原因VS 未安装.NET 桌面开发工作负载或安装了但未重启 VS。解决关闭所有 VS 实例 → 打开 VS Installer → 修改当前 VS → 勾选.NET 桌面开发→ 点击“修改” → 完成后重启 VS。现象编译时报错CS0234: 命名空间“System.Windows”中不存在类型或命名空间名“Controls”原因项目文件.csproj中TargetFramework被意外修改或 VS 读取了错误的 SDK 版本。解决右键项目 → “编辑项目文件”确认首行是TargetFrameworknet6.0-windows/TargetFramework若为net6.0缺-windows手动补全保存后右键项目 → “重新加载项目”。现象运行后窗口空白F12 查看元素树Live Visual Tree发现MainWindow下只有Grid但无子控件原因MainWindow.xaml中的Content属性被清空或StartupUri指向了错误的 XAML 文件。解决打开App.xaml检查Application.Resources上方是否有Application.StartupUri标签其Source值必须为MainWindow.xaml同时打开MainWindow.xaml确认Grid标签内有实际控件如Button Content点击我/。现象点击按钮无反应TextBox文本不更新但编译运行无报错原因Button.Click事件未正确绑定到后台方法或后台方法签名错误如缺少object sender, RoutedEventArgs e参数。解决打开MainWindow.xaml检查Button ClickButton_Click ... /再打开MainWindow.xaml.cs确认存在private void Button_Click(object sender, RoutedEventArgs e)方法且方法体内有textBox.Text ...类似逻辑。现象WpfMvvmDemo运行后列表为空但 ViewModel 中Items集合明明已 Add 了数据原因Items属性返回的是ListT而非ObservableCollectionT导致 UI 无法监听集合变更。解决打开MainViewModel.cs将private ListPerson _items new();改为private ObservableCollectionPerson _items new();并确保Items属性 getter 返回this._items而非new ObservableCollectionPerson(_items)。4. 核心模块拆解WpfMvvmDemo中一个请求从用户点击到数据刷新的完整链路现在我们深入WpfMvvmDemo跟踪一次完整的用户交互点击“添加用户”按钮 → 弹出输入框 → 输入姓名 → 点击确定 → 新用户出现在DataGrid中。这条链路覆盖了 WPF MVVM 的四大支柱Binding、INotifyPropertyChanged、ICommand、ObservableCollection。它不是理论推演而是你打开 VS 后能逐行打断点、亲眼看到数据流动的物理路径。4.1 XAML 层MainView.xaml如何声明“我要什么”MainView.xaml是 UI 的蓝图它不写逻辑只描述“长什么样”和“绑定到谁”!-- MainView.xaml -- Grid Margin10 !-- 1. 添加按钮绑定到 ViewModel 的 AddCommand -- Button Content添加用户 Command{Binding AddCommand} HorizontalAlignmentLeft Margin0,0,0,10/ !-- 2. 数据表格ItemsSource 绑定到 ViewModel 的 Items 集合 -- DataGrid ItemsSource{Binding Items} AutoGenerateColumnsFalse Margin0,30,0,0 DataGrid.Columns !-- 列定义Binding PathName表示取 Items 中每个对象的 Name 属性 -- DataGridTextColumn Header姓名 Binding{Binding Name} Width*/ DataGridTextColumn Header年龄 Binding{Binding Age} Width80/ /DataGrid.Columns /DataGrid /Grid关键点Command{Binding AddCommand}不是调用方法而是声明“这个按钮的点击行为委托给 ViewModel 里的AddCommand属性去处理”ItemsSource{Binding Items}不是赋值而是建立“双向监听”——Items集合一变DataGrid自动刷新Binding{Binding Name}DataGrid的每一行都会把当前数据项即Person对象作为DataContext再从中取Name属性。4.2 ViewModel 层MainViewModel.cs如何响应并驱动状态MainViewModel.cs是大脑它持有数据、定义行为、通知 UI// MainViewModel.cs public class MainViewModel : ViewModelBase { // 1. 私有字段存储数据 private ObservableCollectionPerson _items new(); // 2. 公开属性供 XAML Binding必须 Notify public ObservableCollectionPerson Items { get _items; private set { _items value; OnPropertyChanged(); // 通知 UIItems 属性变了 } } // 3. 命令属性供 XAML Command 绑定 public ICommand AddCommand { get; } // 4. 构造函数初始化命令和默认数据 public MainViewModel() { // 初始化命令传入执行方法和启用条件 AddCommand new RelayCommand(ExecuteAdd, CanAdd); // 加载初始数据模拟从数据库读取 Items.Add(new Person { Name 张三, Age 25 }); Items.Add(new Person { Name 李四, Age 30 }); } // 5. 命令执行方法用户点击后真正干活的地方 private void ExecuteAdd() { // 弹出输入框此处简化为 Console实际可用 Window var name Microsoft.VisualBasic.Interaction.InputBox(请输入姓名, 添加用户); if (!string.IsNullOrWhiteSpace(name)) { // 创建新对象并加入集合 → 触发 ObservableCollection 的 CollectionChanged 事件 Items.Add(new Person { Name name, Age 20 }); } } // 6. 命令启用条件决定按钮是否可点击 private bool CanAdd() true; // 简单起见始终启用 }这里藏着两个关键机制OnPropertyChanged()是ViewModelBase提供的方法它内部调用PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName))WPF 的 Binding 引擎正是监听这个事件来刷新 UIItems.Add(...)调用的是ObservableCollectionT.Add()它内部会触发CollectionChanged事件DataGrid正是订阅了这个事件所以一加就立刻显示。4.3 Model 层Person.cs如何保证属性变更可被感知Person.cs是数据载体它必须实现INotifyPropertyChanged否则Name字段改了UI 也看不到// Person.cs public class Person : INotifyPropertyChanged { private string _name; private int _age; public string Name { get _name; set { if (_name ! value) { _name value; OnPropertyChanged(); // 通知Name 属性变了 } } } public int Age { get _age; set { if (_age ! value) { _age value; OnPropertyChanged(); // 通知Age 属性变了 } } } // 实现 INotifyPropertyChanged 接口 public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }注意if (_name ! value)这个判断它防止无意义的重复通知避免 UI 无效刷新。这是性能优化的起点。很多新手直接写set { _name value; OnPropertyChanged(); }结果改个空格都触发重绘列表滚动卡顿。4.4 Binding 引擎工作流从 XAML 到内存的七步穿透当你点击“添加用户”按钮背后发生了什么不是魔法是七步确定的流程用户点击 → WPF 触发Button.Click事件Button的Command属性绑定了AddCommand→ 引擎调用AddCommand.Execute(null)RelayCommand.Execute()执行ExecuteAdd()方法ExecuteAdd()调用Items.Add(new Person {...})ObservableCollectionPerson.Add()内部触发CollectionChanged事件DataGrid订阅了CollectionChanged→ 收到通知调用Items.Refresh()或增量更新DataGrid为新Person对象生成新行并对每列执行Binding{Binding Name}→ 访问Person.Name→ 因Name是可通知属性DataGridTextColumn自动显示新值。这七步每一步都可在 VS 中打断点验证。比如在ExecuteAdd()第一行打点F10 单步你会清晰看到Items.Count从 2 变成 3紧接着DataGrid的ItemsSource下的Count也同步变化。这才是“看得见的 MVVM”。5. 二次开发实战如何把WpfUserControlDemo改造成一个带搜索的员工卡片组件现在你已经跑通、理解了核心链路。下一步是动手改——这才是掌握的开始。我们以WpfUserControlDemo为基础把它从一个静态示例升级为一个可复用、可搜索、可主题切换的员工信息卡片组件。这个改造不追求大而全只聚焦三个真实需求搜索过滤、卡片样式定制、深色模式适配。做完后你就能把这个组件拖进任何新项目像用原生Button一样使用。5.1 需求分析与改造规划不做“大手术”只加“小插件”原始WpfUserControlDemo包含一个EmployeeCard.xaml它只显示一个Person对象的姓名和头像。我们要让它支持搜索过滤在卡片上方加一个搜索框输入文字后只显示姓名匹配的卡片样式定制允许外部通过Style覆盖卡片背景、字体大小等深色模式当系统切换深色主题时卡片自动变暗。改造原则不破坏原有接口所有新增功能通过附加属性Attached Property或依赖属性DependencyProperty暴露。这样老代码继续用新功能按需开启。5.2 搜索功能实现在EmployeeCard.xaml中注入FilterText依赖属性第一步在EmployeeCard.xaml.cs中定义FilterText属性它是DependencyProperty因此能被 Binding 和动画驱动// EmployeeCard.xaml.cs public partial class EmployeeCard : UserControl { // 1. 定义依赖属性FilterText public static readonly DependencyProperty FilterTextProperty DependencyProperty.Register( FilterText, // 属性名 typeof(string), // 类型 typeof(EmployeeCard), // 所属类 new PropertyMetadata(string.Empty, OnFilterTextChanged)); // 默认值 变更回调 // 2. .NET 属性包装器供 C# 代码访问 public string FilterText { get (string)GetValue(FilterTextProperty); set SetValue(FilterTextProperty, value); } // 3. 属性变更回调当 FilterText 改变时触发过滤逻辑 private static void OnFilterTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var card d as EmployeeCard; card?.ApplyFilter((string)e.NewValue); } // 4. 过滤逻辑根据 FilterText 更新 UI 可见性 private void ApplyFilter(string filter) { if (string.IsNullOrWhiteSpace(filter)) { this.Visibility Visibility.Visible; // 无过滤显示 return; } // 假设 DataContext 是 Person检查 Name 是否包含 filter if (DataContext is Person person !string.IsNullOrEmpty(person.Name) person.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) 0) { this.Visibility Visibility.Visible; } else { this.Visibility Visibility.Collapsed; // 不匹配隐藏 } } public EmployeeCard() { InitializeComponent(); } }第二步在EmployeeCard.xaml中让搜索框绑定到这个属性!-- EmployeeCard.xaml -- UserControl x:ClassWpfUserControlDemo.EmployeeCard xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml Grid !-- 搜索框绑定到自身 FilterText 属性 -- TextBox x:NameSearchBox Text{Binding RelativeSource{RelativeSource AncestorTypeUserControl}, PathFilterText, UpdateSourceTriggerPropertyChanged} Margin0,0,0,10 Height25/ !-- 员工信息区 -- StackPanel Margin0,30,0,0 Image Source{Binding Avatar} Width60 Height60 / TextBlock Text{Binding Name} FontSize16 Margin0,10,0,0/ /StackPanel /Grid /UserControl关键点RelativeSource{RelativeSource AncestorTypeUserControl}让TextBox的Text绑定到EmployeeCard自身的FilterText而不是DataContext即Person。UpdateSourceTriggerPropertyChanged确保每敲一个字就触发过滤而非失去焦点才更新。5.3 样式与主题适配用DynamicResource替代StaticResource原始EmployeeCard.xaml中字体颜色写死为Black。我们要让它随主题变在Resources/Colors.xaml中定义动态资源!-- Resources/Colors.xaml -- ResourceDictionary xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation SolidColorBrush x:KeyCardForegroundBrush ColorBlack/ SolidColorBrush x:KeyCardBackgroundBrush ColorWhite/ /ResourceDictionary在EmployeeCard.xaml中使用DynamicResourceTextBlock Text{Binding Name} Foreground{DynamicResource CardForegroundBrush} Background{DynamicResource CardBackgroundBrush}/在App.xaml中根据系统主题动态合并资源字典!-- App.xaml -- Application.Resources ResourceDictionary ResourceDictionary.MergedDictionaries !-- 默认浅色主题 -- ResourceDictionary SourceResources/Colors.xaml/ !-- 深色主题可由代码切换 -- !-- ResourceDictionary SourceResources/DarkColors.xaml/ -- /ResourceDictionary.MergedDictionaries /ResourceDictionary /Application.ResourcesDynamicResource的魔力在于它不缓存资源引用每次渲染时都重新查找。所以当你在运行时替换App.Current.Resources.MergedDictionaries中的Colors.xaml为DarkColors.xaml所有用了{DynamicResource CardForegroundBrush}的控件会立刻变色。这比StaticResource只在加载时查一次更适合主题切换场景。5.4 验证与交付如何测试你的组件是否真的“可复用”改完后别急着打包。用三步验证它是否达到“可复用”标准独立测试新建一个空白 WPF 项目NuGet 安装WpfUserControlDemo的 DLL或直接项目引用在MainWindow.xaml中写local:EmployeeCard DataContext{Binding SelectedEmployee} FilterText{Binding SearchQuery}/确认SearchQuery改变时卡片能正确显示/隐藏。样式覆盖测试在新项目的App.xaml中重写CardForegroundBrushSolidColorBrush x:KeyCardForegroundBrush ColorRed/确认卡片文字变成红色——证明样式可被外部定制。主题切换测试在新项目中添加按钮点击时执行// 切换到深色主题 var darkDict new ResourceDictionary { Source new Uri(pack://application:,,,/Resources/DarkColors.xaml) }; App.Current.Resources.MergedDictionaries.Clear(); App.Current.Resources.MergedDictionaries.Add(darkDict);确认卡片背景变暗文字变亮。这三步走完你就完成了一个工业级可复用组件的最小闭环。从那以后我每次封装UserControl都强制走一遍这三步验证能否独立引用、能否样式覆盖、能否主题适配。少一步上线后就可能被产品追着改样式或者被测试报“深色模式下文字看不见”。希望帮到你。本文还有配套的精品资源点击获取