Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En este tema se muestra cómo crear API de C++/WinRT mediante la estructura base winrt::implements , ya sea directa o indirectamente. Los sinónimos de crear en este contexto son producir o implementar. En este tema se tratan los siguientes escenarios para implementar API en un tipo C++/WinRT, en este orden.
Note
En este tema se aborda el tema de los componentes de Windows Runtime, pero solo en el contexto de C++/WinRT. Si busca contenido sobre Windows Runtime componentes que abarcan todos los idiomas de Windows Runtime, consulte Windows Runtime componentes.
- No está definiendo una clase de Windows Runtime (clase de entorno de ejecución); simplemente quiere implementar una o varias interfaces de Windows Runtime para su uso local en la aplicación. Derivas directamente de winrt::implements en este caso, e implementas funciones.
- Está creando una clase en tiempo de ejecución. Es posible que cree un componente que se va a consumir desde una aplicación. O bien, es posible que esté definiendo un tipo para que se consuma desde una interfaz de usuario XAML y, en ese caso, implementará y consumirá una clase del entorno de ejecución dentro de la misma unidad de compilación. En estos casos, permite que las herramientas generen clases para usted que derivan de winrt::implements.
En ambos casos, el tipo que implementa las API de C++/WinRT se denomina tipo de implementación.
Important
Es importante distinguir el concepto de un tipo de implementación del tipo proyectado. El tipo proyectado se describe en Consumir APIs con C++/WinRT.
Si no está creando una clase en tiempo de ejecución
El escenario más sencillo se da cuando tu tipo implementa una interfaz de Windows Runtime y vas a usar ese tipo en la misma aplicación. En ese caso, el tipo no necesita ser una clase en tiempo de ejecución; simplemente una clase de C++ normal. Por ejemplo, podría escribir una aplicación de escritorio de WinUI 3 basada en Microsoft::UI::Xaml::Application.
Si la interfaz de usuario XAML hace referencia a tu tipo, debe ser una clase en tiempo de ejecución, aunque esté en el mismo proyecto que el XAML. Para ese caso, consulta la sección Si vas a crear una clase en tiempo de ejecución a la que se hace referencia en la interfaz de usuario XAML.
Note
Para obtener información sobre cómo instalar y usar la extensión de Visual Studio de C++/WinRT (VSIX) y el paquete NuGet (que juntos proporcionan compatibilidad con la compilación y la plantilla de proyecto), consulta Visual Studio compatibilidad con C++/WinRT.
En Visual Studio, la plantilla de proyecto Aplicación vacía, empaquetada (WinUI 3 en escritorio) para C++ muestra el patrón de aplicación WinUI 3. La clase App deriva de Microsoft::UI::Xaml::Application y el punto de entrada llama a su 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>(); });
}
La clase App crea una Microsoft::UI::Xaml::Window y la activa en 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();
}
C++/WinRT tiene la plantilla de estructura base winrt::implements para facilitar la implementación de una interfaz (o varias) sin recurrir a la programación de estilo COM. Simplemente haz que tu tipo derive de implements y luego implementa las funciones de la interfaz. Este es un ejemplo que implementa una interfaz personalizada.
struct MyType : implements<MyType, IStringable>
{
hstring ToString()
{
return L"MyType";
}
};
Si va a crear una clase en tiempo de ejecución en un componente de Windows Runtime
Si su tipo está empaquetado en un componente de Windows Runtime para que lo consuma otro binario (normalmente, una aplicación), entonces su tipo debe ser una clase de runtime. Declara una clase en tiempo de ejecución en un archivo de lenguaje de definición de interfaz (IDL) (.idl) de Microsoft (vea Factoring runtime classes into Midl files (.idl)).
Cada archivo IDL genera un archivo .winmd, y Visual Studio fusiona todos ellos en un único archivo con el mismo nombre que el espacio de nombres raíz. Ese archivo final .winmd será al que harán referencia los usuarios de tu componente.
Este es un ejemplo de declaración de una clase en tiempo de ejecución en un archivo 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 una clase de Windows Runtime (runtime). Una clase en tiempo de ejecución es un tipo que se puede activar y consumir a través de interfaces COM modernas, normalmente a través de límites ejecutables. Al agregar un archivo IDL al proyecto y compilar, la cadena de herramientas de C++/WinRT (midl.exe y cppwinrt.exe) genera un tipo de implementación automáticamente. Para obtener un ejemplo del flujo de trabajo de archivos IDL en acción, consulta Controles XAML; enlazar a una propiedad C++/WinRT.
Usando el IDL de ejemplo anterior, el tipo de implementación es un stub de estructura de C++ llamado winrt::MyProject::implementation::MyRuntimeClass en los archivos de código fuente llamados \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h y MyRuntimeClass.cpp.
El tipo de implementación tiene este aspecto.
// 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.
Tenga en cuenta que el patrón de polimorfismo con límite F que se está utilizando (MyRuntimeClass se usa a sí misma como argumento de plantilla de su clase base, MyRuntimeClassT). Esto también se denomina patrón de plantilla recurrente curiosamente (CRTP). Si sigue la cadena de herencia hacia arriba, se encontrará MyRuntimeClass_base.
Puede simplificar la implementación de propiedades simples mediante Windows Bibliotecas de implementación (WIL). A continuación se muestra cómo hacerlo:
// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
{
MyRuntimeClass() = default;
wil::single_threaded_rw_property<winrt::hstring> Name;
};
}
Consulte Propiedades simples.
template <typename D, typename... I>
struct MyRuntimeClass_base : implements<D, MyProject::IMyRuntimeClass, I...>
Por lo tanto, en este escenario, en la raíz de la jerarquía de herencia se encuentra una vez más la plantilla de estructura base winrt::implements.
Para obtener más información, código y un tutorial sobre la creación de API en un componente de Windows Runtime, consulte Componentes de Windows Runtime con C++/WinRT y Creación de eventos en C++/WinRT.
Si vas a crear una clase en tiempo de ejecución a la que se hace referencia en la interfaz de usuario XAML
Si tu tipo está referenciado por la interfaz de usuario XAML, entonces debe ser una clase de tiempo de ejecución, aunque se encuentre en el mismo proyecto que el XAML. Aunque normalmente se activan a través de límites ejecutables, se puede usar una clase en tiempo de ejecución dentro de la unidad de compilación que la implementa.
En este escenario, va a crear y consumir las API. El procedimiento para implementar la clase en tiempo de ejecución es básicamente el mismo que para un componente de Windows Runtime. Por lo tanto, consulte la sección anterior: si va a crear una clase en tiempo de ejecución en un componente de Windows Runtime. El único detalle que difiere es que, de IDL, la cadena de herramientas de C++/WinRT genera no solo un tipo de implementación, sino también un tipo proyectado. Es importante apreciar que decir solo "MyRuntimeClass" en este escenario puede ser ambiguo; hay varias entidades con ese nombre, de diferentes tipos.
- MyRuntimeClass es el nombre de una clase en tiempo de ejecución. Pero esto es realmente una abstracción: declarada en IDL e implementada en algún lenguaje de programación.
-
MyRuntimeClass es el nombre de la estructura de C++ winrt::MyProject::implementation::MyRuntimeClass, que es la implementación de C++/WinRT de la clase en tiempo de ejecución. Como hemos visto, si hay proyectos de implementación y consumo independientes, esta estructura solo existe en el proyecto de implementación. Este es el tipo de implementación o la implementación. Este tipo se genera (mediante la
cppwinrt.exeherramienta) en los archivos\MyProject\MyProject\Generated Files\sources\MyRuntimeClass.hyMyRuntimeClass.cpp. -
MyRuntimeClass es el nombre del tipo proyectado en forma de la estructura de C++ winrt::MyProject::MyRuntimeClass. Si hay proyectos de implementación y consumo independientes, esta estructura solo existe en el proyecto de consumo. Este es el tipo proyectado o la proyección. Este tipo se genera (por
cppwinrt.exe) en el archivo\MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h.
Estas son las partes del tipo proyectado que son relevantes para este tema.
// MyProject.2.h
...
namespace winrt::MyProject
{
struct MyRuntimeClass : MyProject::IMyRuntimeClass
{
MyRuntimeClass(std::nullptr_t) noexcept {}
MyRuntimeClass();
};
}
Para ver un tutorial de ejemplo sobre cómo implementar la interfaz INotifyPropertyChanged en una clase en tiempo de ejecución, consulta Controles XAML; enlazar a una propiedad de C++/WinRT.
El procedimiento para consumir su clase en tiempo de ejecución en este escenario se describe en Consumir API con C++/WinRT.
Factorización de clases en tiempo de ejecución en archivos Midl (.idl)
Las plantillas de proyecto y elemento de Visual Studio generan un archivo IDL independiente para cada clase en tiempo de ejecución. Esto proporciona una correspondencia lógica entre un archivo IDL y sus archivos de código fuente generados.
Sin embargo, si consolida todas las clases en tiempo de ejecución del proyecto en un único archivo IDL, esto puede mejorar significativamente el tiempo de compilación. Si de otro modo hubiera dependencias complejas (o circulares) import entre ellas, entonces consolidarlas puede ser realmente necesario. Y es posible que le resulte más fácil crear y revisar las clases en tiempo de ejecución si están juntas.
Constructores de clases en tiempo de ejecución
Estos son algunos puntos clave que podemos extraer de los listados que hemos visto más arriba.
- Cada constructor que declare en el IDL hace que se genere un constructor en el tipo de implementación y en el tipo proyectado. Los constructores declarados por IDL se usan para consumir la clase en tiempo de ejecución de una unidad de compilación diferente .
- Tanto si tengas constructores declarados en IDL como si no, se genera una sobrecarga de constructor que acepta std::nullptr_t en tu tipo proyectado. Llamar al constructor std::nullptr_t es el primero de dos pasos para consumir la clase en tiempo de ejecución desde la misma unidad de compilación. Para obtener más información y un ejemplo de código, consulte Consumir API con C++/WinRT.
- Si usa la clase en tiempo de ejecución desde la misma unidad de compilación, también puede implementar constructores no predeterminados directamente en el tipo de implementación (que, recuerde, está en
MyRuntimeClass.h).
Note
Si espera que la clase en tiempo de ejecución se consuma desde una unidad de compilación diferente (que es común), incluya constructores en el IDL (al menos un constructor predeterminado). Al hacerlo, también obtendrás una implementación de factoría junto con tu tipo de implementación.
Si desea crear y consumir la clase en tiempo de ejecución solo dentro de la misma unidad de compilación, no declare ningún constructor en su IDL. No necesita una implementación de fábrica y no se generará una. El constructor predeterminado del tipo de implementación se eliminará, pero puede editarlo fácilmente y configurarlo de forma predeterminada en su lugar.
Si desea crear y consumir la clase en tiempo de ejecución solo dentro de la misma unidad de compilación y necesita parámetros de constructor, cree los constructores que necesita directamente en el tipo de implementación.
Métodos, propiedades y eventos de clase en tiempo de ejecución
Hemos visto que el flujo de trabajo consiste en usar IDL para declarar la clase del entorno de ejecución y sus miembros, y que, a continuación, las herramientas generen prototipos e implementaciones esqueleto automáticamente. En cuanto a los prototipos generados automáticamente para los miembros de la clase en tiempo de ejecución, puede editarlos para que pasen por distintos tipos de los tipos que declare en su IDL. Pero solo puede hacerlo siempre que el tipo que declare en IDL se pueda reenviar al tipo que declare en la versión implementada.
A continuación, encontrará algunos ejemplos.
- Puede flexibilizar los tipos de parámetros. Por ejemplo, si en IDL el método toma una clase SomeClass, podría optar por cambiarla a IInspectable en la implementación. Esto funciona porque cualquier SomeClass se puede reenviar a IInspectable (el inverso, por supuesto, no funcionaría).
- Puede aceptar un parámetro copiable por valor, en lugar de por referencia. Por ejemplo, cambie
SomeClass const&aSomeClass. Esto es necesario cuando es necesario evitar capturar una referencia en una corrutina (consulte Paso de parámetros). - Puede flexibilizar el valor de retorno. Por ejemplo, puede cambiar void a winrt::fire_and_forget.
Los dos últimos son muy útiles cuando se escribe un controlador de eventos asincrónico.
Creación de instancias y devolución de tipos e interfaces de implementación
En esta sección, vamos a tomar como ejemplo un tipo de implementación denominado MyType, que implementa las interfaces IStringable e IClosable .
Puede derivar MyType directamente desde winrt::implements (no es una clase en tiempo de ejecución).
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
struct MyType : implements<MyType, IStringable, IClosable>
{
winrt::hstring ToString(){ ... }
void Close(){}
};
O bien, puede generarlo a partir de IDL (es una clase en tiempo de ejecución).
// MyType.idl
namespace MyProject
{
runtimeclass MyType: Windows.Foundation.IStringable, Windows.Foundation.IClosable
{
MyType();
}
}
No se puede asignar directamente el tipo de implementación.
MyType myimpl; // error C2259: 'MyType': cannot instantiate abstract class
Pero puede pasar de MyType a un objeto IStringable o IClosable que puede usar o devolver como parte de su proyección llamando a la plantilla de función winrt::make. make devuelve la interfaz predeterminada del tipo de implementación.
IStringable istringable = winrt::make<MyType>();
Note
Sin embargo, si haces referencia al tipo desde la interfaz de usuario XAML, habrá un tipo de implementación y un tipo proyectado en el mismo proyecto. En ese caso, make devuelve una instancia del tipo proyectado. Para obtener un ejemplo de código de ese escenario, consulta Controles XAML; enlazar a una propiedad C++/WinRT.
Solo podemos usar istringable (en el ejemplo de código anterior) para llamar a los miembros de la interfaz IStringable . Pero una interfaz de C++/WinRT (que es una interfaz proyectada) deriva de winrt::Windows::Foundation::IUnknown. Por lo tanto, puede llamar a IUnknown::as (o IUnknown::try_as) sobre él para obtener otros tipos o interfaces proyectados, que luego puede usar o devolver.
Tip
Un escenario en el que no se debe llamar como o try_as es la derivación de clases en tiempo de ejecución ("clases que se pueden componer"). Cuando un tipo de implementación compone otra clase, no llame a as ni a try_as para efectuar una QueryInterface sin comprobación o con comprobación de la clase que se está componiendo. En su lugar, acceda al miembro de datos (this->) m_inner, y llame a as o try_as sobre este. Para obtener más información, consulte Derivación de clases en tiempo de ejecución en este tema.
istringable.ToString();
IClosable iclosable = istringable.as<IClosable>();
iclosable.Close();
Si necesita acceder a todos los miembros de la implementación y, después, devolver una interfaz a un autor de llamada, use la plantilla de función winrt::make_self . make_self devuelve un winrt::com_ptr encapsulando el tipo de implementación. Puede acceder a los miembros de todas sus interfaces (mediante el operador de flecha), puede devolverlo tal cual a quien realiza la llamada, o puede invocar as en él y devolver el objeto de interfaz resultante a quien realiza la llamada.
winrt::com_ptr<MyType> myimpl = winrt::make_self<MyType>();
myimpl->ToString();
myimpl->Close();
IClosable iclosable = myimpl.as<IClosable>();
iclosable.Close();
La clase MyType no forma parte de la proyección; es la implementación. Pero de esta manera puede invocar directamente sus métodos de implementación, sin la sobrecarga de una llamada a función virtual. En el ejemplo anterior, aunque MyType::ToString usa la misma firma que el método proyectado en IStringable, llamamos al método no virtual directamente, sin cruzar la interfaz binaria de la aplicación (ABI). El com_ptr simplemente contiene un puntero a la estructura MyType , por lo que también puede acceder a cualquier otro detalle interno de MyType a través de la myimpl variable y el operador de flecha.
En el caso de que tengas un objeto de interfaz y sepas que corresponde a una interfaz de tu implementación, puedes volver a la implementación usando la plantilla de función winrt::get_self. De nuevo, es una técnica que evita las llamadas a funciones virtuales y permite acceder directamente a la implementación.
Note
Si no ha instalado el SDK de Windows versión 10.0.17763.0 (Windows 10, versión 1809) o posterior, debe llamar a winrt::from_abi en lugar de winrt::get_self.
Este es un ejemplo. Hay otro ejemplo en Implementar la clase de control personalizado BgLabelControl.
void ImplFromIClosable(IClosable const& from)
{
MyType* myimpl = winrt::get_self<MyType>(from);
myimpl->ToString();
myimpl->Close();
}
Pero solo el objeto de interfaz original se mantiene en una referencia. Si quiere conservarlo, puede llamar a 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.
El propio tipo de implementación no deriva de winrt::Windows::Foundation::IUnknown, por lo que no tiene la función as. Aun así, como se puede ver en la función ImplFromIClosable mostrada arriba, puede acceder a los miembros de todas sus interfaces. Pero si haces eso, no devuelvas directamente a quien llama la instancia del tipo de implementación. En su lugar, use una de las técnicas que ya se muestran y devuelva una interfaz proyectada o un com_ptr.
Si tiene una instancia del tipo de implementación y necesita pasarla a una función que espera el tipo proyectado correspondiente, puede hacerlo, como se muestra en el ejemplo de código siguiente. Existe un operador de conversión en su tipo de implementación (siempre que el tipo de implementación haya sido generado por la herramienta cppwinrt.exe) que hace esto posible. Puede pasar un valor de tipo de implementación directamente a un método que espera un valor del tipo proyectado correspondiente. Desde una función miembro de un tipo de implementación, se puede pasar *this a un método que espera un valor del tipo proyectado correspondiente.
// 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);
}
...
Derivación de clases en tiempo de ejecución
Puede crear una clase en tiempo de ejecución que derive de otra clase en tiempo de ejecución, siempre que la clase base se declare como "no sellada". El término Windows Runtime para la derivación de clases es "clases que se pueden componer". El código para implementar una clase derivada depende de si otro componente o el mismo componente proporciona la clase base. Afortunadamente, no tiene que aprender estas reglas; simplemente puede copiar las implementaciones de ejemplo de la sources carpeta de salida generada por el cppwinrt.exe compilador.
Considere este ejemplo.
// 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();
}
}
En el ejemplo anterior, MyButton se deriva del control Botón XAML, que proporciona otro componente. En ese caso, la implementación es similar a la implementación de una clase no compuesta:
namespace winrt::MyProject::implementation
{
struct MyButton : MyButtonT<MyButton>
{
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyButton : MyButtonT<MyButton, implementation::MyButton>
{
};
}
Por otro lado, en el ejemplo anterior, MyDerived se deriva de otra clase en el mismo componente. En este caso, la implementación requiere un parámetro de plantilla adicional que especifique la clase de implementación para la clase base.
namespace winrt::MyProject::implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
{ // ^^^^^^^^^^^^^^^^^^^^^^
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyDerived>
{
};
}
En cualquier caso, su implementación puede llamar a un método de la clase base calificándolo con el 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
Cuando un tipo de implementación compone otra clase, no llame a as ni a try_as para efectuar una QueryInterface sin comprobación o con comprobación de la clase que se está componiendo. En su lugar, acceda al miembro de datos (this->) m_inner, y llame a as o try_as sobre este.
Derivación de un tipo que tiene un constructor no predeterminado
ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) es un ejemplo de un constructor no predeterminado. No existe un constructor por defecto, así que, para crear un ToggleButtonAutomationPeer, debe pasar un owner. Por lo tanto, si deriva de ToggleButtonAutomationPeer, debe proporcionar un constructor que tome owner y lo pase a la clase base. Veamos cómo es eso en la práctica.
// 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);
};
}
El constructor generado para el tipo de implementación tiene este aspecto.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner)
{
...
}
...
Lo único que falta es que necesitas pasar ese parámetro del constructor a la clase base. ¿Recuerdas el patrón de polimorfismo enlazado a F que mencionamos anteriormente? Una vez que esté familiarizado con los detalles de ese patrón que usa C++/WinRT, puede averiguar a qué se llama a la clase base (o simplemente puede buscar en el archivo de encabezado de la clase de implementación). Así es como llamar al constructor de clase base en este caso.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner) :
MySpecializedToggleButtonAutomationPeerT<MySpecializedToggleButtonAutomationPeer>(owner)
{
...
}
...
El constructor de clase base espera un ToggleButton. Y MySpecializedToggleButtones unToggleButton.
Hasta que realice la edición descrita anteriormente (para pasar ese parámetro de constructor a la clase base), el compilador marcará el constructor y señalará que no hay ningún constructor predeterminado adecuado disponible en un tipo llamado (en este caso) MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer>. En realidad, es la clase base de la clase bass de tu tipo de implementación.
Espacios de nombres: tipos proyectados, tipos de implementación y factorías
Como ha visto anteriormente en este tema, existe una clase en tiempo de ejecución de C++/WinRT en forma de más de una clase de C++ en más de un espacio de nombres. Por lo tanto, el nombre MyRuntimeClass tiene un significado en el espacio de nombres winrt::MyProject y un significado diferente en el espacio de nombres winrt::MyProject::implementation . Tenga en cuenta qué espacio de nombres tiene actualmente en contexto y, a continuación, use prefijos de espacio de nombres si necesita un nombre de otro espacio de nombres. Echemos un vistazo más detenidamente a los espacios de nombres en cuestión.
- winrt::MyProject. Este espacio de nombres contiene tipos proyectados. Un objeto de un tipo proyectado es un proxy; es básicamente un puntero inteligente a un objeto de respaldo, donde ese objeto de respaldo podría implementarse aquí en el proyecto, o bien podría implementarse en otra unidad de compilación.
- winrt::MyProject::implementation. Este espacio de nombres contiene tipos de implementación. Un objeto de un tipo de implementación no es un puntero; es un valor: un objeto de pila de C++ completo. No construya directamente un tipo de implementación; en su lugar, llame a winrt::make y pase el tipo de implementación como parámetro de plantilla. Hemos mostrado ejemplos de winrt::make en acción anteriormente en este tema y hay otro ejemplo en controles XAML; enlazar a una propiedad C++/WinRT. Consulte también Diagnóstico de asignaciones directas.
- winrt::MyProject::factory_implementation. Este espacio de nombres contiene factorías. Un objeto de este espacio de nombres admite IActivationFactory.
Esta tabla muestra la cualificación mínima del espacio de nombres que necesita usar en distintos contextos.
| Espacio de nombres que está en contexto | Para especificar el tipo proyectado | Para especificar el tipo de implementación |
|---|---|---|
| winrt::MyProject | MyRuntimeClass |
implementation::MyRuntimeClass |
| winrt::MyProject::implementation | MyProject::MyRuntimeClass |
MyRuntimeClass |
Important
Cuando quiera devolver un tipo proyectado desde su implementación, tenga cuidado de no crear una instancia del tipo de implementación escribiendo MyRuntimeClass myRuntimeClass;. Las técnicas y el código correctos para ese escenario se muestran anteriormente en este tema en la sección Creación de instancias y devolución de tipos e interfaces de implementación.
El problema con MyRuntimeClass myRuntimeClass; en ese escenario es que crea un objeto winrt::MyProject::implementation::MyRuntimeClass en la pila. Ese objeto (de tipo de implementación) se comporta como el tipo proyectado de algunas maneras: puede invocar métodos en él de la misma manera; e incluso se convierte en un tipo proyectado. Pero el objeto se destruye, según las reglas normales de C++, cuando se sale del ámbito. Por lo tanto, si devolviera un tipo proyectado (un puntero inteligente) a ese objeto, entonces ese puntero ahora sería un puntero colgante.
Este tipo de error dañado en la memoria es difícil de diagnosticar. Por lo tanto, para las compilaciones de depuración, una aserción de C++/WinRT le ayuda a detectar este error mediante un detector de pila. Pero las corrutinas se asignan en el montón, por lo que no obtendrá ayuda con este error si lo hace dentro de una corrutina. Para obtener más información, consulta Diagnóstico de asignaciones directas.
Uso de tipos proyectados y tipos de implementación con varias características de C++/WinRT
Estos son algunos de los lugares en los que las funcionalidades de C++/WinRT esperan un tipo, y qué clase de tipo esperan (tipo proyectado, tipo de implementación o ambos).
| Feature | Acepta | Notes |
|---|---|---|
T (representa un puntero inteligente) |
Proyectado | Consulte la advertencia en Espacios de nombres: tipos proyectados, tipos de implementación y factorías sobre el uso accidental del tipo de implementación. |
agile_ref<T> |
Ambas | Si usa el tipo de implementación, el argumento constructor debe ser com_ptr<T>. |
com_ptr<T> |
Implementation | El uso del tipo proyectado genera el error: 'Release' is not a member of 'T'. |
default_interface<T> |
Ambas | Si usa el tipo de implementación, se devuelve la primera interfaz implementada. |
get_self<T> |
Implementation | El uso del tipo proyectado genera el error: '_abi_TrustLevel': is not a member of 'T'. |
guid_of<T>() |
Ambas | Devuelve el GUID de la interfaz predeterminada. |
IWinRTTemplateInterface<T> |
Proyectado | Usar el tipo de implementación compila, pero es un error; consulte la advertencia en Espacios de nombres: tipos proyectados, tipos de implementación y factorías. |
make<T> |
Implementation | El uso del tipo proyectado genera el error: 'implements_type': is not a member of any direct or indirect base class of 'T' |
make_agile(T const&) |
Ambas | Si usa el tipo de implementación, el argumento debe ser com_ptr<T>. |
make_self<T> |
Implementation | El uso del tipo proyectado genera el error: 'Release': is not a member of any direct or indirect base class of 'T' |
name_of<T> |
Proyectado | Si utiliza el tipo de implementación, obtendrá la representación en cadena del GUID de la interfaz predeterminada. |
weak_ref<T> |
Ambas | Si usa el tipo de implementación, el argumento constructor debe ser com_ptr<T>. |
Participación en la construcción uniforme y acceso directo a la implementación
Esta sección describe una característica de C++/WinRT 2.0 que es opcional, aunque está habilitada de forma predeterminada en los proyectos nuevos. Para un proyecto existente, deberás activar esta opción configurando la herramienta cppwinrt.exe. En Visual Studio, establezca la propiedad de proyecto Propiedades> comunesC++/WinRT>Optimizadas en Sí. Esto tiene el efecto de agregar <CppWinRTOptimized>true</CppWinRTOptimized> al archivo del proyecto. Y tiene el mismo efecto que añadir la opción al invocar cppwinrt.exe desde la línea de comandos.
La -opt[imize] opción activa lo que suele denominarse construcción uniforme. Con una construcción uniforme (o unificada), usa la propia proyección de lenguaje de C++/WinRT para crear y usar eficazmente sus tipos de implementación (tipos implementados por su componente para que las aplicaciones los consuman), sin ningún problema con el cargador.
Antes de describir la característica, primero mostraremos la situación sin construcción uniforme. Para ilustrarlo, comenzaremos con este ejemplo Windows Runtime clase.
// MyClass.idl
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void Method();
static void StaticMethod();
}
}
Como desarrollador de C++ familiarizado con el uso de la biblioteca de C++/WinRT, es posible que quiera usar la clase como esta.
using namespace winrt::MyProject;
MyClass c;
c.Method();
MyClass::StaticMethod();
Y eso sería perfectamente razonable siempre que el código de consumo mostrado no residiera en el mismo componente que implementa esta clase. Como proyección del lenguaje, C++/WinRT te aísla, como desarrollador, de la ABI (la interfaz binaria de aplicaciones basada en COM que define Windows Runtime). C++/WinRT no llama directamente a la implementación; viaja a través de la ABI.
Por lo tanto, en la línea de código donde se construye un objeto MyClass (MyClass c;), la proyección de C++/WinRT llama a RoGetActivationFactory para recuperar la clase o generador de activación y, a continuación, usa esa factoría para crear el objeto. La última línea también usa la fábrica para hacer lo que parece ser una llamada de método estático. Todo esto requiere que se registre la clase y que el módulo implemente el punto de entrada DllGetActivationFactory . C++/WinRT tiene una caché de fábrica muy rápida, por lo que ninguno de estos provoca un problema para una aplicación que consume el componente. El problema es que, dentro de tu componente, acabas de hacer algo que es un poco problemático.
En primer lugar, independientemente de la rapidez con la que esté la caché de fábrica de C++/WinRT, llamar a través de RoGetActivationFactory (o incluso las llamadas posteriores a través de la caché de fábrica) siempre será más lenta que llamar directamente a la implementación. Una llamada a RoGetActivationFactory seguida de IActivationFactory::ActivateInstance seguida de QueryInterface no va a ser tan eficaz como usar una expresión de C++ new para un tipo definido localmente. Como consecuencia, los desarrolladores de C++/WinRT experimentados están acostumbrados a usar las funciones auxiliares winrt::make o winrt::make_self al crear objetos dentro de un componente.
// MyClass c;
MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
Pero, como puede ver, eso no es casi tan cómodo ni conciso. Debe usar una función auxiliar para crear el objeto y también debe desambiguar entre el tipo de implementación y el tipo proyectado.
En segundo lugar, el uso de la proyección para crear la clase significa que su factoría de activación se almacenará en caché. Normalmente, eso es lo que quiere, pero si la factoría reside en el mismo módulo (DLL) que realiza la llamada, entonces ha anclado eficazmente el archivo DLL y ha evitado que se descargue nunca. En muchos casos, eso no importa; pero algunos componentes del sistema deben admitir la descarga.
Aquí es donde entra en vigencia el término construcción uniforme . Independientemente de si el código de creación reside en un proyecto que simplemente consume la clase o si reside en el proyecto que realmente implementa la clase, puede usar libremente la misma sintaxis para crear el objeto.
// MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
MyClass c;
Al compilar su proyecto de componentes con el modificador -opt[imize], la llamada a través de la proyección de lenguaje se reduce a la misma llamada eficiente a la función winrt::make, que crea directamente el tipo de implementación. Esto hace que la sintaxis sea sencilla y predecible, evita cualquier impacto de rendimiento de la llamada a través de la fábrica y evita anclar el componente en el proceso. Además de los proyectos de componentes, esto también es útil para las aplicaciones XAML. Omitir RoGetActivationFactory para las clases implementadas en la misma aplicación permite construirlas (sin necesidad de registrarlas) de las mismas formas en que podría hacerlo si estuvieran fuera de su componente.
La construcción uniforme se aplica a cualquier invocación gestionada internamente por la factoría. Prácticamente, esto significa que la optimización sirve tanto a constructores como a miembros estáticos. Este es el ejemplo original de nuevo.
MyClass c;
c.Method();
MyClass::StaticMethod();
Sin -opt[imize], las instrucciones primera y última requieren llamadas a través del objeto factory.
Con-opt[imize] ellos, ninguno de ellos sí. Y esas llamadas se compilan directamente en la implementación e incluso tienen el potencial de insertarse. Lo que remite a otro término que se usa a menudo al hablar de -opt[imize], a saber, el acceso de implementación directa.
Las proyecciones de lenguaje son cómodas, pero, cuando puede acceder directamente a la implementación, puede y debe aprovecharlo para generar el código más eficaz posible. C++/WinRT puede hacerlo por ti, sin obligarte a renunciar a la seguridad y la productividad que ofrece la proyección.
Este es un cambio importante porque el componente debe cooperar para permitir que la proyección de lenguaje pueda acceder directamente a sus tipos de implementación. Como C++/WinRT es una biblioteca formada únicamente por archivos de cabecera, puedes mirar dentro y ver qué está ocurriendo. Sin -opt[imize], el constructor MyClass y el miembro StaticMethod se definen mediante la proyección 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(); });
}
}
No es necesario seguir todo lo anterior; la intención es mostrar que ambas llamadas implican una llamada a una función denominada call_factory. Esa es la pista de que estas llamadas implican la memoria caché de fábrica y no acceden directamente a la implementación.
Con-opt[imize], estas mismas funciones no se definen en absoluto. En su lugar, se declaran mediante la proyección y sus definiciones se dejan al componente.
A continuación, el componente puede proporcionar definiciones que llaman directamente a la implementación. Ahora hemos llegado al cambio importante. Esas definiciones se generan para ti cuando usas tanto -component como -opt[imize], y aparecen en un archivo llamado Type.g.cpp, donde Type es el nombre de la clase de tiempo de ejecución que se está implementando. Por eso puede encontrarse con varios errores del enlazador al habilitar -opt[imize] por primera vez en un proyecto existente. Tienes que incorporar ese archivo generado a tu implementación para que todo funcione conjuntamente.
En nuestro ejemplo, MyClass.h podría tener este aspecto (independientemente de si -opt[imize] se usa).
// 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>
{
};
}
Tu MyClass.cpp es donde todo cobra sentido.
#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()
{
}
}
Por lo tanto, para usar la construcción uniforme en un proyecto existente, debes editar el archivo .cpp de cada implementación de modo que #include <Sub/Namespace/Type.g.cpp> después de la inclusión y definición de la clase de implementación. Ese archivo proporciona las definiciones de esas funciones que la proyección dejó sin definir. Así es como se ven esas definiciones dentro del archivo MyClass.g.cpp.
namespace winrt::MyProject
{
MyClass::MyClass() :
MyClass(make<MyProject::implementation::MyClass>())
{
}
void MyClass::StaticMethod()
{
return MyProject::implementation::MyClass::StaticMethod();
}
}
Y eso completa perfectamente la proyección con llamadas eficaces directamente en la implementación, evitando llamadas a la memoria caché de fábrica y satisfaciendo al enlazador.
Lo último que hace -opt[imize] por ti es cambiar la implementación del archivo module.g.cpp de tu proyecto (el archivo que te ayuda a implementar las exportaciones DllGetActivationFactory y DllCanUnloadNow de tu DLL) de modo que las compilaciones incrementales suelen ser mucho más rápidas al eliminar el fuerte acoplamiento entre tipos que requería C++/WinRT 1.0. Esto suele denominarse factorías con borrado de tipos. Sin -opt[imize], el archivo module.g.cpp que se genera para el componente comienza incluyendo las definiciones de todas las clases de implementación: las MyClass.h, en este ejemplo. A continuación, crea directamente el generador de implementación para cada clase como esta.
if (requal(name, L"MyProject.MyClass"))
{
return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
De nuevo, no es necesario seguir todos los detalles. Lo que resulta útil para ver es que esto requiere la definición completa para cualquiera y todas las clases implementadas por el componente. Esto puede tener un efecto dramático en el bucle interno, ya que cualquier cambio en una sola implementación hará que module.g.cpp se vuelva a compilar. Con -opt[imize], este ya no es el caso. En su lugar, se producen dos cosas en el archivo generado module.g.cpp . La primera es que ya no incluye ninguna clase de implementación. En este ejemplo, no se incluirá MyClass.h en absoluto. En su lugar, crea las factorías de implementación sin ningún conocimiento de su implementación.
void* winrt_make_MyProject_MyClass();
if (requal(name, L"MyProject.MyClass"))
{
return winrt_make_MyProject_MyClass();
}
Obviamente, no es necesario incluir sus definiciones y es necesario que el enlazador resuelva la definición de la función winrt_make_Component_Class . Por supuesto, no es necesario pensar en esto, ya que el MyClass.g.cpp archivo que se genera automáticamente (y que incluyó anteriormente para admitir la construcción uniforme) también define esta función. Esta es la totalidad del MyClass.g.cpp archivo que se genera para este ejemplo.
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 puede ver, la función winrt_make_MyProject_MyClass crea directamente la factoría de su implementación. Todo esto significa que puede modificar sin problema cualquier implementación concreta, y que module.g.cpp no hace falta volver a compilarla en absoluto. Solo cuando se agregan o quitan clases de Windows Runtime, module.g.cpp se actualizará y deberá volver a compilarse.
Sobrescritura de métodos virtuales de la clase base
La clase derivada puede tener problemas con los métodos virtuales si tanto la clase base como la clase derivada son clases definidas por la aplicación, pero el método virtual está definido en una clase ancestro de Windows Runtime. En la práctica, esto sucede si derivas de clases XAML. El resto de esta sección continúa en el ejemplo de Clases 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);
};
}
La jerarquía es Microsoft::UI::Xaml::Controls::Page<- BasePage<- DerivedPage. El método BasePage::OnNavigatedFrom invalida correctamente Page::OnNavigatedFrom, pero DerivedPage::OnNavigatedFrom no invalida BasePage::OnNavigatedFrom.
Aquí, DerivedPage reutiliza la tabla virtual IPageOverrides de BasePage, lo que significa que no se puede invalidar el método IPageOverrides::OnNavigatedFrom . Una posible solución requiere que BasePage sea una clase de plantilla y que su implementación esté completamente en un archivo de encabezado, pero esto hace que las cosas sean inaceptablemente complicadas.
Como solución alternativa, declare el método OnNavigatedFrom como virtual explícitamente en la clase base. De este modo, cuando la entrada de vtable para DerivedPage::IPageOverrides::OnNavigatedFrom llama a BasePage::IPageOverrides::OnNavigatedFrom, el productor llama a BasePage::OnNavigatedFrom, que (debido a su virtualidad), termina llamando a 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);
};
}
Esto requiere que todos los miembros de la jerarquía de clases acepten el valor devuelto y los tipos de parámetro del método OnNavigatedFrom . Si no están de acuerdo, debe usar la versión anterior como método virtual y encapsular las alternativas.
Note
Su IDL no necesita declarar el método invalidado. Para obtener más información, consulte Implementación de métodos reemplazables.
API importantes
- Plantilla de estructura winrt::com_ptr
- función winrt::com_ptr::copy_from
- Plantilla de función winrt::from_abi
- Plantilla de la función winrt::get_self
- Plantilla de estructura winrt::implements
- Plantilla de funciones winrt::make
- Plantilla de la función winrt::make_self
- función winrt::Windows::Foundation::IUnknown::as
- función winrt::Windows::Foundation::IUnknown::try_as