本文是我在读 WPF 源代码做的笔记。在 WPF 中的启动界面,为了能让 WPF 的启动界面显示足够快,需要在应用的 WPF 主机还没有启动完成之前就显示出启动图,此时的启动图需要自己解析图片同时也需要自己创建显示窗口
从 WPF 的 src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\SplashScreen.cs
文件可以看到 WPF 的 SplashScreen 的核心逻辑
在 SplashScreen 的构造函数会传入资源名,也就是启动图的资源名,或者加上指定程序集和图片资源名
public SplashScreen(string resourceName) : this(Assembly.GetEntryAssembly(), resourceName)
{
}
public SplashScreen(Assembly resourceAssembly, string resourceName)
{
// 忽略代码
}
当然了,这个设计扩展性不够好哈,不支持指定任意的图片。如果想要指定本地路径的任意图片作为启动图的,可以使用 lsj 提供的 kkwpsv/SplashImage: Fast splash Image with GDI+ in C# 库,当然了,这个库代码量特别少,我推荐大家可以抄抄代码。这个库提供的是高性能的版本,可以在另一个线程中执行,换句话说,就是使用 kkwpsv/SplashImage 作为欢迎界面,是可以做到不占用 WPF 主线程时间的,性能比 WPF 提供的好
在 WPF 的 SplashScreen 的 Show 方法,就是启动图的核心逻辑
先调用 GetResourceStream 从自己的程序集里面读取图片资源的原始 Stream 对象,通过此方式的读取性能特别强,因此不是真的读取到内存里面,而是获取一个指针而已。但是有趣的是在这个方法上面有注释说比 Assembly.GetManifestResourceStream 慢 200-300 毫秒,也许是当年的设备才需要这么长的时间
// This is 200-300 ms slower than Assembly.GetManifestResourceStream() but works with localization.
private UnmanagedMemoryStream GetResourceStream()
在获取到启动图片的 UnmanagedMemoryStream 之后,将使用下面代码转换为指针,用于后续传入给 WIC 层
IntPtr pImageSrcBuffer;
unsafe
{
pImageSrcBuffer = new IntPtr(umemStream.PositionPointer);
}
接下来就是调用 CreateLayeredWindowFromImgBuffer 创建一个窗口然后这个窗口显示图片内容
if (CreateLayeredWindowFromImgBuffer(pImageSrcBuffer, umemStream.Length, topMost) && autoClose == true)
{
Dispatcher.CurrentDispatcher.BeginInvoke(
DispatcherPriority.Loaded,
(DispatcherOperationCallback)ShowCallback,
this);
}
可以看到在调用 CreateLayeredWindowFromImgBuffer 方法成功之后,就会调用 Dispatcher 插入 ShowCallback 函数,在 ShowCallback 里面用来自动关闭启动界面,如下面代码
private static object ShowCallback(object arg)
{
SplashScreen splashScreen = (SplashScreen)arg;
splashScreen.Close(TimeSpan.FromSeconds(0.3));
return null;
}
从上面代码可以看到,在 WPF 中默认的启动图界面将会在 Loaded 完成之后延迟 0.3 秒执行,而具体是什么 Loaded 就不需要关注了。因为通过 BeginInvoke 插入的优先级是 DispatcherPriority.Loaded 优先级,也就是启动过程如果再没有什么比 DispatcherPriority.Loaded 更高的优先级,那就是启动完成了
在 WPF 里面的 SplashScreen 的核心逻辑里面包含以下三步
第一步是通过 WIC 层解码咱传入的图片,这样就支持不做任何优