Click here to Skip to main content
15,875,017 members
Articles / Desktop Programming / WPF

Pulse Button (WPF)

Rate me:
Please Sign up or sign in to vote.
4.81/5 (17 votes)
1 Jun 2015CPOL2 min read 57.6K   2.5K   25   33
Custom button as rectangle or ellipse that emits pulses

Introduction

This article demostrates how to make a button that emits pulses in .NET 4.5 WPF (C#).
Also available as NuGet package.

Image 1

Background

I made a similar control in .NET 2.0 in 2009 using WinForms and for some time, I wanted to make the same in WPF.
After cracking the initial challenge of structuring the control, everything went smoothly and I managed to add some additional features.

The Button Layout

The basic button layout, shown below:

Image 2

Layout

The control is built using a gradient ellipsis as basis with a semitransparent stroke as border.
The reflex is created as a second ellipsis on top of the button where its fill area is indicated with the large bounding box around the semitransparent white area.
The text is just a content presenter on top.

The pulses are animated ellipsis placed under the button.

Here is the markup (XAML) to produce the control:

XML
<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>

The Button States

The button states are shown below (except pulsing and highlight with focus):

Image 4

The states are mainly set using ControlTemplate triggers. The markup (XAML) for handling the triggers is shown below:

XML
<!-- 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>

The Code

The button consists of a single button class with a corresponding Style. The properties and methods for the PulseButton class are shown below:

Image 5

The corresponding Style is shown below:

XML
<!-- 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>

The style has a trigger that will reference different templates depending on the property IsEllipsis.
Setting the IsEllipsis to true will let the control render an ellipsis instead of a rectangle.

The style is referenced in the static constructor of the PulseButton control:

C#
static PulseButton()
 {
   DefaultStyleKeyProperty.OverrideMetadata(typeof(PulseButton),
               new FrameworkPropertyMetadata(typeof(PulseButton)));
 }

It is possible to reference the style directly in the constructor but the code above lets you change the style using the BasedOn property.

Setting Up the Pulses

The pulses are shape(s) with animations of the scale and the opacity.
For each of the properties that affect the pulses, the method PulsesChanged is called.
The method will recalculate the pulses and set up the animation, see the method below:

C#
/// <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);
}

The animations of each individual pulse is done in the method SetStoryBoard:

C#
/// <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();
  }

The correctedFactor is needed to let the pulses spread out from the control evenly.

Usage

Here are some samples on how to use the control.

XML
<!-- 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" />

Remember to add a reference to the control in the App.xaml like this:

XML
<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>

Additional Feature

It is possible to add an EasingFunction to the control. For possible easing functions, see here.

The example below shows how to add a QuadraticEase to the PulseEasing property:

XML
<!-- 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}"

Where EasingIn is placed in resources of the window or application:

XML
<!-- Easing functions -->
<QuadraticEase x:Key="EasingIn" EasingMode="EaseIn" />

The example looks like this, where the pulses accelerate towards the edges.

Image 6

History

  • 1.0.3 Bug fix
  • 1.0.2 The style is changed, IsHitTestVisible set to false on Grid
  • 1.0.1 The property IsHitTestVisible set to false on pulses (Nuget also updated)
  • 1.0.0 The initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Denmark Denmark
Name: Niel Morgan Thomas
Born: 1970 in Denmark
Education:
Dataengineer from Odense Technical University.
More than 20 years in IT-business.
Current employment:
Cloud architect at University College Lillebaelt

Comments and Discussions

 
QuestionPulse Button Pin
Member 1373273322-Apr-19 10:13
Member 1373273322-Apr-19 10:13 
Questionthx Pin
Burak Tunçbilek20-Jan-17 4:13
Burak Tunçbilek20-Jan-17 4:13 
SuggestionWow Pin
Imagiv28-Oct-16 9:17
Imagiv28-Oct-16 9:17 
GeneralRe: Wow Pin
Niel M.Thomas28-Oct-16 9:36
professionalNiel M.Thomas28-Oct-16 9:36 
QuestionPulse Button Click Not Working Pin
Syed Asad Jahangir14-Jun-16 9:44
Syed Asad Jahangir14-Jun-16 9:44 
AnswerRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir14-Jun-16 9:45
Syed Asad Jahangir14-Jun-16 9:45 
AnswerRe: Pulse Button Click Not Working Pin
Niel M.Thomas14-Jun-16 9:51
professionalNiel M.Thomas14-Jun-16 9:51 
GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir14-Jun-16 9:58
Syed Asad Jahangir14-Jun-16 9:58 
GeneralRe: Pulse Button Click Not Working Pin
Niel M.Thomas14-Jun-16 10:11
professionalNiel M.Thomas14-Jun-16 10:11 
GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir14-Jun-16 10:18
Syed Asad Jahangir14-Jun-16 10:18 
GeneralRe: Pulse Button Click Not Working Pin
Niel M.Thomas15-Jun-16 5:43
professionalNiel M.Thomas15-Jun-16 5:43 
GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir15-Jun-16 8:48
Syed Asad Jahangir15-Jun-16 8:48 
GeneralRe: Pulse Button Click Not Working Pin
Niel M.Thomas15-Jun-16 8:57
professionalNiel M.Thomas15-Jun-16 8:57 
GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir15-Jun-16 9:31
Syed Asad Jahangir15-Jun-16 9:31 
GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir15-Jun-16 9:38
Syed Asad Jahangir15-Jun-16 9:38 
GeneralRe: Pulse Button Click Not Working Pin
Niel M.Thomas15-Jun-16 22:17
professionalNiel M.Thomas15-Jun-16 22:17 
GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir16-Jun-16 11:54
Syed Asad Jahangir16-Jun-16 11:54 
GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir14-Jun-16 10:54
Syed Asad Jahangir14-Jun-16 10:54 
Downloaded Same Error When Giving Name To Pulse Button Of Your Project Or Adding A Click Event..!!

C#
System.Windows.Markup.XamlParseException occurred
  HResult=-2146233087
  LineNumber=32
  LinePosition=10
  Message='Set connectionId threw an exception.' Line number '32' and line position '10'.
  Source=PresentationFramework
  StackTrace:
       at System.Windows.Markup.WpfXamlLoader.Load(XamlReader xamlReader, IXamlObjectWriterFactory writerFactory, Boolean skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri)
  InnerException: 
       HResult=-2147467262
       Message=[A]NMT.Wpf.Controls.PulseButton cannot be cast to [B]NMT.Wpf.Controls.PulseButton. Type A originates from 'PulseButton, Version=1.0.3.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Documents\Visual Studio 2015\Projects\Pulse Button (WPF)\PulseButtonDemo\bin\Debug\PulseButton.dll'. Type B originates from 'PulseButtonDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Documents\Visual Studio 2015\Projects\Pulse Button (WPF)\PulseButtonDemo\bin\Debug\PulseButtonDemo.exe'.
       Source=PulseButtonDemo
       StackTrace:
            at PulseControlTest.MainWindow.System.Windows.Markup.IComponentConnector.Connect(Int32 connectionId, Object target) in C:\Documents\Visual Studio 2015\Projects\Pulse Button (WPF)\PulseButtonDemo\obj\Debug\MainWindow.g.cs:line 0
            at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetConnectionId(Object root, Int32 connectionId, Object instance)
       InnerException: 

GeneralRe: Pulse Button Click Not Working Pin
Syed Asad Jahangir14-Jun-16 10:05
Syed Asad Jahangir14-Jun-16 10:05 
Generalvery nice Pin
BillW3320-Jul-15 11:19
professionalBillW3320-Jul-15 11:19 
Questionreponse on mouse click Pin
abcque9-Jul-15 3:06
abcque9-Jul-15 3:06 
AnswerRe: reponse on mouse click Pin
Niel M.Thomas9-Jul-15 7:37
professionalNiel M.Thomas9-Jul-15 7:37 
GeneralMy vote of 5 Pin
Simon Gulliver9-Jun-15 1:14
professionalSimon Gulliver9-Jun-15 1:14 
QuestionPulses on click Pin
Flem100DK7-Jun-15 0:52
Flem100DK7-Jun-15 0:52 
AnswerRe: Pulses on click Pin
Niel M.Thomas7-Jun-15 9:50
professionalNiel M.Thomas7-Jun-15 9:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.