目录
前言
Windows API 在WinRT中的投影
C++/WinRT的头文件(投影标头)
通过对象、接口或通过 ABI 访问成员
投影类型的初始化方法
不要错误地使用延迟初始化
不要错误地使用复制构造
使用 winrt::make 进行构造
标准构造方法
在WinRT组件中实现API
实例化和返回投影类型和接口
激活工厂(常用的创建对象的方法)
成员/类型多义性
如何创作自己的WinRT类
前言
本文主要介绍如何合理的使用 C++/WinRT API,如果你刚开始接触WinRT,建议先阅读第一篇。
C++/WinRT教程(第一篇)-CSDN博客
C++/WinRT教程(第二篇)基础类型的使用-CSDN博客
Windows API 在WinRT中的投影
Windows 命名空间中的每个类型,C++/WinRT 都定义了 C++ 友好等效项(称为投影类型 )。
投影类型具有与 Windows 类型相同的完全限定名称,但使用 C++ 语法放置于 C++ winrt 命名空间中。 例如,Windows::Foundation::Uri 投影到 winrt::Windows::Foundation::Uri 。
示例
// 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
包含 winrt::Windows::Foundation::Uri ,它是运行时类 Windows::Foundation::Uri 的投影类型。
如果希望使用来自 Windows 命名空间的类型,请包括与该命名空间对应的 C++/WinRT 标头。
在有了 C++/WinRT 投影类型值后,你可以将其视为实际 Windows 运行时类型的实例,因为它具有所有相同的成员。
事实上,该投影值是一个代理;它本质上只是支持对象的智能指针。 投影值的构造函数调用 RoActivateInstance 来创建 Windows 运行时支持类(本例中为 Windows.Foundation.Uri )的实例,并将该对象的默认接口存储在新投影值内。 如下所示,你对投影值的成员的调用实际上通过智能指针代理给支持对象;这是发生状态变化的地方。
简单来说就是虽然调用的是WinRT,但其实是WIndows原本API的代理,使用方法基本一致。
当 contosoUri
值超出范围时,它将自行销毁,并将其引用发布到默认接口。 如果该引用是对支持 Windows 运行时 Windows.Foundation.Uri 对象的最后一个引用,支持对象也会自行销毁。
注:投影类型 是出于使用运行时类的 API 针对运行时类的包装器。 投影接口 是针对 Windows 运行时接口的包装器。
C++/WinRT的头文件(投影标头)
可以使用来自 %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt
文件夹的标头。
%WindowsSdkDir%是你安装windowsSDK的路径。
每个 C++/WinRT 投影标头将自动包括其父命名空间标头文件;因此你不需要 明确包括它。(如果你想包含也行,不会报错)
自动包含只会自动包含声明,不包含实现,所以会报错
例如,如果你仅包括 winrt/Windows.Security.Cryptography.Certificates.h
,则会导致声明从这些命名空间(等等,间接)拉入。
- Windows.Foundation
- Windows.Foundation.Collections
- Windows.Networking
- Windows.Storage.Streams
- Windows.Security.Cryptography
换言之,某些 API 在你包括的标头中进行了前向声明。 但它们的定义在你尚未包括的标头中。 因此,如果你随后调用 Windows::Foundation::Uri::RawUri ,那么你会收到指示成员未定义的链接器错误。 解决方法是明确 #include <winrt/Windows.Foundation.h>
。 一般情况下,当你看到此类链接器错误时,应包括为该 API 的命名空间命名的标头,并重新生成。
通过对象、接口或通过 ABI 访问成员
使用 C++/WinRT,类的方法可以直接调用底层接口的方法
例如,你可以调用 Uri 的 ToString 方法,就像它是该类的方法(实际上,再深入一层,它是单独的 IStringable 接口上的方法)。
Uri contosoUri{ L"http://www.contoso.com" };
WINRT_ASSERT(contosoUri.ToString() == L"http://www.contoso.com/"); // QueryInterface is called at this point.
你可以通过自己检索 IStringable 接口并直接使用它来放弃一点便利,从而提高一点性能。
在下方的代码示例中,你在运行时获取实际的 IStringable 接口指针(通过一次性查询)。
...
IStringable stringable = contosoUri; // One-off QueryInterface.
WINRT_ASSERT(stringable.ToString() == L"http://www.contoso.com/");
这种方式可以优化代码,避免每次调用 QueryInterface。
如果你希望访问 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 与 ABI 之间的互操作中还提供了更多详细信息和代码示例。
投影类型的初始化方法
在 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 构造函数本质上不执行任何操作。 它预期投影对象会在后续时间初始化。 因此,不论运行时类是否具有默认的构造函数,你都可以使用此技巧实现有效的延迟初始化。
注:非必要谨慎使用此方法,使用此方法一定要记得初始化。
当C++ 集合类型使用默认构造函数时,可能导致意外的对象构造。
例如
std::map<int, TextBlock> lookup;
lookup[2] = value;
每次进行分配会创建新 TextBlock ,然后立即使用 value
覆盖它。 下面是补救措施。
std::map<int, TextBlock> lookup;
lookup.insert_or_assign(2, value);
另请参阅默认构造函数如何影响集合。
不要错误地使用延迟初始化
编译器的冲突解决偏向于它而非工厂构造函数。 例如:
// GiftBox.idl
runtimeclass GiftBox
{
GiftBox();
}
// Gift.idl
runtimeclass Gift
{
Gift(GiftBox giftBox); // You can create a gift inside a box.
}
假设我们想要构造一个不在盒子内的 Gift(使用未初始化的 GiftBox 构造的 Gift)。 首先,让我们看看错误 的做法。 我们知道有一个接受 GiftBox 的 Gift 构造函数。 但是,如果想要传递 null GiftBox(如下所示),则不会获得我们想要的结果 。
// 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 得到 Gift。 下面是正确 的做法。
// 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) };
使用 winrt::make 进行构造
让我们从默认方法(C++/WinRT 版本 1.0)开始,因为最好至少要熟悉该模式。 通过其 std::nullptr_t 构造函数构造投影类型。 该构造函数不执行任何初始化,所以你接下来必须通过 winrt::make 帮助程序函数向该实例分配一个值,同时传递任何必要的构造函数参数。 在使用代码的同一项目中实现的运行时类无需进行注册,且无需通过 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
中的 MainPage 构造函数中,无需使用代码 m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();
。
有关统一构造的详细信息,请参阅选择加入统一构造和直接实现访问。
在WinRT组件中实现API
无论你是自行创作该组件,还是该组件来自供应商,本部分均适用。
在应用程序项目中,引用 Windows 运行时组件的 Windows 运行时元数据 (.winmd
) 文件,然后生成。 在生成过程中,cppwinrt.exe
工具生成标准 C++ 库,该库全面描述(或投影)该组件的 API 接口。 换言之,生成的库包含该组件的投影类型。
与 Windows 命名空间类型一样,你只需包含标头并通过其构造函数之一构造投影类型。 应用程序项目的启动代码注册运行时类,然后投影类型的构造函数调用 RoActivateInstance 来激活引用组件中的运行时类。
#include <winrt/ThermometerWRC.h>
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
ThermometerWRC::Thermometer thermometer;
...
};
有关更多详细信息、代码以及使用在 Windows 运行时组件实现的 API 的演练,请参阅使用 C++/WinRT 创建 Windows 运行时组件和使用 C++/WinRT 创作事件。
实例化和返回投影类型和接口
投影类型(如此示例中的一种投影类型)是工具生成的,而不是你自己创作的内容。
struct MyRuntimeClass : MyProject::IMyRuntimeClass, impl::require<MyRuntimeClass,
Windows::Foundation::IStringable, Windows::Foundation::IClosable>
MyRuntimeClass 是投影类型;投影接口包括 IMyRuntimeClass 、IStringable 和 IClosable 。 本主题已展示你可用来实例化投影类型的不同方法。 以下是提醒和总结,这里使用 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 以查询其他也可使用或返回到调用方的投影接口。 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 命名空间的类型。 在接下来的示例中,ThermometerWRC::Thermometer 是在 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
一词后面跟有 ::
,因此系统会忽略方法名称,编译器会查找枚举类。
如何创作自己的WinRT类
这个一般情况下新手用不到,大佬们请移步官方文档
使用 C++/WinRT 创作 API - UWP applications | Microsoft Learn