自定义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
,通过重写OnClick
和OnToggle
方法,实现自定义点击行为。特别地,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
,实现动画进度的绑定与更新。动画时钟控制动画的进度,并在每次进度更新时请求重绘。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗