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() 开始修改 Opacity 和 Scale |
动画仍然在运行(虽然作用在其他元素上) | ⚠️ 属性被动画系统锁定 |
| 100-600ms | 不断赋值 Opacity 从 1→0 |
Storyboard 仍在运行 | ❌ ViewModel 的赋值被忽略 |
| 600ms | Publish() 切换界面 |
动画突然停止 | 💥 白屏出现 |
为什么白屏?
Opacity和Scale没有真正变化,因为被 Storyboard 系统锁定了- 切换瞬间,Splash 界面被卸载,但属性还没来得及变化到最终值
- 主界面还没完全渲染好
- 中间出现短暂空白 → 白屏
改动一:只改 XAML(去掉所有 Storyboard)
xml
<!-- 改动前 -->
<Ellipse>
<Ellipse.Triggers>
<EventTrigger>
<BeginStoryboard>...</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
<!-- 改动后 -->
<Ellipse x:Name="OuterEllipse" ... /> <!-- 只保留名字,去掉 Triggers -->
为什么白屏消失?
因为去掉了所有 Storyboard,动画系统不再锁定任何属性。
现在:
Grid.Opacity和Grid.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(); // 立即切换
}
为什么白屏消失?
因为不再尝试修改被动画锁定的属性。
原来的问题是:
- Storyboard 在运行
- ViewModel 试图修改
Opacity和Scale - 修改无效 → 属性没变化 → 切换时白屏
现在:
- Storyboard 仍在运行(动画继续)
- ViewModel 不修改任何属性
- 直接切换界面
- 虽然 Storyboard 在运行,但没有属性冲突
- 切换干净利落 → 无白屏
关键对比
| 方案 | XAML 改动 | ViewModel 改动 | 为什么有效 |
|---|---|---|---|
| 只改 XAML | ✅ 去掉所有 Triggers | ❌ 保留 FadeOut | 属性完全由 Binding 控制,动画不锁定属性 |
| 只改 ViewModel | ❌ 保留所有 Triggers | ✅ 去掉 FadeOut,直接切换 | 没有属性冲突,动画系统不被干扰 |
| 两个都改 | ✅ 去掉所有 Triggers | ✅ 去掉 FadeOut | 最干净,但非必须 |
本质总结
白屏的根源是:ViewModel 试图修改被 Storyboard 锁定的属性。
解决方案有两个方向:
方向一:解除锁定(改 XAML)
- 去掉所有 Storyboard
- 属性完全由 Binding 控制
- ViewModel 可以自由修改属性
- 优点:有淡出效果
- 缺点:失去了所有装饰动画(外圈脉动、旋转等)
方向二:避免冲突(改 ViewModel)
- 保留 Storyboard
- ViewModel 不修改任何被动画控制的属性
- 直接切换,不做淡出
- 优点:保留了所有装饰动画
- 缺点:没有淡出效果(但切换足够快,人眼感知不到白屏)
所以真正的问题是
你原来同时在两个地方控制属性:
- XAML 的 Storyboard(虽然作用在其他元素,但整体占用了动画系统)
- 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 中:
- 不要修改
Opacity和Scale - 直接触发这个 Storyboard
- 在 Storyboard 的
Completed事件中切换界面
这样所有动画都由 Storyboard 统一控制,属性只有一个控制源,就不会有冲突和白屏了。
总结:白屏不是 bug,是 WPF 属性优先级机制导致的必然结果。解决方案就是消除属性控制的冲突。

浙公网安备 33010602011771号