Использование интерфейсов API с помощью C++/WinRT

В этом разделе показано, как использовать API-интерфейсы C++/WinRT, если они являются компонентом Windows, реализованы сторонним поставщиком компонентов или вами самостоятельно.

Внимание

Примеры кода в этом разделе компактны и просты. Вы можете воспроизвести их, создав проект консольного приложения Windows (C++/WinRT) и скопировав и вставив в него код. Но вы не можете использовать произвольные пользовательские (сторонние) типы среды выполнения Windows из такого неупакованного приложения. Оно позволяет использовать только типы Windows.

Чтобы использовать пользовательские (сторонние) типы среды выполнения Windows из консольного приложения, ему необходимо предоставить идентификатор пакета, чтобы оно могло разрешить регистрацию пользовательских типов. Дополнительные сведения см. в статье о проект упаковки приложений Windows.

Вы также можете создать проект из шаблонов для пустого приложения (C++/WinRT), приложения основных компонентов (C++/WinRT) или компонента среды выполнения Windows (C++/WinRT). Приложения таких типов уже включают идентификатор пакета.

Если API находится в пространстве имен Windows

Это наиболее распространенный случай, в котором вы будете использовать API среды выполнения Windows. Для каждого типа в пространстве имен Windows, указанного в метаданных, C++/WinRT определяет подходящий для C++ эквивалент (называемый проецируемый тип). Проецируемый тип имеет то же полное доменное имя, что и тип в Windows, но он размещается в пространстве имен C++ winrt с использованием синтаксиса C++. Например, 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\. Заголовки в этой папке содержат типы пространства имен Windows, проецируемые в C++/WinRT. В этом примере winrt/Windows.Foundation.h содержит winrt::Windows::Foundation::Uri, который представляет собой проецируемый тип для класса среды выполнения Windows::Foundation::Uri.

Совет

Каждый раз, когда вы хотите использовать тип из пространства имен Windows, включайте заголовок C++/WinRT, соответствующий этому пространству имен. Директивы using namespace являются необязательными, но удобными.

В примере кода выше после инициализации C++/WinRT значение проецируемого типа winrt::Windows::Foundation::Uri помещается в стек с помощью одного из его задокументированных конструкторов (в этом примере — Uri(String)). Для данного наиболее распространенного случая использования обычно это все, что нужно сделать. Когда у вас есть значение проецируемого типа C++/WinRT, вы можете работать с ним, как будто это экземпляр фактического типа среды выполнения Windows, поскольку он имеет те же члены.

На самом деле это проецируемое значение является своего рода псевдонимом; по сути это просто смарт-указатель на расположенный за ним объект. Конструктор(ы) проецируемого значения вызывает RoActivateInstance для создания экземпляра опорного класса среды выполнения Windows (в этом случае — Windows.Foundation.Uri) и сохранения интерфейса по умолчанию этого объекта внутри нового проецируемого значения. Как показано ниже, ваши вызовы членов проецируемого значения фактически делегируются, с помощью смарт-указателя, на стоящий за ним объект, и в нем и происходят изменения состояния.

Проецируемый тип Windows::Foundation::Uri

Когда значение contosoUri выходит за пределы области, оно разрушается и убирает ссылку на интерфейс по умолчанию. Если эта ссылка является последней ссылкой на опорный объект Windows.Foundation.Uri среды выполнения Windows, этот опорный объект также разрушается.

Совет

Проецируемый тип — это оболочка над типом среда выполнения Windows для использования api-интерфейсов. Например, проецируемый интерфейс — это оболочка через интерфейс среда выполнения Windows.

Проецируемые заголовки C++/WinRT

Для использования API-интерфейсов пространства имен Windows из C++/WinRT требуется включить заголовки из папки %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt. Необходимо включить заголовки, соответствующие каждому используемому пространству имен.

Например, для пространства имен Windows::Security::Cryptography::Certificates эквивалентные определения типов C++/WinRT располагаются в winrt/Windows.Security.Cryptography.Certificates.h. Включая этот заголовок, предоставляет доступ ко всем типам в пространстве имен Windows::Security::Cryptography::Certificates .

Иногда один заголовок пространства имен будет включать части связанных заголовков пространства имен, но не следует полагаться на эти сведения о реализации. Явно включите заголовки для используемых пространств имен.

Например, метод Certificate::GetCertificateBlob возвращает интерфейс Windows::служба хранилища::Потоки::IBuffer. Перед вызовом метода Certificate::GetCertificateBlob необходимо включить winrt/Windows.Storage.Streams.h файл заголовка пространства имен, чтобы убедиться, что вы можете получать и работать с возвращаемым Windows::служба хранилища::Потоки::IBuffer.

Забыв включить необходимые заголовки пространства имен перед использованием типов в этом пространстве имен, является общим источником ошибок сборки.

Доступ к членам через объект, через интерфейс или с помощью ABI

С помощью проекции C++/WinRT представление времени выполнения для класса среды выполнения Windows представляет собой не что иное, как лежащие в основе базовые интерфейсы ABI. Но для удобства можно использовать классы в коде так, как задумывалось их автором. Например, можно вызвать метод ToString для Uri, как если бы он был методом класса (на самом деле, по своей сути это метод отдельного интерфейса 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 (объект Gift, созданный с неинициализированным типом GiftBox). Сначала рассмотрим, как не нужно это делать. Мы знаем, что существует конструктор Gift, который принимает параметр GiftBox. Но если мы захотим передать параметру GiftBox значение NULL (вызвав конструктор 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, а не объект Gift с неинициализированным параметром GiftBox. Вот как правильно это сделать.

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

Дополнительные сведения, код и пошаговое руководство по потреблению API-интерфейсов, реализованных в компоненте среды выполнения Windows, см. в статьях Создание компонентов среды выполнения Windows с помощью C++/WinRT и Создание событий в C++/WinRT.

Если API-интерфейс реализован в использующем проекте

Пример кода в этом разделе приведен из раздела Элементы управления XAML; привязка к свойству C++/WinRT. В этом разделе вы также найдете дополнительные сведения, код и пошаговое руководство по использованию класса среды выполнения, реализованного в использующем его проекте.

Тип, используемый из пользовательского интерфейса XAML, должен являться классом среды выполнения, даже если он находится в том же проекте, что и 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 в файле MainPage.cpp не требуется указывать код m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();.

Дополнительные сведения об универсальном создании и примеры кода см. в статье о предоставлении согласия на использование универсального создания и прямого обращения к реализации.

Создание и возврат прогнозируемых типов и интерфейсов

Вот пример того, как проецируемые типы и интерфейсы могут выглядеть в вашем использующем их проекте. Помните, что проецируемых типы (например, указанному в этом примере), формируемые инструментами и не будут созданы самостоятельно.

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

MyRuntimeClass является проецируемым типом; проецируемые интерфейсы включают IMyRuntimeClass, IStringable и 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();
}

Фабрики активации

Удобный прямолинейный способ создания объекта C++/WinRT выглядит следующим образом.

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

Но бывают ситуации, в которых вам потребуется создать фабрику активации самостоятельно, а затем создавать объекты из нее по мере необходимости. Вот несколько примеров, показывающих использование шаблона функции 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::Collapsed после Visibility стоит ::, поэтому имя метода игнорируется, и компилятор находит класс перечисления.

Важные API