本主题演示如何使用 winrt::实现基本结构,直接或间接创作 C++/WinRT API。 在此语境中,创作的近义词是 生成 或 实现。 本主题介绍按此顺序在 C++/WinRT 类型上实现 API 的以下方案。
注释
本主题会涉及 Windows 运行时 组件,但仅限于 C++/WinRT 的上下文。 如果要查找涵盖所有Windows 运行时语言的Windows 运行时组件的内容,请参阅Windows 运行时组件。
- 你未创作Windows 运行时类(运行时类);你只想实现一个或多个Windows 运行时接口,以便在应用中进行本地使用。 在本例中,可以直接从 winrt::implements 派生,并实现函数。
- 你正在编写运行时类。 你可能正在创作要从应用使用的组件。 或者,你可能正在创作要从 XAML 用户界面(UI)中使用的类型,在这种情况下,你要在同一编译单元中实现和使用运行时类。 在这些情况下,你可以让工具为你生成派生自 winrt::implements 的类。
在这两种情况下,实现 C++/WinRT API 的类型称为 实现类型。
Important
重要的是区分实现类型和投影类型这两个概念。 投影类型已在 Consume APIs with C++/WinRT 中进行了说明。
如果你不是在编写运行时类
最简单的情况是,你的类型实现了 Windows 运行时 接口,并且你会在同一应用中使用该类型。 在这种情况下,类型不需要是运行时类;只是一个普通的 C++ 类。 例如,你可能基于 Microsoft::UI::Xaml::Application 编写 WinUI 3 桌面应用。
如果你的类型被 XAML UI 引用,那么它确实就必须是运行时类,即使它与 XAML 在同一个项目中。 对于这种情况,请参阅“ 如果要在 XAML UI 中编写要引用的运行时类”部分。
注释
有关安装和使用 C++/WinRT Visual Studio 扩展(VSIX)和 NuGet 包(一起提供项目模板和生成支持)的信息,请参阅Visual Studio对 C++/WinRT 的支持。
在 Visual Studio 中,用于 C++ 的 Blank App, Packaged (WinUI 3 in Desktop) 项目模板说明了 WinUI 3 应用程序模式。 应用类派生自 Microsoft::UI::Xaml::Application,入口点调用其 Start 方法。
#include "App.xaml.h"
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
winrt::init_apartment();
::winrt::Microsoft::UI::Xaml::Application::Start(
[](auto&&) { ::winrt::make<App>(); });
}
应用类创建一个 Microsoft::UI::Xaml::Window 并在 OnLaunched 中激活它。
// App.xaml.h
struct App : AppT<App>
{
App();
void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);
private:
winrt::Microsoft::UI::Xaml::Window window{ nullptr };
};
// App.xaml.cpp
void App::OnLaunched(LaunchActivatedEventArgs const&)
{
window = make<MainWindow>();
window.Activate();
}
C++/WinRT 具有基本结构模板 winrt::implements ,以便轻松实现接口(或多个接口),而无需使用 COM 样式编程。 只需从 实现派生类型,然后实现接口的函数。 下面是实现自定义接口的示例。
struct MyType : implements<MyType, IStringable>
{
hstring ToString()
{
return L"MyType";
}
};
如果要在 Windows 运行时 组件中编写运行时类
如果你的类型封装在 Windows 运行时 组件中,供另一个二进制文件使用(该二进制文件通常是某个应用程序),那么你的类型就需要是运行时类。 在Microsoft接口定义语言(IDL)(.idl)文件中声明运行时类(请参阅将运行时类分解为 Midl 文件 (.idl))。
每个 IDL 文件都会生成一个.winmd文件,Visual Studio将所有 IDL 文件合并到与根命名空间同名的单个文件中。 最终 .winmd 文件将是组件使用者将引用的文件。
下面是在 IDL 文件中声明运行时类的示例。
// MyRuntimeClass.idl
namespace MyProject
{
runtimeclass MyRuntimeClass
{
// Declaring a constructor (or constructors) in the IDL causes the runtime class to be
// activatable from outside the compilation unit.
MyRuntimeClass();
String Name;
}
}
此 IDL 声明一个 Windows 运行时类。 运行时类是一种类型,可通过新式 COM 接口激活和使用,通常跨可执行边界。 将 IDL 文件添加到项目并生成时,C++/WinRT 工具链(midl.exe 和 cppwinrt.exe)会为你生成实现类型。 有关 IDL 文件工作流实际运作方式的示例,请参阅 XAML 控件;绑定到 C++/WinRT 属性。
使用上面的 IDL 示例,实现类型是在名为 \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h 和 MyRuntimeClass.cpp 的源代码文件中的一个名为 winrt::MyProject::implementation::MyRuntimeClass 的 C++ 结构体存根。
实现类型如下所示。
// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
{
MyRuntimeClass() = default;
winrt::hstring Name();
void Name(winrt::hstring const& value);
};
}
// winrt::MyProject::factory_implementation::MyRuntimeClass is here, too.
请注意,这里使用的是 F 绑定多态模式(MyRuntimeClass 将自身作为传递给其基类 MyRuntimeClassT 的模板参数)。 这也称为奇怪的重复模板模式(CRTP)。 如果向上跟踪继承链,你将遇到 MyRuntimeClass_base。
可以使用Windows实现库(WIL)简化简单属性的实现。 操作方法如下:
// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
{
MyRuntimeClass() = default;
wil::single_threaded_rw_property<winrt::hstring> Name;
};
}
请参阅 简单属性。
template <typename D, typename... I>
struct MyRuntimeClass_base : implements<D, MyProject::IMyRuntimeClass, I...>
因此,在这种情况下,继承层次结构最根部再次是 winrt::implements 基结构模板。
有关在 Windows 运行时 组件中编写 API 的更多详细信息、代码和演练,请参阅 使用 C++/WinRT 编写 Windows 运行时 组件 和 在 C++/WinRT 中创作事件。
如果你正在编写一个供 XAML UI 引用的运行时类
如果类型由 XAML UI 引用,则它必须是运行时类,即使它与 XAML 位于同一项目中。 尽管它们通常跨可执行边界激活,但运行时类可以在实现它的编译单元中使用。
在这种情况下,你既要编写这些 API 又要使用这些 API。 实现运行时类的过程实质上与Windows 运行时组件的过程相同。 那么,请参阅上一节——如果要在 Windows 运行时 组件中编写运行时类。 唯一不同的细节是,与 IDL 不同,C++/WinRT 工具链不仅生成实现类型,而且还生成投影类型。 请务必注意,在此方案中仅说“MyRuntimeClass”可能模棱两可:有多个实体具有该名称,类型不同。
- MyRuntimeClass 是运行时类的名称。 但这是一个抽象:在 IDL 中声明,并在一些编程语言中实现。
-
MyRuntimeClass 是 C++ 结构 winrt::MyProject::implementation::MyRuntimeClass 的名称,它是运行时类的 C++/WinRT 实现。 正如我们所看到的,如果有单独的实现和使用项目,则此结构仅存在于实现项目中。 这是 实现类型或 实现。 此类型由
cppwinrt.exe工具在\MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h和MyRuntimeClass.cpp文件中生成。 -
MyRuntimeClass 是以 C++ 结构 winrt::MyProject::MyRuntimeClass 的形式投影类型的名称。 如果存在单独的实现和使用项目,则此结构仅存在于使用项目中。 这是 投影类型或 投影。 此类型由
cppwinrt.exe生成于文件\MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h中。
下面是与本主题相关的投影类型的部分。
// MyProject.2.h
...
namespace winrt::MyProject
{
struct MyRuntimeClass : MyProject::IMyRuntimeClass
{
MyRuntimeClass(std::nullptr_t) noexcept {}
MyRuntimeClass();
};
}
有关在运行时类上实现 INotifyPropertyChanged 接口的示例演练,请参阅 XAML 控件;绑定到 C++/WinRT 属性。
在这种情况下,使用你的运行时类的过程已在 Consume APIs with C++/WinRT 中说明。
将运行时类提取到 MIDL 文件(.idl)中
Visual Studio项目和项模板为每个运行时类生成单独的 IDL 文件。 这使得 IDL 文件及其生成的源代码文件之间形成了逻辑上的对应关系。
但是,如果将项目的所有运行时类合并到单个 IDL 文件中,则可以显著缩短生成时间。 否则,如果它们之间具有复杂的(或循环) import 依赖项,则实际上可能需要合并。 如果把这些运行时类放在一起,你可能会发现编写和审查它们更容易。
运行时类构造函数
以下是我们从上述列表中可以总结出的一些要点。
- 在 IDL 中声明的每个构造函数都会导致在实现类型和投影类型上生成构造函数。 IDL 中声明的构造函数用于从 不同的 编译单元中使用运行时类。
- 无论你是否有在 IDL 中声明的一个或多个构造函数,系统都会为你的投影类型生成一个接受 std::nullptr_t 的构造函数重载。 调用 std::nullptr_t 构造函数,是从同一编译单元中使用运行时类的两个步骤中的第一步。 有关更多详细信息和代码示例,请参阅 使用 C++/WinRT 的 API。
- 如果你在 同一 编译单元中使用该运行时类,那么也可以直接在实现类型上实现非默认构造函数(请记住,它位于
MyRuntimeClass.h中)。
注释
如果希望从不同的编译单元(很常见)使用运行时类,请在 IDL 中包括构造函数(至少是默认构造函数)。 这样做,你还会获得一个与你的实现类型配套的工厂实现。
如果只想在同一编译单元中创作和使用运行时类,请不要在 IDL 中声明任何构造函数。 不需要工厂实现,也不会生成工厂实现。 将删除实现类型的默认构造函数,但你可以轻松编辑它并改为默认它。
如果只想在同一编译单元中创作和使用运行时类,并且需要构造函数参数,则直接在实现类型上创作所需的构造函数(s)。
运行时类方法、属性和事件
我们已经看到,工作流是使用 IDL 声明运行时类及其成员,然后工具会为你生成原型和存根实现。 至于为运行时类成员自动生成的原型,你可以对其进行编辑,以便它们传递与你在 IDL 中声明的类型不同的类型。 但是,只要在 IDL 中声明的类型可以转发到在实现版本中声明的类型,就可以执行此操作。
下面是一些示例。
- 可以放宽参数类型。 例如,如果在 IDL 中,方法采用 SomeClass,则可以选择在实现中将它更改为 IInspectable 。 之所以可行,是因为任何 SomeClass 都可以转换为 IInspectable(当然,反过来则不行)。
- 可以按值而不是按引用接受可复制参数。 例如,将
SomeClass const&更改为SomeClass。 当你需要避免将引用捕获到协程中时,这就是必要的(请参阅 参数传递)。 - 可以放宽对返回值的限制。 例如,可以将 void 更改为 winrt::fire_and_forget。
编写异步事件处理程序时,最后两个非常有用。
实例化并返回实现类型和接口
在本部分中,我们以名为 MyType 的实现类型为例,该类型实现 IStringable 和 IClosable 接口。
可以直接从 winrt::implements(它不是运行时类)派生 MyType。
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
struct MyType : implements<MyType, IStringable, IClosable>
{
winrt::hstring ToString(){ ... }
void Close(){}
};
或者,可以从 IDL 生成它(它是运行时类)。
// MyType.idl
namespace MyProject
{
runtimeclass MyType: Windows.Foundation.IStringable, Windows.Foundation.IClosable
{
MyType();
}
}
无法直接分配实现类型。
MyType myimpl; // error C2259: 'MyType': cannot instantiate abstract class
但是,你可以通过调用 winrt::make 函数模板,将 MyType 转换为可作为投影的一部分使用或返回的 IStringable 或 IClosable 对象。 make 返回实现类型的默认接口。
IStringable istringable = winrt::make<MyType>();
注释
但是,如果要从 XAML UI 引用类型,则同一项目中将同时存在实现类型和投影类型。 在这种情况下, make 返回投影类型的实例。 有关该方案的代码示例,请参阅 XAML 控件;绑定到 C++/WinRT 属性。
我们只能使用 istringable (在上面的代码示例中)调用 IStringable 接口的成员。 但 C++/WinRT 接口(这是投影接口)派生自 winrt::Windows::Foundation::IUnknown。 因此,可以对其调用 IUnknown::as (或 IUnknown::try_as),以查询其他投影类型或接口,也可以使用或返回这些类型或接口。
Tip
有一种情况下,你不应调用as或try_as,那就是运行时类派生(“可组合类”)。 当实现类型组合了另一个类时,不要调用 as 或 try_as 来对被组合的类执行未经检查或经过检查的 QueryInterface。 而是访问(this->)m_inner数据成员,并对其调用as或try_as。 有关详细信息,请参阅本主题中的 运行时类派生 。
istringable.ToString();
IClosable iclosable = istringable.as<IClosable>();
iclosable.Close();
如果需要访问所有实现的成员,然后稍后将接口返回到调用方,请使用 winrt::make_self 函数模板。 make_self 返回 一个 winrt::com_ptr 包装实现类型。 你可以访问其所有接口的成员(使用箭头运算符),可以将其原样返回给调用方,也可以对其调用 as,并将所得的接口对象返回给调用方。
winrt::com_ptr<MyType> myimpl = winrt::make_self<MyType>();
myimpl->ToString();
myimpl->Close();
IClosable iclosable = myimpl.as<IClosable>();
iclosable.Close();
MyType 类不是投影的一部分;它是实现。 但是,这样就可以直接调用其实现方法,而无需虚拟函数调用的开销。 在上面的示例中,即使 MyType::ToString 使用与 IStringable 上的投影方法相同的签名,但我们直接调用非虚拟方法,而无需跨越应用程序二进制接口(ABI)。
com_ptr只保存指向 MyType 结构的指针,因此还可以通过变量和箭头运算符访问 myimpl 的任何其他内部详细信息。
如果拥有接口对象,并且你碰巧知道它是实现上的接口,则可以使用 winrt::get_self 函数模板返回到实现。 同样,这是一种可以避免虚函数调用并直接访问具体实现的技术。
注释
如果尚未安装 Windows SDK 版本 10.0.17763.0(Windows 10 版本 1809 或更高版本),则需要调用 winrt::from_abi 而不是 winrt::get_self。
下面是一个示例。 实现 BgLabelControl 自定义控件类还有另一个示例。
void ImplFromIClosable(IClosable const& from)
{
MyType* myimpl = winrt::get_self<MyType>(from);
myimpl->ToString();
myimpl->Close();
}
但只有原始接口对象保留引用。 如果你想保留它,则可以调用 com_ptr::copy_from。
winrt::com_ptr<MyType> impl;
impl.copy_from(winrt::get_self<MyType>(from));
// com_ptr::copy_from ensures that AddRef is called.
实现类型本身并不派生自 winrt::Windows::Foundation::IUnknown,因此它没有 as 函数。 即便如此,正如在上面的 ImplFromIClosable 函数中看到的那样,也可以访问其所有接口的成员。 但是,如果这样做,请不要将原始实现类型实例返回到调用方。 请改用前面已展示的技术之一,并返回一个投影接口或 com_ptr。
如果你有实现类型的实例,并且需要将其传递给需要相应投影类型的函数,则可以执行此操作,如下面的代码示例所示。 实现类型上存在转换运算符(前提是实现类型是由 cppwinrt.exe 工具生成的),这使得它成为可能。 可以将实现类型的值直接传递给接受相应投影类型值的方法。 在实现类型的成员函数中,可以将 *this 传递给需要相应投影类型值的方法。
// MyClass.idl
import "MyOtherClass.idl";
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void MemberFunction(MyOtherClass oc);
}
}
// MyClass.h
...
namespace winrt::MyProject::implementation
{
struct MyClass : MyClassT<MyClass>
{
MyClass() = default;
void MemberFunction(MyProject::MyOtherClass const& oc) { oc.DoWork(*this); }
};
}
...
// MyOtherClass.idl
import "MyClass.idl";
namespace MyProject
{
runtimeclass MyOtherClass
{
MyOtherClass();
void DoWork(MyClass c);
}
}
// MyOtherClass.h
...
namespace winrt::MyProject::implementation
{
struct MyOtherClass : MyOtherClassT<MyOtherClass>
{
MyOtherClass() = default;
void DoWork(MyProject::MyClass const& c){ /* ... */ }
};
}
...
//main.cpp
#include "pch.h"
#include <winrt/base.h>
#include "MyClass.h"
#include "MyOtherClass.h"
using namespace winrt;
// MyProject::MyClass is the projected type; the implementation type would be MyProject::implementation::MyClass.
void FreeFunction(MyProject::MyOtherClass const& oc)
{
auto defaultInterface = winrt::make<MyProject::implementation::MyClass>();
MyProject::implementation::MyClass* myimpl = winrt::get_self<MyProject::implementation::MyClass>(defaultInterface);
oc.DoWork(*myimpl);
}
...
运行时类的派生
可以创建派生自另一个运行时类的运行时类,前提是基类声明为“未密封”。 在 Windows 运行时 中,类派生的术语是“可组合类”。 实现派生类的代码取决于基类是由另一个组件还是由同一组件提供。 幸运的是,你不必学习这些规则,只需从 sources 编译器生成的 cppwinrt.exe 输出文件夹中复制示例实现。
请考虑此示例。
// MyProject.idl
namespace MyProject
{
[default_interface]
runtimeclass MyButton : Microsoft.UI.Xaml.Controls.Button
{
MyButton();
}
unsealed runtimeclass MyBase
{
MyBase();
overridable Int32 MethodOverride();
}
[default_interface]
runtimeclass MyDerived : MyBase
{
MyDerived();
}
}
在上面的示例中, MyButton 派生自由另一个组件提供的 XAML 按钮 控件。 在这种情况下,其实现方式看起来就和不可组合类的实现方式一样:
namespace winrt::MyProject::implementation
{
struct MyButton : MyButtonT<MyButton>
{
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyButton : MyButtonT<MyButton, implementation::MyButton>
{
};
}
另一方面,在上面的示例中, MyDerived 派生自同一组件中的另一个类。 在这种情况下,实现需要一个额外的模板参数,用于指定基类的实现类。
namespace winrt::MyProject::implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
{ // ^^^^^^^^^^^^^^^^^^^^^^
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyDerived>
{
};
}
无论哪种情况,你的实现都可以通过使用 base_type 类型别名进行限定来调用基类中的方法:
namespace winrt::MyProject::implementation
{
struct MyButton : MyButtonT<MyButton>
{
void OnApplyTemplate()
{
// Call base class method
base_type::OnApplyTemplate();
// Do more work after the base class method is done
DoAdditionalWork();
}
};
struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
{
int MethodOverride()
{
// Return double what the base class returns
return 2 * base_type::MethodOverride();
}
};
}
Tip
当实现类型组合了另一个类时,不要调用 as 或 try_as 来对被组合的类执行未经检查或经过检查的 QueryInterface。 而是访问(this->)m_inner数据成员,并对其调用as或try_as。
从具有非默认构造函数的类型派生
ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) 是非默认构造函数的示例。 没有默认构造函数,因此,若要构造 ToggleButtonAutomationPeer,需要传递 所有者。 因此,如果从 ToggleButtonAutomationPeer 派生,则需要提供一个接受 owner 参数并将其传递给基类的构造函数。 让我们看看实际效果如何。
// MySpecializedToggleButton.idl
namespace MyNamespace
{
runtimeclass MySpecializedToggleButton :
Microsoft.UI.Xaml.Controls.Primitives.ToggleButton
{
...
};
}
// MySpecializedToggleButtonAutomationPeer.idl
namespace MyNamespace
{
runtimeclass MySpecializedToggleButtonAutomationPeer :
Microsoft.UI.Xaml.Automation.Peers.ToggleButtonAutomationPeer
{
MySpecializedToggleButtonAutomationPeer(MySpecializedToggleButton owner);
};
}
为实现类型生成的构造函数如下所示。
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner)
{
...
}
...
唯一缺少的部分是需要将构造函数参数传递给基类。 还记得上面提到的 F 绑定多态性模式? 熟悉 C++/WinRT 使用的该模式的详细信息后,可以找出基类的调用内容(或者只需查看实现类的头文件)。 在这种情况下,如何调用基类构造函数。
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner) :
MySpecializedToggleButtonAutomationPeerT<MySpecializedToggleButtonAutomationPeer>(owner)
{
...
}
...
基类构造函数需要 ToggleButton。 并且MySpecializedToggleButton是一个ToggleButton。
在进行上述编辑(若要将该构造函数参数传递给基类)之前,编译器将标记构造函数,并指出在名为 MySpecializedToggleButtonAutomationPeer< 的类型上没有适当的默认构造函数可用 MySpecializedToggleButtonAutomationPeer_base>。 这实际上是实现类型的低音类的基类。
命名空间:投影类型、实现类型和工厂
如本主题前面所述,C++/WinRT 运行时类以多个命名空间中的多个 C++ 类的形式存在。 因此,名称 MyRuntimeClass 在 winrt::MyProject 命名空间中具有一个含义,在 winrt::MyProject::实现 命名空间中具有不同的含义。 请注意上下文中当前具有的命名空间,如果需要来自其他命名空间的名称,请使用命名空间前缀。 让我们更仔细地了解有关命名空间。
- winrt::MyProject。 该命名空间包含了投影类型。 投影类型的对象是一个代理;它本质上是一个指向后备对象的智能指针,而该后备对象可能在你的项目中实现,也可能在另一个编译单元中实现。
- winrt::MyProject::implementation。 此命名空间包含实现类型。 实现类型的对象不是指针;它是一个值 -- 一个完整的 C++ 堆栈对象。 不要直接构造实现类型;改为调用 winrt::make,将实现类型作为模板参数传递。 我们之前已在本主题中展示过 winrt::make 的实际用法示例,而在 XAML 控件;绑定到 C++/WinRT 属性 中还有另一个示例。 另请参见 诊断直接分配。
- winrt::MyProject::factory_implementation。 此命名空间包含工厂。 此命名空间中的对象支持 IActivationFactory。
此表显示了在不同上下文中需要使用的最小命名空间限定形式。
| 上下文中的命名空间 | 指定投影类型 | 指定实现类型 |
|---|---|---|
| winrt::MyProject | MyRuntimeClass |
implementation::MyRuntimeClass |
| winrt::MyProject::implementation | MyProject::MyRuntimeClass |
MyRuntimeClass |
Important
如果要从实现中返回投影类型,请注意不要通过编写 MyRuntimeClass myRuntimeClass;来实例化实现类型。 本主题前面在实例 化和返回实现类型和接口部分中显示了该方案的正确技术和代码。
在这种情况下,问题 MyRuntimeClass myRuntimeClass; 在于它在堆栈上创建 winrt::MyProject::implementation::MyRuntimeClass 对象。 该对象(实现类型)在某些方面的行为类似于投影类型, 你可以以相同的方式调用其上的方法;它甚至转换为投影类型。 但是,当范围退出时,对象会根据正常的 C++ 规则析构。 因此,如果向该对象返回了投影类型(智能指针),则该指针现在将悬停。
这种内存损坏类型的 bug 难以诊断。 因此,在调试版本中,C++/WinRT 断言会借助栈检测器帮助你发现这种错误。 但是,在堆上分配协同例程,因此,如果在协同例程内进行此错误,则不会得到帮助。 有关详细信息,请参阅 诊断直接分配。
在各种 C++/WinRT 功能中使用投影类型和实现类型
以下列出了 C++/WinRT 功能在不同场合需要类型的情况,以及各自所期望的类型种类(投影类型、实现类型,或两者皆可)。
| 功能 | 接受 | Notes |
|---|---|---|
T (表示智能指针) |
预计 | 请参阅 命名空间:投影类型、实现类型和工厂 中有关误用实现类型的警告。 |
agile_ref<T> |
两者都有 | 如果使用实现类型,则构造函数参数必须是 com_ptr<T>。 |
com_ptr<T> |
Implementation | 使用投影类型会产生以下错误:'Release' is not a member of 'T'。 |
default_interface<T> |
两者都有 | 如果使用实现类型,则返回第一个实现接口。 |
get_self<T> |
Implementation | 使用投影类型会产生以下错误:'_abi_TrustLevel': is not a member of 'T'。 |
guid_of<T>() |
两者都有 | 返回默认接口的 GUID。 |
IWinRTTemplateInterface<T> |
预计 | 使用实现类型虽然可以通过编译,但这样做是错误的——请参阅 命名空间:投影类型、实现类型和工厂 中的警告。 |
make<T> |
Implementation | 使用投影类型会产生以下错误:'implements_type': is not a member of any direct or indirect base class of 'T' |
make_agile(T const&) |
两者都有 | 如果使用实现类型,则参数必须是 com_ptr<T>。 |
make_self<T> |
Implementation | 使用投影类型会产生以下错误:'Release': is not a member of any direct or indirect base class of 'T' |
name_of<T> |
预计 | 如果使用实现类型,则将获得默认接口的 GUID 字符串形式。 |
weak_ref<T> |
两者都有 | 如果使用实现类型,则构造函数参数必须是 com_ptr<T>。 |
选择采用统一构造并直接实现访问
本部分介绍 C++/WinRT 2.0 的一项需选择启用的功能,不过在新项目中,该功能默认处于启用状态。 对于现有项目,你需要通过配置 cppwinrt.exe 工具来启用该功能。 在Visual Studio中,将项目属性 Common Properties>C++/WinRT>Optimized 设置为 Yes。 这样会将 <CppWinRTOptimized>true</CppWinRTOptimized> 添加到你的项目文件中。 在从命令行调用 cppwinrt.exe 时,它与添加开关的效果相同。
该 -opt[imize] 开关可启用通常称为 统一构造 的功能。 使用统一(或 统一式)构造时,你可以直接使用 C++/WinRT 语言投影本身,高效地创建和使用你的实现类型(即由组件实现、供应用程序使用的类型),且不会遇到任何加载器问题。
在介绍该功能之前,我们先来看一下没有统一结构的情况。 为了说明,我们将从此示例Windows 运行时类开始。
// MyClass.idl
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void Method();
static void StaticMethod();
}
}
作为熟悉使用 C++/WinRT 库的 C++ 开发人员,你可能会想像这样使用这个类。
using namespace winrt::MyProject;
MyClass c;
c.Method();
MyClass::StaticMethod();
这完全合理,前提是前面展示的使用代码不位于实现该类的同一组件中。 作为语言投影,C++/WinRT 会保护你作为开发人员免受 ABI(Windows 运行时定义的基于 COM 的应用程序二进制接口)的影响。 C++/WinRT 不会直接调用实现;它是经由 ABI 进行调用的。
因此,在构造 MyClass 对象(MyClass c;)的代码行上,C++/WinRT 投影调用 RoGetActivationFactory 来检索类或激活工厂,然后使用该工厂创建对象。 最后一行同样使用工厂,构成了一个看起来像是对静态方法的调用。 所有这些都要求你的类已注册,并且要求你的模块实现 DllGetActivationFactory 入口点。 C++/WinRT 具有非常快速的工厂缓存,因此这一切都不会导致使用组件的应用程序出现问题。 问题在于,你刚刚在自己的组件中做了一件有点问题的事。
首先,无论 C++/WinRT 工厂缓存有多快,通过 RoGetActivationFactory 调用(甚至通过工厂缓存的后续调用)始终比直接调用实现的速度慢。 调用 RoGetActivationFactory,再调用 IActivationFactory::ActivateInstance,然后再调用 QueryInterface,显然不如对局部定义的类型使用 C++ new 表达式高效。 因此,经验丰富的 C++/WinRT 开发人员习惯于在组件中创建对象时使用 winrt::make 或 winrt::make_self 帮助程序函数。
// MyClass c;
MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
但是,正如你所见,它远不如前者方便,也不够简洁。 必须使用帮助程序函数来创建对象,并且还必须消除实现类型和投影类型之间的歧义。
其次,通过投影创建该类意味着该类的激活工厂将被缓存。 通常,这正是你想要的;但如果工厂位于发起调用的同一个模块(DLL)中,那么实际上你就会让这个 DLL 一直保持加载状态,并使它永远无法卸载。 在许多情况下,这并不重要:但某些系统组件 必须 支持卸载。
这就是统一构造这一术语的由来。 无论创建代码是否驻留在仅使用类的项目中,还是驻留在实际 实现 该类的项目中,都可以自由使用同一语法来创建对象。
// MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
MyClass c;
使用 -opt[imize] 开关生成组件项目时,通过语言投影进行的调用会被编译成对直接创建实现类型的 winrt::make 函数的同样高效的调用。 这样可使语法保持简单且可预测,避免通过工厂进行调用所带来的任何性能损耗,同时也避免在此过程中固定该组件。 除了组件项目,这对 XAML 应用程序也很有用。 对于在同一应用程序中实现的类,绕过 RoGetActivationFactory 可让你以与它们位于组件外部时相同的所有方式来实例化它们(无需注册)。
统一的构造方式适用于底层由工厂处理的任何调用。 实际上,这意味着优化同时为构造函数和静态成员提供服务。 下面是该原始示例。
MyClass c;
c.Method();
MyClass::StaticMethod();
如果没有 -opt[imize],第一个语句和最后一个语句需要通过工厂对象调用。
有了-opt[imize],他们两人都不会这样做。 这些调用直接针对实现进行编译,甚至有可能内联。 这也说明了为什么在谈论-opt[imize]时,经常会用到另一个术语,即直接实现访问。
语言投影很方便,但是,当你可以直接访问实现时,你可以并应该利用这一点来生成最高效的代码。 C++/WinRT 可以为你执行此操作,而无需强制你离开投影的安全性和生产力。
这是一项破坏性变更,因为组件必须协同工作,才能使语言投影深入其中并直接访问其实现类型。 由于 C++/WinRT 是一个纯头文件库,你可以查看其内部实现,了解它是如何运作的。 若不使用 -opt[imize],MyClass 构造函数和 StaticMethod 成员会由投影生成,如下所示。
namespace winrt::MyProject
{
inline MyClass::MyClass() :
MyClass(impl::call_factory<MyClass>([](auto&& f){
return f.template ActivateInstance<MyClass>(); }))
{
}
inline void MyClass::StaticMethod()
{
impl::call_factory<MyClass, MyProject::IClassStatics>([&](auto&& f) {
return f.StaticMethod(); });
}
}
不需要遵循上述所有内容:目的是表明这两个调用都涉及对名为 call_factory的函数的调用。 这就提示你,这些调用涉及工厂缓存,并不是直接访问具体实现。
使用-opt[imize]时,根本不定义这些相同的函数。 相反,它们由投影来声明,其定义则由组件决定。
然后,该组件可以提供直接调用实现的定义。 现在我们已经来到破坏性变更这一部分。 当你同时使用 -component 和 -opt[imize] 时,系统会为你生成这些定义,它们位于名为 Type.g.cpp 的文件中,其中 Type 是正在实现的运行时类的名称。 这就是为什么在现有项目中首次启用 -opt[imize] 时可能会遇到各种链接器错误的原因。 你需要将生成的文件包含在实现中,以便将事情缝合起来。
在我们的示例中, MyClass.h 可能如下所示(无论是否 -opt[imize] 正在使用)。
// MyClass.h
#pragma once
#include "MyClass.g.h"
namespace winrt::MyProject::implementation
{
struct MyClass : ClassT<MyClass>
{
MyClass() = default;
static void StaticMethod();
void Method();
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyClass : ClassT<MyClass, implementation::MyClass>
{
};
}
你的 MyClass.cpp 是所有内容汇聚的地方。
#include "pch.h"
#include "MyClass.h"
#include "MyClass.g.cpp" // !!It's important that you add this line!!
namespace winrt::MyProject::implementation
{
void MyClass::StaticMethod()
{
}
void MyClass::Method()
{
}
}
因此,要在现有项目中使用统一构造,你需要编辑每个实现的 .cpp 文件,以便在包含(并定义)实现类之后 #include <Sub/Namespace/Type.g.cpp>。 该文件提供投影未定义的这些函数的定义。 以下是这些定义在 MyClass.g.cpp 文件中的样子。
namespace winrt::MyProject
{
MyClass::MyClass() :
MyClass(make<MyProject::implementation::MyClass>())
{
}
void MyClass::StaticMethod()
{
return MyProject::implementation::MyClass::StaticMethod();
}
}
这样就能很好地完成投影,高效地直接调用实现,避免调用工厂缓存,并满足链接器的要求。
-opt[imize] 为你做的最后一件事,是以这样一种方式修改项目中 module.g.cpp 的实现(该文件可帮助你实现 DLL 导出的 DllGetActivationFactory 和 DllCanUnloadNow),从而通过消除 C++/WinRT 1.0 所要求的强类型耦合,使增量生成通常快得多。 这通常被称为 类型擦除工厂。 如果没有 -opt[imize],为组件生成的 module.g.cpp 文件一开始会包含所有实现类的定义(本例中为 MyClass.h)。 然后,它直接为每个类创建实现工厂,如下所示。
if (requal(name, L"MyProject.MyClass"))
{
return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
同样,无需关注所有详细信息。 值得注意的是,这就要求提供你的组件所实现的所有类的完整定义。 这可能会影响内部循环,因为对单个实现的任何更改都会导致 module.g.cpp 重新编译。 使用 -opt[imize]时,情况不再如此。 相反,生成的 module.g.cpp 文件会出现两种情况。 第一个是它不再包含任何实现类。 在此示例中,它根本不包含 MyClass.h 。 而是创建实现工厂,而无需了解其实现。
void* winrt_make_MyProject_MyClass();
if (requal(name, L"MyProject.MyClass"))
{
return winrt_make_MyProject_MyClass();
}
显然,无需包含其定义,并且由链接器来解析 winrt_make_Component_Class 函数的定义。 当然,你不需要考虑这个问题,因为为你生成的那个 MyClass.g.cpp 文件(也就是你之前为支持统一构造而包含的那个文件)也定义了这个函数。 下面是为此示例生成的整个 MyClass.g.cpp 文件。
void* winrt_make_MyProject_MyClass()
{
return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
namespace winrt::MyProject
{
MyClass::MyClass() :
MyClass(make<MyProject::implementation::MyClass>())
{
}
void MyClass::StaticMethod()
{
return MyProject::implementation::MyClass::StaticMethod();
}
}
可以看到, winrt_make_MyProject_MyClass 函数直接创建实现的工厂。 这一切都意味着你可以愉快地更改任何给定的实现,并且 module.g.cpp 根本不需要重新编译。 仅当添加或删除 Windows 运行时 类时,module.g.cpp 才会更新,并且需要重新编译。
重写基类虚拟方法
如果基类和派生类都是应用定义的类,但虚拟方法在祖父类Windows 运行时类中定义,则派生类可能会出现问题。 在实践中,如果派生自 XAML 类,则会发生这种情况。 本节的其余内容将在 派生类 中的示例基础上继续展开。
namespace winrt::MyNamespace::implementation
{
struct BasePage : BasePageT<BasePage>
{
void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
struct DerivedPage : DerivedPageT<DerivedPage>
{
void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
}
层次结构为 Microsoft::UI::Xaml::Controls::Page<- BasePage<- DerivedPage。 BasePage::OnNavigatedFrom 方法正确重写了 Page::OnNavigatedFrom,但 DerivedPage::OnNavigatedFrom 并未重写 BasePage::OnNavigatedFrom。
在这里,DerivedPage 复用了 BasePage 的 IPageOverrides vtable,这意味着它未能重写 IPageOverrides::OnNavigatedFrom 方法。 一种潜在的解决方案要求 BasePage 本身是模板类,并且完全在头文件中实现,但这会使事情变得不可接受的复杂。
解决方法是将 OnNavigatedFrom 方法声明为基类中的显式虚拟方法。 这样,当 DerivedPage::IPageOverrides::OnNavigatedFrom 的 vtable 条目调用 BasePage::IPageOverrides::OnNavigatedFrom 时,生成者将调用 BasePage::OnNavigatedFrom(由于其虚拟原因),最终调用 DerivedPage::OnNavigatedFrom。
namespace winrt::MyNamespace::implementation
{
struct BasePage : BasePageT<BasePage>
{
// Note the `virtual` keyword here.
virtual void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
struct DerivedPage : DerivedPageT<DerivedPage>
{
void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
}
这要求类层次结构的所有成员都同意 OnNavigatedFrom 方法的返回值和参数类型。 如果两者不一致,则应将上述版本用作虚方法,并封装备选方法。
注释
IDL 不需要声明重写的方法。 有关详细信息,请参阅 实现可重写方法。
重要 API
- winrt::com_ptr 结构模板
- winrt::com_ptr::copy_from 函数
- winrt::from_abi 函数模板
- winrt::get_self 函数模板
- winrt::implements struct 模板
- winrt::make 函数模板
- winrt::make_self 函数模板
- winrt::Windows::Foundation::IUnknown::as 函数
- winrt::Windows::Foundation::IUnknown::try_as 函数