通过 C++/WinRT 创作 COM 组件
C++/WinRT 可以帮助你创作经典组件对象模型 (COM) 组件(或组件类),就像它可以帮助你创作 Windows 运行时类一样。 本主题演示如何执行该操作。
默认情况下,C++/WinRT 在 COM 接口方面的表现如何
C++/WinRT 的 winrt::implements 模板是直接或间接派生运行时类和激活工厂的基础。
默认情况下,winrt::implements 会以无提示方式忽略经典 COM 接口。 因此,通过 QueryInterface (QI) 调用经典 COM 接口会失败,并出现 E_NOINTERFACE 错误。 默认情况下,winrt::implements 仅支持 C++/WinRT 接口。
- winrt::IUnknown 是一个 C++/WinRT 接口,因此 winrt::implements 支持 winrt::IUnknown 接口 。
- winrt::implements 默认情况下不支持 ::IUnknown 本身 。
稍后你将了解如何克服这些默认情况下不支持的情况。 但首先来看看一个代码示例,了解默认情况下会发生什么。
// Sample.idl
namespace MyProject
{
runtimeclass Sample
{
Sample();
void DoWork();
}
}
// Sample.h
#include "pch.h"
#include <shobjidl.h> // Needed only for this file.
namespace winrt::MyProject::implementation
{
struct Sample : implements<Sample, IInitializeWithWindow>
{
IFACEMETHOD(Initialize)(HWND hwnd);
void DoWork();
}
}
下面是使用 Sample 类的客户端代码。
// Client.cpp
Sample sample; // Construct a Sample object via its projection.
// This next line doesn't compile yet.
sample.as<IInitializeWithWindow>()->Initialize(hwnd);
启用经典 COM 支持
好在要让 winrt::implements 支持经典 COM 接口,只需在包括任何 C++/WinRT 头文件之前包括 unknwn.h
标头文件即可。
可以显式这样做,也可以间接这样做,只需包括一些其他的头文件(例如 ole2.h
)即可。 一个建议的方法是包括 wil\cppwinrt.h
头文件,该文件是 Windows 实现库 (WIL) 的一部分。 wil\cppwinrt.h
头文件不仅可确保在 winrt/base.h
之前包括 unknwn.h
,而且可以让 C++/WinRT 和 WIL 了解彼此的异常和错误代码。
然后,可以对经典 COM 接口调用 as<>,以上示例中的代码将进行编译。
注意
在上面的示例中,即使在客户端(代码使用类)中启用了经典 COM 支持,如果尚未在服务器(代码实现类)中启用经典 COM 支持,则客户端中对 as<> 的调用会失败,因为 IInitializeWithWindow 的 QI 会失败。
局部(非投影)类
局部类在同一编译单元(应用或其他二进制)中实现和使用;因此没有任何投影。
下面是仅实现经典 COM 接口的局部类的示例。
struct LocalObject :
winrt::implements<LocalObject, IInitializeWithWindow>
{
...
};
如果实现该示例,但不启用经典 COM 支持,则下面的代码将失败。
winrt::make<LocalObject>(); // error: ‘first_interface’: is not a member of ‘winrt::impl::interface_list<>’
同样,IInitializeWithWindow 不会被识别为 COM 接口,因此 C++/WinRT 将忽略它。 对于 LocalObject 示例,忽略 COM 接口的结果意味着 LocalObject 根本没有接口 。 但每个 COM 类必须至少实现一个接口。
COM 组件的简单示例
下面是使用 C++/WinRT 编写的 COM 组件的简单示例。 这是一个微型应用程序的完整代码清单,因此如果将其粘贴到新 Windows 控制台应用程序 (C++/WinRT) 项目的 pch.h
和 main.cpp
中,则可对其进行试用。
// pch.h
#pragma once
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
// main.cpp : Defines the entry point for the console application.
#include "pch.h"
struct __declspec(uuid("ddc36e02-18ac-47c4-ae17-d420eece2281")) IMyComInterface : ::IUnknown
{
virtual HRESULT __stdcall Call() = 0;
};
using namespace winrt;
using namespace Windows::Foundation;
int main()
{
winrt::init_apartment();
struct MyCoclass : winrt::implements<MyCoclass, IPersist, IStringable, IMyComInterface>
{
HRESULT __stdcall Call() noexcept override
{
return S_OK;
}
HRESULT __stdcall GetClassID(CLSID* id) noexcept override
{
*id = IID_IPersist; // Doesn't matter what we return, for this example.
return S_OK;
}
winrt::hstring ToString()
{
return L"MyCoclass as a string";
}
};
auto mycoclass_instance{ winrt::make<MyCoclass>() };
CLSID id{};
winrt::check_hresult(mycoclass_instance->GetClassID(&id));
winrt::check_hresult(mycoclass_instance.as<IMyComInterface>()->Call());
}
一个更现实且有趣的示例
此主题的其余部分演练如何创建使用 C++/WinRT 实现基本组件类(COM 组件或 COM 类)和类工厂的最小控制台应用程序项目。 示例应用程序演示如何提供具有一个回调按钮的 toast 通知,组件类(实现 INotificationActivationCallback COM 接口)使应用程序可以启动并在用户单击 toast 上的该按钮时进行回调。
有关 toast 通知功能区域的更多背景信息可以在发送本地 toast 通知中找到。 不过文档该部分中没有代码示例使用 C++/WinRT,因此建议首选本主题中演示的代码。
创建 Windows 控制台应用程序项目 (ToastAndCallback)
首先在 Microsoft Visual Studio 中创建新项目。 创建“Windows 控制台应用程序(C++/WinRT)”项目,然后将它命名为 ToastAndCallback。
打开 pch.h
,在用于任何 C++/WinRT 标头的 include 语句之前添加 #include <unknwn.h>
。 下面是结果;可以将你的 pch.h
的内容替换为此清单。
// pch.h
#pragma once
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
打开 main.cpp
,删除项目模板生成 using 指令。 在其位置插入以下代码(该代码提供我们所需的库、标头和类型名称)。 下面是结果;可以将你的 main.cpp
的内容替换为此清单(我们还会在下面的清单中删除 main
中的代码,因为我们会在以后替换该函数)。
// main.cpp : Defines the entry point for the console application.
#include "pch.h"
#pragma comment(lib, "advapi32")
#pragma comment(lib, "ole32")
#pragma comment(lib, "shell32")
#include <iomanip>
#include <iostream>
#include <notificationactivationcallback.h>
#include <propkey.h>
#include <propvarutil.h>
#include <shlobj.h>
#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>
using namespace winrt;
using namespace Windows::Data::Xml::Dom;
using namespace Windows::UI::Notifications;
int main() { }
项目尚未生成;完成添加代码之后,系统会提示生成并运行。
实现组件类和类工厂
在 C++/WinRT 中,通过从 winrt::implements 基结构派生来实现组件类和类工厂。 紧接着上面所示的三个 using 指令之后(并且在 main
之前),粘贴此代码以实现 toast 通知 COM 激活器组件。
static constexpr GUID callback_guid // BAF2FA85-E121-4CC9-A942-CE335B6F917F
{
0xBAF2FA85, 0xE121, 0x4CC9, {0xA9, 0x42, 0xCE, 0x33, 0x5B, 0x6F, 0x91, 0x7F}
};
std::wstring const this_app_name{ L"ToastAndCallback" };
struct callback : winrt::implements<callback, INotificationActivationCallback>
{
HRESULT __stdcall Activate(
LPCWSTR app,
LPCWSTR args,
[[maybe_unused]] NOTIFICATION_USER_INPUT_DATA const* data,
[[maybe_unused]] ULONG count) noexcept final
{
try
{
std::wcout << this_app_name << L" has been called back from a notification." << std::endl;
std::wcout << L"Value of the 'app' parameter is '" << app << L"'." << std::endl;
std::wcout << L"Value of the 'args' parameter is '" << args << L"'." << std::endl;
return S_OK;
}
catch (...)
{
return winrt::to_hresult();
}
}
};
struct callback_factory : implements<callback_factory, IClassFactory>
{
HRESULT __stdcall CreateInstance(
IUnknown* outer,
GUID const& iid,
void** result) noexcept final
{
*result = nullptr;
if (outer)
{
return CLASS_E_NOAGGREGATION;
}
return make<callback>()->QueryInterface(iid, result);
}
HRESULT __stdcall LockServer(BOOL) noexcept final
{
return S_OK;
}
};
上面的组件类实现遵循在使用 C++/WinRT 创作 API 中演示的相同模式。 因此,可以使用相同技术实现 COM 接口以及 Windows 运行时接口。 COM 组件和 Windows 运行时类会通过接口公开其功能。 每个 COM 接口最终派生自 IUnknown 接口接口。 Windows 运行时基于 COM,一个区别是 Windows 运行时接口最终派生自 IInspectable 接口(并且 IInspectable 派生自 IUnknown)。
在上面代码中的组件类中,我们实现 INotificationActivationCallback::Activate 方法,这是在用户单击 toast 通知上的回调按钮时调用的函数。 但是在可以调用该函数之前,需要创建组件类的实例,这是 IClassFactory::CreateInstance 函数的工作。
我们刚刚实现的组件类称为通知的 COM 激活器,其类 id (CLSID) 的形式为如上所示的 callback_guid
标识符(类型为 GUID)。 我们会在以后采用“开始”菜单快捷方式和 Windows 注册表项的形式来使用该标识符。 COM 激活器 CLSID 以及指向其关联 COM 服务器的路径(这是在此处生成的可执行文件的路径)是一种机制,toast 通知该机制可知道在单击其回调按钮时要创建哪个类的实例(无论是否在操作中心内单击通知)。
实现 COM 方法的最佳做法
用于错误处理和资源管理的方法可以一起进行。 使用异常比错误代码更加方便且实用。 如果采用资源获取即初始化 (RAII) 惯用做法,则可以避免显式检查错误代码,然后显式释放资源。 这类显式检查使代码比实际所需更复杂,并使 bug 有大量位置可以隐藏。 相反,使用 RAII,并引发/捕获异常。 这样,资源分配是异常安全的,并且代码十分简单。
但是,不得允许异常转义 COM 方法实现。 可以通过对 COM 方法使用 noexcept
说明符来确保这一点。 可以在方法调用关系图中的任意位置引发异常,只要在方法退出之前处理它们。 如果使用 noexcept
,但随后允许异常转义方法,则应用程序会终止。
添加帮助程序类型和函数
在此步骤中,我们会添加一些帮助程序类型和函数,代码的其余部分会使用它们。 就在 main
之前添加以下内容。
struct prop_variant : PROPVARIANT
{
prop_variant() noexcept : PROPVARIANT{}
{
}
~prop_variant() noexcept
{
clear();
}
void clear() noexcept
{
WINRT_VERIFY_(S_OK, ::PropVariantClear(this));
}
};
struct registry_traits
{
using type = HKEY;
static void close(type value) noexcept
{
WINRT_VERIFY_(ERROR_SUCCESS, ::RegCloseKey(value));
}
static constexpr type invalid() noexcept
{
return nullptr;
}
};
using registry_key = winrt::handle_type<registry_traits>;
std::wstring get_module_path()
{
std::wstring path(100, L'?');
uint32_t path_size{};
DWORD actual_size{};
do
{
path_size = static_cast<uint32_t>(path.size());
actual_size = ::GetModuleFileName(nullptr, path.data(), path_size);
if (actual_size + 1 > path_size)
{
path.resize(path_size * 2, L'?');
}
} while (actual_size + 1 > path_size);
path.resize(actual_size);
return path;
}
std::wstring get_shortcut_path()
{
std::wstring format{ LR"(%ProgramData%\Microsoft\Windows\Start Menu\Programs\)" };
format += (this_app_name + L".lnk");
auto required{ ::ExpandEnvironmentStrings(format.c_str(), nullptr, 0) };
std::wstring path(required - 1, L'?');
::ExpandEnvironmentStrings(format.c_str(), path.data(), required);
return path;
}
实现其余函数和 wmain 入口点函数
删除 main
函数,在其原位置粘贴此代码清单,其中包括用于注册组件类,然后提供能够回调应用程序的 toast 的代码。
void register_callback()
{
DWORD registration{};
winrt::check_hresult(::CoRegisterClassObject(
callback_guid,
make<callback_factory>().get(),
CLSCTX_LOCAL_SERVER,
REGCLS_SINGLEUSE,
®istration));
}
void create_shortcut()
{
auto link{ winrt::create_instance<IShellLink>(CLSID_ShellLink) };
std::wstring module_path{ get_module_path() };
winrt::check_hresult(link->SetPath(module_path.c_str()));
auto store = link.as<IPropertyStore>();
prop_variant value;
winrt::check_hresult(::InitPropVariantFromString(this_app_name.c_str(), &value));
winrt::check_hresult(store->SetValue(PKEY_AppUserModel_ID, value));
value.clear();
winrt::check_hresult(::InitPropVariantFromCLSID(callback_guid, &value));
winrt::check_hresult(store->SetValue(PKEY_AppUserModel_ToastActivatorCLSID, value));
auto file{ store.as<IPersistFile>() };
std::wstring shortcut_path{ get_shortcut_path() };
winrt::check_hresult(file->Save(shortcut_path.c_str(), TRUE));
std::wcout << L"In " << shortcut_path << L", created a shortcut to " << module_path << std::endl;
}
void update_registry()
{
std::wstring key_path{ LR"(SOFTWARE\Classes\CLSID\{????????-????-????-????-????????????})" };
::StringFromGUID2(callback_guid, key_path.data() + 23, 39);
key_path += LR"(\LocalServer32)";
registry_key key;
winrt::check_win32(::RegCreateKeyEx(
HKEY_CURRENT_USER,
key_path.c_str(),
0,
nullptr,
0,
KEY_WRITE,
nullptr,
key.put(),
nullptr));
::RegDeleteValue(key.get(), nullptr);
std::wstring path{ get_module_path() };
winrt::check_win32(::RegSetValueEx(
key.get(),
nullptr,
0,
REG_SZ,
reinterpret_cast<BYTE const*>(path.c_str()),
static_cast<uint32_t>((path.size() + 1) * sizeof(wchar_t))));
std::wcout << L"In " << key_path << L", registered local server at " << path << std::endl;
}
void create_toast()
{
XmlDocument xml;
std::wstring toastPayload
{
LR"(
<toast>
<visual>
<binding template='ToastGeneric'>
<text>)"
};
toastPayload += this_app_name;
toastPayload += LR"(
</text>
</binding>
</visual>
<actions>
<action content='Call back )";
toastPayload += this_app_name;
toastPayload += LR"(
' arguments='the_args' activationKind='Foreground' />
</actions>
</toast>)";
xml.LoadXml(toastPayload);
ToastNotification toast{ xml };
ToastNotifier notifier{ ToastNotificationManager::CreateToastNotifier(this_app_name) };
notifier.Show(toast);
::Sleep(50); // Give the callback chance to display.
}
void LaunchedNormally(HANDLE, INPUT_RECORD &, DWORD &);
void LaunchedFromNotification(HANDLE, INPUT_RECORD &, DWORD &);
int wmain(int argc, wchar_t * argv[], wchar_t * /* envp */[])
{
winrt::init_apartment();
register_callback();
HANDLE consoleHandle{ ::GetStdHandle(STD_INPUT_HANDLE) };
INPUT_RECORD buffer{};
DWORD events{};
::FlushConsoleInputBuffer(consoleHandle);
if (argc == 1)
{
LaunchedNormally(consoleHandle, buffer, events);
}
else if (argc == 2 && wcscmp(argv[1], L"-Embedding") == 0)
{
LaunchedFromNotification(consoleHandle, buffer, events);
}
}
void LaunchedNormally(HANDLE consoleHandle, INPUT_RECORD & buffer, DWORD & events)
{
try
{
bool runningAsAdmin{ ::IsUserAnAdmin() == TRUE };
std::wcout << this_app_name << L" is running" << (runningAsAdmin ? L" (administrator)." : L" (NOT as administrator).") << std::endl;
if (runningAsAdmin)
{
create_shortcut();
update_registry();
}
std::wcout << std::endl << L"Press 'T' to display a toast notification (press any other key to exit)." << std::endl;
::ReadConsoleInput(consoleHandle, &buffer, 1, &events);
if (towupper(buffer.Event.KeyEvent.uChar.UnicodeChar) == L'T')
{
create_toast();
}
}
catch (winrt::hresult_error const& e)
{
std::wcout << L"Error: " << e.message().c_str() << L" (" << std::hex << std::showbase << std::setw(8) << static_cast<uint32_t>(e.code()) << L")" << std::endl;
}
}
void LaunchedFromNotification(HANDLE consoleHandle, INPUT_RECORD & buffer, DWORD & events)
{
::Sleep(50); // Give the callback chance to display its message.
std::wcout << std::endl << L"Press any key to exit." << std::endl;
::ReadConsoleInput(consoleHandle, &buffer, 1, &events);
}
如何测试示例应用程序
生成应用程序,然后至少以管理员身份运行一次,以便使注册(和其他设置)代码运行。 执行此操作的一种方法是以管理员身份运行 Visual Studio,然后从 Visual Studio 运行应用。 在任务栏中右键单击 Visual Studio 以显示跳转列表,在跳转列表右键单击 Visual Studio,然后单击“以管理员身份运行”。 同意提示,然后打开项目。 运行应用程序时,会显示一条消息,指出是否在以管理员身份运行应用程序。 如果不是,则注册和其他设置不会运行。 该注册和其他设置必须至少运行一次,才能使应用程序正常工作。
无论是否在以管理员身份运行应用程序,按“T”使 toast 显示。 然后可以直接从弹出的 toast 通知或是从操作中心单击“回调 ToastAndCallback”按钮,这样会启动应用程序,实例化组件类并执行 INotificationActivationCallback::Activate 方法。
进程内 COM 服务器
上面的 ToastAndCallback 示例应用可充当本地(或进程外)COM 服务器。 这由 LocalServer32 Windows 注册表项进行指示,该项用于注册其组件类的 CLSID。 本地 COM 服务器在可执行二进制文件 (.exe
) 中承载其组件类。
或者(并且可能更有可能),可以选择在动态链接库 (.dll
) 中承载组件类。 采用 DLL 形式的 COM 服务器称为进程内 COM 服务器,由使用 InprocServer32 Windows 注册表项注册 CLSID 进行指示。
创建动态链接库 (DLL) 项目
可以通过在 Microsoft Visual Studio 中创建新项目,来开始创建进程内 COM 服务器的任务。 创建“Visual C++”>“Windows 桌面”>“动态链接库(DLL)”项目。
若要向新项目添加 C++/WinRT 支持,请执行修改 Windows 桌面应用程序项目以添加 C++/WinRT 支持中所述的步骤。
实现组件类、类工厂和进程内服务器导出
打开 dllmain.cpp
,并向其添加如下所示的代码清单。
如果已具有一个实现 C++WinRT Windows 运行时类的 DLL,则表示已具有如下所示的 DllCanUnloadNow 函数。 如果要将组件类添加到该 DLL,则可以添加 DllGetClassObject 函数。
如果没有要保持与之兼容的现有 Windows 运行时 C++ 模板库 (WRL) 代码,则可以从显示的代码中删除 WRL 部分。
// dllmain.cpp
struct MyCoclass : winrt::implements<MyCoclass, IPersist>
{
HRESULT STDMETHODCALLTYPE GetClassID(CLSID* id) noexcept override
{
*id = IID_IPersist; // Doesn't matter what we return, for this example.
return S_OK;
}
};
struct __declspec(uuid("85d6672d-0606-4389-a50a-356ce7bded09"))
MyCoclassFactory : winrt::implements<MyCoclassFactory, IClassFactory>
{
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject) noexcept override
{
try
{
return winrt::make<MyCoclass>()->QueryInterface(riid, ppvObject);
}
catch (...)
{
return winrt::to_hresult();
}
}
HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) noexcept override
{
// ...
return S_OK;
}
// ...
};
HRESULT __stdcall DllCanUnloadNow()
{
#ifdef _WRL_MODULE_H_
if (!::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().Terminate())
{
return S_FALSE;
}
#endif
if (winrt::get_module_lock())
{
return S_FALSE;
}
winrt::clear_factory_cache();
return S_OK;
}
HRESULT __stdcall DllGetClassObject(GUID const& clsid, GUID const& iid, void** result)
{
try
{
*result = nullptr;
if (clsid == __uuidof(MyCoclassFactory))
{
return winrt::make<MyCoclassFactory>()->QueryInterface(iid, result);
}
#ifdef _WRL_MODULE_H_
return ::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().GetClassObject(clsid, iid, result);
#else
return winrt::hresult_class_not_available().to_abi();
#endif
}
catch (...)
{
return winrt::to_hresult();
}
}
对弱引用的支持
另请参阅 C++/WinRT 中的弱引用。
如果类型实现 IInspectable(或任何派生自 IInspectable 的接口),则 C++/WinRT(具体而言,winrt::implements 基结构模板)会实现 IWeakReferenceSource。
这是因为 IWeakReferenceSource 和 IWeakReference 旨在用于 Windows 运行时类型。 因此,只需通过向实现添加 winrt::Windows::Foundation::IInspectable(或派生自 IInspectable 的接口),即可为组件类启用弱引用支持。
struct MyCoclass : winrt::implements<MyCoclass, IMyComInterface, winrt::Windows::Foundation::IInspectable>
{
// ...
};
实现从另一个接口派生的 COM 接口
接口派生是经典 COM 的一项功能(Windows 运行时中刻意不提供此功能)。 下例展示了接口派生的显示效果。
IFileSystemBindData2 : public IFileSystemBindData { /* ... */ };
如果要编写需要实现的类,例如 IFileSystemBindData 和 IFileSystemBindData2,则表达的第一步是声明仅实现派生接口,如下所示。
// pch.h
#pragma once
#include <Shobjidl.h>
...
// main.cpp
...
struct MyFileSystemBindData :
implements<MyFileSystemBindData,
IFileSystemBindData2>
{
// IFileSystemBindData
IFACEMETHOD(SetFindData)(const WIN32_FIND_DATAW* pfd) override { /* ... */ return S_OK; };
IFACEMETHOD(GetFindData)(WIN32_FIND_DATAW* pfd) override { /* ... */ return S_OK; };
// IFileSystemBindData2
IFACEMETHOD(SetFileID)(LARGE_INTEGER liFileID) override { /* ... */ return S_OK; };
IFACEMETHOD(GetFileID)(LARGE_INTEGER* pliFileID) override { /* ... */ return S_OK; };
IFACEMETHOD(SetJunctionCLSID)(REFCLSID clsid) override { /* ... */ return S_OK; };
IFACEMETHOD(GetJunctionCLSID)(CLSID* pclsid) override { /* ... */ return S_OK; };
};
...
int main()
...
下一步是在针对 MyFileSystemBindData 示例对 IID_IFileSystemBindData(基接口)直接或间接调用 QueryInterface 时,确保 QueryInterface 成功 。 为此,需要为 winrt::is_guid_of 函数模板提供专用化。
winrt::is_guid_of 是可变参数,因此可以为其提供接口列表。 下面介绍如何提供专用化,以使对 IFileSystemBindData2 的检查还包括对 IFileSystemBindData 的测试 。
// pch.h
...
namespace winrt
{
template<>
inline bool is_guid_of<IFileSystemBindData2>(guid const& id) noexcept
{
return is_guid_of<IFileSystemBindData2, IFileSystemBindData>(id);
}
}
// main.cpp
...
int main()
{
...
auto mfsbd{ winrt::make<MyFileSystemBindData>() };
auto a{ mfsbd.as<IFileSystemBindData2>() }; // Would succeed even without the **is_guid_of** specialization.
auto b{ mfsbd.as<IFileSystemBindData>() }; // Needs the **is_guid_of** specialization in order to succeed.
}
winrt::is_guid_of 的专用化在项目的所有文件中必须相同,并且在 winrt::implements 或 winrt::delegate 模板使用接口时可见 。 通常会将该内容放在一个通用标头文件中。