文章目录
- [一 、Template](#一 、Template)
-
- [1.1 重点掌握](#1.1 重点掌握)
- [1.2 my example](#1.2 my example)
- [二、 Resource](#二、 Resource)
-
- [2.1 重点掌握](#2.1 重点掌握)
- [2.2 三种引用方式](#2.2 三种引用方式)
- 三、关键帧
- [四、 路由命令](#四、 路由命令)
-
- [4.1 命令介绍](#4.1 命令介绍)
- [4.2 使用详例](#4.2 使用详例)
-
- 示例场景:文本编辑器中的复制粘贴功能
- [1. **XAML布局(MainWindow.xaml)**](#1. XAML布局(MainWindow.xaml))
- [2. **代码隐藏(MainWindow.xaml.cs)**](#2. 代码隐藏(MainWindow.xaml.cs))
- [3. **完整执行流程解析**](#3. 完整执行流程解析)
- 五、依赖属性
-
-
- [5.1 **传统属性 vs 依赖属性**](#5.1 传统属性 vs 依赖属性)
-
- [5.1.1. **传统.NET属性**](#5.1.1. 传统.NET属性)
- [5.1.2. **依赖属性**](#5.1.2. 依赖属性)
- [5.2 **依赖属性的核心特性**](#5.2 依赖属性的核心特性)
- [5.3 **自定义依赖属性的步骤**](#5.3 自定义依赖属性的步骤)
-
- 六、用户控件
-
- [6.1 介绍](#6.1 介绍)
-
- [**1. 用户控件 vs 自定义控件**](#1. 用户控件 vs 自定义控件)
- [**2. 创建用户控件的步骤**](#2. 创建用户控件的步骤)
- [**3. 在窗口中使用用户控件**](#3. 在窗口中使用用户控件)
- [**4. 为用户控件添加依赖属性**](#4. 为用户控件添加依赖属性)
- [**5. 事件封装**](#5. 事件封装)
- [**6. 数据绑定与MVVM支持**](#6. 数据绑定与MVVM支持)
- [**7. 样式与模板**](#7. 样式与模板)
- [**8. 用户控件的最佳实践**](#8. 用户控件的最佳实践)
- [**9. 常见应用场景**](#9. 常见应用场景)
- [**10. 用户控件的局限**](#10. 用户控件的局限)
- **总结**
- [6.2 完整示例](#6.2 完整示例)
-
- [**1. 创建用户控件(MyUserControl.xaml)**](#1. 创建用户控件(MyUserControl.xaml))
- [**2. 用户控件代码隐藏(MyUserControl.xaml.cs)**](#2. 用户控件代码隐藏(MyUserControl.xaml.cs))
- [**3. 用户控件的ViewModel(MyUserControlViewModel.cs)**](#3. 用户控件的ViewModel(MyUserControlViewModel.cs))
- [**4. 主窗口(MainWindow.xaml)**](#4. 主窗口(MainWindow.xaml))
- [**5. 主窗口ViewModel(MainViewModel.cs)**](#5. 主窗口ViewModel(MainViewModel.cs))
- [**6. 主窗口代码隐藏(MainWindow.xaml.cs)**](#6. 主窗口代码隐藏(MainWindow.xaml.cs))
- [**7. 应用程序入口(App.xaml.cs)**](#7. 应用程序入口(App.xaml.cs))
- **功能说明**
- **运行步骤**
一 、Template
1.1 重点掌握
在WPF(Windows Presentation Foundation)中,ControlTemplate
是定义控件外观和行为的核心机制。一个模板可以包含以下类型的元素和功能:
一、可视化元素
模板的核心是定义控件的视觉结构
,可包含:
-
布局容器
-
Grid
、StackPanel
、Canvas
、DockPanel
等,用于组织子元素的排列方式。 -
示例:
xml<ControlTemplate TargetType="Button"> <Grid> <!-- 按钮内容将在此处显示 --> </Grid> </ControlTemplate>
-
-
图形元素
-
Rectangle
、Ellipse
、Path
、Line
等形状,用于创建自定义外观。 -
示例:
xml<Rectangle x:Name="Background" Fill="Blue" />
-
-
内容展示
-
ContentPresenter
:显示控件的Content
属性(如按钮的文本)。 -
ItemsPresenter
:用于ItemsControl
(如ListBox
)显示子项集合。 -
示例:
xml<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
-
-
装饰器
-
Border
、DropShadowEffect
、BlurEffect
等,用于添加边框、阴影或视觉效果。 -
示例:
xml<Border BorderBrush="Black" BorderThickness="1" CornerRadius="4"> <!-- 内部元素 --> </Border>
-
二、触发器(Triggers)
用于根据控件状态改变外观,包括:
-
属性触发器(PropertyTrigger)
-
基于控件属性值触发(如
IsMouseOver
、IsPressed
)。 -
示例:
xml<Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="Background" Property="Fill" Value="LightBlue" /> </Trigger>
-
-
事件触发器(EventTrigger)
-
基于事件触发动画(如
MouseEnter
、Loaded
)。 -
示例:
xml<EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard> <!-- 动画定义 --> </Storyboard> </BeginStoryboard> </EventTrigger>
-
-
数据触发器(DataTrigger)
- 基于绑定数据触发(需配合
DataContext
使用)。
- 基于绑定数据触发(需配合
三、动画(Animations)
通过 Storyboard
定义平滑过渡效果,可应用于:
-
属性动画 :如改变颜色、大小、位置等。
示例:
xml<DoubleAnimation Storyboard.TargetName="Background" Storyboard.TargetProperty="Width" From="0" To="100" Duration="0:0:0.5" />
-
关键帧动画:定义多个中间状态,实现更复杂的动画路径。
四、命名元素引用(x:Name)
为元素分配名称,以便在模板内部或触发器中引用:
xml
<Rectangle x:Name="Highlight" Fill="Red" Visibility="Hidden" />
在触发器中使用:
xml
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Highlight" Property="Visibility" Value="Visible" />
</Trigger>
五、模板绑定(TemplateBinding)
将模板内元素的属性绑定到控件的属性:
xml
<Border BorderThickness="{TemplateBinding BorderThickness}" />
常见绑定属性:Width
、Height
、Foreground
、Background
等。
六、样式与资源
-
内联样式:在模板内定义局部样式。
xml<Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14" /> </Style>
-
资源引用 :使用
StaticResource
或DynamicResource
引用外部资源(如颜色、画笔)。xml<SolidColorBrush x:Key="ButtonHoverBrush" Color="LightBlue" /> <!-- 在模板中使用 --> <Setter Property="Background" Value="{StaticResource ButtonHoverBrush}" />
七、逻辑交互元素
-
子控件 :在模板中嵌入其他控件(如
TextBox
、CheckBox
)。xml<CheckBox x:Name="ToggleButton" Content="选项" />
-
交互行为 :通过
Behavior
(需引用System.Windows.Interactivity
)添加自定义交互逻辑。
八、模板参数(TemplatePart)
使用 TemplatePart
特性声明模板需要的特定元素,供控件逻辑使用:
xml
[TemplatePart(Name = "PART_ContentHost", Type = typeof(FrameworkElement))]
public class CustomControl : Control
{
// 控件逻辑
}
完整示例:自定义按钮模板
xml
<ControlTemplate TargetType="Button">
<Grid>
<!-- 背景 -->
<Rectangle x:Name="Background"
Fill="{TemplateBinding Background}"
RadiusX="4" RadiusY="4" />
<!-- 内容显示 -->
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
<!-- 鼠标悬停时的高亮效果 -->
<Rectangle x:Name="Highlight"
Fill="LightBlue"
RadiusX="4" RadiusY="4"
Opacity="0" />
</Grid>
<ControlTemplate.Triggers>
<!-- 鼠标悬停时显示高亮 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Highlight" Property="Opacity" Value="0.5" />
</Trigger>
<!-- 点击时改变背景色 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Background" Property="Fill" Value="DarkBlue" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
总结
ControlTemplate
可以包含几乎所有WPF元素,核心功能是:
- 定义控件的视觉结构(布局、形状、内容)
- 通过触发器实现状态驱动的外观变化
- 使用动画增强交互体验
- 通过模板绑定保持与控件属性的同步
合理使用模板,可以完全自定义任何控件的外观和行为,同时保留其原始功能。
1.2 my example

xml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Ellipse x:Name="myEllipse">
<Ellipse.Fill>
<SolidColorBrush x:Name="ellipseBrush" Color="Red" />
</Ellipse.Fill>
</Ellipse>
<Rectangle Grid.Column="1" x:Name="rectangle" Width="10">
<Rectangle.Fill>
<SolidColorBrush Color="Red" />
</Rectangle.Fill>
</Rectangle>
<Button x:Name="myButton" Grid.Row="1" Width="250" Height="50" Content="动画触发启动">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border BorderBrush="Fuchsia" BorderThickness="2">
<Grid>
<!-- 定义动画目标Rectangle -->
<Rectangle x:Name="rectangle"
Fill="LightBlue"
Width="10"
Height="{TemplateBinding Height}"
HorizontalAlignment="Left"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<!-- 鼠标悬停时启动动画 -->
<Trigger Property="IsMouseOver" Value="true">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="rectangle"
Storyboard.TargetProperty="Width"
Duration="0:0:1"
From="10" To="250"
AutoReverse="True"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<!-- 鼠标离开时停止动画 -->
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="rectangle"
Storyboard.TargetProperty="Width"
Duration="0:0:0.3"
To="10"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
<Grid.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<!-- 第一个椭圆的动画 -->
<ColorAnimation
Storyboard.TargetName="ellipseBrush"
Storyboard.TargetProperty="Color"
Duration="00:00:10"
From="Red" To="Black"
AutoReverse="True"
RepeatBehavior="Forever"
SpeedRatio="10"/>
<DoubleAnimation
Storyboard.TargetName="rectangle"
Storyboard.TargetProperty="Width"
Duration="00:00:10"
From="10" To="200"
AutoReverse="True"
RepeatBehavior="Forever"
SpeedRatio="5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
</Window>
二、 Resource
在WPF中,Resources
是一种强大的机制,用于集中管理和复用应用程序中的各种资源(如样式、画笔、动画等)。以下是关于 Resources
的详细使用指南:
2.1 重点掌握
一、资源的定义与分类
资源可以定义在多个位置,按作用域分为:
-
应用程序级别 :在
App.xaml
中定义,全局可用xml<Application.Resources> <SolidColorBrush x:Key="PrimaryColor" Color="#3498DB" /> </Application.Resources>
-
窗口/控件级别 :在窗口或控件的
Resources
中定义,仅在当前范围内可用xml<Window.Resources> <Style x:Key="ButtonStyle" TargetType="Button"> <Setter Property="Background" Value="{StaticResource PrimaryColor}" /> </Style> </Window.Resources>
-
局部资源:在元素内部定义,仅作用于该元素及其子元素
xml<Grid.Resources> <DataTemplate x:Key="PersonTemplate"> <TextBlock Text="{Binding Name}" /> </DataTemplate> </Grid.Resources>
二、常见资源类型
-
样式(Style)
统一设置控件的外观属性:
xml<Style x:Key="TextBoxStyle" TargetType="TextBox"> <Setter Property="Foreground" Value="DarkBlue" /> <Setter Property="BorderBrush" Value="Gray" /> </Style>
-
画笔(Brush)
定义颜色、渐变等填充效果:
xml<SolidColorBrush x:Key="AccentBrush" Color="Orange" /> <LinearGradientBrush x:Key="GradientBrush" StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="White" Offset="0" /> <GradientStop Color="LightGray" Offset="1" /> </LinearGradientBrush>
-
模板(Template)
自定义控件的视觉结构:
xml<ControlTemplate x:Key="CustomButtonTemplate" TargetType="Button"> <Border Background="{TemplateBinding Background}" CornerRadius="5"> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" /> </Border> </ControlTemplate>
-
数据模板(DataTemplate)
定义数据的显示方式:
xml<DataTemplate x:Key="EmployeeTemplate"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Margin="5" /> <TextBlock Text="{Binding Age}" Margin="5" /> </StackPanel> </DataTemplate>
-
动画(Storyboard)
预定义动画效果:
xml<Storyboard x:Key="FadeInAnimation"> <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.5" /> </Storyboard>
三、资源的引用方式
使用 StaticResource
或 DynamicResource
标记扩展引用资源:
-
静态资源(StaticResource)
在编译时解析,性能较高,适合不会动态变化的资源:
xml<Button Style="{StaticResource ButtonStyle}" /> <Rectangle Fill="{StaticResource GradientBrush}" />
-
动态资源(DynamicResource)
在运行时动态解析,支持资源值的实时更新:
xml<TextBlock Foreground="{DynamicResource AccentColor}" />
四、资源字典(ResourceDictionary)
将资源单独提取到 .xaml
文件中,实现跨项目共享:
-
创建资源字典文件 (如
Styles.xaml
):xml<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Style x:Key="TitleText" TargetType="TextBlock"> <Setter Property="FontSize" Value="24" /> <Setter Property="FontWeight" Value="Bold" /> </Style> </ResourceDictionary>
-
在应用程序中引用:
xml<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Styles.xaml" /> <ResourceDictionary Source="Themes/BlueTheme.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>
五、隐式资源(无Key的资源)
通过省略 x:Key
并指定 TargetType
,创建可自动应用的资源:
xml
<Style TargetType="Button">
<Setter Property="Background" Value="LightGray" />
<Setter Property="Foreground" Value="Black" />
</Style>
所有未显式设置样式的 Button
都会自动应用此样式。
六、资源查找顺序
当使用 StaticResource
或 DynamicResource
引用资源时,WPF会按以下顺序查找:
- 当前元素的
Resources
集合 - 父元素的
Resources
集合(向上递归查找) - 窗口/页面的
Resources
集合 - 应用程序级别的
Resources
集合(App.xaml
) - 所有合并的资源字典
- 系统资源(如默认字体、颜色等)
七、示例:完整应用
App.xaml(定义全局资源):
xml
<Application.Resources>
<SolidColorBrush x:Key="PrimaryColor" Color="#2C3E50" />
<SolidColorBrush x:Key="AccentColor" Color="#E74C3C" />
<Style x:Key="MainWindowStyle" TargetType="Window">
<Setter Property="Background" Value="{StaticResource PrimaryColor}" />
</Style>
</Application.Resources>
MainWindow.xaml(使用资源):
xml
<Window x:Class="MyApp.MainWindow"
Style="{StaticResource MainWindowStyle}">
<Window.Resources>
<Style x:Key="TitleText" TargetType="TextBlock">
<Setter Property="FontSize" Value="24" />
<Setter Property="Foreground" Value="{StaticResource AccentColor}" />
</Style>
</Window.Resources>
<Grid>
<TextBlock Style="{StaticResource TitleText}" Text="欢迎使用我的应用" />
<Button Content="点击" Background="{StaticResource AccentColor}" />
</Grid>
</Window>
八、注意事项
- 资源键必须唯一 :同一范围内不能有重复的
x:Key
- 静态资源 vs 动态资源 :
- 静态资源在初始化时解析,修改资源不会影响已应用的元素
- 动态资源会实时更新,但性能开销略高
- 避免循环引用:资源间的引用不能形成闭环
- 资源优先级:局部资源会覆盖全局资源
合理使用 Resources
可以大幅提高代码的可维护性和复用性,使UI设计更加统一和灵活。
2.2 三种引用方式
要使用在 Resources
中定义的 mybutton
样式,你需要通过 Style
属性将其应用到 Button 控件上。以下是具体的实现方法:
方法1:显式引用命名样式
如果在资源中定义了带 x:Key
的样式(如 mybutton
),需要通过 Style="{StaticResource mybutton}"
显式应用:
xml
<Window.Resources>
<!-- 定义名为"mybutton"的样式 -->
<Style x:Key="mybutton" TargetType="Button">
<Setter Property="Background" Value="LightBlue" />
<Setter Property="Foreground" Value="DarkBlue" />
<Setter Property="FontSize" Value="16" />
</Style>
</Window.Resources>
<Grid>
<!-- 应用自定义样式 -->
<Button Style="{StaticResource mybutton}" Content="自定义按钮" />
</Grid>
方法2:隐式应用样式(无 x:Key
)
如果希望所有 Button 自动应用该样式,可以省略 x:Key
只保留 TargetType
:
xml
<Window.Resources>
<!-- 所有Button将自动应用此样式 -->
<Style TargetType="Button">
<Setter Property="Background" Value="LightBlue" />
<Setter Property="Foreground" Value="DarkBlue" />
<Setter Property="FontSize" Value="16" />
</Style>
</Window.Resources>
<Grid>
<!-- 无需指定Style属性,自动应用上面的样式 -->
<Button Content="自动应用样式的按钮" />
</Grid>
方法3:基于现有样式扩展
如果想在默认 Button 样式基础上扩展,可以使用 BasedOn
属性:
xml
<Window.Resources>
<Style x:Key="mybutton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="LightBlue" />
<Setter Property="Foreground" Value="DarkBlue" />
</Style>
</Window.Resources>
<Grid>
<Button Style="{StaticResource mybutton}" Content="扩展默认样式的按钮" />
</Grid>
关键点说明
-
x:Key
的作用:- 有
x:Key
(如x:Key="mybutton"
):需显式引用 - 无
x:Key
:隐式应用到所有TargetType
类型的控件
- 有
-
资源作用域:
- 在
<Window.Resources>
中定义:仅当前窗口可用 - 在
<Application.Resources>
中定义:全局所有窗口可用
- 在
-
引用语法:
StaticResource
:编译时解析(推荐)DynamicResource
:运行时动态解析(适用于需要实时更新的场景)
完整示例
以下是一个完整的窗口示例,包含自定义 Button 样式的定义和使用:
xml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="自定义按钮样式" Height="300" Width="400">
<Window.Resources>
<!-- 自定义按钮样式 -->
<Style x:Key="mybutton" TargetType="Button">
<Setter Property="Background" Value="#3498DB" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="4" />
<!-- 鼠标悬停效果 -->
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2980B9" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<!-- 使用自定义样式的按钮 -->
<Button Style="{StaticResource mybutton}" Content="点击我" HorizontalAlignment="Center" VerticalAlignment="Center" />
<!-- 普通按钮(未应用样式) -->
<Button Content="普通按钮" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,20" />
</Grid>
</Window>
常见问题
-
样式不生效:
- 检查
x:Key
拼写是否正确 - 确保
TargetType
与控件类型匹配(如TargetType="Button"
)
- 检查
-
全局应用样式 :
将样式定义移至
App.xaml
的<Application.Resources>
中:xml<Application.Resources> <Style x:Key="mybutton" TargetType="Button"> <!-- 样式内容 --> </Style> </Application.Resources>
通过上述方法,你可以灵活地复用自定义的 Button 样式,提高代码的可维护性和一致性。
三、关键帧
在WPF中,含有关键帧的时间线(KeyFrame Animation) 和不含关键帧的时间线(基本动画) 主要区别在于动画的复杂度和控制精度。以下是具体对比:
特性 | 基本动画 | 关键帧动画 |
---|---|---|
变化路径 | 线性(直线) | 支持非线性(曲线、突变) |
控制点数量 | 2个(起始值和结束值) | ≥2个(可定义任意数量关键帧) |
时间控制 | 整体持续时间(Duration) | 每个关键帧可单独指定时间 |
插值方式 | 固定线性插值 | 支持线性、曲线、离散等多种方式 |
复杂度 | 简单 | 高(适合复杂动画) |
XAML代码量 | 少 | 多(需要定义多个关键帧) |
一、基本动画(不含关键帧)
特点
-
简单线性变化
从起始值(
From
)到结束值(To
)或相对变化值(By
)进行线性插值。
示例:xml<DoubleAnimation From="0" To="100" Duration="0:0:1" Storyboard.TargetProperty="Width" />
-
单一变化路径
只支持直线过渡,无法定义复杂的变化曲线。
-
适用场景
- 简单的淡入淡出、大小缩放、位置移动等线性变化。
二、关键帧动画(含有关键帧)
特点
-
多阶段复杂变化
通过定义多个关键帧(KeyFrame) 指定不同时间点的属性值,支持非线性变化。
示例:xml<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Width" Duration="0:0:2"> <LinearDoubleKeyFrame Value="0" KeyTime="0:0:0" /> <!-- 0秒时宽度为0 --> <LinearDoubleKeyFrame Value="100" KeyTime="0:0:1" /> <!-- 1秒时宽度为100 --> <SplineDoubleKeyFrame Value="150" KeyTime="0:0:1.5" <!-- 1.5秒时宽度为150(使用曲线插值) --> KeySpline="0.5,0 0.7,1" /> <!-- 定义贝塞尔曲线,控制加速/减速 --> <DiscreteDoubleKeyFrame Value="200" KeyTime="0:0:2" /> <!-- 2秒时立即跳转到200 --> </DoubleAnimationUsingKeyFrames>
-
多种插值方式
- LinearKeyFrame:线性插值(匀速变化)
- SplineKeyFrame:使用贝塞尔曲线插值(可自定义加速/减速)
- DiscreteKeyFrame:离散变化(瞬间跳转)
-
精确时间控制
每个关键帧都可以指定精确的
KeyTime
,实现复杂的时间序列。 -
适用场景
- 不规则运动(如弹跳、弹性效果)
- 多阶段动画(如先加速后减速)
- 复杂过渡效果(如模拟物理运动)
三、核心区别对比表
特性 | 基本动画 | 关键帧动画 |
---|---|---|
变化路径 | 线性(直线) | 支持非线性(曲线、突变) |
控制点数量 | 2个(起始值和结束值) | ≥2个(可定义任意数量关键帧) |
时间控制 | 整体持续时间(Duration) | 每个关键帧可单独指定时间 |
插值方式 | 固定线性插值 | 支持线性、曲线、离散等多种方式 |
复杂度 | 简单 | 高(适合复杂动画) |
XAML代码量 | 少 | 多(需要定义多个关键帧) |
四、何时选择哪种方式?
-
选择基本动画 :
当动画是简单的线性变化,且不需要精细控制中间过程时(如淡入淡出、匀速移动)。
-
选择关键帧动画:
- 需要非线性变化(如物体弹跳、弹性效果)。
- 需要在特定时间点触发特定值(如动画的暂停、跳跃)。
- 需要组合多种变化方式(如先加速后减速)。
五、示例对比
1. 基本动画:匀速淡入
xml
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="0" To="1"
Duration="0:0:1" />
2. 关键帧动画:弹性淡入(先过度再回弹)
xml
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="Opacity"
Duration="0:0:1.5">
<LinearDoubleKeyFrame Value="0" KeyTime="0:0:0" />
<SplineDoubleKeyFrame Value="1.2" KeyTime="0:0:0.8"
KeySpline="0.5,0 0.7,1" /> <!-- 先超过目标值(1.2) -->
<SplineDoubleKeyFrame Value="1" KeyTime="0:0:1.5"
KeySpline="0.3,0 0.5,1" /> <!-- 回弹到最终值(1) -->
</DoubleAnimationUsingKeyFrames>
总结
关键帧动画是基本动画的超集,提供了更强大的控制能力,但也增加了代码复杂度。根据动画需求的简单或复杂程度,选择合适的方式可以在保证效果的同时优化代码结构。
四、 路由命令
4.1 命令介绍
在 WPF(Windows Presentation Foundation)里,ApplicationCommands
是一个静态类,它属于 System.Windows.Input
命名空间。这个类的作用是提供一系列应用程序层面的通用命令,像复制、粘贴、保存等操作都涵盖其中。借助这些预定义好的命令,开发者能更便捷地实现常见的用户交互功能,而无需手动编写大量代码。
核心功能与优势
- 标准化操作 :
ApplicationCommands
把一些常用的操作进行了标准化处理,使应用程序能够遵循统一的用户界面(UI)惯例。 - 自动状态管理:这些命令能够依据当前的上下文自动调整自身状态,比如判断"剪切"命令在没有选中内容时是否应该禁用。
- 多设备支持:它支持多种输入设备,包括键盘快捷键、菜单以及工具栏按钮等。
- 简化实现:命令可以直接和视图模型(ViewModel)或者代码隐藏文件(Code-Behind)中的处理逻辑绑定,减少了重复编码。
常用命令示例
ApplicationCommands
类中包含了许多静态属性,下面是一些比较常用的:
ApplicationCommands.New
:用于创建新文档。ApplicationCommands.Open
:用于打开已有文档。ApplicationCommands.Save
:用于保存当前文档。ApplicationCommands.SaveAs
:用于将当前文档另存为其他文件。ApplicationCommands.Cut
、ApplicationCommands.Copy
、ApplicationCommands.Paste
:对应剪切、复制和粘贴操作。ApplicationCommands.Undo
、ApplicationCommands.Redo
:对应撤销和重做操作。ApplicationCommands.Print
:用于打印文档。ApplicationCommands.Find
:用于查找功能。ApplicationCommands.Close
:用于关闭当前窗口或文档。
应用场景
- XAML 中的命令绑定
下面是一个使用ApplicationCommands.Save
的例子:
xml
<Button Content="保存" Command="ApplicationCommands.Save" />
如果想要为命令指定一个处理函数,可以使用 CommandBinding
:
xml
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Save"
Executed="SaveCommand_Executed"
CanExecute="SaveCommand_CanExecute" />
</Window.CommandBindings>
- 代码隐藏文件中的处理函数
在窗口(Window)的代码隐藏文件里实现处理函数:
csharp
private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
// 实现保存逻辑
MessageBox.Show("文档已保存!");
}
private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// 判断是否可以执行保存命令
e.CanExecute = IsDocumentModified; // 假设这是一个判断文档是否修改的属性
}
- MVVM 模式下的命令绑定
在 MVVM 模式中,可以借助RelayCommand
或者DelegateCommand
来绑定命令:
xml
<Button Content="保存"
Command="{Binding SaveCommand}"
CommandParameter="{Binding CurrentDocument}" />
对应的视图模型代码如下:
csharp
public ICommand SaveCommand { get; }
public MyViewModel()
{
SaveCommand = new RelayCommand(
ExecuteSaveCommand,
CanExecuteSaveCommand
);
}
private void ExecuteSaveCommand(object parameter)
{
// 保存文档的实现
}
private bool CanExecuteSaveCommand(object parameter)
{
return CurrentDocument != null && CurrentDocument.IsModified;
}
总结
ApplicationCommands
为 WPF 应用程序提供了一套标准化的命令集,它简化了常见操作的实现过程,同时保证了应用程序符合用户的使用习惯。通过将命令与处理逻辑绑定,能够高效地实现各种功能,并且代码结构更加清晰。无论是在传统的代码隐藏模式还是 MVVM 模式中,ApplicationCommands
都是实现用户交互功能的重要工具。
4.2 使用详例
下面通过一个完整的示例来详细说明WPF路由命令的执行流程。这个示例包含命令定义、绑定、状态检查和执行的全过程。
示例场景:文本编辑器中的复制粘贴功能
假设我们有一个简单的文本编辑器,包含一个文本框和复制/粘贴按钮。当文本框中有选中文本时,复制按钮可用;无论何时,粘贴按钮都可用(假设剪贴板可能有内容)。
1. XAML布局(MainWindow.xaml)
xml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="路由命令示例" Height="300" Width="400">
<Window.CommandBindings>
<!-- 定义命令绑定:将ApplicationCommands.Copy映射到自定义处理函数 -->
<CommandBinding Command="ApplicationCommands.Copy"
CanExecute="Copy_CanExecute"
Executed="Copy_Executed" />
<!-- 粘贴命令绑定 -->
<CommandBinding Command="ApplicationCommands.Paste"
CanExecute="Paste_CanExecute"
Executed="Paste_Executed" />
</Window.CommandBindings>
<Grid>
<StackPanel Margin="10">
<!-- 复制按钮:自动绑定到ApplicationCommands.Copy -->
<Button Content="复制" Command="ApplicationCommands.Copy"
CommandTarget="{Binding ElementName=myTextBox}" />
<!-- 粘贴按钮 -->
<Button Content="粘贴" Command="ApplicationCommands.Paste"
CommandTarget="{Binding ElementName=myTextBox}" />
<!-- 文本框:作为命令的目标元素 -->
<TextBox x:Name="myTextBox" Height="200" Margin="0,10,0,0"
VerticalScrollBarVisibility="Auto" />
</StackPanel>
</Grid>
</Window>
2. 代码隐藏(MainWindow.xaml.cs)
csharp
using System.Windows;
using System.Windows.Input;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
// 复制命令的CanExecute处理函数
private void Copy_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// 检查文本框是否有选中文本 此处应该添加判断 myTextBox!=0 //否则会报异常故障
if(myTextBox!=0)
e.CanExecute = myTextBox.SelectedText.Length > 0;
e.Handled = true; // 标记事件已处理,停止路由
}
// 复制命令的执行处理函数
private void Copy_Executed(object sender, ExecutedRoutedEventArgs e)
{
// 执行复制操作(实际上WPF会自动处理,这里仅作演示)
myTextBox.Copy();
MessageBox.Show("文本已复制到剪贴板");
e.Handled = true;
}
// 粘贴命令的CanExecute处理函数
private void Paste_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// 检查剪贴板是否有文本内容(简化处理,实际应使用Clipboard.ContainsText())
e.CanExecute = true; // 假设剪贴板始终有内容
e.Handled = true;
}
// 粘贴命令的执行处理函数
private void Paste_Executed(object sender, ExecutedRoutedEventArgs e)
{
// 执行粘贴操作
myTextBox.Paste();
e.Handled = true;
}
}
}
3. 完整执行流程解析
步骤1:用户界面初始化
- WPF创建窗口并加载XAML。
- 按钮自动根据
Command
属性关联到ApplicationCommands.Copy
和ApplicationCommands.Paste
。 CommandTarget
明确指定文本框myTextBox
为命令的目标元素。
步骤2:命令状态初始检查
- 当窗口加载完成,WPF会自动调用所有命令的
CanExecute
方法:Copy_CanExecute
检查文本框的SelectedText.Length
,初始时无选中内容,返回false
→ 复制按钮禁用。Paste_CanExecute
返回true
→ 粘贴按钮启用。
步骤3:用户操作触发命令
-
用户选中文本:
- 文本框的
SelectionChanged
事件触发。 - WPF自动重新检查
Copy
命令的状态(通过CommandManager
)。 Copy_CanExecute
重新执行,发现有选中内容,返回true
→ 复制按钮变为启用状态。
- 文本框的
-
用户点击复制按钮:
- 按钮作为命令源,发起
ApplicationCommands.Copy
命令请求。 - WPF根据
CommandTarget
找到文本框myTextBox
。 - 再次调用
Copy_CanExecute
确认可执行状态(确保状态未变化)。 - 调用
Copy_Executed
处理函数执行复制逻辑。
- 按钮作为命令源,发起
-
用户点击粘贴按钮:
- 类似流程,但
Paste_CanExecute
始终返回true
,直接执行Paste_Executed
。
- 类似流程,但
-
关键机制详解
命令路由过程
当按钮被点击时,WPF会:
- 从按钮开始,向上遍历视觉树,查找
CommandBinding
(这里在Window层级找到)。 - 如果未找到
CommandBinding
,命令会继续向上冒泡,直到找到处理程序或到达根元素。
状态自动更新
- 焦点变化:当用户切换到其他控件再回到文本框时,WPF会重新检查命令状态。
- 属性变化 :文本框的
SelectedText
属性变化时,WPF不会自动触发状态检查(这是示例的简化处理)。在实际应用中,你可能需要手动调用CommandManager.InvalidateRequerySuggested()
或使用MVVM模式中的CanExecuteChanged
事件。
命令绑定优先级
-
显式绑定 :通过
CommandBinding
定义的处理函数(如示例中的代码)。 -
元素内置处理 :如果目标元素(如TextBox)本身支持该命令(如
ApplicationCommands.Copy
),WPF会使用其内置实现。 -
默认行为:如果没有找到任何处理程序,命令将被忽略。
-
MVVM模式下的实现(简化示例)
在MVVM模式中,通常使用
RelayCommand
类替代CommandBinding
:
csharp
// RelayCommand.cs (通用实现)
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
// ViewModel.cs
public class ViewModel
{
public ICommand CopyCommand { get; }
public ICommand PasteCommand { get; }
private readonly TextBox _textBox;
public ViewModel(TextBox textBox)
{
_textBox = textBox;
CopyCommand = new RelayCommand(
param => _textBox.Copy(), // Execute逻辑
param => _textBox.SelectedText.Length > 0 // CanExecute逻辑
);
PasteCommand = new RelayCommand(param => _textBox.Paste());
}
}
总结
路由命令的执行是一个涉及命令源、命令目标、命令绑定和状态管理的复杂过程。通过合理设计CanExecute
逻辑和使用CommandBinding
或ICommand
接口,你可以实现:
- 自动控制UI元素的启用/禁用状态。
- 集中管理命令处理逻辑。
- 实现跨元素的交互行为。
这个示例展示了WPF路由命令的核心机制,实际应用中可以根据需求扩展更复杂的场景。
五、依赖属性
WPF的**依赖属性(Dependency Property)**是一种特殊的属性系统,它扩展了普通.NET属性的功能,为WPF控件提供了强大的动态值计算、数据绑定、样式设置、动画等能力。理解依赖属性是掌握WPF编程的关键。
5.1 传统属性 vs 依赖属性
5.1.1. 传统.NET属性
普通属性的本质是一对get/set方法,值存储在对象的私有字段中:
csharp
public class Person
{
private string _name;
public string Name
{
get => _name;
set => _name = value;
}
}
- 值存储:直接保存在对象实例中。
- 单一数据源:值只能由对象自己管理。
5.1.2. 依赖属性
依赖属性的值存储在WPF的依赖属性系统中,而不是对象的私有字段:
csharp
public class Person : DependencyObject
{
// 注册依赖属性
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(
"Name", // 属性名称
typeof(string), // 属性类型
typeof(Person), // 所有者类型
new PropertyMetadata("默认值") // 元数据
);
// 包装器属性(可选,但推荐)
public string Name
{
get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
}
- 值存储 :通过
DependencyObject
的哈希表间接存储,多个对象可共享同一属性定义。 - 多数据源:值可来自数据绑定、样式、动画、继承等多种来源,系统会自动处理优先级。
5.2 依赖属性的核心特性
- 动态值解析
依赖属性的值可以由多个来源动态决定,优先级从高到低: - 动画
- 显式设置的值(如
TextBox.Text = "Hello"
) - 数据绑定
- 样式触发器
- 模板触发器
- 样式设置
- 继承值(如
FontSize
可从父控件继承) - 默认值(通过
PropertyMetadata
设置)
示例:当为控件同时设置样式和数据绑定时,数据绑定的值会覆盖样式中的值。
- 数据绑定与通知
依赖属性自带变更通知机制,无需手动实现INotifyPropertyChanged
:
xml
<TextBox Text="{Binding Path=Name}" />
当Name
属性值变化时,UI会自动更新。
- 样式与动画支持
依赖属性可以直接被样式和动画系统操作:
xml
<Style TargetType="Button">
<Setter Property="Background" Value="Red" />
</Style>
- 属性继承
某些依赖属性(如FontSize
、Foreground
)可以从父控件继承值,减少重复设置:
xml
<StackPanel FontSize="20">
<TextBlock Text="继承字体大小" />
<Button Content="按钮也继承" />
</StackPanel>
- 验证与回调
注册依赖属性时可添加验证逻辑和值变更回调:
csharp
public static readonly DependencyProperty AgeProperty =
DependencyProperty.Register(
"Age",
typeof(int),
typeof(Person),
new PropertyMetadata(
0, // 默认值
PropertyChangedCallback, // 值变更回调
CoerceValueCallback // 值强制回调
),
ValidateValueCallback // 验证回调
);
private static bool ValidateValueCallback(object value)
{
return (int)value >= 0 && (int)value <= 150;
}
依赖属性的实现原理
-
DependencyObject类:
- 所有支持依赖属性的类必须继承自
DependencyObject
。 - 内部维护一个哈希表,存储属性值。
- 所有支持依赖属性的类必须继承自
-
DependencyProperty类:
- 依赖属性的元数据(名称、类型、默认值等)存储在静态字段中。
- 通过
DependencyProperty.Register
方法注册。
-
GetValue/SetValue方法:
- 依赖属性的值通过这两个方法读写,而非直接访问字段。
为什么需要依赖属性?
-
减少内存占用:
- 对于大量控件,依赖属性系统共享属性定义,节省内存。
-
高级UI功能:
- 数据绑定、样式、动画等功能依赖于依赖属性系统。
-
属性值优先级管理:
- 自动处理多个值来源的冲突。
-
XAML支持:
- XAML通过依赖属性系统直接设置属性值。
5.3 自定义依赖属性的步骤
- 继承
DependencyObject
。 - 使用
DependencyProperty.Register
注册静态属性字段。 - 创建CLR包装器(可选但推荐)。
示例:自定义圆形控件的Radius属性
csharp
public class Circle : DependencyObject
{
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register(
"Radius",
typeof(double),
typeof(Circle),
new FrameworkPropertyMetadata(
50.0,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender
)
);
public double Radius
{
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
}
常见应用场景
-
自定义控件开发:
- 创建可绑定、可样式化的属性。
-
数据驱动UI:
- 通过数据绑定将视图与ViewModel连接。
-
动画效果:
- 对依赖属性应用动画。
-
布局系统:
- 影响控件测量和渲染的属性(如
Width
、Height
)通常是依赖属性。
- 影响控件测量和渲染的属性(如
注意事项
-
静态字段命名规范:
- 依赖属性的静态字段必须以
Property
结尾(如NameProperty
)。
- 依赖属性的静态字段必须以
-
线程安全:
- 依赖属性只能在创建它们的线程上访问(通常是UI线程)。
-
性能考虑:
- 依赖属性的查找比普通属性略慢,但在大多数场景下可忽略不计。
总结
依赖属性是WPF的核心机制之一,它通过将属性值存储在外部系统中,提供了强大的动态值计算、数据绑定和样式支持。理解依赖属性的工作原理,是掌握WPF高级编程的基础。
六、用户控件
6.1 介绍
在WPF中,用户控件(UserControl) 是一种封装了多个UI元素并可复用的组件,类似于自定义的"复合控件"。它允许你将多个控件组合成一个独立的功能单元,提高代码复用性和可维护性。下面详细介绍用户控件的使用方法、场景和最佳实践。
1. 用户控件 vs 自定义控件
特性 | 用户控件(UserControl) | 自定义控件(CustomControl) |
---|---|---|
实现方式 | XAML + Code-Behind | 纯代码(通常基于Control 类) |
适用场景 | 封装现有控件的组合功能 | 创建全新的控件行为(如进度条) |
样式扩展性 | 有限(依赖内部实现) | 高(通过Template 重写外观) |
复杂度 | 低(类似普通窗口) | 高(需理解控件模板和样式) |
2. 创建用户控件的步骤
步骤1:添加用户控件项
在Visual Studio中:
- 右键项目 → 添加 → 新建项 → 用户控件 (WPF)
- 命名(如
MyUserControl.xaml
)
步骤2:设计UI(XAML)
xml
<UserControl x:Class="WpfApp.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="200" Width="300">
<Grid>
<TextBox x:Name="txtInput" Margin="10" />
<Button Content="提交" Click="Button_Click" Margin="10,50,10,10" />
<TextBlock x:Name="lblOutput" Margin="10,90,10,10" />
</Grid>
</UserControl>
步骤3:实现逻辑(Code-Behind)
csharp
using System.Windows;
namespace WpfApp
{
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
lblOutput.Text = "你输入的是:" + txtInput.Text;
}
}
}
3. 在窗口中使用用户控件
步骤1:引用命名空间
在主窗口的XAML中添加命名空间引用:
xml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp" <!-- 引用当前项目命名空间 -->
Title="主窗口" Height="450" Width="800">
步骤2:在XAML中使用控件
xml
<Grid>
<local:MyUserControl Margin="20" />
</Grid>
4. 为用户控件添加依赖属性
为了让用户控件更灵活,通常需要添加依赖属性以支持外部数据绑定。
示例:添加可绑定的Text
属性
csharp
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text", // 属性名
typeof(string), // 属性类型
typeof(MyUserControl), // 所有者类型
new PropertyMetadata( // 元数据
string.Empty, // 默认值
OnTextPropertyChanged // 值变更回调
)
);
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyUserControl)d;
control.lblOutput.Text = "属性值:" + e.NewValue;
}
5. 事件封装
用户控件可以将内部控件的事件封装为自定义事件,暴露给外部使用。
示例:封装按钮点击事件
csharp
// 定义事件
public event RoutedEventHandler SubmitClicked;
private void Button_Click(object sender, RoutedEventArgs e)
{
// 触发自定义事件
SubmitClicked?.Invoke(this, e);
// 内部逻辑
lblOutput.Text = "你输入的是:" + txtInput.Text;
}
在主窗口中订阅事件:
xml
<local:MyUserControl SubmitClicked="UserControl_SubmitClicked" />
6. 数据绑定与MVVM支持
用户控件可以集成MVVM模式,通过DataContext
绑定数据。
示例:绑定到ViewModel
csharp
public class MyViewModel
{
public string InputText { get; set; }
public string OutputText { get; set; }
public RelayCommand SubmitCommand { get; }
public MyViewModel()
{
SubmitCommand = new RelayCommand(OnSubmit);
}
private void OnSubmit()
{
OutputText = "ViewModel处理:" + InputText;
}
}
在用户控件中设置DataContext
:
csharp
public MyUserControl()
{
InitializeComponent();
DataContext = new MyViewModel();
}
XAML绑定:
xml
<TextBox Text="{Binding InputText, Mode=TwoWay}" />
<Button Content="提交" Command="{Binding SubmitCommand}" />
<TextBlock Text="{Binding OutputText}" />
7. 样式与模板
用户控件的样式可以在内部定义,也可以通过Style
属性外部定制。
示例:在用户控件内部定义样式
xml
<UserControl.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="LightBlue" />
</Style>
</UserControl.Resources>
示例:外部样式覆盖
xml
<Window.Resources>
<Style TargetType="local:MyUserControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MyUserControl">
<!-- 自定义模板 -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
8. 用户控件的最佳实践
- 保持功能单一:用户控件应专注于特定功能,避免过于复杂。
- 使用依赖属性:通过依赖属性暴露可配置项,支持数据绑定。
- 封装内部实现 :内部控件的命名使用
x:FieldModifier="internal"
,减少外部访问。 - 提供清晰的API:通过事件和属性与外部通信,避免直接访问内部控件。
- 支持样式定制 :考虑通过
Template
或Style
让控件外观可定制。 - 考虑MVVM:复杂的用户控件应使用ViewModel分离逻辑与UI。
9. 常见应用场景
- 表单组件:如登录框、搜索框。
- 数据展示组件:如数据卡片、图表。
- 导航组件:如菜单、分页控件。
- 功能模块:如购物车、评论区。
10. 用户控件的局限
- 样式定制性有限:相比自定义控件,用户控件的样式扩展能力较弱。
- 性能开销:多个用户控件实例可能比单个自定义控件消耗更多资源。
- 不适合复杂交互:对于需要深度定制行为的场景(如自定义绘图),建议使用自定义控件。
总结
用户控件是WPF中封装和复用UI组件的重要方式,适合快速构建中等复杂度的复合控件。通过合理使用依赖属性、事件和数据绑定,可以创建灵活且易于维护的组件。在需要更高扩展性和性能时,可考虑升级为自定义控件。
6.2 完整示例
下面是一个完整的WPF用户控件示例,包含用户控件定义、依赖属性、事件封装、MVVM模式集成以及主窗口使用方法。这个示例实现了一个简单的带提交功能的输入框组件。
1. 创建用户控件(MyUserControl.xaml)
xml
<UserControl x:Class="WpfApp.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="200" Width="300">
<Grid>
<TextBox x:Name="txtInput"
Text="{Binding InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Margin="10"
HorizontalAlignment="Stretch" />
<Button Content="提交"
Command="{Binding SubmitCommand}"
Margin="10,50,10,10"
HorizontalAlignment="Stretch" />
<TextBlock x:Name="lblOutput"
Text="{Binding OutputText}"
Margin="10,90,10,10"
HorizontalAlignment="Stretch" />
</Grid>
</UserControl>
2. 用户控件代码隐藏(MyUserControl.xaml.cs)
csharp
using System.Windows;
using System.Windows.Controls;
namespace WpfApp
{
public partial class MyUserControl : UserControl
{
// 依赖属性:外部可绑定的文本
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(MyUserControl),
new FrameworkPropertyMetadata(
string.Empty,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnTextPropertyChanged
)
);
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
// 自定义事件:提交按钮点击
public static readonly RoutedEvent SubmitClickedEvent =
EventManager.RegisterRoutedEvent(
"SubmitClicked",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyUserControl)
);
public event RoutedEventHandler SubmitClicked
{
add => AddHandler(SubmitClickedEvent, value);
remove => RemoveHandler(SubmitClickedEvent, value);
}
public MyUserControl()
{
InitializeComponent();
DataContext = new MyUserControlViewModel(this);
}
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyUserControl)d;
control.RaiseEvent(new RoutedEventArgs(SubmitClickedEvent));
}
}
}
3. 用户控件的ViewModel(MyUserControlViewModel.cs)
csharp
using System.Windows.Input;
namespace WpfApp
{
public class MyUserControlViewModel
{
private readonly MyUserControl _view;
public string InputText { get; set; }
public string OutputText { get; private set; }
public ICommand SubmitCommand { get; }
public MyUserControlViewModel(MyUserControl view)
{
_view = view;
SubmitCommand = new RelayCommand(OnSubmit);
}
private void OnSubmit()
{
OutputText = "处理结果:" + InputText;
_view.Text = OutputText; // 更新依赖属性
}
}
// 简单的RelayCommand实现
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object parameter) => _execute();
}
}
4. 主窗口(MainWindow.xaml)
xml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="用户控件示例" Height="450" Width="800">
<Grid>
<StackPanel Orientation="Vertical" Margin="20">
<TextBlock Text="用户控件示例" FontSize="18" FontWeight="Bold" Margin="0,0,0,10" />
<!-- 使用用户控件 -->
<local:MyUserControl
Text="{Binding UserInput, Mode=TwoWay}"
SubmitClicked="UserControl_SubmitClicked"
Margin="0,0,0,20" />
<!-- 显示从用户控件获取的数据 -->
<TextBlock Text="来自用户控件的数据:" FontSize="14" Margin="0,10,0,5" />
<TextBlock Text="{Binding UserInput}" FontSize="16" Margin="0,0,0,20" />
<!-- 测试按钮 -->
<Button Content="重置用户控件" Command="{Binding ResetCommand}" Margin="0,10,0,0" />
</StackPanel>
</Grid>
</Window>
5. 主窗口ViewModel(MainViewModel.cs)
csharp
using System.Windows.Input;
namespace WpfApp
{
public class MainViewModel
{
public string UserInput { get; set; }
public ICommand ResetCommand { get; }
public MainViewModel()
{
ResetCommand = new RelayCommand(OnReset);
}
private void OnReset()
{
UserInput = string.Empty;
}
}
}
6. 主窗口代码隐藏(MainWindow.xaml.cs)
csharp
using System.Windows;
namespace WpfApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
private void UserControl_SubmitClicked(object sender, RoutedEventArgs e)
{
MessageBox.Show("用户控件提交事件触发!");
}
}
}
7. 应用程序入口(App.xaml.cs)
csharp
using System.Windows;
namespace WpfApp
{
public partial class App : Application
{
// 应用程序入口点
}
}
功能说明
-
用户控件核心功能:
- 包含文本输入框、提交按钮和结果显示区
- 通过依赖属性
Text
与外部双向绑定 - 封装
SubmitClicked
事件通知外部操作
-
MVVM集成:
- 用户控件和主窗口均使用ViewModel
- 通过
RelayCommand
处理按钮点击 - 数据绑定实现UI与数据分离
-
交互流程:
- 在用户控件输入文本并点击提交
- 控件内部处理并更新结果
- 通过依赖属性和事件通知主窗口
- 主窗口显示从控件获取的数据
运行步骤
- 创建新的WPF应用程序项目
- 按上述代码创建各个文件
- 确保命名空间一致
- 运行程序,测试用户控件功能
这个示例展示了用户控件的完整实现,包括依赖属性、事件封装和MVVM模式的应用,你可以基于此进行扩展和定制。