castorix / WinUI3_SwapChainPanel_Layered

47 stars 2 forks source link

Click through not working with IsAlwaysOnTop = true #5

Open tibitoth opened 1 year ago

tibitoth commented 1 year ago

@castorix I've been very deep in the "WinUI 3 / WinAppSDK transparent click through Window" rabbit hole and I've just found your sample which is really great. Thank you! Only one thing not working right now which I cannot figure out why.

If you set the presenter's IsAlwaysOnTop property to true, click through will stop working.

castorix commented 1 year ago

@castorix I've been very deep in the "WinUI 3 / WinAppSDK transparent click through Window" rabbit hole and I've just found your sample which is really great. Thank you! Only one thing not working right now which I cannot figure out why.

If you set the presenter's IsAlwaysOnTop property to true, click through will stop working.

The problem is that the window get focus back after SwitchToThisWindow because it is TopMost If I test with Explorer, it can keep focus with the following update, but of course the main window, TopMost, stays in foreground :

  if (bOK && tsClickThrough.IsOn)
  {
      System.Threading.Thread.Sleep(100);
      SwitchToThisWindow(hWnd, true);
      System.Threading.Thread.Sleep(100);
  }

TopMostFocus

tibitoth commented 1 year ago

It's not a perfect click through because the underlying windows does not get the mouse event.

castorix commented 1 year ago

It's not a perfect click through because the underlying windows does not get the mouse event.

Yes, I just set the focus You can add the mouse event with SendInput or mouse_event

castorix commented 1 year ago

It's not a perfect click through because the underlying windows does not get the mouse event.

I added SendInput (I let the test for TopMost in comments _//presenter.IsAlwaysOnTop = true; )

Pietro228 commented 1 year ago

I got working click through using this code:

[DllImport("user32.dll")]
private static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);

[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", SetLastError = true)]
private static extern IntPtr IntSetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

[DllImport("user32.dll", EntryPoint = "SetWindowLong", SetLastError = true)]
private static extern Int32 IntSetWindowLong(IntPtr hWnd, int nIndex, Int32 dwNewLong);

private static int IntPtrToInt32(IntPtr intPtr)
{
    return unchecked((int)intPtr.ToInt64());
}

[DllImport("kernel32.dll", EntryPoint = "SetLastError")]
public static extern void SetLastError(int dwErrorCode);

[Flags]
private enum ExtendedWindowStyles
{
    // ...
    WS_EX_TOOLWINDOW = 0x00000080,
    WS_EX_TRANSPARENT = 32
    // ...
}

private enum GetWindowLongFields
{
    // ...
    GWL_EXSTYLE = (-20),
    // ...
}

private static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
{
    int error = 0;
    IntPtr result = IntPtr.Zero;
    // Win32 SetWindowLong doesn't clear error on success
    SetLastError(0);

    if (IntPtr.Size == 4)
    {
        // use SetWindowLong
        Int32 tempResult = IntSetWindowLong(hWnd, nIndex, IntPtrToInt32(dwNewLong));
        error = Marshal.GetLastWin32Error();
        result = new IntPtr(tempResult);
    }
    else
    {
        // use SetWindowLongPtr
        result = IntSetWindowLongPtr(hWnd, nIndex, dwNewLong);
        error = Marshal.GetLastWin32Error();
    }

    if ((result == IntPtr.Zero) && (error != 0))
    {
        throw new System.ComponentModel.Win32Exception(error);
    }

    return result;
}

// This changes clickability
public static void CaptureMouseClick(this Window window, bool condition)
{
    try
    {
        var hwnd = WindowNative.GetWindowHandle(window);

        if (condition)
        {
            int windowLong = (int)WindowInteropHelper.GetWindowLong(hwnd, (int)GetWindowLongFields.GWL_EXSTYLE);
            WindowInteropHelper.SetWindowLong(hwnd, (int)GetWindowLongFields.GWL_EXSTYLE, (IntPtr)(windowLong & -33));
        }
        else
        {
            int windowLong = (int)WindowInteropHelper.GetWindowLong(hwnd, (int)GetWindowLongFields.GWL_EXSTYLE);
            WindowInteropHelper.SetWindowLong(hwnd, (int)GetWindowLongFields.GWL_EXSTYLE, (IntPtr)(windowLong | (int)ExtendedWindowStyles.WS_EX_TRANSPARENT));
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex.Message);
    }
}
Pietro228 commented 1 year ago

https://github.com/castorix/WinUI3_SwapChainPanel_Layered/assets/53868994/10c3be69-e2b7-48da-9bca-5a75498ad103

Slals commented 11 months ago

Hi @Pietro228, where do you put this function CaptureMouseClick(this Window window, bool condition)? In WinUI3 I'm only aware of listening to events in a element of the page. Can this work on a FullscreenPresenter?

The demo you did in the video is a private project? It would really help to see how it's done. Thanks!

Pietro228 commented 11 months ago

Hey @Slals ! You can declare it anywhere you want. I suggest you to make a WindowExtensions.cs file.

This project is private, but this shouldn't be that hard to implement

Slals commented 11 months ago

Thanks for your reply. Which window is expected as parameter?

Actually I'm doing this using the code you provided

private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
    try
    {
        var hwnd = WindowNative.GetWindowHandle(this);

        Debug.WriteLine(hwnd);

        if (this.AppState.IsRecording)
        {
            int windowLong = (int)GetWindowLong(hwnd, (-20));
            SetWindowLong(hwnd, (-20), (IntPtr)(windowLong & -33));
        }
        else
        {
            int windowLong = (int)GetWindowLong(hwnd, (-20));
            SetWindowLong(hwnd, (-20), (IntPtr)(windowLong | 32));
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex.Message);
    }
}

And the event is bind to

// MainWindow.xaml.cs
this.Content.PointerPressed += this.OnPointerPressed;

But doesn't seem to get expected results, the main window of the app is always capturing the mouse pressed. I dont have extensive experience in Windows dev, so I don't really get what does SetWindowLong(hwnd, (-20), (IntPtr)(windowLong & -33)); and the other

Slals commented 10 months ago

I got it working, it only works with the approach in this project using SwapChainPanel, I was using one approach with DWM API. The solution provided by @castorix (https://github.com/castorix/WinUI3_SwapChainPanel_Layered/blob/master/MainWindow.xaml.cs#L721) is quite heavy because for each event it has to loop through all elements. Plus, since it needs overlapped presenter it creates a blink for each events (even without thread sleeping). It's caused by defocusing the main app Window and focusing it again thanks to AlwaysOnTop.

I need to register all events (without capturing them) and send them through the window below the App window.

Your approach @Pietro228 is nice because it doesn't go through SendInput creating this blinking issue. But there is no way - that I can think of - for registering all inputs (even pointer moves) and letting those events go to the window below the App window.

I created a topic about it here https://learn.microsoft.com/en-us/answers/questions/1418063/(winui3)-semi-transparent-window-click-through-win