自定义ToggleButton动画与远程控制的实现

本文档解析了一段实现自定义ToggleButton动画与样式的代码,以及通过远程信号控制ToggleButton的示例。这段代码展示了如何使用WPF中的XAML和C#结合动画、样式和事件触发器来实现交互式控件,ToggleButton主要用来关联远程通信的状态,使用RemoteToggleButton取消OnToggle的状态变化,防止通信状态更新前的状态跳变提升用户体验。以下是详细的解析与实现笔记。

1. 资源字典与样式定义

资源字典(ResourceDictionary)

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="WithSateHoldToggle" TargetType="ToggleButton">
        <Setter Property="MinWidth" Value="60" />
        <Setter Property="MinHeight" Value="40" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ToggleButton">
                    <Border
                        x:Name="PART_border"
                        Background="{TemplateBinding Background}"
                        BorderBrush="Black"
                        BorderThickness="1">
                        <ContentPresenter
                            x:Name="PART_Presenter"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center" />
                    </Border>
                    <ControlTemplate.Triggers>
                        <EventTrigger RoutedEvent="PreviewMouseLeftButtonDown">
                            <BeginStoryboard x:Name="Storyboard_Down">
                                <Storyboard>
                                    <ColorAnimation
                                        Storyboard.TargetName="PART_border"
                                        Storyboard.TargetProperty="Background.Color"
                                        To="#1b6aa5"
                                        Duration="0:0:0.2" />
                                    <ColorAnimation
                                        Storyboard.TargetProperty="Foreground.Color"
                                        To="White"
                                        Duration="0:0:0.2" />
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="FontWeight">
                                        <DiscreteObjectKeyFrame KeyTime="0:0:0.1">
                                            <DiscreteObjectKeyFrame.Value>
                                                <FontWeight>Bold</FontWeight>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                        <EventTrigger RoutedEvent="PreviewMouseLeftButtonUp">
                            <BeginStoryboard x:Name="Storyboard_Up">
                                <Storyboard>
                                    <ColorAnimation
                                        Storyboard.TargetName="PART_border"
                                        Storyboard.TargetProperty="Background.Color"
                                        To="{x:Null}"
                                        Duration="0:0:0.2" />
                                    <ColorAnimation
                                        Storyboard.TargetProperty="Foreground.Color"
                                        To="{x:Null}"
                                        Duration="0:0:0.2" />
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="FontWeight">
                                        <DiscreteObjectKeyFrame KeyTime="0:0:0.1">
                                            <DiscreteObjectKeyFrame.Value>
                                                <FontWeight>Normal</FontWeight>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                        <Trigger Property="IsChecked" Value="True">
                            <Setter Property="Background" Value="#1b6aa5" />
                            <Setter Property="Foreground" Value="White" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

该资源字典定义了一个名为WithSateHoldToggle的样式,应用于ToggleButton控件。主要特性包括:

  • 设置控件的最小宽度和高度。
  • 定义控件模板,包含一个边框和内容呈现器。
  • 使用事件触发器实现点击动画效果。
  • 使用触发器在ToggleButton被选中时改变背景色和前景色。

事件触发器与动画

事件触发器实现了ToggleButton在鼠标左键按下和抬起时的动画效果。具体动画包括背景颜色、前景颜色和字体粗细的变化。

2. 远程控制ToggleButton

自定义控件RemoteToggleButton

public class RemoteToggleButton: ToggleButton
{
    protected override void OnClick()
    {
        base.OnClick();
    }

    protected override void OnToggle()
    {
        // Do nothing
    }
}

RemoteToggleButton继承自ToggleButton,通过重写OnClickOnToggle方法,实现自定义点击行为。特别地,OnToggle方法被重写为空,以禁用默认的切换行为,防止通信状态更新前的状态跳变提升用户体验。

数据模板与绑定

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <control:RemoteToggleButton
            Margin="2,0"
            Checked="ToggleButton_Checked"
            Command="{Binding DataContext.JogWithStateHoldCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
            CommandParameter="{Binding}"
            Content="{Binding DisplayName}"
            IsChecked="{Binding Status}"
            Loaded="ToggleButton_Loaded"
            Style="{StaticResource WithSateHoldToggle}"
            Unchecked="ToggleButton_Unchecked" />
    </DataTemplate>
</ItemsControl.ItemTemplate>

数据模板定义了RemoteToggleButton的绑定属性,包括命令、命令参数、内容、状态等。使用Style属性将先前定义的样式应用到RemoteToggleButton

3. ManualControl 用户控件

事件处理

public partial class ManualControl : UserControl
{
    public ManualControl()
    {
        InitializeComponent();
    }

    private void ToggleButton_Checked(object sender, RoutedEventArgs e)
    {
        var btn = sender as ToggleButton;
        var layter = AdornerLayer.GetAdornerLayer(btn);
        if (layter == null) return;
        var adorners = layter.GetAdorners(btn);
        if (adorners != null)
        {
            ToggleButtonAdorner adorner = adorners.OfType<ToggleButtonAdorner>().FirstOrDefault();
            if (adorner != null) layter.Remove(adorner);
        }
        layter.Add(new ToggleButtonAdorner(btn, "ON", "OFF"));
    }

    private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
    {
        var btn = sender as ToggleButton;
        var layter = AdornerLayer.GetAdornerLayer(btn);
        if (layter == null) return;
        var adorners = layter.GetAdorners(btn);
        if (adorners != null)
        {
            ToggleButtonAdorner adorner = adorners.OfType<ToggleButtonAdorner>().FirstOrDefault();
            if (adorner != null) layter.Remove(adorner);
        }
        layter.Add(new ToggleButtonAdorner(btn, "ON", "OFF"));
    }

    private void ToggleButton_Loaded(object sender, RoutedEventArgs e)
    {
        var btn = sender as ToggleButton;
        var layter = AdornerLayer.GetAdornerLayer(btn);
        if (layter == null) return;
        var adorners = layter.GetAdorners(btn);
        if (adorners != null)
        {
            ToggleButtonAdorner adorner = adorners.OfType<ToggleButtonAdorner>().FirstOrDefault();
            if (adorner != null) layter.Remove(adorner);
        }
        layter.Add(new ToggleButtonAdorner(btn, "ON", "OFF"));
    }
}

ManualControl类实现了ToggleButton的加载、选中和取消选中的事件处理。在这些事件中,使用AdornerLayer动态添加或移除自定义的ToggleButtonAdorner装饰器。

4. 自定义装饰器ToggleButtonAdorner

类定义与动画实现

public class ToggleButtonAdorner : Adorner
{
    private double _animationProgress;
    private AnimationClock _animationClock;

    private readonly string _checkedStr;
    private readonly string _uncheckedStr;
    private readonly ToggleButton _toggleBtn;

    public ToggleButtonAdorner(ToggleButton toggleBtn, string checkedStr, string uncheckedStr) : base(toggleBtn)
    {
        _toggleBtn = toggleBtn;
        _checkedStr = checkedStr;
        _uncheckedStr = uncheckedStr;
        StartAnimation();
    }

    private void StartAnimation()
    {
        DoubleAnimation animation = new DoubleAnimation(0, 1, new Duration(TimeSpan.FromSeconds(2)));
        animation.RepeatBehavior = RepeatBehavior.Forever;
        animation.AutoReverse = false;

        _animationClock = animation.CreateClock();
        _animationClock.CurrentTimeInvalidated += (s, e) =>
        {
            if (_animationClock.CurrentProgress.HasValue)
            {
                _animationProgress = _animationClock.CurrentProgress.Value;
                InvalidateVisual();
            }
            ApplyAnimationClock(AnimationProgressProperty, _animationClock);
        };
    }

    public double AnimationProgress
    {
        get { return (double)GetValue(AnimationProgressProperty); }
        set { SetValue(AnimationProgressProperty, value); }
    }
    public static readonly DependencyProperty AnimationProgressProperty =
        DependencyProperty.Register("AnimationProgress", typeof(double), typeof(ToggleButtonAdorner), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        Rect adornedElementRect = new Rect(this.AdornedElement.RenderSize);

        if (_toggleBtn.IsChecked.Value)
        {
            double dashOffset = -adornedElementRect.Width * _animationProgress;
            Pen pen = new Pen(Brushes.LawnGreen, 2);
            FormattedText ft = new FormattedText(_checkedStr,
                CultureInfo.CurrentCulture,
                FlowDirection.RightToLeft,
                new Typeface(new FontFamily("微软雅黑"), FontStyles.Normal, FontWeights.Bold, FontStretches.Normal),
                8,
                Brushes.White,
                96);
            drawingContext.DrawText(ft, new Point(adornedElementRect.TopRight.X - 2, adornedElementRect.TopRight.Y));
            pen.DashStyle = new DashStyle(new double[] { adornedElementRect.Width / 4, adornedElementRect.Width / 4 }, dashOffset);
            drawingContext.DrawRectangle(null, pen, adornedElementRect);
        }
        else
        {
            Pen pen = new Pen(new SolidColorBrush(Colors.Red), 2);
            drawingContext.DrawRectangle(null, pen, adornedElementRect);
            FormattedText ft = new FormattedText(_uncheckedStr,
                CultureInfo.CurrentCulture,
                FlowDirection.RightToLeft,
                new Typeface(new FontFamily("微软雅黑"), FontStyles.Normal, FontWeights.Bold, FontStretches.Normal),
                8,
                Brushes.Red,
                96);
            drawingContext.DrawText(ft, new Point(adornedElementRect.TopRight.X - 2, adornedElementRect.TopRight.Y));
        }
    }
}

ToggleButtonAdorner类继承自Adorner,实现了对ToggleButton的自定义装饰。主要功能包括:

  • 启动一个双动画,循环改变动画进度。
  • OnRender方法中,根据ToggleButton的选中状态绘制不同的边框和文本。

依赖属性与动画时钟

通过定义依赖属性AnimationProgressProperty,实现动画进度的绑定与更新。动画时钟控制动画的进度,并在每次进度更新时请求重绘。

posted @ 2024-06-03 09:35  非法关键字  阅读(10)  评论(0编辑  收藏  举报