Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: How to expose/access WinUI Win32 interop APIs? (e.g. IWindowNative::get_WindowHandle) #199

Open
stackotter opened this issue Jan 6, 2025 · 11 comments

Comments

@stackotter
Copy link

I'm trying to access the window handle of a WinUI Window and I'm struggling to figure out how to generate bindings for IWindowNative::get_WindowHandle or even just call it via raw C APIs.

Is this something that's supported by swift-winrt? If so how should I modify my swift-cwinrt and swift-winui projections to generate bindings for IWindowNative? And if not are there any known workarounds?

The part that I'm finding tricky is that IWindowNative is defined in this weird idl directory which only defines a few other types, none of which I can find mentioned in any of the (now archived) pre-generated swift-winui family of bindings (which I searched for examples of bindings for similarly located symbols to no avail).

I've tried adding Microsoft.UI.Xaml.Window to swift-cwinrt's projections.json and regenerating but that didn't help (which I kinda expected given that the docs for IWindowNative::get_WindowHandle aren't in the regular namespaced part of the Windows App SDK docs).


For context, my actual goal is to set a minimum size for a WinUI window, and I've got all of the parts figured out other than getting a native window handle. I believe I need to get window handles to be able to map the window handle my hook receives back to the corresponding Window instance so that I can fetch the minimum size assigned to the window receiving the message.

I've been kinda vaguely trying to replicate this WinUI Window minimum size workaround without Win32 subclassing (cause I figured that that would probably make things even harder given that I have basically zero Windows development experience let alone Win32/WinRT development experience). To do it without Win32 subclassing I've adapted the hooking code from the wonderfully named MainRunLoopTickler and successfully received WM_GETMINMAXSIZE messages.

If anyone has any alternative suggestions for approaches I'd be greatly appreciative!

@stackotter
Copy link
Author

Here's what I have so far. Just including it as a bit more context around my current approach.

import WinSDK
import CWinRT
@_spi(WinRTInternal) import WindowsFoundation

let minSizeHook: HOOKPROC = { (nCode: Int32, wParam: WPARAM, lParam: LPARAM) in
    if nCode >= 0 {
        let ptr = UnsafeRawPointer(bitPattern: Int(lParam))?
            .assumingMemoryBound(to: CWPRETSTRUCT.self)
        if let msgInfo = ptr?.pointee, msgInfo.message == WM_GETMINMAXINFO {
            print("Received WM_GETMINMAXINFO")

            // I believe that I need to get CWinRT to generate a struct for `IWindowNative`
            // somehow?
            var value: __HWND
            _ = try! window._inner.perform(
                as: __x_ABI_CMicrosoft_CUI_CXaml_CIWindowNative.self // Doesn't exist
            ) { pThis in
                try! CHECKED(pThis.pointee.lpVtbl.pointee.get_WindowHandle(pThis, &handle))
            }
        }
    }
    return CallNextHookEx(nil, nCode, wParam, lParam)
}

_ = SetWindowsHookExW(WH_CALLWNDPROCRET, minSizeHook, nil, GetCurrentThreadId())

@tristanlabelle
Copy link
Contributor

Hi @stackotter . It is possible to query COM interfaces on swift-winrt-generated objects, but it's a little involved. In Arc, we do this by creating a WinRT component in C++, which takes the WinRT window type as a parameter, uses QueryInterface for IWindowNative in the C++ code, and calls the necessary methods. Alternatively, you may be able to define __x_ABI_CMicrosoft_CUI_CXaml_CIWindowNative manually in a C++ module, then get the IUnknown pointer in Swift and call QueryInterface. @stevenbrix might have more ideas.

@ktraunmueller
Copy link
Contributor

ktraunmueller commented Jan 21, 2025

@stackotter You probably already came across WinUIEx, but in case not, that repo (although C#) may have some solutions worked out for problems around missing features in WinUI (like the minimum window size) you may run into.

@stackotter
Copy link
Author

Thanks, I hadn't come across that yet! It looks very useful and it has given me some things to try

@stevenbrix
Copy link
Collaborator

stevenbrix commented Jan 23, 2025

You'll have to do a few things:

  1. either manually define the IWindowNative header type in your own C module
  2. or import the headers from the WindowsAppSDK nuget package (you'll still need to do this through your own c module)

Then create the IWindowNative type:

import <SOME_C_MODULE> // module from above
import WindowsFoundation

class IWindowNative: IUnknown {
   func get_WindowHandle() throws -> HWND? {
      var hwnd: HWND?
      _ = try perform(as: CIWindowNative.self) { pThis in
             try CHECKED(pThis.pointee.lpVtbl.pointee.put_ArrayProperty(pThis, @hwnd)
        }
      return hwnd
  }
} 

then something like:

let windowNative: IWindowNative = try! window.thisPtr.QueryInterface()
let windowHandle = windowNative.get_WindowHandle()

@stevenbrix
Copy link
Collaborator

In Arc, we avoided this and get the HWND through the AppWindow object using the Microsoft.UI.Windowing headers. It's cached on the Window due to the expensive access of the AppWindow property.

@stackotter @ktraunmueller how are you building swift projects on Windows? Via SPM or CMake?

@ktraunmueller
Copy link
Contributor

@stevenbrix I'm using SPM.

I more or less copied the Browser Company's Swift/WinRT-based repos after they had been archived. Here's my swift-winui, swift-windowsappsdk, swift-windowsfoundation, swift-uwp, swift-cwinrt and test app repos.

For context, I'm currently porting my Mac app to Windows.

@stackotter
Copy link
Author

You'll have to do a few things:

  1. either manually define the IWindowNative header type in your own C module
  2. or import the headers from the WindowsAppSDK nuget package (you'll still need to do this through your own c module)

Then create the IWindowNative type:

import <SOME_C_MODULE> // module from above
import WindowsFoundation

class IWindowNative: IUnknown {
   func get_WindowHandle() throws -> HWND? {
      var hwnd: HWND?
      _ = try perform(as: CIWindowNative.self) { pThis in
             try CHECKED(pThis.pointee.lpVtbl.pointee.put_ArrayProperty(pThis, @hwnd)
        }
      return hwnd
  }
} 

then something like:

let windowNative: IWindowNative = try! window.thisPtr.QueryInterface()
let windowHandle = windowNative.get_WindowHandle()

Ah thanks! The fact that those symbols are available in the NuGet package is what I was missing. I'll let you know how it goes.

Re spm or cmake: I'm also using SPM (I'm making a cross platform UI framework for other people to use so SPM is my best option)

@stackotter
Copy link
Author

In Arc, we avoided this and get the HWND through the AppWindow object using the Microsoft.UI.Windowing headers. It's cached on the Window due to the expensive access of the AppWindow property.

Which symbols in the Microsoft.UI.Windowing namespace let you access the HWND? I had a quick look through the docs and couldn't find anything

@stevenbrix
Copy link
Collaborator

@stackotter i don't know if they are documented, but if you look at the nuget in the include\Microsoft.UI.Interop.h file

@stevenbrix
Copy link
Collaborator

actually scratch that, we just copy what that header does, but do it in swift:

import CWinRT
import WinAppSDK
import WindowsFoundation
import WinSDK
import WinUI

public func getWindowIDFromWindow(_ hWnd: HWND?) -> WinAppSDK.WindowId {
    Interop.shared.getWindowIDFromWindow(hWnd)
}

public func getWindowFromWindowId(_ windowID: WinAppSDK.WindowId) -> HWND? {
    Interop.shared.getWindowFromWindowId(windowID)
}

extension WinAppSDK.AppWindow {
    /// Returns the window handle for the app window.
    public func getHWND() -> HWND? {
        Interop.shared.getWindowFromWindowId(id)
    }
}

extension WinUI.Window {
    /// Returns the window handle for the window.
    ///
    /// - Note: This is a relatively expensive operation, particularly due to its use
    /// of the `appWindow` getter. If an `AppWindow` is already available, prefer to
    /// use `getHWND()` on that instead; better yet, if the window handle will be used
    /// frequently, assign it to a stored property, as it will not change during the
    /// lifetime of the window.
    public func getHWND() -> HWND? {
        // The appWindow can become nil when a Window is closed.
        guard let appWindow else { return nil }
        return appWindow.getHWND()
    }
}

private struct Interop {
    private typealias pfnGetWindowIdFromWindow = @convention(c) (HWND?, UnsafeMutablePointer<__x_ABI_CMicrosoft_CUI_CWindowId>?) -> HRESULT
    private typealias pfnGetWindowFromWindowId = @convention(c) (__x_ABI_CMicrosoft_CUI_CWindowId, UnsafeMutablePointer<HWND?>?) -> HRESULT
    private var hModule: HMODULE!
    private var getWindowIDFromWindow_impl: pfnGetWindowIdFromWindow!
    private var getWindowFromWindowID_impl: pfnGetWindowFromWindowId!

    static let shared = Interop()

    init() {
        "Microsoft.Internal.FrameworkUdk.dll".withCString(encodedAs: UTF16.self) {
            hModule = GetModuleHandleW($0)
            if hModule == nil {
                hModule = LoadLibraryW($0)
            }
        }

        if let pfn = GetProcAddress(hModule, "Windowing_GetWindowIdFromWindow") {
            getWindowIDFromWindow_impl = unsafeBitCast(pfn, to: pfnGetWindowIdFromWindow.self)
        }

        if let pfn = GetProcAddress(hModule, "Windowing_GetWindowFromWindowId") {
            getWindowFromWindowID_impl = unsafeBitCast(pfn, to: pfnGetWindowFromWindowId.self)
        }
    }

    fileprivate func getWindowIDFromWindow(_ hWnd: HWND?) -> WinAppSDK.WindowId {
        var windowID = __x_ABI_CMicrosoft_CUI_CWindowId()
        let hr: HRESULT = getWindowIDFromWindow_impl(hWnd, &windowID)
        if hr != S_OK {
            fatalError("Unable to get window ID")
        }
        return .init(value: windowID.Value)
    }

    fileprivate func getWindowFromWindowId(_ windowID: WinAppSDK.WindowId) -> HWND? {
        var hWnd: HWND?
        let hr: HRESULT = getWindowFromWindowID_impl(.from(swift: windowID), &hWnd)
        if hr != S_OK {
            fatalError("Unable to get window from window ID")
        }
        return hWnd
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants