APIs de autor com C++/WinRT

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

Note

Este tópico aborda componentes do Windows Runtime, mas apenas no contexto do C++/WinRT. Se procura conteúdo sobre componentes do Windows Runtime que cubra todas as linguagens do Windows Runtime, então veja componentes do Windows Runtime.

  • Não estás a criar uma classe Windows Runtime (classe de tempo de execução); só queres implementar uma ou mais interfaces Windows Runtime para consumo local dentro da tua aplicação. Neste caso, derivas diretamente do winrt::implements e implementas funções.
  • Estás a criar uma classe de runtime. Pode estar a criar um componente para ser consumido a partir de uma aplicação. Ou podes estar a criar um tipo para ser consumido a partir da interface de utilizador (UI) de XAML, e nesse caso estás a implementar e a consumir uma classe de runtime dentro da mesma unidade de compilação. Nestes casos, deixas as ferramentas gerarem classes para ti que derivam de winrt::implements.

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

Important

É importante distinguir o conceito de um tipo de implementação do de um tipo projetado. O tipo projetado é descrito em Consumir APIs com C++/WinRT.

Se não estás a criar uma classe em tempo de execução

O cenário mais simples é aquele em que o teu tipo implementa uma interface Windows Runtime e vais usar esse tipo na mesma aplicação. Nesse caso, o teu tipo não precisa de ser uma classe de runtime; apenas uma classe comum de C++. Por exemplo, pode estar a escrever uma aplicação de desktop para WinUI 3 baseada na Microsoft::UI::Xaml::Application.

Se o teu tipo for referenciado pela interface XAML, então tem de ser uma classe de runtime, mesmo estando no mesmo projeto que o XAML. Para esse caso, veja a secção Se estiver a criar uma classe de runtime para ser referenciada na sua interface XAML.

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 Visual Studio, o modelo de projeto Blank App, Packaged (WinUI 3 in Desktop) para C++ ilustra o padrão de aplicação WinUI 3. A classe App deriva de Microsoft::UI::Xaml::Application, e o ponto de entrada chama o 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 uma janela Microsoft::UI::Xaml:::Window e ativa-a 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 base de estruturas winrt::implementes para facilitar a implementação de uma interface (ou várias) sem recorrer à programação ao estilo COM. Basta deduzires o teu tipo a partir dos implementos e depois implementares 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 estiver a criar uma classe de runtime num componente do Windows Runtime

Se o teu tipo for empacotado num componente do Windows Runtime para consumo a partir de outro binário (o outro binário é normalmente uma aplicação), então o teu tipo precisa de ser uma classe de runtime. Declaras uma classe de runtime num ficheiro Microsoft Interface Definition Language (IDL) (.idl) (ver Fatoração de classes de runtime em ficheiros Midl (.idl)).

Cada ficheiro IDL resulta num .winmd ficheiro, e o Visual Studio funde todos esses num único ficheiro com o mesmo nome do teu espaço de nomes raiz. Esse ficheiro final .winmd será aquele que os consumidores do seu componente irão consultar.

Aqui está um exemplo de declarar uma classe de runtime num ficheiro 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;
    }
}

Este IDL declara uma classe Windows Runtime (runtime). Uma classe de runtime é um tipo que pode ser ativado e consumido através de interfaces COM modernas, tipicamente através de fronteiras executáveis. Quando adiciona um ficheiro IDL ao seu projeto e constrói, a cadeia de ferramentas C++/WinRT (midl.exe e cppwinrt.exe) gera um tipo de implementação para si. Para ver um exemplo do fluxo de trabalho do ficheiro IDL em funcionamento, consulte controlos XAML; associar a uma propriedade C++/WinRT.

Utilizando o exemplo de IDL acima, o tipo de implementação é um stub de uma estrutura C++ denominada winrt::MyProject::implementation::MyRuntimeClass nos ficheiros de código-fonte denominados \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h e MyRuntimeClass.cpp.

O tipo de implementação é assim.

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

Note que o padrão de polimorfismo com limite F está a ser usado (MyRuntimeClass usa-se como argumento modelo para a sua base, MyRuntimeClassT). Isto também é chamado de padrão de modelo curiosamente recorrente (CRTP). Se seguires a cadeia de herança para cima, vais encontrar MyRuntimeClass_base.

Pode simplificar a implementação de propriedades simples utilizando as Bibliotecas de Implementação do Windows (WIL). Saiba como:

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

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

Ver Propriedades simples.

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

Assim, neste cenário, na raiz da hierarquia de herança está, mais uma vez, o modelo base de estrutura winrt::implements.

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

Se estiveres a criar uma classe em tempo de execução para ser referenciada na tua interface XAML

Se o teu tipo for referenciado pela interface XAML, então tem de ser uma classe de runtime, mesmo que esteja no mesmo projeto que o XAML. Embora normalmente sejam ativadas através dos limites dos executáveis, uma classe de runtime pode ser usada dentro da unidade de compilação que a implementa.

Neste cenário, estás a criar e a consumir as APIs. O procedimento para implementar a sua classe de runtime é essencialmente o mesmo que para um componente do Windows Runtime. Por isso, veja a secção anterior—Se está a criar uma classe de runtime num componente do Windows Runtime. O único detalhe que difere é que, a partir do IDL, a cadeia de ferramentas C++/WinRT gera não só um tipo de implementação, mas também um tipo projetado. É importante perceber que dizer apenas "MyRuntimeClass" neste cenário pode ser ambíguo; Existem várias entidades com esse nome, de diferentes tipos.

  • MyRuntimeClass é o nome de uma classe em tempo de execução. Mas isto é realmente uma abstração: declarada em IDL e implementada numa linguagem de programação.
  • MyRuntimeClass é o nome da estrutura C++ winrt::MyProject::implementation::MyRuntimeClass, que é a implementação C++/WinRT da classe runtime. Como vimos, se existirem projetos separados de implementação e de consumo, então essa estrutura existe apenas no projeto de implementação. Este é o tipo de implementação, ou a implementação. Este tipo é gerado (pela cppwinrt.exe ferramenta) nos ficheiros \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h e MyRuntimeClass.cpp.
  • MyRuntimeClass é o nome do tipo projetado na forma da estrutura C++ winrt::MyProject::MyRuntimeClass. Se existirem projetos separados de implementação e de consumo, então essa estrutura existe apenas no projeto consumidor. Este é o tipo projetado, ou a projeção. Este tipo é gerado (por cppwinrt.exe) no ficheiro \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 tema.

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

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

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

Fatoração de classes de runtime em ficheiros Midl (.idl)

O projeto Visual Studio e os modelos de itens produzem um ficheiro IDL separado para cada classe de runtime. Isso fornece uma correspondência lógica entre um ficheiro IDL e os seus ficheiros de código-fonte gerados.

No entanto, se consolidar todas as classes de runtime do seu projeto num único ficheiro IDL, isso pode melhorar significativamente o tempo de construção. Se, de outra forma, existissem dependências complexas (ou circulares import ) entre elas, então a consolidação pode ser realmente necessária. E podes achar mais fácil criar e rever as tuas classes de execução se estiverem juntas.

Construtores de classes em tempo de execução

Eis alguns pontos a reter das listagens que vimos acima.

  • Cada construtor que declaras no teu IDL faz com que um construtor seja gerado tanto no teu tipo de implementação como no tipo projetado. Construtores declarados por IDL são usados para consumir a classe de runtime de uma unidade de compilação diferente .
  • Quer tenhas ou não construtores declarados em IDL, é gerada uma sobrecarga de construtor que aceita std::nullptr_t para o teu tipo projetado. Chamar o construtor std::nullptr_t é o primeiro de dois passos para consumir a classe de runtime da mesma unidade de compilação. Para mais detalhes e um exemplo de código, veja Consumir APIs com o C++/WinRT.
  • Se estiver a consumir a classe de runtime da mesma unidade de compilação, então também pode implementar construtores não padrão diretamente no tipo de implementação (que, lembre-se, está em MyRuntimeClass.h).

Note

Se esperar que a sua classe de runtime seja consumida de uma unidade de compilação diferente (o que é comum), então inclua(s) construtor(es) no seu IDL (pelo menos um construtor padrão). Ao fazer isso, também obterá uma implementação de fábrica juntamente com o seu tipo de implementação.

Se quiseres criar e consumir a tua classe de runtime apenas dentro da mesma unidade de compilação, então não declares nenhum construtor no teu IDL. Não precisas de uma implementação de fábrica, e nenhuma será gerada. O construtor predefinido do seu tipo de implementação será apagado, mas pode editá-lo facilmente e declará-lo como predefinido.

Se quiseres criar e consumir a tua classe de runtime apenas dentro da mesma unidade de compilação, e precisares de parâmetros de construtor, então cria o(s) construtor(es) que precisares diretamente no teu tipo de implementação.

Métodos de classe de execução, propriedades e eventos

Vimos que o fluxo de trabalho consiste em usar o IDL para declarar a tua classe de runtime e os seus membros, e depois as ferramentas geram protótipos e implementações de stubs para ti. No que diz respeito aos protótipos autogerados dos membros da tua classe em tempo de execução, podes editá-los para que utilizem tipos diferentes dos que declaras no teu IDL. Mas só podes fazer isso desde que o tipo que declaras no IDL possa ser encaminhado para o tipo que declaras na versão implementada.

Eis alguns exemplos.

  • Podes relaxar os tipos de parâmetros. Por exemplo, se no IDL o seu método aceita uma SomeClass, então pode optar por mudar isso para IInspectable na sua implementação. Isto funciona porque qualquer SomeClass pode ser convertida em IInspectable (o contrário, claro, não funcionaria).
  • Podes aceitar um parâmetro copiável por valor, em vez de por referência. Por exemplo, altere SomeClass const& para SomeClass. Isso é necessário quando se pretende evitar capturar uma referência numa corrotina (ver Parameter-passing).
  • Podes relaxar o valor de retorno. Por exemplo, podes mudar o void para winrt::fire_and_forget.

Os dois últimos são muito úteis quando estás a escrever um gestor de eventos assíncrono.

Instanciar e devolver tipos e interfaces de implementação

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

Pode derivar MyType diretamente de winrt::implements (não é uma classe em tempo de execução).

#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;

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

Ou podes gerá-lo a partir do IDL (é uma classe em tempo de execução).

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

Não podes alocar diretamente o tipo de implementação.

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

Mas podes passar do MyType para um objeto IStringable ou IClosable que podes usar ou devolver como parte da tua projeção, chamando o modelo da função winrt::make . Make returns a interface padrão do tipo de implementação.

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

Note

No entanto, se estiveres a referenciar o teu tipo a partir da interface XAML, então haverá tanto um tipo de implementação como um tipo projetado no mesmo projeto. Nesse caso, make devolve uma instância do tipo projectado. Para obter um exemplo de código para esse cenário, veja Controlos XAML; associar 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. Assim, podes chamar IUnknown::as (ou IUnknown::try_as) nele para consultar outros tipos projetados ou interfaces, que também podes usar ou devolver.

Tip

Um cenário em que não deves chamar as ou try_as é a derivação de classes em tempo de execução ("classes componíveis"). Quando um tipo de implementação compõe outra classe, não chame as ou try_as para efetuar uma QueryInterface não verificada ou verificada da classe que está a ser composta. Em vez disso, aceda ao membro de dados (this->) m_inner e chame as ou try_as nesse membro. Para mais informações, veja Derivação de classes em tempo de execução neste tópico.

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

Se precisar de aceder a todos os membros da implementação e, mais tarde, devolver uma interface a um chamador, use o modelo de função winrt::make_self . make_self retorna um winrt::com_ptr que encapsula o tipo de implementação. Pode aceder aos membros de todas as suas interfaces (usando o operador de seta), pode devolvê-lo a um chamador tal como está, ou pode chamar as nele e devolver o objeto da interface resultante a um 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 desta forma podes chamar diretamente os seus métodos de implementação, sem a sobrecarga de uma chamada de função virtual. No exemplo acima, embora MyType::ToString use a mesma assinatura do método projetado no IStringable, estamos a chamar diretamente o método não-virtual, sem cruzar a interface binária da aplicação (ABI). O com_ptr simplesmente contém um ponteiro para a estrutura MyType, pelo que também pode aceder a quaisquer outros detalhes internos de MyType através da variável myimpl e do operador de seta.

No caso de ter um objeto de interface e de saber que se trata de uma interface da sua implementação, pode regressar à implementação usando o modelo de função winrt::get_self. Mais uma vez, é uma técnica que evita chamadas de funções virtuais e permite aceder diretamente à implementação.

Note

Se ainda não instalaste o SDK do Windows versão 10.0.17763.0 (Windows 10, versão 1809), ou posterior, então tens de chamar winrt::from_abi em vez de winrt::get_self.

Eis um exemplo. Há outro exemplo em Implementar a classe de controlo 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 mantém uma referência. Se tu quiseres mantê-lo, então podes 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, por isso não tem função as. Mesmo assim, como pode ver na função ImplFromIClosable apresentada acima, pode aceder aos membros de todas as suas interfaces. Mas se fizeres isso, não devolves a instância bruta do tipo de implementação ao chamador. Em vez disso, use uma das técnicas já mostradas e retorne uma interface projetada, ou um com_ptr.

Se tiver uma instância do seu tipo de implementação e precisar de a passar para uma função que espere o tipo projetado correspondente, então pode fazê-lo, como 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 cppwinrt.exe ferramenta) que torna isto possível. Pode passar um valor de tipo de implementação diretamente a um método que espera um valor do tipo projetado correspondente. Numa função membro de um tipo de implementação, pode passar *this para um método que espera 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 classes em tempo de execução

Podes criar uma classe de runtime que deriva de outra classe de runtime, desde que a classe base seja declarada como "não selada". O termo do Windows Runtime para derivação de classes é "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, não tens de aprender estas regras — podes simplesmente 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, o MyButton deriva do controlo XAML Button , que é fornecido por outro componente. Nesse caso, a implementação assemelha-se à implementação de uma classe não componí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 deriva de outra classe no mesmo componente. Neste caso, a implementação requer um parâmetro modelo adicional que especifique 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 qualquer dos casos, a sua implementação pode chamar um método da classe base, qualificando-o com o base_type alias do tipo:

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 as ou try_as para efetuar uma QueryInterface não verificada ou verificada da classe que está a ser composta. Em vez disso, aceda ao membro de dados (this->) m_inner e chame as ou try_as nesse membro.

Derivar de um tipo que possui um construtor sem predefinição

ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) é um exemplo de um construtor não predefinido. Não existe um construtor predefinido, pelo que, para criar um ToggleButtonAutomationPeer, é necessário passar um owner. Por conseguinte, se derivar de ToggleButtonAutomationPeer, tem de fornecer um construtor que receba um owner e o passe à classe base. Vamos ver como isso fica 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 teu tipo de implementação é assim.

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

A única peça que falta é que precisas de passar esse parâmetro do construtor para a classe base. Lembram-se do padrão de polimorfismo ligado a F que mencionámos acima? Quando estiver familiarizado com os detalhes desse padrão usado pelo C++/WinRT, pode perceber como se chama a sua classe base (ou pode simplesmente procurar no ficheiro de cabeçalho da sua classe de implementação). É assim que se chama o construtor da classe base neste caso.

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

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

Até fazeres a edição descrita acima (para passar esse parâmetro do construtor para a classe base), o compilador irá assinalar o teu construtor e apontar que não existe um construtor padrão apropriado disponível num tipo chamado (neste caso) MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer>. Na verdade, essa é a classe base da classe de baixo do teu tipo de implementação.

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

Como já viste anteriormente neste tópico, existe uma classe de runtime C++/WinRT sob a forma de mais do que uma classe C++ em mais do que um namespace. Assim, o nome MyRuntimeClass tem um significado no espaço de nomes winrt::MyProject e um significado diferente no espaço de nomes winrt::MyProject::implementation. Tenha em atenção qual namespace está atualmente no contexto e use prefixos de namespace caso precise de um nome de outro namespace. Vamos analisar mais de perto os namespaces em questão.

  • winrt::O Meu Projeto. Este espaço de nomes contém tipos projetados. Um objeto de um tipo projetado é um proxy; É essencialmente um apontador inteligente para um objeto de apoio, onde esse objeto de apoio pode ser implementado aqui no teu projeto, ou pode ser implementado noutra unidade de compilação.
  • winrt::MeuProjeto::implementação. Este espaço de nomes contém tipos de implementação. Um objeto de um tipo de implementação não é um apontador; é um valor — um objeto completo de pilha C++. Não construa diretamente um tipo de implementação; Em vez disso, chama winrt::make, passando o teu tipo de implementação como parâmetro do modelo. Já mostrámos anteriormente neste tópico exemplos de winrt::make a ser utilizado, e existe outro exemplo em controlos XAML; associar a uma propriedade C++/WinRT. Veja também Diagnosticar alocações diretas.
  • winrt::MyProject::factory_implementation. Este espaço de nomes contém fábricas. Um objeto neste espaço de nomes suporta IActivationFactory.

Esta tabela mostra a qualificação mínima do espaço de nomes de que necessita para utilizar em diferentes contextos.

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

Important

Quando quiser devolver um tipo projetado da 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 apresentados anteriormente neste tópico, na secção Instanciar e devolver tipos e interfaces de implementação.

O problema com MyRuntimeClass myRuntimeClass; nesse cenário é o facto de criar um objeto winrt::MyProject::implementation::MyRuntimeClass na pilha. Esse objeto (do tipo de implementação) comporta-se de certa forma como o tipo projetado — podes invocar métodos nele da mesma forma; e até converte para um tipo projetado. Mas o objeto é destruído, de acordo com as regras normais do C++, quando se sai do âmbito. Portanto, se devolveste um tipo projetado (um apontador inteligente) a esse objeto, esse ponteiro fica agora pendurado.

Este tipo de bug de corrupção de memória é difícil de diagnosticar. Assim, para compilações de depuração, uma asserção C++/WinRT ajuda-te a detetar este erro, usando um detector de pilha. Mas as corrotinas são atribuídas no heap, por isso não vais receber ajuda com este erro se o cometeres dentro de uma corrotina. Para mais informações, consulte Diagnóstico de alocações diretas.

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

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

Feature Aceita Notes
T (representando um indicador inteligente) Projetado Consulte o aviso em Namespaces: tipos projetados, tipos de implementação e fábricas sobre a utilização inadvertida do tipo de implementação.
agile_ref<T> Both Se usar o tipo de implementação, então o argumento construtor deve 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> Both Se usares o tipo de implementação, então a primeira interface implementada é devolvida.
get_self<T> Implementation Usar o tipo projetado gera o erro: '_abi_TrustLevel': is not a member of 'T'.
guid_of<T>() Both Devolve 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 Usar o 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;) Both Se usar o tipo de implementação, então o argumento deve ser com_ptr<T>.
make_self<T> Implementation Usar o tipo projetado gera o erro: 'Release': is not a member of any direct or indirect base class of 'T'
name_of<T> Projetado Se usares o tipo de implementação, obténs o GUID stringificado da interface predefinida.
weak_ref<T> Both Se usar o tipo de implementação, então o argumento construtor deve ser com_ptr<T>.

Optar por construção uniforme e acesso direto à implementação

Esta secção descreve uma funcionalidade do C++/WinRT 2.0 de ativação opcional, embora esteja ativada por defeito em novos projetos. No caso de um projeto existente, terá de ativar essa opção configurando a ferramenta cppwinrt.exe. No Visual Studio, defina a propriedade do projeto Common Properties>C++/WinRT>Optimized para Yes. Isso tem o efeito de adicionar <CppWinRTOptimized>true</CppWinRTOptimized> ao teu ficheiro de projeto. E tem o mesmo efeito que adicionar o switch ao invocar cppwinrt.exe a partir da linha de comandos.

O -opt[imize] interruptor permite o que muitas vezes se chama construção uniforme. Com uma construção uniforme (ou unificada), utiliza-se a própria projeção da linguagem C++/WinRT para criar e utilizar os seus tipos de implementação (tipos implementados pelo seu componente, para consumo por aplicações) de forma eficiente e sem quaisquer dificuldades no loader.

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

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

Como programador C++ familiarizado com a utilização da biblioteca C++/WinRT, pode querer usar a classe desta forma.

using namespace winrt::MyProject;

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

E isso seria perfeitamente razoável, desde que o código consumidor mostrado não residisse no mesmo componente que implementa esta classe. Como projeção de linguagem, o C++/WinRT protege-te como programador do ABI (a interface binária de aplicações baseada em COM que o Windows Runtime define). O C++/WinRT não chama diretamente a implementação; passa pela ABI.

Consequentemente, na linha de código onde estás a construir um objeto MyClass (MyClass c;), a projeção C++/WinRT chama RoGetActivationFactory para recuperar a classe ou fábrica de ativação, e depois usa essa fábrica para criar o objeto. A última linha também usa a factory para realizar o que parece ser uma chamada estática de método. Tudo isto exige que a sua classe esteja registada e que o seu módulo implemente o ponto de entrada DllGetActivationFactory. O C++/WinRT tem uma cache de fábrica muito rápida, por isso nada disto causa problemas para uma aplicação que consome o seu componente. O problema é que, dentro do teu componente, acabaste de fazer algo que é um pouco problemático.

Por primeiro, independentemente da rapidez da cache da fábrica do C++/WinRT, fazer chamadas através de RoGetActivationFactory (ou mesmo chamadas subsequentes através da cache da fábrica) será sempre mais lento do que invocar diretamente a implementação. Uma chamada para RoGetActivationFactory seguida de IActivationFactory::ActivateInstance seguida de QueryInterface obviamente não será tão eficiente como usar uma expressão C++ new para um tipo definido localmente. Como consequência, os programadores experientes de C++/WinRT estão habituados a utilizar as funções auxiliares winrt::make ou winrt::make_self ao criar objetos no interior de um componente.

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

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

Em segundo lugar, usar a projeção para criar a classe significa que a sua fábrica de ativação será armazenada em cache. Normalmente, é isso que se pretende, mas, se a fábrica estiver no mesmo módulo (DLL) que está a fazer a chamada, então, na prática, mantiveste a DLL carregada e impediste-a de alguma vez ser descarregada. Em muitos casos, isso não importa; mas alguns componentes do sistema têm de suportar o descarregamento.

É aqui que entra o termo construção uniforme . Independentemente de o código de criação residir num projeto que está apenas a consumir a classe, ou se reside no projeto que está realmente a implementar , podes usar livremente a mesma sintaxe para criar o objeto.

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

Quando compila o seu projeto de componente com a opção -opt[imize], a chamada através 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 a sua sintaxe simples e previsível, evita qualquer perda de desempenho ao chamar através da fábrica e evita prender o componente no processo. Para além dos projetos de componentes, isto também é útil para aplicações XAML. Ignorar RoGetActivationFactory para classes implementadas na mesma aplicação permite construí-las (sem necessidade de estarem registadas) de todas as mesmas formas que poderia se estivessem fora do componente.

A estrutura uniforme aplica-se a qualquer chamada processada pela factory nos bastidores. Na prática, isso significa que a otimização serve tanto os construtores como os membros estáticos. Aqui está novamente esse exemplo original.

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

Sem -opt[imize], a primeira e a última instruções requerem chamadas através do objeto de fábrica. Nenhum-opt[imize] deles o faz. E essas chamadas são compiladas diretamente em função da implementação e podem até ser integradas em linha. Isto refere-se ao outro termo frequentemente usado ao falar de -opt[imize], nomeadamente acesso direto à implementação .

As projeções de linguagem são convenientes mas, quando se pode aceder diretamente à implementação, pode-se e deve aproveitar isso para produzir o código mais eficiente possível. O C++/WinRT pode fazer isso por ti, sem te obrigar a abandonar a segurança e produtividade da projeção.

Esta é uma mudança decisiva porque o componente tem de cooperar para permitir que a projeção da linguagem alcance e aceda diretamente aos seus tipos de implementação. Como o C++/WinRT é uma biblioteca apenas de cabeçalho, pode olhar para dentro e ver o que se passa. Sem -opt[imize], o construtor MyClass e o membro StaticMethod são definidos pela projeção desta forma.

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 tudo o que foi dito acima; A intenção é mostrar que ambas as chamadas envolvem uma chamada para uma função chamada call_factory. Essa é a sua pista de que estas chamadas envolvem a cache de fábrica e não estão a aceder diretamente à implementação. Com-opt[imize] isso, estas mesmas funções não estão definidas de todo. Em vez disso, são declarados pela projeção, e as suas definições ficam ao critério do componente.

O componente pode então fornecer definições que ligam diretamente à implementação. Agora chegámos à mudança decisiva. Essas definições são geradas para ti quando usas tanto -component como -opt[imize], e aparecem num ficheiro chamado Type.g.cpp, onde Type é o nome da classe de runtime que está a ser implementada. É por isso que pode deparar-se com vários erros do linker ao ativar -opt[imize] pela primeira vez num projeto existente. Precisas de incluir esse ficheiro gerado na tua implementação para conseguires juntar as coisas.

No nosso exemplo, MyClass.h pode parecer assim (independentemente de estar a ser usado ou não -opt[imize] ).

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

O seu MyClass.cpp é onde tudo se junta.

#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 num projeto existente, é necessário editar o ficheiro .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 ficheiro fornece as definições das funções que a projeção deixou indefinidas. Aqui está como essas definições se apresentam dentro do MyClass.g.cpp ficheiro.

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

E isso completa bem a projeção com chamadas eficientes diretamente para a implementação, evitando chamadas para a cache de fábrica e satisfazendo o linker.

A última coisa que -opt[imize] faz por si é alterar a implementação do ficheiro module.g.cpp do seu projeto (o ficheiro que o ajuda a implementar as exportações DllGetActivationFactory e DllCanUnloadNow da sua DLL) de modo a que as compilações incrementais tendam a ser muito mais rápidas, eliminando o forte acoplamento de tipos exigido pelo C++/WinRT 1.0. Isto é frequentemente referido como fábricas apagadas por tipos. Sem -opt[imize], o module.g.cpp ficheiro gerado para o seu componente começa por incluir as definições de todas as suas classes de implementação — o MyClass.h, neste exemplo. Depois, cria diretamente a fábrica de implementação para cada classe, da seguinte forma.

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

Mais uma vez, não precisas de seguir todos os detalhes. O que é útil perceber é que isto requer a definição completa para todas as classes implementadas pelo seu componente. Isto pode ter um efeito dramático no seu ciclo interno, pois qualquer alteração numa única implementação levará module.g.cpp à recompilação. Com -opt[imize], isto já não é o caso. Em vez disso, duas coisas acontecem ao ficheiro gerado module.g.cpp . A primeira é que já não inclui classes de implementação. Neste exemplo, não incluirá MyClass.h de todo. Em vez disso, cria as fábricas de implementação sem qualquer conhecimento da sua implementação.

void* winrt_make_MyProject_MyClass();

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

Obviamente, não há necessidade de incluir as suas definições, e cabe ao linker resolver a definição da função winrt_make_Component_Class . Claro que não precisas de pensar nisso, porque o MyClass.g.cpp ficheiro que é gerado para ti (e que já incluíste para suportar uma construção uniforme) também define esta função. Aqui está a totalidade do MyClass.g.cpp ficheiro 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 podes ver, a função winrt_make_MyProject_MyClass cria diretamente a fábrica da tua implementação. Tudo isto significa que pode alterar qualquer implementação sem problemas, e o module.g.cpp nem sequer precisa de ser recompilado de todo. Só quando adicionas ou removes classes do Windows Runtime é que module.g.cpp é que será atualizada e precisa de ser recompilada.

Sobrescrever métodos virtuais da classe base

A sua classe derivada pode ter problemas com métodos virtuais se tanto a classe base como a classe derivada forem classes definidas pela aplicação, mas o método virtual estiver definido numa classe avó do Windows Runtime. Na prática, isto acontece se derivares de classes de XAML. O resto desta secçã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 IPageOverrides vtable de BasePage, o que significa que não substitui o método IPageOverrides::OnNavigatedFrom. Uma solução potencial exige que o BasePage seja ele próprio uma classe template e que a sua implementação seja inteiramente num ficheiro de cabeçalho, mas isso torna as coisas inaceitavelmente complicadas.

Como solução alternativa, declare o método OnNavigatedFrom como explicitamente virtual na classe base. Assim, quando a entrada da vtable de DerivedPage::IPageOverrides::OnNavigatedFrom invoca BasePage::IPageOverrides::OnNavigatedFrom, a implementação gerada chama BasePage::OnNavigatedFrom, que, por ser virtual, acaba por chamar 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);
    };
}

Isto exige que todos os membros da hierarquia de classes concordem com o valor de retorno e os tipos de parâmetros do método OnNavigatedFrom . Se discordarem, deverá usar a versão acima como método virtual e encapsular as alternativas.

Note

O seu IDL não precisa de declarar o método substituído. Para mais detalhes, consulte Implementar métodos que podem ser substituídos.

APIs importantes