C++/WinRT 可協助您撰寫經典的元件物件模型(COM)元件(或稱共同類別),就如同它可協助您撰寫 Windows 執行階段類別一樣。 本主題說明如何。
C++/WinRT 預設對於 COM 介面的行為方式
C++/WinRT winrt::implements 範本是您的運行時類別和啟動工廠直接或間接衍生的基礎。
根據預設,winrt::implements 會靜默忽略傳統 COM 介面。 任何對傳統 COM 介面的 QueryInterface (QI) 呼叫會因此失敗,返回 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();
}
}
以下是用戶端程式碼以取用 範例 類別。
// 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
標頭檔不僅可確保 unknwn.h
包含在 winrt/base.h
之前,也會進行設定,讓 C++/WinRT 和 WIL 能夠相互理解彼此的例外狀況和錯誤碼。
您可以將 作為傳統 COM 介面的<>,如此上述範例中的程式代碼將會編譯。
備註
在前面的例子中,即使在用戶端中啟用傳統 COM 支援(使用這個類別的程式碼),如果您尚未在伺服器中啟用傳統 COM 支援(實作這個類別的程式碼),則在用戶端呼叫 作為<> 時會當機,因為用於 的 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 元件的簡單範例。 這是迷你應用程式的完整列舉,因此,如果您將其貼到新的 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 元件。
更現實且有趣的範例
本主題的其餘部分將逐步解說如何建立一個使用 C++/WinRT 來實作基本 coclass(COM 元件或 COM 類別)和類別工廠的簡約控制台應用程式專案。 範例應用程式示範如何使用快顯通知上的回呼按鈕來傳遞快顯通知,而 coclass(實作 INotificationActivationCallback COM 介面)可讓使用者在快顯通知上按兩下該按鈕時啟動並回呼應用程式。
如需有關快顯通知功能區域的詳細資訊,請參閱 傳送本機快顯通知。 不過,該章節中的程式碼範例都未使用 C++/WinRT,因此我們建議您使用本主題中顯示的程式碼。
建立 Windows 主控台應用程式專案 (ToastAndCallback)
從在Visual Studio Microsoft建立新項目開始。 建立一個 Windows 控制台應用程式(C++/WinRT) 專案,並將其命名為 ToastAndCallback。
開啟pch.h
,並在包含任何 C++/WinRT 標頭之前新增#include <unknwn.h>
。 以下是結果;您可以使用此清單取代 pch.h
的內容。
// pch.h
#pragma once
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
開啟 main.cpp
,並移除專案模板生成的 using 指令。 在它們的位置中,插入下列程式代碼(這會提供我們需要的 libs、標頭和類型名稱)。 以下是結果,您可以將 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() { }
專案尚未建置;完成新增程式代碼之後,系統會提示您建置並執行。
實作 coclass 和 Class Factory
在 C++/WinRT 中,藉由從 winrt::implements 基底結構衍生來實作 coclasses 和 class factories。 緊接在上面顯示的三個using-directives 之後(以及 main
之前),貼上此程式代碼以實作快顯通知 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;
}
};
上述 coclass 的實作遵循與 C++/WinRT中
在上述程式碼的 coclass 中,我們實作了 INotificationActivationCallback::Activate 方法,這是在使用者單擊通知上的回呼按鈕時所呼叫的函式。 但在呼叫該函式之前,必須建立 coclass 的實例,也就是 IClassFactory::CreateInstance 函式的工作。
我們剛實作的 coclass 稱為通知 COM 啟動器,且其類別識別碼 (CLSID) 格式為 callback_guid
標識碼(類型為 GUID),如上所示。 我們稍後會使用該識別碼,以[開始]功能表快捷方式和 Windows 登錄項目形式出現。 COM 啟動器 CLSID 及其相關聯 COM 伺服器的路徑(這是我們在這裡建置的可執行檔路徑)是動態通知用來識別當用户按下回呼按鈕時要建立實例的類別的機制(無論通知是否在操作中心被按下)。
實作 COM 方法的最佳做法
錯誤處理和資源管理的技術可以相輔相成。 使用例外狀況比錯誤碼更方便且實用。 如果您採用資源獲取即初始化(RAII)慣用法,則可以避免明確檢查錯誤碼以及明確釋放資源。 這類明確的檢查會使您的程式碼變得比必要的更複雜,並且讓程式漏洞有許多藏身之處。 請改用 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
函式,並將此程式碼列表貼到原位置,其中包含註冊 coclass 的程式碼,然後傳送一個可以回呼應用程式的通知。
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' 鍵即可顯示快顯通知。 然後,您可以直接從快顯通知或從操作中心按 [Call back ToastAndCallback] 按鈕,然後啟動您的應用程式、coclass 具現化,以及執行 INotificationActivationCallback::Activate 方法。
進程中 COM 伺服器
上述 ToastAndCallback 範例應用程式會以本機(或跨進程)COM 伺服器的形式運作。 這表示您用來註冊其 coclass CLSID 的 LocalServer32 Windows 登錄機碼項。 本機 COM 伺服器會在可執行的二進位檔內裝載其coclass(es)(.exe
)。
或者(更可能的是),您可以選擇在動態連結庫(.dll
)中託管您的協同類別。 DLL 格式的 COM 伺服器稱為同進程 COM 伺服器,並以使用 InprocServer32 Windows 登錄機碼來註冊 CLSID 表示。
建立 Dynamic-Link 函式庫(DLL)專案
您可以在 Microsoft Visual Studio 建立新專案,開始進行建立進程內 COM 伺服器的工作。 建立 Visual C++>Windows Desktop>Dynamic-Link Library (DLL) 專案。
若要將C++/WinRT 支援新增至新專案,請遵循 修改 Windows 傳統型應用程式專案以新增C++/WinRT 支援中所述的步驟。
實作coclass、class Factory和內部伺服器匯出
開啟 dllmain.cpp
,並將程式代碼清單新增至它,如下所示。
如果您已經有實作 C++/WinRT Windows 執行時間類別的 DLL,則您已經擁有如下所示的 DllCanUnloadNow 函 式。 如果您想要將 coclasses 新增至該 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 中的 弱參考。
C++/WinRT(特別是,winrt::implements 基底結構範本)會為您實作 IWeakReferenceSource,如果您的類型實作 IInspectable(或任何衍生自 IInspectable的介面)。
這是因為 IWeakReferenceSource 和 IWeakReference 是針對 Windows 運行時間類型所設計。 因此,只要將 winrt::Windows::Foundation::IInspectable 新增
struct MyCoclass : winrt::implements<MyCoclass, IMyComInterface, winrt::Windows::Foundation::IInspectable>
{
// ...
};
實作一個從另一個 COM 介面衍生的 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實例直接或間接呼叫 QueryInterface ,並針對 IID_IFileSystemBindData (即 基礎 介面)時能夠成功。 透過為 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 範本使用時可見。 一般而言,您會將它放在共用標頭檔案中。