C# 零基础到精通教程 - WPF 专题三:高级控件与自定义控件

专题二我们学习了数据绑定和 MVVM 模式,掌握了 WPF 的核心架构。但 WPF 内置的控件不能满足所有需求,有时我们需要创建自定义控件或者对现有控件进行深度定制。这一专题我们将学习 WPF 的高级控件和自定义控件开发,让你能够创建任何你想要的界面。


3.1 控件模板(ControlTemplate)

3.1.1 为什么需要 ControlTemplate?

ControlTemplate 可以完全改变控件的外观,而不改变其行为

csharp

复制代码
// 默认 Button 是一个矩形按钮
// 通过 ControlTemplate,可以变成圆形、椭圆形、或者任何形状

// 默认外观是硬编码的
<Button Content="普通按钮"/>

// 自定义模板后可以是任何样子
<Button Content="圆形按钮">
    <Button.Template>
        <ControlTemplate TargetType="Button">
            <Ellipse Fill="LightBlue">
                <ContentPresenter HorizontalAlignment="Center" 
                                  VerticalAlignment="Center"/>
            </Ellipse>
        </ControlTemplate>
    </Button.Template>
</Button>

3.1.2 ControlTemplate 基础

xml

复制代码
<!-- 一个完整的按钮模板 -->
<Window.Resources>
    <Style TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <!-- 模板根元素 -->
                    <Grid>
                        <!-- 背景 -->
                        <Border x:Name="border" 
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                CornerRadius="5">
                            
                            <!-- 内容 -->
                            <ContentPresenter x:Name="contentPresenter"
                                              HorizontalAlignment="Center"
                                              VerticalAlignment="Center"
                                              Content="{TemplateBinding Content}"/>
                        </Border>
                        
                        <!-- 鼠标悬停时的发光效果 -->
                        <Border x:Name="glowBorder" 
                                Background="Transparent"
                                BorderBrush="Gold"
                                BorderThickness="2"
                                CornerRadius="5"
                                Opacity="0"/>
                    </Grid>
                    
                    <!-- 模板触发器 -->
                    <ControlTemplate.Triggers>
                        <!-- 鼠标悬停 -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="border" Property="Background" Value="LightBlue"/>
                            <Setter TargetName="glowBorder" Property="Opacity" Value="1"/>
                        </Trigger>
                        
                        <!-- 按钮按下 -->
                        <Trigger Property="IsPressed" Value="True">
                            <Setter TargetName="border" Property="RenderTransform">
                                <Setter.Value>
                                    <ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                        
                        <!-- 禁用状态 -->
                        <Trigger Property="IsEnabled" Value="False">
                            <Setter TargetName="border" Property="Opacity" Value="0.5"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

3.1.3 TemplateBinding 和 ContentPresenter

xml

复制代码
<!-- TemplateBinding:绑定到目标控件的属性 -->
<ControlTemplate TargetType="Button">
    <Border Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}">
        
        <!-- ContentPresenter:显示内容的地方 -->
        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
    </Border>
</ControlTemplate>

3.1.4 自定义按钮模板示例

xml

复制代码
<!-- 玻璃风格按钮 -->
<Style x:Key="GlassButtonStyle" TargetType="Button">
    <Setter Property="Width" Value="120"/>
    <Setter Property="Height" Value="40"/>
    <Setter Property="FontSize" Value="14"/>
    <Setter Property="FontWeight" Value="Bold"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="Cursor" Value="Hand"/>
    
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Grid>
                    <!-- 按钮背景 -->
                    <Border x:Name="border"
                            Background="#2196F3"
                            CornerRadius="20">
                        <Border.Effect>
                            <DropShadowEffect BlurRadius="8" ShadowDepth="2" Opacity="0.3"/>
                        </Border.Effect>
                    </Border>
                    
                    <!-- 玻璃高光效果 -->
                    <Border x:Name="glass"
                            Background="White"
                            Opacity="0.2"
                            CornerRadius="20"
                            Height="20"
                            VerticalAlignment="Top"/>
                    
                    <!-- 内容 -->
                    <ContentPresenter HorizontalAlignment="Center"
                                      VerticalAlignment="Center"/>
                    
                    <!-- 点击动画 -->
                    <Border x:Name="ripple"
                            Background="White"
                            Opacity="0"
                            CornerRadius="20">
                        <Border.RenderTransform>
                            <ScaleTransform ScaleX="0" ScaleY="0"/>
                        </Border.RenderTransform>
                    </Border>
                </Grid>
                
                <ControlTemplate.Triggers>
                    <!-- 鼠标悬停 -->
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="border" Property="Background" Value="#1976D2"/>
                        <Setter TargetName="glass" Property="Height" Value="25"/>
                    </Trigger>
                    
                    <!-- 按钮按下 -->
                    <Trigger Property="IsPressed" Value="True">
                        <Setter TargetName="border" Property="RenderTransform">
                            <Setter.Value>
                                <ScaleTransform ScaleX="0.97" ScaleY="0.97"/>
                            </Setter.Value>
                        </Setter>
                        <Setter TargetName="ripple" Property="Opacity" Value="0.3"/>
                        <Setter TargetName="ripple" Property="RenderTransform">
                            <Setter.Value>
                                <ScaleTransform ScaleX="1" ScaleY="1"/>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                    
                    <!-- 禁用状态 -->
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="border" Property="Background" Value="#B0BEC5"/>
                        <Setter TargetName="glass" Property="Opacity" Value="0"/>
                        <Setter Property="Foreground" Value="#ECEFF1"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

3.2 数据模板(DataTemplate)

3.2.1 DataTemplate 基础

DataTemplate 定义了数据对象如何显示在 UI 上

xml

复制代码
<!-- 基本 DataTemplate -->
<ListBox ItemsSource="{Binding Users}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border BorderBrush="Gray" BorderThickness="1" Margin="5" Padding="10" CornerRadius="5">
                <StackPanel>
                    <TextBlock Text="{Binding Name}" FontWeight="Bold" FontSize="14"/>
                    <TextBlock Text="{Binding Email}" Foreground="Gray"/>
                    <TextBlock Text="{Binding Age, StringFormat=年龄:{0}岁}"/>
                </StackPanel>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

3.2.2 复杂的 DataTemplate

xml

复制代码
<Window.Resources>
    <!-- 根据数据状态改变显示样式 -->
    <DataTemplate x:Key="UserTemplate" DataType="local:User">
        <Border x:Name="rootBorder" Margin="5" Padding="10" CornerRadius="5">
            <Border.Style>
                <Style TargetType="Border">
                    <Setter Property="BorderBrush" Value="LightGray"/>
                    <Setter Property="BorderThickness" Value="1"/>
                    <Setter Property="Background" Value="White"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsActive}" Value="False">
                            <Setter Property="Background" Value="#F5F5F5"/>
                            <Setter Property="Opacity" Value="0.6"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <Setter Property="BorderBrush" Value="#2196F3"/>
                            <Setter Property="BorderThickness" Value="2"/>
                            <Setter Property="Background" Value="#E3F2FD"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Border.Style>
            
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                
                <!-- 头像 -->
                <Border Grid.Column="0" Width="40" Height="40" CornerRadius="20" 
                        Background="{Binding AvatarColor}">
                    <TextBlock Text="{Binding Initials}" 
                               HorizontalAlignment="Center" 
                               VerticalAlignment="Center"
                               Foreground="White"
                               FontWeight="Bold"/>
                </Border>
                
                <!-- 用户信息 -->
                <StackPanel Grid.Column="1" Margin="10,0">
                    <TextBlock Text="{Binding Name}" FontWeight="Bold" FontSize="14"/>
                    <TextBlock Text="{Binding Email}" Foreground="Gray" FontSize="11"/>
                    <TextBlock Text="{Binding Department}" FontSize="11"/>
                </StackPanel>
                
                <!-- 状态指示器 -->
                <StackPanel Grid.Column="2" VerticalAlignment="Center">
                    <Ellipse Width="10" Height="10" Fill="{Binding StatusColor}" Margin="0,0,0,5"/>
                    <TextBlock Text="{Binding Status}" FontSize="10" Foreground="Gray"/>
                </StackPanel>
            </Grid>
        </Border>
    </DataTemplate>
</Window.Resources>

3.2.3 DataTemplateSelector(模板选择器)

csharp

复制代码
// 根据数据选择不同的模板
public class MessageTemplateSelector : DataTemplateSelector
{
    public DataTemplate IncomingTemplate { get; set; }
    public DataTemplate OutgoingTemplate { get; set; }
    
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item is Message message)
        {
            return message.IsIncoming ? IncomingTemplate : OutgoingTemplate;
        }
        return base.SelectTemplate(item, container);
    }
}

xml

复制代码
<Window.Resources>
    <!-- 接收消息模板(左侧)-->
    <DataTemplate x:Key="IncomingTemplate">
        <Border Background="LightGray" CornerRadius="10" Margin="5" 
                HorizontalAlignment="Left" MaxWidth="300">
            <TextBlock Text="{Binding Content}" Margin="10"/>
        </Border>
    </DataTemplate>
    
    <!-- 发送消息模板(右侧)-->
    <DataTemplate x:Key="OutgoingTemplate">
        <Border Background="LightBlue" CornerRadius="10" Margin="5" 
                HorizontalAlignment="Right" MaxWidth="300">
            <TextBlock Text="{Binding Content}" Margin="10"/>
        </Border>
    </DataTemplate>
    
    <!-- 模板选择器 -->
    <local:MessageTemplateSelector x:Key="MessageTemplateSelector"
        IncomingTemplate="{StaticResource IncomingTemplate}"
        OutgoingTemplate="{StaticResource OutgoingTemplate}"/>
</Window.Resources>

<!-- 使用模板选择器 -->
<ListBox ItemsSource="{Binding Messages}"
         ItemTemplateSelector="{StaticResource MessageTemplateSelector}"/>

3.3 自定义控件开发

3.3.1 自定义控件类型

控件类型 说明 适用场景
UserControl 组合现有控件 创建复合控件,如登录面板
CustomControl 继承自 Control 需要完全自定义外观和行为
TemplatedControl 模板化控件 支持样式和模板重定义
Decorator 装饰器 包装单个子元素

3.3.2 UserControl 示例:评分控件

csharp

复制代码
// Controls/RatingControl.xaml.cs
public partial class RatingControl : UserControl
{
    // 依赖属性:值
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register(nameof(Value), typeof(int), typeof(RatingControl),
            new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnValueChanged));
    
    // 依赖属性:最大值
    public static readonly DependencyProperty MaxValueProperty =
        DependencyProperty.Register(nameof(MaxValue), typeof(int), typeof(RatingControl),
            new PropertyMetadata(5));
    
    // 依赖属性:只读模式
    public static readonly DependencyProperty IsReadOnlyProperty =
        DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(RatingControl),
            new PropertyMetadata(false));
    
    // 路由事件:值改变
    public static readonly RoutedEvent ValueChangedEvent =
        EventManager.RegisterRoutedEvent(nameof(ValueChanged), RoutingStrategy.Bubble,
            typeof(RoutedPropertyChangedEventHandler<int>), typeof(RatingControl));
    
    public int Value
    {
        get => (int)GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }
    
    public int MaxValue
    {
        get => (int)GetValue(MaxValueProperty);
        set => SetValue(MaxValueProperty, value);
    }
    
    public bool IsReadOnly
    {
        get => (bool)GetValue(IsReadOnlyProperty);
        set => SetValue(IsReadOnlyProperty, value);
    }
    
    public event RoutedPropertyChangedEventHandler<int> ValueChanged
    {
        add => AddHandler(ValueChangedEvent, value);
        remove => RemoveHandler(ValueChangedEvent, value);
    }
    
    public RatingControl()
    {
        InitializeComponent();
    }
    
    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (RatingControl)d;
        control.UpdateStars();
        
        var args = new RoutedPropertyChangedEventArgs<int>(
            (int)e.OldValue, (int)e.NewValue, ValueChangedEvent);
        control.RaiseEvent(args);
    }
    
    private void UpdateStars()
    {
        // 更新星星显示逻辑
        for (int i = 1; i <= MaxValue; i++)
        {
            var star = FindName($"Star{i}") as Button;
            if (star != null)
            {
                star.Content = i <= Value ? "★" : "☆";
                star.Foreground = i <= Value ? 
                    new SolidColorBrush(Colors.Gold) : 
                    new SolidColorBrush(Colors.Gray);
            }
        }
    }
    
    private void Star_Click(object sender, RoutedEventArgs e)
    {
        if (IsReadOnly) return;
        
        var button = sender as Button;
        var value = int.Parse(button.Tag.ToString());
        Value = value;
    }
    
    private void Star_MouseEnter(object sender, MouseEventArgs e)
    {
        if (IsReadOnly) return;
        
        var button = sender as Button;
        var value = int.Parse(button.Tag.ToString());
        
        for (int i = 1; i <= MaxValue; i++)
        {
            var star = FindName($"Star{i}") as Button;
            if (star != null)
            {
                star.Content = i <= value ? "★" : "☆";
                star.Foreground = i <= value ? 
                    new SolidColorBrush(Colors.Gold) : 
                    new SolidColorBrush(Colors.LightGray);
            }
        }
    }
    
    private void Star_MouseLeave(object sender, MouseEventArgs e)
    {
        if (IsReadOnly) return;
        UpdateStars();
    }
}

xml

复制代码
<!-- Controls/RatingControl.xaml -->
<UserControl x:Class="WpfControls.Controls.RatingControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    <StackPanel x:Name="StarsPanel" Orientation="Horizontal">
        <Button x:Name="Star1" Tag="1" Content="☆" FontSize="24" 
                Click="Star_Click" MouseEnter="Star_MouseEnter" MouseLeave="Star_MouseLeave"
                Background="Transparent" BorderThickness="0" Cursor="Hand"/>
        <Button x:Name="Star2" Tag="2" Content="☆" FontSize="24" 
                Click="Star_Click" MouseEnter="Star_MouseEnter" MouseLeave="Star_MouseLeave"
                Background="Transparent" BorderThickness="0" Cursor="Hand"/>
        <Button x:Name="Star3" Tag="3" Content="☆" FontSize="24" 
                Click="Star_Click" MouseEnter="Star_MouseEnter" MouseLeave="Star_MouseLeave"
                Background="Transparent" BorderThickness="0" Cursor="Hand"/>
        <Button x:Name="Star4" Tag="4" Content="☆" FontSize="24" 
                Click="Star_Click" MouseEnter="Star_MouseEnter" MouseLeave="Star_MouseLeave"
                Background="Transparent" BorderThickness="0" Cursor="Hand"/>
        <Button x:Name="Star5" Tag="5" Content="☆" FontSize="24" 
                Click="Star_Click" MouseEnter="Star_MouseEnter" MouseLeave="Star_MouseLeave"
                Background="Transparent" BorderThickness="0" Cursor="Hand"/>
    </StackPanel>
</UserControl>

3.3.3 CustomControl 示例:数字输入框

csharp

复制代码
// Controls/NumericUpDown.cs
public class NumericUpDown : Control
{
    // 依赖属性:值
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register(nameof(Value), typeof(double), typeof(NumericUpDown),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnValueChanged, CoerceValue));
    
    // 依赖属性:最小值
    public static readonly DependencyProperty MinimumProperty =
        DependencyProperty.Register(nameof(Minimum), typeof(double), typeof(NumericUpDown),
            new PropertyMetadata(double.MinValue, OnMinimumChanged));
    
    // 依赖属性:最大值
    public static readonly DependencyProperty MaximumProperty =
        DependencyProperty.Register(nameof(Maximum), typeof(double), typeof(NumericUpDown),
            new PropertyMetadata(double.MaxValue, OnMaximumChanged));
    
    // 依赖属性:步长
    public static readonly DependencyProperty IncrementProperty =
        DependencyProperty.Register(nameof(Increment), typeof(double), typeof(NumericUpDown),
            new PropertyMetadata(1.0));
    
    // 依赖属性:小数位数
    public static readonly DependencyProperty DecimalPlacesProperty =
        DependencyProperty.Register(nameof(DecimalPlaces), typeof(int), typeof(NumericUpDown),
            new PropertyMetadata(0, OnDecimalPlacesChanged));
    
    // 依赖属性:只读模式
    public static readonly DependencyProperty IsReadOnlyProperty =
        DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(NumericUpDown),
            new PropertyMetadata(false));
    
    // 路由事件:值改变
    public static readonly RoutedEvent ValueChangedEvent =
        EventManager.RegisterRoutedEvent(nameof(ValueChanged), RoutingStrategy.Bubble,
            typeof(RoutedPropertyChangedEventHandler<double>), typeof(NumericUpDown));
    
    static NumericUpDown()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
            new FrameworkPropertyMetadata(typeof(NumericUpDown)));
    }
    
    public double Value
    {
        get => (double)GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }
    
    public double Minimum
    {
        get => (double)GetValue(MinimumProperty);
        set => SetValue(MinimumProperty, value);
    }
    
    public double Maximum
    {
        get => (double)GetValue(MaximumProperty);
        set => SetValue(MaximumProperty, value);
    }
    
    public double Increment
    {
        get => (double)GetValue(IncrementProperty);
        set => SetValue(IncrementProperty, value);
    }
    
    public int DecimalPlaces
    {
        get => (int)GetValue(DecimalPlacesProperty);
        set => SetValue(DecimalPlacesProperty, value);
    }
    
    public bool IsReadOnly
    {
        get => (bool)GetValue(IsReadOnlyProperty);
        set => SetValue(IsReadOnlyProperty, value);
    }
    
    public event RoutedPropertyChangedEventHandler<double> ValueChanged
    {
        add => AddHandler(ValueChangedEvent, value);
        remove => RemoveHandler(ValueChangedEvent, value);
    }
    
    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (NumericUpDown)d;
        var args = new RoutedPropertyChangedEventArgs<double>(
            (double)e.OldValue, (double)e.NewValue, ValueChangedEvent);
        control.RaiseEvent(args);
    }
    
    private static object CoerceValue(DependencyObject d, object baseValue)
    {
        var control = (NumericUpDown)d;
        var value = (double)baseValue;
        
        if (value < control.Minimum) return control.Minimum;
        if (value > control.Maximum) return control.Maximum;
        
        // 限制小数位数
        return Math.Round(value, control.DecimalPlaces);
    }
    
    private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (NumericUpDown)d;
        control.CoerceValue(ValueProperty);
    }
    
    private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (NumericUpDown)d;
        control.CoerceValue(ValueProperty);
    }
    
    private static void OnDecimalPlacesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (NumericUpDown)d;
        control.CoerceValue(ValueProperty);
    }
    
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        
        var increaseButton = GetTemplateChild("PART_IncreaseButton") as Button;
        var decreaseButton = GetTemplateChild("PART_DecreaseButton") as Button;
        var textBox = GetTemplateChild("PART_TextBox") as TextBox;
        
        if (increaseButton != null)
            increaseButton.Click += IncreaseButton_Click;
        
        if (decreaseButton != null)
            decreaseButton.Click += DecreaseButton_Click;
        
        if (textBox != null)
        {
            textBox.TextChanged += TextBox_TextChanged;
            textBox.LostFocus += TextBox_LostFocus;
            textBox.KeyDown += TextBox_KeyDown;
            
            // 更新格式化显示
            UpdateTextBox(textBox);
        }
    }
    
    private void IncreaseButton_Click(object sender, RoutedEventArgs e)
    {
        if (IsReadOnly) return;
        Value = Math.Min(Value + Increment, Maximum);
    }
    
    private void DecreaseButton_Click(object sender, RoutedEventArgs e)
    {
        if (IsReadOnly) return;
        Value = Math.Max(Value - Increment, Minimum);
    }
    
    private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        var textBox = sender as TextBox;
        if (textBox == null || textBox.IsFocused) return;
        
        UpdateFromTextBox(textBox);
    }
    
    private void TextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        var textBox = sender as TextBox;
        if (textBox == null) return;
        
        UpdateFromTextBox(textBox);
        UpdateTextBox(textBox);
    }
    
    private void TextBox_KeyDown(object sender, KeyEventArgs e)
    {
        var textBox = sender as TextBox;
        if (textBox == null) return;
        
        if (e.Key == Key.Enter)
        {
            UpdateFromTextBox(textBox);
            UpdateTextBox(textBox);
            textBox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
        }
        else if (e.Key == Key.Up)
        {
            IncreaseButton_Click(sender, e);
        }
        else if (e.Key == Key.Down)
        {
            DecreaseButton_Click(sender, e);
        }
    }
    
    private void UpdateFromTextBox(TextBox textBox)
    {
        if (double.TryParse(textBox.Text, out double newValue))
        {
            Value = newValue;
        }
    }
    
    private void UpdateTextBox(TextBox textBox)
    {
        textBox.Text = Value.ToString($"F{DecimalPlaces}");
    }
}

xml

复制代码
<!-- Themes/Generic.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfControls.Controls">
    
    <Style TargetType="local:NumericUpDown">
        <Setter Property="Height" Value="30"/>
        <Setter Property="Width" Value="150"/>
        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
        <Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:NumericUpDown">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        
                        <!-- 输入框 -->
                        <TextBox x:Name="PART_TextBox" 
                                 Grid.Column="0"
                                 Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, 
                                        StringFormat=F{0}, UpdateSourceTrigger=PropertyChanged}"
                                 BorderThickness="{TemplateBinding BorderThickness}"
                                 BorderBrush="{TemplateBinding BorderBrush}"
                                 Background="{TemplateBinding Background}"
                                 Foreground="{TemplateBinding Foreground}"
                                 IsReadOnly="{TemplateBinding IsReadOnly}"
                                 VerticalContentAlignment="Center"
                                 Padding="5,0"/>
                        
                        <!-- 增减按钮区域 -->
                        <StackPanel Grid.Column="1" Orientation="Horizontal">
                            <RepeatButton x:Name="PART_IncreaseButton" 
                                          Content="▲" 
                                          Width="25"
                                          FontSize="10"
                                          IsEnabled="{TemplateBinding IsReadOnly, Converter={StaticResource NotConverter}}"/>
                            <RepeatButton x:Name="PART_DecreaseButton" 
                                          Content="▼" 
                                          Width="25"
                                          FontSize="10"
                                          IsEnabled="{TemplateBinding IsReadOnly, Converter={StaticResource NotConverter}}"/>
                        </StackPanel>
                    </Grid>
                    
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsEnabled" Value="False">
                            <Setter TargetName="PART_TextBox" Property="Opacity" Value="0.5"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

3.4 附加属性

3.4.1 什么是附加属性?

附加属性可以让父容器控制子元素的布局行为

csharp

复制代码
// Grid.Row、Grid.Column 是典型的附加属性
<Grid>
    <Button Grid.Row="0" Grid.Column="1"/>  <!-- 附加属性 -->
</Grid>

3.4.2 自定义附加属性

csharp

复制代码
// 示例:让任何控件都能显示水印
public static class WatermarkService
{
    // 附加属性:水印文本
    public static readonly DependencyProperty WatermarkProperty =
        DependencyProperty.RegisterAttached("Watermark", typeof(string), typeof(WatermarkService),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits));
    
    // 附加属性:水印颜色
    public static readonly DependencyProperty WatermarkColorProperty =
        DependencyProperty.RegisterAttached("WatermarkColor", typeof(Brush), typeof(WatermarkService),
            new PropertyMetadata(Brushes.Gray));
    
    // 附加属性:是否显示水印
    public static readonly DependencyProperty IsWatermarkVisibleProperty =
        DependencyProperty.RegisterAttached("IsWatermarkVisible", typeof(bool), typeof(WatermarkService),
            new PropertyMetadata(false));
    
    public static void SetWatermark(DependencyObject obj, string value)
    {
        obj.SetValue(WatermarkProperty, value);
    }
    
    public static string GetWatermark(DependencyObject obj)
    {
        return (string)obj.GetValue(WatermarkProperty);
    }
    
    public static void SetWatermarkColor(DependencyObject obj, Brush value)
    {
        obj.SetValue(WatermarkColorProperty, value);
    }
    
    public static Brush GetWatermarkColor(DependencyObject obj)
    {
        return (Brush)obj.GetValue(WatermarkColorProperty);
    }
    
    public static void SetIsWatermarkVisible(DependencyObject obj, bool value)
    {
        obj.SetValue(IsWatermarkVisibleProperty, value);
    }
    
    public static bool GetIsWatermarkVisible(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsWatermarkVisibleProperty);
    }
}

xml

复制代码
<!-- 样式中使用附加属性 -->
<Style TargetType="TextBox">
    <Setter Property="local:WatermarkService.Watermark" Value="请输入..."/>
    <Setter Property="local:WatermarkService.WatermarkColor" Value="LightGray"/>
    
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="TextBox">
                <Grid>
                    <ScrollViewer x:Name="PART_ContentHost" />
                    <TextBlock x:Name="Watermark" 
                               Text="{Binding Path=(local:WatermarkService.Watermark), RelativeSource={RelativeSource TemplatedParent}}"
                               Foreground="{Binding Path=(local:WatermarkService.WatermarkColor), RelativeSource={RelativeSource TemplatedParent}}"
                               Opacity="0.5"
                               IsHitTestVisible="False"
                               Margin="5,0,0,0"
                               VerticalAlignment="Center"/>
                </Grid>
                
                <ControlTemplate.Triggers>
                    <Trigger Property="Text" Value="">
                        <Setter TargetName="Watermark" Property="Visibility" Value="Visible"/>
                    </Trigger>
                    <Trigger Property="Text" Value="{x:Null}">
                        <Setter TargetName="Watermark" Property="Visibility" Value="Visible"/>
                    </Trigger>
                    <Trigger Property="IsFocused" Value="True">
                        <Setter TargetName="Watermark" Property="Visibility" Value="Collapsed"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

3.5 行为(Behaviors)

3.5.1 什么是行为?

行为允许在 XAML 中给控件附加交互功能,而不需要创建子类

bash

复制代码
# 安装 Behaviors 包
dotnet add package Microsoft.Xaml.Behaviors.Wpf

3.5.2 自定义行为示例

csharp

复制代码
// Behaviors/DragInCanvasBehavior.cs
public class DragInCanvasBehavior : Behavior<UIElement>
{
    private Point _startPoint;
    private bool _isDragging;
    
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.MouseLeftButtonDown += OnMouseLeftButtonDown;
        AssociatedObject.MouseMove += OnMouseMove;
        AssociatedObject.MouseLeftButtonUp += OnMouseLeftButtonUp;
    }
    
    protected override void OnDetaching()
    {
        AssociatedObject.MouseLeftButtonDown -= OnMouseLeftButtonDown;
        AssociatedObject.MouseMove -= OnMouseMove;
        AssociatedObject.MouseLeftButtonUp -= OnMouseLeftButtonUp;
        base.OnDetaching();
    }
    
    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        var element = sender as FrameworkElement;
        if (element != null)
        {
            _startPoint = e.GetPosition(element.Parent as UIElement);
            _isDragging = true;
            element.CaptureMouse();
            element.Cursor = Cursors.Hand;
        }
    }
    
    private void OnMouseMove(object sender, MouseEventArgs e)
    {
        if (!_isDragging) return;
        
        var element = sender as FrameworkElement;
        if (element != null && element.Parent is Canvas canvas)
        {
            var currentPoint = e.GetPosition(canvas);
            var offset = currentPoint - _startPoint;
            
            double left = Canvas.GetLeft(element);
            double top = Canvas.GetTop(element);
            
            if (double.IsNaN(left)) left = 0;
            if (double.IsNaN(top)) top = 0;
            
            Canvas.SetLeft(element, left + offset.X);
            Canvas.SetTop(element, top + offset.Y);
            
            _startPoint = currentPoint;
        }
    }
    
    private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        var element = sender as FrameworkElement;
        if (element != null)
        {
            _isDragging = false;
            element.ReleaseMouseCapture();
            element.Cursor = null;
        }
    }
}

// Behaviors/TextBoxFilterBehavior.cs
public class TextBoxFilterBehavior : Behavior<TextBox>
{
    public static readonly DependencyProperty FilterTypeProperty =
        DependencyProperty.Register(nameof(FilterType), typeof(TextBoxFilter), typeof(TextBoxFilterBehavior),
            new PropertyMetadata(TextBoxFilter.None));
    
    public TextBoxFilter FilterType
    {
        get => (TextBoxFilter)GetValue(FilterTypeProperty);
        set => SetValue(FilterTypeProperty, value);
    }
    
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewTextInput += OnPreviewTextInput;
        AssociatedObject.PreviewKeyDown += OnPreviewKeyDown;
        DataObject.AddPastingHandler(AssociatedObject, OnPaste);
    }
    
    protected override void OnDetaching()
    {
        AssociatedObject.PreviewTextInput -= OnPreviewTextInput;
        AssociatedObject.PreviewKeyDown -= OnPreviewKeyDown;
        DataObject.RemovePastingHandler(AssociatedObject, OnPaste);
        base.OnDetaching();
    }
    
    private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        foreach (char c in e.Text)
        {
            if (!IsValidChar(c))
            {
                e.Handled = true;
                return;
            }
        }
    }
    
    private void OnPreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Space && !IsValidChar(' '))
        {
            e.Handled = true;
        }
    }
    
    private void OnPaste(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(DataFormats.Text))
        {
            string text = e.DataObject.GetData(DataFormats.Text) as string;
            if (text != null && !IsValidText(text))
            {
                e.CancelCommand();
            }
        }
    }
    
    private bool IsValidChar(char c)
    {
        return FilterType switch
        {
            TextBoxFilter.Numeric => char.IsDigit(c),
            TextBoxFilter.Integer => char.IsDigit(c) || c == '-',
            TextBoxFilter.Decimal => char.IsDigit(c) || c == '.' || c == '-',
            TextBoxFilter.Alphabetic => char.IsLetter(c) || c == ' ',
            TextBoxFilter.Alphanumeric => char.IsLetterOrDigit(c) || c == ' ',
            _ => true
        };
    }
    
    private bool IsValidText(string text)
    {
        return text.All(IsValidChar);
    }
}

public enum TextBoxFilter
{
    None,
    Numeric,
    Integer,
    Decimal,
    Alphabetic,
    Alphanumeric
}

xml

复制代码
<!-- 使用行为 -->
<Window xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:WpfControls.Behaviors">

    <Canvas Background="LightGray" Width="400" Height="300">
        <!-- 可拖拽的矩形 -->
        <Rectangle Width="50" Height="50" Fill="Red" Canvas.Left="100" Canvas.Top="100">
            <i:Interaction.Behaviors>
                <local:DragInCanvasBehavior/>
            </i:Interaction.Behaviors>
        </Rectangle>
    </Canvas>
    
    <!-- 只允许输入数字的文本框 -->
    <TextBox>
        <i:Interaction.Behaviors>
            <local:TextBoxFilterBehavior FilterType="Numeric"/>
        </i:Interaction.Behaviors>
    </TextBox>
</Window>

3.6 资源字典

3.6.1 创建资源字典

xml

复制代码
<!-- Styles/Brushes.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    <!-- 颜色资源 -->
    <SolidColorBrush x:Key="PrimaryBrush" Color="#2196F3"/>
    <SolidColorBrush x:Key="PrimaryDarkBrush" Color="#1976D2"/>
    <SolidColorBrush x:Key="PrimaryLightBrush" Color="#BBDEFB"/>
    <SolidColorBrush x:Key="AccentBrush" Color="#FF4081"/>
    <SolidColorBrush x:Key="SuccessBrush" Color="#4CAF50"/>
    <SolidColorBrush x:Key="WarningBrush" Color="#FF9800"/>
    <SolidColorBrush x:Key="ErrorBrush" Color="#F44336"/>
    
    <!-- 渐变画刷 -->
    <LinearGradientBrush x:Key="PrimaryGradient" StartPoint="0,0" EndPoint="1,1">
        <GradientStop Color="#2196F3" Offset="0"/>
        <GradientStop Color="#1976D2" Offset="1"/>
    </LinearGradientBrush>
</ResourceDictionary>

xml

复制代码
<!-- Styles/ButtonStyles.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Brushes.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    
    <!-- 主要按钮样式 -->
    <Style x:Key="PrimaryButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="FontSize" Value="14"/>
        <Setter Property="FontWeight" Value="SemiBold"/>
        <Setter Property="Padding" Value="15,8"/>
        <Setter Property="BorderThickness" Value="0"/>
        <Setter Property="Cursor" Value="Hand"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}" 
                            CornerRadius="4">
                        <ContentPresenter HorizontalAlignment="Center" 
                                          VerticalAlignment="Center"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="{StaticResource PrimaryDarkBrush}"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="RenderTransform">
                    <Setter.Value>
                        <ScaleTransform ScaleX="0.98" ScaleY="0.98"/>
                    </Setter.Value>
                </Setter>
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Opacity" Value="0.6"/>
            </Trigger>
        </Style.Triggers>
    </Style>
    
    <!-- 次要按钮样式 -->
    <Style x:Key="SecondaryButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
        <Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}"/>
        <Setter Property="BorderThickness" Value="1"/>
    </Style>
</ResourceDictionary>

3.6.2 合并资源字典

xml

复制代码
<!-- App.xaml -->
<Application x:Class="WpfControls.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <!-- 全局样式 -->
                <ResourceDictionary Source="Styles/Brushes.xaml"/>
                <ResourceDictionary Source="Styles/ButtonStyles.xaml"/>
                <ResourceDictionary Source="Styles/TextBoxStyles.xaml"/>
                <ResourceDictionary Source="Styles/DataGridStyles.xaml"/>
                
                <!-- 主题(可在运行时切换)-->
                <ResourceDictionary x:Name="ThemeDictionary" Source="Themes/LightTheme.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            
            <!-- 隐式样式:自动应用于所有 Button -->
            <Style TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}"/>
            
            <!-- 全局资源 -->
            <SolidColorBrush x:Key="GlobalForeground" Color="#333333"/>
        </ResourceDictionary>
    </Application.Resources>
</Application>

3.6.3 动态切换主题

csharp

复制代码
// ThemeManager.cs
public static class ThemeManager
{
    public static event EventHandler<string> ThemeChanged;
    
    private static string _currentTheme = "Light";
    
    public static string CurrentTheme
    {
        get => _currentTheme;
        set
        {
            if (_currentTheme != value)
            {
                _currentTheme = value;
                ApplyTheme(value);
                ThemeChanged?.Invoke(null, value);
            }
        }
    }
    
    private static void ApplyTheme(string themeName)
    {
        var newTheme = new ResourceDictionary
        {
            Source = new Uri($"Themes/{themeName}Theme.xaml", UriKind.Relative)
        };
        
        var app = Application.Current;
        var oldTheme = app.Resources.MergedDictionaries
            .FirstOrDefault(rd => rd.Source?.OriginalString?.Contains("Theme") == true);
        
        if (oldTheme != null)
        {
            var index = app.Resources.MergedDictionaries.IndexOf(oldTheme);
            app.Resources.MergedDictionaries[index] = newTheme;
        }
        else
        {
            app.Resources.MergedDictionaries.Add(newTheme);
        }
    }
}

3.7 动画

3.7.1 基础动画

xml

复制代码
<!-- 简单动画 -->
<Button Content="淡入动画">
    <Button.Triggers>
        <EventTrigger RoutedEvent="Button.Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                     From="0" To="1" Duration="0:0:1"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Button.Triggers>
</Button>

3.7.2 多种动画类型

xml

复制代码
<Window.Resources>
    <!-- 颜色动画 -->
    <Storyboard x:Key="ColorAnimation">
        <ColorAnimation Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
                        From="LightBlue" To="DarkBlue" Duration="0:0:0.5"
                        AutoReverse="True" RepeatBehavior="Forever"/>
    </Storyboard>
    
    <!-- 缩放动画 -->
    <Storyboard x:Key="ScaleAnimation">
        <DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(ScaleTransform.ScaleX)"
                        From="1" To="1.2" Duration="0:0:0.3"
                        AutoReverse="True"/>
        <DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(ScaleTransform.ScaleY)"
                        From="1" To="1.2" Duration="0:0:0.3"
                        AutoReverse="True"/>
    </Storyboard>
    
    <!-- 旋转动画 -->
    <Storyboard x:Key="RotateAnimation">
        <DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(RotateTransform.Angle)"
                        From="0" To="360" Duration="0:0:2"
                        RepeatBehavior="Forever"/>
    </Storyboard>
    
    <!-- 位移动画 -->
    <Storyboard x:Key="MoveAnimation">
        <DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(TranslateTransform.X)"
                        From="0" To="100" Duration="0:0:1"
                        AutoReverse="True" RepeatBehavior="Forever"/>
    </Storyboard>
</Window.Resources>

<StackPanel>
    <Button Content="颜色动画" Width="120" Height="40" Margin="5">
        <Button.RenderTransform>
            <RotateTransform/>
        </Button.RenderTransform>
        <Button.Triggers>
            <EventTrigger RoutedEvent="Button.MouseEnter">
                <BeginStoryboard Storyboard="{StaticResource ColorAnimation}"/>
            </EventTrigger>
        </Button.Triggers>
    </Button>
</StackPanel>

3.7.3 关键帧动画

xml

复制代码
<Storyboard x:Key="BounceAnimation">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Button.RenderTransform).(TranslateTransform.Y)"
                                   RepeatBehavior="Forever">
        <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
        <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="-30">
            <EasingDoubleKeyFrame.EasingFunction>
                <QuadraticEase EasingMode="EaseOut"/>
            </EasingDoubleKeyFrame.EasingFunction>
        </EasingDoubleKeyFrame>
        <EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="0">
            <EasingDoubleKeyFrame.EasingFunction>
                <BounceEase Bounces="2" Bounciness="2"/>
            </EasingDoubleKeyFrame.EasingFunction>
        </EasingDoubleKeyFrame>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

3.8 综合示例:自定义控件库

csharp

复制代码
// Controls/ToggleSwitch.cs
[TemplatePart(Name = "PART_Track", Type = typeof(Border))]
[TemplatePart(Name = "PART_Thumb", Type = typeof(Thumb))]
public class ToggleSwitch : Control
{
    public static readonly DependencyProperty IsCheckedProperty =
        DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(ToggleSwitch),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnIsCheckedChanged));
    
    public static readonly DependencyProperty CheckedContentProperty =
        DependencyProperty.Register(nameof(CheckedContent), typeof(object), typeof(ToggleSwitch),
            new PropertyMetadata("ON"));
    
    public static readonly DependencyProperty UncheckedContentProperty =
        DependencyProperty.Register(nameof(UncheckedContent), typeof(object), typeof(ToggleSwitch),
            new PropertyMetadata("OFF"));
    
    public static readonly DependencyProperty CheckedBrushProperty =
        DependencyProperty.Register(nameof(CheckedBrush), typeof(Brush), typeof(ToggleSwitch),
            new PropertyMetadata(Brushes.Green));
    
    public static readonly DependencyProperty UncheckedBrushProperty =
        DependencyProperty.Register(nameof(UncheckedBrush), typeof(Brush), typeof(ToggleSwitch),
            new PropertyMetadata(Brushes.Gray));
    
    static ToggleSwitch()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ToggleSwitch),
            new FrameworkPropertyMetadata(typeof(ToggleSwitch)));
    }
    
    public bool IsChecked
    {
        get => (bool)GetValue(IsCheckedProperty);
        set => SetValue(IsCheckedProperty, value);
    }
    
    public object CheckedContent
    {
        get => GetValue(CheckedContentProperty);
        set => SetValue(CheckedContentProperty, value);
    }
    
    public object UncheckedContent
    {
        get => GetValue(UncheckedContentProperty);
        set => SetValue(UncheckedContentProperty, value);
    }
    
    public Brush CheckedBrush
    {
        get => (Brush)GetValue(CheckedBrushProperty);
        set => SetValue(CheckedBrushProperty, value);
    }
    
    public Brush UncheckedBrush
    {
        get => (Brush)GetValue(UncheckedBrushProperty);
        set => SetValue(UncheckedBrushProperty, value);
    }
    
    private Border _track;
    private Thumb _thumb;
    
    private static void OnIsCheckedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (ToggleSwitch)d;
        control.UpdateVisualState((bool)e.NewValue);
    }
    
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        
        _track = GetTemplateChild("PART_Track") as Border;
        _thumb = GetTemplateChild("PART_Thumb") as Thumb;
        
        if (_thumb != null)
        {
            _thumb.DragCompleted += Thumb_DragCompleted;
        }
        
        UpdateVisualState(IsChecked);
    }
    
    private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
    {
        IsChecked = !IsChecked;
    }
    
    private void UpdateVisualState(bool isChecked)
    {
        if (_track != null)
        {
            _track.Background = isChecked ? CheckedBrush : UncheckedBrush;
        }
        
        if (_thumb != null)
        {
            var margin = _thumb.Margin;
            if (isChecked)
            {
                var trackWidth = _track?.ActualWidth ?? 100;
                var thumbWidth = _thumb.ActualWidth;
                margin.Left = trackWidth - thumbWidth - margin.Right;
            }
            else
            {
                margin.Left = 0;
            }
            _thumb.Margin = margin;
        }
    }
}

xml

复制代码
<!-- Themes/Generic.xaml(添加到现有文件)-->
<Style TargetType="local:ToggleSwitch">
    <Setter Property="Height" Value="30"/>
    <Setter Property="Width" Value="80"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:ToggleSwitch">
                <Border x:Name="PART_Track" 
                        Background="{TemplateBinding UncheckedBrush}"
                        CornerRadius="15">
                    <Grid>
                        <!-- 状态文本 -->
                        <TextBlock x:Name="StatusText"
                                   Text="{TemplateBinding UncheckedContent}"
                                   HorizontalAlignment="Center"
                                   VerticalAlignment="Center"
                                   Foreground="White"
                                   FontWeight="Bold"/>
                        
                        <!-- 滑块 -->
                        <Thumb x:Name="PART_Thumb" 
                               Width="25" Height="25"
                               Cursor="Hand">
                            <Thumb.Template>
                                <ControlTemplate TargetType="Thumb">
                                    <Border Background="White" CornerRadius="12.5">
                                        <Border.Effect>
                                            <DropShadowEffect BlurRadius="3" ShadowDepth="1"/>
                                        </Border.Effect>
                                    </Border>
                                </ControlTemplate>
                            </Thumb.Template>
                        </Thumb>
                    </Grid>
                </Border>
                
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter TargetName="StatusText" Property="Text" 
                                Value="{Binding CheckedContent, RelativeSource={RelativeSource TemplatedParent}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

3.9 常见错误与陷阱

错误1:ControlTemplate 中忘记设置 TemplateBinding

xml

复制代码
<!-- ❌ 错误:硬编码值,样式无法覆盖 -->
<ControlTemplate TargetType="Button">
    <Border Background="LightBlue" />  <!-- 固定颜色 -->
</ControlTemplate>

<!-- ✅ 正确:使用 TemplateBinding -->
<ControlTemplate TargetType="Button">
    <Border Background="{TemplateBinding Background}" />
</ControlTemplate>

错误2:DataTemplate 中绑定路径错误

xml

复制代码
<!-- ❌ 错误:没有指定数据上下文 -->
<DataTemplate>
    <TextBlock Text="{Binding Name}"/>  <!-- 绑定的是 DataContext,不是数据项 -->
</DataTemplate>

<!-- ✅ 正确:在 ItemsControl 的 ItemTemplate 中 -->
<ListBox ItemsSource="{Binding Users}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Name}"/>  <!-- 绑定 User 对象的 Name -->
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

错误3:自定义控件中忘记注册 DefaultStyleKey

csharp

复制代码
// ❌ 错误
static NumericUpDown()
{
    // 没有注册,控件会使用默认样式
}

// ✅ 正确
static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
        new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
相关推荐
SoftLipaRZC1 小时前
C语言自定义类型:结构体完全指南
c语言·开发语言
方也_arkling1 小时前
【Java-Day19】集合3 List中常见的方法和5种遍历方式
java·开发语言
AI玫瑰助手1 小时前
Python函数:局部变量与全局变量的作用域
开发语言·python·信息可视化
字节高级特工1 小时前
C++11(二) 革新:引用折叠与lambda表达式
java·开发语言·c++·算法
萨小耶1 小时前
[Java学习日记11】聊聊深拷贝和浅拷贝
java·开发语言·学习
xiaoshuaishuai81 小时前
C# AvaloniaUI‌的IValueConverter
开发语言·c#
白驹笙鸣2 小时前
STL allocator作用
开发语言·c++
小小编程路2 小时前
C++ STL 原理与性能
开发语言·c++
码不停蹄的玄黓2 小时前
Java线程池生命周期
java·开发语言