从 C++/CX 移动到 C++/WinRT

本主题是系列文章中的第一篇,介绍如何将 C++/CX 项目中的源代码移植到 C++/WinRT 中的等效项。

如果你的项目还使用 Windows 运行时 C++ 模板库 (WRL) 类型,请参阅从 WRL 迁移到 C++/WinRT

移植策略

值得注意的是,从 C++/CX 到 C++/WinRT 的移植通常很简单,但从并行模式库 (PPL) 任务迁移到协同程序的情况例外。 其模型不同。 从 PPL 任务到协同程序没有自然的一对一映射,也没有适用于所有情况的机械移植代码的简单方法。 有关移植的这一特定方面的帮助信息,以及在这两个模型之间进行互操作的选项,请参阅实现 C++/WinRT 与 C++/CX 之间的异步和互操作

开发团队通常会报告说,一旦克服了移植异步代码的障碍,其余的移植工作大部分是机械式的。

一次性移植

如果你能够一次性移植整个项目,那么只需要阅读本主题即可获取所需的信息(无需阅读本主题后面的互操作主题)。 建议首先使用 C++/WinRT 项目模板之一在 Visual Studio 中创建一个新项目(请参阅 Visual Studio 对 C++/WinRT 的支持)。 然后将源代码文件移入该新项目,与此同时,将所有 C++/CX 源代码移植到 C++/WinRT 中。

或者,如果你希望在现有的 C++/CX 项目中进行移植工作,则需要向其添加 C++/WinRT 支持。 采用 C++/CX 项目并添加 C++/WinRT 支持中介绍了执行此操作的步骤。 完成移植之时,便已将原来的纯 C++/CX 项目变成了纯 C++/WinRT 项目。

注意

如果你有 Windows 运行时组件项目,则一次性移植是你唯一的选项。 用 C++ 编写的 Windows 运行时组件项目包含的要么全部是 C++/CX 源代码,要么全部是 C++/WinRT 源代码。 它们不能在此项目类型中共存。

逐步移植项目

除了前面部分中提到的 Windows 运行时组件项目以外,如果代码库的大小或复杂性使得有必要逐步移植项目,则需要一个移植过程,在此过程中的某段时间,C++/CX 和 C++/WinRT 代码将在同一项目中并存。 除了阅读本主题,还应参阅实现 C++/WinRT 与 C++/CX 之间的互操作实现 C++/WinRT 与 C++/CX 之间的异步和互操作。 这些主题提供了一些信息和代码示例,演示如何在这两种语言投影之间进行互操作。

若要使项目为逐步移植过程做好准备,一种选择是向 C++/CX 项目添加 C++/WinRT 支持。 采用 C++/CX 项目并添加 C++/WinRT 支持中介绍了执行此操作的步骤。 然后,你就可以从这里逐步进行移植。

另一种选择是使用 C++/WinRT 项目模板之一在 Visual Studio 中创建一个新项目(请参阅 Visual Studio 对 C++/WinRT 的支持)。 然后向该项目添加 C++/CX 支持。 采用 C++/WinRT 项目并添加 C++/CX 支持中介绍了执行此操作的步骤。 然后,你可以开始将源代码移入其中,与此同时,将一些 C++/CX 源代码移植到 C++/WinRT 中。

无论是哪种情况,都需要在 C++/WinRT 代码与尚未移植的任何 C++/CX 代码之间进行互操作(双向)。

注意

C++/CX 和 Windows SDK 都在根命名空间 Windows 中声明类型。 投影到 C++/WinRT 的 Windows 类型具有与 Windows 类型相同的完全限定名称,但放置于 C++ winrt 命名空间中。 这些不同的命名空间可让你按照自己的节奏从 C++/CX 移植到 C++/WinRT。

逐步移植 XAML 项目

重要

对于使用 XAML 的项目,无论何时,均要求所有 XAML 页面类型要么完全是 C++/CX,要么完全是 C++/WinRT。 你仍可以在同一项目中 XAML 页面类型以外的位置(在模型和视图模型中以及其他位置)混合使用 C++/CX 和 C++/WinRT。

对于这种情况,我们建议的工作流是创建一个新的 C++/WinRT 项目并从 C++/CX 项目复制源代码和标记。 只要所有 XAML 页面类型都是 C++/WinRT,就可以使用“项目”>“添加新项...”>“Visual C++”>“空白页(C++/WinRT)”来添加新 XAML 页面。

或者,可以在移植 XAML C++/CX 项目时使用 Windows 运行时组件 (WRC) 从中提出代码。

  • 可以创建一个新的 C++/CX WRC 项目,将尽可能多的 C++/CX 代码移入该项目,然后将 XAML 项目更改为 C++/WinRT。
  • 或者,可以创建一个新的 C++/WinRT WRC 项目,将 XAML 项目保留为 C++/CX,然后开始将 C++/CX 移植到 C++/WinRT,将所得到的代码移出 XAML 项目并移入组件项目。
  • 还可以让 C++/CX 组件项目以及 C++/WinRT 组件项目处于同一个解决方案中,从应用程序项目引用两者,然后逐渐从一个项目移植到另一个项目。 同样,请参阅实现 C++/WinRT 与 C++/CX 之间的互操作,了解有关在同一个项目中使用这两种语言投影的更多详细信息。

将 C++/CX 项目移植到 C++/WinRT 的第一步

无论使用哪种移植策略(一次性移植或逐步移植),第一步都是准备要移植的项目。 下面回顾了移植策略中所述的内容,其中涉及你将要开始使用的项目类型以及如何对其进行设置。

  • 一次性移植。 使用 C++/WinRT 项目模板之一在 Visual Studio 中创建一个新项目。 将文件从 C++/CX 项目移入该新项目,然后移植 C++/CX 源代码。
  • 逐步移植非 XAML 项目。 可以选择向 C++/CX 项目添加 C++/WinRT 支持(请参阅采用 C++/CX 项目并添加 C++/WinRT 支持),然后逐步进行移植。 也可以选择创建一个新的 C++/WinRT 项目并向其添加 C++/CX 支持(请参阅采用 C++/WinRT 项目并添加 C++/CX 支持),移入文件并逐步进行移植。
  • 逐步移植 XAML 项目。 创建一个新的 C++/WinRT 项目,移入文件并逐步进行移植。 无论何时,均要求 XAML 页面类型要么完全是 C++/WinRT,要么完全是 C++/CX。

无论选择哪种移植策略,本主题的其余部分均适用。 它包含将源代码从 C++/CX 移植到 C++/WinRT 所涉及的技术细节的目录。 如果要进行逐步移植,则可能还需要查看实现 C++/WinRT 与 C++/CX 之间的互操作实现 C++/WinRT 与 C++/CX 之间的异步和互操作

文件命名约定

XAML 标记文件

文件原始格式 C++/CX C++/WinRT
开发人员 XAML 文件 MyPage.xaml
MyPage.xaml.h
MyPage.xaml.cpp
MyPage.xaml
MyPage.h
MyPage.cpp
MyPage.idl(见下)
生成的 XAML 文件 MyPage.xaml.g.h
MyPage.xaml.g.hpp
MyPage.xaml.g.h
MyPage.xaml.g.hpp
MyPage.g.h

请注意,C++/WinRT 会从 *.h*.cpp 文件名中删除 .xaml

C++/WinRT 添加了一个额外的开发人员文件,即 Midl 文件 (.idl) 。 C++/CX 在内部自动生成此文件,并将其添加到每个公开的和受保护的成员。 在 C++/WinRT 中,你自行创作并添加此文件。 如需更多详细信息、代码示例以及 IDL 创作演练,请参阅 XAML 控件;绑定到 C++/WinRT 属性

另请参阅将运行时类重构到 Midl 文件 (.idl) 中

运行时类

C++/CX 不对头文件的名称施加限制;通常会将多个运行时类定义置于单个头文件中,小型类尤其如此。 但是,C++/WinRT 要求每个运行时类将自己的头文件以类名称来命名。

C++/CX C++/WinRT
Common.h
ref class A { ... }
ref class B { ... }
Common.idl
runtimeclass A { ... }
runtimeclass B { ... }
A.h
namespace implements {
  struct A { ... };
}
B.h
namespace implements {
  struct B { ... };
}

在 C++/CX 中,使用不同命名方式的头文件进行 XAML 自定义控制的做法不太常见(但仍属合法)。 需根据类名来重命名这些头文件。

C++/CX C++/WinRT
A.xaml
<Page x:Class="LongNameForA" ...>
A.xaml
<Page x:Class="LongNameForA" ...>
A.h
partial ref class LongNameForA { ... }
LongNameForA.h
namespace implements {
  struct LongNameForA { ... };
}

头文件要求

C++/CX 不要求你包括任何特殊的头文件,因为它会在内部根据 .winmd 文件自动生成头文件。 在 C++/CX 中,通常会对按名称使用的命名空间使用 using 指令。

using namespace Windows::Media::Playback;

String^ NameOfFirstVideoTrack(MediaPlaybackItem^ item)
{
    return item->VideoTracks->GetAt(0)->Name;
}

可以通过 using namespace Windows::Media::Playback 指令在不使用命名空间前缀的情况下编写 MediaPlaybackItem。 我们还接触了 Windows.Media.Core 命名空间,因为 item->VideoTracks->GetAt(0) 返回 Windows.Media.Core.VideoTrack。 但是,我们不需要在任何位置键入 VideoTrack 这个名称,因此不需要 using Windows.Media.Core 指令。

但是,C++/WinRT 要求你针对每个所使用的命名空间包括一个头文件,即使不为其命名。

#include <winrt/Windows.Media.Playback.h>
#include <winrt/Windows.Media.Core.h> // !!This is important!!

using namespace winrt;
using namespace Windows::Media::Playback;

winrt::hstring NameOfFirstVideoTrack(MediaPlaybackItem const& item)
{
    return item.VideoTracks().GetAt(0).Name();
}

另一方面,即使 MediaPlaybackItem.AudioTracksChanged 事件的类型为 TypedEventHandler<MediaPlaybackItem, Windows.Foundation.Collections.IVectorChangedEventArgs>,我们也不需包括 winrt/Windows.Foundation.Collections.h,因为并未使用该事件。

C++/WinRT 还要求你针对 XAML 标记所使用的命名空间包括相应的头文件。

<!-- MainPage.xaml -->
<Rectangle Height="400"/>

使用 Rectangle 类意味着你必须添加以下 include。

// MainPage.h
#include <winrt/Windows.UI.Xaml.Shapes.h>

如果忘记了头文件,则仍可正常编译,但会出现链接器错误,因为缺少 consume_ 类。

参数传递

编写 C++/CX 源代码时,在顶帽 (^) 引用时,你将 C++/CX 类型作为函数参数传递。

void LogPresenceRecord(PresenceRecord^ record);

在 C++/WinRT 中,对于同步函数,默认情况下应该使用 const& 参数。 这将避免复制和互锁开销。 但你的协同程序应使用按值传递来确保它们按值捕获,并避免生命周期问题(更多详细信息,请参阅利用 C++/WinRT 实现的并发和异步操作)。

void LogPresenceRecord(PresenceRecord const& record);
IASyncAction LogPresenceRecordAsync(PresenceRecord const record);

C++/WinRT 对象根本上是一个保留支持 Windows 运行时对象的接口指针的值。 在复制 C++/WinRT 对象时,编译器复制封装的接口指针,从而递增其引用计数。 副本的最终销毁涉及递减引用计数。 因此,仅在必要时产生复制开销。

变量和字段引用

编写 C++/CX 源代码时,你使用顶帽 (^) 变量引用 Windows 运行时对象,使用箭头 (->) 运算符来取消引用顶帽变量。

IVectorView<User^>^ userList = User::Users;

if (userList != nullptr)
{
    for (UINT32 iUser = 0; iUser < userList->Size; ++iUser)
    ...

移植到等效 C++/WinRT 代码时,可以通过删除顶帽,并将箭头运算符 (->) 更改为点运算符 (.),来完成大量工作。 C++/WinRT 投影类型是值,而不是指针。

IVectorView<User> userList = User::Users();

if (userList != nullptr)
{
    for (UINT32 iUser = 0; iUser < userList.Size(); ++iUser)
    ...

C++/CX 顶帽引用的默认构造函数会将它初始化为 null。 下面是一个 C++/CX 代码示例,我们在其中创建一个具有正确类型,但是未初始化的变量/字段。 换句话说,它最初不引用 TextBlock;我们打算在以后分配引用。

TextBlock^ textBlock;

class MyClass
{
    TextBlock^ textBlock;
};

有关 C++/WinRT 中的等效项,请参阅延迟初始化

“属性”

C++/CX 语言扩展包括属性概念。 编写 C++/CX 源代码时,你可以像访问字段那样访问属性。 标准 C++ 没有属性概念,因此,在 C++/WinRT 中,你调用获取和设置函数。

在随后的示例中,XboxUserId、UserState、PresenceDeviceRecords 和 Size 全部都是属性。

从属性检索值

下面介绍如何在 C++/CX 中获取属性值。

void Sample::LogPresenceRecord(PresenceRecord^ record)
{
    auto id = record->XboxUserId;
    auto state = record->UserState;
    auto size = record->PresenceDeviceRecords->Size;
}

等效的 C++/WinRT 源代码调用与属性同名但没有参数的函数。

void Sample::LogPresenceRecord(PresenceRecord const& record)
{
    auto id = record.XboxUserId();
    auto state = record.UserState();
    auto size = record.PresenceDeviceRecords().Size();
}

请注意,PresenceDeviceRecords 函数返回其本身具有 Size 函数的 Windows 运行时对象。 由于返回的对象也是 C++/WinRT 投影类型,因此我们使用点运算符调用 Size 来取消引用。

将属性设置为新值

将属性设置为新值遵循类似模式。 首先,在 C++/CX 中。

record->UserState = newValue;

若要在 C++/WinRT 执行同等操作,调用与属性同名的函数,并传递参数。

record.UserState(newValue);

创建类的实例

你通过 C++/CX 对象的句柄来处理它,通常称为顶帽 (^) 引用。 通过 ref new 关键字创建新对象,这反过来会调用 RoActivateInstance 来激活运行时类的新实例。

using namespace Windows::Storage::Streams;

class Sample
{
private:
    Buffer^ m_gamerPicBuffer = ref new Buffer(MAX_IMAGE_SIZE);
};

C++/WinRT 对象是一个值;因此你可以在堆栈上进行分配,或作为对象字段分配。 切勿使用 ref new(或 new)来分配 C++/WinRT 对象。 在后台,仍然在调用 RoActivateInstance。

using namespace winrt::Windows::Storage::Streams;

struct Sample
{
private:
    Buffer m_gamerPicBuffer{ MAX_IMAGE_SIZE };
};

如果初始化资源的成本很高,通常可以推迟资源的初始化,直到实际需要时再执行。 如前所述,C++/CX 顶帽引用的默认构造函数会将它初始化为 null。

using namespace Windows::Storage::Streams;

class Sample
{
public:
    void DelayedInit()
    {
        // Allocate the actual buffer.
        m_gamerPicBuffer = ref new Buffer(MAX_IMAGE_SIZE);
    }

private:
    Buffer^ m_gamerPicBuffer;
};

移植到 C++/WinRT 的同一个代码。 请注意 std::nullptr_t 构造函数的使用。 有关该构造函数的详细信息,请参阅延迟初始化

using namespace winrt::Windows::Storage::Streams;

struct Sample
{
    void DelayedInit()
    {
        // Allocate the actual buffer.
        m_gamerPicBuffer = Buffer(MAX_IMAGE_SIZE);
    }

private:
    Buffer m_gamerPicBuffer{ nullptr };
};

默认构造函数如何影响集合

C++ 集合类型使用默认构造函数,这可能导致意外的对象构造。

方案 C++/CX C++/WinRT(不正确) C++/WinRT(正确)
本地变量,一开始为空 TextBox^ textBox; TextBox textBox; // Creates a TextBox! TextBox textBox{ nullptr };
成员变量,一开始为空 class C {
  TextBox^ textBox;
};
class C {
  TextBox textBox; // Creates a TextBox!
};
class C {
  TextBox textbox{ nullptr };
};
全局变量,一开始为空 TextBox^ g_textBox; TextBox g_textBox; // Creates a TextBox! TextBox g_textBox{ nullptr };
空引用的矢量 std::vector<TextBox^> boxes(10); // Creates 10 TextBox objects!
std::vector<TextBox> boxes(10);
std::vector<TextBox> boxes(10, nullptr);
在 map 中设置值 std::map<int, TextBox^> boxes;
boxes[2] = value;
std::map<int, TextBox> boxes;
// Creates a TextBox at 2,
// then overwrites it!
boxes[2] = value;
std::map<int, TextBox> boxes;
boxes.insert_or_assign(2, value);
空引用的数组 TextBox^ boxes[2]; // Creates 2 TextBox objects!
TextBox boxes[2];
TextBox boxes[2] = { nullptr, nullptr };
配对 std::pair<TextBox^, String^> p; // Creates a TextBox!
std::pair<TextBox, String> p;
std::pair<TextBox, String> p{ nullptr, nullptr };

有关空引用集合的详细信息

只要在 C++/CX 中有一个 Platform::Array(参见移植 Platform::Array),即可将其移植到 C++/WinRT 中的 std::vector(事实上,可以将其移植到任何邻近的容器),而不是将其作为数组保留。 选择 std::vector 有多种优势。

例如,尽管存在用于创建固定大小的空引用矢量(参见上表)的速记,但没有用于创建空引用数组的速记。 必须针对数组中的每个元素重复执行 nullptr。 如果构造的数目太少,则会默认构造额外的。

对于矢量,可以在初始化时使用空引用填充它(如上表所示),也可以借助如下所示的代码在初始化后使用空引用填充它。

std::vector<TextBox> boxes(10); // 10 default-constructed TextBoxes.
boxes.resize(10, nullptr); // 10 empty references.

有关 std::map 示例的详细信息

std::map[] 下标运算符的行为如下所示。

  • 如果在 map 中发现此键,则返回现有值(可以将其覆盖)的引用。
  • 如果未在 map 中发现此键,则在包含此键(在可移动的情况下已经移动)的 map 中创建新条目和默认构造的值,并返回此值(可以随后将其覆盖)的引用。

换言之,[] 运算符始终在 map 中创建一个条目。 这不同于 C#、Java 和 JavaScript。

从运行时基类转换为派生类

通常有一个已知引用派生类型对象的基类引用。 在 C++/CX 中,使用 dynamic_cast 将基类引用强制转换为派生类引用。 dynamic_cast 实际上只是对 QueryInterface 的隐藏调用。 下面是一个典型示例:你要处理依赖属性更改事件,并且要从 DependencyObject 强制转换回拥有依赖属性的实际类型。

void BgLabelControl::OnLabelChanged(Windows::UI::Xaml::DependencyObject^ d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e)
{
    BgLabelControl^ theControl{ dynamic_cast<BgLabelControl^>(d) };

    if (theControl != nullptr)
    {
        // succeeded ...
    }
}

等效 C++/WinRT 代码将 dynamic_cast 替换为对 IUnknown::try_as 函数的调用,该函数会封装 QueryInterface。 还可以选择改为调用 IUnknown::as,这会在未返回对所需接口(具有所请求的类型的默认接口)的查询时引发异常。 下面是 C++/WinRT 代码示例。

void BgLabelControl::OnLabelChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e)
{
    if (BgLabelControlApp::BgLabelControl theControl{ d.try_as<BgLabelControlApp::BgLabelControl>() })
    {
        // succeeded ...
    }

    try
    {
        BgLabelControlApp::BgLabelControl theControl{ d.as<BgLabelControlApp::BgLabelControl>() };
        // succeeded ...
    }
    catch (winrt::hresult_no_interface const&)
    {
        // failed ...
    }
}

派生类

若要从运行时类派生,基类必须是可组合类。 C++/CX 不需要你执行任何特殊步骤即可将类变为可组合类,但 C++/WinRT 需要。 请使用 unsealed 关键字来指示你希望将类用作基类。

unsealed runtimeclass BasePage : Windows.UI.Xaml.Controls.Page
{
    ...
}
runtimeclass DerivedPage : BasePage
{
    ...
}

在实现标头类中,在包括为派生类自动生成的标头之前,必须包括基类标头文件。 否则会出现“将此类型用作表达式非法”之类的错误。

// DerivedPage.h
#include "BasePage.h"       // This comes first.
#include "DerivedPage.g.h"  // Otherwise this header file will produce an error.

namespace winrt::MyNamespace::implementation
{
    struct DerivedPage : DerivedPageT<DerivedPage>
    {
        ...
    }
}

通过代理进行事件处理

下面介绍了在 C++/CX 中处理事件的典型示例,在本例中将 lambda 函数用作代理。

auto token = myButton->Click += ref new RoutedEventHandler([=](Platform::Object^ sender, RoutedEventArgs^ args)
{
    // Handle the event.
    // Note: locals are captured by value, not reference, since this handler is delayed.
});

它是 C++/WinRT 中的等效项。

auto token = myButton().Click([=](IInspectable const& sender, RoutedEventArgs const& args)
{
    // Handle the event.
    // Note: locals are captured by value, not reference, since this handler is delayed.
});

不使用 lambda 函数,你可以选择作为自由函数或指向成员函数的指针实现代理。 有关详细信息,请参阅使用 C++/WinRT 中的代理来处理事件

如果你正在从内部使用(不是跨二进制文件)事件和代理的 C++/CX 基本代码移植,winrt::delegate 将帮助你复制 C++/WinRT 中的这个模式。 另请参阅项目中的参数化委托、简单信号和回调

撤销代理

在 C++/CX 中,使用 -= 运算符来撤销之前的事件注册。

myButton->Click -= token;

它是 C++/WinRT 中的等效项。

myButton().Click(token);

有关详细信息和选项,请参阅撤销已注册的代理

装箱和取消装箱

C++/CX 自动将标量装箱到对象中。 C++/WinRT 要求你显式调用 winrt::box_value 函数。 两种语言都要求你以显式方式取消装箱。 请参阅使用 C++/WinRT 装箱和取消装箱

在下面的表中,我们将使用这些定义。

C++/CX C++/WinRT
int i; int i;
String^ s; winrt::hstring s;
Object^ o; IInspectable o;
操作 C++/CX C++/WinRT
装箱 o = 1;
o = "string";
o = box_value(1);
o = box_value(L"string");
取消装箱 i = (int)o;
s = (String^)o;
i = unbox_value<int>(o);
s = unbox_value<winrt::hstring>(o);

如果尝试取消值类型的 null 指针的装箱,C++/CX 和 C# 会引发异常。 C++/WinRT 将其视为编程错误,因此会崩溃。 在 C++/WinRT 中,请使用 winrt::unbox_value_or 函数来处理对象类型不符合预期的情况。

方案 C++/CX C++/WinRT
取消已知整数的装箱 i = (int)o; i = unbox_value<int>(o);
如果 o 为 null Platform::NullReferenceException 崩溃
如果 o 不是装箱的整数 Platform::InvalidCastException 崩溃
取消整数的装箱,在为 null 的情况下使用回退;任何其他情况则崩溃 i = o ? (int)o : fallback; i = o ? unbox_value<int>(o) : fallback;
尽可能取消整数的装箱;在任何其他情况下使用回退 auto box = dynamic_cast<IBox<int>^>(o);
i = box ? box->Value : fallback;
i = unbox_value_or<int>(o, fallback);

将字符串装箱和取消装箱

字符串在某些情况下是值类型,在另一些情况下是引用类型。 C++/CX 和 C++/WinRT 对待字符串的方式有所不同。

ABI 类型 HSTRING 是一个指向引用计数字符串的指针。 但是,它并非派生自 IInspectable,因此从技术上来说它不是一个对象。 另外,null HSTRING 表示空字符串。 将并非派生自 IInspectable 的项装箱时,需将其包装到 IReference<T> 中,而 Windows 运行时会以 PropertyValue 对象的形式提供标准实现(自定义类型以 PropertyType::OtherType 形式报告)。

C++/CX 将 Windows 运行时字符串表示为引用类型,而 C++/WinRT 则将字符串投影为值类型。 这意味着装箱的 null 字符串可能有不同的表示形式,具体取决于你所采用的方法。

另外,C++/CX 允许取消引用 null String^ ,在这种情况下,其行为类似于字符串 ""

行为 C++/CX C++/WinRT
声明 Object^ o;
String^ s;
IInspectable o;
hstring s;
字符串类型类别 引用类型 值类型
null HSTRING 投影方式 (String^)nullptr hstring{}
null 和 "" 是否相同?
null 的有效性 s = nullptr;
s->Length == 0(有效)
s = hstring{};
s.size() == 0(有效)
如果将 null 字符串分配给对象 o = (String^)nullptr;
o == nullptr
o = box_value(hstring{});
o != nullptr
如果将 "" 分配给对象 o = "";
o == nullptr
o = box_value(hstring{L""});
o != nullptr

基本装箱和取消装箱。

操作 C++/CX C++/WinRT
将字符串装箱 o = s;
空字符串变为 nullptr。
o = box_value(s);
空字符串变为非 null 对象。
取消已知字符串的装箱 s = (String^)o;
Null 对象变为空字符串。
如果不是字符串,则引发 InvalidCastException。
s = unbox_value<hstring>(o);
Null 对象崩溃。
如果不是字符串,则崩溃。
将可能的字符串取消装箱 s = dynamic_cast<String^>(o);
Null 对象或非字符串变为空字符串。
s = unbox_value_or<hstring>(o, fallback);
Null 或非字符串变为 fallback。
空字符串被保留。

并发和异步操作

并行模式库 (PPL)(例如 concurrency::task)已更新,现在支持 C++/CX 顶帽引用。

对于 C++/WinRT,应改用协同程序和 co_await。 详细信息和代码示例,请参阅利用 C++/WinRT 实现的并发和异步运算

使用 XAML 标记中的对象

在 C++/CX 项目中,可以使用 XAML 标记中的专用成员和命名元素。 但在 C++/WinRT 中,以 XAML {x:Bind} 标记扩展形式使用的所有实体必须在 IDL 中以公开方式公开。

另外,绑定到布尔值时,在 C++/CX 中会显示 truefalse,但在 C++/WinRT 中会显示 Windows.Foundation.IReference`1<布尔值>

有关详细信息和代码示例,请参阅使用标记中的对象

将 C++/CX 平台类型映射到 C++/WinRT 类型

C++/CX 在平台命名空间中提供了多个数据类型。 这些类型不是标准的 C++,因此只能在启用 Windows 运行时语言扩展(Visual Studio 项目属性“C/C++”>“常规”>“使用 Windows 运行时扩展”>“是(/ZW)”)的情况下使用。 下表帮助你从平台类型移植到 C++/WinRT 中的等效项。 完成后,由于 C++/WinRT 是标准 C++,因此你可以关闭 /ZW 选项。

C++/CX C++/WinRT
Platform::Agile^ winrt::agile_ref
Platform::Array^ 请参阅移植 Platform::Array^
Platform::Exception^ winrt::hresult_error
Platform::InvalidArgumentException^ winrt::hresult_invalid_argument
Platform::Object^ winrt::Windows::Foundation::IInspectable
Platform::String^ winrt::hstring

将 Platform::Agile^ 移植到 winrt::agile_ref

C++/CX 中的 Platform::Agile^ 类型表示可以从任何线程访问的 Windows 运行时类。 C++/WinRT 的等效项是 winrt::agile_ref

在 C++/CX 中。

Platform::Agile<Windows::UI::Core::CoreWindow> m_window;

在 C++/WinRT 中。

winrt::agile_ref<Windows::UI::Core::CoreWindow> m_window;

移植 Platform::Array

在 C++/CX 要求使用数组的情况下,C++/WinRT 允许使用任何相邻的容器。 请参阅默认构造函数如何影响集合,了解为何可以使用 std::vector

因此,只要在 C++/CX 中有 Platform::Array^,移植选项中就会包括使用初始值设定项列表、std::array 或 std::vector 的选项。 有关详细信息和代码示例,请参阅标准初始值设定项列表标准数组和矢量

将 Platform::Exception^ 移植到 winrt::hresult_error

当 Windows 运行时 API 返回非 S_OK HRESULT 时,Platform::Exception^ 类型在 C++/CX 中生成。 C++/WinRT 的等效项是 winrt::hresult_error

若要移植到 C++/WinRT,将使用 Platform::Exception^ 的所有代码更改为使用 winrt::hresult_error。

在 C++/CX 中。

catch (Platform::Exception^ ex)

在 C++/WinRT 中。

catch (winrt::hresult_error const& ex)

C++/WinRT 提供这些异常类。

例外类型 基类 HRESULT
winrt::hresult_error 调用 hresult_error::to_abi
winrt::hresult_access_denied winrt::hresult_error E_ACCESSDENIED
winrt::hresult_canceled winrt::hresult_error ERROR_CANCELLED
winrt::hresult_changed_state winrt::hresult_error E_CHANGED_STATE
winrt::hresult_class_not_available winrt::hresult_error CLASS_E_CLASSNOTAVAILABLE
winrt::hresult_illegal_delegate_assignment winrt::hresult_error E_ILLEGAL_DELEGATE_ASSIGNMENT
winrt::hresult_illegal_method_call winrt::hresult_error E_ILLEGAL_METHOD_CALL
winrt::hresult_illegal_state_change winrt::hresult_error E_ILLEGAL_STATE_CHANGE
winrt::hresult_invalid_argument winrt::hresult_error E_INVALIDARG
winrt::hresult_no_interface winrt::hresult_error E_NOINTERFACE
winrt::hresult_not_implemented winrt::hresult_error E_NOTIMPL
winrt::hresult_out_of_bounds winrt::hresult_error E_BOUNDS
winrt::hresult_wrong_thread winrt::hresult_error RPC_E_WRONG_THREAD

请注意,每个类(通过 hresult_error 基类)均提供 to_abi 函数,其返回错误 HRESULT,并提供 message 函数,其返回该 HRESULT 的字符串表示形式。

下面是在 C++/CX 中抛出异常的示例。

throw ref new Platform::InvalidArgumentException(L"A valid User is required");

以及 C++/WinRT 中的等效项。

throw winrt::hresult_invalid_argument{ L"A valid User is required" };

将 Platform::Object^ 移植到 winrt::Windows::Foundation::IInspectable

与所有 C++/WinRT 类型一样,winrt::Windows::Foundation::IInspectable 属于值类型。 下面介绍如何初始化类型为 null 的变量。

winrt::Windows::Foundation::IInspectable var{ nullptr };

从 Platform::String^ 移植到 winrt::hstring

Platform::String^ 等同于 Windows 运行时 HSTRING ABI 类型。 对于 C++/WinRT,等效项是 winrt::hstring。 但使用 C++/WinRT,你可以使用 C++ 标准库宽字符串类型(如 std::wstring)和/或宽字符串文字调用 Windows 运行时 API。 有关更多详细信息和代码示例,请参阅 C++/WinRT 中的字符串处理

通过 C++/CX,你可以访问 Platform::String::Data 属性来作为 C 样式 wchar_t* 数组检索字符串(例如,将其传递到 std::wcout)。

auto var{ titleRecord->TitleName->Data() };

使用 C++/WinRT 也一样,你可以使用 hstring::c_str 函数获取 null 结尾的 C 样式字符串版本,就像从 std::wstring 获取的一样。

auto var{ titleRecord.TitleName().c_str() };

在实现获取或返回字符串的 API 时,通常要将使用 Platform::String^ 的任何 C++/CX 代码更改为使用 winrt::hstring。

下面是获取字符串的 C++/CX API 的示例。

void LogWrapLine(Platform::String^ str);

对于 C++/WinRT,你可以像这样在 MIDL 3.0 中声明该 API。

// LogType.idl
void LogWrapLine(String str);

C++/WinRT 工具链随后将为你生成源代码,如下所示。

void LogWrapLine(winrt::hstring const& str);

ToString()

C++/CX 类型提供 Object::ToString 方法。

int i{ 2 };
auto s{ i.ToString() }; // s is a Platform::String^ with value L"2".

C++/ WinRT 不直接提供此工具,不过可以转为使用替代方法。

int i{ 2 };
auto s{ std::to_wstring(i) }; // s is a std::wstring with value L"2".

C++/WinRT 也支持 winrt::to_hstring,但仅限数目有限的一些类型。 对于任何其他需要字符串化的类型,你需要添加重载。

Language 将整数字符串化 将枚举字符串化
C++/CX String^ result = "hello, " + intValue.ToString(); String^ result = "status: " + status.ToString();
C++/WinRT hstring result = L"hello, " + to_hstring(intValue); // must define overload (see below)
hstring result = L"status: " + to_hstring(status);

如果将枚举字符串化,则需提供 winrt::to_hstring 的实现。

namespace winrt
{
    hstring to_hstring(StatusEnum status)
    {
        switch (status)
        {
        case StatusEnum::Success: return L"Success";
        case StatusEnum::AccessDenied: return L"AccessDenied";
        case StatusEnum::DisabledByPolicy: return L"DisabledByPolicy";
        default: return to_hstring(static_cast<int>(status));
        }
    }
}

这些字符串化通常通过数据绑定来隐式使用。

<TextBlock>
You have <Run Text="{Binding FlowerCount}"/> flowers.
</TextBlock>
<TextBlock>
Most recent status is <Run Text="{x:Bind LatestOperation.Status}"/>.
</TextBlock>

这些绑定会对被绑定属性执行 winrt::to_hstring。 至于第二个示例 (StatusEnum),则必须提供你自己的 winrt::to_hstring 重载,否则会出现编译器错误。

字符串生成

C++/CX 和 C++/WinRT 按照标准的 std::wstringstream 来生成字符串。

操作 C++/CX C++/WinRT
追加字符串,保留 null stream.print(s->Data(), s->Length); stream << std::wstring_view{ s };
追加字符串,在遇到第一个 null 时停止 stream << s->Data(); stream << s.c_str();
提取结果 ws = stream.str(); ws = stream.str();

更多示例

在下面的示例中,ws 是一个类型为 std::wstring 的变量。 另外,虽然 C++/CX 可以根据 8 位字符串来构造 Platform::String,C++/WinRT 并不这样做。

操作 C++/CX C++/WinRT
根据文本构造字符串 String^ s = "hello";
String^ s = L"hello";
// winrt::hstring s{ "hello" }; // Doesn't compile
winrt::hstring s{ L"hello" };
std::wstring 进行转换,保留 null String^ s = ref new String(ws.c_str(),
  (uint32_t)ws.size());
winrt::hstring s{ ws };
s = winrt::hstring(ws);
// s = ws; // Doesn't compile
std::wstring 进行转换,在遇到第一个 null 时停止 String^ s = ref new String(ws.c_str()); winrt::hstring s{ ws.c_str() };
s = winrt::hstring(ws.c_str());
// s = ws.c_str(); // Doesn't compile
转换为 std::wstring,保留 null std::wstring ws{ s->Data(), s->Length };
ws = std::wstring(s>Data(), s->Length);
std::wstring ws{ s };
ws = s;
转换为 std::wstring,在遇到一个 null 时停止 std::wstring ws{ s->Data() };
ws = s->Data();
std::wstring ws{ s.c_str() };
ws = s.c_str();
将文本传递给方法 Method("hello");
Method(L"hello");
// Method("hello"); // Doesn't compile
Method(L"hello");
std::wstring 传递给方法 Method(ref new String(ws.c_str(),
  (uint32_t)ws.size()); // Stops on first null
Method(ws);
// param::winrt::hstring accepts std::wstring_view

重要的 API