WPF白屏实战2

在WPF主界面启动之前的动画效果过度到主界面之时,一直出现白屏
代码如下:

    <UserControl.Resources>
        <CubicEase x:Key="EaseOutCirc" EasingMode="EaseOut" />
        <QuadraticEase x:Key="EaseInOutQuad" EasingMode="EaseInOut" />
    </UserControl.Resources>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Grid x:Name="ContentGrid" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Grid.RenderTransform>
                <ScaleTransform ScaleX="{Binding Scale}" ScaleY="{Binding Scale}" />
            </Grid.RenderTransform>
            <Grid.Opacity>
                <Binding Path="Opacity" />
            </Grid.Opacity>

            <Ellipse Width="300" Height="300" Fill="{DynamicResource PrimaryHueMidBrush}" Opacity="0.3">
                <Ellipse.RenderTransform>
                    <ScaleTransform CenterX="150" CenterY="150" />
                </Ellipse.RenderTransform>
                <Ellipse.Triggers>
                    <EventTrigger RoutedEvent="Loaded">
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <DoubleAnimation
                                    Storyboard.TargetProperty="Opacity"
                                    From="0.8" To="0.2"
                                    Duration="0:0:2"
                                    AutoReverse="True"
                                    EasingFunction="{StaticResource EaseInOutQuad}" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
                                    From="1" To="1.2"
                                    Duration="0:0:2"
                                    AutoReverse="True"
                                    EasingFunction="{StaticResource EaseInOutQuad}" />
                                <DoubleAnimation
                                    Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                                    From="1" To="1.2"
                                    Duration="0:0:2"
                                    AutoReverse="True"
                                    EasingFunction="{StaticResource EaseInOutQuad}" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Ellipse.Triggers>
            </Ellipse>

            <Ellipse Width="200" Height="200" Stroke="{DynamicResource PrimaryHueLightBrush}" StrokeThickness="2" Fill="Transparent">
                <Ellipse.RenderTransform>
                    <RotateTransform CenterX="100" CenterY="100" />
                </Ellipse.RenderTransform>
                <Ellipse.Triggers>
                    <EventTrigger RoutedEvent="Loaded">
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <DoubleAnimation
                                    Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                    From="0" To="360"
                                    Duration="0:0:8"
                                    EasingFunction="{StaticResource EaseOutCirc}" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Ellipse.Triggers>
            </Ellipse>

            <Ellipse Width="160" Height="160" Stroke="{DynamicResource PrimaryHueLightForegroundBrush}" StrokeThickness="1" Fill="Transparent" Opacity="0.5">
                <Ellipse.RenderTransform>
                    <RotateTransform CenterX="80" CenterY="80" />
                </Ellipse.RenderTransform>
                <Ellipse.Triggers>
                    <EventTrigger RoutedEvent="Loaded">
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <DoubleAnimation
                                    Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                    From="360" To="0"
                                    Duration="0:0:6"
                                    EasingFunction="{StaticResource EaseOutCirc}" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Ellipse.Triggers>
            </Ellipse>

            <Path Width="200" Height="200" Stroke="{DynamicResource PrimaryHueLightBrush}" StrokeThickness="4" StrokeDashCap="Round" Fill="Transparent"
                  Data="M 100,100 L 100,10 A 90,90 0 1,1 99.99,10 Z">
                <Path.RenderTransform>
                    <RotateTransform CenterX="100" CenterY="100" />
                </Path.RenderTransform>
                <Path.Triggers>
                    <EventTrigger RoutedEvent="Loaded">
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="StrokeDashOffset">
                                    <LinearDoubleKeyFrame Value="0" KeyTime="0:0:0" />
                                    <LinearDoubleKeyFrame Value="-283" KeyTime="0:0:2" />
                                </DoubleAnimationUsingKeyFrames>
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Path.Triggers>
            </Path>

            <Grid Width="80" Height="80">
                <Grid.RenderTransform>
                    <RotateTransform CenterX="40" CenterY="40" />
                </Grid.RenderTransform>
                <Grid.Triggers>
                    <EventTrigger RoutedEvent="Loaded">
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <DoubleAnimation
                                    Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                    From="0" To="360"
                                    Duration="0:0:4"
                                    EasingFunction="{StaticResource EaseInOutQuad}" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Grid.Triggers>
                <Path Width="60" Height="60" Fill="{DynamicResource PrimaryHueLightForegroundBrush}" Stretch="Uniform"
                      Data="M30,10 L34,10 L34,50 L30,50 L30,10 Z M42,20 L46,20 L46,50 L42,50 L42,20 Z M54,30 L58,30 L58,50 L54,50 L54,30 Z M20,40 A10,10 0 0,1 40,40 A10,10 0 0,1 20,40 Z" />
            </Grid>

            <StackPanel Margin="0,240,0,0" HorizontalAlignment="Center" VerticalAlignment="Top">
                <TextBlock Text="{Binding LoadingText}" FontSize="16" FontWeight="Light"
                           Foreground="{DynamicResource PrimaryHueLightForegroundBrush}" TextAlignment="Center">
                    <TextBlock.Triggers>
                        <EventTrigger RoutedEvent="Loaded">
                            <BeginStoryboard>
                                <Storyboard RepeatBehavior="Forever">
                                    <DoubleAnimation
                                        Storyboard.TargetProperty="Opacity"
                                        From="0.6" To="1"
                                        Duration="0:0:1.5"
                                        AutoReverse="True"
                                        EasingFunction="{StaticResource EaseInOutQuad}" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </TextBlock.Triggers>
                </TextBlock>
                <TextBlock Text="{Binding Version}" FontSize="12"
                           Foreground="{DynamicResource PrimaryHueLightForegroundBrush}" Opacity="0.5"
                           TextAlignment="Center" Margin="0,5,0,0" />
            </StackPanel>
        </Grid>
    </Grid>
 public class SplashViewModel : BindableBase
 {
     public ICommand LoadedCommand { get; set; }
     private readonly IEventAggregator _eventAggregator;

     private string _loadingText = "正在加载...";

     public string LoadingText
     {
         get => _loadingText;
         set => SetProperty(ref _loadingText, value);
     }

     private string _version = "v1.0.0";

     public string Version
     {
         get => _version;
         set => SetProperty(ref _version, value);
     }

     private double _scale = 1;

     public double Scale
     {
         get => _scale;
         set => SetProperty(ref _scale, value);
     }

     private double _opacity = 1;

     public double Opacity
     {
         get => _opacity;
         set => SetProperty(ref _opacity, value);
     }

     private string[] _loadingTexts = new[] { "正在加载...", "正在初始化...", "正在连接数据库...", "即将完成..." };
     private int _textIndex = 0;
     private System.Windows.Threading.DispatcherTimer _textTimer;

     public SplashViewModel(IEventAggregator eventAggregator)
     {
         LoadedCommand = new DelegateCommand(ExecuteLoaded);
         _eventAggregator = eventAggregator;
     }

     private void ExecuteLoaded()
     {
         StartTextRotation();
         InitTask();
     }

     private void StartTextRotation()
     {
         _textTimer = new System.Windows.Threading.DispatcherTimer();
         _textTimer.Interval = TimeSpan.FromSeconds(1.5);
         _textTimer.Tick += (s, e) =>
         {
             _textIndex = (_textIndex + 1) % _loadingTexts.Length;
             LoadingText = _loadingTexts[_textIndex];
         };
         _textTimer.Start();
     }

     private void InitTask()
     {
         Task.Run(async () =>
         {
             await Task.Delay(100);
         }).ContinueWith(x =>
         {
             Application.Current.Dispatcher.BeginInvoke(new Action(FadeOut));
         }, TaskScheduler.FromCurrentSynchronizationContext());
     }

     private void FadeOut()
     {
         _textTimer?.Stop();

         var fadeTimer = new System.Windows.Threading.DispatcherTimer();
         fadeTimer.Interval = TimeSpan.FromMilliseconds(16);
         double elapsed = 0;
         double duration = 500;

         fadeTimer.Tick += (s, e) =>
         {
             elapsed += 16;
             double progress = Math.Min(elapsed / duration, 1);
             double easedProgress = 1 - Math.Pow(1 - progress, 3);

             Opacity = 1 - easedProgress;
             Scale = 1 - easedProgress * 0.2;

             if (progress >= 1)
             {
                 fadeTimer.Stop();
                 _eventAggregator.GetEvent<SplashEvent>().Publish();
             }
         };

         fadeTimer.Start();
     }
 }

改动之后:

  <UserControl.Resources>
      <CubicEase x:Key="EaseOutCirc" EasingMode="EaseOut" />
      <QuadraticEase x:Key="EaseInOutQuad" EasingMode="EaseInOut" />
  </UserControl.Resources>
  <i:Interaction.Triggers>
      <i:EventTrigger EventName="Loaded">
          <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
      </i:EventTrigger>
  </i:Interaction.Triggers>
  <Grid>
      <Grid x:Name="ContentGrid" HorizontalAlignment="Center" VerticalAlignment="Center">
          <Grid.RenderTransform>
              <ScaleTransform ScaleX="{Binding Scale}" ScaleY="{Binding Scale}" />
          </Grid.RenderTransform>
          <Grid.Opacity>
              <Binding Path="Opacity" />
          </Grid.Opacity>

          <Ellipse x:Name="OuterEllipse" Width="300" Height="300" Fill="{DynamicResource PrimaryHueMidBrush}" Opacity="0.3">
              <Ellipse.RenderTransform>
                  <ScaleTransform CenterX="150" CenterY="150" />
              </Ellipse.RenderTransform>
          </Ellipse>

          <Ellipse x:Name="MiddleEllipse" Width="200" Height="200" Stroke="{DynamicResource PrimaryHueLightBrush}" StrokeThickness="2" Fill="Transparent">
              <Ellipse.RenderTransform>
                  <RotateTransform CenterX="100" CenterY="100" />
              </Ellipse.RenderTransform>
          </Ellipse>

          <Ellipse x:Name="InnerEllipse" Width="160" Height="160" Stroke="{DynamicResource PrimaryHueLightForegroundBrush}" StrokeThickness="1" Fill="Transparent" Opacity="0.5">
              <Ellipse.RenderTransform>
                  <RotateTransform CenterX="80" CenterY="80" />
              </Ellipse.RenderTransform>
          </Ellipse>

          <Path x:Name="ArcPath" Width="200" Height="200" Stroke="{DynamicResource PrimaryHueLightBrush}" StrokeThickness="4" StrokeDashCap="Round" Fill="Transparent"
                Data="M 100,100 L 100,10 A 90,90 0 1,1 99.99,10 Z">
              <Path.RenderTransform>
                  <RotateTransform CenterX="100" CenterY="100" />
              </Path.RenderTransform>
          </Path>

          <Grid x:Name="IconGrid" Width="80" Height="80">
              <Grid.RenderTransform>
                  <RotateTransform CenterX="40" CenterY="40" />
              </Grid.RenderTransform>
              <Path Width="60" Height="60" Fill="{DynamicResource PrimaryHueLightForegroundBrush}" Stretch="Uniform"
                    Data="M30,10 L34,10 L34,50 L30,50 L30,10 Z M42,20 L46,20 L46,50 L42,50 L42,20 Z M54,30 L58,30 L58,50 L54,50 L54,30 Z M20,40 A10,10 0 0,1 40,40 A10,10 0 0,1 20,40 Z" />
          </Grid>

          <StackPanel Margin="0,240,0,0" HorizontalAlignment="Center" VerticalAlignment="Top">
              <TextBlock x:Name="LoadingText" Text="{Binding LoadingText}" FontSize="16" FontWeight="Light"
                         Foreground="{DynamicResource PrimaryHueLightForegroundBrush}" TextAlignment="Center" />
              <TextBlock Text="{Binding Version}" FontSize="12"
                         Foreground="{DynamicResource PrimaryHueLightForegroundBrush}" Opacity="0.5"
                         TextAlignment="Center" Margin="0,5,0,0" />
          </StackPanel>
      </Grid>
  </Grid>
 public class SplashViewModel : BindableBase
 {
     public ICommand LoadedCommand { get; set; }
     private readonly IEventAggregator _eventAggregator;

     private string _loadingText = "正在加载...";

     public string LoadingText
     {
         get => _loadingText;
         set => SetProperty(ref _loadingText, value);
     }

     private string _version = "v1.0.0";

     public string Version
     {
         get => _version;
         set => SetProperty(ref _version, value);
     }

     private double _scale = 1;

     public double Scale
     {
         get => _scale;
         set => SetProperty(ref _scale, value);
     }

     private double _opacity = 1;

     public double Opacity
     {
         get => _opacity;
         set => SetProperty(ref _opacity, value);
     }

     private string[] _loadingTexts = new[] { "正在加载...", "正在初始化...", "正在连接数据库...", "即将完成..." };
     private int _textIndex = 0;
     private System.Windows.Threading.DispatcherTimer _textTimer;

     public SplashViewModel(IEventAggregator eventAggregator)
     {
         LoadedCommand = new DelegateCommand(ExecuteLoaded);
         _eventAggregator = eventAggregator;
     }

     private void ExecuteLoaded()
     {
         StartTextRotation();
         InitTask();
     }

     private void StartTextRotation()
     {
         _textTimer = new System.Windows.Threading.DispatcherTimer();
         _textTimer.Interval = TimeSpan.FromSeconds(1.5);
         _textTimer.Tick += (s, e) =>
         {
             _textIndex = (_textIndex + 1) % _loadingTexts.Length;
             LoadingText = _loadingTexts[_textIndex];
         };
         _textTimer.Start();
     }

     private void InitTask()
     {
         Task.Run(async () =>
         {
             await Task.Delay(100);
         }).ContinueWith(x =>
         {
             Application.Current.Dispatcher.BeginInvoke(new Action(CompleteSplash));
         }, TaskScheduler.FromCurrentSynchronizationContext());
     }

     private void CompleteSplash()
     {
         _textTimer?.Stop();
         _eventAggregator.GetEvent<SplashEvent>().Publish();
     }
 }

核心原因:属性控制权的冲突

首先要理解 WPF 中依赖属性的优先级

text

1. 动画(Storyboard)—— 最高优先级
2. 本地值(Local Value)—— 如 Opacity = 0.5
3. 绑定(Binding)—— 如 {Binding Opacity}

关键点:当一个属性被 Storyboard 驱动时,绑定的值会被覆盖


原代码为什么会白屏?

XAML 中发生的事:

xml

<Grid Opacity="{Binding Opacity}">  <!-- 绑定到 ViewModel -->
    <Ellipse.Triggers>
        <EventTrigger>
            <BeginStoryboard>
                <DoubleAnimation 
                    Storyboard.TargetProperty="Opacity"
                    From="0.8" To="0.2" />
            </BeginStoryboard>
        </EventTrigger>
    </Ellipse.Triggers>
</Grid>

注意:动画是作用在 Ellipse 上,不是 Grid

但实际上,所有的 Storyboard 都会启动并持续运行,虽然它们作用在不同元素上,但它们共同导致了一个现象:

整个 ContentGrid 的渲染管道被动画系统占据了。

ViewModel 中发生的事:

csharp

private void FadeOut()
{
    // 通过 DispatcherTimer 逐帧修改属性
    Opacity = 1 - easedProgress;  // ← 试图修改 Grid.Opacity
    Scale = 1 - easedProgress * 0.2;  // ← 试图修改 Grid.Scale
}

冲突点:

时间点 ViewModel 正在做 Storyboard 正在做 结果
0-100ms 初始化中 所有动画正常运行 ✅ 正常
100ms FadeOut() 开始修改 OpacityScale 动画仍然在运行(虽然作用在其他元素上) ⚠️ 属性被动画系统锁定
100-600ms 不断赋值 Opacity 从 1→0 Storyboard 仍在运行 ViewModel 的赋值被忽略
600ms Publish() 切换界面 动画突然停止 💥 白屏出现

为什么白屏?

  1. OpacityScale 没有真正变化,因为被 Storyboard 系统锁定了
  2. 切换瞬间,Splash 界面被卸载,但属性还没来得及变化到最终值
  3. 主界面还没完全渲染好
  4. 中间出现短暂空白 → 白屏

改动一:只改 XAML(去掉所有 Storyboard)

xml

<!-- 改动前 -->
<Ellipse>
    <Ellipse.Triggers>
        <EventTrigger>
            <BeginStoryboard>...</BeginStoryboard>
        </EventTrigger>
    </Ellipse.Triggers>
</Ellipse>

<!-- 改动后 -->
<Ellipse x:Name="OuterEllipse" ... />  <!-- 只保留名字,去掉 Triggers -->

为什么白屏消失?

因为去掉了所有 Storyboard,动画系统不再锁定任何属性。

现在:

  • Grid.OpacityGrid.Scale 完全由 ViewModel 的 Binding 控制
  • FadeOut() 中的 Opacity = 1 - easedProgress正常生效
  • 淡出效果完整执行
  • 切换时 Splash 已完全透明 → 无白屏

改动二:只改 ViewModel(去掉 FadeOut,直接切换)

csharp

// 改动前
private void FadeOut()
{
    // 500ms 的逐帧动画
    Opacity = 1 - easedProgress;
    Scale = 1 - easedProgress * 0.2;
    // 500ms 后切换
}

// 改动后
private void CompleteSplash()
{
    _textTimer?.Stop();
    _eventAggregator.GetEvent<SplashEvent>().Publish();  // 立即切换
}

为什么白屏消失?

因为不再尝试修改被动画锁定的属性

原来的问题是:

  1. Storyboard 在运行
  2. ViewModel 试图修改 OpacityScale
  3. 修改无效 → 属性没变化 → 切换时白屏

现在:

  1. Storyboard 仍在运行(动画继续)
  2. ViewModel 不修改任何属性
  3. 直接切换界面
  4. 虽然 Storyboard 在运行,但没有属性冲突
  5. 切换干净利落 → 无白屏

关键对比

方案 XAML 改动 ViewModel 改动 为什么有效
只改 XAML ✅ 去掉所有 Triggers ❌ 保留 FadeOut 属性完全由 Binding 控制,动画不锁定属性
只改 ViewModel ❌ 保留所有 Triggers ✅ 去掉 FadeOut,直接切换 没有属性冲突,动画系统不被干扰
两个都改 ✅ 去掉所有 Triggers ✅ 去掉 FadeOut 最干净,但非必须

本质总结

白屏的根源是:ViewModel 试图修改被 Storyboard 锁定的属性。

解决方案有两个方向:

方向一:解除锁定(改 XAML)

  • 去掉所有 Storyboard
  • 属性完全由 Binding 控制
  • ViewModel 可以自由修改属性
  • 优点:有淡出效果
  • 缺点:失去了所有装饰动画(外圈脉动、旋转等)

方向二:避免冲突(改 ViewModel)

  • 保留 Storyboard
  • ViewModel 不修改任何被动画控制的属性
  • 直接切换,不做淡出
  • 优点:保留了所有装饰动画
  • 缺点:没有淡出效果(但切换足够快,人眼感知不到白屏)

所以真正的问题是

你原来同时在两个地方控制属性:

  1. XAML 的 Storyboard(虽然作用在其他元素,但整体占用了动画系统)
  2. ViewModel 的代码(试图修改 Grid 的属性)

当两者冲突时,动画系统优先,ViewModel 的修改被忽略,导致切换时出现白屏。

任何一种改动,只要打破了这种冲突,白屏就会消失。


如果想同时保留:装饰动画 + 淡出效果

需要让淡出动画也由 Storyboard 控制(而不是 ViewModel 的代码),这样所有属性都由动画系统统一管理,没有冲突。

xml

<!-- 在 ContentGrid 上添加淡出 Storyboard -->
<Grid.Triggers>
    <EventTrigger RoutedEvent="Loaded">
        <BeginStoryboard x:Name="FadeOutStoryboard">
            <Storyboard>
                <DoubleAnimation 
                    Storyboard.TargetProperty="Opacity"
                    From="1" To="0" Duration="0:0:0.5"/>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Grid.Triggers>

然后在 ViewModel 中:

  1. 不要修改 OpacityScale
  2. 直接触发这个 Storyboard
  3. 在 Storyboard 的 Completed 事件中切换界面

这样所有动画都由 Storyboard 统一控制,属性只有一个控制源,就不会有冲突和白屏了。


总结:白屏不是 bug,是 WPF 属性优先级机制导致的必然结果。解决方案就是消除属性控制的冲突。

posted @ 2026-07-01 20:47  孤沉  阅读(2)  评论(0)    收藏  举报