WPF - 如何通过半透明层创建点击

WPF - how to create click through semi transparent layer

我想要一个类似这样的屏幕录制软件。

我的示例 wpf window 看起来像这样

<Window x:Class="WpfTestApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfTestApp"
    mc:Ignorable="d"
    ShowInTaskbar="False" WindowStyle="None" ResizeMode="NoResize"
    AllowsTransparency="True" 
    UseLayoutRounding="True"
    Opacity="1" 
    Cursor="ScrollAll" 
    Topmost="True"
    WindowState="Maximized"
    >
<Window.Background>
    <SolidColorBrush Color="#01ffffff" Opacity="0" />
</Window.Background>

<Grid>

    <Canvas x:Name="canvas1">
        <Path Fill="#CC000000" Cursor="Cross" x:Name="backgroundPath">
            <Path.Data>
                <CombinedGeometry GeometryCombineMode="Exclude">
                    <CombinedGeometry.Geometry1>
                        <RectangleGeometry Rect="0,0,1440,810"/>
                    </CombinedGeometry.Geometry1>
                    <CombinedGeometry.Geometry2>
                        <RectangleGeometry Rect="300,200,800,300" />
                    </CombinedGeometry.Geometry2>
                </CombinedGeometry>
            </Path.Data>
        </Path>
    </Canvas>

</Grid>

现在的问题是我无法让半透明区域backgroundPath点击通过。我已经将它的 IsHitTestVisible 属性 设置为 false,但仍然没有变化。我已经使用 SetWindowLong 使整个 window 透明,这让我可以点击 window,但是我的 window 的所有事件和其中的控件都不会没用。

任何人都可以建议我如何实现这一目标吗?

无法让 window 的 部分 在视觉上都是半透明的 对用户交互透明(例如鼠标点击)。

您要么必须:

  • 使整个 window 对用户交互透明(使用 SetWindowLongCreateParams 等)
  • 或使所需的 window 部分完全透明

解决此问题的方法是手动绘制半透明区域,而无需 window。这将是一项艰巨的工作,据我所知,没有可靠的方法可以做到这一点。 Windows DWM 不为此提供任何 public API,直接在桌面的 HDC 上绘图是行不通的,图形硬件并不总是支持覆盖,Direct2D 也不行让你也这样做。

您可以创建两个最顶层 windows 并同步它们的大小。第一个 window 将只有调整大小的控件和处理鼠标输入,里面没有内容。第二个 window 将显示半透明的灰色背景,内部有一个透明区域 - 就像你当前的示例中的 window - 但对于任何鼠标交互都是完全透明的。

我实际上对此很好奇,看起来确实没有 "proper" 或 "official" 方法可以仅在 window 而不是控件上实现透明度。

代替这个,我想出了一个功能有效的解决方案:

MainWindow XAML(我刚刚添加了一个按钮)

<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:test"
        mc:Ignorable="d"
        Title="MainWindow"
        WindowStyle="None"
        AllowsTransparency="True"
        ShowInTaskbar="False" 
        ResizeMode="NoResize"
        UseLayoutRounding="True"
        Opacity="1" 
        Cursor="ScrollAll" 
        Topmost="True"
        WindowState="Maximized">
    <Window.Background>
        <SolidColorBrush Color="#01ffffff" Opacity="0" />
    </Window.Background>
    <Grid>
        <Canvas x:Name="canvas1">
            <Path Fill="#CC000000" Cursor="Cross" x:Name="backgroundPath">
                <Path.Data>
                    <CombinedGeometry GeometryCombineMode="Exclude">
                        <CombinedGeometry.Geometry1>
                            <RectangleGeometry Rect="0,0,1440,810"/>
                        </CombinedGeometry.Geometry1>
                        <CombinedGeometry.Geometry2>
                            <RectangleGeometry Rect="300,200,800,300" />
                        </CombinedGeometry.Geometry2>
                    </CombinedGeometry>
                </Path.Data>
            </Path>
        </Canvas>

        <Button x:Name="My_Button" Width="100" Height="50" Background="White" IsHitTestVisible="True" HorizontalAlignment="Center" VerticalAlignment="Top" Click="Button_Click"/>
    </Grid>
</Window>

MainWindow C#

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Threading;

namespace test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        const int WS_EX_TRANSPARENT = 0x00000020;
        const int GWL_EXSTYLE = (-20);
        public const uint WS_EX_LAYERED = 0x00080000;

        [DllImport("user32.dll")]
        static extern int GetWindowLong(IntPtr hwnd, int index);

        [DllImport("user32.dll")]
        static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool GetCursorPos(ref Win32Point pt);

        [StructLayout(LayoutKind.Sequential)]
        internal struct Win32Point
        {
            public Int32 X;
            public Int32 Y;
        };

        private bool _isClickThrough = true;

        public MainWindow()
        {
            InitializeComponent();

            // List of controls to make clickable. I'm just adding my button.
            List<System.Windows.Controls.Control> controls = new List<System.Windows.Controls.Control>();
            controls.Add(My_Button);

            Thread globalMouseListener = new Thread(() =>
            {
                while (true)
                {
                    Point p1 = GetMousePosition();
                    bool mouseInControl = false;

                    for (int i = 0; i < controls.Count; i++)
                    {
                        Point p2 = new Point();
                        Rect r = new Rect();

                        System.Windows.Controls.Control iControl = controls[i];

                        Dispatcher.BeginInvoke(new Action(() =>
                        {
                            // Get control position relative to window
                            p2 = iControl.TransformToAncestor(this).Transform(new Point(0, 0));

                            // Add window position to get global control position
                            r.X = p2.X + this.Left;
                            r.Y = p2.Y + this.Top;

                            // Set control width/height
                            r.Width = iControl.Width;
                            r.Height = iControl.Height;

                            if (r.Contains(p1))
                            {
                                mouseInControl = true;
                            }

                            if (mouseInControl && _isClickThrough)
                            {
                                _isClickThrough = false;

                                var hwnd = new WindowInteropHelper(this).Handle;
                                SetWindowExNotTransparent(hwnd);
                            }
                            else if (!mouseInControl && !_isClickThrough)
                            {
                                _isClickThrough = true;

                                var hwnd = new WindowInteropHelper(this).Handle;
                                SetWindowExTransparent(hwnd);
                            }
                        }));
                    }

                    Thread.Sleep(15);
                }
            });

            globalMouseListener.Start();
        }

        public static Point GetMousePosition()
        {
            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);
            return new Point(w32Mouse.X, w32Mouse.Y);
        }

        public static void SetWindowExTransparent(IntPtr hwnd)
        {
            var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
            SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT);
        }

        public static void SetWindowExNotTransparent(IntPtr hwnd)
        {
            var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
            SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle & ~WS_EX_TRANSPARENT);
        }

        private void Button_Click(object sender, EventArgs e)
        {
            System.Windows.Forms.MessageBox.Show("hey it worked");
        }

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);
            var hwnd = new WindowInteropHelper(this).Handle;
            SetWindowExTransparent(hwnd);
        }
    }
}

基本上,如果鼠标悬停在某个控件上,我会调用 SetWindowExNotTransparent 将其变成正常的非点击 window。如果鼠标不在控件上,它会使用 SetWindowExTransparent.

将其切换回点击状态

我有一个线程 运行 不断检查全局鼠标位置与全局控件位置(您在其中填写希望能够单击的控件列表)。全局控制位置是通过获取相对于 MainWindow 的控制位置,然后添加 MainWindow.

TopLeft 属性来确定的

当然,这是一个有点 "hacky" 的解决方案。但如果你找到更好的,我会被诅咒!它似乎对我来说工作正常。 (尽管处理奇怪形状的控件可能会变得很奇怪。此代码仅处理矩形控件。)

另外,我只是很快就把它放在一起看它是否可行,所以它不是很干净。一个概念证明,如果你愿意的话。