使用 C++/WinRT 取用 API

本主題示範如何取用 C++/WinRT API,不管是否為 Windows 的一部分,皆由第三方元件廠商或您自己來實作。

重要

因此,本主題中的程式碼範例簡短且讓您容易試用,您可藉由建立新的 Windows 主控台應用程式 (C++/WinRT) 專案及複製貼上程式碼來重現它們。 不過,您無法從這類未封裝的應用程式取用任意自訂 (第三方) Windows 執行階段類型。 您只能以這種方式取用 Windows 類型。

若要從主控台應用程式取用自訂 (第三方) Windows 執行階段類型,您必須為應用程式提供套件識別資料,以便解析取用的自訂類型註冊。 如需詳細資訊,請參閱 Windows 應用程式封裝專案

或者,從空白應用程式 (C++/WinRT)核心應用程式 (C++/WinRT)Windows 執行階段元件 (C++/WinRT) 專案範本建立新專案。 這些應用程式類型已經有套件識別資料

如果 API 位在 Windows 命名空間中

這是取用 Windows 執行階段 API 最常見的案例。 對於中繼資料中定義的 Windows 命名空間的每種類型,C++/WinRT 定義 C++ 適用的對等項目 (稱為投影類型)。 投影類型有相同的完整名稱做為 Windows 類型,但它放在使用 C++ 語法的 C++ winrt 命名空間。 例如,將 Windows::Foundation::Uri 投影到 C++/WinRT 做為 winrt::Windows::Foundation::Uri

以下是簡單的程式碼範例。 如果您想要將下列程式碼範例直接複製並貼到 Windows 主控台應用程式 (C++/WinRT) 專案的主要原始程式碼檔,請先在專案屬性中設定 [不使用預先編譯的標頭]

// 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 屬於 SDK 的一部分,可在資料夾 %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt\ 中找到。 資料夾中的標頭包含投影到 C++/WinRT 的 Windows 命名空間類型。 在此範例中,winrt/Windows.Foundation.h 包含 winrt::Windows::Foundation::Uri,其適用於執行階段類別 Windows::Foundation::Uri 的投影類型。

提示

每當您要使用來自 Windows 命名空間的類型時,請包含對應到該命名空間的 C++/WinRT 標頭。 using namespace 指示詞是選擇性的,但很便利。

上述的程式碼範例中,初始化 C++/WinRT 後,我們透過其公開記載於文件的建構函式之一 (在此範例中,Uri(String)) 堆疊配置 winrt::Windows::Foundation::Uri 的值投影類型。 針對這點,最常見的使用案例就是您通常需要執行的。 一旦您取得 C++/WinRT 投影類型值,您可以將其視為如同實際 Windows 執行階段類型的執行個體,因為其有相同的成員。

事實上,該投影的值是 proxy;它基本上只是支援物件的智慧型指標。 投影值的建構函式呼叫 RoActivateInstance 來建立支援 Windows 執行階段類別的執行個體 (此案例中為 Windows.Foundation.Uri),並在新投影值中儲存該物件的預設介面。 如下所示,您投影值成員的呼叫透過智慧型指標確實委派至支援物件;其為狀態發生變更的位置。

投影 Windows::Foundation::Uri 類型

contosoUri 值超出範圍時,會解構並將其參考資料發行至預設介面。 如果該參考是支援 Windows 執行階段 Windows.Foundation.Uri 物件的最後一個參考,則支援物件也會解構。

提示

投影類型是透過 Windows 執行階段 類型的包裝函式,用於取用其 API。 例如,投影介面是 Windows 執行階段 介面的包裝函式。

C++/WinRT 投影標頭

若要從 C++/WinRT 取用 Windows 命名空間 API,請包含 %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt 資料夾的標頭。 您必須包含對應至所使用之每個命名空間的標頭。

例如,針對 Windows::Security::Cryptography::Certificates 命名空間,對等項目 C++/WinRT 類型定義位於 winrt/Windows.Security.Cryptography.Certificates.h。 包含該標頭可讓您存取 Windows::Security::Cryptography::Certificates 命名空間中的所有類型。

有時候,一個命名空間標頭會包含相關命名空間標頭的一部分,但您不應該依賴此實作詳細數據。 明確包含您使用之命名空間的標頭。

例如,Certificate::GetCertificateBlob 方法會傳回 Windows::儲存體::Streams::IBuffer 介面。 呼叫 Certificate::GetCertificateBlob 方法之前,您必須包含winrt/Windows.Storage.Streams.h命名空間頭檔,以確保您可以在傳回的 Windows::儲存體::Streams::IBuffer 上接收及操作。

在使用該命名空間中的類型之前,忘記包含必要的命名空間標頭是建置錯誤的常見來源。

透過物件、透過介面,或透過 ABI 存取成員

使用 C++/WinRT 投影,Windows 執行階段類別的執行階段呈現方式是不超過基礎 ABI 介面。 不過,為了方便使用,您可以按照作者希望的方式對類別撰寫程式碼。 例如,您可以呼叫 UriToString 方法,就像類別的方法一樣 (事實上,藉由此做法,這是一種在個別 IStringable 介面上的方法)。

WINRT_ASSERT 是巨集定義,而且會發展為 _ASSERTE

Uri contosoUri{ L"http://www.contoso.com" };
WINRT_ASSERT(contosoUri.ToString() == L"http://www.contoso.com/"); // QueryInterface is called at this point.

透過查詢適當的介面以實現此便利。 但是,您隨時可以控制。 您可以透過自我擷取 IStringable 介面並直接使用,來選擇放棄一點方便以獲得一點效能。 在下列程式碼範例中,您可以在執行階段取得實際 IStringable 介面指標 (透過一次性查詢)。 之後,您對 ToString 的呼叫是直接的,並且可避免任何進一步呼叫 QueryInterface

...
IStringable stringable = contosoUri; // One-off QueryInterface.
WINRT_ASSERT(stringable.ToString() == L"http://www.contoso.com/");

如果您知道會在相同的介面呼叫數種方法,可以選擇這個技巧。

順帶一提,如果您想要存取 ABI 層級的成員,您便可以這麼做。 下列程式碼範例顯示執行的方式,而且 C++/WinRT 和 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 中,每個投影類型都有特殊的 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++/WinRT) 專案。

std::map<int, TextBlock> lookup;
lookup[2] = value;

此指派會建立新的 TextBlock,然後立即使用 value 覆寫。 解決方式如下。

std::map<int, TextBlock> lookup;
lookup.insert_or_assign(2, value);

另請參閱預設建構函式對於集合的影響

不要意外造成延遲初始化

請小心,不要意外叫用 std::nullptr_t 建構函式。 編譯器的衝突解決方法能透過處理站建構函式對其提供協助。 例如,請考慮下列兩個執行階段類別定義。

// GiftBox.idl
runtimeclass GiftBox
{
    GiftBox();
}

// Gift.idl
runtimeclass Gift
{
    Gift(GiftBox giftBox); // You can create a gift inside a box.
}

假設我們要建構不在盒子中的 Gift (以未初始化 GiftBox 建構的 Gift)。 首先,讓我們看看「錯誤」的方法。 我們知道有一個採用 GiftBoxGift 建構函式。 但是,如果我們想要傳遞 null 的 GiftBox (透過我們下方所執行的統一初始化叫用 Gift 建構函式),則我們「不會」得到想要的結果。

// 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) };

如果 API 是在 Windows 執行階段元件中實作

不論您是自己撰寫元件,或由廠商提供,本節皆適用。

注意

如需安裝和使用 C++/WinRT Visual Studio 延伸模組 (VSIX) 與 NuGet 套件 (一起提供專案範本和建置支援) 的資訊,請參閱 C++/WinRT 的 Visual Studio 支援

在您的應用程式專案中,參考 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 中撰寫事件

如果 API 是在取用的專案中實作

本節的程式碼範例取自 XAML 控制項;繫結至 C++/WinRT 屬性主題。 如需更多詳細資料、程式碼,以及在在實作執行階段類別的同一專案中使用該類別的逐步解說,也請參閱該主題。

從 XAML UI 使用的類型必須是執行階段類別,即使它與 XAML 位於相同專案中。 針對這個案例,您可以從執行階段類別的 Windows 執行階段中繼資料 (.winmd) 產生投影類型。 同樣地,包含標頭,但接下來您可以選擇使用 C++/WinRT 1.0 版或 2.0 版的方法來建構執行階段類別的執行個體。 1.0 版方法使用 winrt::make;2.0 版方法則為「統一建構」。 讓我們分別說明。

使用 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.cppMainPage 建構函式中,不需要程式碼 m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();

如需統一建構的詳細資訊和程式碼範例,請參閱加入統一建構和直接實作存取

具現化並傳回投影類型與介面

以下是在您使用專案中的投影類型和介面可能會有的外觀範例。 請記住,投影的類型 (例如此範例中的類型) 是由工具產生,而不是您自己撰寫的。

struct MyRuntimeClass : MyProject::IMyRuntimeClass, impl::require<MyRuntimeClass,
    Windows::Foundation::IStringable, Windows::Foundation::IClosable>

MyRuntimeClass 是投影類型;投影介面包含 IMyRuntimeClassIStringable,以及 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();
}

啟用 Factory

建立 C++/WinRT 物件的簡便方法如下。

using namespace winrt::Windows::Globalization::NumberFormatting;
...
CurrencyFormatter currency{ L"USD" };

但有時,您可能會想要自己建立啟用 Factory,並在適當時從中建立物件。 以下範例示範如何使用 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::CollapsedVisibility 後面加上 ::,因此會忽略方法名稱,而編譯器會找到列舉類別。

重要 API