Partilhar via


APIs de criação com C++/WinRT

Este tópico mostra como criar APIs de C++/WinRT usando o winrt::implements estrutura base, direta ou indiretamente. Os sinónimos de autor 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.

Observação

Este tópico aborda o assunto dos componentes do Tempo de Execução do Windows, mas apenas no contexto do C++/WinRT. Se estiver à procura de 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 Tempo de Execução do Windows (classe de tempo de execução); você só quer implementar uma ou mais interfaces do Tempo de Execução do Windows para consumo local em seu aplicativo. Você deriva diretamente de winrt::implements e implementa funções neste caso.
  • Você está criando uma classe de tempo de execução. Você pode estar a criar um componente para utilização numa aplicação. Ou você pode estar criando um tipo a ser consumido da interface do usuário (UI) XAML e, nesse caso, está implementando e consumindo uma classe de tempo de execução dentro da 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 as APIs C++/WinRT é denominado tipo de implementação .

Importante

É 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 você não estiver criando uma classe de tempo de execução

O cenário mais simples é quando seu tipo está implementando uma interface do Tempo de Execução do Windows e você estará consumindo esse tipo no mesmo aplicativo. Nesse caso, seu tipo não precisa ser uma classe de tempo de execução; apenas uma classe C++ comum. Por exemplo, você pode estar escrevendo um aplicativo baseado em CoreApplication.

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

Observação

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

No Visual Studio, o modelo de projeto Visual C++Windows UniversalCore App (C++/WinRT) ilustra o padrão CoreApplication . O padrão começa com a passagem de uma implementação de Windows::ApplicationModel::Core::IFrameworkViewSource para CoreApplication::Run.

using namespace Windows::ApplicationModel::Core;
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
    IFrameworkViewSource source = ...
    CoreApplication::Run(source);
}

CoreApplication usa a interface para criar a primeira visualização do aplicativo. Conceitualmente, IFrameworkViewSource tem esta aparência.

struct IFrameworkViewSource : IInspectable
{
    IFrameworkView CreateView();
};

Novamente conceitualmente, a implementação do CoreApplication::Run faz isso.

void Run(IFrameworkViewSource viewSource) const
{
    IFrameworkView view = viewSource.CreateView();
    ...
}

Assim, você, como desenvolvedor, implementa o interface de IFrameworkViewSource. C++/WinRT tem o modelo 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 implementae, em seguida, implementa as funções da interface. Veja como.

// App.cpp
...
struct App : implements<App, IFrameworkViewSource>
{
    IFrameworkView CreateView()
    {
        return ...
    }
}
...

Isso está resolvido IFrameworkViewSource. A próxima etapa é retornar um objeto que implemente a interface IFrameworkView. Você também pode optar por implementar essa interface em App. Este próximo exemplo de código representa um aplicativo mínimo que terá, pelo menos, uma janela em execução na área de trabalho.

// App.cpp
...
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    IFrameworkView CreateView()
    {
        return *this;
    }

    void Initialize(CoreApplicationView const &) {}

    void Load(hstring const&) {}

    void Run()
    {
        CoreWindow window = CoreWindow::GetForCurrentThread();
        window.Activate();

        CoreDispatcher dispatcher = window.Dispatcher();
        dispatcher.ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit);
    }

    void SetWindow(CoreWindow const & window)
    {
        // Prepare app visuals here
    }

    void Uninitialize() {}
};
...

Como o seu App tipo é umIFrameworkViewSource, você pode simplesmente passar um para Executar.

using namespace Windows::ApplicationModel::Core;
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
    CoreApplication::Run(winrt::make<App>());
}

Se estiveres a criar uma classe runtime num componente Windows Runtime

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

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

Aqui está um exemplo de declaração de uma classe de tempo de execução 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;
    }
}

Este IDL declara uma classe do Tempo de Execução do Windows (tempo de execução). Uma classe de tempo de execução é um tipo que pode ser ativado e consumido por meio de interfaces COM modernas, normalmente através de limites executáveis. Quando você adiciona um arquivo IDL ao seu projeto e compila, a cadeia de ferramentas C++/WinRT (midl.exe e cppwinrt.exe) gera um tipo de implementação para você. Para um exemplo do fluxo de trabalho do ficheiro IDL em ação, consulte os controlos XAML e veja como ligar a uma propriedade C++/WinRT.

Usando o exemplo IDL acima, o tipo de implementação é um stub struct C++ chamado winrt::MyProject::implementation::MyRuntimeClass em 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 ligado a F está sendo usado (MyRuntimeClass usa a si mesmo como um argumento de modelo para sua base, MyRuntimeClassT). Isso também é chamado de padrão de modelo curiosamente recorrente (CRTP). Se você seguir a cadeia de herança para cima, você vai se deparar com MyRuntimeClass_base.

Você pode simplificar a implementação de propriedades simples usando as Bibliotecas de Implementação do 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á o winrt::implements template de estrutura base.

Para obter mais detalhes, código e um passo a passo sobre a criação de APIs em um componente do Tempo de Execução do Windows, consulte componentes do Tempo de Execução do Windows com C++/WinRT e eventos Author em C++/WinRT.

Se você estiver criando uma classe de tempo de execução a ser referenciada em sua interface do usuário XAML

Se seu tipo for referenciado pela interface do usuário XAML, ele precisará ser uma classe de tempo de execução, mesmo que esteja no mesmo projeto que o XAML. Embora geralmente sejam ativadas através de fronteiras executáveis, uma classe em tempo de execução pode ser usada no seio da unidade de compilação que a implementa.

Nesse cenário, você está a criar e a consumir as APIs. O procedimento para implementar sua classe de tempo de execução é essencialmente o mesmo de um componente do Tempo de Execução do Windows. Portanto, consulte a seção anterior—Se estiver a criar uma classe de execução em um componente do Windows Runtime. O único detalhe que difere é que, do IDL, a cadeia de ferramentas C++/WinRT gera não apenas um tipo de implementação, mas também um tipo projetado. É importante compreender 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 de tempo de execução. Mas esta é realmente uma abstração: declarada em IDL, e implementada em alguma linguagem de programação.
  • MyRuntimeClass é o nome da struct C++ winrt::MyProject::implementation::MyRuntimeClass, que é a implementação C++/WinRT da classe runtime. Como vimos, se houver projetos de implementação e consumo separados, essa estrutura existe apenas no projeto de implementação. Isso é o tipo de implementação, ou a implementação. Este tipo é gerado (pela ferramenta cppwinrt.exe) nos arquivos \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h e MyRuntimeClass.cpp.
  • MyRuntimeClass é o nome do tipo projetado na forma do struct C++ winrt::MyProject::MyRuntimeClass. Se houver projetos de implementação e consumo separados, essa estrutura existe apenas no projeto consumidor. Este é o tipo projetado, ou a projeção. Este 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 exemplo passo a passo da implementação da interface INotifyPropertyChanged numa classe de runtime, consulte os controlos XAML; associo-os a uma propriedade C++/WinRT.

O procedimento para consumir sua classe de tempo de execução neste cenário é descrito em Consumir APIs com C++/WinRT.

Fatoração de classes de tempo de execução em arquivos Midl (.idl)

Os modelos de projeto e item do Visual Studio produzem um arquivo IDL separado para cada classe de tempo de execução. 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 tempo de execução do seu projeto em um único arquivo IDL, isso poderá melhorar significativamente o tempo de compilação. Se, de outra forma, você teria dependências import complexas (ou circulares) entre eles, então a consolidação pode realmente ser necessária. E você pode achar mais fácil criar e revisar suas classes de tempo de execução se elas estiverem juntas.

Construtores de classe de tempo de execução

Aqui estão alguns pontos a reter das listagens que vimos acima.

  • Cada construtor que você declara em seu IDL faz com que um construtor seja gerado em seu tipo de implementação e em seu tipo projetado. Construtores declarados IDL são usados para consumir a classe runtime de uma unidade de compilação diferente.
  • Seja ou não tenha construtor(es) declarado(s) IDL, uma sobrecarga de construtor que aceita std::nullptr_t é gerada no seu tipo projetado. Chamar o construtor std::nullptr_t é a primeira de duas etapas em consumir a classe de tempo de execução 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 runtime de a 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).

Observação

Se espera que a classe de tempo de execução seja utilizada noutra unidade de compilação (o que é comum), então inclua o(os) construtor(es) no seu IDL (pelo menos um construtor padrão). Ao fazer isso, você também terá uma implementação de fábrica juntamente com seu tipo de implementação.

Se você quiser criar e consumir sua classe de tempo de execução somente dentro da mesma unidade de compilação, não declare nenhum construtor (s) em seu 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á eliminado, mas pode facilmente editá-lo e defini-lo como padrão.

Se você quiser criar e consumir sua classe de tempo de execução somente dentro da mesma unidade de compilação, e precisar de parâmetros do construtor, crie o(s) construtor(es) que você precisa diretamente em seu tipo de implementação.

Métodos, propriedades e eventos de classes em tempo de execução

Vimos que o fluxo de trabalho é usar o IDL para declarar sua classe de tempo de execução e seus membros e, em seguida, as ferramentas geram protótipos e implementações de stub para você. Quanto aos protótipos gerados automaticamente para os membros da sua classe de tempo de execução, você pode editá-los para que eles passem por tipos diferentes dos tipos que você declara em seu IDL. Mas você pode fazer isso apenas desde que o tipo que você declara no IDL possa ser encaminhado para o tipo que você declara na versão implementada.

Eis alguns exemplos.

  • Você pode relaxar os tipos de parâmetros. Por exemplo, se no IDL o seu método usa um SomeClass, então pode optar por alterá-lo para IInspectable na 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 copiável por valor, em vez de por referência. Por exemplo, altere SomeClass const& para SomeClass. Isso é necessário quando precisas evitar capturar uma referência para uma co-rotina (ver Passagem de parâmetros).
  • Você pode relaxar o valor de retorno. Por exemplo, 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 tipos e interfaces de implementação

Para esta seção, vamos tomar 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 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 você pode gerá-lo a partir do IDL (é uma classe de tempo de execução).

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

Não é possível alocar diretamente o tipo de implementação.

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

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

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

Observação

No entanto, se você estiver fazendo referência ao seu tipo a partir da interface do usuário XAML, haverá um tipo de implementação e um tipo projetado no mesmo projeto. Nesse caso, faz com que retorne uma instância do tipo projetado. Para obter um exemplo de código desse cenário, consulte controles XAML; vincular 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, você pode chamar IUnknown::as (ou IUnknown::try_as) para consultar outros tipos ou interfaces projetados, que você também pode usar ou retornar.

Sugestão

Um cenário em que você não deve chamar como ou try_as é a derivação de classes 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 não verificado ou verificado da classe que está sendo composta. Em vez disso, acesse o (this->) m_inner membro de dados e chame como ou try_as nele. Para obter mais informações, consulte Derivação de classe de tempo de execução 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 para 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. Você pode acessar os membros de todas as suas interfaces (usando o operador de seta), você pode devolvê-lo a um chamador as-is, ou você pode chamar como nele e retornar o objeto de interface resultante para 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 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, embora MyType::ToString use a mesma assinatura que o método projetado em IStringable, estamos chamando o método não virtual diretamente, sem cruzar a interface binária do aplicativo (ABI). O com_ptr simplesmente mantém um ponteiro para a estrutura MyType, permitindo também o acesso a quaisquer outros detalhes internos de MyType através da variável myimpl e do operador de seta.

No caso de teres um objeto de interface, e saberes que é uma interface na tua implementação, então podes retornar à implementação usando o template de função winrt::get_self. Novamente, é uma técnica que evita chamadas de função virtual e permite que você chegue diretamente na implementação.

Observação

Se você não tiver instalado 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.

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 você quiser mantê-lo, pode então 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 tem como uma função. Mesmo assim, como pode ver na função ImplFromIClosable acima, pode aceder aos membros de todas as suas interfaces. Mas se fizer isso, não devolva a instância do tipo de implementação original ao chamador. Em vez disso, use uma das técnicas já demonstradas e retorne uma interface projetada ou um com_ptr.

Se você tiver uma instância do seu tipo de implementação e precisar passá-la para uma função que espera o tipo projetado correspondente, poderá fazê-lo, 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. De uma função-membro de tipo de implementação, pode-se 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 classe em tempo de execução

Você pode criar uma classe em tempo de execução que deriva de outra classe em tempo de execução, desde que a classe base seja declarada como "não selada". O termo em Windows Runtime para derivação de classe é "classes compósitas". 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 pasta de sources saída produzida pelo cppwinrt.exe compilador.

Considere este exemplo.

// MyProject.idl
namespace MyProject
{
    [default_interface]
    runtimeclass MyButton : Windows.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 Button XAML, que é fornecido por outro componente. Nesse caso, a implementação se parece com a implementação de uma classe não compostá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, a sua implementação pode chamar um método da classe base qualificando-o com o alias do 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();
        }
    };
}

Sugestão

Quando um tipo de implementação compõe outra classe, não chame como ou try_as para executar um QueryInterface não verificado ou verificado da classe que está sendo composta. Em vez disso, acesse o (this->) m_inner membro de dados e chame como 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á nenhum construtor padrão, portanto, para construir um ToggleButtonAutomationPeer, você precisa passar um proprietário . Consequentemente, se tu derivares de ToggleButtonAutomationPeer, então precisas fornecer um construtor que aceita um proprietário e o passa para a base. Vamos ver como isso se parece na prática.

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

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

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

A única parte que falta é que você precisa passar esse parâmetro do construtor para a classe base. Lembra-se do padrão de polimorfismo ligado a F que mencionamos acima? Depois de estar familiarizado com os detalhes desse padrão usado pelo C++/WinRT, você pode descobrir como sua classe base é chamada (ou você pode apenas olhar no arquivo de cabeçalho da sua classe de implementação). Este é como chamar o construtor de classe base neste caso.

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

O construtor da classe base requer um botão de alternância . E MySpecializedToggleButton é umToggleButton.

Até que você faça a edição descrita acima (para passar esse parâmetro do 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>. Essa é, na verdade, a classe base da classe 'bass' do seu tipo de implementação.

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

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

  • winrt::MeuProjeto. Este namespace contém tipos projetados. Um objeto de um tipo projetado é um proxy; É essencialmente um ponteiro inteligente para um objeto de apoio, onde esse objeto de suporte pode ser implementado aqui no seu projeto, ou pode ser implementado em outra unidade de compilação.
  • winrt::MyProject::implementation. Este 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 template. Mostrámos exemplos de winrt::make em ação anteriormente neste tópico, e há outro exemplo em controlos XAML, onde se vincula a uma propriedade C++/WinRT. Veja também Diagnóstico de alocações diretas.
  • winrt::MeuProjeto::factory_implementation. Este namespace contém fábricas. Um objeto neste namespace suporta IActivationFactory.

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

O namespace em contexto Para especificar o tipo projetado Para especificar o tipo de implementação
winrt::Meu Projeto MyRuntimeClass implementation::MyRuntimeClass
winrt::MyProject::Implementation MyProject::MyRuntimeClass MyRuntimeClass

Importante

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é se converte para um tipo projetado. Mas o objeto é destruído, de acordo com as regras normais do C++, quando o escopo é encerrado. Portanto, se você retornou um tipo projetado (um ponteiro inteligente) para esse objeto, esse ponteiro agora está pendurado.

Este tipo de bug de corrupção de memória é difícil de diagnosticar. Portanto, para compilações de debug, uma asserção C++/WinRT ajuda você a detectar esse erro, usando um stack-detector. Mas as corrotinas são alocadas no heap, então você não terá ajuda com esse erro se o cometer dentro de uma corrotina. Para obter mais informações, consulte Diagnóstico das 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 uma funcionalidade C++/WinRT espera um tipo, e qual tipo ele espera (tipo projetado, tipo de implementação ou ambos).

Característica Aceita Observações
T (representando um ponteiro inteligente) Projetado Veja a advertência em Namespaces: tipos projetados, tipos de implementação e fábricas sobre o uso acidental do tipo de implementação.
agile_ref<T> Ambos Se você usar o tipo de implementação, o argumento do construtor deve ser com_ptr<T>.
com_ptr<T> Execução 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> Execução 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 — consulte o aviso em Namespaces: tipos projetados, tipos de implementação e fábricas.
make<T> Execução 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;) Ambos Se você usar o tipo de implementação, o argumento deverá ser com_ptr<T>.
make_self<T> Execução 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 você usar o tipo de implementação, obterá o GUID stringified da interface padrão.
weak_ref<T> Ambos Se você usar o tipo de implementação, o argumento do construtor deve ser com_ptr<T>.

Opte pela construção uniforme e 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, você precisará optar por participar 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 seu arquivo de projeto. E tem o mesmo efeito que adicionar o switch ao invocar cppwinrt.exe da linha de comando.

O interruptor -opt[imize] permite o que é frequentemente chamado de construção uniforme. Com 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 consumo por aplicativos) de forma eficiente e sem dificuldades de carregador.

Antes de descrever a característica, vamos primeiro mostrar a situação sem uma construção uniforme. Para ilustrar, começaremos com este exemplo de classe do Tempo de Execução do Windows.

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

Como um desenvolvedor C++ familiarizado com o uso da biblioteca C++/WinRT, você pode querer 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 dentro do mesmo componente que implementa essa classe. Como uma projeção de linguagem, o C++/WinRT protege você como desenvolvedor da ABI (a interface binária de aplicativo baseada em COM que o Tempo de Execução do Windows define). C++/WinRT não chama diretamente a implementação; passa pela ABI.

Consequentemente, na linha de código em que você está construindo um objeto MyClass (), 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 fazer o que parece ser uma chamada de método estático. Tudo isso requer que sua classe seja registrada e que seu módulo implemente o DllGetActivationFactory ponto de entrada. 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, não importa quão rápido seja o cache da fábrica do C++/WinRT, chamar através de RoGetActivationFactory (ou mesmo chamadas subsequentes através do cache da fábrica) sempre será mais lento do que chamar diretamente na implementação. Uma chamada para RoGetActivationFactory seguida por IActivationFactory::ActivateInstance seguida por QueryInterface obviamente não será tão eficiente quanto usar uma expressão C++ new para um tipo definido localmente. Como consequência, desenvolvedores experientes em 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 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 sua fábrica de ativação será armazenada em cache. Normalmente, é isso que você quer, mas se a fábrica reside no mesmo módulo (DLL) que está fazendo a chamada, então você efetivamente fixou a DLL e impediu que ela fosse descarregada. Em muitos casos, isso não importa; mas alguns componentes do sistema devem suportar o descarregamento.

É aqui que entra o termo de 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 classe, você pode usar livremente a mesma sintaxe para criar o objeto.

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

Quando você cria seu projeto de componente com a opção -opt[imize], a chamada através da projeção de linguagem é compilada para a 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 impacto no desempenho ao chamar através da fábrica e evita a fixação do componente no processo. Além de projetos de componentes, isso também é útil para aplicativos XAML. Ignorar RoGetActivationFactory para classes implementadas na mesma aplicação permite que você as construa (sem precisarem ser registadas) de todas as formas que poderia se estivessem fora do seu componente.

A construção uniforme aplica-se a qualquer chamada de que é servida pela fábrica sob o capô. Na prática, isso significa que a otimização serve tanto aos construtores quanto aos membros estáticos. Aqui está o exemplo original novamente.

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

Sem -opt[imize], a primeira e a última instrução exigem chamadas através do objeto de fábrica. Com-opt[imize], nenhum deles o faz. E essas chamadas são compiladas diretamente contra a implementação, e até têm potencial para ser embutidas. O que remete para o outro termo frequentemente utilizado quando se fala de -opt[imize], ou seja, implementação direta acesso.

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. C++/WinRT pode fazer isso por você, sem forçá-lo a deixar a segurança e a produtividade da projeção.

Esta é uma mudança de rutura porque o componente deve cooperar para permitir que a projeção de linguagem alcance e acesse diretamente seus tipos de implementação. Como o 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 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 todos os itens 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 essas chamadas envolvem o cache de fábrica e não estão acessando diretamente a implementação. Com-opt[imize], essas mesmas funções não são definidas. Em vez disso, eles são declarados pela projeção, e suas definições são deixadas para o componente.

O componente pode então fornecer definições que chamam diretamente para a implementação. Agora chegámos à alteração disruptiva. Essas definições são geradas para você quando você usa -component e -opt[imize]e aparecem em um arquivo chamado Type.g.cpp, onde Type é o nome da classe de tempo de execução que está sendo implementada. É por isso que podem ocorrer vários erros do linking quando ativa -opt[imize], pela primeira vez, num projeto existente. Você precisa incluir esse arquivo gerado na sua implementação para integrar todos os elementos.

No nosso exemplo, MyClass.h pode ter esta aparência (independentemente de -opt[imize] estar a ser utilizado).

// 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 em um projeto existente, você precisa editar o arquivo de .cpp de cada implementação para que você #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 das funções que a projeção deixou indefinidas. Veja como essas definições se parecem dentro do arquivo MyClass.g.cpp.

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

E isso conclui elegantemente a projeção com chamadas eficientes diretamente na implementação, evitando chamadas para o cache da fábrica e satisfazendo o vinculador.

A última coisa que -opt[imize] faz por você é alterar a implementação do module.g.cpp do seu projeto (o arquivo que ajuda você a implementar o DllGetActivationFactory da sua DLL e DllCanUnloadNow exportações) de tal forma que as compilações incrementais tenderão a ser muito mais rápidas, eliminando o acoplamento de tipo forte que era exigido pelo C++/WinRT 1.0. Isto é muitas vezes referido como fábricas apagadas. Sem -opt[imize], o arquivo de module.g.cpp gerado para seu componente começa incluindo as definições de todas as suas classes de implementação — a 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 toda e qualquer classe implementada pelo seu componente. Isso pode ter um efeito dramático em seu loop interno, pois qualquer alteração em uma única implementação fará com que module.g.cpp recompile. Com -opt[imize], isso já não acontece. Em vez disso, duas coisas acontecem com o arquivo module.g.cpp gerado. A primeira é que ele não inclui mais nenhuma classe de implementação. Neste exemplo, ele não incluirá MyClass.h de forma alguma. Em vez disso, cria as fábricas de implementação sem qualquer conhecimento do seu funcionamento.

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

Obviamente, não há necessidade de incluir suas definições, e cabe ao vinculador resolver a definição da função winrt_make_Component_Class. Claro, você não precisa pensar sobre isso, porque o arquivo MyClass.g.cpp que é gerado para você (e que você incluiu anteriormente para suportar a construção uniforme) também define essa função. Aqui está a totalidade do arquivo de MyClass.g.cpp 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 sua implementação. Isso tudo significa que você pode alterar alegremente qualquer implementação, e o module.g.cpp não precisa ser recompilado. É somente quando você adiciona ou remove classes do Tempo de Execução do Windows que module.g.cpp serão atualizadas e precisarão ser recompiladas.

Sobrepondo métodos virtuais da classe base

Sua classe derivada pode ter problemas com métodos virtuais se a classe base e a classe derivada forem classes definidas pelo aplicativo, mas o método virtual for definido em uma classe avó do Tempo de Execução do Windows. Na prática, isto acontece ao derivar de classes XAML. O restante desta seção continua o exemplo de Classes Derivadas.

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

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

A hierarquia é Windows::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 o vtable de IPageOverrides de BasePage, o que significa que ela falha em substituir o método IPageOverrides::OnNavigatedFrom. Uma solução potencial requer que a BasePage seja ela própria uma classe modelo e tenha a sua implementação inteiramente num ficheiro de cabeçalho, mas isso torna as coisas complicadas de forma inaceitável.

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 natureza virtual) acaba chamando DerivedPage::OnNavigatedFrom.

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

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

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

Observação

Seu IDL não precisa declarar o método substituído. Para obter mais detalhes, consulte Implementando métodos substituíveis.

APIs importantes