I have an MVVM-based WPF 4 application which uses a ProgressBar to show the percentage completion of a long-running operation.
<ProgressBar Name="ProgressBar"
IsIndeterminate="False"
Minimum="0"
Maximum="100"
Value="{Binding Path=ProgressPercentageComplete, Mode=OneWay}"
Visibility="Visible"/>
I am happy for the "pulsing" animation to occur while the progress bar is moving, but once it reaches 100% I'd like it to stop animating and just remain static at 100%.
I've tried setting IsIndeterminate="False"
but this doesn't help and I can see why after reading the MSDN Documentation:
When this property is true, the
ProgressBar animates a few bars moving
across the ProgressBar in a continuous
manner and ignores the Value property.
Is it possible to stop this animation? Either completely, or just at 100%.
You can accomplish this by copying the entire ControlTemplate
for the ProgressBar
, then add a Trigger
for the condition where ProgressBar.Value=100
. The XAML as is will make the ProgressBar
behave as it does now. Remove the comment Tags at the bottom and the animation will stop when the ProgressBar
's Value property reaches 100. The only weakness is, that when you change the Maximum Property of the ProgressBar
you need to change the Trigger as well. Anyone know how to bind the Trigger to the actual value of the Maximum Property?
<Window x:Class="ProgressBarSpike.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
Loaded="Window_Loaded">
<Window.Resources>
<LinearGradientBrush x:Key="ProgressBarBackground" EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="#BABABA" Offset="0"/>
<GradientStop Color="#C7C7C7" Offset="0.5"/>
<GradientStop Color="#BABABA" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ProgressBarBorderBrush" EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#B2B2B2" Offset="0"/>
<GradientStop Color="#8C8C8C" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ProgressBarGlassyHighlight" EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#50FFFFFF" Offset="0.5385"/>
<GradientStop Color="#00FFFFFF" Offset="0.5385"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ProgressBarTopHighlight" EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#80FFFFFF" Offset="0.05"/>
<GradientStop Color="#00FFFFFF" Offset="0.25"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ProgressBarIndicatorAnimatedFill" EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="#00FFFFFF" Offset="0"/>
<GradientStop Color="#60FFFFFF" Offset="0.4"/>
<GradientStop Color="#60FFFFFF" Offset="0.6"/>
<GradientStop Color="#00FFFFFF" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ProgressBarIndicatorDarkEdgeLeft" EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="#0C000000" Offset="0"/>
<GradientStop Color="#20000000" Offset="0.3"/>
<GradientStop Color="#00000000" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ProgressBarIndicatorDarkEdgeRight" EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="#00000000" Offset="0"/>
<GradientStop Color="#20000000" Offset="0.7"/>
<GradientStop Color="#0C000000" Offset="1"/>
</LinearGradientBrush>
<RadialGradientBrush x:Key="ProgressBarIndicatorLightingEffectLeft" RadiusY="1" RadiusX="1" RelativeTransform="1,0,0,1,0.5,0.5">
<GradientStop Color="#60FFFFC4" Offset="0"/>
<GradientStop Color="#00FFFFC4" Offset="1"/>
</RadialGradientBrush>
<LinearGradientBrush x:Key="ProgressBarIndicatorLightingEffect" EndPoint="0,0" StartPoint="0,1">
<GradientStop Color="#60FFFFC4" Offset="0"/>
<GradientStop Color="#00FFFFC4" Offset="1"/>
</LinearGradientBrush>
<RadialGradientBrush x:Key="ProgressBarIndicatorLightingEffectRight" RadiusY="1" RadiusX="1" RelativeTransform="1,0,0,1,-0.5,0.5">
<GradientStop Color="#60FFFFC4" Offset="0"/>
<GradientStop Color="#00FFFFC4" Offset="1"/>
</RadialGradientBrush>
<LinearGradientBrush x:Key="ProgressBarIndicatorGlassyHighlight" EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#90FFFFFF" Offset="0.5385"/>
<GradientStop Color="#00FFFFFF" Offset="0.5385"/>
</LinearGradientBrush>
<Style x:Key="ProgressBarStyleStopAnimation" TargetType="{x:Type ProgressBar}">
<Setter Property="Foreground" Value="#01D328"/>
<Setter Property="Background" Value="{StaticResource ProgressBarBackground}"/>
<Setter Property="BorderBrush" Value="{StaticResource ProgressBarBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ProgressBar}">
<Grid x:Name="TemplateRoot" SnapsToDevicePixels="true">
<Rectangle Fill="{TemplateBinding Background}" RadiusY="2" RadiusX="2"/>
<Border Background="{StaticResource ProgressBarGlassyHighlight}" CornerRadius="2" Margin="1"/>
<Border BorderBrush="#80FFFFFF" BorderThickness="1,0,1,1" Background="{StaticResource ProgressBarTopHighlight}" Margin="1"/>
<Rectangle x:Name="PART_Track" Margin="1"/>
<Decorator x:Name="PART_Indicator" HorizontalAlignment="Left" Margin="1">
<Grid x:Name="Foreground">
<Rectangle x:Name="Indicator" Fill="{TemplateBinding Foreground}"/>
<Grid x:Name="Animation" ClipToBounds="true">
<Rectangle x:Name="PART_GlowRect" Fill="{StaticResource ProgressBarIndicatorAnimatedFill}" HorizontalAlignment="Left" Margin="-100,0,0,0" Width="100"/>
</Grid>
<Grid x:Name="Overlay">
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="15"/>
<ColumnDefinition Width="0.1*"/>
<ColumnDefinition MaxWidth="15"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Rectangle x:Name="LeftDark" Fill="{StaticResource ProgressBarIndicatorDarkEdgeLeft}" Margin="1,1,0,1" RadiusY="1" RadiusX="1" Grid.RowSpan="2"/>
<Rectangle x:Name="RightDark" Grid.Column="2" Fill="{StaticResource ProgressBarIndicatorDarkEdgeRight}" Margin="0,1,1,1" RadiusY="1" RadiusX="1" Grid.RowSpan="2"/>
<Rectangle x:Name="LeftLight" Grid.Column="0" Fill="{StaticResource ProgressBarIndicatorLightingEffectLeft}" Grid.Row="2"/>
<Rectangle x:Name="CenterLight" Grid.Column="1" Fill="{StaticResource ProgressBarIndicatorLightingEffect}" Grid.Row="2"/>
<Rectangle x:Name="RightLight" Grid.Column="2" Fill="{StaticResource ProgressBarIndicatorLightingEffectRight}" Grid.Row="2"/>
<Border x:Name="Highlight1" Background="{StaticResource ProgressBarIndicatorGlassyHighlight}" Grid.ColumnSpan="3" Grid.RowSpan="2"/>
<Border x:Name="Highlight2" Background="{StaticResource ProgressBarTopHighlight}" Grid.ColumnSpan="3" Grid.RowSpan="2"/>
</Grid>
</Grid>
</Decorator>
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="LayoutTransform" TargetName="TemplateRoot">
<Setter.Value>
<RotateTransform Angle="-90"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsIndeterminate" Value="true">
<Setter Property="Visibility" TargetName="LeftDark" Value="Collapsed"/>
<Setter Property="Visibility" TargetName="RightDark" Value="Collapsed"/>
<Setter Property="Visibility" TargetName="LeftLight" Value="Collapsed"/>
<Setter Property="Visibility" TargetName="CenterLight" Value="Collapsed"/>
<Setter Property="Visibility" TargetName="RightLight" Value="Collapsed"/>
<Setter Property="Visibility" TargetName="Indicator" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsIndeterminate" Value="false">
<Setter Property="Background" TargetName="Animation" Value="#80B5FFA9"/>
</Trigger>
<!--
<Trigger Property="Value" Value="100">
<Setter Property="Visibility" TargetName="Animation" Value="Collapsed"/>
</Trigger>
-->
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel>
<ProgressBar Name="Progress" Height="50" Style="{DynamicResource ProgressBarStyleStopAnimation}"/>
</StackPanel>
</Window>
I wrote a generalized solution for this using an attached property, allowing me to toggle the behavior on any ProgressBar
simply through a direct property or style setter, like so:
<ProgressBar helpers:ProgressBarHelper.StopAnimationOnCompletion="True" />
The code:
public static class ProgressBarHelper {
public static readonly DependencyProperty StopAnimationOnCompletionProperty =
DependencyProperty.RegisterAttached("StopAnimationOnCompletion", typeof(bool), typeof(ProgressBarHelper),
new PropertyMetadata(OnStopAnimationOnCompletionChanged));
public static bool GetStopAnimationOnCompletion(ProgressBar progressBar) {
return (bool)progressBar.GetValue(StopAnimationOnCompletionProperty);
}
public static void SetStopAnimationOnCompletion(ProgressBar progressBar, bool value) {
progressBar.SetValue(StopAnimationOnCompletionProperty, value);
}
private static void OnStopAnimationOnCompletionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) {
var progressBar = obj as ProgressBar;
if (progressBar == null) return;
var stopAnimationOnCompletion = (bool)e.NewValue;
if (stopAnimationOnCompletion) {
progressBar.Loaded += StopAnimationOnCompletion_Loaded;
progressBar.ValueChanged += StopAnimationOnCompletion_ValueChanged;
} else {
progressBar.Loaded -= StopAnimationOnCompletion_Loaded;
progressBar.ValueChanged -= StopAnimationOnCompletion_ValueChanged;
}
if (progressBar.IsLoaded) {
ReevaluateAnimationVisibility(progressBar);
}
}
private static void StopAnimationOnCompletion_Loaded(object sender, RoutedEventArgs e) {
ReevaluateAnimationVisibility((ProgressBar)sender);
}
private static void StopAnimationOnCompletion_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
var progressBar = (ProgressBar)sender;
if (e.NewValue == progressBar.Maximum || e.OldValue == progressBar.Maximum) {
ReevaluateAnimationVisibility(progressBar);
}
}
private static void ReevaluateAnimationVisibility(ProgressBar progressBar) {
if (GetStopAnimationOnCompletion(progressBar)) {
var animationElement = GetAnimationElement(progressBar);
if (animationElement != null) {
if (progressBar.Value == progressBar.Maximum) {
animationElement.SetCurrentValue(UIElement.VisibilityProperty, Visibility.Collapsed);
} else {
animationElement.InvalidateProperty(UIElement.VisibilityProperty);
}
}
}
}
private static DependencyObject GetAnimationElement(ProgressBar progressBar) {
var template = progressBar.Template;
if (template == null) return null;
return template.FindName("PART_GlowRect", progressBar) as DependencyObject;
}
}
Basically, it adds a ValueChanged
handler which adjusts the visibility of the animated element.
A few notes:
I'm using "PART_GlowRect"
to find the animated element, although someone called this a hack. I disagree: this element name is officially documented through TemplatePartAttribute, which you can see in ProgressBar's declaration. While it's true that this doesn't necessarily guarantee that the named element exists, the only reason it should be missing is if the animation feature isn't supported at all. If it is supported but uses a different element name than the one documented, I would consider that a bug, not an implementation detail.
Since I'm pulling an element out of the template, it's also necessary to handle the Loaded
event (which is raised when a template is applied) to wait for the template to become available before attempting to set initial visibility, and if necessary set it again when the template is replaced on the fly by a theme change.
Rather than explicitly toggling Visibility
between Collapsed
and Visible
, I'm using SetCurrentValue to set to Collapsed
, and InvalidateProperty to reset it. SetCurrentValue
applies a value that does not take priority over other value sources, and InvalidateProperty
re-evaluates the property without taking the SetCurrentValue
setting into consideration. This ensures that if there are existing styles or triggers which would affect the visibility under normal conditions (i.e. when it is not at 100%), it would reset to that behavior if the progress bar is reused (going from 100% back to 0%) rather than being hard-coded to Visible
.
Dabblernl's answer is robust.
Here is a hack (because it relies on the internal name of the element that does the glow, which is an implementation detail and may change in a subsequent version):
void SetGlowVisibility(ProgressBar progressBar, Visibility visibility) {
var anim = progressBar.Template.FindName("Animation", progressBar) as FrameworkElement;
if (anim != null)
anim.Visibility = visibility;
}
If how a ProgressBar
is implemented changes, this hack may stop working.
On the other hand, a solution that completely replaces the XAML and styles may lock-in and fix colours, borders etc. and disable behaviour that might be added to a newer version of the ProgressBar in the future...
Edit: The implementation detail did change. Changed "PART_GlowRect"
to "Animation"
-- the former is used only in aero.normalcolor.xaml
, while the latter is used in more recent aero2.normalcolor.xaml
and aerolite.normalcolor.xaml
too.
You can swap the converter being used by PART_Indicator
which by default is the ProgressBarBrushConverter
which is where the animation is coming from...
...
TranslateTransform transform = new TranslateTransform();
double num11 = num8 * 100;
DoubleAnimationUsingKeyFrames animation = new DoubleAnimationUsingKeyFrames();
animation.Duration = new Duration(TimeSpan.FromMilliseconds(num11));
animation.RepeatBehavior = RepeatBehavior.Forever;
for (int i = 1; i <= num8; i++)
{
double num13 = i * num7;
animation.KeyFrames.Add(new DiscreteDoubleKeyFrame(num13, KeyTime.Uniform));
}
transform.BeginAnimation(TranslateTransform.XProperty, animation);
...
The default logic for the ProgressBarBrushConverter
can then be modified to suit your needs.
You may have to end up passing parameters to the converter so that it can check the value and provide the appropriate animation or lack thereof contingent on the state of the ProgressBar
.
I think the only way is to create a custom style for the ProgressBar
.
The description of MSDN for IsIndeterminate refers to another behaviour and, for default, it is false so setting it doesn't change anything.