Consumir APIs com C++/WinRT

Este tópico mostra como consumir APIs C++/WinRT, quer façam parte do Windows, sejam implementadas por um fornecedor de componentes terceiros ou implementadas por si próprio.

Important

Para que os exemplos de código neste tópico sejam curtos e fáceis de experimentar, pode reproduzi-los criando um novo projeto de Aplicação de Consola Windows (C++/WinRT) e copiando-colando código. No entanto, não podes utilizar tipos personalizados arbitrários do Windows Runtime (de terceiros) a partir de uma aplicação não empacotada dessa forma. Dessa forma, só podes consumir tipos de Windows.

Para consumir tipos de Windows Runtime personalizados (de terceiros) de uma aplicação de consola, terá de atribuir à aplicação uma identidade de pacote para que possa resolver o registo dos tipos personalizados consumidos. Para mais informações, consulte Windows Application Packaging Project.

Em alternativa, crie um novo projeto a partir dos modelos de projeto Blank App, Packaged (WinUI 3 para Ambiente de Trabalho) para C++ ou Windows Runtime Component (C++/WinRT). Esses tipos de aplicação já têm uma identidade de pacote.

Se a API estiver num namespace do Windows

Este é o caso mais comum em que vais consumir uma API do Windows Runtime. Para cada tipo num espaço de nomes Windows definido em metadados, C++/WinRT define um equivalente compatível com C++ (chamado tipo projetado). Um tipo projetado tem o mesmo nome totalmente qualificado que o tipo Windows, mas está colocado no namespace winrt C++ usando sintaxe C++. Por exemplo, o Windows::Foundation::Uri é projetado para C++/WinRT como winrt::Windows::Foundation::Uri.

Aqui está um exemplo simples de código. Se quiser copiar e colar os seguintes exemplos de código diretamente no ficheiro principal de código-fonte de um projeto de Aplicação de Consola Windows (C++/WinRT), então primeiro defina Não Usar 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 dessa pasta contêm tipos de namespace do Windows projetados em 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 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 inicializar o C++/WinRT, alocamos na stack um valor do tipo projetado winrt::Windows::Foundation::Uri, recorrendo a um dos seus construtores documentados publicamente (Uri(String), neste exemplo). Para este, o caso de uso mais comum, normalmente é tudo o que tens de fazer. Depois de ter um valor projetado de tipo C++/WinRT, pode tratá-lo como se fosse uma instância do tipo real de Windows Runtime, já que tem todos os mesmos membros.

Na verdade, esse valor projetado é um proxy; é essencialmente apenas um ponteiro inteligente para um objeto subjacente. O(s) construtor(es) do valor projetado chamam RoActivateInstance para criar uma instância da classe Windows Runtime de suporte (Windows. Foundation.Uri, neste caso), e armazenar a interface padrão desse objeto dentro do novo valor projetado. Conforme ilustrado abaixo, as suas chamadas aos membros do valor projetado são efetivamente delegadas, através do ponteiro inteligente, no objeto subjacente; é aí que ocorrem as alterações de estado.

O tipo projetado de Windows::Foundation::Uri

Quando o valor contosoUri fica fora de âmbito, é destruído e liberta a referência para a interface predefinida. Se essa referência for a última referência ao objeto Windows.Foundation.Uri subjacente do Windows Runtime, o objeto subjacente também é destruído.

Tip

Um tipo projetado é um invólucro de um tipo do Windows Runtime para permitir o consumo das respetivas APIs. Por exemplo, uma interface projetada é um wrapper sobre uma interface do Windows Runtime.

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

Para consumir APIs de namespace do Windows a partir de C++/WinRT, inclui cabeçalhos da %WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt pasta. Deve incluir os cabeçalhos correspondentes a cada namespace que utiliza.

Por exemplo, para o espaço de nomes Windows::Security::Cryptography::Certificates, as definições equivalentes de tipos C++/WinRT residem em winrt/Windows.Security.Cryptography.Certificates.h. Incluir esse cabeçalho dá-lhe acesso a todos os tipos no espaço de nomes Windows::Security::Cryptography::Certificates.

Por vezes, um cabeçalho de namespace inclui partes de cabeçalhos de namespace relacionados, mas não deve confiar neste detalhe de implementação. Inclua explicitamente os cabeçalhos dos namespaces que utiliza.

Por exemplo, o método Certificate::GetCertificateBlob devolve uma interface Windows::Storage::Streams::IBuffer. Antes de chamar o método Certificate::GetCertificateBlob, deve incluir o ficheiro de cabeçalho do espaço de nomes winrt/Windows.Storage.Streams.h para garantir que pode receber e manipular o Windows::Storage::Streams::IBuffer devolvido.

Esquecer-se de incluir os cabeçalhos de namespace exigidos antes de usar tipos nesse namespace é uma fonte comum de erros de build.

Aceder aos membros através do objeto, de uma interface ou do ABI

Com a projeção C++/WinRT, a representação em tempo de execução de uma classe Windows Runtime não passa das interfaces ABI subjacentes. Mas, para tua conveniência, podes programar contra as classes da forma que o autor pretendeu. Por exemplo, pode chamar o método ToString de uma Uri como se fosse um método da classe (na verdade, nos bastidores, é um método na interface separada IStringable).

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

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

Esta conveniência é alcançada através de uma consulta para a interface adequada. Mas tu tens sempre o controlo. Pode optar por ceder um pouco dessa conveniência em troca de um pouco de desempenho, obtendo a interface IStringable e usando-a diretamente. No exemplo de código abaixo, obtém um ponteiro para a interface IStringable durante a execução (através de uma única consulta). Depois disso, a sua chamada para o ToString é direta e evita qualquer chamada adicional para o QueryInterface.

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

Pode optar por esta técnica se souber que vai chamar vários métodos na mesma interface.

Aliás, se quiser aceder a membros ao nível da ABI, pode fazê-lo. O exemplo de código abaixo mostra como, e há mais detalhes e exemplos de código em Interop entre C++/WinRT e o 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

Em C++/WinRT, cada tipo projetado tem um construtor especial C++/WinRT std::nullptr_t . Com exceção desse, todos os construtores de tipos projetados — incluindo o construtor predefinido — criam um objeto subjacente do Windows Runtime e fornecem-lhe um ponteiro inteligente. Assim, essa regra aplica-se em qualquer lugar onde o construtor padrão seja usado, como variáveis locais não inicializadas, variáveis globais não inicializadas e variáveis membros não inicializadas.

Se, por outro lado, quiseres construir uma variável de um tipo projetado sem que ela tenha de construir um objeto Windows Runtime de suporte (para que possas adiar esse trabalho para mais tarde), então podes fazê-lo. Declara a tua variável ou campo usando aquele construtor especial C++/WinRT std::nullptr_t (que a projeção C++/WinRT injeta em todas as classes 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 do tipo projetado exceto o construtor std::nullptr_t levam à criação de um objeto subjacente do Windows Runtime. O construtor std::nullptr_t é essencialmente um no-op. Espera que o objeto projetado seja inicializado num momento subsequente. Portanto, quer uma classe em tempo de execução tenha um construtor predefinido ou não, pode utilizar esta técnica para uma inicialização diferida eficiente.

Esta consideração afeta outros locais onde está a invocar o construtor padrão, como em vetores e mapas. Considere este exemplo de código, para o qual vai precisar de uma Aplicação em Branco, Empacotada (WinUI 3 no Ambiente de Trabalho) para um projeto em C++.

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

A atribuição cria um novo TextBlock e, em seguida, substitui-o imediatamente por 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 atrase a inicialização por engano

Tem cuidado para não invocar o construtor std::nullptr_t por engano. A resolução de conflitos do compilador dá-lhe preferência em detrimento dos construtores de fábrica. Por exemplo, considere estas duas definições de classes de runtime.

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

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

Imaginemos que queremos construir um Presente que não está dentro de uma caixa (um Presente construído com uma Caixa de Presente não inicializada). Primeiro, vejamos a forma errada de o fazer. Sabemos que existe um construtor Gift que aceita um GiftBox. Mas se formos tentados a passar um GiftBox nulo (invocando o construtor Gift via inicialização uniforme, como fazemos abaixo), então não obteremos o resultado que queremos.

// 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 recebes aqui é um Presente não inicializado. Não recebes um Presente com uma GiftBox não inicializada. Aqui está a forma correta de o fazer.

// 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 o literal nullptr faz com que seja escolhido o construtor com inicialização diferida. Para resolver a favor do construtor de fábrica, o tipo do parâmetro deve ser um GiftBox. Ainda tens a opção de passar um GiftBox que inicializa explicitamente com atraso, como mostrado no exemplo correto.

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

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

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

Não copie ou construa por engano.

Este aviso é semelhante ao descrito na secção Não atrase a inicialização por erro acima.

Além do construtor de inicialização por atraso, a projeção C++/WinRT também injeta um construtor de cópias em cada classe de runtime. É um construtor de parâmetro único que aceita o mesmo tipo do objeto que está a ser construído. O ponteiro inteligente resultante aponta para o mesmo objeto subjacente do Windows Runtime que o apontado pelo parâmetro do construtor. O resultado são dois ponteiros inteligentes que apontam para o mesmo objeto subjacente.

Aqui está uma definição de classe em tempo de execução que vamos usar 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 Caixa de Presente 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 forma correta de fazê-lo é chamar explicitamente a fábrica de ativação.

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 estiver implementada num componente do Windows Runtime

Esta secção aplica-se quer tenha criado o componente pessoalmente, quer tenha vindo de um fornecedor.

Note

Para informações sobre a instalação e utilização da Extensão Visual Studio C++/WinRT (VSIX) e do pacote NuGet (que juntos fornecem suporte para templates de projeto e compilação), consulte o suporte Visual Studio para C++/WinRT.

No seu projeto de aplicação, consulte o ficheiro de metadados Windows Runtime (.winmd) do componente Windows Runtime e construa. Durante a compilação, a cppwinrt.exe ferramenta gera uma biblioteca padrão em C++ que descreve completamente—ou projeta—a superfície da API do componente. Ou seja, a biblioteca gerada contém os tipos projetados para o componente.

Depois, tal como para um tipo de namespace Windows, incluis um cabeçalho e constróis o tipo projetado através de um dos seus construtores. O código de arranque do seu projeto de aplicação regista a classe de runtime, e o construtor do tipo projetado chama o RoActivateInstance para ativar a classe de runtime a partir do componente referenciado.

#include <winrt/ThermometerWRC.h>

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

Para mais detalhes, código e uma explicação passo a passo sobre como consumir APIs implementadas num componente do Windows Runtime, consulte Componentes do Windows Runtime com C++/WinRT e Criar eventos em C++/WinRT.

Se a API estiver implementada no projeto que a consome

O exemplo de código nesta secção é retirado do tópico controlos XAML; vincular a uma propriedade C++/WinRT. Veja esse tópico para mais detalhes, código e um guia sobre como consumir uma classe em tempo de execução implementada no mesmo projeto que a consome.

Um tipo que é consumido da interface XAML deve ser uma classe de runtime, mesmo que esteja no mesmo projeto que o XAML. Neste cenário, gera-se um tipo projetado a partir dos metadados Windows Runtime da classe de execução (.winmd). Mais uma vez, incluis um cabeçalho, mas depois tens a escolha entre as formas C++/WinRT versão 1.0 ou 2.0 de construir a instância da classe runtime. O método da versão 1.0 utiliza winrt::make; O método da versão 2.0 é conhecido como Construção Uniforme. Vamos analisar cada um por sua vez.

Construir usando winrt::make

Vamos começar pelo método padrão (C++/WinRT versão 1.0), porque é boa ideia estar pelo menos familiarizado com esse padrão. Constróis o tipo projetado através do seu construtor std::nullptr_t . Esse construtor não realiza qualquer inicialização, por isso deve atribuir um valor à instância através da função helper winrt::make , passando quaisquer argumentos construtores necessários. Uma classe em tempo de execução implementada no mesmo projeto que o código que a consome não precisa de ser registada nem instanciada através da ativação do Windows Runtime/COM.

Veja Controlos XAML associados a uma propriedade C++/WinRT para uma explicação completa. Esta secção mostra excertos desse walkthrough.

// 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 C++/WinRT versão 2.0 e posteriores, existe uma forma otimizada de construção disponível conhecida como construção uniforme (ver Notícias e alterações em C++/WinRT 2.0).

Consulte controlos XAML; associe a uma propriedade C++/WinRT para obter uma explicação detalhada. Esta secção mostra excertos desse walkthrough.

Para usar a construção uniforme em vez do winrt::make, vais precisar de uma fábrica de ativação. Uma boa forma de gerar um é adicionar um construtor ao seu IDL.

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

Depois, em MainPage.h declarar e inicializar m_mainViewModel em apenas um passo, como mostrado abaixo.

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

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

Para mais informações sobre construção uniforme e exemplos de código, veja Optar para construção uniforme e acesso direto à implementação.

Instanciação e devolução de tipos projetados e interfaces

Aqui está um exemplo de como os tipos e interfaces projetados podem ser no seu projeto consumidor. Lembre-se que um tipo projetado (como o deste exemplo) é gerado por ferramentas e não é algo que você próprio 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 mostrou as diferentes formas de instanciar um tipo projetado. Aqui fica um lembrete e resumo, usando o 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>();
  • Pode aceder aos membros de todas as interfaces de um tipo projetado.
  • Pode retornar um tipo projetado ao autor da chamada.
  • Os tipos e interfaces projetados derivam de winrt::Windows::Foundation::IUnknown. Assim, pode chamar IUnknown::as num tipo ou interface projetada para consultar outras interfaces projetadas, que também pode usar ou devolver a um chamador. A função-membro as funciona de forma semelhante a QueryInterface.
void f(MyProject::MyRuntimeClass const& myrc)
{
    myrc.ToString();
    myrc.Close();
    IClosable iclosable = myrc.as<IClosable>();
    iclosable.Close();
}

Fábricas de ativação

A forma prática e direta de criar um objeto C++/WinRT é a seguinte.

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

Mas poderá haver situações em que queira criar a fábrica de ativação e, depois, criar objetos a partir dela quando lhe for conveniente. Aqui estão alguns exemplos que mostram como fazê-lo, utilizando 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. No próximo exemplo, o ThermometerWRC::Thermometer é um tipo personalizado implementado num componente do Windows Runtime.

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

Ambiguidades entre membros/tipos

Quando uma função membro tem o mesmo nome que um tipo, há ambiguidade. As regras para a pesquisa de nomes não qualificados em C++ nas funções membros fazem com que procure a classe antes de pesquisar nos namespaces. A regra de falha de substituição não constitui um erro (SFINAE) não se aplica (aplica-se durante a resolução de sobrecargas de modelos de função). Portanto, se o nome dentro da classe não fizer sentido, o compilador não procura sempre uma correspondência melhor—simplesmente reporta 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 pensa que está a passar FrameworkElement.Style() (que, em C++/WinRT, é uma função membro) como parâmetro 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 consulta por nome não qualificado tem uma exceção especial no caso de o nome ser seguido por ::, caso em que ignora funções, variáveis e valores de enum. Isto permite-te fazer coisas assim.

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

A chamada para Visibility() corresponde ao 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 encontra a classe enum.

APIs importantes