本主题介绍将其中一个通用 Windows 平台 (UWP)应用示例从 C# 移植到 C++/WinRT 的案例研究。 你可以跟着演练步骤边进行边亲自移植该示例,从而获得移植实践和经验。
注释
要移植的 源代码 是 UWP C# 应用。 本文中的目标 C++/WinRT 代码是为 WinUI 3 (Windows 应用 SDK) 编写的。 凡是 UWP 源中使用了在 WinUI 3 中有所不同的 API(例如 Windows.UI.Core.CoreDispatcher 与 Microsoft.UI.Dispatching.DispatcherQueue),本文都会在 C++/WinRT 输出中明确给出正确的 WinUI 3 对应项。 可以直接在 WinUI 3 应用中使用 C++/WinRT 列中的代码模式。
有关从 C# 移植到 C++/WinRT 所涉及的技术详细信息的综合目录,请参阅从 C# 移植到 C++/WinRT 的配套主题。
有关 C# 和 C++ 源代码文件的简要前言
在 C# 项目中,源代码文件主要是 .cs 文件。 移动到 C++ 时,你会注意到有更多类型的源代码文件可供使用。 原因是编译器之间的差异、重用 C++ 源代码的方式以及 声明 和 定义 类型及其函数(其方法)的概念。
函数 声明 仅描述函数的 签名 (其返回类型、其名称和参数类型和名称)。 函数 定义 包括函数的 主体 (其实现)。
在类型方面,这有点不同。 通过提供其名称和(至少)仅声明其所有成员函数(和其他成员)来定义类型。 正确,即使未定义其成员函数,也可以 定义 类型。
- 常见的 C++ 源文件包括
.h(dot 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 版本共同使用的其他文件可在
shared和SharedContent文件夹中找到。
本主题中的演练演示如何通过从 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.png、squaretile-sdk.pngstorelogo-sdk.png、tile-sdk.png和windows-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,其 Title 和 ClassType 属性。
在接下来的几个子节中,我们将了解如何移植 MainPage 和 Scenario。
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.h 和 MainPage.cpp 中。 而对于 SampleConfiguration.cs 中的逻辑,我们会将其转换为 SampleConfiguration.h 和 SampleConfiguration.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.cs和SampleConfiguration.cs来找到这些成员。
我们共找到 12 个 protected 和 private 字段及方法。 我们找到了以下public成员。
- 默认构造函数
MainPage()。 - 静态字段 “当前 ”和 “FEATURE_NAME”。
- 属性 IsClipboardContentChangedEnabled 和 Scenarios。
- BuildClipboardFormatsOutputString、DisplayToast、EnableClipboardContentChangedNotifications 和 NotifyUser 的方法。
正是那些 public 成员可以在 MainPage.idl 中声明。 因此,让我们检查每个类,看看它们是否需要成为 MainPage 运行时类的一部分,还是它们是否需要成为其实现的一部分。
- 默认构造函数
MainPage()。 对于 XAML 页,通常在其 IDL 中声明默认构造函数。 这样,XAML UI 框架就可以激活类型。 - 静态字段 Current 用于从单个方案 XAML 页中访问应用程序的 MainPage 实例。 由于 Current 不用于与 XAML 框架进行互操作(也不跨编译单元使用),因此我们可以将其保留为实现类型的唯一成员。 如果是你自己的项目,在这种情况下,你可能会选择这么做。 但是,既然该字段是投影类型的一个实例,那么在 IDL 中声明它似乎是合乎逻辑的。 因此,这就是我们将在这里执行的操作(这样做也使代码稍微更简洁)。
- 对于在 MainPage 类型中访问的静态FEATURE_NAME字段,这是类似的情况。 同样,选择在 IDL 中声明它会使代码稍微更简洁。
- 属性 IsClipboardContentChangedEnabled 仅在 OtherScenarios 类中使用。 因此,在移植过程中,我们会稍微简化一下,将其设为 OtherScenarios 运行时类的私有字段。 这样一个人就不会进入 IDL 了。
- 属性 方案 是 方案 类型的对象的集合(我们前面提到的类型)。 我们将在下一小节讨论 场景,因此我们也先暂且不讨论 Scenarios 属性,留到那时再说。
- BuildClipboardFormatsOutputString、DisplayToast 和 EnableClipboardContentChangedNotifications 这些方法属于实用工具函数,与其说它们和主页面有关,不如说它们更多是与示例的整体状态有关。 因此,在移植过程中,我们将把这三个方法重构到一个名为 SampleState 的新实用类型中(它不需要是 Windows 运行时 类型)。 因此,这三种方法不会被纳入 IDL。
- NotifyUser 方法在各个方案的 XAML 页面中被调用,调用对象是从静态 Current 字段返回的 MainPage 实例。 由于(如前所述) Current 是投影类型的实例,因此我们需要在 IDL 中声明 NotifyUser 。 NotifyUser 采用 NotifyType 类型的参数。 我们将在下一个子部分中讨论这一点。
任何要进行数据绑定的成员也都需要在 IDL 中声明(无论使用的是 {x:Bind} 还是 {Binding})。 有关详细信息,请参阅 数据绑定。
我们正在取得进展:我们正在整理一份清单,列出哪些成员要添加到 MainPage.idl 文件中,哪些不要添加。 但我们仍必须讨论 Scenarios 属性和 NotifyType 类型。 那么接下来我们就这样做吧。
Scenario 和 NotifyType 类型的 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 对象分配给 ListBox 的 ItemsSource 属性(它是一个项控件)。 由于 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.h 和 MainPage.cpp。
在这两个存根文件中,你会看到我们添加到 IDL 中的 MainPage 成员的新存根实现(例如 Current 和 FEATURE_NAME)。 你需要将这些存根实现复制到项目中已有的 MainPage.h 和 MainPage.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.xaml和App.xaml。 命名空间出现在x:Class和xmlns:local属性的值中。 -
App.idl。 -
App.h。 -
App.cpp。 有两个using namespace指令(搜索子字符串using namespace Clipboard),以及 MainPage 类型的两个限定(搜索Clipboard::MainPage)。 这些需要改变。
由于我们从 MainPage 中删除了事件处理程序,因此还进入 MainPage.xaml 并删除标记中的 Button 元素。
保存所有文件。 清理解决方案(生成>干净解决方案),然后生成它。 如果到目前为止一直严格按照所写内容完成了所有更改,则构建应会成功。
实现我们在 IDL 中声明的 MainPage 成员
构造函数、Current 和 FEATURE_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_NAME 是 MainPage 的静态字段(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 也是如此。
Current 是 MainPage 的静态(非常量)字段。 同样,我们将将其(C++/WinRT 表达式)设为只读属性,并再次使其为静态。 如果 FEATURE_NAME 为常量, 则当前 不是。 因此,在 C++/WinRT 中,我们需要一个后备字段,并且访问器将返回该字段。 因此,在 C++/WinRT 项目中,我们将在名为 MainPage.h 的专用静态字段中声明,我们将定义/初始化当前(MainPage.cpp 因为它具有静态存储持续时间),我们将通过名为 Current 的公共静态访问器函数访问它。
构造函数本身进行了几次赋值操作,这些操作很容易移植。
在 C++/WinRT 项目中,添加一个新的 Visual C++>> 文件(.cpp)项,其名称SampleConfiguration.cpp为 。
编辑 MainPage.xaml、MainPage.h、MainPage.cpp 和 SampleConfiguration.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 访问器函数以返回字符串文本。 总之,这行代码设置了名为 SampleTitle 的 TextBlock 的 Text 属性。
请注意,由于字符串在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 这种情况,如果这些元素本身不是运行时类,则它们必须属于能够与 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 中定义并初始化。 并且,Scenarios 是 MainPage 的一个只读属性,在 MainPage.xaml.cs 中定义(其实现只是简单地返回 scenarios 字段)。 我们将在 C++/WinRT 项目中执行类似操作;但是,我们将使两个成员保持静态(因为我们只需要整个应用程序一个实例;因此我们可以访问它们而无需类实例)。 我们将分别把它们命名为 scenariosInner 和 scenarios。 我们将在MainPage.h中声明scenariosInner。 而且,由于它具有静态存储持续时间,因此我们会在 .cpp 文件中定义/初始化它(SampleConfiguration.cpp在本例中)。
编辑 MainPage.h 和 SampleConfiguration.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 页面CopyImage、CopyFiles和HistoryAndRoamingOtherScenarios。
如果你愿意的话,现在你可以再次构建了。
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.cs 和 SampleConfiguration.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.h 和 SampleConfiguration.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 中移植 var 到 auto 。 但在 C++/WinRT 中,在某些情况下(为了避免拷贝),你会需要一个指向推断(或推导)类型的引用,而你可以用 auto& 来表示对推导类型的左值引用。 在某些情况下,需要一种特殊类型的引用,无论它是使用 左值 还是 右值进行初始化,该引用都是正确绑定的。 你用 auto&& 来表达这一点。 这就是你在下面移植后的代码中的 for 循环里看到使用的那种形式。 有关左值和右值的介绍,请参阅值类别及其引用。
编辑 pch.h、SampleConfiguration.h 和 SampleConfiguration.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 中引用 ToastNotification 和 XmlDocument 类型时,可以按命名空间名称限定它们,也可以编辑 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.h 和 SampleConfiguration.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){}
暂时将事件处理委托本身(OnClipboardChanged 和 OnWindowActivated)保留为存根。 这些成员已经在我们的待移植成员列表中,所以我们会在后续小节中再处理它们。
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,并将其赋给 ListBox 的 ItemsSource 属性(它是一个项控件)。 在 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.h 和 SampleConfiguration.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.h 和 SampleConfiguration.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 isplayChangedFormats 和 SampleState::BuildClipboardFormatsOutputString 的方法以调用该新方法。 它在功能上等效于本主题中的代码列表,但效率更高。
Footer_Click
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 的并发和异步操作。
现在我们来说说这个方法在做什么。 由于这是 HyperlinkButton 的 Click 事件的事件处理程序,因此名为发送方的对象实际上是 HyperlinkButton。 因此,类型转换是安全的(我们也可以将此转换表示为 sender as HyperlinkButton)。 接下来,我们检索 Tag 属性的值(如果查看 C# 项目中的 XAML 标记,你将看到此值设置为表示 Web URL 的字符串)。 尽管 FrameworkElement.Tag 属性(HyperlinkButton 是 FrameworkElement)的类型 对象,但在 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 版本。
OnClipboardChanged 和 OnWindowActivated
到目前为止,这两个事件处理程序只有空存根。 但是移植它们很简单,它并没有引发任何新的讨论。
ScenarioControl_SelectionChanged
这是属于 C# MainPage 类的另一个私有事件处理程序,并在该类中 MainPage.xaml.cs定义。 在 C++/WinRT 中,我们将把它设为公共,并在 MainPage.h 和 MainPage.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,请添加一个名为 StatusBlock 的 TextBlock。 - 若要
MainPage.xaml,请添加一个名为 StatusPanel 的 StackPanel。 - 若要
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.xaml , MainPage.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.idl、CopyImage.idl、CopyText.idl、HistoryAndRoaming.idl 和 OtherScenarios.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.idl和OtherScenarios.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.xaml 和 CopyText.xaml.cs。
HistoryAndRoaming
在移植 HistoryAndRoaming XAML 页面类型时,有几个值得注意的地方。
首先,查看 C# 源代码,并通过 OnHistoryEnabledChanged 事件处理程序跟踪 OnNavigatedTo 的控制流,最后转到异步函数 CheckHistoryAndRoaming(未等待,因此本质上是触发和忘记)。 由于 CheckHistoryAndRoaming 是异步的,因此需要在 C++/WinRT 中注意指针的 this 生存期。 如果查看源代码文件中的 HistoryAndRoaming.cpp 实现,可以查看结果。 首先,当我们为 Clipboard::HistoryEnabledChanged 和 Clipboard::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.xaml 和 OtherScenarios.xaml.cs。
结论
希望通过本指南,你已经掌握了足够的移植信息和技巧,现在就可以着手将自己的 C# 应用程序移植到 C++/WinRT。 作为回顾,你可以继续参考 before(C#)和 after(C++/WinRT)版本的源代码,这些代码位于 Clipboard sample 中,并将它们并排比较,以查看两者之间的对应关系。