使用 C++/WinRT 调用 API

本主题演示如何使用 C++/WinRT API,无论是属于Windows、由第三方组件供应商实现还是由自己实现。

Important

因此,本主题中的代码示例简短且易于试用,可以通过创建新的Windows控制台应用程序(C++/WinRT)项目和复制粘贴代码来重现它们。 但是,你不能以那种方式在未打包应用中使用任意自定义(第三方)Windows 运行时 类型。 你只能通过那种方式使用 Windows 类型。

若要在控制台应用中使用自定义(第三方)Windows 运行时 类型,需要为该应用赋予包标识,以便它能够解析所使用的自定义类型的注册信息。 有关详细信息,请参阅Windows应用程序打包项目

或者,也可以使用适用于 C++ 的 空白应用,已打包(桌面中的 WinUI 3)Windows 运行时组件(C++/WinRT)项目模板创建新项目。 这些应用类型已有 包标识

如果 API 位于Windows命名空间中

这是使用 Windows 运行时 API 的最常见情况。 对于元数据中定义的Windows命名空间中的每个类型,C++/WinRT 定义了一个 C++友好的等效项(称为投影类型)。 投影类型与 Windows 类型具有相同的完全限定名,但它以 C++ 语法形式位于 C++ winrt 命名空间中。 例如,Windows::Foundation::Uri 投影到 C++/WinRT 中,用作 winrt::Windows::Foundation::Uri

下面是一个简单的代码示例。 如果要将以下代码示例直接复制粘贴到Windows控制台应用程序(C++/WinRT)项目的主源代码文件中,请先在项目属性中设置“不使用预编译标头”。

// main.cpp
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;

int main()
{
    winrt::init_apartment();
    Uri contosoUri{ L"http://www.contoso.com" };
    Uri combinedUri = contosoUri.CombineUri(L"products");
}

包含的标头 winrt/Windows.Foundation.h 是 SDK 的一部分,位于文件夹 %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt\内。 该文件夹中的头文件包含投射到 C++/WinRT 的 Windows 命名空间类型。 在此示例中,winrt/Windows.Foundation.h包含 winrt::Windows::Foundation::Uri,它是运行时类的投影类型Windows::Foundation::Uri

Tip

每当想要从Windows命名空间中使用类型时,请包含对应于该命名空间的 C++/WinRT 标头。 指令 using namespace 是可选的,但很方便。

在上面的代码示例中,初始化 C++/WinRT 后,我们将堆栈分配 winrt::Windows::Foundation::Uri 投影类型的值,通过其中一个公开记录的构造函数(Uri(String)在此示例中)。 对于这种最常见的用例,通常你只需要做到这些。 获得 C++/WinRT 投影类型值后,可以将其视为实际Windows 运行时类型的实例,因为它具有相同的成员。

事实上,那个投影值是一个代理对象;它本质上只是一个指向后备对象的智能指针。 投影值的一个或多个构造函数调用 RoActivateInstance 来创建基础 Windows 运行时 类的实例(在本例中为 Windows.Foundation.Uri),并将该对象的默认接口存储在新的投影值中。 如下所示,对投影值成员的调用实际上会通过智能指针委托给后备对象;状态更改也正是在这里发生的。

投影的 Windows::Foundation::Uri 类型

contosoUri 值超出作用域时,它会被析构,并释放对默认接口的引用。 如果该引用是对基础 Windows 运行时 Windows.Foundation.Uri 对象的最后一个引用,则该基础对象也会被析构。

Tip

投影类型是针对 Windows 运行时 类型的包装器,用于调用其 API。 例如,投射接口是一个 Windows 运行时 接口的包装器。

C++/WinRT 投影标头

若要在 C++/WinRT 中使用 Windows 命名空间 API,请包含 %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt 文件夹中的头文件。 必须包含与所使用的每个命名空间对应的标头。

例如,对于 Windows::Security::Cryptography::Certificates 命名空间,等效的 C++/WinRT 类型定义驻留在 winrt/Windows.Security.Cryptography.Certificates.h。 包括该标头可让你访问 Windows::Security::Cryptography::Certificates 命名空间中的所有类型。

有时,一个命名空间标头将包含相关命名空间标头的一部分,但不应依赖于此实现详细信息。 显式包含你使用的命名空间的标头。

例如,Certificate::GetCertificateBlob 方法返回 Windows::Storage::Streams::IBuffer 接口。 在调用 Certificate::GetCertificateBlob 方法之前,必须包含winrt/Windows.Storage.Streams.h命名空间头文件,以确保可以接收和操作返回的 Windows::Storage::Streams::IBuffer

忘记在使用该命名空间中的类型之前包含所需的命名空间标头是生成错误的常见源。

通过对象、接口或 ABI 访问成员

使用 C++/WinRT 投影时,Windows 运行时类的运行时表示形式不超过基础 ABI 接口。 但是,为方便起见,你可以按照作者的预期方式针对类进行编码。 例如,可以调用 ToString 方法,就像它是 Uri 类的方法一样(实际上在底层,它是单独的 IStringable 接口上的方法)。

WINRT_ASSERT 是宏定义,它扩展到 _ASSERTE

Uri contosoUri{ L"http://www.contoso.com" };
WINRT_ASSERT(contosoUri.ToString() == L"http://www.contoso.com/"); // QueryInterface is called at this point.

通过查询适当的接口来实现这种便利。 但你始终掌握着控制权。 你可以选择牺牲一点这种便利性,方法是自行获取 IStringable 接口并直接使用它,以换取一点性能提升。 在下面的代码示例中,可以在运行时(通过一次性查询)获取实际的 IStringable 接口指针。 之后,对 ToString 的调用是直接的,并避免对 QueryInterface 的任何进一步调用。

...
IStringable stringable = contosoUri; // One-off QueryInterface.
WINRT_ASSERT(stringable.ToString() == L"http://www.contoso.com/");

如果你知道要在同一接口上调用多个方法,则可以选择此技术。

顺便说一句,如果你确实想访问 ABI 级别的成员,则可以。 下面的代码示例演示了如何实现这一点,更多详细信息和代码示例请参阅 C++/WinRT 与 ABI 之间的互操作

#include <Windows.Foundation.h>
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
using namespace winrt::Windows::Foundation;

int main()
{
    winrt::init_apartment();
    Uri contosoUri{ L"http://www.contoso.com" };

    int port{ contosoUri.Port() }; // Access the Port "property" accessor via C++/WinRT.

    winrt::com_ptr<ABI::Windows::Foundation::IUriRuntimeClass> abiUri{
        contosoUri.as<ABI::Windows::Foundation::IUriRuntimeClass>() };
    HRESULT hr = abiUri->get_Port(&port); // Access the get_Port ABI function.
}

延迟初始化

在 C++/WinRT 中,每个投影类型都有一个特殊的 C++/WinRT std::nullptr_t 构造函数。 除了该构造函数之外,所有投影类型的构造函数(包括默认构造函数)都会创建一个底层的 Windows 运行时 对象,并向你返回一个指向该对象的智能指针。 因此,该规则适用于使用默认构造函数的任何位置,例如未初始化的局部变量、未初始化的全局变量和未初始化的成员变量。

另一方面,如果想要构造投影类型的变量,而无需再构造一个后盾Windows 运行时对象(以便可以延迟该工作直到以后),则可以执行此操作。 使用这个特殊的 C++/WinRT std::nullptr_t 构造函数来声明变量或字段(C++/WinRT 投影会将其注入每个运行时类)。 我们在下面的代码示例中使用具有 m_gamerPicBuffer 的特殊构造函数。

#include <winrt/Windows.Storage.Streams.h>
using namespace winrt::Windows::Storage::Streams;

#define MAX_IMAGE_SIZE 1024

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

private:
    Buffer m_gamerPicBuffer{ nullptr };
};

int main()
{
    winrt::init_apartment();
    Sample s;
    // ...
    s.DelayedInit();
}

投影类型上的所有构造函数(除了std::nullptr_t 构造函数)都会导致创建一个后备 Windows 运行时 对象。 std::nullptr_t 构造函数本质上是一个 no-op。 它要求在后续时间初始化投影对象。 因此,无论运行时类是否具有默认构造函数,都可以使用此技术高效延迟初始化。

此注意事项会影响调用默认构造函数的其他位置,例如在矢量和映射中。 请参考以下代码示例,为此你需要一个用于 C++ 的空白应用(已打包,桌面版 WinUI 3)项目。

std::map<int, TextBlock> lookup;
lookup[2] = value;

该赋值会创建一个新的 TextBlock,然后立即用 value 覆盖它。 下面是补救措施。

std::map<int, TextBlock> lookup;
lookup.insert_or_assign(2, value);

另请参阅 默认构造函数如何影响集合

不要错误地延迟初始化

请注意,不要误调用 std::nullptr_t 构造函数。 编译器的冲突解决优先于工厂构造函数。 例如,请考虑这两个运行时类定义。

// GiftBox.idl
runtimeclass GiftBox
{
    GiftBox();
}

// Gift.idl
runtimeclass Gift
{
    Gift(GiftBox giftBox); // You can create a gift inside a box.
}

假设我们要构建一个不在盒子内的礼品(一个用未初始化的 GiftBox 构造的礼物)。 首先,让我们来看看错误的做法。 我们知道,有一个接受GiftBox作为参数的Gift构造函数。 但是,如果我们想要传递一个 null GiftBox(通过统一初始化调用 Gift 构造函数,就像我们下面所做的那样),那么就得不到我们想要的结果。

// These are *not* what you intended. Doing it in one of these two ways
// actually *doesn't* create the intended backing Windows Runtime Gift object;
// only an empty smart pointer.

Gift gift{ nullptr };
auto gift{ Gift(nullptr) };

你在这里得到的是一个未初始化的 Gift。 你没有收到带有未初始化的 GiftBox礼物。 下面是执行此操作 的正确 方法。

// Doing it in one of these two ways creates an initialized
// Gift with an uninitialized GiftBox.

Gift gift{ GiftBox{ nullptr } };
auto gift{ Gift(GiftBox{ nullptr }) };

在该错误示例中,传递 nullptr 字面量时,会优先解析为延迟初始化构造函数。 为了支持工厂构造函数,参数的类型必须是 GiftBox。 仍可以选择传递显式延迟初始化 GiftBox,如正确的示例所示。

下一个示例 也是 正确的,因为参数的类型为 GiftBox,而不是 std::nullptr_t

GiftBox giftBox{ nullptr };
Gift gift{ giftBox }; // Calls factory constructor.

仅当你传递一个 nullptr 文本时,才会出现歧义。

不要错误地复制构造。

这一警告与上文 “不要误进行延迟初始化” 一节中所述的警告类似。

除了延迟初始化构造函数之外,C++/WinRT 投影还会向每个运行时类注入一个复制构造函数。 它是一个单参数构造函数,接受与所构造的对象相同的类型。 生成的智能指针与其构造函数参数一样,指向同一个底层 Windows 运行时 对象。 结果是两个智能指针对象指向同一个底层对象。

下面是将在代码示例中使用的运行时类定义。

// GiftBox.idl
runtimeclass GiftBox
{
    GiftBox(GiftBox biggerBox); // You can place a box inside a bigger box.
}

假设我们想要在一个更大的 GiftBox 内构造一个 GiftBox

GiftBox bigBox{ ... };

// These are *not* what you intended. Doing it in one of these two ways
// copies bigBox's backing-object-pointer into smallBox.
// The result is that smallBox == bigBox.

GiftBox smallBox{ bigBox };
auto smallBox{ GiftBox(bigBox) };

执行此操作 的正确 方法是显式调用激活工厂。

GiftBox bigBox{ ... };

// These two ways call the activation factory explicitly.

GiftBox smallBox{
    winrt::get_activation_factory<GiftBox, IGiftBoxFactory>().CreateInstance(bigBox) };
auto smallBox{
    winrt::get_activation_factory<GiftBox, IGiftBoxFactory>().CreateInstance(bigBox) };

如果 API 是在 Windows 运行时 组件中实现的

无论该组件是由你自己编写的,还是来自供应商,本节都适用。

注释

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

在应用程序项目中,引用 Windows 运行时 组件的 Windows 运行时 元数据 (.winmd) 文件,然后进行生成。 在生成过程中, cppwinrt.exe 该工具生成一个标准 C++ 库,该库完全描述了组件的 API 图面(或 项目)。 换句话说,生成的库包含组件的投影类型。

然后,与Windows命名空间类型一样,可以通过其中一个构造函数包含标头并构造投影类型。 应用程序项目中的启动代码会注册运行时类,而投影类型的构造函数则会调用 RoActivateInstance 来激活引用的组件中的运行时类。

#include <winrt/ThermometerWRC.h>

struct App : AppT<App>
{
    ThermometerWRC::Thermometer thermometer;
    ...
};

有关如何使用在 Windows 运行时 组件中实现的 API 的更多信息、代码和演练,请参阅 使用 C++/WinRT 的 Windows 运行时 组件在 C++/WinRT 中编写事件

如果 API 是在使用项目中实现的

本节中的代码示例取自主题 XAML 控件;绑定到 C++/WinRT 属性。 有关更多详细信息、代码和使用运行时类的演练,请参阅该主题,该类是在使用该运行时类的同一项目中实现的。

从 XAML UI 中使用的类型必须是运行时类,即使它与 XAML 位于同一项目中也是如此。 在这种情况下,你将从运行时类的 Windows 运行时元数据(.winmd)生成投影类型。 同样,可以包含标头,但可以在 C++/WinRT 版本 1.0 或版本 2.0 方法之间选择构造运行时类的实例。 版本 1.0 方法使用 winrt::make;版本 2.0 方法称为 统一构造。 让我们逐一来看。

使用 winrt::make 进行构造

让我们从默认的 (C++/WinRT 版本 1.0) 方法开始,因为至少熟悉该模式是个好主意。 可以通过其 std::nullptr_t 构造函数构造投影类型。 该构造函数不执行任何初始化,因此接下来必须通过 winrt::make helper 函数向实例分配一个值,并传递任何必需的构造函数参数。 在与使用该类的代码相同的项目中实现的运行时类,无需注册,也无需通过 Windows 运行时/COM 激活机制实例化。

有关完整演练,请参阅 XAML 控件;绑定到 C++/WinRT 属性。 本节展示了该操作演练中的摘录。

// MainPage.idl
import "BookstoreViewModel.idl";
namespace Bookstore
{
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        BookstoreViewModel MainViewModel{ get; };
    }
}

// MainPage.h
...
struct MainPage : MainPageT<MainPage>
{
    ...
    private:
        Bookstore::BookstoreViewModel m_mainViewModel{ nullptr };
};
...

// MainPage.cpp
...
#include "BookstoreViewModel.h"

MainPage::MainPage()
{
    m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();
    ...
}

统一结构

使用 C++/WinRT 版本 2.0 及更高版本,有一种优化的构造形式可供你称为 统一构造 (请参阅 C++/WinRT 2.0 中的新闻和更改)。

有关完整演练,请参阅XAML 控件;绑定到 C++/WinRT 属性。 本部分展示了该演练指南中的摘录。

若要使用统一构造而不是 winrt::make,需要激活工厂。 一个不错的生成方式是在 IDL 中添加构造函数。

// MainPage.idl
import "BookstoreViewModel.idl";
namespace Bookstore
{
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
        BookstoreViewModel MainViewModel{ get; };
    }
}

然后,在仅一个步骤中 MainPage.h 声明和初始化 m_mainViewModel ,如下所示。

// MainPage.h
...
struct MainPage : MainPageT<MainPage>
{
    ...
    private:
        Bookstore::BookstoreViewModel m_mainViewModel;
        ...
    };
}
...

然后,在 MainPage.cpp 中的 MainPage 构造函数里,不需要 m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>(); 这段代码。

有关统一构造和代码示例的详细信息,请参阅 “选择加入统一构造”和直接实现访问

实例化并返回投影类型和接口

下面是使用方项目中投影类型和接口可能如下所示的一个示例。 请记住,投影类型(如此示例中的类型)是工具生成的,不是你自己创作的内容。

struct MyRuntimeClass : MyProject::IMyRuntimeClass, impl::require<MyRuntimeClass,
    Windows::Foundation::IStringable, Windows::Foundation::IClosable>

MyRuntimeClass 是投影类型;投影接口包括 IMyRuntimeClassIStringableIClosable。 本主题演示了可以实例化投影类型的不同方式。 下面是使用 MyRuntimeClass 作为示例的提醒和摘要。

// The runtime class is implemented in another compilation unit (it's either a Windows API,
// or it's implemented in a second- or third-party component).
MyProject::MyRuntimeClass myrc1;

// The runtime class is implemented in the same compilation unit.
MyProject::MyRuntimeClass myrc2{ nullptr };
myrc2 = winrt::make<MyProject::implementation::MyRuntimeClass>();
  • 可以访问投影类型的所有接口的成员。
  • 可以将投影类型返回给调用方。
  • 投影类型和接口派生自 winrt::Windows::Foundation::IUnknown。 因此,你可以在投影类型或接口上调用 IUnknown::as 来查询其他投影接口,这些接口也可以供你使用,或返回给调用方。 作为成员函数的工作方式类似于 QueryInterface
void f(MyProject::MyRuntimeClass const& myrc)
{
    myrc.ToString();
    myrc.Close();
    IClosable iclosable = myrc.as<IClosable>();
    iclosable.Close();
}

激活工厂

创建 C++/WinRT 对象的便捷直接方法如下所示。

using namespace winrt::Windows::Globalization::NumberFormatting;
...
CurrencyFormatter currency{ L"USD" };

但有时可能需要自行创建激活工厂,然后在方便时从中创建对象。 下面是一些示例,演示如何使用 winrt::get_activation_factory 函数模板。

using namespace winrt::Windows::Globalization::NumberFormatting;
...
auto factory = winrt::get_activation_factory<CurrencyFormatter, ICurrencyFormatterFactory>();
CurrencyFormatter currency = factory.CreateCurrencyFormatterCode(L"USD");
using namespace winrt::Windows::Foundation;
...
auto factory = winrt::get_activation_factory<Uri, IUriRuntimeClassFactory>();
Uri uri = factory.CreateUri(L"http://www.contoso.com");

上述两个示例中的类是来自Windows命名空间的类型。 在下一个示例中,温度计WRC::温度计是在Windows 运行时组件中实现的自定义类型。

auto factory = winrt::get_activation_factory<ThermometerWRC::Thermometer>();
ThermometerWRC::Thermometer thermometer = factory.ActivateInstance<ThermometerWRC::Thermometer>();

成员/类型歧义

当成员函数与类型同名时,存在歧义。 C++ 中成员函数的非限定名称查找规则会导致其先搜索类,再搜索命名空间。 替换失败不是错误(SFINAE)规则不适用(它在函数模板的重载解析期间适用)。 因此,如果类内的名称没有意义,则编译器不会继续寻找更好的匹配项,它只是报告错误。

struct MyPage : Page
{
    void DoWork()
    {
        // This doesn't compile. You get the error
        // "'winrt::Windows::Foundation::IUnknown::as':
        // no matching overloaded function found".
        auto style{ Application::Current().Resources().
            Lookup(L"MyStyle").as<Style>() };
    }
}

在上面的代码中,编译器认为你传递给 IUnknown::as 的模板参数是 FrameworkElement.Style()(它在 C++/WinRT 中是一个成员函数)。 解决方案是强制将名称Style解释为类型Microsoft::UI::Xaml::Style

struct MyPage : Page
{
    void DoWork()
    {
        // One option is to fully-qualify it.
        auto style{ Application::Current().Resources().
            Lookup(L"MyStyle").as<Microsoft::UI::Xaml::Style>() };

        // Another is to force it to be interpreted as a struct name.
        auto style{ Application::Current().Resources().
            Lookup(L"MyStyle").as<struct Style>() };

        // If you have "using namespace Windows::UI;", then this is sufficient.
        auto style{ Application::Current().Resources().
            Lookup(L"MyStyle").as<Xaml::Style>() };

        // Or you can force it to be resolved in the global namespace (into which
        // you imported the Microsoft::UI::Xaml namespace when you did
        // "using namespace Microsoft::UI::Xaml;".
        auto style = Application::Current().Resources().
            Lookup(L"MyStyle").as<::Style>();
    }
}

非限定名称查找有一种特殊情况:当名称后跟有 :: 时,它会忽略函数、变量和枚举值。 这样,就可以执行此类操作。

struct MyPage : Page
{
    void DoSomething()
    {
        Visibility(Visibility::Collapsed); // No ambiguity here (special exception).
    }
}

Visibility() 的调用将解析为 UIElement.Visibility 成员函数名称。 但参数 Visibility::Collapsed 紧跟在单词 Visibility 之后,并带有 ::,因此方法名会被忽略,编译器会找到该枚举类。

重要 API