microsoft-ui-xaml icon indicating copy to clipboard operation
microsoft-ui-xaml copied to clipboard

Proposal: Add ability to get hosting Window from XamlRoot or UIElement

Open dotMorten opened this issue 1 year ago • 4 comments

Proposal: Add ability to get hosting Window from XamlRoot or UIElement

Summary

Several APIs requires access to the Window handle, but it's currently impossible to get the window that is hosting a visual element. We need an API to make it easier to get the handle.

Rationale

In order to use various pickers like OpenFilePicker, SaveFilePicker etc, you need to first get the Window handle before you can use them as described here.

The approach discussed here for getting that handle requires you to create a MainWindow property on your Application instance in order to do that.

This presents some problems:

  • In a multi-window application, you might not know which one to use.
  • 3rd-party custom controls would not be able to get the handle, since they have no knowledge of the hosting application.
  • Separation of concerns gets a lot harder to handle

One example would be a 3rd party control that includes a save or open button. These buttons would not be able to load or save to/from files, since they have no way of using the file pickers and make them modal to the current window (current as "in the context of that button").

Scope

'Must' implies that the feature should not ship without this capability.

Capability Priority
This proposal will allow developers to access the parent Window or its handle that is currently hosting a visual element Must

dotMorten avatar Jul 25 '24 21:07 dotMorten

You can do something like (GetByVisual returns null on my Windows 10 OS) :

                Microsoft.UI.Composition.Visual visual = Microsoft.UI.Xaml.Hosting.ElementCompositionPreview.GetElementVisual(myButton);
                var ci = Microsoft.UI.Content.ContentIsland.FindAllForCompositor(visual.Compositor);
                //var ci = Microsoft.UI.Content.ContentIsland.GetByVisual(visual);
                if (ci[0] != null)
                {
                    IntPtr hWndHost = Win32Interop.GetWindowFromWindowId(ci[0].Environment.AppWindowId);
                }

castorix avatar Jul 26 '24 00:07 castorix

XamlRoot.ContentIslandEnvironment.AppWindowId can be also be used here. However, I think functionality should be added to get the XAML window from a UIElement.

lhak avatar Jul 26 '24 13:07 lhak

XamlRoot.ContentIslandEnvironment.AppWindowId can be also be used here. However, I think functionality should be added to get the XAML window from a UIElement.

excelent, is there any limitation or Are there any special situation that lead to a null result? i want to use it in my class library

ghost1372 avatar Jul 28 '24 13:07 ghost1372

also there is another way if there is no uielement!

public static AppWindow GetCurrentAppWindow()
{
    var tops = GetProcessWindowList();

    var firstWinUI3 = tops.FirstOrDefault(w => w.ClassName == "WinUIDesktopWin32WindowClass");

    var windowId = Win32Interop.GetWindowIdFromWindow(firstWinUI3.Handle);

    return AppWindow.GetFromWindowId(windowId);
}

public static IReadOnlyList<Win32Window> GetProcessWindowList()
{
    var process = Process.GetCurrentProcess();
    var list = new List<Win32Window>();
    NativeMethods.EnumWindows((h, l) =>
    {
        var window = new Win32Window(h);
        if (window.ProcessId == process.Id)
        {
            list.Add(window);
        }
        return true;
    }, IntPtr.Zero);
    return list.AsReadOnly();
}

public class Win32Window
{
    public Win32Window(IntPtr handle)
    {
        Handle = handle;
        ThreadId = NativeMethods.GetWindowThreadProcessId(handle, out var processId);
        ProcessId = processId;
    }

    public IntPtr Handle { get; }
    public int ThreadId { get; }
    public int ProcessId { get; }
    public string ClassName => WindowHelper.GetClassName(Handle);
    public string Text => WindowHelper.GetWindowText(Handle);
    public bool IsEnabled => NativeMethods.IsWindowEnabled(Handle);

    public override string ToString()
    {
        var s = ClassName;
        var text = Text;
        if (text != null)
        {
            s += " '" + text + "'";
        }
        return s;
    }
}
public static string GetWindowText(IntPtr hwnd)
{
    var sb = new StringBuilder(1024);
    NativeMethods.GetWindowText(hwnd, sb, sb.Capacity - 1);
    return sb.ToString();
}

public static string GetClassName(IntPtr hwnd)
{
    var sb = new StringBuilder(256);
    NativeMethods.GetClassName(hwnd, sb, sb.Capacity - 1);
    return sb.ToString();
}

public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);

[DllImport(ExternDll.User32, SetLastError = true)]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

[DllImport(ExternDll.User32)]
public static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);

[DllImport(ExternDll.User32)]
public static extern bool IsWindowEnabled(IntPtr hwnd);

[DllImport(ExternDll.User32, CharSet = CharSet.Unicode)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

[DllImport(ExternDll.User32, CharSet = CharSet.Unicode)]
public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

ghost1372 avatar Aug 01 '24 07:08 ghost1372