一、核心概念
| 术语 | 说明 |
|---|---|
| WPF | Windows Presentation Foundation,基于 DirectX 的 UI 框架,支持 2D/3D 图形、动画、矢量渲染 |
| WinForm | 基于 GDI+ 的传统 UI 框架,控件样式受限,WPF 是其现代化替代方案 |
| XAML | Extensible Application Markup Language,可扩展应用程序标记语言,用于声明式定义 UI |
| Code-Behind | 与 XAML 文件关联的 .cs 后台代码文件,负责交互逻辑 |
| Application | 应用程序类,管理生命周期和全局资源(对应 App.xaml) |
| Window | 窗体类,WPF 应用的基本容器 |
| Page | 页面类,配合 NavigationWindow 实现导航 |
| UserControl | 用户控件,可复用的自定义 UI 组件 |
| DataContext | 数据上下文,数据绑定的数据来源入口 |
| xmlns | XAML 命名空间声明,相当于 C# 中的 using |
| BAML | Binary Application Markup Language,XAML 编译后的二进制格式,嵌入到程序集中 |
| 依赖属性 | DependencyProperty,WPF 特有的属性系统,支持数据绑定、样式、动画等功能的基础 |
| 附加属性 | 由非自身类定义的依赖属性,如 Grid.Row,本质上是一种特殊的依赖属性 |
XAML 与 HTML/XML 的关系
cs
SGML(标准通用标记语言)
├─→ HTML(超文本标记语言,1991,用于网页)
└─→ XML(可扩展标记语言,1998,通用数据描述)
└─→ XAML(可扩展应用程序标记语言,2006,用于 WPF UI 定义)
XAML 本质上是符合 XML 规范的标记语言,但专门用于描述 .NET 对象树。每个 XAML 标签对应一个 .NET 类的实例。
编译原理: XAML → BAML(二进制资源文件)→ 嵌入 .dll/.exe 程序集。运行时由 WPF 引擎解析 BAML 重建对象树。
XAML 标签语法
| 语法要素 | 说明 | 示例 |
|---|---|---|
| 双标签 | 开始标签 + 内容区 + 结束标签 | <Button>确定</Button> |
| 单标签 | 自闭合,无内容区 | <Button /> |
| 特性语法 | 开始标签中的键值对属性 | Width="100" |
| 属性元素语法 | 将属性展开为子标签 | <Button.Background>...</Button.Background> |
| 命名元素 | 使用 x:Name 为控件命名,不可重复 |
x:Name="btnOK" |
| 附加属性 | 由父容器定义,附加到子元素 | Grid.Row="0" |
| 标记扩展 | 花括号语法,运行时求值 | {Binding Name}, {StaticResource Key} |
注意事项:
-
XAML 区分大小写(标签名与 C# 类名对应,均为大驼峰)
-
属性之间必须有空白分隔
-
标签的本质是 C# 类的实例化
项目结构
| 文件/目录 | 作用 |
|---|---|
.sln |
解决方案文件,管理多个项目 |
.csproj |
项目文件,记录引用、目标框架等配置 |
App.xaml |
应用程序入口,定义全局资源(类似 Program.cs) |
App.xaml.cs |
应用启动逻辑与全局事件 |
MainWindow.xaml |
主窗口界面定义 |
MainWindow.xaml.cs |
主窗口交互逻辑(Code-Behind) |
bin/ |
编译输出目录 |
obj/ |
编译中间文件(含 BAML) |
依赖属性与普通属性的区别
| 对比项 | CLR 普通属性 | 依赖属性 (DependencyProperty) |
|---|---|---|
| 值存储 | 私有字段(对象实例内) | WPF 属性系统全局哈希表 |
| 内存 | 每个实例都分配内存 | 未设置时共享默认值,节省内存 |
| 数据绑定 | 不支持 | ✔ 支持 |
| 样式/动画 | 不支持 | ✔ 支持 |
| 值继承 | 不支持 | ✔ 父元素可传递给子元素(如 FontSize) |
| 使用感受 | 完全相同(WPF 用 CLR 属性包装依赖属性) | 完全相同 |
日常开发中,使用依赖属性和使用普通属性写法完全一样 (
btn.Width = 100)。只有自定义控件需要注册新的依赖属性时,才需要了解其定义方式。
自定义依赖属性注册(VS 快捷输入:propdp + Tab):
cs
using System.Windows;
using System.Windows.Controls;
// 自定义按钮控件,添加圆角依赖属性
public class MyButton : Button
{
// 1. CLR 属性包装器(外部使用时的入口)
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
// 2. 注册依赖属性(静态只读字段,命名规则:属性名 + Property)
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register(
"CornerRadius", // 属性名
typeof(CornerRadius), // 属性类型
typeof(MyButton), // 所有者类型
new PropertyMetadata(default(CornerRadius)) // 默认值
);
}
要点: 注册后即可在 XAML 中像普通属性一样使用
<local:MyButton CornerRadius="10" />,并支持绑定、样式和动画。模板内通过{TemplateBinding CornerRadius}引用。
控件分类
| 类别 | 说明 | 常见控件 |
|---|---|---|
| 内容控件 | 只能包含单个子元素(Content 属性) | Button, Label, Border, Window, GroupBox |
| 条目控件 | 可包含多个子项(Items/ItemsSource) | ListBox, ComboBox, TabControl, Menu, TreeView |
| 文本控件 | 用于文本输入或显示 | TextBox, TextBlock, RichTextBox, PasswordBox |
| 范围控件 | 有最小/最大值范围 | Slider, ProgressBar, ScrollBar |
| 布局面板 | 管理子元素排列方式 | Grid, StackPanel, DockPanel, WrapPanel, Canvas |
| 图形控件 | 绘制形状 | Rectangle, Ellipse, Line, Path, Polygon |
| 媒体控件 | 显示图片/播放媒体 | Image, MediaElement |
| 日期控件 | 日期选择 | DatePicker, Calendar |
二、常用操作
2.1 启动窗体的三种方式
| 方式 | 说明 | 推荐 |
|---|---|---|
| StartupUri | 在 App.xaml 中直接指定启动页面 | ✔ 简单直接 |
| Startup 事件 | 通过事件方法启动,可携带参数/做初始化 | ✔ 灵活 |
| 自定义 Main | 删除 App.xaml 的 Build Action,手动创建 Application | ✘ 不建议 |
cs
<!-- 方式1:StartupUri(最简单,推荐) -->
<Application x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources />
</Application>
<!-- 方式2:Startup 事件(可做初始化逻辑,推荐) -->
<Application x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="Application_Startup">
<Application.Resources />
</Application>
// 方式2 后台代码:可在启动前执行初始化
private void Application_Startup(object sender, StartupEventArgs e)
{
// 示例:可以在此做登录验证、加载配置等
MainWindow win = new MainWindow();
win.Show();
}
// 方式3:自定义 Main 方法(不建议,仅了解)
[STAThread]
static void Main()
{
Application app = new Application();
Window1 win = new Window1();
app.Run(win); // Run() 会阻塞直到窗体关闭
}
2.2 Window 窗体属性
| 属性 | 说明 | 常用值 |
|---|---|---|
| Title | 窗体标题栏文字 | "我的应用" |
| Icon | 窗体图标(.ico 文件) | "Assets/app.ico" |
| WindowState | 启动时窗体状态 | Normal / Maximized / Minimized |
| WindowStyle | 窗体边框样式 | SingleBorderWindow / None(无边框) |
| WindowStartupLocation | 启动位置 | CenterScreen / CenterOwner / Manual |
| ResizeMode | 是否可调整大小 | CanResize / NoResize / CanMinimize |
| Topmost | 是否始终在最前 | True / False |
| ShowInTaskbar | 是否显示在任务栏 | True / False |
cs
<!-- 常见窗体配置示例 -->
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="仓库管理系统" Width="1024" Height="768"
WindowStartupLocation="CenterScreen"
WindowState="Normal"
Icon="Assets/app.ico"
ResizeMode="CanResize">
</Window>
2.3 常用控件属性速查
| 属性 | 说明 | 值示例 |
|---|---|---|
| Width / Height | 宽度 / 高度(单位:设备无关像素) | 200 |
| MinWidth / MaxWidth | 最小/最大宽度约束 | 100 / 500 |
| Content | 内容(Button、Label 等内容控件) | "确定" |
| Text | 文本(TextBlock、TextBox) | "Hello" |
| Background | 背景色 | "Red" / "#FF6B6B" |
| Foreground | 前景色(字体颜色) | "White" / "#333333" |
| FontFamily | 字体族 | "微软雅黑" / "Consolas" |
| FontSize | 字号 | 14 |
| FontWeight | 字体粗细 | Normal / Bold / SemiBold |
| FontStyle | 字体样式 | Normal / Italic |
| TextAlignment | 文本水平对齐(用于文本控件内部) | Left / Center / Right |
| TextWrapping | 文本换行 | NoWrap / Wrap / WrapWithOverflow |
| Margin | 外边距(元素与外部的距离) | "10" / "10,5" / "10,5,10,5" |
| Padding | 内边距(元素内容与边框的距离) | 同 Margin |
| HorizontalAlignment | 元素在容器中水平对齐 | Left / Center / Right / Stretch |
| VerticalAlignment | 元素在容器中垂直对齐 | Top / Center / Bottom / Stretch |
| HorizontalContentAlignment | 内容在元素中水平对齐 | 同上 |
| VerticalContentAlignment | 内容在元素中垂直对齐 | 同上 |
| BorderBrush | 边框颜色 | "Gray" |
| BorderThickness | 边框粗细 | "2" / "1,2,1,2" |
| Opacity | 透明度(0=全透明,1=不透明) | 0.8 |
| Visibility | 可见性 | Visible / Hidden / Collapsed |
| Panel.ZIndex | 控件层级(值越大越靠前) | 10 |
| IsEnabled | 是否启用 | True / False |
| Cursor | 鼠标光标样式 | Hand / Arrow / Wait |
Margin/Padding/BorderThickness 多值规则:
1 个值 → 四边相同:
Margin="10"等价于10,10,10,102 个值 → 左右, 上下:
Margin="10,5"等价于10,5,10,54 个值 → 左, 上, 右, 下:
Margin="10,5,20,15"
Visibility 三值区别:
Visible--- 可见
Hidden--- 不可见,但仍占据布局空间
Collapsed--- 不可见,且不占据空间
2.4 属性元素语法与内容语法
cs
<!-- 特性语法:简单值直接写在属性中 -->
<Button Background="Red" Content="按钮" Width="100" />
<!-- 属性元素语法:复杂值需展开为子标签 -->
<Button Width="100" Height="50">
<Button.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#FF6B6B" Offset="0" />
<GradientStop Color="#4ECDC4" Offset="1" />
</LinearGradientBrush>
</Button.Background>
<Button.Content>
<!-- Content 也可以是复杂对象 -->
<StackPanel Orientation="Horizontal">
<Ellipse Width="10" Height="10" Fill="White" Margin="0,0,5,0" />
<TextBlock Text="渐变按钮" Foreground="White" />
</StackPanel>
</Button.Content>
</Button>
2.5 XAML 实体字符
| 实体 | 字符 | 说明 |
|---|---|---|
< |
< | 小于号(less than) |
> |
> | 大于号(greater than) |
& |
& | 和号 |
" |
" | 双引号 |
  |
(空格) | 不间断空格 |
在 XAML 中如果 Content 或 Text 需要包含
<>等特殊字符,必须使用实体字符。
2.6 布局面板
| 面板 | 说明 | 适用场景 |
|---|---|---|
| Grid | 行列网格布局,最灵活 | 表单、复杂页面整体布局 |
| StackPanel | 线性堆叠(垂直/水平) | 工具栏、简单列表 |
| WrapPanel | 超出宽度自动换行 | 标签云、图片墙 |
| DockPanel | 子元素停靠在四边 | 应用主框架(顶部导航+侧边栏) |
| Canvas | 绝对坐标定位 | 绘图、拖拽场景 |
| UniformGrid | 等分网格 | 计算器按钮、棋盘 |
cs
<!-- Grid:定义行列,使用 * 按比例分配空间 -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Auto:根据内容自适应 -->
<RowDefinition Height="*" /> <!-- *:占据剩余全部空间 -->
<RowDefinition Height="50" /> <!-- 固定50像素 -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" /> <!-- 固定200像素 -->
<ColumnDefinition Width="*" /> <!-- 1份剩余空间 -->
<ColumnDefinition Width="2*" /> <!-- 2份剩余空间 -->
</Grid.ColumnDefinitions>
<!-- 通过附加属性指定行列位置(从0开始) -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="姓名:" />
<TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" />
<Button Grid.Row="2" Grid.Column="2" Content="提交" />
</Grid>
<!-- DockPanel:子元素依次停靠,最后一个填充剩余空间 -->
<DockPanel LastChildFill="True">
<Menu DockPanel.Dock="Top" Height="25" />
<StatusBar DockPanel.Dock="Bottom" Height="25" />
<TreeView DockPanel.Dock="Left" Width="200" />
<!-- 最后一个子元素自动填充中间区域 -->
<Grid Background="White" />
</DockPanel>
<!-- StackPanel:垂直/水平堆叠 -->
<StackPanel Orientation="Vertical" Margin="10">
<TextBlock Text="用户名:" Margin="0,0,0,5" />
<TextBox Width="200" HorizontalAlignment="Left" />
<TextBlock Text="密码:" Margin="0,10,0,5" />
<PasswordBox Width="200" HorizontalAlignment="Left" />
</StackPanel>
<!-- WrapPanel:超出时自动换行 -->
<WrapPanel Orientation="Horizontal">
<Button Content="标签1" Margin="3" Padding="8,4" />
<Button Content="标签2" Margin="3" Padding="8,4" />
<Button Content="标签3" Margin="3" Padding="8,4" />
<!-- 宽度不够时自动换到下一行 -->
</WrapPanel>
<!-- Canvas:绝对定位 -->
<Canvas>
<Rectangle Canvas.Left="50" Canvas.Top="30"
Width="100" Height="60" Fill="Blue" />
<Ellipse Canvas.Left="200" Canvas.Top="50"
Width="80" Height="80" Fill="Red" />
</Canvas>
2.7 导航窗体
| 步骤 | 操作 |
|---|---|
| 1 | XAML 中将 <Window> 改为 <NavigationWindow> |
| 2 | .cs 文件中将基类 Window 改为 NavigationWindow |
| 3 | 设置 Source 属性指向初始 Page |
| 4 | 在 Page 中通过 NavigationService 进行页面跳转 |
cs
<!-- NavigationWindow 声明 -->
<NavigationWindow x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Source="Pages/HomePage.xaml"
Title="导航示例" Width="800" Height="450">
</NavigationWindow>
// MainWindow.xaml.cs - 基类对应修改
public partial class MainWindow : NavigationWindow
{
public MainWindow()
{
InitializeComponent();
}
}
// Page 中执行页面跳转
private void BtnNext_Click(object sender, RoutedEventArgs e)
{
// 前进到新页面
this.NavigationService.Navigate(new Page2());
}
private void BtnBack_Click(object sender, RoutedEventArgs e)
{
// 返回上一页
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
2.8 路由事件
| 事件类型 | 传播方向 | 说明 |
|---|---|---|
| Direct(直接事件) | 不传播 | 仅在事件源触发,如双击绑定 |
| Bubbling(冒泡事件) | 内 → 外 | 从子元素向父元素逐层冒泡 |
| Tunneling(隧道事件) | 外 → 内 | 从父元素向子元素逐层捕获,名称通常以 Preview 开头 |
cs
<!-- 冒泡事件示例:点击内部 Button,外层 Grid 也能捕获 -->
<Grid ButtonBase.Click="Grid_Click">
<StackPanel>
<Button Content="按钮A" />
<Button Content="按钮B" />
</StackPanel>
</Grid>
// Grid 统一处理子元素的 Click 事件(冒泡)
private void Grid_Click(object sender, RoutedEventArgs e)
{
// e.Source:引发事件的逻辑源(Button)
// e.OriginalSource:可视化树中最初触发的元素(可能是 Button 内部的 TextBlock)
Button btn = e.Source as Button;
if (btn != null)
MessageBox.Show($"你点击了:{btn.Content}");
}
2.9 数据绑定
| 绑定方式 | 说明 |
|---|---|
| 后台代码赋值 | 在 .cs 中直接操作控件属性 |
| 静态资源绑定 | 在 Resources 中定义数据,用 {StaticResource Key} 引用 |
| 元素绑定 | 绑定到其他控件的属性,用 {Binding ElementName=..., Path=...} |
| ViewModel 绑定 | 设置 DataContext,用 {Binding 属性名} |
| 绑定模式 (Mode) | 方向 | 典型场景 |
|---|---|---|
| Default | 由控件决定 | 大多数情况 |
| OneWay | 源 → 目标 | 只读显示(TextBlock) |
| TwoWay | 源 ↔ 目标 | 表单输入(TextBox) |
| OneTime | 仅初始化一次 | 固定配置 |
| OneWayToSource | 目标 → 源 | 将 UI 状态回写 |
UpdateSourceTrigger(源更新时机,仅 TwoWay/OneWayToSource 有效):
| 值 | 说明 |
|---|---|
| Default | 取决于控件类型(TextBox 默认 LostFocus,其他多数为 PropertyChanged) |
| PropertyChanged | 属性值一变就立即更新源 |
| LostFocus | 控件失去焦点时更新源(TextBox 默认值) |
| Explicit | 仅在代码中手动调用 UpdateSource() 时更新 |
cs
<!-- 1. 在 Window.Resources 中定义数据 -->
<!-- 需引入命名空间:xmlns:sys="clr-namespace:System;assembly=mscorlib" -->
<Window.Resources>
<sys:String x:Key="appTitle">Hello WPF</sys:String>
<x:Array x:Key="languages" Type="sys:String">
<sys:String>C#</sys:String>
<sys:String>Java</sys:String>
<sys:String>Python</sys:String>
</x:Array>
</Window.Resources>
<!-- 使用静态资源绑定 -->
<TextBlock Text="{StaticResource appTitle}" FontSize="20" />
<ListBox ItemsSource="{StaticResource languages}" />
<!-- 2. 元素绑定:Slider 控制 TextBlock 字号 -->
<StackPanel>
<Slider x:Name="fontSlider" Minimum="10" Maximum="40" Value="14" />
<TextBlock Text="实时预览"
FontSize="{Binding ElementName=fontSlider, Path=Value}" />
</StackPanel>
<!-- 3. ViewModel 绑定 -->
<!-- 引入命名空间:xmlns:vm="clr-namespace:MyApp.ViewModel" -->
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBlock Text="{Binding UserName}" />
<TextBox Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ListBox ItemsSource="{Binding TaskList}" />
</StackPanel>
// ViewModel 基础版(属性变更后 UI 不会自动更新)
public class MainViewModel
{
public int Id { get; set; } = 1;
public string UserName { get; set; } = "张三";
public List<string> TaskList { get; set; } = new List<string>
{
"完成登录模块",
"编写单元测试",
"部署上线"
};
}
cs
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
// ViewModel 完整版(实现 INotifyPropertyChanged,属性变更后 UI 自动刷新)
public class MainViewModel : INotifyPropertyChanged
{
// 事件:通知 UI 某个属性已变更
public event PropertyChangedEventHandler PropertyChanged;
// 触发通知的辅助方法(CallerMemberName 自动获取调用属性名)
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _userName = "张三";
public string UserName
{
get => _userName;
set
{
if (_userName != value)
{
_userName = value;
OnPropertyChanged(); // 通知 UI 刷新
}
}
}
// ObservableCollection:增删项时自动通知 UI(List<T> 不具备此能力)
public ObservableCollection<string> TaskList { get; set; } = new ObservableCollection<string>
{
"完成登录模块",
"编写单元测试",
"部署上线"
};
}
// 后台代码直接操作控件(简单场景)
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// 方式1:逐个添加(适合少量固定项)
listBox1.Items.Add("选项A");
listBox1.Items.Add("选项B");
// 方式2:绑定集合(推荐,适合动态数据)
// 注意:Items.Add 和 ItemsSource 互斥,不可在同一控件上同时使用!
// 此处仅为分别演示,实际只能选其一
listBox2.ItemsSource = new List<string> { "C#", "Java", "Python" };
}
private void listBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (listBox1.SelectedItem != null)
MessageBox.Show(listBox1.SelectedItem.ToString());
}
List<T> vs ObservableCollection<T>:
| 对比项 | List<T> | ObservableCollection<T> |
|---|---|---|
| 增删通知 | ✘ 不通知 UI | ✔ 增删项时自动通知 UI 刷新 |
| 性能 | 略优(无通知开销) | 略低 |
| 适用场景 | 一次性加载不变的数据 | 运行时动态增删项的列表 |
| 命名空间 | System.Collections.Generic |
System.Collections.ObjectModel |
重要:
ObservableCollection只通知集合的增/删/清空操作。若需通知集合中某个对象的属性变化 ,该对象自身仍需实现INotifyPropertyChanged。
三、问题排查
错误1:命名空间未找到
-
现象 :
The name "XXX" does not exist in the namespace "clr-namespace:..." -
原因:引入自定义命名空间后项目未重新编译,编译器无法识别新增类型
-
解决 :右键项目 → 重新生成(Rebuild),编译通过后智能提示恢复
错误2:数据绑定无效果
-
现象:界面未显示 ViewModel 中的属性值,TextBlock 显示为空
-
原因 :① 未设置
DataContext;②{Binding}中属性名拼写错误;③ 属性不是 public -
解决:
-
确认 XAML 中
<Window.DataContext>已正确设置 -
检查 Binding 路径与 ViewModel 属性名大小写一致
-
ViewModel 属性必须是
public且有get访问器
-
错误3:布局中控件"消失"
-
现象:控件添加了但界面看不到
-
原因:① Width/Height 为 0;② Visibility 为 Collapsed;③ Grid 中未指定 Row/Column 导致重叠
-
解决 :检查尺寸属性;在 Grid 中明确设置
Grid.Row和Grid.Column;使用Panel.ZIndex处理层叠
错误4:NavigationWindow 页面跳转报错
-
现象 :
NavigationService is null -
原因 :在 Window 中(而非 Page 中)调用了
this.NavigationService -
解决 :
NavigationService仅在 Page 类中可用;若在 Window 中需使用NavigationWindow.Navigate()方法