wpfclick-through

WPF - how to create click through semi transparent layer


I want something like this for a screen recording software.

enter image description here My sample wpf window looks like this

<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>

Now the problem is I can't make the semi transparent area backgroundPath click through. I have set its IsHitTestVisible property to false, but still no change. I have used SetWindowLong to make the whole window transparent, and that lets me click through the window, but that then all the events of my window and controls in it don't work.

Can any one suggest how can I achieve that?


Solution

  • I was actually curious about this and it doesn't look like there really is a "proper" or "official" way to achieve transparency on only the window but not the controls.

    In lieu of this, I came up with a functionally effective solution:

    MainWindow XAML (I just added a button)

    <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);
            }
        }
    }
    

    Basically If the mouse is over a control, I call SetWindowExNotTransparent to turn it into a normal, non click-through window. If the mouse is not over a control, it switches it back to a click-through state with SetWindowExTransparent.

    I have a thread running that continuously checks the global mouse position against global control positions (where you fill a list of controls you want to be able to click). The global control positions are determined by getting the control position relative to MainWindow and then adding the Top and Left attributes of MainWindow.

    Sure, this is a somewhat "hacky" solution. But I'll be damned if you find a better one! And it seems to be working fine for me. (Albeit it might get weird to handle oddly shaped controls. This code only handles rectangular controls.)

    Also I just threw this together really quick to see if it would work, so it's not very clean. A proof of concept, if you will.