Using the WinRT XAML hosting API in a C++ desktop (Win32) app
Is the WinRT XAML hosting API the right choice for your desktop app?
对于想用XAML的Win32桌面程序,只能直接使用XAML Island的用编口。
对于WPF和WinForms,强烈建议使用https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/xaml-islands#wpf-and-windows-forms-applications。
Learn how to use the XAML Hosting API
……
Samples
C++ desktop (Win32)
https://github.com/microsoft/Xaml-Islands-Samples/tree/master/Standalone_Samples/CppWinRT_Basic_Win32App展示在非MSIX包装的C++桌面程序中使用XAML Islands。
https://github.com/microsoft/Xaml-Islands-Samples/tree/master/Samples/Win32展示如何在MSIX封包的桌面程序中使用自定义控件。
WPF and Windows Forms
……
Architecture of the API
- WindowsXamlManager,表征UWP XAML框架,提供一个静态的InitializeForCurrentThread方法
- DesktopWindowXamlSource,表征一个寄宿的UWP XAML内容,最重要的辖属是Content。可以设置一个Windows.UI.Xaml.UIElement。
- IDesktopWindowXamlSourceNative,此COM接口提供了AttachToWindow方法,可以增加一个XAML Island到上级UI元素。 DesktopWindowXamlSource 实现了此方法
- IDesktopWindowXamlSourceNative2,此COM接口提供了 PreTranslateMessage 方法,使得UWP XAML框架可以正确处理某些窗口消息。DesktopWindowXamlSource 实现了此方法
寄宿有XAML Island的UI元素必须有一个窗口控把HWND。例如:
- Win32中的窗口
- WPF中的System.Windows.Interop.HwndHost
- WinForms中的System.Windows.Forms.Control
HWND的下一级是DesktopWindowXamlSource标的,其会自动创建一个子窗口用于承载WinRT的XAML控件。你的代码中不需要处理此子窗口,但是可以获取此子窗口的HWND。
最内侧是寄宿的WinRT XAM控件,从Windows.UI.Xaml.UIElement派生处理的。
同一桌面应用可以在多处使用XAML Island,因此会有多个XAML树根。
Best practices
- 每个线程创建一个专有的WindowsXamlManager
- 对于每个XAML控件,创建一个DesktopWindowXamlSource.
- 不需要DesktopWindowXamlSource的时候需要将其销毁
- 退出线程之前需要销毁WindowsXamlManager。这个消耗过程是异步的,记得需要清空消息队列。
- 销毁WindowsXamlManager后,就没办法在同一个线程再创建之。
Troubleshooting
……
Host a standard WinRT XAML control in a C++ desktop (Win32) app
需要安装以下NuGet料包
- Microsoft.Toolkit.Win32.UI.SDK
- Microsoft.Windows.CppWinRT
注意点:
- 在Application Manifests里面将maxversiontested设置成为"10.0.18632.0"
- 可以在工程中添加一个名为app.manifest的文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<maxversiontested Id="10.0.18362.0"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
说是要在工程中添加引导到C:\Program Files (x86)\Windows Kits\10\UnionMetadat
中的对应版本的Windows.winmd,不过感觉我用的C++/WinRT版本自己找到Windows.winmd了。
Use the XAML hosting API to host a WinRT XAML control
- 首先要初始化WinRT的XAML框架,有几个做法
- 如果在创建Windows.UI.Xaml.UIElement之前创建DesktopWindowXamlSource,那么创建Windows.UI.Xaml.UIElement时会自动初始化框架
- 否则,需要显式调用WindowsXamlManager.InitializeForCurrentThread来初始化框架,以便使用Windows.UI.Xaml.UIElement。返回的是WindowsXamlManager。注意,不用时要将返回的WindowsXamlManager处置掉。
- 创建一个DesktopWindowXamlSource标的,附加到上级UI元素
- 需要将创建的DesktopWindowXamlSource,转筑成IDesktopWindowXamlSourceNative或IDesktopWindowXamlSourceNative2。然后再调用上面的AttachToWindow方法。
- 接下来需要设置DesktopWindowXamlSource内部子窗口的大小,默认为0
- 将所需的Windows.UI.Xaml.UIElement设置到DesktopWindowXamlSource标的之辖属
有一些提示信息(比如C4002)可以被忽略。
查看完整版的源代码文件https://github.com/microsoft/Xaml-Islands-Samples/blob/master/Standalone_Samples/Contoso/App/XamlBridge.cpp。
Package the app
可以通过https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-packaging-dot-net来打包项目。注意最低版本要选Windows 10, version 1903 (10.0; Build 18362)
。
Host a custom WinRT XAML control in a C++ desktop (Win32) app
此项目中要自定义一个WinRT控件。
首先创建一个Windows Desktop Application,基于C++的,然后添加以下NuGet料包
- 在上篇文中添加的两个料包
- Microsoft.Toolkit.Win32.UI.XamlApplication,定义了Microsoft.Toolkit.Win32.UI.XamlHost.XamlApplication,
- Microsoft.VCRTForwarders.140
然后添加一个UWP(C++/WinRT)工程(Blank App (C++/WinRT)),安装如下料包
- Microsoft.Toolkit.Win32.UI.XamlApplication
添加一个文本格式的placeholder.exe
,将其Content辖属设置为True。在Package.appxmanifest中,设置`<Application Id=“App” Executable=“placeholder.exe”……·
将<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
,替换成如下内容:
<PropertyGroup Label="Globals">
<WindowsAppContainer>true</WindowsAppContainer>
<AppxGeneratePriEnabled>true</AppxGeneratePriEnabled>
<ProjectPriIndexName>App</ProjectPriIndexName>
<AppxPackage>true</AppxPackage>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
Configure the solution
添加一个文件Solution.props到解决方案,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<IntDir>$(SolutionDir)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</IntDir>
<OutDir>$(SolutionDir)\bin\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
<GeneratedFilesDir>$(IntDir)Generated Files\</GeneratedFilesDir>
</PropertyGroup>
</Project>
打开Property Manager(View -> Other Windws),为两个工程都添加上到Solution.props的property sheet。效果是工程的Properties的General页面有OutputDirectory和Intermediate Directory的设置,内容来自于Solution.props。
在Solution上右击,设置Project Dependencies,让Win32工程依赖于UWP工程。
Add code to the UWP app project
Define a custom WinRT XAML control
创建一个MyUserControl.xaml。
Define a XamlApplication class
让UWP APP从Microsoft.Toolkit.Win32.UI.XamlHost.XamlApplication派生,以便支持IXamlMetadataProvider接口。允许应用在运行时从当前目录发现XAML控件的的配装件。
每个使用XAML Islands的解决方案中只能有一个XamlApplication。
删掉MainPage.xaml。
经过一系列骚操作,将原本的App.xaml替换成基于XamlApplication的实现。
Configure the desktop project to consume custom control types
Option 1: Package the app using MSIX
通过Windows Application Packaging Project来封包。
Option 2: Create an application manifest
通过Application Manifests,不做封包。
Configure additional desktop project properties
因为工程类型不一致,所以需要手动添加UWP工程的引用到Win32工程。编辑Win32工程文件,添加:
<!-- Configure these for your UWP project -->
<PropertyGroup>
<AppProjectName>MyUWPApp</AppProjectName>
</PropertyGroup>
<PropertyGroup>
<AppIncludeDirectories>$(SolutionDir)\obj\$(Platform)\$(Configuration)\$(AppProjectName)\;$(SolutionDir)\obj\$(Platform)\$(Configuration)\$(AppProjectName)\Generated Files\;</AppIncludeDirectories>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\$(AppProjectName)\$(AppProjectName).vcxproj" />
</ItemGroup>
<!-- End Section-->
在Win32工程辖属中,将Manifest Tool -> Input and Output的DPI Awareness 设置成Per Monitor High DPI Aware。
Host the custom WinRT XAML control in the desktop project
在Win32工程的framework.h文件中注释掉以下行:
#define WIN32_LEAN_AND_MEAN
然后添加好多好多代码。最后发现运行出现错误,不知道问题出在哪里~~
这边有个议疏 Samples crash on launch #46 ,现象类似:
Debug Error!
Program
abort() has been called
还有Why do I get an application configuration error when using XAML Islands?
Be advised, if you downgrade the Microsoft.Toolkit.Win32.UI.SDK inside the sample to anything <= 6.0.0-preview7.1, things started to work (for me at least). https://github.com/CommunityToolkit/Microsoft.Toolkit.Win32
Add a control from the WinUI 2 library to the custom control
略。
Advanced scenarios for XAML Islands in C++ desktop (Win32) apps
Keyboard input
为了让XAML Island能够正常处理键盘事件,需要将窗口消息转给XAML Island。做法是将DesktopWindowXamlSource 转为IDesktopWindowXamlSourceNative2 ,然后传消息给PreTranslateMessage 方法。
Keyboard focus navigation
Tab焦点变化的时候,需要安排好焦点出入DesktopWindowXamlSource。键盘导览到DesktopWindowXamlSource的时候,需要将焦点移到导览顺序上的第一个Windows.UI.Xaml.UIElement ,随着导览的继续,在DesktopWindowXamlSource绕完一圈后返回上级UI元素。
XAML寄宿用编口提供了一些帮助:
- 导览到DesktopWindowXamlSource的时候,会触发GotFocus事件。调用NavigateFocus将焦点移到第一个元素
- 从导览完最后一个元素时,会触发TakeFocusRequested。这时候在宿主中将焦点移到下一个元素。
Handle layout changes
要跟上宿主布局的变化,下面是需要考虑的点:
- 宿主程序接收到WM_SIZE的时候,要通过SetWindowPos重新定位XAML Island
- 调用Windows.UI.Xaml.UIElement的Measure可以获取其大小测量信息
- 当上级UI元素大小改变时,调用Windows.UI.Xaml.UIElement的Arrange方法
Handle DPI changes
最佳的体验是,把桌面程序配置成能感知显示器DPI的。对此,需要添加一个side-by-side assembly manifest,内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>
其他
- Referencing a Windows Runtime component from a Desktop app
- XAML Islands Getting Started Guide – Adding UWP Controls to Windows Forms or WPF Application
- Getting Started with XAML Islands: Hosting a UWP Control in WPF and WinForms Apps
(完)
2022-03-19更新
DesktopWindowXamlSource必须和父窗口的声明周期相同,如果提前销毁,那么父窗口上的XamlIsland也会消失。
WindowsXamlManager::InitializeForCurrentThread();
之后不能立即创建DesktopWindowXamlSource
,否则会出现前一个示例尚未清楚完成的错误。
(更新完)
2022-03-26更新
Windows Dev AppConsult上的一篇文章Host Custom UWP Controls in C++ Win32 Project using XAML Islands给出了使用自定义XAML控件的更加详细的步骤,但是有些方面给https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/using-the-xaml-hosting-api#host-a-custom-uwp-control列举的有所不同。
- 似乎没有添加Win32应用到UWP应用的引用
struct App_baseWithProvider
使用std::shared_ptr<XamlMetaDataProvider> AppProvider()
来返回例现,而不是使用com_ptr- 除了app.manifest之外,还添加
<AppxManifest Include="$(SolutionDir)\bin\$(Platform)\$(Configuration)\$(AppProjectName)\AppxManifest.xml" />
- 在win32应用中主动添加规则,将UWP应用输出中的所需文件拷贝过来
- 展示了
$(AppIncludeDirectories)
的用法- https://github.com/microsoft/Xaml-Islands-Samples/blob/master/Samples/Win32/SampleCppApp/SampleCppApp.vcxproj中有
(AppIncludeDirectories);%(AdditionalIncludeDirectories);$(GeneratedFilesDir)
。这个设置中将(AppIncludeDirectories)
放置在最前面,甚至是本工程头文件之前,存在本工程的头文件被覆改的问题。
- https://github.com/microsoft/Xaml-Islands-Samples/blob/master/Samples/Win32/SampleCppApp/SampleCppApp.vcxproj中有
<ProjectPriIndexName>App</ProjectPriIndexName>
的值设为App,是否不妥呢?
其Github上的文章Host Custom UWP Controls in MFC MDI Project using XAML Islands中还有WinUI相关内容。
同样的内容出现在Host Custom UWP Controls in MFC MDI Project using XAML Islands。
示例代码在https://github.com/freistli/ModernizeApp/tree/master/C++/SimpleAppWithWinUI/SimpleApp。
贴士:
- 让Win32项目引用UWP项目,在使用
Ctrl+F7
编译的时候会报道MIDL的Source辖属没有设置。应该是MSBuild设置上的一个bug。解决办法是不让Win32项目引用UWP项目,而是引用UWP项目生成的winmd文件。需要这个winmd文件来生成映射后的头文件,否则构建的时候链接器会报错。
(更新完)