默认的 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 欢迎小伙伴访问,这里面包含了所有逻辑,包括博客里面没有放的代码。可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 67f3f2ac002be5ab54f09045ed0999034bf38f78

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 67f3f2ac002be5ab54f09045ed0999034bf38f78

获取代码之后,进入 RuhuyagayBemkaijearfear 文件夹

尽管上面代码有点 Hack 但我已经在尝试在产品级使用了,暂时还没有发现什么锅


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/WPF-%E5%88%B6%E4%BD%9C%E6%94%AF%E6%8C%81%E7%82%B9%E5%87%BB%E7%A9%BF%E9%80%8F%E7%9A%84%E9%AB%98%E6%80%A7%E8%83%BD%E7%9A%84%E9%80%8F%E6%98%8E%E8%83%8C%E6%99%AF%E5%BC%82%E5%BD%A2%E7%AA%97%E5%8F%A3.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利