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

В этом разделе показано, как использовать API C++/WinRT, будь то часть Windows, реализованная сторонним поставщиком компонентов или реализованная самостоятельно.

Это важно

Чтобы примеры кода в этой теме были короткими и их было легко опробовать, вы можете воспроизвести их, создав новый проект Windows Console Application (C++/WinRT), а затем скопировав и вставив код. Однако в непакованном приложении нельзя таким образом использовать произвольные пользовательские (сторонние) типы среда выполнения Windows. Таким образом можно использовать только типы Windows.

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

В качестве альтернативы создайте новый проект на основе шаблонов проектов «Пустое упакованное приложение (WinUI 3 в классическом приложении)» для C++ или «среда выполнения Windows Component (C++/WinRT)». Эти типы приложений уже имеют удостоверение пакета.

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

Это наиболее распространенный случай, в котором вы будете использовать API среда выполнения Windows. Для каждого типа в пространстве имен Windows, определенном в метаданных, C++/WinRT определяет эквивалент C++, понятный для C++(называемый проецируемым типом). Проецируемый тип имеет то же полное имя, что и тип Windows, но он помещается в пространство имен winrt C++ с помощью синтаксиса C++. Например, Windows::Foundation::Uri проектируется в C++/WinRT как winrt::Windows::Foundation::Uri.

Ниже приведен простой пример кода. Если вы хотите напрямую скопировать и вставить следующие примеры кода в основной файл исходного кода проекта Windows Console Application (C++/WinRT), сначала задайте в свойствах проекта параметр Not Using Precompiled Headers.

// 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 Windows.Foundation.Uri, то базовый объект также уничтожается.

Подсказка

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

Заголовки проекции C++/WinRT

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

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

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

Например, метод Certificate::GetCertificateBlob возвращает интерфейс Windows::Storage::Streams::IBuffer. Перед вызовом метода Certificate::GetCertificateBlob необходимо подключить файл заголовка пространства имен winrt/Windows.Storage.Streams.h, чтобы можно было получить возвращаемый Windows::Storage::Streams::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 по сути является no-op. Он ожидает, что проектируемый объект будет инициализирован в последующее время. Таким образом, имеет ли класс среды выполнения конструктор по умолчанию или нет, этот метод можно использовать для эффективной отложенной инициализации.

Это касается других мест, где вы вызываете конструктор по умолчанию, например в векторах и картах. Рассмотрим этот пример кода; для него вам понадобится проект C++ Blank App, Packaged (WinUI 3 in Desktop).

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. Но если у нас возникнет соблазн передать 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) };

То, что вы получаете здесь, является неинициализированный подарок. Вы не получите подарок при использовании неинициализированного 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

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

Note

Сведения об установке и использовании расширения C++/WinRT Visual Studio (VSIX) и пакета NuGet (которые вместе предоставляют поддержку шаблона проекта и сборки), см. Visual Studio поддержку C++/WinRT.

В проекте приложения добавьте ссылку на файл метаданных среда выполнения Windows (.winmd) компонента среда выполнения Windows, а затем выполните сборку. Во время сборки средство cppwinrt.exe создает стандартную библиотеку C++, которая полностью описывает — или представляет — поверхность API компонента. Другими словами, созданная библиотека содержит проецируемые типы для компонента.

Затем, как и для типа пространства имен Windows, вы включаете заголовок и создаете проецируемый тип с помощью одного из его конструкторов. Код запуска проекта приложения регистрирует класс среды выполнения, а конструктор проецируемого типа вызывает RoActivateInstance для активации класса среды выполнения из указанного компонента.

#include <winrt/ThermometerWRC.h>

struct App : AppT<App>
{
    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 helper, передав все необходимые аргументы конструктора. Класс среды выполнения, реализованный в том же проекте, что и потребляемый код, не должен быть зарегистрирован или создан с помощью активации среда выполнения Windows/COM.

См. элементы управления XAML с привязкой к свойству C++/WinRT для подробного пошагового руководства. В этом разделе показано извлечение из этого пошагового руководства.

// MainPage.idl
import "BookstoreViewModel.idl";
namespace Bookstore
{
    runtimeclass MainPage : Microsoft.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 : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
        BookstoreViewModel MainViewModel{ get; };
    }
}

Затем в MainPage.h объявите и инициализируйте m_mainViewModel всего за один шаг, как показано ниже.

// MainPage.h
...
struct MainPage : MainPageT<MainPage>
{
    ...
    private:
        Bookstore::BookstoreViewModel m_mainViewModel;
        ...
    };
}
...

А затем в конструкторе 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 как типа Microsoft::UI::Xaml::Style.

struct MyPage : Page
{
    void DoWork()
    {
        // One option is to fully-qualify it.
        auto style{ Application::Current().Resources().
            Lookup(L"MyStyle").as<Microsoft::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 Microsoft::UI::Xaml namespace when you did
        // "using namespace Microsoft::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