WPF - 如何创建可点击的半透明层

8

我希望有一个类似于这样的屏幕录制软件。

enter image description here 我的示例wpf窗口看起来像这样

<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将整个窗口设为透明,这样我就可以点击窗口穿透,但这样会导致窗口和其中控件的所有事件都无效。

是否有人能建议我如何实现这一点呢?


获取此项目:https://github.com/MaciekSwiszczowski/PresentationTools,并在Frame.xaml中更改填充有ShadowColor颜色的矩形为透明。这是您需要的吗? - Maciek Świszczowski
2个回答

6
我很想了解这个问题,看起来似乎没有一种“正确”或“官方”的方法来实现窗口透明但不透明控件的效果。
因此,我提出了一个功能有效的解决方案: 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>

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将其转换为普通的非透明窗口。 如果鼠标不在控件上,则使用 SetWindowExTransparent 将其切换回透明状态。
我有一个线程持续检查全局鼠标位置与全局控件位置(您填写要能够单击的控件列表)。 全局控件位置是通过获取相对于 MainWindow 的控件位置,然后添加 MainWindow Top Left 属性来确定的。
当然,这是一种有点“hacky”的解决方案。 但是,如果你能找到更好的方案,请告诉我!并且它似乎对我很好用。(尽管可能难以处理形状奇特的控件。此代码仅处理矩形控件。)
此外,我只是匆忙地把这些内容拼凑在一起看看是否可行,所以代码不是很整洁。这是一个概念证明。

1
感谢@Shane花时间写了详细的答案。这就是我们开发它的方式,它运行得相当不错。但是有一些缺点,特别是鼠标滞后和圆形控件的控制。 - Riz

2

无法使窗口的一部分既视觉上半透明又对用户交互(例如鼠标点击)半透明。

您只能:

  • 使整个窗口对用户交互透明(使用SetWindowLong, CreateParams等)
  • 或使所需的窗口部分完全透明

解决方法是手动绘制半透明区域,而不需要窗口。这将是一项艰巨的工作,并且据我所知,没有可靠的方法来实现此目的。Windows DWM没有为此提供任何公共API,直接在桌面的HDC上绘制不起作用,覆盖层并非所有图形硬件都支持,Direct2D也不允许您这样做。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接