翼度科技»论坛 编程开发 .net 查看内容

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

4

主题

4

帖子

12

积分

新手上路

Rank: 1

积分
12
众所周知,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方法。具体内容如下:
  1. #r "nuget:Detours.Win32Metadata"
  2. #r "nuget:Microsoft.Windows.CsWin32"
  3. using System;
  4. using System.Runtime.CompilerServices;
  5. using System.Runtime.InteropServices;
  6. using Windows.Win32;
  7. using Windows.Win32.Foundation;
  8. using Windows.Win32.Storage.Packaging.Appx;
  9. using Detours = Microsoft.Detours.PInvoke;
  10. /// <summary>
  11. /// Represents a hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
  12. /// </summary>
  13. public sealed partial class HookWindowingModel : IDisposable
  14. {
  15.     /// <summary>
  16.     /// The value that indicates whether the class has been disposed.
  17.     /// </summary>
  18.     private bool disposed;
  19.     /// <summary>
  20.     /// The reference count for the hook.
  21.     /// </summary>
  22.     private static int refCount;
  23.     /// <summary>
  24.     /// The value that represents the current process token.
  25.     /// </summary>
  26.     private const int currentProcessToken = -6;
  27.     /// <remarks>The original <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.</remarks>
  28.     /// <inheritdoc cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/>
  29.     private static unsafe delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR> AppPolicyGetWindowingModel;
  30.     /// <summary>
  31.     /// Initializes a new instance of the <see cref="HookWindowingModel"/> class.
  32.     /// </summary>
  33.     public HookWindowingModel()
  34.     {
  35.         refCount++;
  36.         StartHook();
  37.     }
  38.     /// <summary>
  39.     /// Finalizes this instance of the <see cref="HookWindowingModel"/> class.
  40.     /// </summary>
  41.     ~HookWindowingModel()
  42.     {
  43.         Dispose();
  44.     }
  45.     /// <summary>
  46.     /// Gets the value that indicates whether the hook is active.
  47.     /// </summary>
  48.     public static bool IsHooked { get; private set; }
  49.     /// <summary>
  50.     /// Gets or sets the windowing model to use when the hooked <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function is called.
  51.     /// </summary>
  52.     internal static AppPolicyWindowingModel WindowingModel { get; set; } = AppPolicyWindowingModel.AppPolicyWindowingModel_ClassicDesktop;
  53.     /// <summary>
  54.     /// Starts the hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
  55.     /// </summary>
  56.     private static unsafe void StartHook()
  57.     {
  58.         if (!IsHooked)
  59.         {
  60.             using FreeLibrarySafeHandle library = PInvoke.GetModuleHandle("KERNEL32.dll");
  61.             if (!library.IsInvalid && NativeLibrary.TryGetExport(library.DangerousGetHandle(), nameof(PInvoke.AppPolicyGetWindowingModel), out nint appPolicyGetWindowingModel))
  62.             {
  63.                 void* appPolicyGetWindowingModelPtr = (void*)appPolicyGetWindowingModel;
  64.                 delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR> overrideAppPolicyGetWindowingModel = &OverrideAppPolicyGetWindowingModel;
  65.                 _ = Detours.DetourRestoreAfterWith();
  66.                 _ = Detours.DetourTransactionBegin();
  67.                 _ = Detours.DetourUpdateThread(PInvoke.GetCurrentThread());
  68.                 _ = Detours.DetourAttach(ref appPolicyGetWindowingModelPtr, overrideAppPolicyGetWindowingModel);
  69.                 _ = Detours.DetourTransactionCommit();
  70.                 AppPolicyGetWindowingModel = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR>)appPolicyGetWindowingModelPtr;
  71.                 IsHooked = true;
  72.             }
  73.         }
  74.     }
  75.     /// <summary>
  76.     /// Ends the hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
  77.     /// </summary>
  78.     private static unsafe void EndHook()
  79.     {
  80.         if (--refCount == 0 && IsHooked)
  81.         {
  82.             void* appPolicyGetWindowingModelPtr = AppPolicyGetWindowingModel;
  83.             delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, WIN32_ERROR> overrideAppPolicyGetWindowingModel = &OverrideAppPolicyGetWindowingModel;
  84.             _ = Detours.DetourTransactionBegin();
  85.             _ = Detours.DetourUpdateThread(PInvoke.GetCurrentThread());
  86.             _ = Detours.DetourDetach(&appPolicyGetWindowingModelPtr, overrideAppPolicyGetWindowingModel);
  87.             _ = Detours.DetourTransactionCommit();
  88.             AppPolicyGetWindowingModel = null;
  89.             IsHooked = false;
  90.         }
  91.     }
  92.     /// <param name="policy">A pointer to a variable of the <a target="_blank" href="https://docs.microsoft.com/windows/win32/api/appmodel/ne-appmodel-apppolicywindowingmodel">AppPolicyWindowingModel</a> enumerated type.
  93.     /// 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>
  94.     /// <remarks>The overridden <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.</remarks>
  95.     /// <inheritdoc cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/>
  96.     [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
  97.     private static unsafe WIN32_ERROR OverrideAppPolicyGetWindowingModel(HANDLE processToken, AppPolicyWindowingModel* policy)
  98.     {
  99.         if ((int)processToken.Value == currentProcessToken)
  100.         {
  101.             *policy = WindowingModel;
  102.             return WIN32_ERROR.ERROR_SUCCESS;
  103.         }
  104.         return AppPolicyGetWindowingModel(processToken, policy);
  105.     }
  106.     /// <inheritdoc/>
  107.     public void Dispose()
  108.     {
  109.         if (!disposed && IsHooked)
  110.         {
  111.             EndHook();
  112.         }
  113.         GC.SuppressFinalize(this);
  114.         disposed = true;
  115.     }
  116. }
复制代码
准备工作完成,接下来我们就可以创建窗口了,如果顺利的话我们只需要new Microsoft.UI.Xaml.Window()就行了,但是很遗憾,经过测试在 UWP 并不能正常初始化这个类,有可能是我使用的方法不太正确,或许以后可能能找到正常使用的办法,不过现在我们只能去手动创建一个 Win32 窗口了。
首先我们需要新创建一个线程,CoreWindow 线程无法新建 XAML 岛,不过在 XAML 岛线程可以,新建线程只需要用Thread就行了。
  1. new Thread(() => { ... });
复制代码
WAS 提供了AppWindow来管理 win32 窗口,我们只需要使用它创建一个窗口就行了。
  1. AppWindow window = AppWindow.Create();
复制代码
接下来我们需要创建 XAML 岛,这时我们就需要利用上面劫持器来劫持获取应用模型的方法了。
  1. DispatcherQueueController controller;
  2. DesktopWindowXamlSource source;
  3. using (HookWindowingModel hook = new())
  4. {
  5.     controller = DispatcherQueueController.CreateOnCurrentThread();
  6.     source = new DesktopWindowXamlSource();
  7. }
复制代码
然后我们就可以把 XAML 岛糊到之前创建的 AppWindow 上了。
  1. source.Initialize(window.Id);
  2. DesktopChildSiteBridge bridge = source.SiteBridge;
  3. bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
  4. bridge.Show();
  5. DispatcherQueue dispatcherQueue = controller.DispatcherQueue;
  6. window.AssociateWithDispatcherQueue(dispatcherQueue);
复制代码
由于 XAML 岛存在的一些特性,当窗口扩展标题栏或者全屏化的时候窗口内容并不会跟着变化,所以我们需要一些小魔法来让它在变化时调整大小。
  1. window.Changed += (sender, args) =>
  2. {
  3.     if (args.DidPresenterChange)
  4.     {
  5.         bridge.ResizePolicy = ContentSizePolicy.None;
  6.         bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
  7.     }
  8. };
复制代码
最后不要忘了保持当前线程,不然这里跑完了窗口就退出了。
  1. dispatcherQueue.RunEventLoop();
  2. await controller.ShutdownQueueAsync();
复制代码
当窗口关闭后记得执行DispatcherQueue.EnqueueEventLoopExit()来释放保持的线程。
最后把之前的东西组合起来,再加点东西:
  1. /// <summary>
  2. /// Create a new <see cref="DesktopWindow"/> instance.
  3. /// </summary>
  4. /// <param name="launched">Do something after <see cref="DesktopWindowXamlSource"/> created.</param>
  5. /// <returns>The new instance of <see cref="DesktopWindow"/>.</returns>
  6. public static Task<DesktopWindow> CreateAsync(Action<DesktopWindowXamlSource> launched)
  7. {
  8.     TaskCompletionSource<DesktopWindow> taskCompletionSource = new();
  9.     new Thread(async () =>
  10.     {
  11.         try
  12.         {
  13.             DispatcherQueueController controller;
  14.             DesktopWindowXamlSource source;
  15.             AppWindow window = AppWindow.Create();
  16.             using (HookWindowingModel hook = new())
  17.             {
  18.                 controller = DispatcherQueueController.CreateOnCurrentThread();
  19.                 source = new DesktopWindowXamlSource();
  20.             }
  21.             source.Initialize(window.Id);
  22.             DesktopChildSiteBridge bridge = source.SiteBridge;
  23.             bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
  24.             bridge.Show();
  25.             window.Changed += (sender, args) =>
  26.             {
  27.                 if (args.DidPresenterChange)
  28.                 {
  29.                     bridge.ResizePolicy = ContentSizePolicy.None;
  30.                     bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
  31.                 }
  32.             };
  33.             DispatcherQueue dispatcherQueue = controller.DispatcherQueue;
  34.             window.AssociateWithDispatcherQueue(dispatcherQueue);
  35.             TrackWindow(window);
  36.             launched(source);
  37.             DesktopWindow desktopWindow = new()
  38.             {
  39.                 AppWindow = window,
  40.                 WindowXamlSource = source
  41.             };
  42.             taskCompletionSource.SetResult(desktopWindow);
  43.             dispatcherQueue.RunEventLoop();
  44.             await controller.ShutdownQueueAsync();
  45.         }
  46.         catch (Exception e)
  47.         {
  48.             taskCompletionSource.SetException(e);
  49.         }
  50.     })
  51.     {
  52.         Name = nameof(DesktopWindowXamlSource)
  53.     }.Start();
  54.     return taskCompletionSource.Task;
  55. }
  56. /// <summary>
  57. /// Create a new <see cref="DesktopWindow"/> instance.
  58. /// </summary>
  59. /// <param name="dispatcherQueue">The <see cref="DispatcherQueue"/> to provide thread.</param>
  60. /// <param name="launched">Do something after <see cref="DesktopWindowXamlSource"/> created.</param>
  61. /// <returns>The new instance of <see cref="DesktopWindow"/>.</returns>
  62. public static Task<DesktopWindow> CreateAsync(DispatcherQueue dispatcherQueue, Action<DesktopWindowXamlSource> launched)
  63. {
  64.     TaskCompletionSource<DesktopWindow> taskCompletionSource = new();
  65.     _ = dispatcherQueue.TryEnqueue(() =>
  66.     {
  67.         try
  68.         {
  69.             DesktopWindowXamlSource source;
  70.             AppWindow window = AppWindow.Create();
  71.             window.AssociateWithDispatcherQueue(dispatcherQueue);
  72.             TrackWindow(window);
  73.             using (HookWindowingModel hook = new())
  74.             {
  75.                 source = new DesktopWindowXamlSource();
  76.             }
  77.             source.Initialize(window.Id);
  78.             DesktopChildSiteBridge bridge = source.SiteBridge;
  79.             bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
  80.             bridge.Show();
  81.             window.Changed += (sender, args) =>
  82.             {
  83.                 if (args.DidPresenterChange)
  84.                 {
  85.                     bridge.ResizePolicy = ContentSizePolicy.None;
  86.                     bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
  87.                 }
  88.             };
  89.             launched(source);
  90.             DesktopWindow desktopWindow = new()
  91.             {
  92.                 AppWindow = window,
  93.                 WindowXamlSource = source
  94.             };
  95.             taskCompletionSource.SetResult(desktopWindow);
  96.         }
  97.         catch (Exception e)
  98.         {
  99.             taskCompletionSource.SetException(e);
  100.         }
  101.     });
  102.     return taskCompletionSource.Task;
  103. }
  104. private static void TrackWindow(AppWindow window)
  105. {
  106.     if (ActiveDesktopWindows.ContainsKey(window.DispatcherQueue))
  107.     {
  108.         ActiveDesktopWindows[window.DispatcherQueue] += 1;
  109.     }
  110.     else
  111.     {
  112.         ActiveDesktopWindows[window.DispatcherQueue] = 1;
  113.     }
  114.     window.Destroying -= AppWindow_Destroying;
  115.     window.Destroying += AppWindow_Destroying;
  116. }
  117. private static void AppWindow_Destroying(AppWindow sender, object args)
  118. {
  119.     if (ActiveDesktopWindows.TryGetValue(sender.DispatcherQueue, out ulong num))
  120.     {
  121.         num--;
  122.         if (num == 0)
  123.         {
  124.             ActiveDesktopWindows.Remove(sender.DispatcherQueue);
  125.             sender.DispatcherQueue.EnqueueEventLoopExit();
  126.             return;
  127.         }
  128.         ActiveDesktopWindows[sender.DispatcherQueue] = num;
  129.     }
  130. }
  131. private static Dictionary<DispatcherQueue, ulong> ActiveDesktopWindows { get; } = [];
复制代码
其中DesktopWindow是用来存放 AppWindow 和 DesktopWindowXamlSource的类,如果不嫌麻烦的话可以包裹成一个和 Microsoft.UI.Xaml.Window一样的东西。
最后附上示例应用:https://github.com/wherewhere/CoreAppUWP/tree/muxc

来源:https://www.cnblogs.com/wherewhere/p/18446822
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具