1. 程式人生 > 其它 >WPF 製作支援點選穿透的高效能的透明背景異形視窗

WPF 製作支援點選穿透的高效能的透明背景異形視窗

技術標籤:WPFc#dotnetcoredotnetC#WPF

預設的 WPF 的支援點選穿透的透明背景視窗,是通過 AllowsTransparency 實現的,但是此方法的效能比較低。本文來告訴大家一個高效能的方法,通過此方法制作出來的 WPF 視窗可以獲取很高的效能,設定透明和設定視窗不透明之間幾乎沒有效能差別

本文的方法由 少珺 小夥伴提供,我只是代為整理部落格。本文的方法是基於 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 但是 walterlv 大大的方法沒有提供可穿透的功能,而本文是提供了全穿透的功能

預設的 WPF 提供的 AllowsTransparency 的方法,這個方法可以適用在讓視窗透明的部分能點選穿透,視窗不透明部分點選不穿透。但根據 WPF 從最底層原始碼瞭解 AllowsTransparency 效能差的原因 可以瞭解到此方法的效能比較低

本文提供的方法是使用 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 來實現高效能的,同時通過 WS_EX_TRANSPARENT 設定整個視窗全穿透

因此本文的方法是要麼整個視窗透明不穿透,要麼就是整個視窗透明穿透。而做不到和 WPF 提供的 AllowsTransparency 的方法讓透明的部分支援穿透。但本文的方法的效能特別強

在開始之前,請完全抄襲 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 這篇部落格的內容

接下來給上面的這個方法新增支援全視窗點選穿透功能,因為本文使用到 WS_EX_TRANSPARENT 的方法設定視窗全穿透,此時需要給視窗加上 WS_EX_LAYERED 樣式。而在 WPF 中,如果視窗在未設定 AllowsTransparency = true 時,會自動去掉 WS_EX_LAYERED 樣式。根據完全開源的 WPF 倉庫,可以找到這段邏輯,放在 HwndTarget 類,如下面程式碼

    public class HwndTarget : CompositionTarget
    {
        /// <summary>
        /// The HwndTarget needs to see all windows messages so that
        /// it can appropriately react to them.
        /// </summary>
        internal IntPtr HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam)
        {
            switch (msg)
            {
                	// 忽略其他程式碼
                case WindowMessage.WM_STYLECHANGING:
                    unsafe
                    {
                        NativeMethods.STYLESTRUCT * styleStruct = (NativeMethods.STYLESTRUCT *) lparam;

                        if ((int)wparam == NativeMethods.GWL_EXSTYLE)
                        {
                        	// 這裡的 UsesPerPixelOpacity 屬性就是由 AllowsTransparency 決定的
                            if(UsesPerPixelOpacity)
                            {
                                // We need layered composition to accomplish per-pixel opacity.
                                //
                                styleStruct->styleNew |= NativeMethods.WS_EX_LAYERED;
                            }
                            else
                            {
                                // No properties that require layered composition exist.
                                // Make sure the layered bit is off.
                                //
                                // Note: this prevents an external program from making
                                // us system-layered (if we are a top-level window).
                                //
                                // If we are a child window, we still can't stop our
                                // parent from being made system-layered, and we will
                                // end up leaving visual artifacts on the screen under
                                // WindowsXP.
                                //
                                styleStruct->styleNew &= (~NativeMethods.WS_EX_LAYERED);
                            }
                        }
                    }

                    break;
             }
        }
    }

為了能夠讓 WPF 支援在沒有設定 AllowsTransparency = true 時也能設定為 WS_EX_LAYERED 樣式,就需要使用一點 Hack 的程式碼,感謝 少珺 小夥伴找到這個有趣的方法。在 WPF 機制裡面,新增 AddHook 執行邏輯是有順序的,而上面程式碼的 HandleMessage 其實也是一個訊息迴圈的 Hook 的邏輯。為了讓 WPF 支援設定 WS_EX_LAYERED 樣式,可以在上面 HwndTarget 的邏輯執行完成之後,執行咱自己的邏輯,再設定一遍。此時因為咱的邏輯在 HwndTarget 之後執行,因此咱的邏輯就覆蓋了 HwndTarget 的設定

在視窗的 Loaded 事件裡面新增下面程式碼

        private void PerformanceDesktopTransparentWindow_Loaded(object sender, RoutedEventArgs e)
        {
            ((HwndSource)PresentationSource.FromVisual(this)).AddHook((IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) =>
            {
                //想要讓視窗透明穿透滑鼠和觸控等,需要同時設定 WS_EX_LAYERED 和 WS_EX_TRANSPARENT 樣式,
                //確保視窗始終有 WS_EX_LAYERED 這個樣式,並在開啟穿透時設定 WS_EX_TRANSPARENT 樣式
                //但是WPF視窗在未設定 AllowsTransparency = true 時,會自動去掉 WS_EX_LAYERED 樣式(在 HwndTarget 類中),
                //如果設定了 AllowsTransparency = true 將使用WPF內建的低效能的透明實現,
                //所以這裡通過 Hook 的方式,在不使用WPF內建的透明實現的情況下,強行保證這個樣式存在。
                if (msg == (int)Win32.WM.STYLECHANGING && (long)wParam == (long)Win32.GetWindowLongFields.GWL_EXSTYLE)
                {
                    var styleStruct = (STYLESTRUCT)Marshal.PtrToStructure(lParam, typeof(STYLESTRUCT));
                    styleStruct.styleNew |= (int)Win32.ExtendedWindowStyles.WS_EX_LAYERED;
                    Marshal.StructureToPtr(styleStruct, lParam, false);
                    handled = true;
                }
                return IntPtr.Zero;
            });
        }

此時就完成了讓視窗設定 WS_EX_LAYERED 這個樣式的功能了,以上程式碼完成之後,在設定視窗是否點選穿透,就可以用上 WS_EX_TRANSPARENT 樣式了,如下面程式碼

        /// <summary>
        /// 設定點選穿透到後面透明的視窗
        /// </summary>
        public void SetTransparentHitThrough()
        {
            if (_dwmEnabled)
            {
                Win32.User32.SetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE,
                    (IntPtr)(int)((long)Win32.User32.GetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE) | (long)Win32.ExtendedWindowStyles.WS_EX_TRANSPARENT));
            }
            else
            {
                Background = Brushes.Transparent;
            }
        }

        /// <summary>
        /// 設定點選命中,不會穿透到後面的視窗
        /// </summary>
        public void SetTransparentNotHitThrough()
        {
            if (_dwmEnabled)
            {
                Win32.User32.SetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE,
                    (IntPtr)(int)((long)Win32.User32.GetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE) & ~(long)Win32.ExtendedWindowStyles.WS_EX_TRANSPARENT));
            }
            else
            {
                Background = BrushCreator.GetOrCreate("#0100000");
            }
        }

通過 WS_EX_TRANSPARENT 樣式,就能設定視窗是否全穿透。上面程式碼用到了我定義的 Win32 的相關方法,這部分程式碼很多用到了 Enum 列舉的二進位制計算方法,因此看起來相對複雜一點

細心的小夥伴會看到,其實我是區分了 _dwmEnabled 才決定是否使用 WS_EX_TRANSPARENT 的方式設定透明,原因是 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 的方法只支援在有開啟 DWM 的模式下才能用上,否則透明部分會顯示黑色

判斷是否開啟 DWM 可以使用 Dwmapi.dll 提供的 DwmIsCompositionEnabled 方法,如下面程式碼

        public static class Dwmapi
        {
            public const string LibraryName = "Dwmapi.dll";

            [DllImport(LibraryName, ExactSpelling = true, PreserveSig = false)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool DwmIsCompositionEnabled();
        }

在 win7 系統,可以動態更改這個值。但是在 Win10 系統預設都是開啟的

如果沒有開啟 DwmIsCompositionEnabled 那麼依然只能使用 AllowsTransparency 的方式設定透明

本文的沒有在部落格寫的程式碼包括了,如何設定視窗樣式以及 win32 方法的定義,這些程式碼我都放在 github 歡迎小夥伴訪問,這裡麵包含了所有邏輯,包括部落格裡面沒有放的程式碼

儘管上面程式碼有點 Hack 但我已經在嘗試在產品級使用了,暫時還沒有發現什麼鍋

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

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

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

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