WPF 初识
大约 10 分钟约 3063 字
WPF 初识
简介
WPF(Windows Presentation Foundation)是微软推出的桌面应用开发框架,基于 .NET 平台,使用 XAML 标记语言定义界面,C# 编写逻辑代码。WPF 支持数据绑定、样式模板、动画、3D 渲染等特性,是构建现代 Windows 桌面应用的首选技术。
特点
WPF 架构
分层结构
┌─────────────────────────────────┐
│ Application (App.xaml) │
├─────────────────────────────────┤
│ PresentationFramework │ ← WPF 核心框架
│ (Window, Controls, Binding) │
├─────────────────────────────────┤
│ PresentationCore │ ← 核心类型 (UIElement, Visual)
├─────────────────────────────────┤
│ milcore (Media Integration) │ ← DirectX 交互层
├─────────────────────────────────┤
│ DirectX / User32 │ ← 系统渲染层
└─────────────────────────────────┘.NET 版本选择
| 版本 | 运行环境 | VS 支持 |
|---|---|---|
| .NET 8 WPF | .NET 8+ | Visual Studio 2022 17.8+ |
| .NET Framework 4.8 | .NET Framework 4.8 | VS 2019/2022 |
XAML 基础
命名空间
<!-- 默认命名空间:WPF 控件 -->
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<!-- XAML 语言命名空间 -->
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<!-- 常用 x: 前缀指令 -->
<!-- x:Key — 资源字典中的唯一键 -->
<!-- x:Class — 代码隐藏类(命名空间.类名) -->
<!-- x:Name — 运行时对象名称,用于代码引用 -->
<!-- x:Static — 引用静态值 -->
<!-- x:Type — 类型引用 -->第一个窗口
<!-- MainWindow.xaml -->
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="我的第一个 WPF 应用" Height="350" Width="500">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="WPF 欢迎界面"
FontSize="24" FontWeight="Bold" Margin="0,0,0,20"/>
<TextBox Grid.Row="1" x:Name="InputBox"
VerticalAlignment="Top" Height="30" FontSize="14"
Text="请输入你的名字"/>
<StackPanel Grid.Row="2" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,20,0,0">
<Button Content="确定" Width="80" Height="30"
Margin="0,0,10,0" Click="OnConfirm"/>
<Button Content="取消" Width="80" Height="30"
Click="OnCancel"/>
</StackPanel>
</Grid>
</Window>// MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnConfirm(object sender, RoutedEventArgs e)
{
MessageBox.Show($"你好,{InputBox.Text}!");
}
private void OnCancel(object sender, RoutedEventArgs e)
{
InputBox.Text = "";
}
}布局面板
常用布局容器
| 面板 | 特点 | 适用场景 |
|---|---|---|
| Grid | 行列网格,最灵活 | 大多数界面的主布局 |
| StackPanel | 水平或垂直堆叠 | 按钮组、表单项 |
| WrapPanel | 自动换行排列 | 标签云、工具栏 |
| DockPanel | 停靠式布局 | 经典菜单+内容布局 |
| Canvas | 绝对坐标定位 | 绘图、图表 |
Grid 布局示例
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- 标题栏 -->
<RowDefinition Height="*"/> <!-- 内容区 -->
<RowDefinition Height="Auto"/> <!-- 状态栏 -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/> <!-- 侧边栏 -->
<ColumnDefinition Width="*"/> <!-- 主内容 -->
</Grid.ColumnDefinitions>
<!-- 标题栏:跨两列 -->
<Border Grid.Row="0" Grid.ColumnSpan="2" Background="#2D2D2D">
<TextBlock Text="应用标题" Foreground="White"
FontSize="16" Margin="15,0"/>
</Border>
<!-- 侧边栏 -->
<ListBox Grid.Row="1" Grid.Column="0" Margin="5">
<ListBoxItem Content="首页"/>
<ListBoxItem Content="设置"/>
<ListBoxItem Content="关于"/>
</ListBox>
<!-- 主内容 -->
<ContentControl Grid.Row="1" Grid.Column="1" Margin="10"/>
<!-- 状态栏:跨两列 -->
<StatusBar Grid.Row="2" Grid.ColumnSpan="2">
<StatusBarItem Content="就绪"/>
</StatusBar>
</Grid>常用控件
输入控件
<!-- 文本输入 -->
<TextBox Text="{Binding Username}" Width="200" Height="25"/>
<PasswordBox PasswordChanged="OnPasswordChanged" Width="200" Height="25"/>
<!-- 选择控件 -->
<CheckBox Content="记住密码" IsChecked="{Binding RememberMe}"/>
<RadioButton Content="选项A" GroupName="Options" IsChecked="True"/>
<RadioButton Content="选项B" GroupName="Options"/>
<!-- 下拉选择 -->
<ComboBox ItemsSource="{Binding Categories}" SelectedItem="{Binding Selected}"
Width="200" Height="25"/>
<!-- 滑块 -->
<Slider Minimum="0" Maximum="100" Value="{Binding Progress}" Width="200"/>
<!-- 日期选择 -->
<DatePicker SelectedDate="{Binding BirthDate}" Width="200"/>数据展示
<!-- 列表 -->
<ListBox ItemsSource="{Binding Users}" DisplayMemberPath="Name"
SelectedItem="{Binding SelectedUser}" Height="200"/>
<!-- 数据表格 -->
<DataGrid ItemsSource="{Binding Products}" AutoGenerateColumns="False"
CanUserAddRows="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="价格" Binding="{Binding Price}" Width="100"/>
<DataGridTextColumn Header="库存" Binding="{Binding Stock}" Width="80"/>
</DataGrid.Columns>
</DataGrid>
<!-- 树形控件 -->
<TreeView ItemsSource="{Binding Departments}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>事件处理
路由事件
<!-- 路由事件分为三种策略:Bubble(冒泡)、Tunnel(隧道)、Direct(直达) -->
<StackPanel Button.Click="StackPanel_Click">
<!-- 点击按钮时,事件会冒泡到 StackPanel -->
<Button Content="冒泡事件演示" Click="Button_Click"/>
<!-- 隧道事件以 Preview 开头,从根向目标传递 -->
<TextBox PreviewKeyDown="TextBox_PreviewKeyDown"
KeyDown="TextBox_KeyDown"/>
</StackPanel>// 冒泡事件处理 — 子控件事件冒泡到父容器
private void StackPanel_Click(object sender, RoutedEventArgs e)
{
// 判断事件来源
if (e.Source is Button btn)
{
MessageBox.Show($"点击了:{btn.Content}");
}
// 标记事件已处理,阻止继续冒泡
// e.Handled = true;
}
// 隧道事件先于冒泡事件触发
private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
// 可以在隧道阶段拦截输入
if (e.Key == Key.Space)
{
e.Handled = true; // 阻止空格输入
}
}命令绑定
<!-- WPF 命令系统:ICommand 接口 -->
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.New"
Executed="NewCommand_Executed"
CanExecute="NewCommand_CanExecute"/>
<CommandBinding Command="ApplicationCommands.Save"
Executed="SaveCommand_Executed"
CanExecute="SaveCommand_CanExecute"/>
</Window.CommandBindings>
<StackPanel>
<!-- 菜单项自动关联命令 -->
<Menu>
<MenuItem Header="文件">
<MenuItem Header="新建" Command="ApplicationCommands.New"/>
<MenuItem Header="保存" Command="ApplicationCommands.Save"/>
</MenuItem>
</Menu>
<!-- 快捷键自动绑定 -->
<Button Content="新建 (Ctrl+N)" Command="ApplicationCommands.New"/>
</StackPanel>// 命令执行逻辑
private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("执行新建操作");
}
private void NewCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// 控制命令是否可用(按钮灰显)
e.CanExecute = true;
}
private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("执行保存操作");
}
private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = !string.IsNullOrEmpty(InputBox.Text);
}数据绑定基础
绑定模式
<StackPanel>
<!-- OneWay:源 → 目标(默认,大多数场景) -->
<TextBlock Text="{Binding Username}" FontSize="16"/>
<!-- TwoWay:双向绑定(输入控件常用) -->
<TextBox Text="{Binding Username, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<!-- OneWayToSource:目标 → 源(只向源推送) -->
<Slider Value="{Binding Volume, Mode=OneWayToSource}"/>
<!-- OneTime:只绑定一次,不监听变化 -->
<TextBlock Text="{Binding CreateTime, Mode=OneTime}"/>
</StackPanel>绑定更新触发时机
<!-- UpdateSourceTrigger 控制绑定何时写回源 -->
<StackPanel>
<!-- PropertyChanged:每次输入立即更新(推荐用于实时搜索) -->
<TextBox Text="{Binding SearchKeyword, UpdateSourceTrigger=PropertyChanged}"/>
<!-- LostFocus:失去焦点时更新(默认,适合表单输入) -->
<TextBox Text="{Binding Email, UpdateSourceTrigger=LostFocus}"/>
<!-- Explicit:手动调用 UpdateSource -->
<TextBox x:Name="PhoneBox"
Text="{Binding Phone, UpdateSourceTrigger=Explicit}"/>
<Button Content="确认" Click="(_,__) => PhoneBox.GetBindingExpression(TextBox.TextProperty)?.UpdateSource()"/>
</StackPanel>值转换器
/// <summary>
/// 布尔值转可见性转换器
/// </summary>
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
// 支持反转:parameter="Invert"
bool invert = parameter?.ToString() == "Invert";
bool result = invert ? !boolValue : boolValue;
return result ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
return visibility == Visibility.Visible;
return false;
}
}
/// <summary>
/// 数值转进度条颜色
/// </summary>
public class ValueToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double num)
{
if (num < 30) return new SolidColorBrush(Colors.Green);
if (num < 70) return new SolidColorBrush(Colors.Orange);
return new SolidColorBrush(Colors.Red);
}
return Brushes.Gray;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}<!-- 在资源中声明转换器 -->
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVis"/>
<local:ValueToColorConverter x:Key="ValueToColor"/>
</Window.Resources>
<!-- 使用转换器 -->
<TextBlock Text="加载中..."
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVis}}"/>
<ProgressBar Value="{Binding CpuUsage}"
Foreground="{Binding CpuUsage, Converter={StaticResource ValueToColor}}"/>资源与样式
资源字典
<!-- App.xaml 中定义全局资源 -->
<Application.Resources>
<!-- 颜色 -->
<SolidColorBrush x:Key="PrimaryBrush" Color="#0078D4"/>
<SolidColorBrush x:Key="DangerBrush" Color="#E74C3C"/>
<!-- 样式 -->
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="15,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</Application.Resources>
<!-- 使用样式 -->
<Button Content="确定" Style="{StaticResource PrimaryButton}"/>应用程序生命周期
App.xaml 配置
<!-- App.xaml — 应用程序入口 -->
<Application x:Class="WpfApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Views/MainWindow.xaml"
Startup="App_OnStartup"
Exit="App_OnExit"
DispatcherUnhandledException="App_OnDispatcherUnhandledException">
<Application.Resources>
<!-- 全局资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/Colors.xaml"/>
<ResourceDictionary Source="Styles/ControlStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>// App.xaml.cs — 应用程序生命周期管理
public partial class App : Application
{
// 启动事件
private void App_OnStartup(object sender, StartupEventArgs e)
{
// 处理命令行参数
if (e.Args.Contains("--debug"))
{
// 开启调试模式
}
// 初始化服务容器
var services = new ServiceCollection();
services.AddSingleton<IMainViewModel, MainViewModel>();
services.AddSingleton<IDialogService, DialogService>();
// ... 注册其他服务
// 配置日志
// ...
}
// 未处理异常捕获
private void App_OnDispatcherUnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs e)
{
// 记录异常日志
_logger.LogError(e.Exception, "未处理的异常");
// 显示友好提示
MessageBox.Show(
$"程序发生未知错误:{e.Exception.Message}\n\n请联系技术支持。",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
// 标记为已处理,防止程序崩溃
e.Handled = true;
}
// 退出事件
private void App_OnExit(object sender, ExitEventArgs e)
{
// 释放资源
// 保存配置
}
}单实例启动
/// <summary>
/// 防止 WPF 程序多开 — 使用 Mutex
/// </summary>
public partial class App : Application
{
private static Mutex? _mutex;
private const string MutexName = "MyApp_SingleInstance_Mutex";
protected override void OnStartup(StartupEventArgs e)
{
bool isNew;
_mutex = new Mutex(true, MutexName, out isNew);
if (!isNew)
{
// 已有实例运行,激活已有窗口后退出
ActivateExistingWindow();
Shutdown();
return;
}
base.OnStartup(e);
}
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private void ActivateExistingWindow()
{
var current = Process.GetCurrentProcess();
var running = Process.GetProcessesByName(current.ProcessName)
.FirstOrDefault(p => p.Id != current.Id);
if (running != null)
{
SetForegroundWindow(running.MainWindowHandle);
}
}
}优点
缺点
总结
WPF 是 .NET 桌面开发的首选框架,核心优势在于 XAML 驱动的界面设计、强大的数据绑定和高度可定制的样式模板。入门需要掌握布局面板、常用控件、资源样式三大基础,进阶再学习 MVVM 模式和自定义控件。配合 HandyControl 等开源库可以快速构建现代化桌面应用。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《WPF 初识》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《WPF 初识》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《WPF 初识》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《WPF 初识》最大的收益和代价分别是什么?
