使用 C++/WinRT 调用 API

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

重要

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

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

或者,从 空白应用(C++/WinRT)核心应用(C++/WinRT)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的投影类型。

小窍门

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

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

事实上,预测值是一个代理对象,它实质上只是指向后端对象的智能指针。 投影值的构造函数调用 RoActivateInstance 来创建对应的 Windows 运行时类的实例(本例中为Windows.Foundation.Uri),并将该对象的默认接口存储在新的投影值中。 通过智能指针,如下所示,对投影值成员的调用实际上被委托给支持对象,这就是状态更改发生的位置。

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

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

小窍门

投影类型是 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 接口。 但是,为方便起见,你可以按照作者的预期方式针对类进行编码。 例如,可以调用 UriToString 方法,就像在类中的方法一样(实际上,它是底层的单独 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++/WinRT) 项目。

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.
}

假设我们要构建一个不在盒子内的 礼品(一个使用未初始化 礼品盒构建的 礼品)。 首先,让我们看看 错误的 方法来做到这一点。 我们知道有一个 Gift 构造函数,它使用 GiftBox。 但是,如果我们想通过一个 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) };

你在这里得到的是一个未初始化的 礼品。 未初始化的 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)文件并生成。 在生成过程中, 工具会生成一个标准C++库,该库完全描述组件项目。 换句话说,生成的库包含组件的投影类型。

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

#include <winrt/ThermometerWRC.h>

struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    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 : Windows.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 : Windows.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 构造函数里,代码 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>() };
    }
}

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

struct MyPage : Page
{
    void DoWork()
    {
        // One option is to fully-qualify it.
        auto style{ Application::Current().Resources().
            Lookup(L"MyStyle").as<Windows::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 Windows::UI::Xaml namespace when you did
        // "using namespace Windows::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