WPF学习(三)

文章目录

  • [一 、Template](#一 、Template)
    • [1.1 重点掌握](#1.1 重点掌握)
    • [1.2 my example](#1.2 my example)
  • [二、 Resource](#二、 Resource)
    • [2.1 重点掌握](#2.1 重点掌握)
    • [2.2 三种引用方式](#2.2 三种引用方式)
  • 三、关键帧
  • [四、 路由命令](#四、 路由命令)
  • 五、依赖属性
      • [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 自定义依赖属性的步骤)
  • 六、用户控件

一 、Template

1.1 重点掌握

在WPF(Windows Presentation Foundation)中,ControlTemplate 是定义控件外观和行为的核心机制。一个模板可以包含以下类型的元素和功能:

一、可视化元素
模板的核心是定义控件的视觉结构,可包含:

  1. 布局容器

    • GridStackPanelCanvasDockPanel 等,用于组织子元素的排列方式。

    • 示例:

      xml 复制代码
      <ControlTemplate TargetType="Button">
          <Grid>
              <!-- 按钮内容将在此处显示 -->
          </Grid>
      </ControlTemplate>
  2. 图形元素

    • RectangleEllipsePathLine 等形状,用于创建自定义外观。

    • 示例:

      xml 复制代码
      <Rectangle x:Name="Background" Fill="Blue" />
  3. 内容展示

    • ContentPresenter:显示控件的 Content 属性(如按钮的文本)。

    • ItemsPresenter:用于 ItemsControl(如 ListBox)显示子项集合。

    • 示例:

      xml 复制代码
      <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
  4. 装饰器

    • BorderDropShadowEffectBlurEffect 等,用于添加边框、阴影或视觉效果。

    • 示例:

      xml 复制代码
      <Border BorderBrush="Black" BorderThickness="1" CornerRadius="4">
          <!-- 内部元素 -->
      </Border>

二、触发器(Triggers)

用于根据控件状态改变外观,包括:

  1. 属性触发器(PropertyTrigger)

    • 基于控件属性值触发(如 IsMouseOverIsPressed)。

    • 示例:

      xml 复制代码
      <Trigger Property="IsMouseOver" Value="True">
          <Setter TargetName="Background" Property="Fill" Value="LightBlue" />
      </Trigger>
  2. 事件触发器(EventTrigger)

    • 基于事件触发动画(如 MouseEnterLoaded)。

    • 示例:

      xml 复制代码
      <EventTrigger RoutedEvent="Button.Click">
          <BeginStoryboard>
              <Storyboard>
                  <!-- 动画定义 -->
              </Storyboard>
          </BeginStoryboard>
      </EventTrigger>
  3. 数据触发器(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}" />

常见绑定属性:WidthHeightForegroundBackground 等。

六、样式与资源

  1. 内联样式:在模板内定义局部样式。

    xml 复制代码
    <Style TargetType="TextBlock">
        <Setter Property="FontSize" Value="14" />
    </Style>
  2. 资源引用 :使用 StaticResourceDynamicResource 引用外部资源(如颜色、画笔)。

    xml 复制代码
    <SolidColorBrush x:Key="ButtonHoverBrush" Color="LightBlue" />
    <!-- 在模板中使用 -->
    <Setter Property="Background" Value="{StaticResource ButtonHoverBrush}" />

七、逻辑交互元素

  1. 子控件 :在模板中嵌入其他控件(如 TextBoxCheckBox)。

    xml 复制代码
    <CheckBox x:Name="ToggleButton" Content="选项" />
  2. 交互行为 :通过 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 重点掌握

一、资源的定义与分类

资源可以定义在多个位置,按作用域分为:

  1. 应用程序级别 :在 App.xaml 中定义,全局可用

    xml 复制代码
    <Application.Resources>
        <SolidColorBrush x:Key="PrimaryColor" Color="#3498DB" />
    </Application.Resources>
  2. 窗口/控件级别 :在窗口或控件的 Resources 中定义,仅在当前范围内可用

    xml 复制代码
    <Window.Resources>
        <Style x:Key="ButtonStyle" TargetType="Button">
            <Setter Property="Background" Value="{StaticResource PrimaryColor}" />
        </Style>
    </Window.Resources>
  3. 局部资源:在元素内部定义,仅作用于该元素及其子元素

    xml 复制代码
    <Grid.Resources>
        <DataTemplate x:Key="PersonTemplate">
            <TextBlock Text="{Binding Name}" />
        </DataTemplate>
    </Grid.Resources>

二、常见资源类型

  1. 样式(Style)

    统一设置控件的外观属性:

    xml 复制代码
    <Style x:Key="TextBoxStyle" TargetType="TextBox">
        <Setter Property="Foreground" Value="DarkBlue" />
        <Setter Property="BorderBrush" Value="Gray" />
    </Style>
  2. 画笔(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>
  3. 模板(Template)

    自定义控件的视觉结构:

    xml 复制代码
    <ControlTemplate x:Key="CustomButtonTemplate" TargetType="Button">
        <Border Background="{TemplateBinding Background}" CornerRadius="5">
            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Border>
    </ControlTemplate>
  4. 数据模板(DataTemplate)

    定义数据的显示方式:

    xml 复制代码
    <DataTemplate x:Key="EmployeeTemplate">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Name}" Margin="5" />
            <TextBlock Text="{Binding Age}" Margin="5" />
        </StackPanel>
    </DataTemplate>
  5. 动画(Storyboard)

    预定义动画效果:

    xml 复制代码
    <Storyboard x:Key="FadeInAnimation">
        <DoubleAnimation 
            Storyboard.TargetProperty="Opacity" 
            From="0" To="1" Duration="0:0:0.5" />
    </Storyboard>

三、资源的引用方式

使用 StaticResourceDynamicResource 标记扩展引用资源:

  1. 静态资源(StaticResource)

    在编译时解析,性能较高,适合不会动态变化的资源:

    xml 复制代码
    <Button Style="{StaticResource ButtonStyle}" />
    <Rectangle Fill="{StaticResource GradientBrush}" />
  2. 动态资源(DynamicResource)

    在运行时动态解析,支持资源值的实时更新:

    xml 复制代码
    <TextBlock Foreground="{DynamicResource AccentColor}" />

四、资源字典(ResourceDictionary)

将资源单独提取到 .xaml 文件中,实现跨项目共享:

  1. 创建资源字典文件 (如 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>
  2. 在应用程序中引用

    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 都会自动应用此样式。

六、资源查找顺序

当使用 StaticResourceDynamicResource 引用资源时,WPF会按以下顺序查找:

  1. 当前元素的 Resources 集合
  2. 父元素的 Resources 集合(向上递归查找)
  3. 窗口/页面的 Resources 集合
  4. 应用程序级别的 Resources 集合(App.xaml
  5. 所有合并的资源字典
  6. 系统资源(如默认字体、颜色等)

七、示例:完整应用
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>

八、注意事项

  1. 资源键必须唯一 :同一范围内不能有重复的 x:Key
  2. 静态资源 vs 动态资源
    • 静态资源在初始化时解析,修改资源不会影响已应用的元素
    • 动态资源会实时更新,但性能开销略高
  3. 避免循环引用:资源间的引用不能形成闭环
  4. 资源优先级:局部资源会覆盖全局资源

合理使用 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>

关键点说明

  1. x:Key 的作用

    • x:Key(如 x:Key="mybutton"):需显式引用
    • x:Key:隐式应用到所有 TargetType 类型的控件
  2. 资源作用域

    • <Window.Resources> 中定义:仅当前窗口可用
    • <Application.Resources> 中定义:全局所有窗口可用
  3. 引用语法

    • 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>

常见问题

  1. 样式不生效

    • 检查 x:Key 拼写是否正确
    • 确保 TargetType 与控件类型匹配(如 TargetType="Button"
  2. 全局应用样式

    将样式定义移至 App.xaml<Application.Resources> 中:

    xml 复制代码
    <Application.Resources>
        <Style x:Key="mybutton" TargetType="Button">
            <!-- 样式内容 -->
        </Style>
    </Application.Resources>

通过上述方法,你可以灵活地复用自定义的 Button 样式,提高代码的可维护性和一致性。

三、关键帧

在WPF中,含有关键帧的时间线(KeyFrame Animation)不含关键帧的时间线(基本动画) 主要区别在于动画的复杂度和控制精度。以下是具体对比:

特性 基本动画 关键帧动画
变化路径 线性(直线) 支持非线性(曲线、突变)
控制点数量 2个(起始值和结束值) ≥2个(可定义任意数量关键帧)
时间控制 整体持续时间(Duration) 每个关键帧可单独指定时间
插值方式 固定线性插值 支持线性、曲线、离散等多种方式
复杂度 简单 高(适合复杂动画)
XAML代码量 多(需要定义多个关键帧)

一、基本动画(不含关键帧)
特点

  1. 简单线性变化

    从起始值(From)到结束值(To)或相对变化值(By)进行线性插值。
    示例

    xml 复制代码
    <DoubleAnimation 
        From="0" To="100" 
        Duration="0:0:1" 
        Storyboard.TargetProperty="Width" />
  2. 单一变化路径

    只支持直线过渡,无法定义复杂的变化曲线。

  3. 适用场景

    • 简单的淡入淡出、大小缩放、位置移动等线性变化。

二、关键帧动画(含有关键帧)
特点

  1. 多阶段复杂变化

    通过定义多个关键帧(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>
  2. 多种插值方式

    • LinearKeyFrame:线性插值(匀速变化)
    • SplineKeyFrame:使用贝塞尔曲线插值(可自定义加速/减速)
    • DiscreteKeyFrame:离散变化(瞬间跳转)
  3. 精确时间控制

    每个关键帧都可以指定精确的 KeyTime,实现复杂的时间序列。

  4. 适用场景

    • 不规则运动(如弹跳、弹性效果)
    • 多阶段动画(如先加速后减速)
    • 复杂过渡效果(如模拟物理运动)

三、核心区别对比表

特性 基本动画 关键帧动画
变化路径 线性(直线) 支持非线性(曲线、突变)
控制点数量 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 命名空间。这个类的作用是提供一系列应用程序层面的通用命令,像复制、粘贴、保存等操作都涵盖其中。借助这些预定义好的命令,开发者能更便捷地实现常见的用户交互功能,而无需手动编写大量代码。

核心功能与优势

  1. 标准化操作ApplicationCommands 把一些常用的操作进行了标准化处理,使应用程序能够遵循统一的用户界面(UI)惯例。
  2. 自动状态管理:这些命令能够依据当前的上下文自动调整自身状态,比如判断"剪切"命令在没有选中内容时是否应该禁用。
  3. 多设备支持:它支持多种输入设备,包括键盘快捷键、菜单以及工具栏按钮等。
  4. 简化实现:命令可以直接和视图模型(ViewModel)或者代码隐藏文件(Code-Behind)中的处理逻辑绑定,减少了重复编码。

常用命令示例

ApplicationCommands 类中包含了许多静态属性,下面是一些比较常用的:

  • ApplicationCommands.New:用于创建新文档。
  • ApplicationCommands.Open:用于打开已有文档。
  • ApplicationCommands.Save:用于保存当前文档。
  • ApplicationCommands.SaveAs:用于将当前文档另存为其他文件。
  • ApplicationCommands.CutApplicationCommands.CopyApplicationCommands.Paste:对应剪切、复制和粘贴操作。
  • ApplicationCommands.UndoApplicationCommands.Redo:对应撤销和重做操作。
  • ApplicationCommands.Print:用于打印文档。
  • ApplicationCommands.Find:用于查找功能。
  • ApplicationCommands.Close:用于关闭当前窗口或文档。

应用场景

  1. 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>
  1. 代码隐藏文件中的处理函数
    在窗口(Window)的代码隐藏文件里实现处理函数:
csharp 复制代码
private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    // 实现保存逻辑
    MessageBox.Show("文档已保存!");
}

private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    // 判断是否可以执行保存命令
    e.CanExecute = IsDocumentModified; // 假设这是一个判断文档是否修改的属性
}
  1. 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.CopyApplicationCommands.Paste
  • CommandTarget明确指定文本框myTextBox为命令的目标元素。

步骤2:命令状态初始检查

  • 当窗口加载完成,WPF会自动调用所有命令的CanExecute方法:
    • Copy_CanExecute检查文本框的SelectedText.Length,初始时无选中内容,返回false复制按钮禁用
    • Paste_CanExecute返回true粘贴按钮启用

步骤3:用户操作触发命令

  1. 用户选中文本

    • 文本框的SelectionChanged事件触发。
    • WPF自动重新检查Copy命令的状态(通过CommandManager)。
    • Copy_CanExecute重新执行,发现有选中内容,返回true复制按钮变为启用状态
  2. 用户点击复制按钮

    • 按钮作为命令源,发起ApplicationCommands.Copy命令请求。
    • WPF根据CommandTarget找到文本框myTextBox
    • 再次调用Copy_CanExecute确认可执行状态(确保状态未变化)。
    • 调用Copy_Executed处理函数执行复制逻辑。
  3. 用户点击粘贴按钮

    • 类似流程,但Paste_CanExecute始终返回true,直接执行Paste_Executed
  4. 关键机制详解

命令路由过程

当按钮被点击时,WPF会:

  1. 从按钮开始,向上遍历视觉树,查找CommandBinding(这里在Window层级找到)。
  2. 如果未找到CommandBinding,命令会继续向上冒泡,直到找到处理程序或到达根元素。

状态自动更新

  • 焦点变化:当用户切换到其他控件再回到文本框时,WPF会重新检查命令状态。
  • 属性变化 :文本框的SelectedText属性变化时,WPF不会自动触发状态检查(这是示例的简化处理)。在实际应用中,你可能需要手动调用CommandManager.InvalidateRequerySuggested()或使用MVVM模式中的CanExecuteChanged事件。

命令绑定优先级

  1. 显式绑定 :通过CommandBinding定义的处理函数(如示例中的代码)。

  2. 元素内置处理 :如果目标元素(如TextBox)本身支持该命令(如ApplicationCommands.Copy),WPF会使用其内置实现。

  3. 默认行为:如果没有找到任何处理程序,命令将被忽略。

  4. 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逻辑和使用CommandBindingICommand接口,你可以实现:

  • 自动控制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 依赖属性的核心特性

  1. 动态值解析
    依赖属性的值可以由多个来源动态决定,优先级从高到低:
  2. 动画
  3. 显式设置的值(如TextBox.Text = "Hello"
  4. 数据绑定
  5. 样式触发器
  6. 模板触发器
  7. 样式设置
  8. 继承值(如FontSize可从父控件继承)
  9. 默认值(通过PropertyMetadata设置)

示例:当为控件同时设置样式和数据绑定时,数据绑定的值会覆盖样式中的值。

  1. 数据绑定与通知
    依赖属性自带变更通知机制,无需手动实现INotifyPropertyChanged
xml 复制代码
<TextBox Text="{Binding Path=Name}" />

Name属性值变化时,UI会自动更新。

  1. 样式与动画支持
    依赖属性可以直接被样式和动画系统操作:
xml 复制代码
<Style TargetType="Button">
    <Setter Property="Background" Value="Red" />
</Style>
  1. 属性继承
    某些依赖属性(如FontSizeForeground)可以从父控件继承值,减少重复设置:
xml 复制代码
<StackPanel FontSize="20">
    <TextBlock Text="继承字体大小" />
    <Button Content="按钮也继承" />
</StackPanel>
  1. 验证与回调
    注册依赖属性时可添加验证逻辑和值变更回调:
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;
}

依赖属性的实现原理

  1. DependencyObject类

    • 所有支持依赖属性的类必须继承自DependencyObject
    • 内部维护一个哈希表,存储属性值。
  2. DependencyProperty类

    • 依赖属性的元数据(名称、类型、默认值等)存储在静态字段中。
    • 通过DependencyProperty.Register方法注册。
  3. GetValue/SetValue方法

    • 依赖属性的值通过这两个方法读写,而非直接访问字段。

为什么需要依赖属性?

  1. 减少内存占用

    • 对于大量控件,依赖属性系统共享属性定义,节省内存。
  2. 高级UI功能

    • 数据绑定、样式、动画等功能依赖于依赖属性系统。
  3. 属性值优先级管理

    • 自动处理多个值来源的冲突。
  4. XAML支持

    • XAML通过依赖属性系统直接设置属性值。

5.3 自定义依赖属性的步骤

  1. 继承DependencyObject
  2. 使用DependencyProperty.Register注册静态属性字段。
  3. 创建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);
    }
}

常见应用场景

  1. 自定义控件开发

    • 创建可绑定、可样式化的属性。
  2. 数据驱动UI

    • 通过数据绑定将视图与ViewModel连接。
  3. 动画效果

    • 对依赖属性应用动画。
  4. 布局系统

    • 影响控件测量和渲染的属性(如WidthHeight)通常是依赖属性。

注意事项

  1. 静态字段命名规范

    • 依赖属性的静态字段必须以Property结尾(如NameProperty)。
  2. 线程安全

    • 依赖属性只能在创建它们的线程上访问(通常是UI线程)。
  3. 性能考虑

    • 依赖属性的查找比普通属性略慢,但在大多数场景下可忽略不计。

总结

依赖属性是WPF的核心机制之一,它通过将属性值存储在外部系统中,提供了强大的动态值计算、数据绑定和样式支持。理解依赖属性的工作原理,是掌握WPF高级编程的基础。

六、用户控件

6.1 介绍

在WPF中,用户控件(UserControl) 是一种封装了多个UI元素并可复用的组件,类似于自定义的"复合控件"。它允许你将多个控件组合成一个独立的功能单元,提高代码复用性和可维护性。下面详细介绍用户控件的使用方法、场景和最佳实践。

1. 用户控件 vs 自定义控件

特性 用户控件(UserControl) 自定义控件(CustomControl)
实现方式 XAML + Code-Behind 纯代码(通常基于Control类)
适用场景 封装现有控件的组合功能 创建全新的控件行为(如进度条)
样式扩展性 有限(依赖内部实现) 高(通过Template重写外观)
复杂度 低(类似普通窗口) 高(需理解控件模板和样式)

2. 创建用户控件的步骤

步骤1:添加用户控件项

在Visual Studio中:

  1. 右键项目 → 添加 → 新建项 → 用户控件 (WPF)
  2. 命名(如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. 用户控件的最佳实践

  1. 保持功能单一:用户控件应专注于特定功能,避免过于复杂。
  2. 使用依赖属性:通过依赖属性暴露可配置项,支持数据绑定。
  3. 封装内部实现 :内部控件的命名使用x:FieldModifier="internal",减少外部访问。
  4. 提供清晰的API:通过事件和属性与外部通信,避免直接访问内部控件。
  5. 支持样式定制 :考虑通过TemplateStyle让控件外观可定制。
  6. 考虑MVVM:复杂的用户控件应使用ViewModel分离逻辑与UI。

9. 常见应用场景

  1. 表单组件:如登录框、搜索框。
  2. 数据展示组件:如数据卡片、图表。
  3. 导航组件:如菜单、分页控件。
  4. 功能模块:如购物车、评论区。

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
    {
        // 应用程序入口点
    }
}

功能说明

  1. 用户控件核心功能

    • 包含文本输入框、提交按钮和结果显示区
    • 通过依赖属性Text与外部双向绑定
    • 封装SubmitClicked事件通知外部操作
  2. MVVM集成

    • 用户控件和主窗口均使用ViewModel
    • 通过RelayCommand处理按钮点击
    • 数据绑定实现UI与数据分离
  3. 交互流程

    • 在用户控件输入文本并点击提交
    • 控件内部处理并更新结果
    • 通过依赖属性和事件通知主窗口
    • 主窗口显示从控件获取的数据

运行步骤

  1. 创建新的WPF应用程序项目
  2. 按上述代码创建各个文件
  3. 确保命名空间一致
  4. 运行程序,测试用户控件功能

这个示例展示了用户控件的完整实现,包括依赖属性、事件封装和MVVM模式的应用,你可以基于此进行扩展和定制。