WPF - how to create click through semi transparent layer
Asked Answered
M

2

8

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?

Maillol answered 22/8, 2019 at 8:20 Comment(1)
Get this project: github.com/MaciekSwiszczowski/PresentationTools, and in Frame.xaml change rectangles filled with ShadowColor color, to be Transparent. Is that what you need?Thevenot
L
6

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.

Leshia answered 27/8, 2019 at 22:43 Comment(1)
Thanks @Shane for spending time to write detailed answer. This is how we have developed this and it works pretty much fine. But there are few drawback especially about lag of mouse and for controls in circular shapes.Maillol
C
2

There is no way to have a part of a window both visually semi-transparent and transparent for user interaction (e.g. mouse clicks).

You either have to:

  • make the whole window transparent for the user interaction (using SetWindowLong, CreateParams etc)
  • or make the desired window parts fully transparent

A workaround for this is to draw the semi-transparent area manually, without having a window. This is going to be a tough job, and AFAIK there is no reliable method for doing this. The Windows DWM doesn't offer any public API for that, drawing directly on the Desktop's HDC won't do, overlays are not always supported by the graphics hardware, Direct2D won't let you do that either.

You can create two top-most windows and synchronize their size. The first window would only have the controls for resizing and would handle the mouse input, no content inside. The second window would display the semi-transparent grey background with a transparent region inside - just as your current window in your sample - but completely transparent for any mouse interaction.

Carmella answered 27/8, 2019 at 9:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.