65.9K
CodeProject 正在变化。 阅读更多。
Home

脉冲按钮 (WPF)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (17投票s)

2015年6月1日

CPOL

2分钟阅读

viewsIcon

61895

downloadIcon

2618

矩形或椭圆形自定义按钮,会发出脉冲。

引言

本文演示了如何在 .NET 4.5 WPF (C#) 中制作一个发出脉冲的按钮。
也可以作为 NuGet 包使用。

背景

我在 2009 年使用 WinForms 在 .NET 2.0 中制作了一个类似的控件,并且一段时间以来,我一直想在 WPF 中做同样的事情。
在解决了控件结构化的初步挑战后,一切都很顺利,并且我设法添加了一些附加功能。

按钮布局

基本按钮布局,如下所示

Layout

该控件是使用渐变椭圆作为基础,并带有半透明笔触作为边框构建的。
反射被创建为按钮顶部的第二个椭圆,其填充区域由半透明白色区域周围的大边界框指示。
文本只是顶部的内容呈现器。

脉冲是放置在按钮下方的动画椭圆。

这是产生控件的标记 (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 初始版本
© . All rights reserved.