The goal is to display the information that the application is working. So I'm looking for an intelligent implementation sample of a loading spinner using WPF / MVVM.
I wrote this user control which may help, it will display messages with a progress bar spinning to show it is currently loading something.
<ctr:LoadingPanel x:Name="loadingPanel"
IsLoading="{Binding PanelLoading}"
Message="{Binding PanelMainMessage}"
SubMessage="{Binding PanelSubMessage}"
ClosePanelCommand="{Binding PanelCloseCommand}" />
It has a couple of basic properties that you can bind to.
A very simple "plug and play" spinner could be one of the spinning icons from the Font Awesome Wpf Package (Spinning icons).
The usage is quite simple, just install the nuget package:
PM> Install-Package FontAwesome.WPF
Then add the reference to the namespace
xmlns:fa="http://schemas.fontawesome.io/icons/"
and use the ImageAwesome control. Set the Spin="True" property and select one of the "Spinner", "Refresh", "Cog" and "CircleOutlinedNotched" Icon. It's scalable and can be resized by setting width and height.
<Window x:Class="Example.FontAwesome.WPF.Single"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fa="http://schemas.fontawesome.io/icons/"
Title="Single" Height="300" Width="300">
<Grid Margin="20">
<fa:ImageAwesome Icon="Refresh" Spin="True" Height="48" Width="48" />
</Grid>
</Window>
FontAwesome.WPF
is not compatible with recent .NET versions, like .Net Core 3.x, or .NET 5 or above, as it only strictly a .NET Framework project. This makes this solution hard to use in modern projects. –
Carpentaria To get this:
Stick this in a user control:
<UserControl.Resources>
<Color x:Key="FilledColor" A="255" B="155" R="155" G="155"/>
<Color x:Key="UnfilledColor" A="0" B="155" R="155" G="155"/>
<Style x:Key="BusyAnimationStyle" TargetType="Control">
<Setter Property="Background" Value="#7F000000"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<ControlTemplate.Resources>
<Storyboard x:Key="Animation0" BeginTime="00:00:00.0" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation1" BeginTime="00:00:00.2" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse1" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation2" BeginTime="00:00:00.4" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse2" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation3" BeginTime="00:00:00.6" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse3" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation4" BeginTime="00:00:00.8" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse4" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation5" BeginTime="00:00:01.0" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse5" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation6" BeginTime="00:00:01.2" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse6" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation7" BeginTime="00:00:01.4" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse7" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<ControlTemplate.Triggers>
<Trigger Property="IsVisible" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Animation0}" x:Name="Storyboard0" />
<BeginStoryboard Storyboard="{StaticResource Animation1}" x:Name="Storyboard1"/>
<BeginStoryboard Storyboard="{StaticResource Animation2}" x:Name="Storyboard2"/>
<BeginStoryboard Storyboard="{StaticResource Animation3}" x:Name="Storyboard3"/>
<BeginStoryboard Storyboard="{StaticResource Animation4}" x:Name="Storyboard4"/>
<BeginStoryboard Storyboard="{StaticResource Animation5}" x:Name="Storyboard5"/>
<BeginStoryboard Storyboard="{StaticResource Animation6}" x:Name="Storyboard6"/>
<BeginStoryboard Storyboard="{StaticResource Animation7}" x:Name="Storyboard7"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<StopStoryboard BeginStoryboardName="Storyboard0"/>
<StopStoryboard BeginStoryboardName="Storyboard1"/>
<StopStoryboard BeginStoryboardName="Storyboard2"/>
<StopStoryboard BeginStoryboardName="Storyboard3"/>
<StopStoryboard BeginStoryboardName="Storyboard4"/>
<StopStoryboard BeginStoryboardName="Storyboard5"/>
<StopStoryboard BeginStoryboardName="Storyboard6"/>
<StopStoryboard BeginStoryboardName="Storyboard7"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
<Grid>
<Canvas Height="60" Width="60">
<Canvas.Resources>
<Style TargetType="Ellipse">
<Setter Property="Width" Value="15"/>
<Setter Property="Height" Value="15" />
<Setter Property="Fill" Value="#009B9B9B" />
</Style>
</Canvas.Resources>
<Ellipse x:Name="ellipse0" Canvas.Left="1.75" Canvas.Top="21"/>
<Ellipse x:Name="ellipse1" Canvas.Top="7" Canvas.Left="6.5"/>
<Ellipse x:Name="ellipse2" Canvas.Left="20.5" Canvas.Top="0.75"/>
<Ellipse x:Name="ellipse3" Canvas.Left="34.75" Canvas.Top="6.75"/>
<Ellipse x:Name="ellipse4" Canvas.Left="40.5" Canvas.Top="20.75" />
<Ellipse x:Name="ellipse5" Canvas.Left="34.75" Canvas.Top="34.5"/>
<Ellipse x:Name="ellipse6" Canvas.Left="20.75" Canvas.Top="39.75"/>
<Ellipse x:Name="ellipse7" Canvas.Top="34.25" Canvas.Left="7" />
<Ellipse Width="39.5" Height="39.5" Canvas.Left="8.75" Canvas.Top="8" Visibility="Hidden"/>
</Canvas>
<Label Content="{Binding Path=Text}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Control Style="{StaticResource BusyAnimationStyle}"/>
To get a cool disappearing effect on each ellipse add the following after each ColorAnimationUsingKeyFrames
element. Be sure to point it to the correct ellipse..
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="Width" >
<DoubleAnimationUsingKeyFrames.KeyFrames>
<SplineDoubleKeyFrame KeyTime="00:00:00.0" Value="15" />
<SplineDoubleKeyFrame KeyTime="00:00:01.0" Value="12" />
<SplineDoubleKeyFrame KeyTime="00:00:01.6" Value="0" />
</DoubleAnimationUsingKeyFrames.KeyFrames>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="Height" >
<DoubleAnimationUsingKeyFrames.KeyFrames>
<SplineDoubleKeyFrame KeyTime="00:00:00.0" Value="15" />
<SplineDoubleKeyFrame KeyTime="00:00:01.0" Value="12" />
<SplineDoubleKeyFrame KeyTime="00:00:01.6" Value="0" />
</DoubleAnimationUsingKeyFrames.KeyFrames>
</DoubleAnimationUsingKeyFrames>
<Setter Property="Background" Value="#7F000000"/>
to <Setter Property="Background" Value="Transparent"/>
. -- Visibility binding just works out of the box. Just bind visibility to a proper value (e.g. I do it using a ValueConverter). Nothing fancy needed there, it just works. :-) –
Refrigerate I wrote this user control which may help, it will display messages with a progress bar spinning to show it is currently loading something.
<ctr:LoadingPanel x:Name="loadingPanel"
IsLoading="{Binding PanelLoading}"
Message="{Binding PanelMainMessage}"
SubMessage="{Binding PanelSubMessage}"
ClosePanelCommand="{Binding PanelCloseCommand}" />
It has a couple of basic properties that you can bind to.
With Images
Visual summary of options for spinning icons. Recorded using Screen To Gif.
Font-Awesome-WPF
Install via NuGet:
PM> Install-Package FontAwesome.WPF
Looks like this:
XAML:
<fa:ImageAwesome Icon="Spinner" Spin="True" SpinDuration="4" />
Icons pictured are Spinner
, CircleOutlineNotch
, Refresh
and Cog
. There are many others.
Method from @HAdes
XAML copy/paste.
In WPF, you can now simply do:
Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait; // set the cursor to loading spinner
Mouse.OverrideCursor = System.Windows.Input.Cursors.Arrow; // set the cursor back to arrow
null
, otherwise you will loose default behaviours such as switching to IBeam when hovering a field, etc. The doc : learn.microsoft.com/en-us/dotnet/api/… –
Nectarous You can do it without any additional controls and libraries, using only Image control and transform:
<Image
Source="/images/spinner.png"
Width="100"
Height="100"
RenderTransformOrigin="0.5, 0.5" Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}">
<Image.RenderTransform>
<RotateTransform x:Name="noFreeze" />
</Image.RenderTransform>
<Image.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(Image.RenderTransform).(RotateTransform.Angle)"
To="360" Duration="0:0:1" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Image.Triggers>
</Image>
Replace /images/spinner.png
with your image. Change To="360"
to To="-360"
if you want to rotate it counterclockwise. Duration="0:0:1"
equals to 1 second per rotation.
This is an update to the code given by @HAdes to parameterize width, height, and ellipse size.
This implementation automatically calculates required angles, widths, and heights on the fly.
The user control is bound to itself (code-behind) which takes care of all calculations.
XAML
<UserControl x:Class="WpfApplication2.Spinner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication2"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<Color x:Key="FilledColor" A="255" B="155" R="155" G="155"/>
<Color x:Key="UnfilledColor" A="0" B="155" R="155" G="155"/>
<Style x:Key="BusyAnimationStyle" TargetType="Control">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<ControlTemplate.Resources>
<Storyboard x:Key="Animation0" BeginTime="00:00:00.0" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseN" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation1" BeginTime="00:00:00.2" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseNE" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation2" BeginTime="00:00:00.4" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseE" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation3" BeginTime="00:00:00.6" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseSE" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation4" BeginTime="00:00:00.8" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseS" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation5" BeginTime="00:00:01.0" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseSW" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation6" BeginTime="00:00:01.2" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseW" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Animation7" BeginTime="00:00:01.4" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="ellipseNW" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<ControlTemplate.Triggers>
<Trigger Property="IsVisible" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Animation0}" x:Name="Storyboard0" />
<BeginStoryboard Storyboard="{StaticResource Animation1}" x:Name="Storyboard1"/>
<BeginStoryboard Storyboard="{StaticResource Animation2}" x:Name="Storyboard2"/>
<BeginStoryboard Storyboard="{StaticResource Animation3}" x:Name="Storyboard3"/>
<BeginStoryboard Storyboard="{StaticResource Animation4}" x:Name="Storyboard4"/>
<BeginStoryboard Storyboard="{StaticResource Animation5}" x:Name="Storyboard5"/>
<BeginStoryboard Storyboard="{StaticResource Animation6}" x:Name="Storyboard6"/>
<BeginStoryboard Storyboard="{StaticResource Animation7}" x:Name="Storyboard7"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<StopStoryboard BeginStoryboardName="Storyboard0"/>
<StopStoryboard BeginStoryboardName="Storyboard1"/>
<StopStoryboard BeginStoryboardName="Storyboard2"/>
<StopStoryboard BeginStoryboardName="Storyboard3"/>
<StopStoryboard BeginStoryboardName="Storyboard4"/>
<StopStoryboard BeginStoryboardName="Storyboard5"/>
<StopStoryboard BeginStoryboardName="Storyboard6"/>
<StopStoryboard BeginStoryboardName="Storyboard7"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
<Grid>
<Canvas>
<Canvas.Resources>
<Style TargetType="Ellipse">
<Setter Property="Width" Value="{Binding Path=EllipseSize}"/>
<Setter Property="Height" Value="{Binding Path=EllipseSize}" />
<Setter Property="Fill" Value="Transparent" />
</Style>
</Canvas.Resources>
<Ellipse x:Name="ellipseN" Canvas.Left="{Binding Path=EllipseN.Left}" Canvas.Top="{Binding Path=EllipseN.Top}"/>
<Ellipse x:Name="ellipseNE" Canvas.Left="{Binding Path=EllipseNE.Left}" Canvas.Top="{Binding Path=EllipseNE.Top}"/>
<Ellipse x:Name="ellipseE" Canvas.Left="{Binding Path=EllipseE.Left}" Canvas.Top="{Binding Path=EllipseE.Top}"/>
<Ellipse x:Name="ellipseSE" Canvas.Left="{Binding Path=EllipseSE.Left}" Canvas.Top="{Binding Path=EllipseSE.Top}"/>
<Ellipse x:Name="ellipseS" Canvas.Left="{Binding Path=EllipseS.Left}" Canvas.Top="{Binding Path=EllipseS.Top}"/>
<Ellipse x:Name="ellipseSW" Canvas.Left="{Binding Path=EllipseSW.Left}" Canvas.Top="{Binding Path=EllipseSW.Top}"/>
<Ellipse x:Name="ellipseW" Canvas.Left="{Binding Path=EllipseW.Left}" Canvas.Top="{Binding Path=EllipseW.Top}"/>
<Ellipse x:Name="ellipseNW" Canvas.Left="{Binding Path=EllipseNW.Left}" Canvas.Top="{Binding Path=EllipseNW.Top}"/>
</Canvas>
<Label Content="{Binding Path=Text}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Border>
<Control Style="{StaticResource BusyAnimationStyle}"/>
</Border>
</UserControl>
Code Behind (C#)
using System;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication2
{
/// <summary>
/// Interaction logic for Spinner.xaml
/// </summary>
public partial class Spinner : UserControl
{
public int EllipseSize { get; set; } = 8;
public int SpinnerHeight { get; set; } = 0;
public int SpinnerWidth { get; set; } = 0;
// start positions
public EllipseStartPosition EllipseN { get; private set; }
public EllipseStartPosition EllipseNE { get; private set; }
public EllipseStartPosition EllipseE { get; private set; }
public EllipseStartPosition EllipseSE { get; private set; }
public EllipseStartPosition EllipseS { get; private set; }
public EllipseStartPosition EllipseSW { get; private set; }
public EllipseStartPosition EllipseW { get; private set; }
public EllipseStartPosition EllipseNW { get; private set; }
public Spinner()
{
InitializeComponent();
}
private void initialSetup()
{
float horizontalCenter = (float)(SpinnerWidth / 2);
float verticalCenter = (float)(SpinnerHeight / 2);
float distance = (float)Math.Min(SpinnerHeight, SpinnerWidth) /2;
double angleInRadians = 44.8;
float cosine = (float)Math.Cos(angleInRadians);
float sine = (float)Math.Sin(angleInRadians);
EllipseN = newPos(left: horizontalCenter, top: verticalCenter - distance);
EllipseNE = newPos(left: horizontalCenter + (distance * cosine), top: verticalCenter - (distance * sine));
EllipseE = newPos(left: horizontalCenter + distance, top: verticalCenter);
EllipseSE = newPos(left: horizontalCenter + (distance * cosine), top: verticalCenter + (distance * sine));
EllipseS = newPos(left: horizontalCenter, top: verticalCenter + distance);
EllipseSW = newPos(left: horizontalCenter - (distance * cosine), top: verticalCenter + (distance * sine));
EllipseW = newPos(left: horizontalCenter - distance, top: verticalCenter);
EllipseNW = newPos(left: horizontalCenter - (distance * cosine), top: verticalCenter - (distance * sine));
}
private EllipseStartPosition newPos(float left, float top)
{
return new EllipseStartPosition() { Left = left, Top = top };
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if(e.Property.Name == "Height")
{
SpinnerHeight = Convert.ToInt32(e.NewValue);
}
if (e.Property.Name == "Width")
{
SpinnerWidth = Convert.ToInt32(e.NewValue);
}
if(SpinnerHeight > 0 && SpinnerWidth > 0)
{
initialSetup();
}
base.OnPropertyChanged(e);
}
}
public struct EllipseStartPosition
{
public float Left { get; set; }
public float Top { get; set; }
}
}
Sample Use
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication2"
xmlns:animated="WpfApplication2.MainWindow"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel Background="DarkGoldenrod" Width="200" Height="200" VerticalAlignment="Top" HorizontalAlignment="Left" >
<Button Height="35">
<Button.Content >
<DockPanel LastChildFill="True" Height="NaN" Width="NaN" HorizontalAlignment="Left">
<local:Spinner EllipseSize="4" DockPanel.Dock="Left" HorizontalAlignment="Left" Margin="0,0,10,5" Height="16" Width="16"/>
<TextBlock Text="Cancel" VerticalAlignment="Center"/>
</DockPanel>
</Button.Content>
</Button>
</StackPanel>
</Window>
Here's an example of an all-xaml solution. It binds to an "IsWorking" boolean in the viewmodel to show the control and start the animation.
<UserControl x:Class="MainApp.Views.SpinnerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
</UserControl.Resources>
<StackPanel Orientation="Horizontal" Margin="5"
Visibility="{Binding IsWorking, Converter={StaticResource BoolToVisConverter}}">
<Label>Wait...</Label>
<Ellipse x:Name="spinnerEllipse"
Width="20" Height="20">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="1,1" EndPoint="0,0" >
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="CornflowerBlue" Offset="1"/>
</LinearGradientBrush>
</Ellipse.Fill>
<Ellipse.RenderTransform>
<RotateTransform x:Name="SpinnerRotate" CenterX="10" CenterY="10"/>
</Ellipse.RenderTransform>
<Ellipse.Style>
<Style TargetType="Ellipse">
<Style.Triggers>
<DataTrigger Binding="{Binding IsWorking}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="SpinStoryboard">
<Storyboard TargetProperty="RenderTransform.Angle" >
<DoubleAnimation
From="0" To="360" Duration="0:0:01"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="SpinStoryboard"></StopStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</StackPanel>
</UserControl>
This repo on github seems to do the job quite well:
https://github.com/blackspikeltd/Xaml-Spinners-WPF
The spinners are all light weight and can easily be placed wherever needed. There is a sample project included in the repo that shows how to use them.
No nasty code-behinds with a bunch of logic either. If MVVM support is needed, one can just take these and throw them in a Grid with a Visibility binding.
use an enum type to indicate your ViewModel's State
public enum ViewModeType
{
Default,
Busy
//etc.
}
then in your ViewModels Base class use a property
public ViewModeType ViewMode
{
get { return this.viewMode; }
set
{
if (this.viewMode != value)
{
this.viewMode = value;
//You should notify property changed here
}
}
}
and in view trigger the ViewMode and if it is busy show busyindicator:
<Trigger Property="ViewMode" Value="Busy">
<!-- Show BusyIndicator -->
</Trigger>
The customized spinner posted by @Menol had a small issue where the spinner would be shifted down and to the right by the size of one dot. I have updated the code so that it compensates for this offset by subtracting by half the dot.
Here is the updated code:
private void initialSetup()
{
float horizontalCenter = (float)(SpinnerWidth / 2);
float verticalCenter = (float)(SpinnerHeight / 2);
float distance = (float)Math.Min(SpinnerHeight, SpinnerWidth) / 2;
float dotComp = (float)(EllipseSize / 2);
double angleInRadians = 44.8;
float cosine = (float)Math.Cos(angleInRadians);
float sine = (float)Math.Sin(angleInRadians);
EllipseN = newPos(left: horizontalCenter - dotComp, top: verticalCenter - distance - dotComp);
EllipseNE = newPos(left: horizontalCenter + (distance * cosine) - dotComp, top: verticalCenter - (distance * sine) - dotComp);
EllipseE = newPos(left: horizontalCenter + distance - dotComp, top: verticalCenter - dotComp);
EllipseSE = newPos(left: horizontalCenter + (distance * cosine) - dotComp, top: verticalCenter + (distance * sine) - dotComp);
EllipseS = newPos(left: horizontalCenter - dotComp, top: verticalCenter + distance - dotComp);
EllipseSW = newPos(left: horizontalCenter - (distance * cosine) - dotComp, top: verticalCenter + (distance * sine) - dotComp);
EllipseW = newPos(left: horizontalCenter - distance - dotComp, top: verticalCenter - dotComp);
EllipseNW = newPos(left: horizontalCenter - (distance * cosine) - dotComp, top: verticalCenter - (distance * sine) - dotComp);
}
CircularProgressBarBlue.xaml
<UserControl
x:Class="CircularProgressBarBlue"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="Transparent"
Name="progressBar">
<UserControl.Resources>
<Storyboard x:Key="spinning" >
<DoubleAnimation
Storyboard.TargetName="SpinnerRotate"
Storyboard.TargetProperty="(RotateTransform.Angle)"
From="0"
To="360"
RepeatBehavior="Forever"/>
</Storyboard>
</UserControl.Resources>
<Grid
x:Name="LayoutRoot"
Background="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image Source="C:\SpinnerImage\BlueSpinner.png" RenderTransformOrigin="0.5,0.5" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<Image.RenderTransform>
<RotateTransform
x:Name="SpinnerRotate"
Angle="0"/>
</Image.RenderTransform>
</Image>
</Grid>
CircularProgressBarBlue.xaml.cs
using System;
using System.Windows;
using System.Windows.Media.Animation;
/// <summary>
/// Interaction logic for CircularProgressBarBlue.xaml
/// </summary>
public partial class CircularProgressBarBlue
{
private Storyboard _sb;
public CircularProgressBarBlue()
{
InitializeComponent();
StartStoryBoard();
IsVisibleChanged += CircularProgressBarBlueIsVisibleChanged;
}
void CircularProgressBarBlueIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (_sb == null) return;
if (e != null && e.NewValue != null && (((bool)e.NewValue)))
{
_sb.Begin();
_sb.Resume();
}
else
{
_sb.Stop();
}
}
void StartStoryBoard()
{
try
{
_sb = (Storyboard)TryFindResource("spinning");
if (_sb != null)
_sb.Begin();
}
catch
{ }
}
}
The approach is to use geometry with animations applied. Add the required geometry to the Path and animate its RotateTransform from 0-360°.
My spinner support two types of spinners:
- Circles :
- Rings :
And the central logic looks like:
if(spinner.SpinnerType == SpinnerType.Ring)
{
double innerRad = spinner.Radius - spinner.ItemRadius;
Point center = new Point(0, 0);
grp.Children.Add(new EllipseGeometry( center, spinner.Radius, spinner.Radius));
grp.Children.Add(new EllipseGeometry(center, innerRad, innerRad));
return;
}
var points = GetPointsOnCircle( spinner.Diameter/ 2);
double r = spinner.ItemRadius;
foreach (var point in points)
{
grp.Children.Add(new EllipseGeometry(point, r, r));
r -= spinner.ContinuousSizeReduction;
}
Usage is as simple as follows:
<local:SpinnerControl Diameter="60" Fill="#FFE8B311"/>
Here is the source code!
<Grid Width="100" Height="100">
<Ellipse Width="20" Height="20" Stroke="Gray" StrokeThickness="4">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="rotateTransform"
Storyboard.TargetProperty="Angle"
From="0" To="360"
Duration="0:0:1"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
<Ellipse.RenderTransform>
<RotateTransform x:Name="rotateTransform" CenterX="10" CenterY="10"/>
</Ellipse.RenderTransform>
</Ellipse>
In this example, an Ellipse with a Stroke of "Gray" and a StrokeThickness of "4" is used to create the ring shape. The RenderTransform property is used to rotate the Ellipse using a RotateTransform object, and the animation is triggered by the Loaded event of the FrameworkElement. The Duration of the DoubleAnimation is set to "0:0:1", meaning the rotation will take 1 second to complete. The RepeatBehavior is set to "Forever", so the animation will repeat indefinitely.
Other spiner with Square shape:
<Grid Width="100" Height="100">
<Path Stroke="Gray" StrokeThickness="4" StrokeEndLineCap="Round">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="50,10">
<LineSegment Point="10,50" />
<LineSegment Point="50,90" />
<LineSegment Point="90,50" />
<LineSegment Point="50,10" />
</PathFigure>
</PathGeometry>
</Path.Data>
<Path.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="rotateTransform"
Storyboard.TargetProperty="Angle"
From="0" To="360"
Duration="0:0:1"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Path.Triggers>
<Path.RenderTransform>
<RotateTransform x:Name="rotateTransform" CenterX="50" CenterY="50" />
</Path.RenderTransform>
</Path>
I guess I'll add my solution as well because it does not require any external resources like images and can be scaled indefinately without losing quality because it uses a path. You can bind the paths.IsEnabled to the property that indicates if the spinner shall spin or not. The speed can be adjusted using the duration property:
the result:
the XAML:
<Path Width="30" Height="30" Stretch="Fill" Fill="Black" Data="M20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C12.76,4 13.5,4.11 14.2,4.31L15.77,2.74C14.61,2.26 13.34,2 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12" RenderTransformOrigin="0.5,0.5">
<!--
Use this if you prefer the dots instead of the open circle
<Path Stretch="Fill" Fill="Black" Data="M12 19C13.1 19 14 19.9 14 21S13.1 23 12 23 10 22.1 10 21 10.9 19 12 19M12 1C13.1 1 14 1.9 14 3S13.1 5 12 5 10 4.1 10 3 10.9 1 12 1M6 16C7.1 16 8 16.9 8 18S7.1 20 6 20 4 19.1 4 18 4.9 16 6 16M3 10C4.1 10 5 10.9 5 12S4.1 14 3 14 1 13.1 1 12 1.9 10 3 10M6 4C7.1 4 8 4.9 8 6S7.1 8 6 8 4 7.1 4 6 4.9 4 6 4M18 16C19.1 16 20 16.9 20 18S19.1 20 18 20 16 19.1 16 18 16.9 16 18 16M21 10C22.1 10 23 10.9 23 12S22.1 14 21 14 19 13.1 19 12 19.9 10 21 10M18 4C19.1 4 20 4.9 20 6S19.1 8 18 8 16 7.1 16 6 16.9 4 18 4Z" RenderTransformOrigin="0.5,0.5">
-->
<Path.RenderTransform>
<RotateTransform/>
</Path.RenderTransform>
<Path.Style>
<Style>
<Style.Triggers>
<Trigger Property="Image.IsEnabled" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="RenderTransform.Angle"
From="0"
To="360"
Duration="0:0:1"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
© 2022 - 2024 — McMap. All rights reserved.