Criar APIs com C++/WinRT

Este tópico mostra como criar APIs C++/WinRT usando o struct base winrt::implements , direta ou indiretamente. Sinônimos de criar neste contexto são produzir ou implementar. Este tópico aborda os seguintes cenários para implementar APIs em um tipo C++/WinRT, nesta ordem.

Note

Este tópico aborda o tema dos componentes do Windows Runtime, mas apenas no contexto de C++/WinRT. Se você está procurando conteúdo sobre componentes do Windows Runtime que abranja todas as linguagens do Windows Runtime, consulte componentes do Windows Runtime.

  • Você não está criando uma classe do Windows Runtime (classe de runtime); você só quer implementar uma ou mais interfaces do Windows Runtime para uso local no seu aplicativo. Você deriva diretamente de winrt::implements neste caso e implementa funções.
  • Você está criando uma classe de runtime. Você pode estar criando um componente para ser consumido por um aplicativo. Ou você pode estar definindo um tipo para ser consumido por uma interface do usuário (UI) XAML e, nesse caso, estará tanto implementando quanto consumindo uma classe de tempo de execução na mesma unidade de compilação. Nesses casos, você permite que as ferramentas gerem classes para você que derivam de winrt::implements.

Em ambos os casos, o tipo que implementa suas APIs C++/WinRT é chamado de tipo de implementação.

Important

É importante distinguir o conceito de um tipo de implementação do tipo projetado. O tipo projetado está descrito em Consume APIs with C++/WinRT.

Se você não estiver criando uma classe de runtime

O cenário mais simples é aquele em que seu tipo implementa uma interface do Windows Runtime e você consumirá esse tipo no mesmo aplicativo. Nesse caso, seu tipo não precisa ser uma classe de runtime; apenas uma classe C++ comum. Por exemplo, você pode estar desenvolvendo um aplicativo de desktop WinUI 3 baseado em Microsoft::UI::Xaml::Application.

Se o tipo for referenciado pela interface do usuário XAML , ele precisará ser uma classe de runtime, mesmo que esteja no mesmo projeto que o XAML. Para esse caso, consulte a seção Se você estiver criando uma classe de runtime a ser referenciada na interface do usuário XAML.

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.

No Visual Studio, o modelo de projeto Aplicativo em branco, empacotado (WinUI 3 para Desktop) para C++ ilustra o padrão de aplicativo do WinUI 3. A classe App deriva de Microsoft::UI::Xaml::Application e o ponto de entrada chama seu método Start.

#include "App.xaml.h"

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
    winrt::init_apartment();
    ::winrt::Microsoft::UI::Xaml::Application::Start(
        [](auto&&) { ::winrt::make<App>(); });
}

A classe App cria um Microsoft::UI::Xaml::Window e a ativa no OnLaunched.

// App.xaml.h
struct App : AppT<App>
{
    App();
    void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);

private:
    winrt::Microsoft::UI::Xaml::Window window{ nullptr };
};

// App.xaml.cpp
void App::OnLaunched(LaunchActivatedEventArgs const&)
{
    window = make<MainWindow>();
    window.Activate();
}

O C++/WinRT tem o modelo de struct base winrt::implements para facilitar a implementação de uma interface (ou várias) sem recorrer à programação no estilo COM. Você apenas deriva seu tipo de implementações e, em seguida, implementa as funções da interface. Aqui está um exemplo que implementa uma interface personalizada.

struct MyType : implements<MyType, IStringable>
{
    hstring ToString()
    {
        return L"MyType";
    }
};

Se você estiver criando uma classe de runtime em um componente Windows Runtime

Se o seu tipo estiver empacotado em um componente do Windows Runtime para ser consumido por outro binário (o outro binário geralmente é um aplicativo), então seu tipo precisará ser uma classe de runtime. Você declara uma classe de tempo de execução em um arquivo (.idl) da Microsoft Interface Definition Language (IDL) (consulte Separando classes de tempo de execução em arquivos Midl (.idl)).

Cada arquivo IDL resulta em um .winmd arquivo e Visual Studio mescla todos eles em um único arquivo com o mesmo nome do namespace raiz. Esse arquivo final .winmd será aquele que os consumidores do componente referenciarão.

Aqui está um exemplo de declaração de uma classe de runtime em um arquivo IDL.

// MyRuntimeClass.idl
namespace MyProject
{
    runtimeclass MyRuntimeClass
    {
        // Declaring a constructor (or constructors) in the IDL causes the runtime class to be
        // activatable from outside the compilation unit.
        MyRuntimeClass();
        String Name;
    }
}

Essa IDL declara uma classe Windows Runtime (runtime). Uma classe de runtime é um tipo que pode ser ativado e consumido por meio de interfaces COM modernas, normalmente entre limites executáveis. Quando você adiciona um arquivo IDL ao seu projeto e cria, a cadeia de ferramentas C++/WinRT (midl.exe e cppwinrt.exe) gera um tipo de implementação para você. Para ver um exemplo do fluxo de trabalho do arquivo IDL em funcionamento, consulte Controles XAML; vincular a uma propriedade C++/WinRT.

Com base no IDL de exemplo acima, o tipo de implementação é uma estrutura stub em C++ chamada winrt::MyProject::implementation::MyRuntimeClass nos arquivos de código-fonte chamados \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h e MyRuntimeClass.cpp.

O tipo de implementação tem esta aparência.

// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
    struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
    {
        MyRuntimeClass() = default;

        winrt::hstring Name();
        void Name(winrt::hstring const& value);
    };
}

// winrt::MyProject::factory_implementation::MyRuntimeClass is here, too.

Observe que o padrão de polimorfismo associado a F que está sendo usado (MyRuntimeClass usa-se como um argumento de modelo para sua base, MyRuntimeClassT). Isso também é chamado de CRTP (padrão de modelo curiosamente recorrente). Se você seguir a cadeia de herança para cima, verá MyRuntimeClass_base.

Você pode simplificar a implementação de propriedades simples usando bibliotecas de implementação de Windows (WIL). Veja como:

// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
    struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
    {
        MyRuntimeClass() = default;

        wil::single_threaded_rw_property<winrt::hstring> Name;
    };
}

Consulte Propriedades simples.

template <typename D, typename... I>
struct MyRuntimeClass_base : implements<D, MyProject::IMyRuntimeClass, I...>

Portanto, nesse cenário, na raiz da hierarquia de herança está mais uma vez o template de struct base winrt::implements.

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

Se você estiver criando uma classe de runtime a ser referenciada na interface do usuário XAML

Se o tipo for referenciado pela interface do usuário XAML, ele precisará ser uma classe de runtime, mesmo que esteja no mesmo projeto que o XAML. Embora normalmente sejam ativados entre limites executáveis, uma classe de runtime pode, em vez disso, ser usada dentro da unidade de compilação que a implementa.

Nesse cenário, você está criando e consumindo as APIs. O procedimento para implementar sua classe de runtime é essencialmente o mesmo que para um componente Windows Runtime. Portanto, consulte a seção anterior: se você estiver criando uma classe de runtime em um componente Windows Runtime. O único detalhe que difere é que, da IDL, a cadeia de ferramentas C++/WinRT gera não apenas um tipo de implementação, mas também um tipo projetado. É importante apreciar que dizer apenas "MyRuntimeClass" neste cenário pode ser ambíguo; há várias entidades com esse nome, de tipos diferentes.

  • MyRuntimeClass é o nome de uma classe de runtime. Mas isso é realmente uma abstração: declarada em IDL e implementada em alguma linguagem de programação.
  • MyRuntimeClass é o nome do struct do C++ winrt::MyProject::implementation::MyRuntimeClass, que é a implementação C++/WinRT da classe de runtime. Como vimos, se houver projetos separados de implementação e consumo, esse struct existirá apenas no projeto de implementação. Esse é o tipo de implementação ou a implementação. Esse tipo é gerado (pela cppwinrt.exe ferramenta) nos arquivos \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h e MyRuntimeClass.cpp.
  • MyRuntimeClass é o nome do tipo projetado na forma da struct C++ winrt::MyProject::MyRuntimeClass. Se houver projetos de implementação e consumo separados, esse struct existirá apenas no projeto de consumo. Esse é o tipo projetado ou a projeção. Esse tipo é gerado (por cppwinrt.exe) no arquivo \MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h.

Tipo projetado e tipo de implementação

Aqui estão as partes do tipo projetado que são relevantes para este tópico.

// MyProject.2.h
...
namespace winrt::MyProject
{
    struct MyRuntimeClass : MyProject::IMyRuntimeClass
    {
        MyRuntimeClass(std::nullptr_t) noexcept {}
        MyRuntimeClass();
    };
}

Para obter um passo a passo de exemplo da implementação da interface INotifyPropertyChanged em uma classe de runtime, consulte controles XAML; associar a uma propriedade C++/WinRT.

O procedimento para consumir sua classe de runtime neste cenário é descrito em Consumir APIs com C++/WinRT.

Fatorando classes de runtime em arquivos Midl (.idl)

Os modelos de projeto e de item Visual Studio produzem um arquivo IDL separado para cada classe de runtime. Isso fornece uma correspondência lógica entre um arquivo IDL e seus arquivos de código-fonte gerados.

No entanto, se você consolidar todas as classes de runtime do projeto em um único arquivo IDL, isso poderá melhorar significativamente o tempo de build. Se, de outra forma, você teria dependências complexas (ou circulares) import entre elas, então consolidá-las pode realmente ser necessário. E você pode achar mais fácil criar e revisar suas classes de runtime se elas estiverem juntas.

Construtores de classe de runtime

Aqui estão alguns pontos para tirar das listagens que vimos acima.

  • Cada construtor que você declara em sua IDL faz com que um construtor seja gerado tanto no seu tipo de implementação quanto no seu tipo projetado. Construtores declarados por IDL são usados para consumir a classe de runtime de uma unidade de compilação diferente .
  • Independentemente de você ter ou não construtor(es) declarado(s) em IDL, uma sobrecarga de construtor que aceita std::nullptr_t como parâmetro é gerada para o seu tipo projetado. Chamar o construtor std::nullptr_t é a primeira de duas etapas para consumir a classe de runtime da mesma unidade de compilação. Para obter mais detalhes e um exemplo de código, consulte Consumir APIs com C++/WinRT.
  • Se você estiver consumindo a classe de runtime da mesma unidade de compilação, também poderá implementar construtores não padrão diretamente no tipo de implementação (que, lembre-se, está em MyRuntimeClass.h).

Note

Se você espera que sua classe de runtime seja consumida de uma unidade de compilação diferente (o que é comum), inclua construtores em sua IDL (pelo menos um construtor padrão). Ao fazer isso, você também obterá uma implementação de fábrica junto com seu tipo de implementação.

Se você quiser criar e consumir sua classe de runtime somente na mesma unidade de compilação, não declare nenhum construtor em sua IDL. Você não precisa de uma implementação de fábrica e uma não será gerada. O construtor padrão do seu tipo de implementação será excluído, mas você pode editá-lo facilmente e defini-lo como padrão.

Se você quiser criar e consumir sua classe de runtime somente na mesma unidade de compilação e precisar de parâmetros de construtor, crie os construtores necessários diretamente no tipo de implementação.

Métodos, propriedades e eventos de classe de runtime

Vimos que o fluxo de trabalho é usar a IDL para declarar sua classe de runtime e seus membros e, em seguida, a ferramenta gera protótipos e implementações de stub para você. Quanto aos protótipos gerados automaticamente dos membros da sua classe de tempo de execução, você pode editá-los para que aceitem tipos diferentes daqueles que você declara na sua IDL. Mas você pode fazer isso apenas desde que o tipo que você declara na IDL possa ser encaminhado para o tipo que você declara na versão implementada.

Aqui estão alguns exemplos.

  • Você pode relaxar os tipos de parâmetro. Por exemplo, se no IDL seu método usa um SomeClass, você pode optar por alterá-lo para IInspectable em sua implementação. Isso funciona porque qualquer SomeClass pode ser encaminhada para IInspectable (o inverso , é claro, não funcionaria).
  • Você pode aceitar um parâmetro copiado por valor, em vez de por referência. Por exemplo, altere SomeClass const& para SomeClass. Isso é necessário quando você precisa evitar capturar uma referência em uma coroutina (consulte Parameter-passing).
  • Você pode afrouxar as restrições do valor de retorno. Por exemplo, você pode alterar void para winrt::fire_and_forget.

Os dois últimos são muito úteis quando você está escrevendo um manipulador de eventos assíncrono.

Instanciando e retornando interfaces e tipos de implementação

Para esta seção, vamos usar como exemplo um tipo de implementação chamado MyType, que implementa as interfaces IStringable e IClosable .

Você pode derivar MyType diretamente de winrt::implements (não é uma classe de runtime).

#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;

struct MyType : implements<MyType, IStringable, IClosable>
{
    winrt::hstring ToString(){ ... }
    void Close(){}
};

Ou você pode gerá-lo de IDL (é uma classe de runtime).

// MyType.idl
namespace MyProject
{
    runtimeclass MyType: Windows.Foundation.IStringable, Windows.Foundation.IClosable
    {
        MyType();
    }
}

Você não pode alocar diretamente seu tipo de implementação.

MyType myimpl; // error C2259: 'MyType': cannot instantiate abstract class

Mas você pode converter MyType em um objeto IStringable ou IClosable que pode usar ou retornar como parte da sua projeção chamando o template de função winrt::make. make retorna a interface padrão do tipo de implementação.

IStringable istringable = winrt::make<MyType>();

Note

No entanto, se você estiver referenciando seu tipo a partir da sua interface do usuário XAML, haverá tanto um tipo de implementação quanto um tipo projetado no mesmo projeto. Nesse caso, make retorna uma instância do tipo projetado. Para ver um exemplo de código desse cenário, consulte Controles XAML; vincular-se a uma propriedade C++/WinRT.

Podemos usar istringable (no exemplo de código acima) apenas para chamar os membros da interface IStringable . Mas uma interface C++/WinRT (que é uma interface projetada) deriva de winrt::Windows::Foundation::IUnknown. Portanto, você pode chamar IUnknown::as (ou IUnknown::try_as) nela para consultar outros tipos ou interfaces projetados, que você também pode usar ou retornar.

Tip

Um cenário em que você não deve usar as ou try_as é a derivação de classe em tempo de execução ("classes compostas"). Quando um tipo de implementação compõe outra classe, não chame como ou try_as para executar um QueryInterface desmarcado ou verificado da classe que está sendo composta. Em vez disso, acesse o membro de dados (this->) m_inner, e chame as ou try_as nele. Para obter mais informações, consulte a derivação de classe do Runtime neste tópico.

istringable.ToString();
IClosable iclosable = istringable.as<IClosable>();
iclosable.Close();

Se você precisar acessar todos os membros da implementação e, posteriormente, retornar uma interface a um chamador, use o modelo de função winrt::make_self . make_self retorna um winrt::com_ptr encapsulando o tipo de implementação. Você pode acessar os membros de todas as suas interfaces (usando o operador de seta), pode retorná-lo ao chamador como está ou pode chamar as nele e retornar o objeto de interface resultante ao chamador.

winrt::com_ptr<MyType> myimpl = winrt::make_self<MyType>();
myimpl->ToString();
myimpl->Close();
IClosable iclosable = myimpl.as<IClosable>();
iclosable.Close();

A classe MyType não faz parte da projeção; é a implementação. Mas dessa forma você pode chamar seus métodos de implementação diretamente, sem a sobrecarga de uma chamada de função virtual. No exemplo acima, mesmo que MyType::ToString use a mesma assinatura que o método projetado no IStringable, estamos chamando o método não virtual diretamente, sem cruzar a ABI (interface binária do aplicativo). O com_ptr simplesmente contém um ponteiro para o struct MyType , para que você também possa acessar quaisquer outros detalhes internos do MyType por meio da myimpl variável e do operador de seta.

No caso em que você tiver um objeto de interface e souber que ele é uma interface em sua implementação, você poderá voltar à implementação usando o modelo de função winrt::get_self . Novamente, é uma técnica que evita chamadas de função virtual e permite acessar diretamente a implementação.

Note

Se você ainda não instalou o SDK do Windows versão 10.0.17763.0 (Windows 10, versão 1809) ou posterior, precisará chamar winrt::from_abi em vez de winrt::get_self.

Veja um exemplo. Há outro exemplo em Implementar a classe de controle personalizado BgLabelControl.

void ImplFromIClosable(IClosable const& from)
{
    MyType* myimpl = winrt::get_self<MyType>(from);
    myimpl->ToString();
    myimpl->Close();
}

Mas apenas o objeto de interface original se mantém em uma referência. Se você quiser manter isso, pode chamar com_ptr::copy_from.

winrt::com_ptr<MyType> impl;
impl.copy_from(winrt::get_self<MyType>(from));
// com_ptr::copy_from ensures that AddRef is called.

O tipo de implementação em si não deriva de winrt::Windows::Foundation::IUnknown, portanto não possui a função as. Mesmo assim, como você pode ver na função ImplFromIClosable acima, você pode acessar os membros de todas as suas interfaces. Mas, se você fizer isso, não retorne ao chamador a instância bruta do tipo de implementação. Em vez disso, use uma das técnicas já mostradas e retorne uma interface projetada ou uma com_ptr.

Se você tiver uma instância do tipo de implementação e precisar passá-la para uma função que espera o tipo projetado correspondente, poderá fazer isso, conforme mostrado no exemplo de código abaixo. Existe um operador de conversão no seu tipo de implementação (desde que o tipo de implementação tenha sido gerado pela ferramenta cppwinrt.exe) que torna isso possível. Você pode passar um valor de tipo de implementação diretamente para um método que espera um valor do tipo projetado correspondente. Em uma função membro de um tipo de implementação, você pode passar *this para um método que espera receber um valor do tipo projetado correspondente.

// MyClass.idl
import "MyOtherClass.idl";
namespace MyProject
{
    runtimeclass MyClass
    {
        MyClass();
        void MemberFunction(MyOtherClass oc);
    }
}

// MyClass.h
...
namespace winrt::MyProject::implementation
{
    struct MyClass : MyClassT<MyClass>
    {
        MyClass() = default;
        void MemberFunction(MyProject::MyOtherClass const& oc) { oc.DoWork(*this); }
    };
}
...

// MyOtherClass.idl
import "MyClass.idl";
namespace MyProject
{
    runtimeclass MyOtherClass
    {
        MyOtherClass();
        void DoWork(MyClass c);
    }
}

// MyOtherClass.h
...
namespace winrt::MyProject::implementation
{
    struct MyOtherClass : MyOtherClassT<MyOtherClass>
    {
        MyOtherClass() = default;
        void DoWork(MyProject::MyClass const& c){ /* ... */ }
    };
}
...

//main.cpp
#include "pch.h"
#include <winrt/base.h>
#include "MyClass.h"
#include "MyOtherClass.h"
using namespace winrt;

// MyProject::MyClass is the projected type; the implementation type would be MyProject::implementation::MyClass.

void FreeFunction(MyProject::MyOtherClass const& oc)
{
    auto defaultInterface = winrt::make<MyProject::implementation::MyClass>();
    MyProject::implementation::MyClass* myimpl = winrt::get_self<MyProject::implementation::MyClass>(defaultInterface);
    oc.DoWork(*myimpl);
}
...

Derivação de classe em tempo de execução

Você pode criar uma classe de runtime derivada de outra classe de runtime, desde que a classe base seja declarada como "não selada". O termo usado no Windows Runtime para derivação de classe é "classes componíveis". O código para implementar uma classe derivada depende se a classe base é fornecida por outro componente ou pelo mesmo componente. Felizmente, você não precisa aprender essas regras, basta copiar as implementações de exemplo da sources pasta de saída produzida pelo cppwinrt.exe compilador.

Considere este exemplo.

// MyProject.idl
namespace MyProject
{
    [default_interface]
    runtimeclass MyButton : Microsoft.UI.Xaml.Controls.Button
    {
        MyButton();
    }

    unsealed runtimeclass MyBase
    {
        MyBase();
        overridable Int32 MethodOverride();
    }

    [default_interface]
    runtimeclass MyDerived : MyBase
    {
        MyDerived();
    }
}

No exemplo acima, MyButton é derivado do controle botão XAML, que é fornecido por outro componente. Nesse caso, a implementação é semelhante à implementação de uma classe não composável:

namespace winrt::MyProject::implementation
{
    struct MyButton : MyButtonT<MyButton>
    {
    };
}

namespace winrt::MyProject::factory_implementation
{
    struct MyButton : MyButtonT<MyButton, implementation::MyButton>
    {
    };
}

Por outro lado, no exemplo acima, MyDerived é derivado de outra classe no mesmo componente. Nesse caso, a implementação requer um parâmetro de modelo adicional especificando a classe de implementação para a classe base.

namespace winrt::MyProject::implementation
{
    struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
    {                                     // ^^^^^^^^^^^^^^^^^^^^^^
    };
}

namespace winrt::MyProject::factory_implementation
{
    struct MyDerived : MyDerivedT<MyDerived, implementation::MyDerived>
    {
    };
}

Em ambos os casos, sua implementação pode chamar um método da classe base qualificando-o com o alias de tipo base_type:

namespace winrt::MyProject::implementation
{
    struct MyButton : MyButtonT<MyButton>
    {
        void OnApplyTemplate()
        {
            // Call base class method
            base_type::OnApplyTemplate();

            // Do more work after the base class method is done
            DoAdditionalWork();
        }
    };

    struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
    {
        int MethodOverride()
        {
            // Return double what the base class returns
            return 2 * base_type::MethodOverride();
        }
    };
}

Tip

Quando um tipo de implementação compõe outra classe, não chame como ou try_as para executar um QueryInterface desmarcado ou verificado da classe que está sendo composta. Em vez disso, acesse o membro de dados (this->) m_inner, e chame as ou try_as nele.

Derivando de um tipo que tem um construtor não padrão

ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) é um exemplo de um construtor não padrão. Não há construtor padrão, portanto, para instanciar um ToggleButtonAutomationPeer, você precisa passar um owner. Portanto, se você herdar de ToggleButtonAutomationPeer, precisará fornecer um construtor que receba um owner e o passe para a classe base. Vamos ver como isso se parece na prática.

// MySpecializedToggleButton.idl
namespace MyNamespace
{
    runtimeclass MySpecializedToggleButton :
        Microsoft.UI.Xaml.Controls.Primitives.ToggleButton
    {
        ...
    };
}
// MySpecializedToggleButtonAutomationPeer.idl
namespace MyNamespace
{
    runtimeclass MySpecializedToggleButtonAutomationPeer :
        Microsoft.UI.Xaml.Automation.Peers.ToggleButtonAutomationPeer
    {
        MySpecializedToggleButtonAutomationPeer(MySpecializedToggleButton owner);
    };
}

O construtor gerado para o tipo de implementação tem esta aparência.

// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
    (MyNamespace::MySpecializedToggleButton const& owner)
{
    ...
}
...

A única peça ausente é que você precisa passar esse parâmetro de construtor para a classe base. Lembra-se do padrão de polimorfismo associado a F que mencionamos acima? Quando estiver familiarizado com os detalhes desse padrão, conforme usado pelo C++/WinRT, você poderá descobrir como é chamada a classe base (ou pode apenas examinar o arquivo de cabeçalho da classe de implementação). É assim que se chama o construtor de classe base nesse caso.

// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
    (MyNamespace::MySpecializedToggleButton const& owner) :
    MySpecializedToggleButtonAutomationPeerT<MySpecializedToggleButtonAutomationPeer>(owner)
{
    ...
}
...

O construtor de classe base espera um ToggleButton. E MySpecializedToggleButtoné umToggleButton.

Até que você faça a edição descrita acima (para passar esse parâmetro de construtor para a classe base), o compilador sinalizará seu construtor e apontará que não há nenhum construtor padrão apropriado disponível em um tipo chamado (neste caso) MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer>. Na verdade, essa é a classe base da classe base do seu tipo de implementação.

Namespaces: tipos projetados, tipos de implementação e fábricas

Como você viu anteriormente neste tópico, existe uma classe de runtime C++/WinRT na forma de mais de uma classe C++ em mais de um namespace. Portanto, o nome MyRuntimeClass tem um significado no namespace winrt::MyProject e um significado diferente no namespace winrt::MyProject::implementation . Lembre-se de qual namespace você tem atualmente no contexto e use prefixos de namespace se precisar de um nome de um namespace diferente. Vamos examinar mais de perto os namespaces em questão.

  • winrt::MyProject. Esse namespace contém tipos projetados. Um objeto de um tipo projetado é um proxy; trata-se essencialmente de um ponteiro inteligente para um objeto subjacente, em que esse objeto subjacente pode ser implementado aqui no seu projeto ou em outra unidade de compilação.
  • winrt::MyProject::implementation. Esse namespace contém tipos de implementação. Um objeto de um tipo de implementação não é um ponteiro; é um valor— um objeto de pilha C++ completo. Não construa um tipo de implementação diretamente; em vez disso, chame winrt::make, passando seu tipo de implementação como o parâmetro de modelo. Mostramos exemplos de winrt::make em ação anteriormente neste tópico e há outro exemplo em controles XAML; associar a uma propriedade C++/WinRT. Veja também o diagnóstico de alocações diretas.
  • winrt::MyProject::factory_implementation. Esse namespace contém fábricas. Um objeto nesse namespace dá suporte a IActivationFactory.

Esta tabela mostra a qualificação mínima do namespace que você precisa usar em contextos diferentes.

O espaço de nomes que está em contexto Para especificar o tipo projetado Para especificar o tipo de implementação
winrt::MyProject MyRuntimeClass implementation::MyRuntimeClass
winrt::MyProject::implementation MyProject::MyRuntimeClass MyRuntimeClass

Important

Quando você quiser retornar um tipo projetado de sua implementação, tenha cuidado para não instanciar o tipo de implementação escrevendo MyRuntimeClass myRuntimeClass;. As técnicas e o código corretos para esse cenário são mostrados anteriormente neste tópico na seção Instanciando e retornando tipos de implementação e interfaces.

O problema com MyRuntimeClass myRuntimeClass; nesse cenário é que ele cria um objeto winrt::MyProject::implementation::MyRuntimeClass na pilha. Esse objeto (do tipo de implementação) se comporta como o tipo projetado de algumas maneiras– você pode invocar métodos nele da mesma maneira; e até converte em um tipo projetado. Mas o objeto é destruído, conforme as regras normais do C++, quando o escopo termina. Portanto, se você retornou um tipo projetado (um ponteiro inteligente) para esse objeto, esse ponteiro agora está pendurado.

Esse tipo de bug de corrupção de memória é difícil de diagnosticar. Assim, em compilações de depuração, uma asserção de C++/WinRT ajuda você a identificar esse erro usando um detector da pilha de chamadas. Mas as coroutinas são alocadas no heap, portanto, você não receberá ajuda com esse erro se fizer isso dentro de uma coroutina. Para obter mais informações, consulte Diagnosticando alocações diretas.

Usando tipos projetados e tipos de implementação com vários recursos do C++/WinRT

Aqui estão vários lugares onde um recurso C++/WinRT espera um tipo e que tipo de tipo ele espera (tipo projetado, tipo de implementação ou ambos).

Característica Aceita Notes
T (representando um ponteiro inteligente) Projetado Veja a advertência em Namespaces: tipos projetados, tipos de implementação e fábricas sobre o uso, por engano, do tipo de implementação.
agile_ref<T> Ambos Se você usar o tipo de implementação, o argumento do construtor deverá ser com_ptr<T>.
com_ptr<T> Implementation Usar o tipo projetado gera o erro: 'Release' is not a member of 'T'.
default_interface<T> Ambos Se você usar o tipo de implementação, a primeira interface implementada será retornada.
get_self<T> Implementation Usar o tipo projetado gera o erro: '_abi_TrustLevel': is not a member of 'T'.
guid_of<T>() Ambos Retorna o GUID da interface padrão.
IWinRTTemplateInterface<T>
Projetado Usar o tipo de implementação compila, mas é um erro — veja o aviso em Namespaces: tipos projetados, tipos de implementação e fábricas.
make<T> Implementation O uso do tipo projetado gera o erro: 'implements_type': is not a member of any direct or indirect base class of 'T'
make_agile(T const&amp;) Ambos Se você usar o tipo de implementação, o argumento deverá ser com_ptr<T>.
make_self<T> Implementation O uso do tipo projetado gera o erro: 'Release': is not a member of any direct or indirect base class of 'T'
name_of<T> Projetado Se você usar o tipo de implementação, você obterá o GUID na forma de cadeia de caracteres da interface padrão.
weak_ref<T> Ambos Se você usar o tipo de implementação, o argumento do construtor deverá ser com_ptr<T>.

Aceitar a construção uniforme e o acesso direto à implementação

Esta seção descreve um recurso C++/WinRT 2.0 que é opt-in, embora esteja habilitado por padrão para novos projetos. Para um projeto existente, será necessário ativá-lo configurando a ferramenta cppwinrt.exe. Em Visual Studio, defina a propriedade de projeto Common Properties>C++/WinRT>Otimizada para Sim. Isso tem o efeito de adicionar <CppWinRTOptimized>true</CppWinRTOptimized> ao arquivo de projeto. E tem o mesmo efeito de adicionar a opção ao invocar cppwinrt.exe pela linha de comando.

A opção -opt[imize] permite o que geralmente é chamado de construção uniforme. Com a construção uniforme (ou unificada), você usa a própria projeção da linguagem C++/WinRT para criar e usar seus tipos de implementação (tipos implementados pelo seu componente, para serem consumidos por aplicativos) com eficiência e sem problemas de carregamento.

Antes de descrever a funcionalidade, primeiro vamos mostrar a situação sem estrutura uniforme. Para ilustrar, começaremos com este exemplo Windows Runtime classe.

// MyClass.idl
namespace MyProject
{
    runtimeclass MyClass
    {
        MyClass();
        void Method();
        static void StaticMethod();
    }
}

Como um desenvolvedor C++ familiarizado com o uso da biblioteca C++/WinRT, talvez você queira usar a classe assim.

using namespace winrt::MyProject;

MyClass c;
c.Method();
MyClass::StaticMethod();

E isso seria perfeitamente razoável, desde que o código de consumo mostrado não residisse no mesmo componente que implementa essa classe. Como uma projeção de linguagem, o C++/WinRT protege você como um desenvolvedor da ABI (a interface binária de aplicativo baseada em COM que o Windows Runtime define). O C++/WinRT não chama diretamente a implementação; ele passa pela ABI.

Consequentemente, na linha de código em que você está construindo um objeto MyClass (MyClass c;), a projeção C++/WinRT chama RoGetActivationFactory para recuperar a classe ou a fábrica de ativação e, em seguida, usa essa fábrica para criar o objeto. A última linha também usa a fábrica para realizar o que parece ser uma chamada de método estático. Tudo isso requer que sua classe seja registrada e que o módulo implemente o ponto de entrada DllGetActivationFactory . O C++/WinRT tem um cache de fábrica muito rápido, portanto, nada disso causa um problema para um aplicativo que consome seu componente. O problema é que, dentro do seu componente, você acabou de fazer algo que é um pouco problemático.

Em primeiro lugar, por mais rápido que seja o cache da fábrica do C++/WinRT, fazer uma chamada por meio de RoGetActivationFactory (ou mesmo chamadas subsequentes por meio do cache da fábrica) sempre será mais lento do que chamar diretamente a implementação. Uma chamada para RoGetActivationFactory seguida por IActivationFactory::ActivateInstance seguido por QueryInterface obviamente não será tão eficiente quanto usar uma expressão C++ new para um tipo definido localmente. Como consequência, os desenvolvedores experientes do C++/WinRT estão acostumados a usar as funções auxiliares winrt::make ou winrt::make_self ao criar objetos dentro de um componente.

// MyClass c;
MyProject::MyClass c{ winrt::make<implementation::MyClass>() };

Mas, como você pode ver, isso não é tão conveniente nem conciso. Você deve usar uma função auxiliar para criar o objeto e também desambiguar entre o tipo de implementação e o tipo projetado.

Em segundo lugar, usar a projeção para criar a classe significa que sua fábrica de ativação será armazenada em cache. Normalmente, é isso que você quer, mas, se a factory estiver no mesmo módulo (DLL) que está fazendo a chamada, então, na prática, você manterá a DLL carregada e impedirá que ela seja descarregada. Em muitos casos, isso não importa; mas alguns componentes do sistema devem dar suporte ao descarregamento.

É aqui que entra o termo construção uniforme . Independentemente de o código de criação residir em um projeto que está apenas consumindo a classe ou se ele reside no projeto que está realmente implementando a classe, você pode usar livremente a mesma sintaxe para criar o objeto.

// MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
MyClass c;

Quando você compila seu projeto de componente com o alternador -opt[imize], a chamada feita por meio da projeção de linguagem é compilada na mesma chamada eficiente para a função winrt::make, que cria diretamente o tipo de implementação. Isso torna sua sintaxe simples e previsível, evita qualquer perda de desempenho ao chamar por meio da fábrica e evita manter o componente fixado no processo. Além de projetos de componente, isso também é útil para aplicativos XAML. Ignorar o RoGetActivationFactory para classes implementadas no mesmo aplicativo permite que você as construa (sem precisar ser registrado) de todas as mesmas maneiras possíveis se elas estivessem fora do componente.

A construção padronizada se aplica a qualquer chamada processada pela fábrica nos bastidores. Na prática, isso significa que a otimização atende a construtores e membros estáticos. Este é o exemplo original novamente.

MyClass c;
c.Method();
MyClass::StaticMethod();

Sem -opt[imize], a primeira e a última declarações exigem chamadas por meio do objeto de fábrica. Com-opt[imize], nenhum dos dois faz isso. E essas chamadas são compiladas diretamente em relação à implementação e até têm o potencial de serem embutidas. O que remete ao outro termo frequentemente usado ao falar de -opt[imize], a saber, acesso de implementação direta.

As projeções de linguagem são convenientes, mas, quando você pode acessar diretamente a implementação, você pode e deve aproveitar isso para produzir o código mais eficiente possível. O C++/WinRT pode fazer isso por você, sem forçar você a deixar a segurança e a produtividade da projeção.

Essa é uma alteração significativa porque o componente deve cooperar para permitir que a projeção de idioma entre e acesse diretamente seus tipos de implementação. Como C++/WinRT é uma biblioteca somente de cabeçalho, você pode olhar para dentro e ver o que está acontecendo. Sem -opt[imize]o construtor MyClass e o membro StaticMethod , são definidos pela projeção como esta.

namespace winrt::MyProject
{
    inline MyClass::MyClass() :
        MyClass(impl::call_factory<MyClass>([](auto&& f){
		    return f.template ActivateInstance<MyClass>(); }))
    {
    }
    inline void MyClass::StaticMethod()
    {
        impl::call_factory<MyClass, MyProject::IClassStatics>([&](auto&& f) {
		    return f.StaticMethod(); });
    }
}

Não é necessário seguir todos os itens acima; a intenção é mostrar que ambas as chamadas envolvem uma chamada para uma função chamada call_factory. Esse é o indício de que essas chamadas envolvem o cache da fábrica e não acessam diretamente a implementação. Com-opt[imize], essas mesmas funções nem sequer são definidas. Em vez disso, eles são declarados pela projeção e suas definições são deixadas para o componente.

Em seguida, o componente pode fornecer definições que chamam diretamente para a implementação. Agora chegamos à alteração incompatível. Essas definições são geradas para você quando você usa ambas -component e -opt[imize], e elas aparecem em um arquivo chamado Type.g.cpp, em que Type é o nome da classe de runtime que está sendo implementada. É por isso que você pode encontrar vários erros do vinculador quando ativa -opt[imize] pela primeira vez em um projeto existente. Você precisa incluir esse arquivo gerado à sua implementação para integrar tudo.

Em nosso exemplo, MyClass.h pode ter esta aparência (independentemente de estar -opt[imize] sendo usado).

// MyClass.h
#pragma once
#include "MyClass.g.h"

namespace winrt::MyProject::implementation
{
    struct MyClass : ClassT<MyClass>
    {
        MyClass() = default;

        static void StaticMethod();
        void Method();
    };
}
namespace winrt::MyProject::factory_implementation
{
    struct MyClass : ClassT<MyClass, implementation::MyClass>
    {
    };
}

Seu MyClass.cpp é onde tudo se reúne.

#include "pch.h"
#include "MyClass.h"
#include "MyClass.g.cpp" // !!It's important that you add this line!!

namespace winrt::MyProject::implementation
{
    void MyClass::StaticMethod()
    {
    }

    void MyClass::Method()
    {
    }
}

Portanto, para usar a construção uniforme em um projeto existente, você precisa editar o arquivo .cpp de cada implementação para que #include <Sub/Namespace/Type.g.cpp> após a inclusão (e definição) da classe de implementação. Esse arquivo fornece as definições dessas funções que a projeção deixou indefinida. Esta é a aparência dessas definições dentro do MyClass.g.cpp arquivo.

namespace winrt::MyProject
{
    MyClass::MyClass() :
        MyClass(make<MyProject::implementation::MyClass>())
    {
    }
    void MyClass::StaticMethod()
    {
        return MyProject::implementation::MyClass::StaticMethod();
    }
}

E isso completa adequadamente a projeção com chamadas eficientes diretamente para a implementação, evitando chamadas ao cache de fábrica e atendendo aos requisitos do vinculador.

A última coisa que -opt[imize] faz para você é alterar a implementação do seu projeto module.g.cpp (o arquivo que ajuda você a implementar as exportações DllGetActivationFactory e DllCanUnloadNow da DLL) de modo que os builds incrementais tendem a ser muito mais rápidos eliminando o acoplamento de tipo forte exigido pelo C++/WinRT 1.0. Isso é frequentemente chamado de fábricas apagadas por tipo. Sem -opt[imize], o arquivo module.g.cpp gerado para seu componente começa incluindo as definições de todas as suas classes de implementação — as MyClass.h, neste exemplo. Em seguida, ele cria diretamente a fábrica de implementação para cada classe como esta.

if (requal(name, L"MyProject.MyClass"))
{
    return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}

Novamente, você não precisa seguir todos os detalhes. O que é útil ver é que isso requer a definição completa para todas as classes implementadas pelo componente. Isso pode ter um impacto significativo no seu loop interno, pois qualquer alteração em uma única implementação fará com que module.g.cpp seja recompilado. Com -opt[imize], este não é mais o caso. Em vez disso, duas coisas acontecem com o arquivo gerado module.g.cpp . A primeira é que ela não inclui mais nenhuma classe de implementação. Neste exemplo, não incluirá MyClass.h de forma alguma. Em vez disso, cria as fábricas de implementação sem qualquer conhecimento de sua implementação.

void* winrt_make_MyProject_MyClass();

if (requal(name, L"MyProject.MyClass"))
{
    return winrt_make_MyProject_MyClass();
}

Obviamente, não é necessário incluir suas definições e cabe ao vinculador resolver a definição da função winrt_make_Component_Class . É claro que você não precisa pensar nisso, pois o MyClass.g.cpp arquivo que é gerado para você (e que você incluiu anteriormente para dar suporte à construção uniforme) também define essa função. Aqui está a totalidade do MyClass.g.cpp arquivo gerado para este exemplo.

void* winrt_make_MyProject_MyClass()
{
    return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
namespace winrt::MyProject
{
    MyClass::MyClass() :
        MyClass(make<MyProject::implementation::MyClass>())
    {
    }
    void MyClass::StaticMethod()
    {
        return MyProject::implementation::MyClass::StaticMethod();
    }
}

Como você pode ver, a função winrt_make_MyProject_MyClass cria diretamente a fábrica da implementação. Isso tudo significa que você pode tranquilamente alterar qualquer implementação específica, e o module.g.cpp nem precisa ser recompilado. É apenas quando você adiciona ou remove Windows Runtime classes que module.g.cpp serão atualizadas e precisam ser recompiladas.

Sobrescrevendo métodos virtuais da classe base

Sua classe derivada poderá ter problemas com métodos virtuais se tanto a classe base quanto a classe derivada forem classes definidas no aplicativo, mas o método virtual estiver definido em uma classe ancestral do Windows Runtime. Na prática, isso ocorrerá se você derivar de classes XAML. O restante desta seção continua a partir do exemplo em classes derivadas.

namespace winrt::MyNamespace::implementation
{
    struct BasePage : BasePageT<BasePage>
    {
        void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };

    struct DerivedPage : DerivedPageT<DerivedPage>
    {
        void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };
}

A hierarquia é Microsoft::UI::Xaml::Controls::Page<- BasePage<- DerivedPage. O método BasePage::OnNavigatedFrom substitui corretamente Page::OnNavigatedFrom, mas DerivedPage::OnNavigatedFrom não substitui BasePage::OnNavigatedFrom.

Aqui, DerivedPage reutiliza a vtable IPageOverrides de BasePage, o que significa que ela não substitui o método IPageOverrides::OnNavigatedFrom . Uma solução em potencial requer que BasePage seja uma classe de modelo e tenha sua implementação inteiramente em um arquivo de cabeçalho, mas isso torna as coisas inaceitávelmente complicadas.

Como solução alternativa, declare o método OnNavigatedFrom como explicitamente virtual na classe base. Dessa forma, quando a entrada vtable para DerivedPage::IPageOverrides::OnNavigatedFrom chama BasePage::IPageOverrides::OnNavigatedFrom, o produtor chama BasePage::OnNavigatedFrom, que (devido à sua virtualidade), acaba chamando DerivedPage::OnNavigatedFrom.

namespace winrt::MyNamespace::implementation
{
    struct BasePage : BasePageT<BasePage>
    {
        // Note the `virtual` keyword here.
        virtual void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };

    struct DerivedPage : DerivedPageT<DerivedPage>
    {
        void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };
}

Isso requer que todos os membros da hierarquia de classe concordem com o valor retornado e os tipos de parâmetro do método OnNavigatedFrom . Se eles discordarem, você deverá usar a versão acima como o método virtual e encapsular as alternativas.

Note

Sua IDL não precisa declarar o método substituído. Para mais detalhes, consulte Implementando métodos que podem ser substituídos.

APIs importantes