如何停止WPF ProgressBar在达到100%时的脉动/动画?

27

我有一个基于MVVM的WPF 4应用程序,它使用ProgressBar来显示长时间运行操作的百分比完成情况。

<ProgressBar Name="ProgressBar"
    IsIndeterminate="False"
    Minimum="0"
    Maximum="100"
    Value="{Binding Path=ProgressPercentageComplete, Mode=OneWay}"
    Visibility="Visible"/>

我希望进度条在移动时进行“脉动”动画,但一旦达到100%,我希望它停止动画并保持静态状态。

我尝试设置IsIndeterminate="False",但这并没有帮助,我读了MSDN文档后发现这是为什么:

当此属性为true时,ProgressBar以连续的方式动画显示几个条形块移动过程,并忽略Value属性。

是否可以停止这个动画? 完全停止,或者只停止在100%时。


2
杀死跳动的视觉效果是一个坏主意,因为UI的理由是它提供连续的用户反馈;以避免用户相信某些东西被锁定了并且不再响应。 - Aaron McIver
5
应用程序假死时,UI动画仍然在运行。仅凭此还不足以让用户相信应用程序仍在运行。 - Robert Rossney
1
@RobertRossney,我故意锁定了我的UI线程,,动画停止了,立即给出了反馈。aaron-mciver是正确的,脉动有UX目的。 - NH.
5个回答

11

我使用一个附加属性编写了一个通用解决方案,使我可以通过直接属性或样式设置器在任何ProgressBar上切换行为,像这样:

<ProgressBar helpers:ProgressBarHelper.StopAnimationOnCompletion="True" />

这段代码:

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;
    }
}

基本上,它添加了一个ValueChanged处理程序,它会调整动画元素的可见性。

几点需要注意:

  • 我正在使用"PART_GlowRect"来查找动画元素,尽管有人称之为hack。我不同意:通过TemplatePartAttribute,您可以在ProgressBar的声明中正式记录此元素名称。虽然确实如此,这并不一定保证命名元素存在,但如果完全不支持动画功能,则唯一缺少命名元素的原因是。如果它受支持但使用与文档中不同的元素名称,则我认为这是一个错误,而不是一个实现细节。

  • 由于我从模板中拉出了一个元素,因此还需要处理Loaded事件(当应用模板时引发)等待模板可用后再尝试设置初始可见性,并且如果必要,在主题更改时将其再次设置。

  • 我没有显式地在CollapsedVisible之间切换Visibility,而是使用SetCurrentValue设置为Collapsed,并使用InvalidateProperty进行重置。SetCurrentValue应用一个不优先于其他值源的值,InvalidateProperty重新评估属性而不考虑SetCurrentValue设置。这样可以确保如果存在会在正常情况下影响可见性的现有样式或触发器(即当它不是100%时),如果进度条被重用(从100%返回到0%),则会重置为该行为而不是硬编码为Visible


它运行良好,但我如何设置进度条的实色画刷,就像之前答案中那样? - monstr
@monstr,如果不替换整个控件模板,你无法可靠地完成这项任务。理论上,你可以重写OnApplyTemplate来查找和修改构成3D外观的元素(“Animation”和“Overlay”),就像我对“PART_GlowRect”所做的那样。但是这些并不是模板的固定部分,它们可能会根据当前的Windows主题而改变。 - nmclean
1
我通过在您的“ReevaluateAnimationVisibility”方法中添加一些代码来实现。我使用了模板的“Animation”部分,并对其进行了空值验证,因此即使当前Windows主题中没有“Animation”部分,也不会引发异常。 - monstr

8
Dabblernl的回答非常有力。 以下是一个技巧(因为它依赖于执行发光效果的元素的内部名称,这是一个实现细节,可能会在后续版本中更改):
void SetGlowVisibility(ProgressBar progressBar, Visibility visibility) {
    var anim = progressBar.Template.FindName("Animation", progressBar) as FrameworkElement;
    if (anim != null)
        anim.Visibility = visibility;
}

如果一个进度条的实现方式发生变化,这个方法可能会失效。另一方面,完全替换XAML和样式的解决方案可能会锁定和固定颜色、边框等,并禁用未来版本ProgressBar中可能添加的行为...编辑:实现细节确实发生了变化。将"PART_GlowRect"更改为"Animation"——前者仅在"aero.normalcolor.xaml"中使用,而后者也在更新的"aero2.normalcolor.xaml"和"aerolite.normalcolor.xaml"中使用。

8
你可以通过复制整个ProgressBarControlTemplate,然后为ProgressBar.Value=100的条件添加一个Trigger来实现这一点。目前的XAML将使ProgressBar的行为与现在相同。删除底部的注释标签,当ProgressBar的Value属性达到100时,动画将停止。唯一的弱点是,当你改变ProgressBar的Maximum属性时,你需要同时改变Trigger。有人知道如何将Trigger绑定到Maximum属性的实际值吗?
<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>

有没有一种方法可以不明确指定“Value =”100“?因为这种方法并不通用。它使我将任何“ProgressBar”的“Maximum”设置为“100”。我尝试使用“Binding”,但它不起作用:( - monstr
应认真考虑Dave Clemmer的答案(见下文),因为像Dave所指出的那样,替换控件模板也有一些缺点。 - user2261015

1

您可以交换由 PART_Indicator 使用的转换器,默认情况下是来自 ProgressBarBrushConverter 的动画...

...

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);

...

可以根据您的需求修改 ProgressBarBrushConverter 默认逻辑

您可能需要向转换器传递参数,以便它可以检查值并根据 ProgressBar 的状态提供适当的动画或缺少动画。


0

我认为唯一的方法是为ProgressBar创建自定义样式。
MSDN对于IsIndeterminate的描述涉及到另一种行为,而默认情况下它是false,因此设置它不会改变任何东西。


请提高这个答案的正确性,说明为什么是-1。 - as-cii

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接