Search code examples
wpfwpf-controls

How can I make an "overlay" window that allows mouse clicks to pass through to windows below it, but still allow some controls to be clicked?


I am trying to make a window that acts as an "overlay" on top of everything else on my screen. It should not interfere with input to other windows, so I should be able to click through to whatever is under it, with a few exceptions, like small buttons on the overlay that I can click to "dismiss" other UI elements on the overlay.

What I have found, and the problems with each:

  1. Setting Topmost="True" on the window. There is no problem with this. This does exactly what I want, it makes my window always drawn on top of other windows.
  2. Making the window background to {x:Null} and adding AllowsTransparency="True" and WindowStyle="None" on the Window. This allows click through allows clicking through the transparent parts, and allows clicking on buttons that are visible, however, since the click-through only works on invisible things, that's not very useful. Can never, you know, actually display any information on the overlay or that breaks the click-through behavior.
  3. Doing the above, but also setting IsHitTestVisible="False" on elements that I want to be able to click through. This disabled interacting with the controls using the mouse, but it doesn't allow the clicks to pass through to the window beneath it.

  4. Using the SetWindowLong method from this accepted answer. The problem with this is that it makes the entire window click through. You are no longer able to make certain buttons on the overlay clickable.

  5. Using the SetWindowLong method from above, but pass it the hwnd of the button and make the button not transparent to clicks. This does not work. It's actually not even possible to do because unlike in WinForms, WPF buttons are not windows and they do not have window handles.

I've tried toggling the window's extended style to not be "transparent" when the mouse is over the button and make it transparent again when the mouse leaves the button, but that also does not work because when it is set to be transparent then it also does not pick up MouseMove, MouseEnter, or MouseLeave events.


Solution

  • After some more digging around, I found the answer. It's combining what I described in #4 and #5 in the question, but had to do it a different way. I read that WPF controls do not have window handles because wpf controls are not windows like winforms controls are. That's not exactly true. WPF controls do indeed have window handles.

    The code

    WindowsServices class copied from this answer, with an extra method to remove the transparency:

    public static class WindowsServices
    {
      const int WS_EX_TRANSPARENT = 0x00000020;
      const int GWL_EXSTYLE = (-20);
    
      [DllImport("user32.dll")]
      static extern int GetWindowLong(IntPtr hwnd, int index);
    
      [DllImport("user32.dll")]
      static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
    
      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);
      }
    }
    

    *Note: Transparency in this context refers to the the window being transparent to mouse input, not visually transparent. You can still see anything that is rendered on the window, but mouse clicks will be sent to whatever window is behind it.

    Window xaml:

    <Window x:Class="TransClickThrough.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="450" Width="800"
            Topmost="True"
            WindowStyle="None"
            AllowsTransparency="True"
            Background="{x:Null}">
        <Grid>
        <Button x:Name="button1" Content="Button" HorizontalAlignment="Left" Margin="183,136,0,0" VerticalAlignment="Top"/>
      </Grid>
    </Window>
    
    • Topmost="True" causes this window to always be drawn on top even when other windows have focus.
    • WindowStyle="None" removes the window title bar and borders AllowsTransparency="True" does what it's name suggests, it allows the window be to be transparent.
    • Background="{x:Null}" makes it transparent. You could also set it to a SolidColorBrush that has 0 Opacity. Either way works.

    Window code-behind:

    protected override void OnSourceInitialized(EventArgs e)
    {
      base.OnSourceInitialized(e);
    
      // Make entire window and everything in it "transparent" to the Mouse
      var windowHwnd = new WindowInteropHelper(this).Handle;
      WindowsServices.SetWindowExTransparent(windowHwnd);
    
      // Make the button "visible" to the Mouse
      var buttonHwndSource = (HwndSource)HwndSource.FromVisual(btn);
      var buttonHwnd = buttonHwndSource.Handle;
      WindowsServices.SetWindowExNotTransparent(buttonHwnd);
    }