形状填充颜色更新停止故事板闪烁动画

Shape Fill color update stops Storyboard blinking animation

我的 WPF 应用程序是生产系统的监视器。主 window 显示显示连接状态(DOWN、LOADING 或 UP)的 3 个控件之一。 UP 控件有几个 high-level 按钮形状(下例中的椭圆),代表生产系统的各种元素(路由器、配置、内部连接、外部连接……)。 object 的颜色反映了其 children(或 grandchildren,或 great-grandchildren ...)的“最差”状态。如果 children 之一改变状态,它也会 flash/blink(通过 Storyboard 动画)。

每个 Button 形状映射到一个 ColorStatus object:

    public class ColorStatus: INotifyPropertyChanged
    {
        private eStatusColor StatusColor {get; set;}
        public bool          IsFlashing  {get; set;}
    
          // ...
    }

eStatusColor 是一个枚举(INACTIVE、NORMAL、WARNING、CRITICAL),我在其中使用 Converter Level2Color_Converter() 将其更改为 SolidColorBrush (grey/green/yellow/red),并将其应用于形状填充。 bool IsFlashing 用于触发填充颜色的动画(将从当前颜色变为黑色的 StatusBlinked,然后返回当前颜色,持续时间约为 1 秒)。这是一个示例按钮形状(路由器椭圆形):

                <!-- Router Oval -->
                <Button Grid.Column="1"             Margin="0,3"
                        VerticalAlignment="Center"  HorizontalAlignment="Center"
                        Focusable="False"           ToolTip="Open Router Window"
                        Command="{Binding OpenRouterWindow_Command}" >
                    <Button.Template>
                          <!-- This part turns off button borders/on mouseover animations -->
                        <ControlTemplate TargetType="Button">
                            <ContentPresenter Content="{TemplateBinding Content}"/>
                        </ControlTemplate>
                    </Button.Template>
                    <Grid>
                        <Ellipse HorizontalAlignment="Center"  VerticalAlignment="Center" 
                                 Height="60"                   Width="155"
                                 Stroke="Black"                StrokeThickness="2"
                                 Fill="{Binding RouterStatus.StatusColor, 
                                       Converter={local:Level2Color_Converter}}">
                            <Ellipse.Style>
                                <Style TargetType="{x:Type Ellipse}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding RouterStatus.IsFlashing}" Value="True">
                                            <DataTrigger.EnterActions>
                                                <BeginStoryboard Name="Router_storyboard">
                                                    <BeginStoryboard.Storyboard>
                                                        <Storyboard>
                                                            <ColorAnimation To="{StaticResource StatusBlinked}" 
                                                                            Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)" 
                                                                            AutoReverse="True"
                                                                            RepeatBehavior="Forever"
                                                                            Duration="0:0:0.5"/>
                                                        </Storyboard>
                                                    </BeginStoryboard.Storyboard>
                                                </BeginStoryboard>
                                            </DataTrigger.EnterActions>
                                        </DataTrigger>
                                        <DataTrigger Binding="{Binding RouterStatus.IsFlashing}" Value="False">
                                            <DataTrigger.EnterActions>
                                                <StopStoryboard BeginStoryboardName="Router_storyboard"/>
                                            </DataTrigger.EnterActions>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Ellipse.Style>
                        </Ellipse>
                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" 
                                   FontFamily="Arial"         FontSize="13"
                                   Foreground="White"         Text="ROUTER">
                            <TextBlock.Effect>
                                <DropShadowEffect/>
                            </TextBlock.Effect>
                        </TextBlock>
                    </Grid>
                </Button>

问题是,如果 object 已经在闪烁并且您更改了颜色(例如,从黄色变为红色),它会关闭故事板动画(即使 bool IsFlashing 仍然为真)。

在手动测试中(我有一个测试 window 带有强制 color/flashing 状态更改的按钮),我通过以下方式解决了这个问题:

            if (mockup_IsBlinking)
            {
                  // Toggle is blinking to keep it blinking
                mockup_IsBlinking = false;
                mockup_IsBlinking = true;
            }

也就是说,如果它正在闪烁,请关闭闪烁然后再次打开以重新启动动画。

对于实际测试(从我正在监视的应用程序获取状态更新),我修改了 ColorStatus 以通过修改 setter:

来模拟相同的方法
    public class ColorStatus: INotifyPropertyChanged
    {
        private eStatusColor _StatusColor;
        public  eStatusColor  StatusColor {   get
                                              {
                                                  return _StatusColor;
                                              }
                                              set 
                                              { 
                                                  _StatusColor = value; 
                                                  if (IsFlashing)
                                                  {
                                                      IsFlashing = false;
                                                      IsFlashing = true;
                                                  }
                                              }     
                                          }
        public bool         IsFlashing    {get; set;}
    
          // ...
    }

但是,当我连接到我正在监视的实际应用程序时,我相信 children 状态更新的冲击如此之快以至于动画不同步(我们得到 135 children 添加为 NORMAL,这会导致 top-level 按钮形状从 INACTIVE 变为 NORMAL,IsFlashing=false,然后实际的 child 状态更新出现,3 个 NORMAL 到 WARNING,IsFlashing=true 和 27 个 NORMAL 到 CRITICAL使用 IsFlashing=true)。当我启动监视器时,~ 1/5 的时间顶层闪烁是正确的(闪烁),4/5 的时间是不正确的(IsFlashing 是真的,但故事板动画不工作)。

注意,children状态的接收没有固定的顺序(我们得到的是顺序 它们是从生产应用程序发送的)。另请注意,这是一个 multi-threaded 应用程序(reader 线程排队 消息到消息处理程序线程,它更新 ViewModel,并通过 INotifyPropertyChanged 更新 GUI。

有没有办法解决这个问题,如果 IsFlashing 为真,故事板动画会继续,而不管填充颜色如何变化? (或者在颜色变化时不同步?)

最初,我使用了 Clemens 的评论,在它后面放置一个黑色椭圆,并将动画与不透明度相关联,同时将颜色与填充相关联。如果只有少数项目闪烁,这很好。

但是有这么多项目闪烁,似乎性能受到影响(至少在调试器中),而且所有项目在不同时间进入动画时都“不同步”。

所以,我把动画全部去掉了。相反,我启动了一个 Heartbeat 线程,它每秒切换一个 bool 值 FlashMaster。在我的 .xaml 中,我使用 Elipse.Fill MultiBinding:

<Ellipse.Fill>
    <MultiBinding Converter="{StaticResource LevelBoolFlash2Color_Converter}">
        <Binding Path="MrGWColorStatus.StatusColor"/>
        <Binding Path="MrGWColorStatus.IsFlashing"/>
        <Binding Path="AppVM.FlashMaster"/>
    </MultiBinding>
</Ellipse.Fill>

其中 StatusColor 是状态级别(当前颜色),IsFlashing 是表示是否应该闪烁的布尔值,FlashMaster 是每秒切换的布尔值。

这是转换器:

/// <summary>
/// A converter that takes in 
///   [0] StatusLevel enum 
///   [1] IsFlashing boolean 
///   [2] FlashMaster boolean 
/// and converts it to the correct WPF brush for Normal/Warning/Critical/Inactive (if not Error bool), or StatusERROR if error.
/// </summary>
public class LevelBoolFlash2Color_Converter   : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        SolidColorBrush returnValue;

        if ( (bool)values[1] && (bool)values[2])
        {
              // Blink is True, so use StatusBlinked (black) instead of using eStatusColor
            returnValue = new SolidColorBrush((Color)Application.Current.FindResource("StatusBlinked"));
        }
        else
        {
              // Use the status color
            switch ((eStatusColor)values[0])
            {
                case eStatusColor.NORMAL:   returnValue = new SolidColorBrush((Color)Application.Current.FindResource("StatusNormal"  )); break;
                case eStatusColor.WARNING:  returnValue = new SolidColorBrush((Color)Application.Current.FindResource("StatusWarning" )); break;
                case eStatusColor.CRITICAL: returnValue = new SolidColorBrush((Color)Application.Current.FindResource("StatusCritical")); break;
                default:                    returnValue = new SolidColorBrush((Color)Application.Current.FindResource("StatusInactive")); break;
            }
        }

        return returnValue;
    }

    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

其中 StatusBlinked 为黑色,.NORMAL 为绿色,.WARNING 为黄色,.CRITICAL 为红色,默认为灰色