【UWP】讓 UWP 自己托管自己 —— Windows App SDK 篇
眾所周知,UWP 使用的窗口模型是 CoreWindow,但是 UWP 本身只是一個應用模型,所以完全可以創建 win32 窗口,那么我們可以不可以創建一個 win32 窗口,然后像 XAML 島 (XAML Islands) 一樣把 XAML 托管上去呢?本篇將講述如何利用 WAS (Windows App SDK,俗稱 WinUI3) 在 UWP 創建一個 XAML 島窗口。

演示視頻:https://x.com/wherewhere7/status/1721570411388039587
由于 WAS 在 win32 應用模型下本身就是個 XAML 島,所以 WAS 對 XAML 島的支持要比 WUXC (Windows.UI.Xaml.Controls) 要好多了,接下來的內容大多是將 WAS 中實現窗口的方法遷移到 C#。
首先,不管是 WUXC 還是 WAS 的 XAML 島都會判斷當前的應用模型是否為ClassicDesktop,所以我們需要利用Detours劫持AppPolicyGetWindowingModel方法。具體內容如下:
#r "nuget:Detours.Win32Metadata"
#r "nuget:Microsoft.Windows.CsWin32"
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Storage.Packaging.Appx;
using Detours = Microsoft.Detours.PInvoke;
/// <summary>
/// Represents a hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
/// </summary>
public sealed partial class HookWindowingModel : IDisposable
{
/// <summary>
/// The value that indicates whether the class has been disposed.
/// </summary>
private bool disposed;
/// <summary>
/// The reference count for the hook.
/// </summary>
private static int refCount;
/// <summary>
/// The value that represents the current process token.
/// </summary>
private const int currentProcessToken = -6;
/// <remarks>The original <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.</remarks>
/// <inheritdoc cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/>
private static unsafe delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR> AppPolicyGetWindowingModel;
/// <summary>
/// Initializes a new instance of the <see cref="HookWindowingModel"/> class.
/// </summary>
public HookWindowingModel()
{
refCount++;
StartHook();
}
/// <summary>
/// Finalizes this instance of the <see cref="HookWindowingModel"/> class.
/// </summary>
~HookWindowingModel()
{
Dispose();
}
/// <summary>
/// Gets the value that indicates whether the hook is active.
/// </summary>
public static bool IsHooked { get; private set; }
/// <summary>
/// Gets or sets the windowing model to use when the hooked <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function is called.
/// </summary>
internal static AppPolicyWindowingModel WindowingModel { get; set; } = AppPolicyWindowingModel.AppPolicyWindowingModel_ClassicDesktop;
/// <summary>
/// Starts the hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
/// </summary>
private static unsafe void StartHook()
{
if (!IsHooked)
{
using FreeLibrarySafeHandle library = PInvoke.GetModuleHandle("KERNEL32.dll");
if (!library.IsInvalid && NativeLibrary.TryGetExport(library.DangerousGetHandle(), nameof(PInvoke.AppPolicyGetWindowingModel), out nint appPolicyGetWindowingModel))
{
void* appPolicyGetWindowingModelPtr = (void*)appPolicyGetWindowingModel;
delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR> overrideAppPolicyGetWindowingModel = &OverrideAppPolicyGetWindowingModel;
_ = Detours.DetourRestoreAfterWith();
_ = Detours.DetourTransactionBegin();
_ = Detours.DetourUpdateThread(PInvoke.GetCurrentThread());
_ = Detours.DetourAttach(ref appPolicyGetWindowingModelPtr, overrideAppPolicyGetWindowingModel);
_ = Detours.DetourTransactionCommit();
AppPolicyGetWindowingModel = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR>)appPolicyGetWindowingModelPtr;
IsHooked = true;
}
}
}
/// <summary>
/// Ends the hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
/// </summary>
private static unsafe void EndHook()
{
if (--refCount == 0 && IsHooked)
{
void* appPolicyGetWindowingModelPtr = AppPolicyGetWindowingModel;
delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR> overrideAppPolicyGetWindowingModel = &OverrideAppPolicyGetWindowingModel;
_ = Detours.DetourTransactionBegin();
_ = Detours.DetourUpdateThread(PInvoke.GetCurrentThread());
_ = Detours.DetourDetach(&appPolicyGetWindowingModelPtr, overrideAppPolicyGetWindowingModel);
_ = Detours.DetourTransactionCommit();
AppPolicyGetWindowingModel = null;
IsHooked = false;
}
}
/// <param name="policy">A pointer to a variable of the <a >AppPolicyWindowingModel</a> enumerated type.
/// When the function returns successfully, the variable contains the <see cref="WindowingModel"/> when the identified process is current; otherwise, the windowing model of the identified process.</param>
/// <remarks>The overridden <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.</remarks>
/// <inheritdoc cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/>
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static unsafe WIN32_ERROR OverrideAppPolicyGetWindowingModel(HANDLE processToken, AppPolicyWindowingModel* policy)
{
if ((int)processToken.Value == currentProcessToken)
{
*policy = WindowingModel;
return WIN32_ERROR.ERROR_SUCCESS;
}
return AppPolicyGetWindowingModel(processToken, policy);
}
/// <inheritdoc/>
public void Dispose()
{
if (!disposed && IsHooked)
{
EndHook();
}
GC.SuppressFinalize(this);
disposed = true;
}
}
準備工作完成,接下來我們就可以創建窗口了,如果順利的話我們只需要new Microsoft.UI.Xaml.Window()就行了,但是很遺憾,經過測試在 UWP 并不能正常初始化這個類,有可能是我使用的方法不太正確,或許以后可能能找到正常使用的辦法,不過現在我們只能去手動創建一個 Win32 窗口了。
首先我們需要新創建一個線程,CoreWindow 線程無法新建 XAML 島,不過在 XAML 島線程可以,新建線程只需要用Thread就行了。
new Thread(() => { ... });
WAS 提供了AppWindow來管理 win32 窗口,我們只需要使用它創建一個窗口就行了。
AppWindow window = AppWindow.Create();
接下來我們需要創建 XAML 島,這時我們就需要利用上面劫持器來劫持獲取應用模型的方法了。
DispatcherQueueController controller;
DesktopWindowXamlSource source;
using (HookWindowingModel hook = new())
{
controller = DispatcherQueueController.CreateOnCurrentThread();
source = new DesktopWindowXamlSource();
}
然后我們就可以把 XAML 島糊到之前創建的 AppWindow 上了。
source.Initialize(window.Id);
DesktopChildSiteBridge bridge = source.SiteBridge;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
bridge.Show();
DispatcherQueue dispatcherQueue = controller.DispatcherQueue;
window.AssociateWithDispatcherQueue(dispatcherQueue);
由于 XAML 島存在的一些特性,當窗口擴展標題欄或者全屏化的時候窗口內容并不會跟著變化,所以我們需要一些小魔法來讓它在變化時調整大小。
window.Changed += (sender, args) =>
{
if (args.DidPresenterChange)
{
bridge.ResizePolicy = ContentSizePolicy.None;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
}
};
最后不要忘了保持當前線程,不然這里跑完了窗口就退出了。
dispatcherQueue.RunEventLoop();
await controller.ShutdownQueueAsync();
當窗口關閉后記得執行DispatcherQueue.EnqueueEventLoopExit()來釋放保持的線程。
最后把之前的東西組合起來,再加點東西:
/// <summary>
/// Create a new <see cref="DesktopWindow"/> instance.
/// </summary>
/// <param name="launched">Do something after <see cref="DesktopWindowXamlSource"/> created.</param>
/// <returns>The new instance of <see cref="DesktopWindow"/>.</returns>
public static Task<DesktopWindow> CreateAsync(Action<DesktopWindowXamlSource> launched)
{
TaskCompletionSource<DesktopWindow> taskCompletionSource = new();
new Thread(async () =>
{
try
{
DispatcherQueueController controller;
DesktopWindowXamlSource source;
AppWindow window = AppWindow.Create();
using (HookWindowingModel hook = new())
{
controller = DispatcherQueueController.CreateOnCurrentThread();
source = new DesktopWindowXamlSource();
}
source.Initialize(window.Id);
DesktopChildSiteBridge bridge = source.SiteBridge;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
bridge.Show();
window.Changed += (sender, args) =>
{
if (args.DidPresenterChange)
{
bridge.ResizePolicy = ContentSizePolicy.None;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
}
};
DispatcherQueue dispatcherQueue = controller.DispatcherQueue;
window.AssociateWithDispatcherQueue(dispatcherQueue);
TrackWindow(window);
launched(source);
DesktopWindow desktopWindow = new()
{
AppWindow = window,
WindowXamlSource = source
};
taskCompletionSource.SetResult(desktopWindow);
dispatcherQueue.RunEventLoop();
await controller.ShutdownQueueAsync();
}
catch (Exception e)
{
taskCompletionSource.SetException(e);
}
})
{
Name = nameof(DesktopWindowXamlSource)
}.Start();
return taskCompletionSource.Task;
}
/// <summary>
/// Create a new <see cref="DesktopWindow"/> instance.
/// </summary>
/// <param name="dispatcherQueue">The <see cref="DispatcherQueue"/> to provide thread.</param>
/// <param name="launched">Do something after <see cref="DesktopWindowXamlSource"/> created.</param>
/// <returns>The new instance of <see cref="DesktopWindow"/>.</returns>
public static Task<DesktopWindow> CreateAsync(DispatcherQueue dispatcherQueue, Action<DesktopWindowXamlSource> launched)
{
TaskCompletionSource<DesktopWindow> taskCompletionSource = new();
_ = dispatcherQueue.TryEnqueue(() =>
{
try
{
DesktopWindowXamlSource source;
AppWindow window = AppWindow.Create();
window.AssociateWithDispatcherQueue(dispatcherQueue);
TrackWindow(window);
using (HookWindowingModel hook = new())
{
source = new DesktopWindowXamlSource();
}
source.Initialize(window.Id);
DesktopChildSiteBridge bridge = source.SiteBridge;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
bridge.Show();
window.Changed += (sender, args) =>
{
if (args.DidPresenterChange)
{
bridge.ResizePolicy = ContentSizePolicy.None;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
}
};
launched(source);
DesktopWindow desktopWindow = new()
{
AppWindow = window,
WindowXamlSource = source
};
taskCompletionSource.SetResult(desktopWindow);
}
catch (Exception e)
{
taskCompletionSource.SetException(e);
}
});
return taskCompletionSource.Task;
}
private static void TrackWindow(AppWindow window)
{
if (ActiveDesktopWindows.ContainsKey(window.DispatcherQueue))
{
ActiveDesktopWindows[window.DispatcherQueue] += 1;
}
else
{
ActiveDesktopWindows[window.DispatcherQueue] = 1;
}
window.Destroying -= AppWindow_Destroying;
window.Destroying += AppWindow_Destroying;
}
private static void AppWindow_Destroying(AppWindow sender, object args)
{
if (ActiveDesktopWindows.TryGetValue(sender.DispatcherQueue, out ulong num))
{
num--;
if (num == 0)
{
ActiveDesktopWindows.Remove(sender.DispatcherQueue);
sender.DispatcherQueue.EnqueueEventLoopExit();
return;
}
ActiveDesktopWindows[sender.DispatcherQueue] = num;
}
}
private static Dictionary<DispatcherQueue, ulong> ActiveDesktopWindows { get; } = [];
其中DesktopWindow是用來存放AppWindow和DesktopWindowXamlSource的類,如果不嫌麻煩的話可以包裹成一個和Microsoft.UI.Xaml.Window一樣的東西。
本文來自博客園,作者:where-where,轉載請注明原文鏈接:http://www.rzrgm.cn/wherewhere/p/18446822

浙公網安備 33010602011771號