本主題說明如何直接或間接使用 winrt::implements 基礎結構體來編寫 C++/WinRT API。 在此語境中,撰寫的同義詞是 produce 或 implement。 本主題將依序涵蓋以下實作 C++/WinRT 類型 API 的情境。
Note
本主題會談到 Windows 執行階段 元件,但僅限於 C++/WinRT 的脈絡下。 如果你在尋找涵蓋所有 Windows 執行階段 語言的 Windows 執行階段 元件內容,請參考 Windows 執行階段 元件。
- 你並不是在撰寫 Windows 執行階段 類別(執行時類別);你只是想實作一個或多個 Windows 執行階段 介面,供應用程式在本地使用。 在此情況下,你直接衍生自 winrt::implements,並實作函式。
- 你 正在 撰寫一個執行時類別。 你可能正在撰寫一個要從應用程式中使用的內容。 或者,你可能正在撰寫要供 XAML 使用者介面 (UI) 取用的型別,而在這種情況下,你會在同一個編譯單元內同時實作及取用執行階段類別。 在這些情況下,你會讓工具為你產生源自 winrt::implements 的類別。
在這兩種情況下,實作 C++/WinRT API 的類型都稱為 實作類型。
這很重要
區分實作類型與投影類型是很重要的。 投影型別已在 Consume APIs with C++/WinRT 中說明。
如果你不是在編寫執行階段類別
最簡單的情況,就是你的型別實作了 Windows 執行階段 介面,而且你會在同一個應用程式中取用該型別。 在這種情況下,你的型別不一定是執行時類別;只是普通的 C++ 類別。 舉例來說,你可能正在撰寫一個以 Microsoft::UI::Xaml::Application 為核心的 WinUI 3 桌面應用程式。
如果你的型別被 XAML UI 參考,那麼它 確實 必須是執行階段類別,即使它與 XAML 位於同一個專案中。 這種情況下,請參考 「如果你正在撰寫一個要在 XAML UI 中引用的執行時類別」這一章節。
Note
關於安裝及使用 C++/WinRT Visual Studio 擴充套件(VSIX)及 NuGet 套件(共同提供專案範本與建置支援)的資訊,請參閱 Visual Studio 對 C++/WinRT 的支援。
在 Visual Studio 中,C++ 的 Blank App, Packaged(桌面版為 WinUI 3)專案範本說明了 WinUI 3 的應用程式模式。 App 類別源自 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>(); });
}
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 會將這些檔案合併成一個與根命名空間名稱相同的檔案。 這個最終 .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 執行階段(runtime)類別。 執行時類別是一種可透過現代 COM 介面啟用並使用的型態,通常跨越可執行檔邊界。 當你將 IDL 檔案加入專案並建置時,C++/WinRT 工具鏈midl.exe (和 cppwinrt.exe)會幫你產生實作類型。 關於 IDL 檔案工作流程的範例,請參見 XAML 控制項;綁定到 C++/WinRT 屬性。
以上述 IDL 範例為例,實作類型是一個名為 winrt::MyProject::實作::MyRuntimeClass 的 C++ 結構體存根,原始碼檔案中名為 \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h and MyRuntimeClass.cpp。
實作類型如下。
// 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 執行階段 元件中 authoring API 的導覽,請參閱 Windows 執行階段 元件搭配 C++/WinRT 及 C++/WinRT 中的 Author 事件。
如果你正在撰寫一個供你的 XAML UI 參考的執行階段類別
如果你的型別被你的 XAML UI 參考,那它就必須是執行時類別,即使它和 XAML 在同一個專案裡。 雖然執行階段類別通常跨可執行邊界啟用,但執行時類別也可以在實作它的編譯單元中使用。
在這種情況下,你既會編寫 API,也會使用 API。 實作你的執行時類別的程序基本上與 Windows 執行階段 元件相同。 所以,請參考前一節——如果你在 Windows 執行階段 元件中撰寫執行時類別。 唯一不同的是,從 IDL 開始,C++/WinRT 工具鏈不僅產生實作類型,還產生投影型別。 重要的是要理解,在這種情況下只說「MyRuntimeClass」可能會有歧義;有好幾個同名的實體,種類各異。
- MyRuntimeClass 是一個執行時類別的名稱。 但這其實是一個抽象:用 IDL 宣告,並用某種程式語言實作。
-
MyRuntimeClass 是 C++ 結構體 winrt:MyProject::實現::MyRuntimeClass 的名稱,這是執行時類別的 C++/WinRT 實作。 如我們所見,如果有獨立的實作專案和消耗專案,這個結構體只存在於實作專案中。 這就是 實作類型,或 實作。 此類型由
cppwinrt.exe工具在檔案\MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h和MyRuntimeClass.cpp中產生。 -
MyRuntimeClass 是以 C++ 結構體 winrt::MyProject::MyRuntimeClass 形式呈現的投影型別名稱。 如果有獨立的實作專案和消耗專案,那麼這個結構體只存在於消費專案中。 這就是 投影型,或稱 投影型。 此類型是在檔案
\MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h中(由cppwinrt.exe)產生的。
以下是與此主題相關的投影類型部分。
// 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 建構子,是從相同的編譯單元中使用執行階段類別時的兩個步驟中的第一步。 更多細節及程式碼範例請參見 Consume API with C++/WinRT。
- 如果你是從 同 一個編譯單元使用執行時類別,那麼你也可以直接在實作類型上實作非預設的建構子(記得,實作類型在
MyRuntimeClass.h)。
Note
如果你預期執行階段類別會供不同的編譯單元使用(這很常見),那麼請在 IDL 中加入建構函式(至少要有預設建構函式)。 這樣做時,你也會同時取得工廠實作以及你的實作類型。
如果你只想在同一編譯單元內撰寫並使用執行時類別,那就不要在 IDL 裡宣告任何建構子。 你不需要工廠實作,而且也不會自動產生它。 你的實作型別的預設建構子將會被刪除,但你可以輕鬆編輯它,改為將其設為預設。
如果你只想在同一編譯單元內編寫並使用執行時類別,且需要建構子參數,那就直接在你的實作類型上編寫你需要的建構子。
執行時類別的方法、屬性與事件
我們看到工作流程是用 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
但你可以從 MyType 轉換到 IStringable 或 IClosable 物件,然後透過呼叫 winrt::make 函式模板作為投影的一部分使用或回傳。 make 會回傳實作類型的預設介面。
IStringable istringable = winrt::make<MyType>();
Note
不過,如果你是從 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 函式模板回到實作。 同樣地,這是一種避免虛擬函式呼叫,並能直接進入實作的技術。
Note
如果你還沒安裝 Windows SDK 版本 10.0.17763.0(Windows 10,版本 1809)或更新版本,那你需要呼叫 winrt::from_abi 而不是 winrt::get_self。
以下是範例。 在 Implement the 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 執行階段 中用於類別推導的術語是「可組合類別」。 實作導出類別的程式碼取決於基底類別是由其他元件提供還是同一元件提供。 幸運的是,你不必學會這些規則——你只要從 cppwinrt.exe 編譯器產生的 sources 輸出資料夾中複製範例實作即可。
請看這個例子。
// 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 alias 型別加限定:
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,您必須傳入一個 owner。 因此,如果你繼承自 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_base<MySpecializedButtonAutomationPear> 的型別上沒有合適的預設建構器可用。 那其實是你實作類型中低音類別的基底類別。
命名空間:投影型別、實作型態與工廠
如您先前在本主題中所見,C++/WinRT 執行階段類別會以位於一個以上命名空間中的一個以上 C++ 類別的形式存在。 因此, MyRuntimeClass 這個名稱在 winrt::MyProject 命名空間中有一個意義,但在 winrt::MyProject::implementation 命名空間中則有不同的意義。 了解你目前在上下文中擁有的命名空間,然後如果需要來自不同命名空間的名稱,可以使用命名空間前綴。 讓我們更仔細地看看這些命名空間。
- winrt::我的計畫。 此命名空間包含投影型別。 投影型別的物件是一個代理物件;它本質上是指向後端物件的智慧指標,而該後端物件可能是在此處的專案中實作,也可能是在另一個編譯單元中實作。
- winrt::我的專案::實作。 此命名空間包含實作類型。 實作類型的物件不是指標;它是一個值——一個完整的 C++ 堆疊物件。 不要直接建構實作型別;相反地,呼叫 Winrt::make,並將你的實作類型作為模板參數傳遞。 我們之前在本主題中展示了 winrt::make 的實際運作範例, XAML 控制項中也有另一個範例;綁定到 C++/WinRT 屬性。 另見「 直接配置的診斷」。
- winrt::MyProject::factory_implementation. 此命名空間包含工廠。 此命名空間中的物件支援 IActivationFactory。
此表顯示在不同情境下你需要使用的最低命名空間資格。
| 上下文中的命名空間 | 指定投影型別 | 指定實作類型 |
|---|---|---|
| winrt::我的專案 | MyRuntimeClass |
implementation::MyRuntimeClass |
| winrt::我的專案::實施 | MyProject::MyRuntimeClass |
MyRuntimeClass |
這很重要
當你想要在實作中回傳投影型別時,請注意不要藉由寫下 MyRuntimeClass myRuntimeClass; 來實例化該實作型別。 該情境的正確技術與程式碼已在本主題先前的 「實例化與回傳實作類型與介面」章節中展示。
在那種情況下,MyRuntimeClass myRuntimeClass; 的問題在於它會在堆疊上建立一個 winrt::MyProject::implementation::MyRuntimeClass 物件。 該物件(實作型態)在某些方面行為類似投影型別——你可以用同樣方式呼叫方法;甚至還能轉換成投影型。 但該物件會依照一般的 C++ 規則,在離開作用域時被銷毀。 所以,如果你回傳一個投影型別(智慧指標)到該物件,那該指標現在會懸空。
這種記憶體損壞類型的錯誤很難診斷。 所以,在偵錯組建中,C++/WinRT 斷言會透過堆疊偵測器幫助你發現這個錯誤。 但協同程式是在堆積區上配置的,所以如果你是在協同程式內犯下這個錯誤,就不會有人幫你發現。 更多資訊請參閱「診斷直接分配」。
使用投影型別與實作型態,搭配各種 C++/WinRT 特性
以下是 C++/WinRT 功能期望型別的各種地方,以及它期望的型別類型(投影型別、實作型別,或兩者兼有)。
| Feature | 接受 | Notes |
|---|---|---|
T (代表智慧指標) |
預計 | 請參閱 Namespaces: projected types, implementation types, and factories 中的警告,避免誤用該實作類型。 |
agile_ref<T> |
Both | 如果你使用實作型別,則建構子參數必須是 com_ptr<T>。 |
com_ptr<T> |
Implementation | 使用投影型別會產生下列錯誤:'Release' is not a member of 'T'。 |
default_interface<T> |
Both | 如果你使用實作類型,則會回傳第一個實作介面。 |
get_self<T> |
Implementation | 使用投影類型會產生下列錯誤:'_abi_TrustLevel': is not a member of 'T'。 |
guid_of<T>() |
Both | 回傳預設介面的 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&) |
Both | 如果你使用實作類型,那麼參數必須是 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> |
Both | 如果你使用實作型別,則建構子參數必須是 com_ptr<T>。 |
選擇採用統一建構,並直接存取實作內容
本節描述一項 C++/WinRT 2.0 功能,該功能需選擇加入,雖然新專案預設已啟用。 對於現有專案,你需要透過設定 cppwinrt.exe 工具來選擇加入。 在 Visual Studio 中,將專案屬性 Common Properties C>++/WinRT>Optimized 設為「是」。 這樣會將 <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 方法的回傳值與參數類型。 如果他們不同意,那麼你應該以上述版本作為虛擬方法,並將替代版本包裝起來。
Note
你的 IDL 不需要宣告被覆寫的方法。 如需更多詳細資訊,請參閱 實作可覆寫的方法。
重要 API
- winrt::com_ptr 結構範本
- WinRT::com_ptr::copy_from函式
- WinRT::from_abi 函式範本
- WinRT::get_self 函式模板
- winrt::實現結構模板
- Winrt::make 函式範本
- WinRT::make_self 函式模板
- winrt::Windows::Foundation::IUnknown::as 函式
- winrt::Windows::Foundation::IUnknown::try_as 函式