Créer des API avec C++/WinRT

Cette rubrique montre comment créer des API C++/WinRT à l’aide de winrt ::implémente le struct de base, directement ou indirectement. Les synonymes de rédiger dans ce contexte sont produire ou mettre en œuvre. Cette rubrique décrit les scénarios suivants pour implémenter des API sur un type C++/WinRT, dans cet ordre.

Note

Cette rubrique aborde le sujet des composants Windows Runtime, mais uniquement dans le contexte de C++/WinRT. Si vous recherchez du contenu sur Windows Runtime composants qui couvrent tous les langages Windows Runtime, consultez Windows Runtime composants.

  • Vous ne créez pas de classe Windows Runtime (classe runtime) ; vous souhaitez simplement implémenter une ou plusieurs interfaces Windows Runtime pour la consommation locale au sein de votre application. Vous dérivez directement de winrt ::implements dans ce cas et implémentez des fonctions.
  • Vous créez une classe runtime. Vous pouvez créer un composant à consommer à partir d’une application. Vous pouvez également créer un type à consommer à partir de l’interface utilisateur XAML et, dans ce cas, vous implémentez et consommez une classe runtime dans la même unité de compilation. Dans ces cas, vous laissez les outils générer des classes pour vous qui dérivent de winrt ::implements.

Dans les deux cas, le type qui implémente vos API C++/WinRT est appelé type d’implémentation.

Important

Il est important de distinguer le concept d’un type d’implémentation de celui d’un type projeté. Le type projeté est décrit dans Consommer des API avec C++/WinRT.

Si vous ne créez pas de classe runtime

Le scénario le plus simple est celui où votre type implémente une interface Windows Runtime et où vous utilisez ce type dans la même application. Dans ce cas, votre type n’a pas besoin d’être une classe runtime ; juste une classe C++ ordinaire. Par exemple, vous pouvez écrire une application de bureau WinUI 3 basée sur Microsoft ::UI ::Xaml ::Application.

Si votre type est référencé par l’interface utilisateur XAML, il doits’agir d’une classe runtime, même s’il se trouve dans le même projet que le code XAML. Pour ce cas, consultez la section Si vous créez une classe runtime à référencer dans votre interface utilisateur XAML.

Note

Pour plus d’informations sur l’installation et l’utilisation de l’extension Visual Studio C++/WinRT (VSIX) et du package NuGet (qui fournissent ensemble un modèle de projet et une prise en charge de build), consultez Visual Studio prise en charge de C++/WinRT.

Dans Visual Studio, le modèle de projet Application vide, Empaqueté (WinUI 3 in Desktop) pour C++ illustre le modèle d’application WinUI 3. La classe App dérive de Microsoft ::UI ::Xaml ::Application, et le point d’entrée appelle sa méthode 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 classe App crée un Microsoft ::UI ::Xaml ::Window et l’active dans 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 a le modèle de structure de base winrt ::implements pour faciliter l’implémentation d’une interface (ou plusieurs) sans recourir à la programmation de style COM. Vous dérivez simplement votre type des implémentations, puis implémentez les fonctions de l’interface. Voici un exemple qui implémente une interface personnalisée.

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

Si vous créez une classe runtime dans un composant Windows Runtime

Si votre type est empaqueté dans un composant Windows Runtime à des fins de consommation à partir d’un autre binaire (l’autre binaire est généralement une application), votre type doit être une classe runtime. Vous déclarez une classe d’exécution dans un fichier Microsoft Interface Definition Language (IDL) (.idl) (voir Factorisation des classes d’exécution dans les fichiers MIDL (.idl)).

Chaque fichier IDL génère un .winmd fichier et Visual Studio fusionne tous ceux-ci dans un seul fichier portant le même nom que votre espace de noms racine. Ce fichier final .winmd sera celui que les consommateurs de votre composant référenceront.

Voici un exemple de déclaration d’une classe runtime dans un fichier 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;
    }
}

Ce fichier IDL déclare une classe d’exécution Windows Runtime (runtime). Une classe runtime est un type qui peut être activé et consommé via des interfaces COM modernes, généralement au-delà des limites exécutables. Lorsque vous ajoutez un fichier IDL à votre projet et générez, la chaîne d’outils C++/WinRT (midl.exe et cppwinrt.exe) génère un type d’implémentation pour vous. Pour un exemple du fonctionnement du flux de travail du fichier IDL, consultez Contrôles XAML ; liaison à une propriété C++/WinRT.

À l’aide de l’exemple IDL ci-dessus, le type d’implémentation est un stub de struct C++ nommé winrt ::MyProject ::implementation ::MyRuntimeClass dans les fichiers de code source nommés \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h et MyRuntimeClass.cpp.

Le type d’implémentation ressemble à ceci.

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

Notez que le modèle de polymorphisme lié à F utilisé (MyRuntimeClass s’utilise lui-même comme argument de modèle pour sa base, MyRuntimeClassT). C’est aussi ce qu’on appelle le patron curieusement récurrent (CRTP). Si vous suivez la chaîne d’héritage vers le haut, vous rencontrerez MyRuntimeClass_base.

Vous pouvez simplifier l’implémentation de propriétés simples à l’aide de Windows bibliothèques d’implémentation (WIL). Voici comment procéder :

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

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

Voir Propriétés simples.

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

Par conséquent, dans ce scénario, à la racine de la hiérarchie d’héritage se trouve une fois de plus le modèle de structure de base winrt::implements.

Pour plus de détails, de code et d’une procédure pas à pas sur la création d’API dans un composant Windows Runtime, consultez Composants Windows Runtime avec C++/WinRT et Création d’événements dans C++/WinRT.

Si vous créez une classe runtime à référencer dans votre interface utilisateur XAML

Si votre type est référencé par votre interface utilisateur XAML, il doit s’agir d’une classe runtime, même si elle se trouve dans le même projet que le code XAML. Bien qu’elles soient généralement activées au-delà des limites exécutables, une classe runtime peut plutôt être utilisée dans l’unité de compilation qui l’implémente.

Dans ce scénario, vous créez et consommez les API. La procédure d’implémentation de votre classe runtime est essentiellement la même que pour un composant Windows Runtime. Par conséquent, consultez la section précédente : si vous créez une classe runtime dans un composant Windows Runtime. Le seul détail qui diffère est que, de l’IDL, la chaîne d’outils C++/WinRT génère non seulement un type d’implémentation, mais également un type projeté. Il est important d’apprécier que le fait de dire uniquement « MyRuntimeClass » dans ce scénario peut être ambigu ; il existe plusieurs entités portant ce nom, de différents types.

  • MyRuntimeClass est le nom d’une classe runtime. Mais il s’agit vraiment d’une abstraction : déclarée dans IDL et implémentée dans un langage de programmation.
  • MyRuntimeClass est le nom du struct C++ winrt ::MyProject ::implementation ::MyRuntimeClass, qui est l’implémentation C++/WinRT de la classe runtime. Comme nous l’avons vu, s’il existe des projets d’implémentation et de consommation distincts, ce struct existe uniquement dans le projet d’implémentation. Il s’agit du type d’implémentation ou de l’implémentation. Ce type est généré (par l’outil cppwinrt.exe ) dans les fichiers \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h et MyRuntimeClass.cpp.
  • MyRuntimeClass est le nom du type projeté sous la forme du struct C++ winrt ::MyProject ::MyRuntimeClass. S’il existe des projets d’implémentation et de consommation distincts, ce struct existe uniquement dans le projet consommateur. Il s’agit du type projeté ou de la projection. Ce type est généré (par cppwinrt.exe) dans le fichier \MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h.

Type projeté et type d’implémentation

Voici les parties du type projeté qui sont pertinentes pour cette rubrique.

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

Pour obtenir un exemple de procédure pas à pas de l’implémentation de l’interface INotifyPropertyChanged sur une classe runtime, consultez les contrôles XAML ; liaison à une propriété C++/WinRT.

La procédure de consommation de votre classe runtime dans ce scénario est décrite dans Consommer des API avec C++/WinRT.

Factoring des classes runtime dans des fichiers Midl (.idl)

Les modèles de projet et d’élément Visual Studio produisent un fichier IDL distinct pour chaque classe runtime. Cela donne une correspondance logique entre un fichier IDL et ses fichiers de code source générés.

Toutefois, si vous consolidez toutes les classes runtime de votre projet dans un seul fichier IDL, cela peut améliorer considérablement le temps de génération. S’il devait autrement y avoir entre eux des dépendances complexes (ou circulaires), il peut en fait être nécessaire de import consolider. Vous trouverez peut-être plus facile de créer et de passer en revue vos classes runtime si elles sont ensemble.

Constructeurs de classes d’exécution

Voici quelques points à retenir des listes présentées ci-dessus.

  • Chaque constructeur que vous déclarez dans votre IDL entraîne la génération d’un constructeur à la fois sur votre type d’implémentation et sur votre type projeté. Les constructeurs déclarés IDL sont utilisés pour consommer la classe runtime à partir d’une unité de compilation différente.
  • Que vous ayez ou non des constructeurs déclarés en IDL, une surcharge de constructeur acceptant std::nullptr_t est générée pour votre type projeté. L’appel du constructeur std ::nullptr_t est la première des deux étapes de consommation de la classe runtime à partir de la même unité de compilation. Pour plus d’informations et un exemple de code, consultez Utiliser des API avec C++/WinRT.
  • Si vous consommez la classe runtime à partir de la même unité de compilation, vous pouvez également implémenter des constructeurs non par défaut directement sur le type d’implémentation (qui, n’oubliez pas, est dans MyRuntimeClass.h).

Note

Si vous attendez que votre classe runtime soit consommée à partir d’une autre unité de compilation (qui est commune), incluez le ou les constructeurs dans votre IDL (au moins un constructeur par défaut). En procédant ainsi, vous obtiendrez également une implémentation de fabrique en même temps que votre type d’implémentation.

Si vous souhaitez créer et consommer votre classe runtime uniquement dans la même unité de compilation, ne déclarez aucun constructeur dans votre IDL. Vous n’avez pas besoin d’une implémentation de fabrique, et aucune ne sera générée. Le constructeur par défaut de votre type d’implémentation est supprimé, mais vous pouvez facilement le modifier et le modifier par défaut à la place.

Si vous souhaitez créer et consommer votre classe runtime uniquement dans la même unité de compilation, et que vous avez besoin de paramètres de constructeur, créez le ou les constructeurs dont vous avez besoin directement sur votre type d’implémentation.

Méthodes, propriétés et événements de classe Runtime

Nous avons vu que le flux de travail consiste à utiliser IDL pour déclarer votre classe runtime et ses membres, puis que l’outil génère des prototypes et des implémentations stub pour vous. Quant à ces prototypes générés automatiquement pour les membres de votre classe d’exécution, vous pouvez les modifier afin qu’ils utilisent des types différents de ceux que vous déclarez dans votre IDL. Toutefois, vous ne pouvez le faire que tant que le type que vous déclarez dans IDL peut être transféré au type que vous déclarez dans la version implémentée.

Voici quelques exemples.

  • Vous pouvez détendre les types de paramètres. Par exemple, si, dans IDL, votre méthode prend une classe SomeClass, vous pouvez choisir de la remplacer par IInspectable dans votre implémentation. Cela fonctionne parce que tout SomeClass peut être transféré à IInspectable (l’inverse, bien sûr, ne fonctionnerait pas).
  • Vous pouvez accepter un paramètre pouvant être copié par valeur, au lieu de référence. Par exemple, remplacez SomeClass const& par SomeClass. C’est nécessaire lorsque vous devez éviter de capturer une référence au sein d’une coroutine (voir le passage de paramètres).
  • Vous pouvez détendre la valeur de retour. Par exemple, vous pouvez modifier void en winrt ::fire_and_forget.

Les deux derniers sont très utiles lorsque vous écrivez un gestionnaire d’événements asynchrone.

Instanciation et retour des types et interfaces d’implémentation

Pour cette section, prenons un exemple de type d’implémentation nommé MyType, qui implémente les interfaces IStringable et IClosable .

Vous pouvez dériver MyType directement à partir de winrt ::implements (il ne s’agit pas d’une classe runtime).

#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;

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

Vous pouvez également le générer à partir d’IDL (il s’agit d’une classe runtime).

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

Vous ne pouvez pas allouer directement votre type d’implémentation.

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

Toutefois, vous pouvez passer de MyType à un objet IStringable ou IClosable que vous pouvez utiliser ou retourner dans le cadre de votre projection en appelant le modèle de fonction winrt ::make . make retourne l’interface par défaut du type d’implémentation.

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

Note

Toutefois, si vous référencez votre type à partir de votre interface utilisateur XAML, il y aura à la fois un type d’implémentation et un type projeté dans le même projet. Dans ce cas, make retourne une instance du type projeté. Pour obtenir un exemple de code de ce scénario, consultez les contrôles XAML ; liez-vous à une propriété C++/WinRT.

Nous pouvons utiliser istringable (dans l’exemple de code ci-dessus) uniquement pour appeler les membres de l’interface IStringable . Mais une interface C++/WinRT (qui est une interface projetée) dérive de winrt ::Windows ::Foundation ::IUnknown. Par conséquent, vous pouvez appeler IUnknown ::as (ou IUnknown ::try_as) pour rechercher d’autres types ou interfaces projetés, que vous pouvez également utiliser ou retourner.

Tip

Un cas où vous ne devez pas appeler as ou try_as est la dérivation de classes à l’exécution (« classes composables »). Lorsqu’un type d’implémentation compose une autre classe, n’appelez pas as ou try_as pour effectuer un QueryInterface sans vérification ou avec vérification de la classe composée. Au lieu de cela, accédez au membre de données (this->) m_inner, et appelez as ou try_as sur celui-ci. Pour plus d’informations, consultez dérivation de classe Runtime dans cette rubrique.

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

Si vous devez accéder à tous les membres de l’implémentation, puis renvoyer ultérieurement une interface à un appelant, utilisez le modèle de fonction winrt ::make_self . make_self retourne un winrt::com_ptr qui encapsule le type d’implémentation. Vous pouvez accéder aux membres de toutes ses interfaces (à l’aide de l’opérateur flèche), vous pouvez le renvoyer tel quel à l’appelant, ou vous pouvez appeler as sur celui-ci et renvoyer l’objet d’interface obtenu à l’appelant.

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

La classe MyType ne fait pas partie de la projection ; c’est l’implémentation. Mais de cette façon, vous pouvez appeler ses méthodes d’implémentation directement, sans la surcharge d’un appel de fonction virtuelle. Dans l’exemple ci-dessus, même si MyType ::ToString utilise la même signature que la méthode projetée sur IStringable, nous appelons directement la méthode non virtuelle, sans traverser l’interface binaire de l’application (ABI). Le com_ptr contient simplement un pointeur vers le struct MyType . Vous pouvez donc également accéder à n’importe quel autre détail interne de MyType via la myimpl variable et l’opérateur de flèche.

Dans le cas où vous avez un objet d’interface et que vous savez qu’il s’agit d’une interface sur votre implémentation, vous pouvez revenir à l’implémentation à l’aide du modèle de fonction winrt ::get_self . Là encore, il s’agit d’une technique qui évite les appels de fonction virtuelle et vous permet d’accéder directement à l’implémentation.

Note

Si vous n'avez pas installé le sdk Windows version 10.0.17763.0 (Windows 10, version 1809) ou ultérieure, vous devez appeler winrt ::from_abi au lieu de winrt ::get_self.

Voici un exemple. Il existe un autre exemple dans Implémenter la classe de contrôle personnalisé BgLabelControl.

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

Mais seul l’objet d’interface original conserve une référence. Si vous souhaitez la conserver, vous pouvez appeler 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.

Le type d'implémentation lui-même ne dérive pas de winrt::Windows::Foundation::IUnknown, et ne dispose donc pas de fonction as. Même si, comme vous pouvez le voir dans la fonction ImplFromIClosable ci-dessus, vous pouvez accéder aux membres de toutes ses interfaces. Mais si vous le faites, ne retournez pas l’instance de type d’implémentation brute à l’appelant. Utilisez plutôt l’une des techniques déjà présentées, et renvoyez une interface projetée, ou une com_ptr.

Si vous avez une instance de votre type d’implémentation et que vous devez le transmettre à une fonction qui attend le type projeté correspondant, vous pouvez le faire, comme illustré dans l’exemple de code ci-dessous. Un opérateur de conversion existe sur votre type d’implémentation (à condition que le type d’implémentation ait été généré par l’outil cppwinrt.exe ) qui rend cela possible. Vous pouvez transmettre une valeur de type d’implémentation directement à une méthode qui attend une valeur du type projeté correspondant. Depuis une fonction membre d’un type d’implémentation, vous pouvez passer *this à une méthode qui prend en paramètre une valeur du type projeté correspondant.

// 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);
}
...

Dérivation de classe Runtime

Vous pouvez créer une classe runtime qui dérive d’une autre classe runtime, à condition que la classe de base soit déclarée comme « non scellée ». Le terme Windows Runtime pour la dérivation de classe est « classes composables ». Le code d’implémentation d’une classe dérivée varie selon que la classe de base est fournie par un autre composant ou par le même composant. Heureusement, vous n’avez pas besoin d’apprendre ces règles : vous pouvez simplement copier les exemples d’implémentations à partir du sources dossier de sortie produit par le cppwinrt.exe compilateur.

Prenons cet exemple.

// 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();
    }
}

Dans l’exemple ci-dessus, MyButton est dérivé du contrôle Xaml Button , fourni par un autre composant. Dans ce cas, l’implémentation ressemble à l’implémentation d’une classe non composable :

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

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

En revanche, dans l’exemple ci-dessus, MyDerived est dérivé d’une autre classe dans le même composant. Dans ce cas, l’implémentation nécessite un paramètre de modèle supplémentaire spécifiant la classe d’implémentation pour la classe de base.

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

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

Dans les deux cas, votre implémentation peut appeler une méthode à partir de la classe de base en la qualifier avec l’alias de base_type 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

Lorsqu’un type d’implémentation compose une autre classe, n’appelez pas as ou try_as pour effectuer un QueryInterface sans vérification ou avec vérification de la classe composée. Au lieu de cela, accédez au membre de données (this->) m_inner, et appelez as ou try_as sur celui-ci.

Dérivation d’un type qui a un constructeur non par défaut

ToggleButtonAutomationPeer ::ToggleButtonAutomationPeer(ToggleButton) est un exemple de constructeur non par défaut. Il n’existe pas de constructeur par défaut ; donc, pour instancier un ToggleButtonAutomationPeer, il faut passer un owner. Par conséquent, si vous héritez de ToggleButtonAutomationPeer, vous devez fournir un constructeur qui prend un owner et le transmet à la classe de base. Voyons ce que cela ressemble dans la pratique.

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

Le constructeur généré pour votre type d’implémentation ressemble à ceci.

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

Le seul élément manquant est que vous devez transmettre ce paramètre de constructeur à la classe de base. N’oubliez pas le modèle de polymorphisme lié à F que nous avons mentionné ci-dessus ? Une fois que vous connaissez les détails de ce modèle comme utilisé par C++/WinRT, vous pouvez déterminer ce que votre classe de base est appelée (ou vous pouvez simplement consulter le fichier d’en-tête de votre classe d’implémentation). Voici comment appeler le constructeur de la classe de base dans ce cas.

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

Le constructeur de classe de base attend un ToggleButton. Et MySpecializedToggleButtonest unToggleButton.

Tant que vous n’avez pas effectué la modification décrite ci-dessus (pour transmettre ce paramètre de constructeur à la classe de base), le compilateur signale votre constructeur et signale qu’il n’existe aucun constructeur par défaut approprié disponible sur un type appelé (dans ce cas) MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer>. C’est en fait la classe de base de la classe basse de votre type d’implémentation.

Espaces de noms : types projetés, types d’implémentation et fabriques

Comme vous l’avez vu précédemment dans cette rubrique, une classe runtime C++/WinRT existe sous la forme de plusieurs classes C++ dans plusieurs espaces de noms. Par conséquent, le nom MyRuntimeClass a une signification dans l’espace de noms winrt ::MyProject , et une signification différente dans l’espace de noms winrt ::MyProject ::implementation . N’oubliez pas l’espace de noms que vous avez actuellement dans le contexte, puis utilisez les préfixes d’espace de noms si vous avez besoin d’un nom à partir d’un autre espace de noms. Examinons plus en détail les espaces de noms en question.

  • winrt ::MyProject. Cet espace de noms contient des types projetés. Un objet d’un type projeté est un proxy ; il s’agit essentiellement d’un pointeur intelligent vers un objet de stockage, où cet objet de stockage peut être implémenté ici dans votre projet, ou il peut être implémenté dans une autre unité de compilation.
  • winrt ::MyProject ::implementation. Cet espace de noms contient des types d’implémentation. Un objet d’un type d’implémentation n’est pas un pointeur ; il s’agit d’une valeur : un objet de pile C++ complet. Ne construisez pas directement un type d’implémentation ; appelez plutôt winrt ::make, en passant votre type d’implémentation en tant que paramètre de modèle. Nous avons présenté des exemples de winrt ::make dans l’action précédemment dans cette rubrique, et il existe un autre exemple dans les contrôles XAML ; lier à une propriété C++/WinRT. Consultez également Diagnostic des allocations directes.
  • winrt::MyProject::factory_implementation. Cet espace de noms contient des fabriques. Un objet de cet espace de noms prend en charge IActivationFactory.

Ce tableau présente la qualification minimale de l’espace de noms que vous devez utiliser dans différents contextes.

Espace de noms qui est dans le contexte Pour spécifier le type projeté Pour spécifier le type d’implémentation
winrt ::MyProject MyRuntimeClass implementation::MyRuntimeClass
winrt ::MyProject ::implementation MyProject::MyRuntimeClass MyRuntimeClass

Important

Lorsque vous souhaitez retourner un type projeté à partir de votre implémentation, veillez à ne pas instancier le type d’implémentation en écrivant MyRuntimeClass myRuntimeClass;. Les techniques et le code appropriés pour ce scénario sont présentés précédemment dans cette rubrique dans la section Instanciation et retour des types et interfaces d’implémentation.

Le problème avec MyRuntimeClass myRuntimeClass; dans ce scénario est qu’il crée un objet winrt::MyProject::implementation::MyRuntimeClass sur la pile. Cet objet (de type d’implémentation) se comporte comme le type projeté de certaines façons , vous pouvez appeler des méthodes sur celle-ci de la même façon ; et il se convertit même en type projeté. Mais l’objet est détruit, conformément aux règles habituelles du C++, à la sortie de la portée. Par conséquent, si vous avez renvoyé un type projeté (un pointeur intelligent) sur cet objet, alors ce pointeur est maintenant pendant.

Ce type de bogue d’altération de la mémoire est difficile à diagnostiquer. Par conséquent, pour les builds de débogage, une assertion C++/WinRT vous aide à intercepter cette erreur, à l’aide d’un détecteur de pile. Mais les coroutines sont allouées sur le tas, donc vous ne bénéficierez d’aucune aide pour cette erreur si vous la commettez dans une coroutine. Pour plus d’informations, consultez Diagnostic des allocations directes.

Utilisation de types projetés et de types d’implémentation avec différentes fonctionnalités C++/WinRT

Voici différents endroits où les fonctionnalités C++/WinRT attendent un type et le type attendu (type projeté, type d’implémentation ou les deux).

Fonctionnalité Accepte Notes
T (représentant un pointeur intelligent) Prévu Consultez la mise en garde dans Espaces de noms : types projetés, types d’implémentation et fabriques concernant l’utilisation par erreur du type d’implémentation.
agile_ref<T> Les deux Si vous utilisez le type d’implémentation, l’argument du constructeur doit être com_ptr<T>.
com_ptr<T> Mise en œuvre L’utilisation du type projeté génère l’erreur : 'Release' is not a member of 'T'.
default_interface<T> Les deux Si vous utilisez le type d’implémentation, la première interface implémentée est retournée.
get_self<T> Mise en œuvre L’utilisation du type projeté génère l’erreur : '_abi_TrustLevel': is not a member of 'T'.
guid_of<T>() Les deux Retourne le GUID de l’interface par défaut.
IWinRTTemplateInterface<T>
Projeté Utiliser le type d’implémentation compile, mais c’est une erreur — voir la mise en garde dans Espaces de noms : types projetés, types d’implémentation et fabriques.
make<T> Mise en œuvre L’utilisation du type projeté génère l’erreur : 'implements_type': is not a member of any direct or indirect base class of 'T'
make_agile(T const&amp;) Les deux Si vous utilisez le type d’implémentation, l’argument doit être com_ptr<T>.
make_self<T> Mise en œuvre L’utilisation du type projeté génère l’erreur : 'Release': is not a member of any direct or indirect base class of 'T'
name_of<T> Prévu Si vous utilisez le type d’implémentation, vous obtenez le GUID de l’interface par défaut sous forme de chaîne.
weak_ref<T> Les deux Si vous utilisez le type d’implémentation, l’argument du constructeur doit être com_ptr<T>.

Optez pour une construction uniforme et un accès direct à l’implémentation

Cette section décrit une fonctionnalité de C++/WinRT 2.0 qui nécessite une activation explicite, bien qu’elle soit activée par défaut dans les nouveaux projets. Pour un projet existant, vous devrez activer cette fonctionnalité en configurant l’outil cppwinrt.exe. Dans Visual Studio, définissez la propriété de projet Propriétés> communesC++/WinRT>optimisée surOui. Cela a pour effet d’ajouter <CppWinRTOptimized>true</CppWinRTOptimized> à votre fichier de projet. Et il a le même effet que l’ajout du commutateur lors de l’appel cppwinrt.exe à partir de la ligne de commande.

Le -opt[imize] commutateur permet ce qu’on appelle souvent la construction uniforme. Avec la construction uniforme (ou unifiée), vous utilisez la projection de langage C++/WinRT elle-même pour créer et utiliser efficacement vos types d’implémentation (types implémentés par votre composant et destinés à être utilisés par les applications), sans aucun problème de chargement.

Avant de décrire la fonctionnalité, commençons par montrer la situation sans construction uniforme. Pour illustrer, nous allons commencer par cet exemple Windows Runtime classe.

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

En tant que développeur C++ familiarisé avec l’utilisation de la bibliothèque C++/WinRT, vous pouvez utiliser la classe comme celle-ci.

using namespace winrt::MyProject;

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

Et cela serait parfaitement raisonnable à condition que le code consommateur indiqué ne réside pas dans le même composant qui implémente cette classe. En tant que projection de langage, C++/WinRT protège le développeur de l’ABI (l’interface binaire d’application basée sur COM que définit le Windows Runtime). C++/WinRT n’appelle pas directement l’implémentation ; il passe par l’ABI.

Par conséquent, sur la ligne de code où vous construisez un objet MyClass (MyClass c;), la projection C++/WinRT appelle RoGetActivationFactory pour récupérer la classe ou la fabrique d’activation, puis utilise cette fabrique pour créer l’objet. La dernière ligne utilise également la fabrique pour effectuer ce qui semble être un appel à une méthode statique. Tout cela nécessite que votre classe soit inscrite et que votre module implémente le point d’entrée DllGetActivationFactory . C++/WinRT dispose d’un cache d’usine très rapide. Par conséquent, aucune de ces raisons ne pose de problème pour une application consommant votre composant. Le problème est que, dans votre composant, vous venez de faire quelque chose qui est un peu problématique.

Tout d’abord, quelle que soit la vitesse du cache d’usine C++/WinRT, l’appel via RoGetActivationFactory (ou même les appels ultérieurs via le cache de fabrique) sera toujours plus lent que d’appeler directement dans l’implémentation. Un appel à RoGetActivationFactory suivi de IActivationFactory ::ActivateInstance suivi de QueryInterface n’est évidemment pas aussi efficace que l’utilisation d’une expression C++ new pour un type défini localement. Par conséquent, les développeurs C++/WinRT chevronnés sont habitués à utiliser les fonctions d’assistance winrt ::make ou winrt ::make_self lors de la création d’objets au sein d’un composant.

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

Mais, comme vous pouvez le voir, ce n’est pas aussi pratique ni concis. Vous devez utiliser une fonction d’assistance pour créer l’objet, et vous devez également lever l’ambiguïté entre le type d’implémentation et le type projeté.

Deuxièmement, l’utilisation de la projection pour créer la classe signifie que sa fabrique d’activation sera mise en cache. Normalement, c’est ce que vous voulez, mais si la fonction de fabrique se trouve dans le même module (DLL) que celui qui effectue l’appel, vous avez de fait verrouillé la DLL et l’avez empêchée d’être déchargée. Dans de nombreux cas, cela n’a pas d’importance ; mais certains composants système doivent prendre en charge le déchargement.

C’est là que vient le terme construction uniforme . Que le code de création réside dans un projet qui consomme simplement la classe ou qu’il réside dans le projet qui implémente réellement la classe, vous pouvez librement utiliser la même syntaxe pour créer l’objet.

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

Lorsque vous générez votre projet de composant avec le commutateur -opt[imize], l’appel via la projection du langage est compilé en le même appel efficace à la fonction winrt::make, qui crée directement le type d’implémentation. Cela rend votre syntaxe simple et prévisible, évite toute perte de performances liée à l’appel via la fabrique et évite de maintenir le composant en mémoire au passage. En plus des projets de composants, cela est également utile pour les applications XAML. Le contournement de RoGetActivationFactory pour les classes implémentées dans la même application vous permet de les construire (sans avoir besoin d’être inscrits) de la même manière que si elles étaient en dehors de votre composant.

La construction uniforme s’applique à tout appel pris en charge par la fabrique en arrière-plan. En pratique, cela signifie que l’optimisation sert à la fois les constructeurs et les membres statiques. Voici cet exemple d’origine à nouveau.

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

Sans -opt[imize], les premières et dernières instructions nécessitent des appels via l’objet factory. Avec-opt[imize], ni l’un ni l’autre ne le font. Ces appels sont compilés directement par rapport à l’implémentation, et peuvent même être incorporés. Ce qui renvoie à l’autre terme souvent utilisé quand on parle de -opt[imize], à savoir l’accès direct à l’implémentation.

Les projections de langage sont pratiques, mais, lorsque vous pouvez accéder directement à l’implémentation, vous pouvez et devez tirer parti de celle-ci pour produire le code le plus efficace possible. C++/WinRT peut le faire pour vous, sans vous forcer à quitter la sécurité et la productivité de la projection.

Il s’agit d’un changement cassant, car le composant doit coopérer pour permettre à la projection de langage d’atteindre et d’accéder directement à ses types d’implémentation. Comme C++/WinRT est une bibliothèque en-tête uniquement, vous pouvez regarder à l’intérieur et voir ce qui se passe. Sans -opt[imize], le constructeur MyClass et le membre StaticMethod sont définis par la projection comme celle-ci.

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(); });
    }
}

Il n’est pas nécessaire de suivre toutes les étapes ci-dessus ; l’intention est de montrer que les deux appels impliquent un appel à une fonction nommée call_factory. C’est votre indice que ces appels impliquent le cache d’usine et qu’ils n’accèdent pas directement à l’implémentation. Ces-opt[imize] mêmes fonctions ne sont pas définies du tout. Au lieu de cela, ils sont déclarés par la projection, et leurs définitions sont laissées au composant.

Le composant peut ensuite fournir des définitions qui font directement appel à l’implémentation. Nous en arrivons maintenant à la rupture de compatibilité. Ces définitions sont générées pour vous lorsque vous utilisez à la fois -component et -opt[imize]qu’elles apparaissent dans un fichier nommé Type.g.cpp, où Type est le nom de la classe runtime implémentée. C’est pourquoi vous pouvez rencontrer diverses erreurs de l’éditeur de liens lorsque vous activez -opt[imize] pour la première fois dans un projet existant. Vous devez inclure ce fichier généré dans votre implémentation afin d’assembler le tout.

Dans notre exemple, MyClass.h peut ressembler à ceci (qu’il soit -opt[imize] utilisé ou non).

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

Votre MyClass.cpp, c’est là que tout prend forme.

#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()
    {
    }
}

Ainsi, pour utiliser la construction uniforme dans un projet existant, vous devez modifier le fichier .cpp de chaque implémentation afin d’y #include <Sub/Namespace/Type.g.cpp> après l’inclusion (et la définition) de la classe d’implémentation. Ce fichier fournit les définitions de ces fonctions que la projection n’a pas définie. Voici à quoi ressemblent ces définitions dans le MyClass.g.cpp fichier.

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

Et cela termine correctement la projection avec des appels efficaces directement dans l’implémentation, en évitant les appels au cache d’usine et en satisfaisant l’éditeur de liens.

La dernière chose que -opt[imize] vous faites est de modifier l’implémentation de votre projet module.g.cpp (le fichier qui vous aide à implémenter les exportations DllGetActivationFactory et DllCanUnloadNow de votre DLL) de telle sorte que les builds incrémentielles ont tendance à être beaucoup plus rapides en éliminant le couplage de type fort requis par C++/WinRT 1.0. Il s’agit souvent de fabriques effacées de type. Sans -opt[imize], le module.g.cpp fichier généré pour votre composant commence par inclure les définitions de toutes vos classes d’implémentation , le MyClass.h, dans cet exemple. Il crée ensuite directement la fabrique d’implémentation pour chaque classe comme suit.

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

Là encore, vous n’avez pas besoin de suivre tous les détails. Ce qui est utile pour voir, c’est que cela nécessite la définition complète pour toutes les classes implémentées par votre composant. Cela peut avoir un effet spectaculaire sur votre boucle interne, car toute modification apportée à une implémentation unique entraîne module.g.cpp la recompilation. Avec -opt[imize], ce n’est plus le cas. Au lieu de cela, deux choses se produisent dans le fichier généré module.g.cpp . La première est qu’elle n’inclut plus de classes d’implémentation. Dans cet exemple, il n’inclut MyClass.h pas du tout. Au lieu de cela, il crée les fabriques d’implémentation sans aucune connaissance de leur implémentation.

void* winrt_make_MyProject_MyClass();

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

Évidemment, il n’est pas nécessaire d’inclure leurs définitions, et c’est à l’éditeur de liens de résoudre la définition de la fonction winrt_make_Component_Class . Bien sûr, vous n’avez pas besoin de réfléchir à cela, car le MyClass.g.cpp fichier généré pour vous (et que vous avez précédemment inclus pour prendre en charge la construction uniforme) définit également cette fonction. Voici l’intégralité du MyClass.g.cpp fichier généré pour cet exemple.

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();
    }
}

Comme vous pouvez le voir, la fonction winrt_make_MyProject_MyClass crée directement la fabrique de votre implémentation. Tout cela signifie que vous pouvez modifier librement n’importe quelle implémentation, et que le module.g.cpp n’a pas besoin d’être recompilé du tout. Ce n’est que lorsque vous ajoutez ou supprimez des classes Windows Runtime que module.g.cpp est mis à jour et doit être recompilé.

Substitution de méthodes virtuelles de classe de base

Votre classe dérivée peut rencontrer des problèmes avec les méthodes virtuelles si la base et la classe dérivée sont des classes définies par l’application, mais que la méthode virtuelle est définie dans une classe Windows Runtime grand-parent. Dans la pratique, cela se produit si vous dérivez de classes XAML. Le reste de cette section continue de l’exemple dans les classes dérivées.

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 hiérarchie est Microsoft::UI::Xaml::Controls::Page<- BasePage<- DerivedPage. La méthode BasePage ::OnNavigatedFrom remplace correctement Page ::OnNavigatedFrom, mais DerivedPage ::OnNavigatedFrom ne remplace pas BasePage ::OnNavigatedFrom.

Ici, DerivedPage réutilise la table virtuelle IPageOverrides à partir de BasePage, ce qui signifie qu’elle ne parvient pas à remplacer la méthode IPageOverrides ::OnNavigatedFrom . Une solution potentielle nécessite que BasePage soit une classe de modèle et que son implémentation soit entièrement dans un fichier d’en-tête, mais cela rend les choses difficiles à résoudre.

Pour contourner ce problème, déclarez la méthode OnNavigatedFrom comme étant explicitement virtuelle dans la classe de base. Ainsi, lorsque l’entrée de table virtuelle pour DerivedPage ::IPageOverrides ::OnNavigatedFrom appelle BasePage ::IPageOverrides ::OnNavigatedFrom, le producteur appelle BasePage ::OnNavigatedFrom, qui (en raison de sa virtualité), finit par appeler 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);
    };
}

Cela nécessite que tous les membres de la hiérarchie de classes acceptent la valeur de retour et les types de paramètres de la méthode OnNavigatedFrom . S’ils ne sont pas d’accord, vous devez utiliser la version ci-dessus comme méthode virtuelle et encapsuler les alternatives.

Note

Votre IDL n’a pas besoin de déclarer la méthode substituée. Pour plus d’informations, consultez Implémentation de méthodes substituables.

API importantes