Creare API con C++/WinRT
Questo argomento descrive come creare API C++/WinRT usando lo struct di base winrt::implements, direttamente o indirettamente. In questo contesto, creare è un sinonimo di produrre o implementare. Questo argomento illustra gli scenari seguenti per l'implementazione di API su un tipo C++/WinRT, in questo ordine.
Nota
Include informazioni sui componenti Windows Runtime, ma solo nel contesto di C++/WinRT. Per informazioni sui componenti Windows Runtime relative a tutti i linguaggi di Windows Runtime, vedi Componenti Windows Runtime.
- Non stai creando una classe Windows Runtime (classe di runtime). Vuoi semplicemente implementare una o più interfacce di Windows Runtime per l'utilizzo in locale nell'app. In questo caso, esegui la derivazione direttamente da winrt::implements e implementi le funzioni.
- Stai creando una classe di runtime. Potresti creare un componente che verrà utilizzato da un'app oppure potresti creare un tipo che verrà utilizzato da un'interfaccia utente XAML. In tal caso, implementi e utilizzi una classe di runtime all'interno della stessa unità di compilazione. In questi casi, consenti agli strumenti di generare le classi derivate da winrt::implements.
In entrambi i casi, il tipo che implementa le API C++/WinRT viene denominato tipo di implementazione.
Importante
È importante distinguere il concetto di tipo di implementazione rispetto a quello di tipo proiettato. Il tipo proiettato è descritto in Utilizzare API con C++/WinRT.
Se non stai creando una classe di runtime
Lo scenario più semplice è quello in cui il tipo implementa un'interfaccia di Windows Runtime e viene usato all'interno della stessa app. In questo caso, non è necessario che il tipo sia una classe di runtime, ma solo una normale classe C++. Ad esempio, potrebbe essere il caso della scrittura di un'app basata su CoreApplication.
Se al tipo viene fatto riferimento nell'interfaccia utente XAML, deve essere una classe di runtime, anche se si trova nello stesso progetto del codice XAML. Per questo caso, vedere la sezione Se stai creando una classe di runtime a cui fare riferimento nell'interfaccia utente XAML.
Nota
Per informazioni sull'installazione e sull'uso dell'Estensione C++/WinRT per Visual Studio (VSIX) e del pacchetto NuGet, che insieme forniscono il modello di progetto e il supporto della compilazione, vedi Supporto di Visual Studio per C++/WinRT.
In Visual Studio il modello di progetto di Visual C++>Windows Universal>App Core (C++/WinRT) illustra il modello CoreApplication. Il modello inizia con il passaggio di un'implementazione di Windows::ApplicationModel::Core::IFrameworkViewSource a CoreApplication::Run.
using namespace Windows::ApplicationModel::Core;
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
IFrameworkViewSource source = ...
CoreApplication::Run(source);
}
CoreApplication usa l'interfaccia per creare la prima visualizzazione dell'app. Dal punto di vista concettuale, IFrameworkViewSource avrà un aspetto simile al seguente.
struct IFrameworkViewSource : IInspectable
{
IFrameworkView CreateView();
};
Sempre dal punto di vista concettuale, l'implementazione di CoreApplication::Run esegue questa operazione.
void Run(IFrameworkViewSource viewSource) const
{
IFrameworkView view = viewSource.CreateView();
...
}
In qualità di sviluppatore, dovrai pertanto implementare l'interfaccia IFrameworkViewSource. C++/WinRT dispone del modello struct di base winrt::implements che semplifica l'implementazione di una o più interfacce, senza ricorrere alla programmazione di tipo COM. Dovrai semplicemente derivare il tipo da implements e quindi implementare le funzioni dell'interfaccia. Ecco come.
// App.cpp
...
struct App : implements<App, IFrameworkViewSource>
{
IFrameworkView CreateView()
{
return ...
}
}
...
Viene preso in considerazione IFrameworkViewSource. Il passaggio successivo consiste nel restituire un oggetto che implementa l'interfaccia IFrameworkView. Puoi anche scegliere di implementare l'interfaccia su App. Il prossimo esempio di codice rappresenta un'app minima che renderà una finestra operativa almeno sul desktop.
// 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() {}
};
...
Poiché il tipo App è un oggetto IFrameworkViewSource, potrai passarne uno solo a Run.
using namespace Windows::ApplicationModel::Core;
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
CoreApplication::Run(winrt::make<App>());
}
Se stai creando una classe di runtime in un componente Windows Runtime
Un tipo incluso in un componente di Windows Runtime per l’utilizzo in un altro file binario (l'altro file binario è di solito un'app), il tipo deve essere necessariamente una classe di runtime. Una classe di runtime viene dichiarata in un file di Microsoft Interface Definition Language (IDL) con estensione idl. Vedi Factoring delle classi di runtime nei file Midl (.idl).
Per ogni file IDL viene generato un file con estensione .winmd
e Visual Studio unisce tutti i file risultanti in un singolo file con lo stesso nome dello spazio dei nomi radice. Tale file .winmd
finale sarà quello a cui faranno riferimento i consumer del componente.
Di seguito è riportato un esempio di dichiarazione di una classe di runtime in un file 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;
}
}
Questo file con estensione idl dichiara una classe Windows Runtime (runtime). Una classe di runtime è un tipo che può essere attivato e utilizzato mediante le interfacce COM moderne, in genere tra limiti eseguibili. Quando aggiungi un file con estensione idl al progetto ed esegui la compilazione, la toolchain C++/WinRT (midl.exe
e cppwinrt.exe
) genera automaticamente un tipo di implementazione. Per un esempio del flusso di lavoro effettivo per il file con estensione idl, vedi Controlli XAML, binding a una proprietà C++/WinRT.
Se usi il file con estensione idl di esempio precedente, il tipo di implementazione è uno stub di struct C++ denominato winrt::MyProject::implementation::MyRuntimeClass nei file del codice sorgente denominati \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h
e MyRuntimeClass.cpp
.
Il tipo di implementazione ha un aspetto simile al seguente.
// 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.
Si noti che il modello di polimorfismo F-bound in uso (MyRuntimeClass usa sé stesso come argomento di modello per la base, MyRuntimeClassT), noto anche con il nome CRTP (Curiously Recurring Template Pattern). Seguendo la catena di ereditarietà verso l'alto, si troverà MyRuntimeClass_base.
È possibile semplificare l'implementazione di proprietà semplici usando le librerie di implementazione di Windows (WIL). In tal caso, eseguire la procedura seguente:
// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
{
MyRuntimeClass() = default;
wil::single_threaded_rw_property<winrt::hstring> Name;
};
}
Vedere Proprietà semplici.
template <typename D, typename... I>
struct MyRuntimeClass_base : implements<D, MyProject::IMyRuntimeClass, I...>
In questo scenario, alla radice della gerarchia di ereditarietà, è di nuovo presente il modello di struct di base winrt::implements.
Per altre informazioni, codice e una procedura dettagliata sulla creazione di API in un componente Windows Runtime, vedere Componenti Windows Runtime con C++/WinRT e Creare eventi in CC++/WinRT.
Se stai creando una classe di runtime a cui fare riferimento nell'interfaccia utente XAML
Se al tipo viene fatto riferimento nell'interfaccia utente XAML, deve essere una classe di runtime, anche se si trova nello stesso progetto del codice XAML. Sebbene l'attivazione avvenga in genere tra limiti eseguibili, una classe di runtime può essere invece usata nell'unità di compilazione che la implementa.
Questo scenario prevede la creazione e l'utilizzo delle API. La procedura per l'implementazione di una classe di runtime equivale in pratica a quella per l'implementazione di un componente Windows Runtime. Vedi quindi la sezione precedente: Se stai creando una classe di runtime in un componente Windows Runtime. L'unica differenza è data dal fatto che dal file con estensione idl la toolchain C++/WinRT non genera solo un tipo di implementazione, ma anche un tipo proiettato. È anche importante notare che la dicitura "MyRuntimeClass" può risultare ambigua in questo scenario, in quanto con questo nome esistono diverse entità, di tipi diversi.
- MyRuntimeClass è il nome di una classe di runtime. È tuttavia un'astrazione: dichiarata in IDL e implementata in un linguaggio di programmazione.
- MyRuntimeClass è il nome dello struct C++ winrt::MyProject::implementation::MyRuntimeClass, che rappresenta l'implementazione C++/WinRT della classe di runtime. Come abbiamo visto, se esistono progetti di implementazione e utilizzo separati, questo struct è presente solo nel progetto di implementazione. Si tratta del tipo di implementazione o dell'implementazione. Questo tipo viene generato (dallo strumento
cppwinrt.exe
) nei file\MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h
eMyRuntimeClass.cpp
. - MyRuntimeClass è il nome del tipo proiettato nel formato dello struct C++ winrt::MyProject::MyRuntimeClass. Se esistono progetti di implementazione e utilizzo separati, questo struct è presente solo nel progetto di utilizzo. Si tratta del tipo proiettato o della proiezione. Questo tipo viene generato (da
cppwinrt.exe
) nel file\MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h
.
Di seguito sono riportate le parti del tipo proiettato rilevanti per questo argomento.
// MyProject.2.h
...
namespace winrt::MyProject
{
struct MyRuntimeClass : MyProject::IMyRuntimeClass
{
MyRuntimeClass(std::nullptr_t) noexcept {}
MyRuntimeClass();
};
}
Per una procedura dettagliata di esempio per l'implementazione dell'interfaccia INotifyPropertyChanged in una classe di runtime, vedi Controlli XAML, binding a una proprietà C++/WinRT.
La procedura per l'utilizzo della classe di runtime in questo scenario è descritta in Utilizzare API con C++/WinRT.
Factoring delle classi di runtime nei file Midl (.idl)
I modelli di progetto ed elemento di Visual Studio generano un file IDL separato per ogni classe di runtime. Ciò consente di ottenere una corrispondenza logica tra un file IDL e i relativi file di codice sorgente generati.
Se tuttavia tutte le classi di runtime del progetto vengono consolidate in un unico file IDL, questo può migliorare notevolmente il tempo di compilazione. Se sono presenti dipendenze di import
complesse (o circolari) tra le classi, il consolidamento può essere effettivamente necessario. Inoltre, se le classi di runtime sono consolidate, può risultare più semplice crearle e rivederle.
Costruttori della classe di runtime
Ecco alcuni punti da eliminare dall'elenco che abbiamo riportato sopra.
- Ogni costruttore dichiarato nel file con estensione idl determina la generazione di un costruttore nel tipo di implementazione e nel tipo proiettato. I costruttori dichiarati nel file con estensione idl consentono l'uso della classe di runtime da un'unità di compilazione differente.
- Indipendentemente dal fatto che disponga di costruttori dichiarati nel file con estensione idl, un overload del costruttore che accetta std::nullptr_t viene generato nel tipo proiettato. La chiamata al costruttore std::nullptr_t è il primo dei due passaggi per l'utilizzo della classe di runtime dalla stessa unità di compilazione. Per altri dettagli e un esempio di codice, vedi Utilizzare API con C++/WinRT.
- Se stai utilizzando la classe di runtime dalla stessa unità di compilazione, potrai anche implementare costruttori non predefiniti direttamente sul tipo di implementazione (che si trova in
MyRuntimeClass.h
).
Nota
Se prevedi che la classe di runtime venga utilizzata da un'unità di compilazione diversa (scenario comune), includi i costruttori nel file con estensione idl (almeno un costruttore predefinito). In questo modo, otterrai anche un'implementazione di factory insieme al tipo di implementazione.
Se intendi creare e utilizzare la classe di runtime solo all'interno della stessa unità di compilazione, non dichiarare eventuali costruttori nel file con estensione idl. Un'implementazione di factory non è necessaria e non verrà pertanto generata. Il costruttore predefinito del tipo di implementazione verrà eliminato, ma potrai facilmente modificarlo e usarlo per impostazione predefinita.
Se intendi creare e utilizzare la classe di runtime solo all'interno della stessa unità di compilazione e hai bisogno di parametri di costruttori, crea i costruttori necessari direttamente sul tipo di implementazione.
Metodi, proprietà ed eventi delle classi di runtime
Abbiamo visto che il flusso di lavoro consiste nell'uso del linguaggio IDL per dichiarare la classe di runtime e i relativi membri e che successivamente i prototipi e le implementazioni dello stub vengono generati in modo automatico dagli strumenti. I prototipi generati automaticamente per i membri della classe di runtime possono essere modificati in modo da passare intorno a tipi diversi da quelli dichiarati nel codice IDL. Questa modifica può tuttavia essere apportata solo se il tipo dichiarato in IDL può essere inoltrato a quello dichiarato nella versione implementata.
Di seguito sono riportati alcuni esempi.
- I tipi di parametro consentono una certa flessibilità. Se ad esempio in IDL il metodo accetta un tipo SomeClass, puoi scegliere di modificarlo in IInspectable nella tua implementazione. Questo è possibile perché qualsiasi tipo SomeClass può essere inoltrato a IInspectable, anche se ovviamente l'inverso non è possibile.
- Puoi accettare un parametro copiabile per valore, anziché per riferimento. Puoi ad esempio modificare
SomeClass const&
inSomeClass
. Questo è necessario quando devi evitare di acquisire un riferimento in una coroutine (vedi Passaggio di parametri). - Anche per il valore restituito è consentita una certa flessibilità. Puoi ad esempio modificare void in winrt::fire_and_forget.
Gli ultimi due valori sono molto utili quando si scrive un gestore eventi asincrono.
Creazione di istanze e restituzione di interfacce e tipi di implementazione
In questa sezione prendiamo come esempio un tipo di implementazione denominato MyType, che implementa le interfacce IStringable e IClosable.
Puoi derivare MyType direttamente da winrt::implements (non è una classe di runtime).
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
struct MyType : implements<MyType, IStringable, IClosable>
{
winrt::hstring ToString(){ ... }
void Close(){}
};
In alternativa puoi generarlo dal file con estensione idl (è una classe di runtime).
// MyType.idl
namespace MyProject
{
runtimeclass MyType: Windows.Foundation.IStringable, Windows.Foundation.IClosable
{
MyType();
}
}
Non puoi allocare direttamente il tipo di implementazione.
MyType myimpl; // error C2259: 'MyType': cannot instantiate abstract class
Puoi tuttavia passare da MyType a un oggetto IStringable o IClosable, che puoi usare o restituire come parte della proiezione chiamando il modello di funzione winrt::make. make restituisce l'interfaccia predefinita del tipo di implementazione.
IStringable istringable = winrt::make<MyType>();
Nota
Se tuttavia fai riferimento al tipo dall'interfaccia utente XAML, nello stesso progetto saranno presenti sia un tipo di implementazione sia un tipo proiettato. In tal caso, make restituisce un'istanza del tipo proiettato. Per un esempio di codice di questo scenario, vedi Controlli XAML, binding a una proprietà C++/WinRT.
Possiamo usare istringable
(nell'esempio di codice precedente) solo per chiamare i membri dell'interfaccia IStringable. Un'interfaccia C++/WinRT (che è un'interfaccia proiettata) deriva tuttavia da winrt::Windows::Foundation::IUnknown. Puoi quindi chiamare IUnknown::as (o IUnknown::try_as) sull'interfaccia per eseguire una query su altre interfacce o tipi proiettati, che potrai usare o restituire.
Suggerimento
Uno scenario in cui non si deve chiamare as o try_as è la derivazione della classe di runtime ("classi componibili"). Quando un tipo di implementazione compone un'altra classe, non chiamare as o try_as per eseguire QueryInterface non controllata o controllata della classe composta. Invece, accedere al membro dati (this->
) m_inner
e chiamare as o try_as su tale membro. Per altre informazioni, vedere Derivazione della classe runtime in questo argomento.
istringable.ToString();
IClosable iclosable = istringable.as<IClosable>();
iclosable.Close();
Se è necessario accedere a tutti i membri dell'implementazione e quindi restituire un'interfaccia a un chiamante, usa il modello di funzione winrt::make_self. make_self restituisce winrt::com_ptr che esegue il wrapping del tipo di implementazione. Puoi accedere ai membri di tutte le relative interfacce (usando l'operatore freccia), eseguirne la restituzione a un chiamante oppure chiamare as su di esso e restituire l'oggetto di interfaccia risultante a un chiamante.
winrt::com_ptr<MyType> myimpl = winrt::make_self<MyType>();
myimpl->ToString();
myimpl->Close();
IClosable iclosable = myimpl.as<IClosable>();
iclosable.Close();
La classe MyType non fa parte della proiezione. È l'implementazione. In questo modo puoi tuttavia chiamare i relativi metodi di implementazione direttamente, senza il sovraccarico di una chiamata di funzione virtuale. Nell'esempio precedente, anche se MyType::ToString usa la stessa firma del metodo proiettato su IStringable, eseguiamo direttamente la chiamata al metodo non virtuale, senza attraversare l'interfaccia ABI (Application Binary Interface). L'oggetto com_ptr contiene semplicemente un puntatore allo struct MyType. Puoi quindi accedere anche a eventuali altri dettagli interni di MyType tramite la variabile myimpl
e l'operatore freccia.
Se disponi di un oggetto di interfaccia che rappresenta un'interfaccia sull'implementazione, puoi tornare all'implementazione tramite il modello di funzione winrt::get_self. Anche in questo caso, si tratta di una tecnica per evitare chiamate di funzione virtuali, che consente di passare direttamente all'implementazione.
Nota
Se non hai installato la versione 10.0.17763.0 o successiva di Windows SDK (Windows 10, versione 1809), devi chiamare winrt::from_abi anziché winrt::get_self.
Ecco un esempio. È disponibile un altro esempio in Implementare la classe di controllo personalizzata BgLabelControl.
void ImplFromIClosable(IClosable const& from)
{
MyType* myimpl = winrt::get_self<MyType>(from);
myimpl->ToString();
myimpl->Close();
}
Tuttavia, solo l'oggetto di interfaccia originale mantiene un riferimento. Se vuoi mantenerlo, puoi chiamare 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.
Il tipo di implementazione stesso non deriva da winrt::Windows::Foundation::IUnknown, quindi non dispone di una funzione as. Anche in questo caso, come puoi vedere nella precedente funzione ImplFromIClosable, puoi accedere ai membri di tutte le relative interfacce. In questo caso, tuttavia, non restituire l'istanza del tipo di implementazione non elaborato al chiamante. Usa, invece, una delle tecniche già illustrate per restituire un'interfaccia proiettata oppure un elemento com_ptr.
Se hai un'istanza del tipo di implementazione da passare a una funzione che prevede di ricevere il tipo proiettato corrispondente, puoi procedere con tale operazione, come illustrato nell'esempio di codice seguente. grazie a un operatore di conversione presente sul tipo di implementazione (purché il tipo di implementazione sia stato generato tramite lo strumento cppwinrt.exe
). Puoi passare un valore del tipo di implementazione direttamente a un metodo che prevede un valore del tipo proiettato corrispondente. Da una funzione membro del tipo di implementazione, puoi passare *this
a un metodo che prevede un valore del tipo proiettato corrispondente.
// 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);
}
...
Derivazione della classe di runtime
È possibile creare una classe di runtime che deriva da un'altra, purché la classe di base sia dichiarata come "non bloccata". Il termine Windows Runtime per la derivazione della classe è "classi componibili". Il codice per l'implementazione di una classe derivata dipende dal fatto che la classe di base venga fornita da un altro componente o dallo stesso componente. Fortunatamente, non è necessario apprendere queste regole, è sufficiente copiare le implementazioni di esempio dalla cartella di output sources
prodotta dal compilatore cppwinrt.exe
.
Si consideri l'esempio seguente.
// 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();
}
}
Nell'esempio precedente, MyButton deriva dal controllo del Pulsante XAML fornito da un altro componente. In tal caso, l'implementazione è simile all'implementazione di una classe non componibile:
namespace winrt::MyProject::implementation
{
struct MyButton : MyButtonT<MyButton>
{
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyButton : MyButtonT<MyButton, implementation::MyButton>
{
};
}
D'altra parte, nell'esempio precedente, MyDerived è derivato da un'altra classe nello stesso componente. In questo caso, l'implementazione richiede un parametro di modello aggiuntivo che specifica la classe di implementazione per la classe base.
namespace winrt::MyProject::implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
{ // ^^^^^^^^^^^^^^^^^^^^^^
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyDerived>
{
};
}
In entrambi i casi, l'implementazione può chiamare un metodo dalla classe base qualificandolo con l'alias del 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();
}
};
}
Suggerimento
Quando un tipo di implementazione compone un'altra classe, non chiamare as o try_as per eseguire QueryInterface non controllata o controllata della classe composta. Invece, accedere al membro dati (this->
) m_inner
e chiamare as o try_as su tale membro.
Derivazione da un tipo con un costruttore non predefinito
ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) è un esempio di costruttore non predefinito. Non esiste alcun costruttore predefinito. Per creare una classe ToggleButtonAutomationPeer, è necessario passare un oggetto owner. Di conseguenza, se esegui la derivazione da ToggleButtonAutomationPeer, è necessario fornire un costruttore che accetti un oggetto owner e lo passi alla base. Di seguito è riportato l'aspetto previsto.
// 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);
};
}
Il costruttore generato per il tipo di implementazione ha un aspetto simile al seguente.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner)
{
...
}
...
A questo punto, non resta che passare tale parametro del costruttore alla classe di base. Ti ricordi il modello di polimorfismo F-bound accennato in precedenza? Dopo aver familiarizzato con i dettagli di tale modello in uso in C++/WinRT, potrai individuare il nome della classe di base o semplicemente esaminare il file di intestazione della classe implementazione. Di seguito è indicato come chiamare il costruttore della classe in questo caso.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner) :
MySpecializedToggleButtonAutomationPeerT<MySpecializedToggleButtonAutomationPeer>(owner)
{
...
}
...
Il costruttore della classe di base prevede l'uso di un oggetto ToggleButton e MySpecializedToggleButton è un elemento ToggleButton.
Finché non apporterai la modifica descritta in precedenza (per passare tale parametro del costruttore alla classe di base), il compilatore contrassegnerà il costruttore e indicherà che un costruttore predefinito appropriato non è disponibile su un tipo denominato (in questo caso) MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer>. Si tratta effettivamente della classe di base del tipo di implementazione.
Spazi dei nomi: tipi proiettati, tipi di implementazione e factory
Come hai già visto in questo argomento, una classe di runtime C++/WinRT esiste sotto forma di più classi C++ in più spazi dei nomi. Di conseguenza, il nome MyRuntimeClass ha un determinato significato nello spazio dei nomi winrt::MyProject e un significato diverso nello spazio dei nomi winrt::MyProject::implementation. Presta attenzione allo spazio dei nomi attualmente presente nel contesto e quindi usa gli appositi prefissi se devi usare un nome di uno spazio dei nomi diverso. Esaminiamo più in dettaglio gli spazi dei nomi in questione.
- winrt::MyProject. Questo spazio dei nomi contiene tipi proiettati. Un oggetto di un tipo proiettato è un proxy. Si tratta essenzialmente di un puntatore intelligente a un oggetto di supporto, in cui l'oggetto può essere implementato all'intero del progetto o in un'altra unità di compilazione.
- winrt::MyProject::implementation. Questo spazio dei nomi contiene tipi di implementazione. Un oggetto di un tipo di implementazione non è un puntatore, ma un valore. Si tratta di un oggetto completo dello stack C++. Non devi creare un tipo di implementazione direttamente. In alternativa, chiama winrt::make passando il tipo di implementazione come parametro del modello. Abbiamo visto esempi di winrt::make in azione in una sezione precedente di questo argomento ed è disponibile un altro esempio in Controlli XAML, binding a una proprietà C++/WinRT. Vedi anche Diagnosi delle allocazioni dirette.
- winrt::MyProject::factory_implementation. Questo spazio dei nomi contiene factory. Un oggetto in questo spazio dei nomi supporta IActivationFactory.
Questa tabella mostra la qualifica minima dello spazio dei nomi che è necessario usare in contesti diversi.
Spazio dei nomi incluso nel contesto | Per specificare il tipo proiettato | Per specificare il tipo di implementazione |
---|---|---|
winrt::MyProject | MyRuntimeClass |
implementation::MyRuntimeClass |
winrt::MyProject::implementation | MyProject::MyRuntimeClass |
MyRuntimeClass |
Importante
Quando vuoi restituire un tipo proiettato dalla tua implementazione, presta attenzione a non creare istanze del tipo di implementazione scrivendo MyRuntimeClass myRuntimeClass;
. Le tecniche e il codice da usare per questo scenario sono illustrate nella precedente sezione Creazione di istanze e restituzione di interfacce e tipi di implementazione di questo argomento.
Il problema relativo a MyRuntimeClass myRuntimeClass;
in questo scenario è dato dal fatto che crea un oggetto winrt::MyProject::implementation::MyRuntimeClass nello stack. Questo oggetto (un tipo di implementazione) si comporta in un certo senso come il tipo proiettato. È infatti possibile richiamare metodi su di esso nello stesso modo e può anche essere convertito in un tipo proiettato. L'oggetto viene tuttavia distrutto, in base alle normali regole di C++, se l'ambito viene chiuso. Se pertanto hai restituito un tipo proiettato (un puntatore intelligente) all'oggetto, tale puntatore è ora scollegato.
Questo tipo di bug di danneggiamento della memoria è difficile da diagnosticare. Pertanto, per le build di debug, un'asserzione C++/WinRT aiuta a rilevare questo errore usando un rilevatore di stack. Tuttavia, poiché le coroutine sono allocate sull'heap, l'errore non viene rilevato facilmente se si trova all'interno di una coroutine. Per altre informazioni, vedi Diagnosi delle allocazioni dirette.
Uso di tipi proiettati e tipi di implementazione con varie funzionalità di C++/WinRT
La tabella seguente elenca i vari punti in cui le funzionalità C++/WinRT prevedono l'uso di un tipo e il tipo previsto (proiettato, implementazione o entrambi).
Funzionalità di | Accetta | Note |
---|---|---|
T (rappresenta un puntatore intelligente) |
Proiettato | Vedi l'avvertimento in Spazi dei nomi: tipi proiettati, tipi di implementazione e factory sull'uso del tipo di implementazione per errore. |
agile_ref<T> |
Entrambi | Se usi il tipo di implementazione, l'argomento del costruttore deve essere com_ptr<T> . |
com_ptr<T> |
Implementazione | Se usi il tipo proiettato, viene generato questo errore: 'Release' is not a member of 'T' . |
default_interface<T> |
Entrambi | Se usi il tipo di implementazione, viene restituita la prima interfaccia implementata. |
get_self<T> |
Implementazione | Se usi il tipo proiettato, viene generato questo errore: '_abi_TrustLevel': is not a member of 'T' . |
guid_of<T>() |
Entrambi | Restituisce il GUID dell'interfaccia predefinita. |
IWinRTTemplateInterface<T> |
Proiettato | Se usi il tipo di implementazione, viene eseguita la compilazione, ma è un errore. Vedi l'avvertimento in Spazi dei nomi: tipi proiettati, tipi di implementazione e factory. |
make<T> |
Implementazione | Se usi il tipo proiettato, viene generato questo errore: 'implements_type': is not a member of any direct or indirect base class of 'T' |
make_agile(T const&) |
Entrambi | Se usi il tipo di implementazione, l'argomento deve essere com_ptr<T> . |
make_self<T> |
Implementazione | Se usi il tipo proiettato, viene generato questo errore: 'Release': is not a member of any direct or indirect base class of 'T' |
name_of<T> |
Proiettato | Se usi il tipo di implementazione, ottieni il GUID sotto forma di stringa dell'interfaccia predefinita. |
weak_ref<T> |
Entrambi | Se usi il tipo di implementazione, l'argomento del costruttore deve essere com_ptr<T> . |
Acconsentire esplicitamente alla costruzione uniforme e all'accesso diretto all'implementazione
Questa sezione illustra una funzionalità di C++/WinRT 2.0 per il consenso esplicito, anche se è abilitata per impostazione predefinita per i nuovi progetti. Per un progetto esistente, devi acconsentire esplicitamente configurando lo strumento cppwinrt.exe
. In Visual Studio imposta la proprietà del progetto Proprietà comuni>C++/WinRT>Ottimizzato su Sì. Questa operazione ha come risultato l'aggiunta di <CppWinRTOptimized>true</CppWinRTOptimized>
al file del progetto e ha lo stesso effetto dell'aggiunta dell'opzione quando viene richiamato cppwinrt.exe
dalla riga di comando.
L'opzione -opt[imize]
abilita una funzionalità spesso definita come costruzione uniforme. Con la costruzione uniforme (o unificata), usi la proiezione di linguaggio C++/WinRT per creare e usare i tipi di implementazione (tipi implementati dal componente per l'utilizzo da parte delle applicazioni) in modo efficiente e facile per il caricatore.
Prima di descrivere la funzionalità, è opportuno illustrare la situazione senza la costruzione uniforme. A tale scopo, è possibile iniziare con questa classe di esempio di Windows Runtime.
// MyClass.idl
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void Method();
static void StaticMethod();
}
}
Se sei uno sviluppatore C++ che ha familiarità con l'uso della libreria C++/WinRT, puoi decidere di usare la classe in questo modo.
using namespace winrt::MyProject;
MyClass c;
c.Method();
MyClass::StaticMethod();
Questa decisione è assolutamente ragionevole, purché il codice di utilizzo mostrato non risieda nello stesso componente che implementa la classe. Essendo una proiezione di linguaggio, C++/WinRT ti protegge come sviluppatore dall'interfaccia ABI, ovvero dall'interfaccia applicativa binaria basata su COM definita da Windows Runtime. C++/WinRT non chiama direttamente nell'implementazione, ma passa attraverso l'interfaccia ABI.
Di conseguenza, nella riga di codice in cui costruisci un oggetto MyClass (MyClass c;
), la proiezione C++/WinRT chiama RoGetActivationFactory per recuperare la class factory o la factory di attivazione e quindi usa tale factory per creare l'oggetto. Analogamente, l'ultima riga usa la factory per creare quella che sembra una chiamata a un metodo statico. Ciò richiede che la classe sia registrata e che il modulo implementi il punto di ingresso DllGetActivationFactory. C++/WinRT dispone di una cache di factory molto rapida, quindi non si verificano problemi per un'applicazione che utilizza il componente. La difficoltà è rappresentata dal fatto che, all'interno del componente, è stata appena eseguita un'operazione leggermente problematica.
In primo luogo, indipendentemente dalla rapidità della cache di factory di C++/WinRT, la chiamata tramite RoGetActivationFactory (o persino le successive chiamate tramite la cache di factory) sarà comunque più lenta rispetto alla chiamata diretta nell'implementazione. Una chiamata a RoGetActivationFactory seguita da IActivationFactory::ActivateInstance, a sua volta seguita da QueryInterface, non può ovviamente essere efficiente quanto l'uso di un'espressione C++ new
per un tipo definito in locale. Pertanto, gli sviluppatori C++/WinRT esperti sono abituati a usare la funzione helper winrt::make o winrt::make_self per creare oggetti all'interno di un componente.
// MyClass c;
MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
Tuttavia, come puoi vedere, non si tratta di una soluzione comoda o breve. Devi usare una funzione helper per creare l'oggetto e distinguere il tipo di implementazione dal tipo proiettato.
In secondo luogo, l'uso della proiezione per creare la classe implica che la relativa factory di attivazione venga memorizzata nella cache. Questo è in genere il risultato da ottenere, ma se la factory risiede nello stesso modulo (DLL) che effettua la chiamata, in pratica hai bloccato la DLL impedendone lo scaricamento. In molti casi questo aspetto non è importante, ma alcuni componenti di sistema devono supportare lo scaricamento.
È a questo punto che entra in gioco il termine costruzione uniforme. Indipendentemente dal fatto che il codice di creazione risieda in un progetto che semplicemente utilizza la classe o nel progetto che effettivamente la implementa, puoi liberamente usare la stessa sintassi per creare l'oggetto.
// MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
MyClass c;
Quando crei il progetto del componente con l'opzione -opt[imize]
, la chiamata tramite la proiezione di linguaggio esegue la compilazione fino alla stessa chiamata efficiente alla funzione winrt::make che crea direttamente il tipo di implementazione. Ciò rende la sintassi semplice e prevedibile, evita le ripercussioni sulle prestazioni derivanti dalla chiamata tramite la factory, nonché il blocco del componente nel corso del processo. Oltre che per i progetti dei componenti, questo è utile anche per le applicazioni XAML. Ignorando RoGetActivationFactory per le classi implementate nella stessa applicazione, puoi crearle (senza che debbano essere registrate) negli stessi modi con cui puoi crearle quando si trovano all'esterno del componente.
La costruzione uniforme si applica a qualsiasi chiamata gestita dalla factory dietro le quinte. In pratica, ciò significa che l'ottimizzazione riguarda sia i costruttori che i membri statici. Di seguito è riportato di nuovo tale esempio originale.
MyClass c;
c.Method();
MyClass::StaticMethod();
Senza -opt[imize]
, la prima e l'ultima istruzione richiedono chiamate tramite l'oggetto factory. Con -opt[imize]
, nessuna delle due istruzioni le richiede. Tali chiamate inoltre vengono compilate direttamente a fronte dell'implementazione e possono persino essere impostate come inline. A questo punto entra in campo l'altro termine spesso usato quando si parla di -opt[imize]
, ovvero l'accesso diretto all'implementazione.
Le proiezioni di linguaggio sono pratiche da usare ma, quando hai la possibilità di accedere direttamente all'implementazione, puoi e anzi devi sfruttare tale opportunità per produrre il codice più efficiente possibile. C++/WinRT può eseguire automaticamente questa operazione, senza obbligarti a rinunciare alla sicurezza e alla produttività della proiezione.
Si tratta di un'importante novità perché il componente deve cooperare per consentire alla proiezione di linguaggio di raggiungere e accedere direttamente ai relativi tipi di implementazione. Poiché C++/WinRT è una libreria costituita solo da un'intestazione, puoi esaminarne l'interno e vedere cosa accade. Senza -opt[imize]
, il costruttore MyClass e il membro StaticMethod sono definiti dalla proiezione come mostrato di seguito.
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(); });
}
}
Non è necessario seguire tutti i dettagli sopra riportati. L'obiettivo è mostrare che entrambe le chiamate comportano una chiamata a una funzione denominata call_factory. Questo indica che tali chiamate coinvolgono la cache di factory e che non accedono direttamente all'implementazione. Con -opt[imize]
, queste stesse funzioni non sono definite in alcun modo. Vengono invece dichiarate dalla proiezione e spetta al componente impostarne le definizioni.
Il componente può quindi fornire definizioni in grado di chiamare direttamente nell'implementazione. È a questo punto che si giunge all'importante novità. Queste definizioni vengono generate automaticamente quando usi sia -component
che -opt[imize]
e si trovano in un file denominato Type.g.cpp
, dove Type è il nome della classe di runtime che viene implementata. Questo è il motivo per cui possono verificarsi diversi errori del linker quando inizialmente abiliti -opt[imize]
in un progetto esistente. Per correggere gli errori, devi includere nell'implementazione il file così generato.
Nell'esempio MyClass.h
può risultare simile al codice riportato di seguito, indipendentemente dal fatto che venga usata o meno l'opzione -opt[imize]
.
// MyClass.h
#pragma once
#include "MyClass.g.h"
namespace winrt::MyProject::implementation
{
struct MyClass : ClassT<MyClass>
{
MyClass() = default;
static void StaticMethod();
void Method();
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyClass : ClassT<MyClass, implementation::MyClass>
{
};
}
Tutto inizia a funzionare in MyClass.cpp
.
#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()
{
}
}
Pertanto, per usare la costruzione uniforme in un progetto esistente, devi modificare il file .cpp
di ogni implementazione in modo da inserire #include <Sub/Namespace/Type.g.cpp>
dopo l'inclusione (e la definizione) della classe di implementazione. Questo file fornisce le definizioni delle funzioni che la proiezione ha lasciato non definite. All'interno del file MyClass.g.cpp
tali definizioni sono simili a quanto mostrato di seguito.
namespace winrt::MyProject
{
MyClass::MyClass() :
MyClass(make<MyProject::implementation::MyClass>())
{
}
void MyClass::StaticMethod()
{
return MyProject::implementation::MyClass::StaticMethod();
}
}
La proiezione viene così completata con chiamate efficienti direttamente nell'implementazione, vengono evitate chiamate alla cache di factory e il linker viene soddisfatto.
L'ultima operazione eseguita automaticamente da -opt[imize]
è la modifica dell'implementazione del file module.g.cpp
del progetto (ovvero del file che ti aiuta a implementare le esportazioni di DllGetActivationFactory e DllCanUnloadNow della DLL), in modo che le compilazioni incrementali tendano a essere notevolmente più rapide eliminando il rigido accoppiamento dei tipi richiesto da C++/WinRT 1.0. In questo caso si parla spesso di factory con cancellazione di tipi. Senza -opt[imize]
, il file module.g.cpp
generato per il componente inizia includendo le definizioni di tutte le classi di implementazione, MyClass.h
in questo esempio. Crea quindi direttamente la factory di implementazione per ogni classe come mostrato di seguito.
if (requal(name, L"MyProject.MyClass"))
{
return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
Anche in questo caso, non devi seguire tutti i dettagli. È però utile vedere come ciò richieda la definizione completa di tutte le classi implementate dal componente. Ciò può incidere in modo significativo sul ciclo interno, in quanto ogni modifica relativa a una singola implementazione causerà la ricompilazione di module.g.cpp
. Con -opt[imize]
ciò non avviene più. Si possono invece notare due novità riguardanti il file module.g.cpp
generato. La prima è che il file non include più classi di implementazione. In questo esempio non include MyClass.h
. Crea invece le factory di implementazione senza avere informazioni sulla relativa implementazione.
void* winrt_make_MyProject_MyClass();
if (requal(name, L"MyProject.MyClass"))
{
return winrt_make_MyProject_MyClass();
}
Non è ovviamente necessario includerne le definizioni e spetta al linker risolvere la definizione della funzione winrt_make_Component_Class. Non devi quindi preoccuparti di questo aspetto perché anche il file MyClass.g.cpp
che viene generato automaticamente, e che hai incluso in precedenza per supportare la costruzione uniforme, definisce tale funzione. Di seguito è riportato l'intero file MyClass.g.cpp
generato per questo esempio.
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();
}
}
Come puoi notare, la funzione winrt_make_MyProject_MyClass crea direttamente la factory dell'implementazione. Questo significa che puoi modificare tranquillamente qualsiasi implementazione senza che module.g.cpp
debba essere ricompilato. module.g.cpp
verrà aggiornato e dovrà essere ricompilato solo se aggiungi o rimuovi classi di Windows Runtime.
Override dei metodi virtuali della classe di base
Una classe derivata può avere problemi con i metodi virtuali se la classe di base e quella derivata sono entrambe definite dall'app, mentre il metodo virtuale è definito in una classe padre di Windows Runtime. In pratica, ciò si verifica se le classi sono derivate da classi XAML. La parte restante di questa sezione è una continuazione dell'esempio riportato in Classi derivate.
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);
};
}
La gerarchia è Windows::UI::Xaml::Controls::Page<- BasePage<- DerivedPage. Il metodo BasePage::OnNavigatedFrom esegue correttamente l'override di Page::OnNavigatedFrom, mentre DerivedPage::OnNavigatedFrom non esegue l'override di BasePage::OnNavigatedFrom.
In questo caso, DerivedPage riutilizza la vtable IPageOverrides di BasePage. Ciò significa che non riesce a eseguire l'override del metodo IPageOverrides::OnNavigatedFrom. Una possibile soluzione consiste nel definire BasePage come una classe modello e includerne l'intera implementazione in un file di intestazione, ma ciò ha l'effetto di complicare eccessivamente le cose.
Come soluzione alternativa, puoi dichiarare il metodo OnNavigatedFrom come esplicitamente virtuale nella classe di base. In questo modo, quando la voce vtable per DerivedPage::IPageOverrides::OnNavigatedFrom chiama BasePage::IPageOverrides::OnNavigatedFrom, il producer chiama BasePage::OnNavigatedFrom e quest'ultimo, a causa della relativa virtualizzazione, finisce col chiamare 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);
};
}
A tale scopo è necessario che tutti i membri della gerarchia di classi siano concordi sul valore restituito e sui tipi di parametro del metodo OnNavigatedFrom. In caso contrario, devi usare la versione precedente come metodo virtuale ed eseguire il wrapping delle alternative.
Nota
Non è necessario che IDL dichiari il metodo sovrascritto. Per ulteriori dettagli, vedere Implementare metodi sovrascrivibili.
API importanti
- Modello di struct winrt::com_ptr
- Funzione winrt::com_ptr::copy_from
- Modello di funzione winrt::from_abi
- Modello di funzione winrt::get_self
- Modello di struct winrt::implements
- Modello di funzione winrt::make
- Modello di funzione winrt::make_self
- Funzione winrt::Windows::Foundation::IUnknown::as
- Funzione winrt::Windows::Foundation::IUnknown::try_as