脉冲按钮 (WPF)






4.81/5 (17投票s)
矩形或椭圆形自定义按钮,会发出脉冲。
引言
本文演示了如何在 .NET 4.5 WPF (C#) 中制作一个发出脉冲的按钮。
也可以作为 NuGet 包使用。
背景
我在 2009 年使用 WinForms 在 .NET 2.0 中制作了一个类似的控件,并且一段时间以来,我一直想在 WPF 中做同样的事情。
在解决了控件结构化的初步挑战后,一切都很顺利,并且我设法添加了一些附加功能。
按钮布局
基本按钮布局,如下所示
该控件是使用渐变椭圆作为基础,并带有半透明笔触作为边框构建的。
反射被创建为按钮顶部的第二个椭圆,其填充区域由半透明白色区域周围的大边界框指示。
文本只是顶部的内容呈现器。
脉冲是放置在按钮下方的动画椭圆。
这是产生控件的标记 (XAML)
<Grid x:Name="PART_body" Background="{TemplateBinding Background}">
<!-- Pulse Container -->
<Grid x:Name="PART_pulse_container" />
<!-- Button -->
<Ellipse x:Name="PART_button" Stroke="#60000000" StrokeThickness="2"
Fill="{TemplateBinding ButtonBrush}"/>
<!-- Focus visual -->
<Ellipse x:Name="PART_focus_visual" IsHitTestVisible="False"
Stroke="{TemplateBinding ButtonHighlightBrush}"
StrokeThickness="2"
StrokeDashArray="1 2"
Fill="Transparent" Margin="2"
Visibility="{TemplateBinding IsFocused,
Converter={StaticResource BoolToVisibilityConverter}}" />
<!-- Reflex -->
<Ellipse x:Name="PART_reflex" IsHitTestVisible="False"
Visibility="{TemplateBinding IsReflective,
Converter={StaticResource BoolToVisibilityConverter}}">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="2.6" RadiusY="2.05" Center="0.5,-1.5"
GradientOrigin="0.5,-1.5">
<RadialGradientBrush.GradientStops>
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="#60FFFFFF" Offset="0.4"/>
<GradientStop Color="#30FFFFFF" Offset="0.995"/>
<GradientStop Color="#00FFFFFF" Offset="1"/>
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!-- Content presenter -->
<ContentPresenter IsHitTestVisible="False"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
按钮状态
按钮状态如下所示(除了脉冲和焦点高亮显示)
这些状态主要使用 ControlTemplate
触发器设置。处理触发器的标记 (XAML) 如下所示
<!-- ControlTemplate Triggers -->
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True" SourceName="PART_button">
<Setter Property="Fill" TargetName="PART_button"
Value="{Binding Path=ButtonHighlightBrush,
RelativeSource={RelativeSource AncestorType={x:Type local:PulseButton}}}" />
<Setter Property="Stroke" TargetName="PART_focus_visual" Value="Black" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="PART_button"
Value="{Binding Path=ButtonPressedBrush,
RelativeSource={RelativeSource AncestorType={x:Type local:PulseButton}}}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Fill" TargetName="PART_button"
Value="{Binding Path=ButtonDisabledBrush,
RelativeSource={RelativeSource AncestorType={x:Type local:PulseButton}}}" />
<Setter Property="Foreground" Value="DimGray" />
<Setter Property="IsPulsing" Value="False" />
<Setter Property="Visibility" TargetName="PART_reflex" Value="Hidden" />
</Trigger>
</ControlTemplate.Triggers>
代码
按钮由一个带有相应 Style
的单个按钮 class
组成。 PulseButton
类的属性和方法如下所示
相应的 Style
如下所示
<!-- PulseButton Style -->
<Style TargetType="{x:Type local:PulseButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="ClipToBounds" Value="False"/>
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="IsReflective" Value="True"></Setter>
<Setter Property="Template" Value="{StaticResource RectangleTemplate}" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Style.Triggers>
<Trigger Property="IsEllipsis" Value="True">
<Setter Property="Template" Value="{StaticResource EllipseTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
该样式具有一个触发器,该触发器将引用不同的模板,具体取决于属性 IsEllipsis
。
将 IsEllipsis
设置为 true
将允许控件呈现椭圆而不是矩形。
该样式在 PulseButton
控件的静态构造函数中引用
static PulseButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PulseButton),
new FrameworkPropertyMetadata(typeof(PulseButton)));
}
可以直接在构造函数中引用该样式,但上面的代码允许您使用 BasedOn
属性更改样式。
设置脉冲
脉冲是具有缩放和不透明度动画的形状。
对于影响脉冲的每个属性,将调用方法 PulsesChanged
。
该方法将重新计算脉冲并设置动画,请参见下面的方法
/// <summary>
/// Pulses changed.
/// </summary>
/// <param name="d">The d.</param>
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/>
/// instance containing the event data.</param>
private static void PulsesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var pb = (PulseButton)d;
if (pb == null || pb.partPulseContainer == null || !pb.IsPulsing) return;
// Clear all pulses
pb.partPulseContainer.Children.Clear();
var items = pb.Pulses;
// Add pulses
for (var i = 0; i < items; i++)
{
var shape = pb.IsEllipsis ?
(Shape)new Ellipse
{
StrokeThickness = pb.PulseWidth,
Stroke = pb.PulseColor,
RenderTransformOrigin = new Point(0.5, 0.5)
} :
new Rectangle
{
RadiusX = pb.RadiusX,
RadiusY = pb.RadiusY,
StrokeThickness = pb.PulseWidth,
Stroke = pb.PulseColor,
RenderTransformOrigin = new Point(0.5, 0.5)
};
pb.partPulseContainer.Children.Add(shape);
}
// Set animations
pb.SetStoryBoard(pb);
}
每个单独脉冲的动画在方法 SetStoryBoard
中完成
/// <summary>
/// Sets the story board for the pulses
/// </summary>
/// <param name="pb">The pb.</param>
private void SetStoryBoard(PulseButton pb)
{
double delay = 0;
// Correct PulseScale according to control dimensions
double correctedFactorX = pb.PulseScale, correctedFactorY = pb.PulseScale;
if (pb.IsMeasureValid)
{
if (pb.ActualHeight < pb.ActualWidth)
correctedFactorY = (pb.PulseScale - 1) * ((pb.ActualWidth - pb.ActualHeight) /
(1 + pb.ActualHeight)) + pb.PulseScale;
else
correctedFactorX = (pb.PulseScale - 1) * ((pb.ActualHeight - pb.ActualWidth) /
(1 + pb.ActualWidth)) + pb.PulseScale;
}
// Add pulses
foreach (Shape shape in pb.partPulseContainer.Children)
{
shape.RenderTransform = new ScaleTransform();
// X-axis animation
var animation = new DoubleAnimation(1, correctedFactorX, pb.PulseSpeed)
{
RepeatBehavior = RepeatBehavior.Forever,
AutoReverse = false,
BeginTime = TimeSpan.FromMilliseconds(delay),
EasingFunction = pb.PulseEasing
};
// Y-axis animation
var animation2 = new DoubleAnimation(1, correctedFactorY, pb.PulseSpeed)
{
RepeatBehavior = RepeatBehavior.Forever,
AutoReverse = false,
BeginTime = TimeSpan.FromMilliseconds(delay),
EasingFunction = pb.PulseEasing
};
// Opacity animation
var animation3 = new DoubleAnimation(1, 0, pb.PulseSpeed)
{
RepeatBehavior = RepeatBehavior.Forever,
AutoReverse = false,
//EasingFunction = new QuarticEase { EasingMode = EasingMode.EaseIn },
BeginTime = TimeSpan.FromMilliseconds(delay)
};
// Set delay between pulses
delay += pb.PulseSpeed.TimeSpan.TotalMilliseconds / pb.Pulses;
// Create storyboard
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
storyboard.Children.Add(animation2);
storyboard.Children.Add(animation3);
Storyboard.SetTarget(animation, shape);
Storyboard.SetTarget(animation2, shape);
Storyboard.SetTarget(animation3, shape);
if (pb.IsEllipsis)
{
Storyboard.SetTargetProperty(animation,
new PropertyPath("(Ellipse.RenderTransform).(ScaleTransform.ScaleX)"));
Storyboard.SetTargetProperty(animation2,
new PropertyPath("(Ellipse.RenderTransform).(ScaleTransform.ScaleY)"));
Storyboard.SetTargetProperty(animation3,
new PropertyPath("(Ellipse.Opacity)"));
}
else
{
Storyboard.SetTargetProperty(animation,
new PropertyPath("(Rectangle.RenderTransform).(ScaleTransform.ScaleX)"));
Storyboard.SetTargetProperty(animation2,
new PropertyPath("(Rectangle.RenderTransform).(ScaleTransform.ScaleY)"));
Storyboard.SetTargetProperty(animation3,
new PropertyPath("(Rectangle.Opacity)"));
}
// Start storyboard
storyboard.Begin();
}
需要 correctedFactor
才能使脉冲从控件均匀散开。
用法
以下是一些关于如何使用该控件的示例。
<!-- START button -->
<controls:PulseButton Margin="30"
IsEllipsis="True"
FontSize="20"
Pulses="3"
PulseScale="1.5"
PulseSpeed="0:0:3"
PulseWidth="2"
Content="START"
ButtonBrush="{StaticResource RedButtonBrush}"
ButtonHighlightBrush="{StaticResource ButtonHighlightBrush}"
ButtonPressedBrush="{StaticResource RedButtonPressedBrush}"
Foreground="White"/>
<!-- Default button -->
<controls:PulseButton IsEllipsis="False" Margin="30,30,53,30"
RadiusX="20"
RadiusY="20"
Content="Default" />
请记住像这样在 App.xaml 中添加对该控件的引用
<Application x:Class="PulseControlTest.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/PulseButton;component/Themes/Generic.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
附加功能
可以将 EasingFunction
添加到控件。有关可能的缓动函数,请参见 这里。
下面的示例显示了如何将 QuadraticEase
添加到 PulseEasing
属性
<!-- Easing in -->
<controls:PulseButton IsEllipsis="True" Margin="30"
PulseScale="1.7"
PulseWidth="1"
PulseSpeed="0:0:5"
PulseColor="Teal"
Pulses="10"
Content="Easing in"
IsReflective="True"
ButtonBrush="MidnightBlue"
ButtonHighlightBrush="Blue"
ButtonPressedBrush="Green"
Foreground="White"
PulseEasing="{StaticResource EasingIn}"
其中 EasingIn
放置在窗口或应用程序的资源中
<!-- Easing functions -->
<QuadraticEase x:Key="EasingIn" EasingMode="EaseIn" />
该示例看起来像这样,其中脉冲向边缘加速。
历史
- 1.0.3 错误修复
- 1.0.2 样式已更改,在
Grid
上将IsHitTestVisible
设置为false
- 1.0.1 在脉冲上将属性
IsHitTestVisible
设置为false
(NuGet 也已更新) - 1.0.0 初始版本