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

示例在https://github.com/microsoft/Xaml-Islands-Samples/tree/master/Standalone_Samples/CppWinRT_Basic_Win32App

需要安装以下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

  1. 首先要初始化WinRT的XAML框架,有几个做法
    • 如果在创建Windows.UI.Xaml.UIElement之前创建DesktopWindowXamlSource,那么创建Windows.UI.Xaml.UIElement时会自动初始化框架
    • 否则,需要显式调用WindowsXamlManager.InitializeForCurrentThread来初始化框架,以便使用Windows.UI.Xaml.UIElement。返回的是WindowsXamlManager。注意,不用时要将返回的WindowsXamlManager处置掉。
  2. 创建一个DesktopWindowXamlSource标的,附加到上级UI元素
    • 需要将创建的DesktopWindowXamlSource,转筑成IDesktopWindowXamlSourceNative或IDesktopWindowXamlSourceNative2。然后再调用上面的AttachToWindow方法。
    • 接下来需要设置DesktopWindowXamlSource内部子窗口的大小,默认为0
  3. 将所需的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>

其他

(完)

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)的用法
  • <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文件来生成映射后的头文件,否则构建的时候链接器会报错。

(更新完)