【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方法。具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#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;
}
}

准备工作完成,接下来我们就可以创建窗口了,如果顺利的话我们只需要new Microsoft.UI.Xaml.Window()就行了,但是很遗憾,经过测试在 UWP 并不能正常初始化这个类,有可能是我使用的方法不太正确,或许以后可能能找到正常使用的办法,不过现在我们只能去手动创建一个 Win32 窗口了。

首先我们需要新创建一个线程,CoreWindow 线程无法新建 XAML 岛,不过在 XAML 岛线程可以,新建线程只需要用Thread就行了。

1
new Thread(() => { ... });

WAS 提供了AppWindow来管理 win32 窗口,我们只需要使用它创建一个窗口就行了。

1
AppWindow window = AppWindow.Create();

接下来我们需要创建 XAML 岛,这时我们就需要利用上面劫持器来劫持获取应用模型的方法了

1
2
3
4
5
6
7
8
DispatcherQueueController controller;
DesktopWindowXamlSource source;

using (HookWindowingModel hook = new())
{
controller = DispatcherQueueController.CreateOnCurrentThread();
source = new DesktopWindowXamlSource();
}

然后我们就可以把 XAML 岛糊到之前创建的 AppWindow 上了。

1
2
3
4
5
6
7
source.Initialize(window.Id);
DesktopChildSiteBridge bridge = source.SiteBridge;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
bridge.Show();

DispatcherQueue dispatcherQueue = controller.DispatcherQueue;
window.AssociateWithDispatcherQueue(dispatcherQueue);

由于 XAML 岛存在的一些特性,当窗口扩展标题栏或者全屏化的时候窗口内容并不会跟着变化,所以我们需要一些小魔法来让它在变化时调整大小。

1
2
3
4
5
6
7
8
window.Changed += (sender, args) =>
{
if (args.DidPresenterChange)
{
bridge.ResizePolicy = ContentSizePolicy.None;
bridge.ResizePolicy = ContentSizePolicy.ResizeContentToParentWindow;
}
};

最后不要忘了保持当前线程,不然这里跑完了窗口就退出了。

1
2
dispatcherQueue.RunEventLoop();
await controller.ShutdownQueueAsync();

当窗口关闭后记得执行DispatcherQueue.EnqueueEventLoopExit()来释放保持的线程。

最后把之前的东西组合起来,再加点东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/// <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

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

【UWP】让 UWP 自己托管自己 —— Windows App SDK 篇 作者 @where-where 2024年11月3日 发表于 博客园,转载请注明出处