【问题标题】:How can I stop the WPF ProgressBar pulsing/animating when it reaches 100%?当 WPF ProgressBar 达到 100% 时,如何停止它的脉冲/动画?
【发布时间】:2011-06-02 00:50:19
【问题描述】:

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

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

我很高兴在进度条移动时出现“脉冲”动画,但一旦达到 100%,我希望它停止动画并保持 100% 的静态。

我尝试设置 IsIndeterminate="False" 但这无济于事,阅读 MSDN 文档后我明白了原因:

当这个属性为真时, ProgressBar 动画一些条移动 连续跨越 ProgressBar 方式并忽略 Value 属性。

可以停止这个动画吗?要么完全,要么只是 100%。

【问题讨论】:

  • 消除脉动的视觉效果是个坏主意,因为 UI 推理是它提供持续的用户反馈;以避免用户认为某些东西已被锁定并且不再响应。
  • 应用程序因 UI 动画一直在运行而锁定。说服用户该应用程序仍然存在需要更多的时间。
  • @RobertRossney,我故意锁定了我的 UI 线程和 poof,动画停止了,立即给出反馈。 aaron-mciver 是正确的,脉动具有 UX 目的。

标签: wpf mvvm progress-bar


【解决方案1】:

我为此使用附加属性编写了一个通用解决方案,允许我通过直接属性或样式设置器来切换任何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's declaration 中看到。虽然这并不一定保证命名元素存在,但缺少它的唯一原因是动画功能根本不受支持。如果它受支持但使用的元素名称与记录的不同,我会认为这是一个错误,而不是实现细节。

  • 由于我要从模板中拉出一个元素,因此还需要处理 Loaded 事件(在应用模板时引发)以等待模板变为可用,然后再尝试设置初始可见性,如有必要,当模板被主题更改动态替换时再次设置。

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

【讨论】:

  • 它工作正常,但如何设置进度条的纯色画笔,如上一个答案?
  • @monstr 如果不替换整个控件模板,您将无法真正可靠地做到这一点。从理论上讲,您可以覆盖OnApplyTemplate 来查找和修改构成 3D 外观的元素(“动画”和“叠加”),就像我对“PART_GlowRect”所做的那样。但这些不是模板的保证部分,它们可能会根据当前的 Windows 主题而改变。
  • 我是通过在您的 ReevaluateAnimationVisibility 方法中添加一些代码来实现的。我使用了模板的Animation 部分,并在null 上进行了验证,因此即使当前Windows 主题中没有Animation 部分-也不会导致异常。
【解决方案2】:

您可以通过为ProgressBar 复制整个ControlTemplate 来完成此操作,然后为ProgressBar.Value=100 的条件添加Trigger。原样的 XAML 将使 ProgressBar 的行为与现在一样。删除底部的注释标签,当ProgressBar 的Value 属性达到100 时动画将停止。唯一的弱点是,当您更改ProgressBar 的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,但它不起作用:(
  • 人们应该认真考虑 Dave Clemmer 的答案(见下文)作为替代方案,因为正如 Dave 所说,替换控件模板也有一些缺点。
【解决方案3】:

Dabblernl 的回答很可靠。 这是一个 hack(因为它依赖于发光的元素的内部名称,这是一个实现细节,可能会在后续版本中更改):

void SetGlowVisibility(ProgressBar progressBar, Visibility visibility) {
    var anim = progressBar.Template.FindName("Animation", progressBar) as FrameworkElement;
    if (anim != null)
        anim.Visibility = visibility;
}

如果 ProgressBar 的实现方式发生变化,此 hack 可能会停止工作。

另一方面,完全替代 XAML 和样式的解决方案可能会锁定和修复颜色、边框等,并禁用将来可能添加到更新版本 ProgressBar 的行为...

编辑:实施细节确实发生了变化。将"PART_GlowRect"改为"Animation"——前者仅用于aero.normalcolor.xaml,而后者也用于较新的aero2.normalcolor.xamlaerolite.normalcolor.xaml

【讨论】:

    【解决方案4】:

    您可以交换 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);
    
    ...
    

    然后可以修改ProgressBarBrushConverterdefault logic 以满足您的需要。

    您最终可能必须将参数传递给转换器,以便它可以检查值并根据ProgressBar 的状态提供适当的动画或缺少动画。

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • 请提高这个答案的正确性,告诉-1的原因。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-02
      • 1970-01-01
      • 1970-01-01
      • 2020-08-31
      相关资源
      最近更新 更多