【UWP】让 UWP 自己托管自己 —— Windows App SDK 篇

  • 【UWP】让 UWP 自己托管自己 —— Windows App SDK 篇已关闭评论
  • 11 次浏览
  • A+
所属分类:.NET技术
摘要

众所周知,UWP 使用的窗口模型是 CoreWindow,但是 UWP 本身只是一个应用模型,所以完全可以创建 win32 窗口,那么我们可以不可以创建一个 win32 窗口,然后像 XAML 岛 (XAML Islands) 一样把 XAML 托管上去呢?本篇将讲述如何利用 WAS (Windows App SDK,俗称 WinUI3) 在 UWP 创建一个 XAML 岛窗口。

众所周知,UWP 使用的窗口模型是 CoreWindow,但是 UWP 本身只是一个应用模型,所以完全可以创建 win32 窗口,那么我们可以不可以创建一个 win32 窗口,然后像 XAML 岛 (XAML Islands) 一样把 XAML 托管上去呢?本篇将讲述如何利用 WAS (Windows App SDK,俗称 WinUI3) 在 UWP 创建一个 XAML 岛窗口。

【UWP】让 UWP 自己托管自己 —— Windows App SDK 篇

演示视频: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 href="https://docs.microsoft.com/windows/win32/api/appmodel/ne-appmodel-apppolicywindowingmodel">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;     } }

准备工作完成,接下来我们就可以创建窗口了,首先我们需要新创建一个线程,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是用来存放 AppWindowDesktopWindowXamlSource的类,如果不嫌麻烦的话可以包裹成一个和 Microsoft.UI.Xaml.Window一样的东西。

最后附上示例应用:https://github.com/wherewhere/CoreAppUWP/tree/muxc