Consumir APIs com C++/WinRT

Este tópico mostra como consumir APIs C++/WinRT, sejam elas parte de Windows, implementadas por um fornecedor de componentes de terceiros ou implementadas por você mesmo.

Important

Para que os exemplos de código neste tópico sejam curtos e fáceis de experimentar, você pode reproduzi-los criando um novo projeto Windows Aplicativo de Console (C++/WinRT) e copiando código. No entanto, você não pode consumir tipos de Windows Runtime personalizados arbitrários (de terceiros) de um aplicativo não empacotado como esse. Você pode consumir apenas tipos do Windows dessa maneira.

Para consumir tipos de Windows Runtime personalizados (de terceiros) de um aplicativo de console, você precisará fornecer ao aplicativo uma identidade de pacote para que ele possa resolver o registro dos tipos personalizados consumidos. Para obter mais informações, consulte Projeto de Empacotamento de Aplicativos do Windows.

Como alternativa, crie um novo projeto com base nos modelos de projeto Aplicativo em Branco, Empacotado (WinUI 3 na Área de Trabalho) para C++ ou componente de Windows Runtime (C++/WinRT). Esses tipos de aplicativo já têm uma identidade de pacote.

Se a API estiver em um namespace Windows

Esse é o caso mais comum em que você consumirá uma API Windows Runtime. Para cada tipo em um namespace Windows definido em metadados, C++/WinRT define um equivalente C++amigável (chamado de tipo projetado). Um tipo projetado tem o mesmo nome totalmente qualificado que o tipo de Windows, mas é colocado no namespace winrt do C++ usando a sintaxe C++. Por exemplo, Windows::Foundation::Uri é projetado em C++/WinRT como winrt::Windows::Foundation::Uri.

Aqui está um exemplo de código simples. Se você quiser copiar e colar os exemplos de código a seguir diretamente no arquivo de código-fonte principal de um projeto do Windows Console Application (C++/WinRT), primeiro defina Não Usando Cabeçalhos Pré-compilados nas propriedades do projeto.

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

O cabeçalho winrt/Windows.Foundation.h incluído faz parte do SDK, encontrado dentro da pasta %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt\. Os cabeçalhos nessa pasta contêm tipos de namespace do Windows projetados para C++/WinRT. Neste exemplo, winrt/Windows.Foundation.h contém winrt::Windows::Foundation::Uri, que é o tipo projetado para a classe de runtime Windows::Foundation::Uri.

Tip

Sempre que você quiser usar um tipo de um namespace Windows, inclua o cabeçalho C++/WinRT correspondente a esse namespace. As using namespace diretivas são opcionais, mas convenientes.

No exemplo de código acima, após a inicialização do C++/WinRT, alocamos um valor do tipo projetado winrt::Windows::Foundation::Uri por meio de um de seus construtores documentados publicamente (Uri(String), neste exemplo). Para este caso de uso mais comum, normalmente isso é tudo o que você precisa fazer. Depois de ter um valor de tipo projetado em C++/WinRT, você poderá tratá-lo como se fosse uma instância do tipo Windows Runtime real, já que ele tem todos os mesmos membros.

Na verdade, esse valor projetado é um proxy; é essencialmente apenas um ponteiro inteligente para um objeto de backup. Os construtores do valor projetado chamam RoActivateInstance para criar uma instância da classe de Windows Runtime de suporte (Windows). Foundation.Uri, nesse caso), e armazene a interface padrão desse objeto dentro do novo valor projetado. Conforme ilustrado abaixo, suas chamadas aos membros do valor projetado na verdade delegam, por meio do ponteiro inteligente, ao objeto subjacente, que é onde ocorrem as mudanças de estado.

O tipo Windows::Foundation::Uri projetado

Quando o valor contosoUri sai do escopo, ele é destruído e libera sua referência à interface padrão. Se essa referência for a última referência ao objeto subjacente Windows.Foundation.Uri do Windows Runtime, o objeto subjacente também será destruído.

Tip

Um tipo projetado é um encapsulamento de um tipo do Windows Runtime para consumir suas APIs. Por exemplo, uma interface projetada é um wrapper sobre uma interface do Windows Runtime.

Cabeçalhos de projeção do C++/WinRT

Para consumir APIs do namespace Windows a partir de C++/WinRT, você inclui os arquivos de cabeçalho da pasta %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt. Você deve incluir os cabeçalhos correspondentes a cada namespace usado.

Por exemplo, para o namespace Windows::Security::Cryptography::Certificates, as definições de tipo C++/WinRT equivalentes residem em winrt/Windows.Security.Cryptography.Certificates.h. A inclusão desse cabeçalho fornece acesso a todos os tipos no namespace Windows::Security::Cryptography::Certificates.

Às vezes, um cabeçalho de namespace incluirá partes de cabeçalhos de namespace relacionados, mas você não deve contar com esses detalhes de implementação. Inclua explicitamente os cabeçalhos para os namespaces que você usa.

Por exemplo, o método Certificate::GetCertificateBlob retorna uma interface Windows::Storage::Streams::IBuffer. Antes de chamar o método Certificate::GetCertificateBlob, você deve incluir o arquivo de cabeçalho do namespace winrt/Windows.Storage.Streams.h para garantir que seja possível receber e manipular o Windows::Storage::Streams::IBuffer retornado.

Esquecer de incluir os cabeçalhos de namespace necessários antes de usar tipos nesse namespace é uma fonte comum de erros de build.

Acessando membros por meio do objeto, por meio de uma interface ou por meio da ABI

Com a projeção C++/WinRT, a representação de runtime de uma classe Windows Runtime não é mais do que as interfaces ABI subjacentes. Mas, para sua conveniência, você pode programar usando classes da maneira como seus autores pretendiam. Por exemplo, você pode chamar o método ToString de uma Uri como se fosse um método da classe (na verdade, nos bastidores, ele é um método da interface IStringable separada).

WINRT_ASSERT é uma definição de macro e se expande para _ASSERTE.

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

Essa conveniência é obtida por meio de uma consulta para a interface apropriada. Mas você está sempre no controle. Você pode optar por abrir mão de um pouco dessa conveniência em troca de um pouco mais de desempenho, ao recuperar a interface IStringable por conta própria e usá-la diretamente. No exemplo de código abaixo, você obtém um ponteiro válido para a interface IStringable em tempo de execução (por meio de uma única consulta). Depois disso, sua chamada para ToString é direta e evita qualquer chamada adicional para QueryInterface.

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

Você poderá escolher essa técnica se souber que chamará vários métodos na mesma interface.

Aliás, se você quiser acessar membros no nível da ABI, poderá. O exemplo de código abaixo mostra como e há mais detalhes e exemplos de código na interoperabilidade entre C++/WinRT e a 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.
}

Inicialização atrasada

No C++/WinRT, cada tipo projetado tem um construtor especial de C++/WinRT std::nullptr_t. Com exceção daquele, todos os construtores de tipo projetado — incluindo o construtor padrão — fazem com que um objeto subjacente do Windows Runtime seja criado e fornecem a você um ponteiro inteligente para ele. Portanto, essa regra se aplica em qualquer lugar em que o construtor padrão seja usado, como variáveis locais não inicializadas, variáveis globais não inicializadas e variáveis de membro não inicializadas.

Se, por outro lado, você quiser construir uma variável de um tipo projetado sem que ele, por sua vez, construa um objeto de Windows Runtime de suporte (para que você possa atrasar esse trabalho até mais tarde), então você pode fazer isso. Declare sua variável ou campo usando esse construtor C++/WinRT std::nullptr_t especial (que a projeção C++/WinRT injeta em cada classe de runtime). Usamos esse construtor especial com m_gamerPicBuffer no exemplo de código abaixo.

#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();
}

Todos os construtores no tipo projetado, exceto o construtor std::nullptr_t, fazem com que um objeto Windows Runtime de suporte seja criado. O construtor std::nullptr_t é essencialmente um no-op. Ele espera que o objeto projetado seja inicializado em um momento subsequente. Portanto, se uma classe de runtime tem um construtor padrão ou não, você pode usar essa técnica para inicialização atrasada eficiente.

Essa consideração afeta outros locais em que você está invocando o construtor padrão, como em vetores e mapas. Considere este exemplo de código, para o qual você precisará de um aplicativo em branco, empacotado (WinUI 3 na Área de Trabalho) para o projeto C++.

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

A atribuição cria um novo TextBlock e, em seguida, substitui-o imediatamente com value. Aqui está o remédio.

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

Veja também como o construtor padrão afeta as coleções.

Não use inicialização tardia por engano

Tenha cuidado para não invocar o construtor std::nullptr_t por engano. A resolução de conflitos do compilador favorece-a sobre os construtores de fábrica. Por exemplo, considere essas duas definições de classe de runtime.

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

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

Digamos que queremos construir um Gift que não esteja em uma caixa (um Gift que foi construído com uma GiftBox não inicializada). Primeiro, vamos ver a maneira errada de fazer isso. Sabemos que existe um construtor Gift que recebe uma GiftBox. Mas se estivermos tentados a passar uma GiftBox nula (invocando o construtor Gift por meio da inicialização uniforme, como fazemos abaixo), então não obteremos o resultado desejado.

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

O que você recebe aqui é um Gift não inicializado. Você não recebe um Gift com um GiftBox não inicializado. Esta é a maneira correta de fazer isso.

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

No exemplo incorreto, passar um literal nullptr faz com que a resolução ocorra em favor do construtor com inicialização adiada. Para resolver a favor do construtor de fábrica, o tipo do parâmetro deve ser um GiftBox. Você ainda tem a opção de passar um GiftBox com inicialização explicitamente adiada, conforme mostrado no exemplo correto.

Este próximo exemplo também está correto, pois o parâmetro tem o tipo GiftBox e não std::nullptr_t.

GiftBox giftBox{ nullptr };
Gift gift{ giftBox }; // Calls factory constructor.

Só quando você passa um nullptr literal é que a ambiguidade surge.

Não copie a construção por engano.

Essa cautela é semelhante à descrita na seção Não atrasar a inicialização por engano acima.

Além do construtor de inicialização adiada, a projeção C++/WinRT também injeta um construtor de cópia em cada classe de tempo de execução. É um construtor de parâmetro único que aceita o mesmo tipo que o objeto que está sendo construído. O ponteiro inteligente resultante aponta para o mesmo objeto subjacente do Windows Runtime para o qual seu parâmetro de construtor aponta. O resultado é dois objetos de ponteiro inteligente apontando para o mesmo objeto subjacente.

Aqui está uma definição de classe de runtime que usaremos nos exemplos de código.

// GiftBox.idl
runtimeclass GiftBox
{
    GiftBox(GiftBox biggerBox); // You can place a box inside a bigger box.
}

Digamos que queremos construir uma GiftBox dentro de uma Caixa de Presente maior.

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

A maneira correta de fazer isso é chamar a fábrica de ativação explicitamente.

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

Se a API for implementada em um componente do Windows Runtime

Esta seção se aplica se você mesmo criou o componente ou se ele veio de um fornecedor.

Note

Para obter informações sobre como instalar e usar o VSIX (Extensão de Visual Studio) do C++/WinRT e o pacote NuGet (que juntos fornecem suporte ao modelo de projeto e ao build), consulte Visual Studio suporte para C++/WinRT.

Em seu projeto de aplicativo, faça referência ao arquivo de metadados Windows Runtime (.winmd) do componente Windows Runtime e ao build. Durante a compilação, a ferramenta cppwinrt.exe gera uma biblioteca C++ padrão que descreve totalmente — ou projeta — a superfície de API do componente. Em outras palavras, a biblioteca gerada contém os tipos projetados para o componente.

Em seguida, assim como para um tipo de namespace Windows, você inclui um cabeçalho e constrói o tipo projetado por meio de um de seus construtores. O código de inicialização do projeto de aplicativo registra a classe de runtime e o construtor do tipo projetado chama RoActivateInstance para ativar a classe de runtime do componente referenciado.

#include <winrt/ThermometerWRC.h>

struct App : AppT<App>
{
    ThermometerWRC::Thermometer thermometer;
    ...
};

Para obter mais detalhes, código e um passo a passo sobre como consumir APIs implementadas em um componente do Windows Runtime, consulte Componentes do Windows Runtime com C++/WinRT e Criar eventos em C++/WinRT.

Se a API for implementada no projeto de consumo

O exemplo de código nesta seção foi extraído do tópico Controles XAML; associar a uma propriedade C++/WinRT. Consulte esse tópico para obter mais detalhes, código e um passo a passo do consumo de uma classe de runtime implementada no mesmo projeto que o consome.

Um tipo consumido da interface do usuário XAML deve ser uma classe de runtime, mesmo que esteja no mesmo projeto que o XAML. Para esse cenário, você gera um tipo projetado com base nos metadados de Windows Runtime da classe de runtime (.winmd). Novamente, você inclui um cabeçalho, mas tem uma opção entre as maneiras C++/WinRT versão 1.0 ou versão 2.0 de construir a instância da classe de runtime. O método versão 1.0 usa winrt::make; o método versão 2.0 é conhecido como construção uniforme. Vamos examinar cada um por sua vez.

Construindo usando winrt::make

Vamos começar com o método padrão (C++/WinRT versão 1.0), pois é uma boa ideia estar pelo menos familiarizado com esse padrão. Você constrói o tipo projetado por meio de seu construtor std::nullptr_t . Esse construtor não executa nenhuma inicialização, portanto, você deve atribuir um valor à instância por meio da função auxiliar winrt::make , passando os argumentos de construtor necessários. Uma classe de runtime implementada no mesmo projeto que o código consumidor não precisa ser registrada nem instanciada via ativação do Windows Runtime/COM.

Consulte controles XAML; associe a uma propriedade C++/WinRT para obter um passo a passo completo. Esta seção mostra os extratos desse passo a passo.

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

Construção uniforme

Com o C++/WinRT versão 2.0 e posterior, há uma forma otimizada de construção disponível para você conhecida como construção uniforme (consulte Notícias e alterações, em C++/WinRT 2.0).

Consulte controles XAML; associe-os a uma propriedade C++/WinRT para obter um passo a passo completo. Esta seção mostra os extratos desse passo a passo.

Para usar a construção uniforme em vez de winrt::make, será necessária uma fábrica de ativação. Uma boa maneira de gerar um deles é adicionar um construtor à sua IDL.

// MainPage.idl
import "BookstoreViewModel.idl";
namespace Bookstore
{
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
        BookstoreViewModel MainViewModel{ get; };
    }
}

Em seguida, em MainPage.h declarar e inicializar m_mainViewModel em apenas uma etapa, conforme mostrado abaixo.

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

E então, no construtor MainPage em MainPage.cpp, não há necessidade de usar o código m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();.

Para obter mais informações sobre construção uniforme e exemplos de código, consulte Aceitar a construção uniforme e acesso direto à implementação.

Instanciando e retornando tipos e interfaces projetados

Aqui está um exemplo de como os tipos e interfaces projetados podem se parecer em seu projeto de consumo. Lembre-se de que um tipo projetado (como o deste exemplo) é gerado por ferramentas e não é algo que você mesmo criaria.

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

MyRuntimeClass é um tipo projetado; As interfaces projetadas incluem IMyRuntimeClass, IStringable e IClosable. Este tópico apresentou as diferentes maneiras pelas quais você pode instanciar um tipo projetado. Aqui está um lembrete e resumo, usando MyRuntimeClass como exemplo.

// 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>();
  • Você pode acessar os membros de todas as interfaces de um tipo projetado.
  • Você pode retornar um tipo projetado para um chamador.
  • Os tipos e interfaces projetados derivam de winrt::Windows::Foundation::IUnknown. Portanto, você pode chamar IUnknown::as em um tipo ou interface projetado para consultar outras interfaces projetadas, que você também pode usar ou retornar a um chamador. A função as member funciona como QueryInterface.
void f(MyProject::MyRuntimeClass const& myrc)
{
    myrc.ToString();
    myrc.Close();
    IClosable iclosable = myrc.as<IClosable>();
    iclosable.Close();
}

Fábricas de ativadores

A maneira conveniente e direta de criar um objeto C++/WinRT é a seguinte.

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

Mas pode haver momentos em que você deseja criar a fábrica de ativação por conta própria e, em seguida, criar objetos a partir dela à sua conveniência. Aqui estão alguns exemplos mostrando como usar o modelo de função 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");

As classes nos dois exemplos acima são tipos de um namespace Windows. Neste próximo exemplo, ThermometerWRC::Thermometer é um tipo personalizado implementado em um componente Windows Runtime.

auto factory = winrt::get_activation_factory<ThermometerWRC::Thermometer>();
ThermometerWRC::Thermometer thermometer = factory.ActivateInstance<ThermometerWRC::Thermometer>();

Ambiguidades de membro/tipo

Quando uma função membro tem o mesmo nome que um tipo, há ambiguidade. As regras de busca de nomes não qualificados do C++ nas funções-membro fazem com que a busca ocorra na classe antes de ocorrer nos espaços de nomes. A regra falha de substituição não é um erro (SFINAE) não se aplica (ela se aplica durante a resolução de sobrecarga de templates de função). Portanto, se o nome dentro da classe não fizer sentido, o compilador não continuará procurando uma correspondência melhor, ele simplesmente relatará um erro.

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

Acima, o compilador considera que você está passando FrameworkElement.Style() (que, em C++/WinRT, é uma função membro) como o parâmetro de modelo para IUnknown::as. A solução é forçar o nome Style a ser interpretado como o tipo 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>();
    }
}

A busca não qualificada por nome tem uma exceção especial no caso em que o nome é seguido por ::, caso em que ignora funções, variáveis e valores de enum. Isso permite que você faça coisas assim.

struct MyPage : Page
{
    void DoSomething()
    {
        Visibility(Visibility::Collapsed); // No ambiguity here (special exception).
    }
}

A chamada para Visibility() é resolvida como o nome da função membro UIElement.Visibility. Mas o parâmetro Visibility::Collapsed segue a palavra Visibility com ::, e assim o nome do método é ignorado e o compilador localiza a classe enumerada.

APIs importantes