1. 程式人生 > 其它 >dotnet 讀 WPF 原始碼筆記 啟動歡迎介面 SplashScreen 的原理

dotnet 讀 WPF 原始碼筆記 啟動歡迎介面 SplashScreen 的原理

技術標籤:WPF原始碼c#WPFdotnetWPFC#

本文是我在讀 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 層解碼咱傳入的圖片,這樣就支援不做任何優化的圖片都能作為啟動圖

第二步就是將解碼之後的圖片編碼為 BGRA 圖片格式傳給 GDI 圖片物件,這樣就能將咱的圖片作為 GDI 圖片物件能使用的資源

第三步是建立視窗顯示這張 GDI 圖片

回到建立視窗的核心方法 CreateLayeredWindowFromImgBuffer 上,這個方法裡面大量呼叫 WIC 層的邏輯,用來處理圖片的渲染,過程程式碼大概如下,下面程式碼為了方便說明,和 WPF 原始碼有些不相同

        private bool CreateLayeredWindowFromImgBuffer(IntPtr pImgBuffer, long cImgBufferLen, bool topMost)
        {
            bool bSuccess = false;
            IntPtr pImagingFactory = IntPtr.Zero;
            IntPtr pDecoder = IntPtr.Zero;
            IntPtr pIStream = IntPtr.Zero;
            IntPtr pDecodedFrame = IntPtr.Zero;
            IntPtr pBitmapSourceFormatConverter = IntPtr.Zero;
            IntPtr pBitmapFlipRotator = IntPtr.Zero;

            // 建立圖片工廠,也就是獲得 pImagingFactory 物件
            // 在 WPF 裡面使用的 WINCODEC_SDK_VERSION 是 0x0236 一個比較古老的版本,在下文有告訴大家有哪些更新
            UnsafeNativeMethods.WIC.CreateImagingFactory(UnsafeNativeMethods.WIC.WINCODEC_SDK_VERSION,
                out pImagingFactory);

            // 使用 pImagingFactory 圖片工廠創建出一個空的 Stream 返回指標給到 pIStream 變數
            // Use the WIC stream class to wrap the unmanaged pointer
            UnsafeNativeMethods.WIC.CreateStream(pImagingFactory, out pIStream);

            // 使用傳進來的圖片指標和長度,初始化圖片工廠創建出來的 pIStream 物件
            UnsafeNativeMethods.WIC.InitializeStreamFromMemory(pIStream, pImgBuffer, (uint) cImgBufferLen);

            // Create an object that will decode the encoded image
            Guid vendor = Guid.Empty;
            // 拿到編解碼器
            UnsafeNativeMethods.WIC.CreateDecoderFromStream(pImagingFactory, pIStream,
                ref vendor, 0, out pDecoder);

            // Get the frame from the decoder. Most image formats have only a single frame, in the case
            // of animated gifs we are ok with only displaying the first frame of the animation.
            // 從圖片解碼裡面獲取圖片的第一幀,如果是 Gif 圖片也只是顯示第一幀
            UnsafeNativeMethods.WIC.GetFrame(pDecoder, 0, out pDecodedFrame);

            // 獲取格式轉換器
            UnsafeNativeMethods.WIC.CreateFormatConverter(pImagingFactory, out pBitmapSourceFormatConverter);

            // 定義了 32 位的 BGRA 圖片格式,轉換為此格式方便建立視窗使用 GDI 渲染
            // Convert the image from whatever format it is in to 32bpp premultiplied alpha BGRA
            Guid pixelFormat = UnsafeNativeMethods.WIC.WICPixelFormat32bppPBGRA;

            // 初始化轉換器
            UnsafeNativeMethods.WIC.InitializeFormatConverter(pBitmapSourceFormatConverter, pDecodedFrame,
                ref pixelFormat, 0 /*DitherTypeNone*/, IntPtr.Zero,
                0, UnsafeNativeMethods.WIC.WICPaletteType.WICPaletteTypeCustom);
            // Reorient the image
            // 獲取圖片的裁剪和旋轉
            UnsafeNativeMethods.WIC.CreateBitmapFlipRotator(pImagingFactory, out pBitmapFlipRotator);

            // 初始化圖片的裁剪和旋轉特效,此時的 pBitmapFlipRotator 就是最終疊加了特效的圖片
            UnsafeNativeMethods.WIC.InitializeBitmapFlipRotator(pBitmapFlipRotator, pBitmapSourceFormatConverter,
                UnsafeNativeMethods.WIC.WICBitmapTransformOptions.WICBitmapTransformFlipVertical);

            // 獲取圖片的大小,用來在下面建立畫素陣列
            Int32 width, height;
            UnsafeNativeMethods.WIC.GetBitmapSize(pBitmapFlipRotator, out width, out height);

            // 因為一個畫素由 BGRA 格式定義
            Int32 stride = width * 4;

            // 建立一個 GDI 物件,物件的大小通過上面的邏輯拿到
            // initialize the bitmap header
            MS.Win32.NativeMethods.BITMAPINFO bmInfo = new MS.Win32.NativeMethods.BITMAPINFO(width, height, 32 /*bpp*/);
            bmInfo.bmiHeader_biCompression = MS.Win32.NativeMethods.BI_RGB;
            bmInfo.bmiHeader_biSizeImage = (int) (stride * height);

            // 建立 GDI 圖片物件的記憶體填充
            // Create a 32bpp DIB.  This DIB must have an alpha channel for UpdateLayeredWindow to succeed.
            IntPtr pBitmapBits = IntPtr.Zero;
            _hBitmap = UnsafeNativeMethods.CreateDIBSection(new HandleRef(), ref bmInfo, 0 /* DIB_RGB_COLORS*/,
                ref pBitmapBits, null, 0);

            // 從 WIC 解碼器裡面拷貝畫素內容到 GDI 圖片裡面
            // Copy the decoded image to the new buffer which backs the HBITMAP
            Int32Rect rect = new Int32Rect(0, 0, width, height);
            UnsafeNativeMethods.WIC.CopyPixels(pBitmapFlipRotator, ref rect, stride, stride * height, pBitmapBits);

            // 使用 GDI 圖片 _hBitmap 去建立一個視窗
            _hwnd = CreateWindow(_hBitmap, width, height, topMost);

            bSuccess = true;
            // 忽略一些清理資源的程式碼
            return bSuccess;
        }

上面程式碼中的 UnsafeNativeMethods.WIC 就是呼叫 WIC 層的邏輯,在 WPF 中的 WIC 層邏輯和其他 Win32 應用一樣,通過 WindowsCodecs.dll 提供,只是在 UnsafeNativeMethods.WIC.CreateImagingFactory(UnsafeNativeMethods.WIC.WINCODEC_SDK_VERSION, out pImagingFactory); 可以看到 WPF 使用的版本是 0x236 比較古老

通過對比 6.2.9200.21830-Windows_7.0 和 6.3.9600.17415-Windows_8.1 版本的 windowscodecs.dll 可以看到有做了如下的更改

- #define WINCODEC_SDK_VERSION 0x0236
+ #define WINCODEC_SDK_VERSION1 0x0236
+ #define WINCODEC_SDK_VERSION2 0x0237
- DEFINE_GUID(CLSID_WICImagingFactory, 0xcacaf262, 0x9370, 0x4615, 0xa1, 0x3b, 0x9f, 0x55, 0x39, 0xda, 0x4c, 0xa);
+ DEFINE_GUID(CLSID_WICImagingFactory, 0xcacaf262, 0x9370, 0x4615, 0xa1, 0x3b, 0x9f, 0x55, 0x39, 0xda, 0x4c, 0xa);
+ DEFINE_GUID(CLSID_WICImagingFactory1, 0xcacaf262, 0x9370, 0x4615, 0xa1, 0x3b, 0x9f, 0x55, 0x39, 0xda, 0x4c, 0xa);
+ DEFINE_GUID(CLSID_WICImagingFactory2, 0x317d06e8, 0x5f24, 0x433d, 0xbd, 0xf7, 0x79, 0xce, 0x68, 0xd8, 0xab, 0xc2);
+ #if(_WIN32_WINNT >= _WIN32_WINNT_WIN8) || defined(_WIN7_PLATFORM_UPDATE)
+    #define WINCODEC_SDK_VERSION WINCODEC_SDK_VERSION2
+    #define CLSID_WICImagingFactory CLSID_WICImagingFactory2
+ #else
+    #define WINCODEC_SDK_VERSION WINCODEC_SDK_VERSION1
+ #endif

新版本的 WindowsCodecs.dll 更新請看 What's New in WIC - Win32 apps

在呼叫到使用 GDI 圖片建立視窗的邏輯就十分簡單了,都是一些 Win32 的介面呼叫

        private IntPtr CreateWindow(NativeMethods.BitmapHandle hBitmap, int width, int height, bool topMost)
        {
            if (_defWndProc == null)
            {
                _defWndProc = new MS.Win32.NativeMethods.WndProc(UnsafeNativeMethods.DefWindowProc);
            }

            // 基本的 Win32 視窗建立方法,沒啥特別的
            MS.Win32.NativeMethods.WNDCLASSEX_D wndClass = new MS.Win32.NativeMethods.WNDCLASSEX_D();
            wndClass.cbSize = Marshal.SizeOf(typeof(MS.Win32.NativeMethods.WNDCLASSEX_D));
            wndClass.style = 3; /* CS_HREDRAW | CS_VREDRAW */
            wndClass.lpfnWndProc = null;
            wndClass.hInstance = _hInstance;
            wndClass.hCursor = IntPtr.Zero;
            wndClass.lpszClassName = CLASSNAME;
            wndClass.lpszMenuName = string.Empty;
            // 加上訊息迴圈,不然會提示應用停止響應
            wndClass.lpfnWndProc = _defWndProc;

            // We chose to ignore re-registration errors in RegisterClassEx on the off chance that the user
            // wants to open multiple splash screens.
            _wndClass = MS.Win32.UnsafeNativeMethods.IntRegisterClassEx(wndClass);
            if (_wndClass == 0)
            {
            	// 下面程式碼不太合理,於是我就提了一個更改 https://github.com/dotnet/wpf/pull/3923
                if (Marshal.GetLastWin32Error() != 0x582) /* class already registered */
                    throw new Win32Exception();
            }

            // 決定啟動視窗顯示到哪
            int screenWidth = MS.Win32.UnsafeNativeMethods.GetSystemMetrics(SM.CXSCREEN);
            int screenHeight = MS.Win32.UnsafeNativeMethods.GetSystemMetrics(SM.CYSCREEN);
            int x = (screenWidth - width) / 2;
            int y = (screenHeight - height) / 2;

            // 視窗的樣式,核心的就是 WS_EX_LAYERED 和 WS_EX_TOOLWINDOW 樣式
            HandleRef nullHandle = new HandleRef(null, IntPtr.Zero);
            int windowCreateFlags =
                (int) NativeMethods.WS_EX_WINDOWEDGE |
                      NativeMethods.WS_EX_TOOLWINDOW |
                      NativeMethods.WS_EX_LAYERED |
                      // 是否顯示到最前
                      (topMost ? NativeMethods.WS_EX_TOPMOST : 0);

            // 建立視窗
            // CreateWindowEx will either succeed or throw
            IntPtr hWnd =  MS.Win32.UnsafeNativeMethods.CreateWindowEx(
                windowCreateFlags,
                CLASSNAME, SR.Get(SRID.SplashScreenIsLoading),
                MS.Win32.NativeMethods.WS_POPUP | MS.Win32.NativeMethods.WS_VISIBLE,
                x, y, width, height,
                nullHandle, nullHandle, new HandleRef(null, _hInstance), IntPtr.Zero);

            // 將圖片在視窗上顯示出來
            // Display the image on the window
            IntPtr hScreenDC = UnsafeNativeMethods.GetDC(new HandleRef());
            IntPtr memDC = UnsafeNativeMethods.CreateCompatibleDC(new HandleRef(null, hScreenDC));
            IntPtr hOldBitmap = UnsafeNativeMethods.SelectObject(new HandleRef(null, memDC), hBitmap.MakeHandleRef(null).Handle);

            NativeMethods.POINT newSize = new NativeMethods.POINT(width, height);
            NativeMethods.POINT newLocation = new NativeMethods.POINT(x, y);
            NativeMethods.POINT sourceLocation = new NativeMethods.POINT(0, 0);
            _blendFunc = new NativeMethods.BLENDFUNCTION();
            _blendFunc.BlendOp = NativeMethods.AC_SRC_OVER;
            _blendFunc.BlendFlags = 0;
            _blendFunc.SourceConstantAlpha = 255;
            _blendFunc.AlphaFormat = 1; /*AC_SRC_ALPHA*/

            bool result = UnsafeNativeMethods.UpdateLayeredWindow(hWnd, hScreenDC, newLocation, newSize,
                memDC, sourceLocation, 0, ref _blendFunc, NativeMethods.ULW_ALPHA);

            UnsafeNativeMethods.SelectObject(new HandleRef(null, memDC), hOldBitmap);
            UnsafeNativeMethods.ReleaseDC(new HandleRef(), new HandleRef(null, memDC));
            UnsafeNativeMethods.ReleaseDC(new HandleRef(), new HandleRef(null, hScreenDC));

            if (result == false)
            {
                UnsafeNativeMethods.HRESULT.Check(Marshal.GetHRForLastWin32Error());
            }

            return hWnd;
        }

當然了,在 WPF 裡面再快的啟動圖顯示速度都不如 UWP 快,因此 UWP 是系統給的優化,通過 AppFrameHost 顯示的,基本上點選應用立刻開啟

當前的 WPF 在 https://github.com/dotnet/wpf 完全開源,使用友好的 MIT 協議,意味著允許任何人任何組織和企業任意處置,包括使用,複製,修改,合併,發表,分發,再授權,或者銷售。在倉庫裡面包含了完全的構建邏輯,只需要本地的網路足夠好(因為需要下載一堆構建工具),即可進行本地構建

我搭建了自己的部落格 https://blog.lindexi.com/ 歡迎大家訪問,裡面有很多新的部落格。只有在我看到部落格寫成熟之後才會放在csdn或部落格園,但是一旦釋出了就不再更新

如果在部落格看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎大家加入

如有不方便在部落格評論的問題,可以加我 QQ 2844808902 交流

知識共享許可協議
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名林德熙(包含連結:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我聯絡。