将剪贴板示例从 C# 移植到 C++/WinRT - 案例研究

本主题介绍将其中一个通用 Windows 平台 (UWP)应用示例C# 移植到 C++/WinRT 的案例研究。 你可以跟着演练步骤边进行边亲自移植该示例,从而获得移植实践和经验。

注释

要移植的 源代码 是 UWP C# 应用。 本文中的目标 C++/WinRT 代码是为 WinUI 3 (Windows 应用 SDK) 编写的。 凡是 UWP 源中使用了在 WinUI 3 中有所不同的 API(例如 Windows.UI.Core.CoreDispatcherMicrosoft.UI.Dispatching.DispatcherQueue),本文都会在 C++/WinRT 输出中明确给出正确的 WinUI 3 对应项。 可以直接在 WinUI 3 应用中使用 C++/WinRT 列中的代码模式。

有关从 C# 移植到 C++/WinRT 所涉及的技术详细信息的综合目录,请参阅从 C# 移植到 C++/WinRT 的配套主题。

有关 C# 和 C++ 源代码文件的简要前言

在 C# 项目中,源代码文件主要是 .cs 文件。 移动到 C++ 时,你会注意到有更多类型的源代码文件可供使用。 原因是编译器之间的差异、重用 C++ 源代码的方式以及 声明定义 类型及其函数(其方法)的概念。

函数 声明 仅描述函数的 签名 (其返回类型、其名称和参数类型和名称)。 函数 定义 包括函数的 主体 (其实现)。

在类型方面,这有点不同。 通过提供其名称和(至少)仅声明其所有成员函数(和其他成员)来定义类型。 正确,即使未定义其成员函数,也可以 定义 类型。

  • 常见的 C++ 源文件包括 .hdot aitch)和 .cpp 文件。 文件 .h 文件,它定义一个或多个类型。 虽然你可以在头文件中定义成员函数,但这通常是.cpp文件的用途。 因此,对于假设的 C++ 类型 MyClass,需要在其中定义 MyClass.h,并在其中定义其成员函数MyClass.cpp。 为了让其他开发人员复用你的类,你只需提供 .h 文件和目标代码。 你会对你的 .cpp 文件保密,因为其实现属于你的知识产权。
  • 预编译标头 (pch.h)。 通常,应用程序中包含一组头文件,并且不会经常更改这些文件。 因此,无需在每次编译时处理该组标头的内容,而是将这些标头聚合到一个文件中,编译一次,然后在每次生成时使用该预编译步骤的输出。 你可以通过 预编译头 文件(通常名为 pch.h)来实现这一点。
  • .idl 文件。 这些文件包含接口定义语言(IDL)。 可以将 IDL 视为Windows 运行时类型的头文件。 我们将在 MainPage 类型的 IDL 部分进一步讨论 IDL。

下载并测试剪贴板示例

访问 剪贴板示例 网页,然后单击“ 下载 ZIP”。 解压缩下载的文件,并查看文件夹结构。

  • 示例源代码的 C# 版本包含在名为 cs 的文件夹中。
  • 示例源代码的 C++/WinRT 版本包含在名为 cppwinrt 的文件夹中。
  • 供 C# 版本和 C++/WinRT 版本共同使用的其他文件可在 sharedSharedContent 文件夹中找到。

本主题中的演练演示如何通过从 C# 源代码移植它来重新创建剪贴板示例的 C++/WinRT 版本。 这样,即可了解如何将自己的 C# 项目移植到 C++/WinRT。

若要了解示例的作用,请打开 C# 解决方案(\Clipboard_sample\cs\Clipboard.sln)、根据需要更改配置(可能为 x64)、生成和运行。 该示例的用户界面(UI)会逐步引导你了解其各项功能。

Tip

下载的示例的根文件夹可能是命名 Clipboard 的,而不是 Clipboard_sample。 但是,我们将继续引用该文件夹 Clipboard_sample ,以便将其与将在后续步骤中创建的 C++/WinRT 版本区分开来。

创建名为剪贴板的空白应用

注释

有关安装和使用 C++/WinRT Visual Studio 扩展(VSIX)和 NuGet 包(一起提供项目模板和生成支持)的信息,请参阅Visual Studio对 C++/WinRT 的支持

通过在Microsoft Visual Studio中创建新的 C++/WinRT 项目来开始移植过程。 使用用于 C++ 的 空白应用(打包,WinUI 3 in Desktop)项目模板创建一个新项目。 将其命名为 Clipboard,并且(为使文件夹结构与演练保持一致)确保取消选中 将解决方案和项目放在同一目录中

为了建立一个基准,请确保这个新的空项目能够成功生成并运行。

Package.appxmanifest,和资源文件

如果示例的 C# 和 C++/WinRT 版本不需要在同一台计算机上并排安装,则两个项目的应用包清单源文件 (Package.appxmanifest) 可以相同。 在这种情况下,只需从 C# 项目复制到 Package.appxmanifest C++/WinRT 项目即可完成。

要使示例的两个版本共存,它们需要不同的标识符。 在这种情况下,在 C++/WinRT 项目中,在 XML 编辑器中打开 Package.appxmanifest 该文件,并记下这三个值。

  • /Package/Identity 元素中,记下 Name 属性的值。 这是 包名称。 对于新建的项目,该项目将为其提供唯一 GUID 的初始值。
  • /Package/Applications/Application 元素中,记下 Id 属性的值。 这是 应用程序 ID
  • /Package/mp:PhoneIdentity 元素中,记下 PhoneProductId 属性的值。 同样,对于新创建的项目,该值将被设置为与包名称所设置的 GUID 相同。

然后将 C# 项目复制到 Package.appxmanifest C++/WinRT 项目。 最后,可以还原记录的三个值。 或者,您也可以编辑复制的值,使其具有唯一性和/或适用于该应用程序及您的组织(就像您通常为新项目所做的那样)。 例如,在这种情况下,我们不必还原包名称的值,只需将复制后的值从 Microsoft.SDKSamples.Clipboard.CS 更改为 Microsoft.SDKSamples.Clipboard.CppWinRT。 我们可以将应用程序 ID 设置为 “应用”。 只要包名称 应用程序 ID 不同,两个应用程序将具有不同的应用程序用户模型 ID(AUMID)。 这是在同一台计算机上并排安装两个应用所必需的。

为了便于本演练,也适合在 Package.appxmanifest 中进行其他一些更改。 字符串 Clipboard C# Sample 出现了三次。 将其更改为 剪贴板 C++/WinRT 示例

在 C++/WinRT 项目中, Package.appxmanifest 文件和项目现在与引用的资产文件不同步。 若要解决此问题,请先从 C++/WinRT 项目中删除资产,方法是选择文件夹中的所有文件Assets(解决方案资源管理器 Visual Studio),然后删除它们(在对话框中选择“删除”。

C# 项目引用共享文件夹中的资产文件。 可以在 C++/WinRT 项目中执行相同的操作,也可以复制文件,就像在本演练中所做的那样。

导航到 \Clipboard_sample\SharedContent\media 文件夹。 选择 C# 项目包括的七个文件(microsoft-sdk.png、、、smalltile-sdk.pngsplash-sdk.pngsquaretile-sdk.pngstorelogo-sdk.pngtile-sdk.pngwindows-sdk.png),复制这些文件,并将其粘贴到\Clipboard\Clipboard\Assets新项目中的文件夹。

右键单击Assets文件夹(在 C++/WinRT 项目中解决方案资源管理器)>添加>现有项...并导航到\Clipboard\Clipboard\Assets。 在文件选取器中,选择七个文件,然后单击“ 添加”。

Package.appxmanifest 现已重新与项目的资源文件同步。

MainPage,包括配置示例的功能

剪贴板示例(与所有通用 Windows 平台 (UWP)应用示例一样)包含一组用户一次可以逐个执行的方案。 给定示例中的方案集合在示例的源代码中配置。 集合中的每个场景都是一个数据项,用于存储标题以及项目中实现该场景的类的类型。

在示例的 C# 版本中,如果查看 SampleConfiguration.cs 源代码文件,你将看到两个类。 大多数配置逻辑都位于 MainPage 类中,该类是分部类(它与标记 MainPage.xaml 和命令性代码 MainPage.xaml.cs结合使用时形成完整的类)。 此源代码文件中的另一个类是 Scenario,其 TitleClassType 属性。

在接下来的几个子节中,我们将了解如何移植 MainPageScenario

MainPage 类型的 IDL

让我们通过简要介绍接口定义语言(IDL),以及使用 C++/WinRT 编程时如何帮助我们开始本部分。 IDL 是一种源代码,描述Windows 运行时类型的可调用图面。 类型对外可调用(或公共)的部分会被暴露给外界,以便该类型可供使用。 该类型对外呈现的 projected 部分与其实际的内部实现截然不同,而后者当然既不可调用,也不是公开的。 它只是我们在 IDL 中定义的投影部分。

.idl 文件中编写好 IDL 源代码后,您便可将 IDL 编译为机器可读的元数据文件(也称为 Windows 元数据)。 这些元数据文件具有扩展名 .winmd,下面是它们的一些用途。

  • A .winmd 可以描述组件中的Windows 运行时类型。 从应用程序项目引用Windows 运行时组件(WRC)时,应用程序项目将读取属于 WRC 的Windows元数据(该元数据可能位于单独的文件中),也可以将其打包到与 WRC 本身相同的文件中,以便可以从应用程序中使用 WRC 的类型。
  • .winmd 可以在应用程序的一个部分中描述 Windows 运行时类型,以便同一应用程序的另一部分可以使用这些类型。 例如,同一应用中 XAML 页面使用的Windows 运行时类型。
  • 为了让你更轻松地使用 Windows 运行时 类型(内置或第三方),C++/WinRT 构建系统会使用 .winmd 文件生成包装类型,用于表示这些 Windows 运行时 类型中经投影的部分。
  • 为了让你更轻松地实现自己的 Windows 运行时 类型,C++/WinRT 构建系统会将 IDL 转换为 .winmd 文件,然后使用该文件为你的投影生成包装器,以及可作为你的实现基础的存根(我们将在本主题的后面进一步讨论这些存根)。

与 C++/WinRT 一起使用的特定 IDL 版本Microsoft接口定义语言 3.0。 在本主题的本部分的其余部分,我们将详细介绍 C# MainPage 类型。 我们将确定其哪些部分需要位于 C++/WinRT MainPage 类型的投影中(即,在其可调用的或公共的图面中),这只能是其实现的一部分。 这一区分很重要,因为当我们开始编写 IDL 时(我们将在下一节中这样做),我们只会在其中定义可调用的部分。

共同实现 MainPage 类型的 C# 源代码文件包括:( MainPage.xaml 我们将很快通过复制它进行移植), MainPage.xaml.cs以及 SampleConfiguration.cs

在 C++/WinRT 版本中,我们将 MainPage 类型以类似的方式纳入源代码文件。 我们会将 MainPage.xaml.cs 中的逻辑大部分移植到 MainPage.hMainPage.cpp 中。 而对于 SampleConfiguration.cs 中的逻辑,我们会将其转换为 SampleConfiguration.hSampleConfiguration.cpp

C# 通用 Windows 平台 (UWP) 应用程序中的类当然是Windows 运行时类型。 但是,在 C++/WinRT 应用程序中创作类型时,可以选择该类型是Windows 运行时类型还是常规 C++ 类/结构/枚举。

项目中的任何 XAML 页面都需要是Windows 运行时类型,因此 MainPage 必须是Windows 运行时类型。 在 C++/WinRT 项目中,MainPage 已经是Windows 运行时类型,因此我们不需要更改它的这一方面。 具体而言,它是 运行时类

  • 有关是否应为给定类型创作运行时类的更多详细信息,请参阅主题 Author API with C++/WinRT
  • 在 C++/WinRT 中,运行时类的内部实现及其投影(公共)部分以两个不同的类的形式存在。 这些称为实现类型和投影类型。 可以在上一项目符号点提到的主题中详细了解它们,还可以在 C++/WinRT 中使用 API
  • 若要了解有关运行时类和 IDL(.idl 文件)之间关系的更多信息,可以参阅并跟着主题 XAML 控件;绑定到 C++/WinRT 属性 进行操作。 本主题将演练创作新运行时类的过程,第一步是向项目添加新 的 Midl File (.idl) 项。

对于 MainPage,我们实际上已在 C++/WinRT 项目中拥有必要的 MainPage.idl 文件。 这是因为项目模板为我们创建了它。 但在本演练的后续部分,我们会将更多 .idl 文件添加到项目中。

我们很快就会看到一份清单,列出需要添加到现有 MainPage.idl 文件中的具体 IDL。 在此之前,我们需要先理清哪些内容需要写入 IDL,哪些不需要。

若要确定哪些 MainPage 成员需要在 MainPage.idl 中声明(以便它们成为 MainPage 运行时类的一部分),以及哪些成员只需作为 MainPage 实现类型的成员,我们来列出 C# MainPage 类的成员。 我们通过查看MainPage.xaml.csSampleConfiguration.cs来找到这些成员。

我们共找到 12 个 protectedprivate 字段及方法。 我们找到了以下public成员。

  • 默认构造函数 MainPage()
  • 静态字段 “当前 ”和 “FEATURE_NAME”。
  • 属性 IsClipboardContentChangedEnabledScenarios
  • BuildClipboardFormatsOutputStringDisplayToastEnableClipboardContentChangedNotificationsNotifyUser 的方法。

正是那些 public 成员可以在 MainPage.idl 中声明。 因此,让我们检查每个类,看看它们是否需要成为 MainPage 运行时类的一部分,还是它们是否需要成为其实现的一部分。

  • 默认构造函数 MainPage()。 对于 XAML ,通常在其 IDL 中声明默认构造函数。 这样,XAML UI 框架就可以激活类型。
  • 静态字段 Current 用于从单个方案 XAML 页中访问应用程序的 MainPage 实例。 由于 Current 不用于与 XAML 框架进行互操作(也不跨编译单元使用),因此我们可以将其保留为实现类型的唯一成员。 如果是你自己的项目,在这种情况下,你可能会选择这么做。 但是,既然该字段是投影类型的一个实例,那么在 IDL 中声明它似乎是合乎逻辑的。 因此,这就是我们将在这里执行的操作(这样做也使代码稍微更简洁)。
  • 对于在 MainPage 类型中访问的静态FEATURE_NAME字段,这是类似的情况。 同样,选择在 IDL 中声明它会使代码稍微更简洁。
  • 属性 IsClipboardContentChangedEnabled 仅在 OtherScenarios 类中使用。 因此,在移植过程中,我们会稍微简化一下,将其设为 OtherScenarios 运行时类的私有字段。 这样一个人就不会进入 IDL 了。
  • 属性 方案方案 类型的对象的集合(我们前面提到的类型)。 我们将在下一小节讨论 场景,因此我们也先暂且不讨论 Scenarios 属性,留到那时再说。
  • BuildClipboardFormatsOutputStringDisplayToastEnableClipboardContentChangedNotifications 这些方法属于实用工具函数,与其说它们和主页面有关,不如说它们更多是与示例的整体状态有关。 因此,在移植过程中,我们将把这三个方法重构到一个名为 SampleState 的新实用类型中(它不需要是 Windows 运行时 类型)。 因此,这三种方法不会被纳入 IDL。
  • NotifyUser 方法在各个方案的 XAML 页面中被调用,调用对象是从静态 Current 字段返回的 MainPage 实例。 由于(如前所述) Current 是投影类型的实例,因此我们需要在 IDL 中声明 NotifyUserNotifyUser 采用 NotifyType 类型的参数。 我们将在下一个子部分中讨论这一点。

任何要进行数据绑定的成员也都需要在 IDL 中声明(无论使用的是 {x:Bind} 还是 {Binding})。 有关详细信息,请参阅 数据绑定

我们正在取得进展:我们正在整理一份清单,列出哪些成员要添加到 MainPage.idl 文件中,哪些不要添加。 但我们仍必须讨论 Scenarios 属性和 NotifyType 类型。 那么接下来我们就这样做吧。

ScenarioNotifyType 类型的 IDL

Scenario 类在 SampleConfiguration.cs 中定义。 我们决定如何将该类移植到 C++/WinRT。 默认情况下,我们可能将其设置为普通 C++ struct。 但是,如果 Scenario 需要跨二进制文件使用,或者需要与 XAML 框架互操作,则需要在 IDL 中将其声明为 Windows 运行时类型。

研究 C# 源代码时,我们发现,在此上下文中使用了 Scenario

<ListBox x:Name="ScenarioControl" ... >
var itemCollection = new List<Scenario>();
int i = 1;
foreach (Scenario s in scenarios)
{
    itemCollection.Add(new Scenario { Title = $"{i++}) {s.Title}", ClassType = s.ClassType });
}
ScenarioControl.ItemsSource = itemCollection;

正在将一组 Scenario 对象分配给 ListBoxItemsSource 属性(它是一个项控件)。 由于 Scenario确实需要与 XAML 互操作,因此它必须是 Windows 运行时 类型。 因此,需要在 IDL 中定义它。 在 IDL 中定义 Scenario 类型会使 C++/WinRT 生成系统在某个后台生成的头文件中为你生成 Scenario 的源代码定义(其名称和位置对此演练并不重要)。

你应该还记得,MainPage.Scenarios 是一组 Scenario 对象,而我们刚才说过,这些对象需要在 IDL 中定义。 因此, MainPage.Scenarios 本身还需要在 IDL 中声明。

NotifyType 是在 C# 的 MainPage.xaml.cs 中声明的一个 enum。 由于我们将 NotifyType 传递给属于 MainPage 运行时类的方法,NotifyType 也需要是Windows 运行时类型;并且需要在其中MainPage.idl定义。

现在,让我们将我们已决定在 IDL 中声明的新类型和 Mainpage 的新成员添加到 MainPage.idl 文件中。 同时,我们还将从 IDL 中删除 Visual Studio 项目模板为我们生成的 Mainpage 的占位符成员。

因此,在 C++/WinRT 项目中,打开 MainPage.idl并编辑它,使其看起来像下面的列表。 请注意,其中一项编辑是将命名空间名称从 剪贴板 更改为 SDKTemplate。 如果需要,只需将整个内容 MainPage.idl 替换为以下代码。 另一个需要注意的调整是,我们将 Scenario::ClassType 的名称更改为 Scenario::ClassName

// MainPage.idl
namespace SDKTemplate
{
    struct Scenario
    {
        String Title;
        Microsoft.UI.Xaml.Interop.TypeName ClassName;
    };

    enum NotifyType
    {
        StatusMessage,
        ErrorMessage
    };

    [default_interface]
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();

        static MainPage Current{ get; };
        static String FEATURE_NAME{ get; };

        static Windows.Foundation.Collections.IVector<Scenario> scenarios{ get; };

        void NotifyUser(String strMessage, NotifyType type);
    };
}

注释

有关 C++/WinRT 项目中 .idl 文件内容的详细信息,请参阅 Microsoft 接口定义语言 3.0

对于你自己的移植工作,你可能既不想,也不需要像我们上面那样更改命名空间。 我们在这里这样做只是因为我们要移植的 C# 项目的默认命名空间是 SDKTemplate;而项目名称和程序集的名称为 剪贴板

但是,随着我们在本演练中继续进行移植,我们会将源代码中每一处出现的 Clipboard 命名空间名称都更改为 SDKTemplate。 C++/WinRT 项目属性中也存在 剪贴板 命名空间名称的显示位置,因此我们现在将有机会更改该名称。

在 Visual Studio 中,对于 C++/WinRT 项目,将项目属性 Common Properties>C++/WinRT>根命名空间设置为值 SDKTemplate

保存 IDL 并重新生成存根文件

主题 XAML 控件;绑定到 C++/WinRT 属性 引入了 存根文件 的概念,并通过实例演示了这些文件的实际使用过程。 我们在本主题前面也提到过存根,当时我们说过,C++/WinRT 生成系统会将你的 .idl 文件内容转换为 Windows 元数据,然后名为 cppwinrt.exe 的工具会根据这些元数据生成存根,你可以在此基础上进行实现。

每次在 IDL 中添加、删除或更改某些内容并生成时,生成系统都会更新这些存根文件中的存根实现。 因此,我们建议你每次更改 IDL 并重新构建后,都查看这些存根文件,复制所有已更改的签名,并将它们粘贴到你的项目中。 稍后我们会通过更具体的细节和示例,准确说明该如何操作。 但这样做的好处在于,它能让你始终以一种不会出错的方式,清楚地知道你的实现类型应当具有怎样的结构,以及其方法应具备怎样的签名。

在本演练进行到这一步时,我们暂时已经完成了对 MainPage.idl 文件的编辑,所以现在应将其保存。 该项目目前还无法成功完成构建,但现在执行一次构建仍然很有用,因为这会重新生成 MainPage 的存根文件。 因此,立即生成项目,并忽略任何生成错误。

对于这个 C++/WinRT 项目,存根文件将在 \Clipboard\Clipboard\Generated Files\sources 文件夹中生成。 部分构建结束后,你将在那里找到它们(同样,正如预期的那样,构建不会完全成功。但我们感兴趣的步骤——生成存根——成功)。 我们感兴趣的文件是 MainPage.hMainPage.cpp

在这两个存根文件中,你会看到我们添加到 IDL 中的 MainPage 成员的新存根实现(例如 CurrentFEATURE_NAME)。 你需要将这些存根实现复制到项目中已有的 MainPage.hMainPage.cpp 文件中。 同时,就像使用 IDL 一样,我们将从这些现有文件中删除Visual Studio项目模板提供的 Mainpage 占位符成员(名为 MyProperty 的虚拟属性和名为 ClickHandler 的事件处理程序)。

事实上,我们要保留的 MainPage 当前版本的唯一成员是构造函数。

从存根文件复制新成员后,删除了我们不需要的成员,并更新了命名空间,MainPage.hMainPage.cpp项目中的文件应类似于下面的代码列表。 请注意,有两种 MainPage 类型。 实现命名空间中的一个,另一个位于factory_implementation命名空间中。 我们对 factory_implementation 的唯一更改是将 SDKTemplate 添加到其命名空间。

// MainPage.h
#pragma once
#include "MainPage.g.h"

namespace winrt::SDKTemplate::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
        MainPage();

        static SDKTemplate::MainPage Current();
        static hstring FEATURE_NAME();
        static Windows::Foundation::Collections::IVector<SDKTemplate::Scenario> scenarios();
        void NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type);
    };
}
namespace winrt::SDKTemplate::factory_implementation
{
    struct MainPage : MainPageT<MainPage, implementation::MainPage>
    {
    };
}
// MainPage.cpp
#include "pch.h"
#include "MainPage.h"
#include "MainPage.g.cpp"

namespace winrt::SDKTemplate::implementation
{
    MainPage::MainPage()
    {
        InitializeComponent();
    }
    SDKTemplate::MainPage MainPage::Current()
    {
        throw hresult_not_implemented();
    }
    hstring MainPage::FEATURE_NAME()
    {
        throw hresult_not_implemented();
    }
    Windows::Foundation::Collections::IVector<SDKTemplate::Scenario> MainPage::scenarios()
    {
        throw hresult_not_implemented();
    }
    void MainPage::NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type)
    {
        throw hresult_not_implemented();
    }
}

对于字符串,C# 使用 System.String。 有关示例,请参阅 MainPage.NotifyUser 方法。 在 IDL 中,我们使用 String 声明字符串,当该工具为我们生成 C++/WinRT 代码时 cppwinrt.exe ,它将使用 winrt::hstring 类型。 每当在 C# 代码中遇到字符串时,都会移植到 winrt::hstring。 有关详细信息,请参阅 C++/WinRT 中的字符串处理

有关方法签名中的参数的说明 const& ,请参阅 参数传递

更新所有剩余的命名空间声明/引用,并生成

在生成 C++/WinRT 项目之前,找到 剪贴板 命名空间的任何声明(并引用),并将其更改为 SDKTemplate

  • MainPage.xamlApp.xaml。 命名空间出现在 x:Classxmlns:local 属性的值中。
  • App.idl
  • App.h
  • App.cpp。 有两个 using namespace 指令(搜索子字符串 using namespace Clipboard),以及 MainPage 类型的两个限定(搜索 Clipboard::MainPage)。 这些需要改变。

由于我们从 MainPage 中删除了事件处理程序,因此还进入 MainPage.xaml 并删除标记中的 Button 元素。

保存所有文件。 清理解决方案(生成>干净解决方案),然后生成它。 如果到目前为止一直严格按照所写内容完成了所有更改,则构建应会成功。

实现我们在 IDL 中声明的 MainPage 成员

构造函数、CurrentFEATURE_NAME

下面是我们需要移植的相关代码(来自 C# 项目)。

<!-- MainPage.xaml -->
...
<TextBlock x:Name="SampleTitle" ... />
...
// MainPage.xaml.cs
...
public sealed partial class MainPage : Page
{
    public static MainPage Current;

    public MainPage()
    {
        InitializeComponent();
        Current = this;
        SampleTitle.Text = FEATURE_NAME;
    }
...
}
...

// SampleConfiguration.cs
...
public partial class MainPage : Page
{
    public const string FEATURE_NAME = "Clipboard C# sample";
...
}
...

很快,我们将完全重新使用 MainPage.xaml (通过复制它)。 现在(下面),我们将暂时将具有适当名称的 TextBlock 元素添加到 MainPage.xaml C++/WinRT 项目中。

FEATURE_NAMEMainPage 的静态字段(C# const 字段在其行为中本质上是静态的),在定义中 SampleConfiguration.cs定义。 对于 C++/WinRT,我们不使用(静态)字段,而是将其表示为 C++/WinRT 中的(静态)只读属性。 在 C++/WinRT 中,属性 getter 的表达方式是一个返回属性值且不接受任何参数的函数(访问器)。 因此,C# FEATURE_NAME 静态字段成为 C++/WinRT FEATURE_NAME 静态访问器函数(在本例中,返回字符串文本)。

顺便说一句,如果在移植 C# 只读属性,我们会执行相同的操作。 对于 C# 可写属性,在 C++/WinRT 中,表达属性设置器的方式是使用一个以属性值为参数的 void 函数(修改器)。 在任一情况下,如果 C# 字段或属性是静态的,则 C++/WinRT 访问器和/或 mutator 也是如此。

CurrentMainPage 的静态(非常量)字段。 同样,我们将将其(C++/WinRT 表达式)设为只读属性,并再次使其为静态。 如果 FEATURE_NAME 为常量, 则当前 不是。 因此,在 C++/WinRT 中,我们需要一个后备字段,并且访问器将返回该字段。 因此,在 C++/WinRT 项目中,我们将在名为 MainPage.h 的专用静态字段中声明,我们将定义/初始化当前(MainPage.cpp 因为它具有静态存储持续时间),我们将通过名为 Current 的公共静态访问器函数访问它。

构造函数本身进行了几次赋值操作,这些操作很容易移植。

在 C++/WinRT 项目中,添加一个新的 Visual C++>> 文件(.cpp)项,其名称SampleConfiguration.cpp为 。

编辑 MainPage.xamlMainPage.hMainPage.cppSampleConfiguration.cpp,使其与下面的列表一致。

<!-- MainPage.xaml -->
...
<StackPanel ...>
    <TextBlock x:Name="SampleTitle" />
</StackPanel>
...
// MainPage.h
...
namespace winrt::SDKTemplate::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
...
        static SDKTemplate::MainPage Current() { return current; }
...
    private:
        static SDKTemplate::MainPage current;
...
    };
...
}

// MainPage.cpp
...
namespace winrt::SDKTemplate::implementation
{
    SDKTemplate::MainPage MainPage::current{ nullptr };

    MainPage::MainPage()
    {
        InitializeComponent();
        MainPage::current = *this;
        SampleTitle().Text(FEATURE_NAME());
    }
...
}

// SampleConfiguration.cpp
#include "pch.h"
#include "MainPage.h"

using namespace winrt;
using namespace SDKTemplate;

hstring implementation::MainPage::FEATURE_NAME()
{
    return L"Clipboard C++/WinRT Sample";
}

此外,请务必从 MainPage.cppMainPage::Current()MainPage::FEATURE_NAME()中删除现有函数主体,因为我们现在正在其他地方定义这些方法。

可以看到,MainPage::current 被声明为 SDKTemplate::MainPage 类型,即该投影类型。 它不是 SDKTemplate::implementation::MainPage 类型,后者是实现类型。 投影类型指的是专门设计为供在项目内部进行 XAML 互操作时使用,或跨二进制边界使用的类型。 实现类型就是你用来实现投影类型中公开的那些功能的类型。 由于 MainPage::current (in MainPage.h)的声明出现在实现命名空间(winrt::SDKTemplate::implementation)中,因此未限定的 MainPage 将引用实现类型。 因此,我们使用 SDKTemplate:: 进行限定,以明确表示我们希望 MainPage::current 是投影类型 winrt::SDKTemplate::MainPage 的一个实例。

在构造函数中,有一些与 MainPage::current = *this; 相关的要点值得说明一下。

  • 当你在实现类型的某个成员内部使用 this 指针时,this 指针当然是 指向实现类型的指针
  • 若要将 this 指针转换为相应的投影类型,请先对其进行解引用。 只要像这里这样从 IDL 生成实现类型,该实现类型就会带有一个可将其转换为其投影类型的转换运算符。 这就是此处的工作分配的原因。

有关这些详细信息的详细信息,请参阅 实例化和返回实现类型和接口

构造函数中也有 SampleTitle().Text(FEATURE_NAME());。 该 SampleTitle() 部分是对名为 SampleTitle 的简单访问器函数的调用,该函数返回我们添加到 XAML 的 TextBlock 。 每当你对某个 XAML 元素执行 x:Name 操作时,XAML 编译器都会为你生成一个以该元素命名的访问器。 部件.Text(...)SampleTitle 访问器返回的 TextBlock 对象上调用 Text Mutator 函数。 并 FEATURE_NAME() 调用静态 MainPage::FEATURE_NAME 访问器函数以返回字符串文本。 总之,这行代码设置了名为 SampleTitleTextBlockText 属性。

请注意,由于字符串在Windows 运行时中为宽,因此要移植字符串文本,请使用宽字符编码前缀L作为前缀。 因此,我们将(例如)"a string literal" 更改为 L"a string literal"。 另请参阅 宽字符串文本

场景

下面是我们需要移植的相关 C# 代码。

// MainPage.xaml.cs
...
public sealed partial class MainPage : Page
{
...
    public List<Scenario> Scenarios
    {
        get { return this.scenarios; }
    }
...
}
...

// SampleConfiguration.cs
...
public partial class MainPage : Page
{
...
    List<Scenario> scenarios = new List<Scenario>
    {
        new Scenario() { Title = "Copy and paste text", ClassType = typeof(CopyText) },
        new Scenario() { Title = "Copy and paste an image", ClassType = typeof(CopyImage) },
        new Scenario() { Title = "Copy and paste files", ClassType = typeof(CopyFiles) },
        new Scenario() { Title = "Other Clipboard operations", ClassType = typeof(OtherScenarios) }
    };
...
}
...

从我们先前的调查中,我们知道此 方案 对象的集合正在 ListBox 中显示。 在 C++/WinRT 中,可赋给项控件的 ItemsSource 属性的 集合类型 是有限制的。 集合必须是向量或可观测向量,并且其元素必须是下列值之一:

对于 IInspectable 这种情况,如果这些元素本身不是运行时类,则它们必须属于能够与 IInspectable 相互进行装箱和取消装箱的类型。 这意味着它们必须是 Windows 运行时 类型(请参阅 将值装箱和拆箱为 IInspectable)。

对于此案例研究,我们没有将 方案 设置为运行时类。 不过,这仍然是一个合理的选择。 而且,在你自己的移植工作中,也会有一些情况表明,运行时类肯定是正确的选择。 例如,如果需要使元素类型 可观察 (请参阅 XAML 控件;绑定到 C++/WinRT 属性),或者该元素需要具有任何其他原因的方法,并且不仅仅是一组数据成员。

由于在本演练中,我们不会Scenario类型使用运行时类,因此需要考虑装箱。 如果我们将 Scenario 设为普通的 C++ struct,那么就无法对其进行装箱。 但我们在 IDL 中将 Scenario 声明为 struct,因此 可以 将其装箱。

我们只能在两者之间做选择:要么预先对 Scenario 进行装箱,要么等到即将将其赋给 ItemsSource 时再按需即时装箱。 下面是有关这两个选项的一些注意事项。

  • 提前装箱。 对于该选项,我们的数据成员是可直接分配给 UI 的 IInspectable 集合。 在初始化时,我们将 Scenario 对象封装到该数据成员中。 我们只需要这个集合的一份副本,但每次需要读取其字段时,都必须对其中的元素进行拆箱。
  • 拳击恰时无时。 对于该选项,我们的数据成员是一个 Scenario 集合。 当需要将其赋给 UI 时,我们会将数据成员中的 Scenario 对象装箱到一个新的 IInspectable 集合中。 我们可以在不进行拆箱的情况下读取数据成员内各元素的字段,但需要保留该集合的两个副本。

正如你所看到的,对于这样的小型集合来说,其优缺点基本相互抵消。 因此,在这个案例研究中,我们将选择即时选项。

scenarios 成员是 MainPage 的一个字段,在 SampleConfiguration.cs 中定义并初始化。 并且,ScenariosMainPage 的一个只读属性,在 MainPage.xaml.cs 中定义(其实现只是简单地返回 scenarios 字段)。 我们将在 C++/WinRT 项目中执行类似操作;但是,我们将使两个成员保持静态(因为我们只需要整个应用程序一个实例;因此我们可以访问它们而无需类实例)。 我们将分别把它们命名为 scenariosInnerscenarios。 我们将在MainPage.h中声明scenariosInner。 而且,由于它具有静态存储持续时间,因此我们会在 .cpp 文件中定义/初始化它(SampleConfiguration.cpp在本例中)。

编辑 MainPage.hSampleConfiguration.cpp,使其与下面的列表一致。

// MainPage.h
...
struct MainPage : MainPageT<MainPage>
{
...
    static Windows::Foundation::Collections::IVector<Scenario> scenarios() { return scenariosInner; }
...
private:
    static winrt::Windows::Foundation::Collections::IVector<Scenario> scenariosInner;
...
};

// SampleConfiguration.cpp
...
using namespace Windows::Foundation::Collections;
...
IVector<Scenario> implementation::MainPage::scenariosInner = winrt::single_threaded_observable_vector<Scenario>(
{
    Scenario{ L"Copy and paste text", xaml_typename<SDKTemplate::CopyText>() },
    Scenario{ L"Copy and paste an image", xaml_typename<SDKTemplate::CopyImage>() },
    Scenario{ L"Copy and paste files", xaml_typename<SDKTemplate::CopyFiles>() },
    Scenario{ L"History and roaming", xaml_typename<SDKTemplate::HistoryAndRoaming>() },
    Scenario{ L"Other Clipboard operations", xaml_typename<SDKTemplate::OtherScenarios>() },
});

此外,请务必从 MainPage.cppMainPage::scenarios()中删除现有函数正文,因为我们现在正在头文件中定义该方法。

正如你所看到的,在 SampleConfiguration.cpp 中,我们通过调用名为 winrt::single_threaded_observable_vector 的 C++/WinRT 辅助函数来初始化静态数据成员 scenariosInner。 该函数为我们创建新的Windows 运行时集合对象,并将其作为 IObservableVector 接口返回。 由于在此示例中,集合不可 观测 (不需要,因为它在初始化后不添加或删除元素),因此我们可以选择调用 winrt::single_threaded_vector。 该函数将集合作为 IVector 接口返回。

有关集合和绑定到集合的详细信息,请参阅 XAML 项控件;绑定到 C++/WinRT 集合以及 使用 C++/WinRT 的集合

刚刚添加的初始化代码引用项目中尚不存在的类型(例如 winrt::SDKTemplate::CopyText)。 为了解决这个问题,接下来我们向项目中添加 5 个新的空白 XAML 页。

添加五个新的空白 XAML 页

向项目中添加一个新的 Visual C++>Blank Page (C++/WinRT) 项,请务必选择 Blank Page (C++/WinRT) 项模板,而不是 Blank Page 模板。 将其命名为 CopyText。 新的 XAML 页面在 SDKTemplate 命名空间中定义,这是我们想要的。

再重复上述过程四次,并命名 XAML 页面CopyImageCopyFilesHistoryAndRoamingOtherScenarios

如果你愿意的话,现在你可以再次构建了。

NotifyUser

在 C# 项目中,你将在 .. 中找到 MainPage.xaml.cs 方法的实现。 MainPage.NotifyUser 依赖于 MainPage.UpdateStatus,该方法又依赖于尚未移植的 XAML 元素。 因此,现在,我们只会在 C++/WinRT 项目中存根 UpdateStatus 方法,稍后我们将移植该方法。

下面是我们需要移植的相关 C# 代码。

// MainPage.xaml.cs
...
public void NotifyUser(string strMessage, NotifyType type)
if (Dispatcher.HasThreadAccess)
{
    UpdateStatus(strMessage, type);
}
else
{
    var task = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => UpdateStatus(strMessage, type));
}
private void UpdateStatus(string strMessage, NotifyType type) { ... }{
...

NotifyUser 将 UI 更新调度到主线程。 在 WinUI 3 中,这将使用Microsoft。UI。Dispatching.DispatcherQueue 而不是较旧的 CoreDispatcher。 在 C++/WinRT 中,每当想要使用Windows或Microsoft命名空间中的类型时,都需要包含相应的 C++/WinRT 命名空间头文件(有关该文件的详细信息,请参阅 C++/WinRT 入门)。 在这种情况下,如下面的代码列表中所示,标头是 winrt/Microsoft.UI.Dispatching.h,我们将将其包含在其中 pch.h

UpdateStatus 是专用的。 因此,我们将在 MainPage 实现类型上创建一个私有方法。 UpdateStatus 不是要在运行时类上调用的,因此我们不会在 IDL 中声明它。

在移植 MainPage.NotifyUser 并阻止 MainPage.UpdateStatus 后,这就是我们在 C++/WinRT 项目中拥有的内容。 在列出这段代码之后,我们将来看其中的一些细节。

// pch.h
...
#include <winrt/Microsoft.UI.Dispatching.h>
...

// MainPage.h
...
struct MainPage : MainPageT<MainPage>
{
...
    void NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type);
...
private:
    void UpdateStatus(hstring const& strMessage, SDKTemplate::NotifyType const& type);
...
};

// MainPage.cpp
...
void MainPage::NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type)
{
    if (DispatcherQueue().HasThreadAccess())
    {
        UpdateStatus(strMessage, type);
    }
    else
    {
        DispatcherQueue().TryEnqueue([strMessage, type, this]()
            {
                UpdateStatus(strMessage, type);
            });
    }
}
void MainPage::UpdateStatus(hstring const& strMessage, SDKTemplate::NotifyType const& type)
{
    throw hresult_not_implemented();
}
...

在 C# 中,可以使用点表示法将 点入 嵌套属性。 因此,C# MainPage 类型可以使用语法访问自己的 Dispatcher 属性。 C# 还可以进一步使用点号访问该值,例如使用 Dispatcher.HasThreadAccess 这样的语法。 在 C++/WinRT 中,属性作为访问器函数实现,因此语法仅在为每个函数调用添加括号时才有所不同。

C# C++/WinRT
Dispatcher.HasThreadAccess DispatcherQueue().HasThreadAccess()

NotifyUser 的 C# 版本调用 Dispatcher.RunAsync 时,WinUI 3 等效项使用 DispatcherQueue.TryEnqueue。 C++/WinRT 版本将回调委托实现为 lambda 函数。 在 C++/WinRT 中,我们 捕获 要使用的两个参数以及 this 指针(因为我们要调用成员函数)。 有关将委托实现为 lambda 表达式的更多信息以及代码示例,请参阅主题 在 C++/WinRT 中使用委托处理事件

实现剩余 MainPage 成员

让我们完整列出 MainPage (实现范围 MainPage.xaml.csSampleConfiguration.cs)的成员,以便我们可以看到到目前为止已移植哪些成员,以及哪些成员尚未这样做。

成员 Access Status
MainPage 构造函数 public 移植
当前 属性 public 移植
FEATURE_NAME 属性 public 移植
IsClipboardContentChangedEnabled 属性 public 未开始
Scenarios 属性 public 移植
BuildClipboardFormatsOutputString 方法 public 未开始
DisplayToast 方法 public 未开始
EnableClipboardContentChangedNotifications 方法 public 未开始
NotifyUser 方法 public 移植
OnNavigatedTo 方法 protected 未开始
isApplicationWindowActive 字段 private 未开始
needToPrintClipboardFormat 字段 private 未开始
场景字段 private 移植
Button_Click 方法 private 未开始
DisplayChangedFormats 方法 private 未开始
Footer_Click 方法 private 未开始
HandleClipboardChanged 方法 private 未开始
OnClipboardChanged 方法 private 未开始
OnWindowActivated 方法 private 未开始
ScenarioControl_SelectionChanged 方法 private 未开始
UpdateStatus 方法 private 斯图布德出去

接下来的几个子部分中,我们将讨论尚未报告的成员。

注释

我们时不时地在源代码中遇到对 XAML 标记(in MainPage.xaml)中的 UI 元素的引用。 当我们讲到这些引用时,我们将通过在 XAML 中添加简单的占位符元素来暂时绕过它们。 这样一来,项目将在每个小节之后继续构建。 另一种做法是现在就通过将 MainPage.xaml全部内容从 C# 项目复制到 C++/WinRT 项目中来解析这些引用。 但是,如果我们这样做,那么我们就要过很久才能停下来稍作检查并重新构建(这样就可能掩盖我们在此过程中出现的任何拼写错误或其他错误)。

完成移植 MainPage 类的命令性代码 后,我们将 复制 XAML 文件的内容,并确保项目仍将生成。

是否启用剪贴板内容更改检测

这是一个可读写的 C# 属性,默认值为 false。 它是 MainPage 的成员,并在 SampleConfiguration.cs 中定义。

对于 C++/WinRT,我们需要一个访问器函数、一个 mutator 函数和一个作为字段的后备数据成员。 由于 IsClipboardContentChangedEnabled 表示示例中某个方案的状态,而不是 MainPage 本身的状态,因此我们将在名为 SampleState 的新实用工具类型上创建新成员。 我们将在源代码文件中实现这一点 SampleConfiguration.cpp ,我们将创建成员 static (因为我们只需要整个应用程序中的一个实例;因此我们可以访问它们而无需类实例)。

为了与 C++/WinRT 项目中的SampleConfiguration.cpp配套,请添加一个名为SampleConfiguration.h的新 Visual C++>Code>头文件(.h)项。 编辑 SampleConfiguration.hSampleConfiguration.cpp,使其与下面的列表匹配。

// SampleConfiguration.h
#pragma once 
#include "pch.h"

namespace winrt::SDKTemplate
{
    struct SampleState
    {
        static bool IsClipboardContentChangedEnabled();
        static void IsClipboardContentChangedEnabled(bool checked);
    private:
        static bool isClipboardContentChangedEnabled;
    };
}

// SampleConfiguration.cpp
...
#include "SampleConfiguration.h"
...
bool SampleState::isClipboardContentChangedEnabled = false;
...
bool SampleState::IsClipboardContentChangedEnabled()
{
    return isClipboardContentChangedEnabled;
}
void SampleState::IsClipboardContentChangedEnabled(bool checked)
{
    if (isClipboardContentChangedEnabled != checked)
    {
        isClipboardContentChangedEnabled = checked;
    }
}

同样,带有 static 存储的字段(例如 SampleState::isClipboardContentChangedEnabled)必须在应用程序中定义一次,而 .cpp 文件是放置这一定义的合适位置(本例中为 SampleConfiguration.cpp)。

BuildClipboardFormatsOutputString

此方法是 MainPage 的公共成员,并在其中 SampleConfiguration.cs定义。

// SampleConfiguration.cs
...
public string BuildClipboardFormatsOutputString()
{
    DataPackageView clipboardContent = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
    StringBuilder output = new StringBuilder();

    if (clipboardContent != null && clipboardContent.AvailableFormats.Count > 0)
    {
        output.Append("Available formats in the clipboard:");
        foreach (var format in clipboardContent.AvailableFormats)
        {
            output.Append(Environment.NewLine + " * " + format);
        }
    }
    else
    {
        output.Append("The clipboard is empty");
    }
    return output.ToString();
}
...

在 C++/WinRT 中,我们将生成 BuildClipboardFormatsOutputString 作为 SampleState 的公共静态方法。 我们可以将其设为 static,因为它不访问任何实例成员。

若要在 C++/WinRT 中使用剪贴板DataPackageView 类型,需要包含 C++/WinRT Windows命名空间头文件winrt/Windows.ApplicationModel.DataTransfer.h

在 C# 中, DataPackageView.AvailableFormats 属性是 IReadOnlyList,因此我们可以访问该属性的 Count 属性。 在 C++/WinRT 中, DataPackageView::AvailableFormats 访问器函数返回一个 IVectorView,该函数具有我们可以调用 的 Size 访问器函数。

若要移植 C# System.Text.StringBuilder 类型的使用,我们将使用标准 C++ 类型 std::wostringstream。 该类型是宽字符串的输出流(要使用它,我们需要包含 sstream 头文件)。 不使用与 StringBuilder 一样使用的 Append 方法,而是将插入运算符<<) 与输出流(如 wostringstream)配合使用。 有关详细信息,请参阅 iostream 编程设置 C++/WinRT 字符串的格式

C# 代码使用关键字构造 new。 在 C# 中,对象默认是引用类型,使用 new 在堆上声明。 在现代标准 C++ 中,对象默认为值类型,在堆栈上声明(不使用 new)。 因此,我们只需将StringBuilder output = new StringBuilder();移植到 C++/WinRT,即std::wostringstream output;

C# var 关键字要求编译器推断类型。 在 C++/WinRT 中移植 varauto 。 但在 C++/WinRT 中,在某些情况下(为了避免拷贝),你会需要一个指向推断(或推导)类型的引用,而你可以用 auto& 来表示对推导类型的左值引用。 在某些情况下,需要一种特殊类型的引用,无论它是使用 左值 还是 右值进行初始化,该引用都是正确绑定的。 你用 auto&& 来表达这一点。 这就是你在下面移植后的代码中的 for 循环里看到使用的那种形式。 有关左值右值的介绍,请参阅值类别及其引用

编辑 pch.hSampleConfiguration.hSampleConfiguration.cpp,使其与下面列出的内容匹配。

// pch.h
...
#include <sstream>
#include "winrt/Windows.ApplicationModel.DataTransfer.h"
...

// SampleConfiguration.h
...
struct SampleState
{
    static hstring BuildClipboardFormatsOutputString();
    ...
}
...

// SampleConfiguration.cpp
...
using namespace Windows::ApplicationModel::DataTransfer;
...
hstring SampleState::BuildClipboardFormatsOutputString()
{
    DataPackageView clipboardContent{ Clipboard::GetContent() };
    std::wostringstream output;

    if (clipboardContent && clipboardContent.AvailableFormats().Size() > 0)
    {
        output << L"Available formats in the clipboard:";
        for (auto&& format : clipboardContent.AvailableFormats())
        {
            output << std::endl << L" * " << std::wstring_view(format);
        }
    }
    else
    {
        output << L"The clipboard is empty";
    }

    return hstring{ output.str() };
}

注释

代码 DataPackageView clipboardContent{ Clipboard::GetContent() }; 行中的语法使用一种称为 统一初始化的新式标准 C++ 功能,其特征使用大括号而不是 = 符号。 该语法清楚地表明,正在进行初始化,而不是赋值。 如果你更喜欢类似于赋值(但实际上不是)的语法形式,则可以将上面的语法替换为等效DataPackageView clipboardContent = Clipboard::GetContent();的语法。 不过,最好是熟悉两种方式来表达初始化,因为很可能在遇到的代码中经常看到这两种方法。

DisplayToast

DisplayToast 是 C# MainPage 类的公共静态方法,可在其中 SampleConfiguration.cs找到它。 在 C++/WinRT 中,我们将使其成为 SampleState 的公共静态方法。

我们已经接触到与移植这种方法相关的大部分细节和技巧。 需要注意的一个新项是,将 C# 逐字字符串文本 (@) 移植到标准 C++ 原始字符串文本LR)。

此外,在 C++/WinRT 中引用 ToastNotificationXmlDocument 类型时,可以按命名空间名称限定它们,也可以编辑 SampleConfiguration.cpp 和添加 using namespace 指令,例如以下示例。

using namespace Windows::UI::Notifications;

引用 XmlDocument 类型时,以及每当引用任何其他Windows 运行时类型时,你都有相同的选择。

除了这些事项外,只需遵循你之前相同的指导来完成以下步骤。

  • SampleConfiguration.h 中声明该方法,并在 SampleConfiguration.cpp 中定义该方法。
  • 编辑pch.h以包含任何必需的 C++/WinRT Windows命名空间头文件。
  • 在栈上构造 C++/WinRT 对象,而不是在堆上构造。
  • 将对属性 get 访问器的调用替换为函数调用语法 (())。

编译器/链接器错误的常见原因是忘记包含所需的 C++/WinRT Windows命名空间头文件。 有关某个可能出现的错误的详细信息,请参阅 C3779:为什么编译器会报告“返回‘auto’的函数在定义之前无法使用,即 consume_Something”这一错误?

如果你想跟着演练一步步操作,并自行移植 DisplayToast,那么你可以将自己的结果与您下载的 Clipboard sample 源代码 ZIP 包中的 C++/WinRT 版本代码进行比较。

启用剪贴板内容更改通知

EnableClipboardContentChangedNotifications 是 C# MainPage 类的公共静态方法,并在其中 SampleConfiguration.cs定义。

// SampleConfiguration.cs
...
public bool EnableClipboardContentChangedNotifications(bool enable)
{
    if (IsClipboardContentChangedEnabled == enable)
    {
        return false;
    }

    IsClipboardContentChangedEnabled = enable;
    if (enable)
    {
        Clipboard.ContentChanged += OnClipboardChanged;
        Window.Current.Activated += OnWindowActivated;
    }
    else
    {
        Clipboard.ContentChanged -= OnClipboardChanged;
        Window.Current.Activated -= OnWindowActivated;
    }
    return true;
}
...
private void OnClipboardChanged(object sender, object e) { ... }
private void OnWindowActivated(object sender, WindowActivatedEventArgs e) { ... }
...

在 C++/WinRT 中,我们将使其成为 SampleState 的公共静态方法。

在 C# 中,可以使用 +=-= 运算符语法来注册和撤销事件处理委托。 在 C++/WinRT 中,可以使用多种语法选项来注册/撤销委托,如 在 C++/WinRT 中使用委托处理事件中所述。 但一般来说,通常是通过调用一对以该事件命名的函数来进行注册和注销。 若要注册,请将委托传递给注册函数,并获得一个撤销令牌作为返回值(winrt::event_token)。 若要撤销,请将该令牌传递给吊销函数。 在这种情况下,手动器是静态的,并且(如以下代码列表所示),函数调用语法非常简单。

类似的令牌实际上也被用于 C# 中,只不过是在幕后。 但语言使细节隐含。 C++/WinRT 使其明确。

对象类型显示在 C# 事件处理程序签名中。 在 C# 语言中,对象是 .NET System.Object 类型的别名。 C++/WinRT 中的等效项是 winrt::Windows::Foundation::IInspectable。 因此,你将在 C++/WinRT 事件处理程序中看到 IInspectable

编辑 SampleConfiguration.hSampleConfiguration.cpp,使其与下面的列表匹配。

// SampleConfiguration.h
...
    static bool EnableClipboardContentChangedNotifications(bool enable);
    ...
private:
    ...
    static event_token clipboardContentChangedToken;
    static event_token activatedToken;
    static void OnClipboardChanged(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
    static void OnWindowActivated(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::WindowActivatedEventArgs const& e);
...

// SampleConfiguration.cpp
...
using namespace Windows::Foundation;
using namespace Microsoft::UI;
using namespace Microsoft::UI::Xaml;
...
event_token SampleState::clipboardContentChangedToken;
event_token SampleState::activatedToken;
...
bool SampleState::EnableClipboardContentChangedNotifications(bool enable)
{
    if (isClipboardContentChangedEnabled == enable)
    {
        return false;
    }

    IsClipboardContentChangedEnabled(enable);
    if (enable)
    {
        clipboardContentChangedToken = Clipboard::ContentChanged(OnClipboardChanged);
        activatedToken = Window::Current().Activated(OnWindowActivated);
    }
    else
    {
        Clipboard::ContentChanged(clipboardContentChangedToken);
        Window::Current().Activated(activatedToken);
    }
    return true;
}
void SampleState::OnClipboardChanged(IInspectable const&, IInspectable const&){}
void SampleState::OnWindowActivated(IInspectable const&, WindowActivatedEventArgs const& e){}

暂时将事件处理委托本身(OnClipboardChangedOnWindowActivated)保留为存根。 这些成员已经在我们的待移植成员列表中,所以我们会在后续小节中再处理它们。

OnNavigatedTo

OnNavigatedTo 是 C# MainPage 类的受保护方法,定义于 MainPage.xaml.cs。 如下所示,以及它所引用的 XAML ListBox

<!-- MainPage.xaml -->
...
<ListBox x:Name="ScenarioControl" ... />
...
// MainPage.xaml.cs
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // Populate the scenario list from the SampleConfiguration.cs file
    var itemCollection = new List<Scenario>();
    int i = 1;
    foreach (Scenario s in scenarios)
    {
        itemCollection.Add(new Scenario { Title = $"{i++}) {s.Title}", ClassType = s.ClassType });
    }
    ScenarioControl.ItemsSource = itemCollection;

    if (Window.Current.Bounds.Width < 640)
    {
        ScenarioControl.SelectedIndex = -1;
    }
    else
    {
        ScenarioControl.SelectedIndex = 0;
    }
}

这是一个重要且有趣的方法,因为我们的 场景 对象集合就是在这里分配给 UI 的。 C# 代码构建了一个包含 Scenario 对象的 System.Collections.Generic.List,并将其赋给 ListBoxItemsSource 属性(它是一个项控件)。 在 C# 中,我们使用 字符串内插 为每个 Scenario 对象生成标题(请注意特殊字符的使用 $ )。

在 C++/WinRT 中,我们将使 OnNavigatedTo 成为 MainPage 的公共方法。 然后,我们将在 XAML 中添加一个占位 ListBox 元素,以使构建能够成功。 在代码清单之后,我们将探讨其中的一些细节。

<!-- MainPage.xaml -->
...
<StackPanel ...>
    ...
    <ListBox x:Name="ScenarioControl" />
</StackPanel>
...
// MainPage.h
...
void OnNavigatedTo(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
...

// MainPage.cpp
...
using namespace winrt::Microsoft::UI::Xaml;
using namespace winrt::Microsoft::UI::Xaml::Navigation;
...
void MainPage::OnNavigatedTo(NavigationEventArgs const& /* e */)
{
    auto itemCollection = winrt::single_threaded_observable_vector<IInspectable>();
    int i = 1;
    for (auto s : MainPage::scenarios())
    {
        s.Title = winrt::to_hstring(i++) + L") " + s.Title;
        itemCollection.Append(winrt::box_value(s));
    }
    ScenarioControl().ItemsSource(itemCollection);

    if (Window::Current().Bounds().Width < 640)
    {
        ScenarioControl().SelectedIndex(-1);
    }
    else
    {
        ScenarioControl().SelectedIndex(0);
    }
}
...

同样,我们调用 winrt::single_threaded_observable_vector 函数,但这一次是用来创建一个 IInspectable 集合。 这是我们决定按需对我们的 Scenario 对象进行装箱的部分原因。

而这里,我们不使用 C# 的 字符串插值,而是使用 to_hstring 函数与 winrt::hstring串联运算符 的组合。

isApplicationWindowActive

在 C# 中,isApplicationWindowActive 是属于 bool 类的简单私有字段,并在其中SampleConfiguration.cs定义。 默认为 false。 在 C++/WinRT 中,我们将在 SampleConfiguration.hSampleConfiguration.cpp 文件中将其设为 SampleState 的私有静态字段(出于我们前面已经说明的原因),并保持相同的默认值。

我们已经了解如何声明、定义和初始化静态字段。 为了回顾一下,请看看我们之前是如何处理 isClipboardContentChangedEnabled 字段的,并对 isApplicationWindowActive 做同样的处理。

needToPrintClipboardFormat

模式与 isApplicationWindowActive 相同(参见紧接在此标题之前的标题)。

Button_Click

Button_Click 是 C# MainPage 类的私有(事件处理)方法,并在其中 MainPage.xaml.cs定义。 在这里,它与它引用的 XAML SplitView 和注册它的 ToggleButton 一起。

<!-- MainPage.xaml -->
...
<SplitView x:Name="Splitter" ... />
...
<ToggleButton Click="Button_Click" .../>
...
private void Button_Click(object sender, RoutedEventArgs e)
{
    Splitter.IsPaneOpen = !Splitter.IsPaneOpen;
}

下面是移植到 C++/WinRT 的对应版本。 请注意,在 C++/WinRT 版本中,事件处理程序为 public(正如你所见,你是在 private: 声明之前声明它的)。 这是因为在 XAML 标记中注册的事件处理程序(如下所示)需要 public 位于 C++/WinRT 中,才能让 XAML 标记访问它。 另一方面,如果在命令性代码中注册事件处理程序(就像之前在 MainPage::EnableClipboardContentChangedNotifications 中所做的那样),则不需要事件处理程序 public

<!-- MainPage.xaml -->
...
<StackPanel ...>
    ...
    <SplitView x:Name="Splitter" />
</StackPanel>
...
// MainPage.h
...
    void Button_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& e);
private:
...

// MainPage.cpp
void MainPage::Button_Click(Windows::Foundation::IInspectable const& /* sender */, Microsoft::UI::Xaml::RoutedEventArgs const& /* e */)
{
    Splitter().IsPaneOpen(!Splitter().IsPaneOpen());
}

DisplayChangedFormats

在 C# 中, DisplayChangedFormats 是属于 MainPage 类的私有方法,并在其中 SampleConfiguration.cs定义。

private void DisplayChangedFormats()
{
    string output = "Clipboard content has changed!" + Environment.NewLine;
    output += BuildClipboardFormatsOutputString();
    NotifyUser(output, NotifyType.StatusMessage);
}

在 C++/WinRT 中,我们将把它设为 SampleState 的私有静态字段(因为它不访问任何实例成员),并将其放在 SampleConfiguration.hSampleConfiguration.cpp 文件中。 此方法的 C# 代码不使用 System.Text.StringBuilder;但它为 C++/WinRT 版本执行了足够的字符串格式设置,这是使用 std::wostringstream 的另一个好位置。

我们将标准 C++ (换行符)插入到输出流中,而不是在 C# 代码中使用的静态 std::endl 属性。

// SampleConfiguration.h
...
private:
    static void DisplayChangedFormats();
...

// SampleConfiguration.cpp
void SampleState::DisplayChangedFormats()
{
    std::wostringstream output;
    output << L"Clipboard content has changed!" << std::endl;
    output << BuildClipboardFormatsOutputString().c_str();
    MainPage::Current().NotifyUser(output.str(), NotifyType::StatusMessage);
}

上述 C++/WinRT 版本的设计中有一点小小的低效之处。 首先,我们将创建 std::wostringstream。 但我们还调用 BuildClipboardFormatsOutputString 方法(我们之前移植的方法)。 该方法创建它自己的 std::wostringstream。 它将其流转换为 winrt::hstring 并返回它。 调用 hstring::c_str 函数将返回 的 hstring 重新转换为 C 样式字符串,然后将其插入流中。 只需创建一个 std::wostringstream,然后传递(对它的引用),以便方法可以直接将字符串插入其中,这一点会更有效。

这就是我们在 剪贴板示例 源代码的 C++/WinRT 版本中执行的操作(在下载的 ZIP 中)。 在该源代码中,有一个名为 SampleState::AddClipboardFormatsOutputString 的新专用静态方法,该方法对对输出流的引用执行操作。 然后重构 SampleState::D isplayChangedFormatsSampleState::BuildClipboardFormatsOutputString 的方法以调用该新方法。 它在功能上等效于本主题中的代码列表,但效率更高。

Footer_Click 是属于 C# MainPage 类的异步事件处理程序,并在其中 MainPage.xaml.cs定义。 下面的代码列表在功能上等效于你下载的源代码中的方法。 但在这里,我已经把它从一行展开成四行,以便更容易看出它在做什么,以及因此该如何移植它。

async void Footer_Click(object sender, RoutedEventArgs e)
{
    var hyperlinkButton = (HyperlinkButton)sender;
    string tagUrl = hyperlinkButton.Tag.ToString();
    Uri uri = new Uri(tagUrl);
    await Windows.System.Launcher.LaunchUriAsync(uri);
}

虽然严格来说,该方法是异步的,但它在 await 之后不执行任何操作,因此不需要 await(也不需要 async 关键字)。 它可能使用它们来避免Visual Studio中的 IntelliSense 消息。

等效的 C++/WinRT 方法也将是异步方法(因为它调用 Launcher.LaunchUriAsync)。 但它既不需要 co_await,也不需要返回异步对象。 有关 co_await 和异步对象的信息,请参阅 使用 C++/WinRT 的并发和异步操作

现在我们来说说这个方法在做什么。 由于这是 HyperlinkButtonClick 事件的事件处理程序,因此名为发送方的对象实际上是 HyperlinkButton。 因此,类型转换是安全的(我们也可以将此转换表示为 sender as HyperlinkButton)。 接下来,我们检索 Tag 属性的值(如果查看 C# 项目中的 XAML 标记,你将看到此值设置为表示 Web URL 的字符串)。 尽管 FrameworkElement.Tag 属性(HyperlinkButtonFrameworkElement)的类型 对象,但在 C# 中,我们可以使用 Object.ToString 来字符串化该属性。 从生成的字符串中,我们 构造 Uri 对象 。 最后(在 Shell 的帮助下),我们启动浏览器并导航到 URL。

下面是移植到 C++/WinRT 的方法(同样,为了清楚起见),之后是详细信息的说明。

// pch.h
...
#include "winrt/Windows.System.h"
...

// MainPage.h
...
    void Footer_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& e);
private:
...

// MainPage.cpp
...
using namespace winrt::Windows::Foundation;
using namespace winrt::Microsoft::UI::Xaml::Controls;
...
void MainPage::Footer_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const&)
{
    auto hyperlinkButton{ sender.as<HyperlinkButton>() };
    hstring tagUrl{ winrt::unbox_value<hstring>(hyperlinkButton.Tag()) };
    Uri uri{ tagUrl };
    Windows::System::Launcher::LaunchUriAsync(uri);
}

和往常一样,我们生成事件处理程序 public。 我们使用发送方对象上的 as 函数将其转换为 HyperlinkButton。 在 C++/WinRT 中, Tag 属性是 IInspectable (等效 对象)。 但 IInspectable 上没有 Tostring。 相反,我们必须将 IInspectable 取消装箱到标量值(在本例中为字符串)。 再次,有关装箱和拆箱的详细信息,请参阅 将值装箱和拆箱为 IInspectable

最后两行重复我们之前看到的移植模式,它们几乎与 C# 版本相呼应。

HandleClipboardChanged

移植此方法并没有什么新的内容。 可以在下载的 剪贴板示例 源代码的 ZIP 中比较 C# 和 C++/WinRT 版本。

OnClipboardChangedOnWindowActivated

到目前为止,这两个事件处理程序只有空存根。 但是移植它们很简单,它并没有引发任何新的讨论。

ScenarioControl_SelectionChanged

这是属于 C# MainPage 类的另一个私有事件处理程序,并在该类中 MainPage.xaml.cs定义。 在 C++/WinRT 中,我们将把它设为公共,并在 MainPage.hMainPage.cpp 中实现它。

对于此方法,我们需要 MainPage::navigating,这是初始化为 false的私有布尔字段。 你需要在 MainPage.xaml 中一个名为 ScenarioFrame框架。 但是,除了这些细节之外,移植此方法不会揭示任何新技术。

如果你不是手动移植,而是从你下载的 Clipboard sample 源代码的 ZIP 压缩包中的 C++/WinRT 版本复制代码,那么你会看到其中使用了 MainPage::NavigateTo。 目前,只需将 NavigateTo 的内容重构为 ScenarioControl_SelectionChanged

UpdateStatus

到目前为止, MainPage.UpdateStatus 只有一个存根。 再次移植其实现,大体上还是在重复旧内容。 需要注意的一个新点是,虽然在 C# 中,我们可以将 字符串String.Empty 进行比较,但在 C++/WinRT 中,我们改为调用 winrt::hstring::empty 函数。 另一个是,nullptr 是 C# 中 null 的标准 C++ 对应项。

可以使用我们已经介绍的技术执行其余端口。 下面是在此方法的移植版本编译之前需要执行的操作类型的列表。

  • 若要 MainPage.xaml,请添加一个名为 StatusBorder边框
  • 若要MainPage.xaml,请添加一个名为 StatusBlockTextBlock
  • 若要MainPage.xaml,请添加一个名为 StatusPanelStackPanel
  • 若要 pch.h,请添加 #include "winrt/Microsoft.UI.Xaml.Media.h"
  • 若要 pch.h,请添加 #include "winrt/Microsoft.UI.Xaml.Automation.Peers.h"
  • MainPage.cpp添加 using namespace winrt::Microsoft::UI::Xaml::Media;
  • MainPage.cpp添加 using namespace winrt::Microsoft::UI::Xaml::Automation::Peers;

复制完成 MainPage 移植所需的 XAML 和样式

对于 XAML,理想的情况是,可以在 C# 和 C++/WinRT 项目中使用相同的 XAML 标记。 剪贴板示例是其中一种情况。

在其 Styles.xaml 文件中,Clipboard 示例包含一个用于样式的 XAML ResourceDictionary,其中的样式被应用于整个应用程序 UI 中的按钮、菜单和其他 UI 元素。 页面 Styles.xaml 合并到 App.xaml。 然后是 UI 的标准 MainPage.xaml 起始点,我们前面已经简单看过它了。 现在可以在项目的 C++/WinRT 版本中重复使用这三 .xaml 个文件(未更改)。

与资产文件一样,可以选择从应用程序的多个版本引用相同的共享 XAML 文件。 在本演练中,为了简单起见,我们将文件复制到 C++/WinRT 项目中,然后以这种方式添加文件。

导航到 \Clipboard_sample\SharedContent\xaml 文件夹,选择并复制 App.xamlMainPage.xaml然后将这两个文件粘贴到 \Clipboard\Clipboard C++/WinRT 项目中的文件夹,选择在出现提示时替换文件。

在Visual Studio的 C++/WinRT 项目中,单击“显示所有文件”将其打开。 现在,在项目节点下添加新文件夹,并将其命名。Styles 在“文件资源管理器”中,导航到 \Clipboard_sample\SharedContent\xaml 文件夹,选择并复制 Styles.xaml,然后将其粘贴到 \Clipboard\Clipboard\Styles 刚刚创建的文件夹。 返回 C++/WinRT 项目中解决方案资源管理器,右键单击Styles文件夹>“添加>现有项...”,然后导航到 \Clipboard\Clipboard\Styles。 在文件选取器中,选择 Styles 并单击“ 添加”。

将新文件夹添加到 C++/WinRT 项目,紧接在项目节点下,并命名 Styles。 导航到 \Clipboard_sample\SharedContent\xaml 文件夹,选择并复制 Styles.xaml,并将其粘贴到 \Clipboard\Clipboard\Styles C++/WinRT 项目中的文件夹。 右键单击Styles文件夹(在 C++/WinRT 项目中解决方案资源管理器)>添加>现有项...并导航到\Clipboard\Clipboard\Styles。 在文件选取器中,选择 Styles 并单击“ 添加”。

再次单击“ 显示所有文件 ”将其关闭。

我们现在已完成移植 MainPage,如果一直在执行这些步骤,则 C++/WinRT 项目现在将生成并运行。

合并您的 .idl 文件

除了 UI 的标准 MainPage.xaml 起点外,剪贴板示例还包含其他五个特定于方案的 XAML 页面,以及相应的代码隐藏文件。 我们将在项目的 C++/WinRT 版本中重新使用所有这些页面的实际 XAML 标记(未更改)。 接下来的几个主要部分将介绍如何移植代码隐藏。 但在此之前,我们先来了解一下 IDL。

将运行时类的 IDL 整合到一个 IDL 文件中是有价值的。 若要了解该值,请参阅 将运行时类分解到 MIDL 文件 (.idl) 中。 接下来,我们将把 CopyFiles.idlCopyImage.idlCopyText.idlHistoryAndRoaming.idlOtherScenarios.idl 的内容合并,将这些 IDL 归并到一个名为 Project.idl 的单个文件中(然后删除原始文件)。

在执行此操作时,让我们从这五个 XAML 页面类型中删除自动生成的虚拟属性(Int32 MyProperty;及其实现)。

首先,将新的 Midl 文件 (.idl) 项添加到 C++/WinRT 项目。 将其命名为 Project.idl。 将 Project.idl 的所有内容替换为以下代码。

// Project.idl
namespace SDKTemplate
{
    [default_interface]
    runtimeclass CopyFiles : Microsoft.UI.Xaml.Controls.Page
    {
        CopyFiles();
    }

    [default_interface]
    runtimeclass CopyImage : Microsoft.UI.Xaml.Controls.Page
    {
        CopyImage();
    }

    [default_interface]
    runtimeclass CopyText : Microsoft.UI.Xaml.Controls.Page
    {
        CopyText();
    }

    [default_interface]
    runtimeclass HistoryAndRoaming : Microsoft.UI.Xaml.Controls.Page
    {
        HistoryAndRoaming();
    }

    [default_interface]
    runtimeclass OtherScenarios : Microsoft.UI.Xaml.Controls.Page
    {
        OtherScenarios();
    }
}

正如你所看到的,这只是单个 .idl 文件内容的副本,所有内容都在一个命名空间内,并从 MyProperty 每个运行时类中删除。

在Visual Studio中的解决方案资源管理器中,多选所有原始 IDL 文件(CopyFiles.idl、、CopyImage.idlCopyText.idlHistoryAndRoaming.idlOtherScenarios.idl)和“编辑>删除”文件(在对话框中选择“删除”。

最后,为了完成移除 MyProperty,对于同样这五种 XAML 页面类型中的每一种,在其 .h.cpp 文件中,删除 int32_t MyProperty() 访问器函数和 void MyProperty(int32_t) 修改器函数的声明与定义。

顺便说一句,最好让 XAML 文件的名称与它们所表示的类的名称匹配。 例如,如果 XAML 标记文件中有 x:Class="MyNamespace.MyPage",则该文件应命名为 MyPage.xaml。 虽然这不是一项技术要求,但如果不必在同一工件的不同名称之间来回切换,你的项目将更易于理解、维护,也更容易协作。

CopyFiles

在 C# 项目中,CopyFiles XAML 页面类型在CopyFiles.xamlCopyFiles.xaml.cs源代码文件中实现。 我们依次来看一下 CopyFiles 的各个成员。

rootPage

这是一个专用字段。

// CopyFiles.xaml.cs
...
public sealed partial class CopyFiles : Page
{
    MainPage rootPage = MainPage.Current;
    ...
}
...

在 C++/WinRT 中,我们可以像这样定义和初始化它。

// CopyFiles.h
...
struct CopyFiles : CopyFilesT<CopyFiles>
{
    ...
private:
    SDKTemplate::MainPage rootPage{ MainPage::Current() };
};
...

同样(与 MainPage::current 一样), CopyFiles::rootPage 声明为 SDKTemplate::MainPage 类型,它是投影类型,而不是实现类型。

CopyFiles (构造函数)

在 C++/WinRT 项目中, CopyFiles 类型已有一个包含所需代码的构造函数(它只调用 InitializeComponent)。

CopyButton_Click

C# CopyButton_Click 方法是事件处理程序,从 async 其签名中的关键字中,我们可以判断该方法执行异步工作。 在 C++/WinRT 中,我们将异步方法实现为 协同例程。 有关 C++/WinRT 中的并发简介以及 协同例程 是什么的说明,请参阅 C++/WinRT 的并发和异步操作

通常希望在协同例程完成之后安排进一步的工作,在这种情况下,协同例程将返回一些可以等待的异步对象类型,并选择性地报告进度。 但这些注意事项通常不适用于事件处理程序。 因此,如果事件处理程序需要执行异步操作,那么可以将其实现为返回 winrt::fire_and_forget 的协程。 有关详细信息,请参阅 “火灾”和“忘记”。

虽然“发后即忘”协程这一概念意味着你不关心它何时完成,但其工作仍会在后台继续进行(或处于挂起状态,等待恢复执行)。 从 CopyButton_Click的 C# 实现中可以看到,该实现依赖于 this 指针(它访问实例数据成员 rootPage)。 因此,我们必须确保 this 指针(指向 CopyFiles 对象的指针)的生命周期长于 CopyButton_Click 协程的生命周期。 在类似于此示例应用程序的情况下,用户可在 UI 页面之间导航,我们无法直接控制这些页面的生存期。 如果 CopyFiles 页面在 CopyButton_Click 仍在后台线程中执行时被销毁(例如由于导航离开该页面),那么访问 rootPage 就不安全了。 要使该协程正确运行,它需要获取对 this 指针的强引用,并在协程的整个生命周期内保持该引用。 有关详细信息,请参阅 C++/WinRT 中的强引用和弱引用

如果你查看该示例的 C++/WinRT 版本中的 CopyFiles::CopyButton_Click,就会发现这是通过在栈上进行一个简单声明来实现的。

fire_and_forget CopyFiles::CopyButton_Click(IInspectable const&, RoutedEventArgs const&)
{
    auto lifetime{ get_strong() };
    ...
}

让我们看看移植的代码的其他方面,这些方面值得注意。

在代码中,我们实例化一个 FileOpenPicker 对象,稍后我们将访问该对象的 FileTypeFilter 属性两行。 该属性的返回类型实现字符串的 IVector 。 并且对于该 IVector,我们调用 IVector<T>.ReplaceAll(T[]) 方法。 有意思的地方在于,我们传递给该方法的值,而该方法此处需要的是一个数组。 下面是代码行。

filePicker.FileTypeFilter().ReplaceAll({ L"*" });

我们传递的值 ({ L"*" }) 是标准 C++ 初始值设定项列表。 在这种情况下,它包含单个对象,但初始值设定项列表可以包含任意数量的逗号分隔对象。 C++/WinRT 中让你能够方便地将初始化列表传递给此类方法的相关部分,已在 标准初始化列表 中进行了说明。

我们将 C# await 关键字移植到 co_await C++/WinRT 中。 下面是代码中的示例。

auto storageItems{ co_await filePicker.PickMultipleFilesAsync() };

接下来,请考虑此 C# 代码行。

dataPackage.SetStorageItems(storageItems);

C# 能够将由 storageItems 表示的 IReadOnlyList<StorageFile> 隐式转换为 DataPackage.SetStorageItems 所需的 IEnumerable<IStorageItem>。 但在 C++/WinRT 中,我们需要从 IVectorView<StorageFile> 显式转换为 IIterable<IStorageItem>。 因此,我们又看到了一个 as 函数的实际应用示例。

dataPackage.SetStorageItems(storageItems.as<IVectorView<IStorageItem>>());

在 C# 中,我们使用 null 关键字(例如,Clipboard.SetContentWithOptions(dataPackage, null));而在 C++/WinRT 中,我们使用 nullptr(例如,Clipboard::SetContentWithOptions(dataPackage, nullptr))。

PasteButton_Click

这是另一个事件处理程序,其形式为 fire-and-forget 协同例程。 让我们看看移植的代码的各个方面,这些方面值得注意。

在该示例的 C# 版本中,我们使用 catch (Exception ex) 捕获异常。 在移植的 C++/WinRT 代码中,你将看到表达式 catch (winrt::hresult_error const& ex)。 有关 winrt::hresult_error 以及如何使用它的详细信息,请参阅 C++/WinRT 的错误处理

测试 C# 对象是否为 null 的一个示例是 if (storageItems != null)。 在 C++/WinRT 中,我们可以依赖到 bool 的转换运算符,因为它会在内部对 nullptr 进行检查。

下面是示例移植的 C++/WinRT 版本中代码片段的稍微简化的版本。

std::wostringstream output;
output << std::wstring_view(ApplicationData::Current().LocalFolder().Path());

winrt::hstring 构造 std::wstring_view,这说明了调用 hstring::c_str 函数的替代方法(将 winrt::hstring 转换为 C 样式字符串)。 这种替代方案之所以可行,是因为 hstring 提供了到 std::wstring_view转换运算符

请考虑此 C# 片段。

var file = storageItem as StorageFile;
if (file != null)
...

若要将 C# 中的 as 关键字移植到 C++/WinRT 中,到目前为止,我们已经看到 as 函数被使用过几次。 如果类型转换失败,该函数将引发异常。 但是,如果我们希望在转换失败时返回 nullptr(以便我们可以在代码中处理这种情况),那么就改用 try_as 函数。

auto file{ storageItem.try_as<StorageFile>() };
if (file)
...

复制完成移植 CopyFiles 所需的 XAML

现在,您可以从原始示例源代码下载内容中的 shared 文件夹里,选中 CopyFiles.xaml 文件的全部内容,并将其粘贴到 C++/WinRT 项目中的 CopyFiles.xaml 文件中(替换该 C++/WinRT 项目中文件的现有内容)。

最后,编辑 CopyFiles.h.cpp 删除虚拟 ClickHandler 函数,因为我们刚刚重写了相应的 XAML 标记。

我们现在已完成 复制文件移植,如果一直在执行这些步骤,则 C++/WinRT 项目现在将生成并运行, CopyFiles 方案将正常运行。

CopyImage

若要移植 CopyImage XAML 页面类型,请遵循与 CopyFiles 相同的过程。 移植 CopyImage 时,你将遇到 C# using 语句的使用,这可确保正确释放实现 IDisposable 接口的对象。

if (imageReceived != null)
{
    using (var imageStream = await imageReceived.OpenReadAsync())
    {
        ... // Pass imageStream to other APIs, and do other work.
    }
}

C++/WinRT 中的等效接口是 IClosable,该接口仅包含一个 Close 方法。 下面是上述 C# 代码的 C++/WinRT 等效项。

if (imageReceived)
{
    auto imageStream{ co_await imageReceived.OpenReadAsync() };
    ... // Pass imageStream to other APIs, and do other work.
    imageStream.Close();
}

C++/WinRT 对象实现 IClosable 主要是为了方便那些缺乏确定性终结机制的语言。 C++/WinRT 具有确定性最终化,因此在编写 C++/WinRT 时,我们通常不需要调用 IClosable::Close 。 但有时最好就此打住,而现在就是这种时候之一。 此处,imageStream 标识符是对底层 Windows 运行时 对象的引用计数包装(在本例中,该对象实现了 IRandomAccessStreamWithContentType)。 虽然我们可以确定 imageStream (其析构函数)的终结器将在封闭范围(大括号)的末尾运行,但我们无法确定终结器将调用 Close。 这是因为我们将imageStream传递给了其他 API,而它们仍可能对底层 Windows 运行时 对象的引用计数产生影响。 因此,在这种情况下,最好显式调用 Close 。 有关详细信息,请参阅是否需要在使用的运行时类上调用 IClosable::Close?

接下来,请考虑在 (uint)(imageDecoder.OrientedPixelWidth * 0.5) 事件处理程序中找到的 C# 表达式。 该表达式将一个 uint 与一个 double 相乘,从而得到一个 double。 然后,将其强制转换为 uint。 在 C++/WinRT 中,我们可以使用一种看起来类似的 C 风格强制转换((uint32_t)(imageDecoder.OrientedPixelWidth() * 0.5)),但最好明确说明我们究竟想进行的是哪一种强制转换;而在这个例子中,我们会使用 static_cast<uint32_t>(imageDecoder.OrientedPixelWidth() * 0.5) 来做到这一点。

CopyImage.OnDeferredImageRequestedHandler 的 C# 版本有一个finally子句,但没有catch子句。 我们在 C++/WinRT 版本中稍有进一步,并实现了一个 catch 子句,以便我们可以报告延迟呈现是否成功。

移植此 XAML 页面的其余部分不会产生任何新内容来讨论。 请记住删除虚拟 的 ClickHandler 函数。 而且,与 CopyFiles 一样,端口的最后一步是选择整个内容 CopyImage.xaml,并将其粘贴到 C++/WinRT 项目中的同一文件中。

CopyText

你可以使用我们已经介绍过的技术来移植 CopyText.xamlCopyText.xaml.cs

HistoryAndRoaming

在移植 HistoryAndRoaming XAML 页面类型时,有几个值得注意的地方。

首先,查看 C# 源代码,并通过 OnHistoryEnabledChanged 事件处理程序跟踪 OnNavigatedTo 的控制流,最后转到异步函数 CheckHistoryAndRoaming(未等待,因此本质上是触发和忘记)。 由于 CheckHistoryAndRoaming 是异步的,因此需要在 C++/WinRT 中注意指针的 this 生存期。 如果查看源代码文件中的 HistoryAndRoaming.cpp 实现,可以查看结果。 首先,当我们为 Clipboard::HistoryEnabledChangedClipboard::RoamingEnabledChanged 事件附加委托时,我们仅保留对 HistoryAndRoaming 页面对象的弱引用。 我们是通过创建一个依赖于 winrt::get_weak 返回值的委托来做到这一点的,而不是依赖于 this 指针本身。 这意味着,如果我们从 HistoryAndRoaming 页面导航离开,最终会调用异步代码的委托本身不会让该页面继续保持存活。

其次,当我们最终执行这个发后不理的 CheckHistoryAndRoaming 协程时,首先要做的就是获取对 this 的一个强引用,以确保 HistoryAndRoaming 页面至少在该协程最终完成之前一直保持存活。 有关刚才介绍的两个方面的详细信息,请参阅 C++/WinRT 中的强引用和弱引用

在移植 CheckHistoryAndRoaming 时,我们发现另一个兴趣点。 它包含用于更新 UI 的代码;因此,我们需要确定我们在主 UI 线程上执行此操作。 最初调用事件处理程序的线程是主 UI 线程。 但通常,异步方法可以在任何任意线程上执行和/或恢复。 在 C# 中,解决方案是将工作调度到 UI 线程。 在 C++/WinRT 中,我们可以将 winrt::resume_foreground 函数与 this 指针的 DispatcherQueue 结合使用,使协程挂起,并立即在主 UI 线程上继续执行。

相关表达式为 co_await winrt::resume_foreground(DispatcherQueue());. 较短的版本是由 C++/WinRT 提供的转换运算符实现的。

移植此 XAML 页面的其余部分不会产生任何新内容来讨论。 请记住删除虚拟 ClickHandler 函数,并复制 XAML 标记。

OtherScenarios

你可以使用我们已经介绍过的技术移植 OtherScenarios.xamlOtherScenarios.xaml.cs

结论

希望通过本指南,你已经掌握了足够的移植信息和技巧,现在就可以着手将自己的 C# 应用程序移植到 C++/WinRT。 作为回顾,你可以继续参考 before(C#)和 after(C++/WinRT)版本的源代码,这些代码位于 Clipboard sample 中,并将它们并排比较,以查看两者之间的对应关系。