WPF 制作支持点击穿透的高性能的透明背景异形窗口

Posted lindexi_gd

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

以上是关于WPF 制作支持点击穿透的高性能的透明背景异形窗口的主要内容,如果未能解决你的问题,请参考以下文章

Windows界面编程第五篇 静态控件背景透明化

WPF:如何使用透明画布和可点击子项制作覆盖控件

使WPF窗口的一部分点击,而不是它的控件

Qml 透明窗口,设置不规则等透明窗口,鼠标可穿透到桌面

Qml 透明窗口,设置不规则等透明窗口,鼠标可穿透到桌面

使用Layered分层窗口实现视频会议中的桌面区域共享