Share via


Extensions de composants

Visite guidée de C++/CX

Thomas Petchel

Télécharger l'exemple de code

Êtes-vous prêt à écrire votre première application du Windows Store ? Ou bien avez-vous déjà écrit des applications du Windows Store en HTML/Javascript, C# ou Visual Basic et vous souhaitez simplement découvrir C++ ?

Les extensions de composant Visual C++ (C++/CX) vous permettent d'approfondir vos compétences actuelles en combinant du code C++ avec un vaste éventail de contrôles et de bibliothèques fournis par Windows Runtime (WinRT). Si vous utilisez Direct3D, vous pouvez vraiment mettre en avant vos applications dans le Windows Store.

Certains pensent, à tort, qu'il faut apprendre un nouveau langage pour pouvoir utiliser C++/CX. En fait, dans la plupart des cas, vous devrez seulement gérer quelques éléments de langage non standard, tels que le modificateur « ^ » ou les mots clés ref new. Par ailleurs, vous utiliserez seulement ces éléments à la limite de votre application, c'est-à-dire seulement lorsque vous devrez interagir avec Windows Runtime. Votre ISO C++ portable continuera d'être le moteur de votre application. Et surtout, sachez que C++/CX est entièrement constitué de code natif. Bien que sa syntaxe ressemble à C++/Common Language Infrastructure (CLI), votre application n'introduira pas le CLR, sauf si vous le souhaitez.

Si vous travaillez avec du code C++ qui a déjà été testé ou si vous préférez la flexibilité et les performances de C++, vous pouvez être sûr que vous ne devrez pas apprendre l'ensemble d'un nouveau langage avec C++/CX. Dans cet article, vous allez apprendre en quoi les extensions de langage C++/CX sont uniques pour créer des applications du Windows Store. Vous apprendrez également quand utiliser C++/CX pour créer votre application du Windows Store.

Pourquoi choisir C++/CX ?

Chaque application possède ses exigences propres, tout comme chaque développeur possède ses propres compétences et aptitudes. Vous pouvez créer avec succès une application du Windows Store à l'aide de C++, HTML/JavaScript ou de Microsoft .NET Framework, mais vous pouvez préférer utiliser C++, notamment dans les cas suivants :

  • Vous avez une préférence pour C++ et vous avez acquis des compétences.
  • Vous souhaitez tirer parti du code que vous avez déjà écrit et testé.
  • Vous voulez utiliser des bibliothèques telles que Direct3D et C++ AMP pour optimiser le potentiel de votre matériel.

Vous n'avez pas besoin de trancher :vous pouvez également combiner les langages. Par exemple, lorsque j'ai écrit l'exemple de l'optimiseur de voyage Bing Maps (bit.ly/13hkJhA), j'ai utilisé HTML et JavaScript pour définir l'interface utilisateur, et C++ pour effectuer le traitement en arrière-plan. Le traitement en arrière-plan résout avant tout le problème du voyageur de commerce. J'ai utilisé la bibliothèque de modèles parallèles (PPL, Parallel Patterns Library) (bit.ly/155DPtQ) dans un composant C++ WinRT pour exécuter mon algorithme en parallèle sur tous les processeurs disponibles, afin d'améliorer les performances globales. J'aurais eu du mal à réaliser tout cela en utilisant uniquement JavaScript !

Comment fonctionne C++/CX ?

Windows Runtime est au cœur de chaque application du Windows Store. L'interface binaire d'application (ABI) est au cœur de Windows Runtime. Les bibliothèques WinRT définissent les métadonnées via des fichiers de métadonnées Windows (.winmd). Un fichier .winmd décrit les types publics disponibles. Son format ressemble à celui utilisé dans les assemblys .NET Framework. Dans un composant C++, le fichier .winmd contient uniquement des métadonnées, alors que le code exécutable se trouve dans un fichier distinct. C'est notamment le cas pour les composants WinRT inclus dans Windows. (Pour les langages .NET Framework, le fichier .winmd contient à la fois le code et les métadonnées, à l'instar d'une assembly .NET Framework.) Vous pouvez consulter ces métadonnées via le désassembleur MSIL (ILDASM) ou n'importe quel lecteur de métadonnées du CLR. La figure 1 montre à quoi ressemble Windows.Foundation.winmd dans ILDASM. Windows.Foundation.winmd, qui contient un grand nombre des types WinRT fondamentaux.

Inspecting Windows.Foundation.winmd with ILDASMFigure 1 Inspection de Windows.Foundation.winmd avec ILDASM

L'ABI est créée à l'aide d'un sous-ensemble de COM, afin de permettre à Windows Runtime d'interagir avec plusieurs langages. Pour appeler des API WinRT, le .NET Framework et JavaScript requièrent des projections spécifiques à chaque environnement de langage. Par exemple, le type de chaîne WinRT sous-jacent, HSTRING, est représenté par System.String dans .NET, par un objet String dans JavaScript et par la classe de référence Platform::String en C++/CX.

Bien que C++ puisse interagir directement avec COM, C++/CX a pour objectif de simplifier cette tâche grâce à :

  • Comptage de références automatique (ARC). Les objets WinRT sont comptés par référence et généralement alloués par tas (quels que soient les langages qui les utilisent). Les objets sont détruits lorsque le compte de référence atteint zéro. C++/CX offre un réel avantage : le comptage de références est à la fois automatique et uniforme. La syntaxe ^ permet les deux.
  • Gestion des exceptions. Pour indiquer les défaillances, C++/CX s'appuie sur des exceptions et non sur des codes d'erreur. Les valeurs COM HRESULT sous-jacentes sont converties en types d'exception WinRT.
  • Syntaxe facile à utiliser pour se servir des API WinRT, tout en préservant des performances élevées.
  • Syntaxe facile à utiliser pour créer de nouveaux types WinRT.
  • Syntaxe facile à utiliser pour exécuter la conversion de type, utiliser des événements et exécuter d'autres tâches.

Souvenez-vous que, même si C++/CX emprunte la syntaxe C++/CLI, un code natif pur est produit. Vous pouvez également interagir avec Windows Runtime en utilisant la bibliothèque de modèles C++ Windows Runtime (WRL), que je vous présenterai plus tard. J'espère qu'une fois que vous aurez utilisé C++/CX, vous partagerez mon opinion. Vous profitez des performances et du contrôle du code natif, vous n'avez pas besoin d'apprendre COM et le code qui interagit avec Windows Runtime est le plus concis possible. Vous pouvez alors vous concentrer sur la logique de base qui rend votre application unique. 

C++/CX est activé via l'option de compilateur /ZW. Ce commutateur est défini automatiquement lorsque vous utilisez Visual Studio pour créer un projet Windows Store.

Le jeu du Morpion

Je suis convaincu que la meilleure façon d'apprendre un nouveau langage est de l'utiliser pour créer quelque chose. Pour expliquer les éléments les plus courants de C++/CX, j'ai écrit une application du Windows Store qui joue au Morpion (« Tic-Tac-Toe » en anglais).

Pour cette application, j'ai utilisé le modèle Application vide de Visual Studio (XAML). J'ai appelé ce projet TicTacToe (Morpion). Ce projet utilise le XAML pour définir l'interface utilisateur de l'application. Je ne vais pas consacrer beaucoup de temps à XAML. Pour en savoir plus à ce sujet, reportez-vous à l'article d'Andy Rich, « Présentation de C++/CX et XAML » (msdn.microsoft.com/magazine/jj651573), dans l'édition spéciale Windows 8 2012.

J'ai également utilisé le modèle de composant Windows Runtime pour créer un composant WinRT qui définit la logique de l'application. L'idée que le code soit réutilisé m'est chère, c'est pourquoi j'ai créé un projet de composant distinct, pour que tout le monde puisse utiliser la logique de base du jeu dans n'importe quelle application du Windows Store à l'aide de XAML et de C#, Visual Basic ou C++.

La figure 2 illustre l'application.

The TicTacToe ApFigure 2 L'application TicTacToe

Lorsque j'ai travaillé sur le projet Hilo C++ (bit.ly/Wy5E92), j'ai craqué pour le modèle MVVM (Model-View-ViewModel, Modèle-Vue-VueModèle). MVVM est un modèle d'architecture qui vous aide à séparer l'apparence (la vue) de votre application de ses données sous-jacentes (le modèle). Le modèle de vue connecte la vue au modèle. Je n'ai pas complètement utilisé MVVM pour mon jeu de Morpion, mais j'ai découvert que l'utilisation de la liaison de données pour séparer l'interface utilisateur de la logique de l'application rendait l'application plus simple à écrire, plus lisible et plus facile à gérer au fil du temps. Pour en savoir plus sur l'utilisation de MVVM dans le projet Hilo C++, reportez-vous à bit.ly/XxigPg.

Pour connecter l'application au composant WinRT, j'ai ajouté une référence au projet de bibliothèque Morpion à partir de la boîte de dialogue des pages de propriétés du projet Morpion.

En définissant simplement la référence, le projet Morpion a accès à l'ensemble des types C++/CX publics du projet de bibliothèque Morpion. Vous n'avez pas besoin de spécifier de directive #include ni aucun autre élément.

Création de l'interface utilisateur du Morpion

Comme je l'ai déjà précisé, je ne vais pas m'attarder sur le XAML. Néanmoins, dans la disposition verticale, j'ai configuré une zone pour l'affichage du score, une pour la zone de jeu principale et une pour la configuration du jeu suivant (vous pouvez consulter le XAML dans le fichier MainPage.xaml fourni dans le code à télécharger qui accompagne cet article). Là encore, j'ai beaucoup utilisé la liaison de données.

La définition de la classe MainPage (MainPage.h) est illustrée figure 3.

Figure 3 Définition de la classe MainPage

#pragma once
#include "MainPage.g.h"
namespace TicTacToe
{
  public ref class MainPage sealed
  {
  public:
    MainPage();
    property TicTacToeLibrary::GameProcessor^ Processor
    {
      TicTacToeLibrary::GameProcessor^ get() { return m_processor; }
    }
  protected:
    virtual void OnNavigatedTo(      
        Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override;
  private:
    TicTacToeLibrary::GameProcessor^ m_processor;
  };
}

Que contient le fichier MainPage.g.h ? Un fichier .g.h contient une définition de classe partielle générée par le compilateur pour les pages XAML. En résumé, ces définitions partielles définissent les classes de base et les variables de membre requises pour tout élément XAML ayant l'attribut x:Name. Voici MainPage.g.h :

namespace TicTacToe
{
  partial ref class MainPage :
    public ::Windows::UI::Xaml::Controls::Page,
    public ::Windows::UI::Xaml::Markup::IComponentConnector
  {
  public:
    void InitializeComponent();
    virtual void Connect(int connectionId, ::Platform::Object^ target);
  private:
    bool _contentLoaded;
  };
}

Le mot clé partiel est important, car il permet à une déclaration de type de parcourir des fichiers. Dans le cas présent, MainPage.g.h contient des éléments générés par le compilateur et MainPage.h contient les éléments supplémentaires que je définis.

Remarquez les mots clés « public » et « ref class » dans la déclaration MainPage. Le concept d'accessibilité de classe est l'une des différences entre C++/CX et C++. Si vous êtes programmeur .NET, vous connaissez ce concept. L'accessibilité de classe indique si un type ou une méthode est visible dans les métadonnées, et donc accessible à partir de composants externes. Un type C++/CX peut être public ou privé. S'il est public, cela signifie qu'il est possible d'accéder à la classe MainPage en dehors du module (par exemple, par Windows Runtime ou par un autre composant WinRT). S'il est privé, l'accès est uniquement possible dans le module. Avec les types privés, vous disposez de plus de liberté pour utiliser les types C++ dans les méthodes publiques, ce qui n'est pas possible avec les types publics. Dans le cas présent, la classe MainPage est publique. Elle est donc accessible pour le XAML. Nous examinerons des exemples de types privés plus tard.

Les mots clés « ref class » indiquent au compilateur qu'il s'agit d'un type WinRT et non d'un type C++. Une « ref class » est allouée sur le tas et sa durée de vie est comptée par référence. Étant donné que les types ref sont comptés par référence, leurs durées de vie sont déterministes. Lorsque la dernière référence à un type ref est libérée, son destructeur est appelé et la mémoire de cet objet est libérée. Comparez cela à .NET, où les durées de vie sont moins déterministes et où le nettoyage de la mémoire est utilisé pour libérer de la mémoire.

Lorsque vous instanciez un type ref, vous utilisez en général le modificateur ^ (prononcer « hat »). Le modificateur ^ s'apparente à un pointeur C++ (*), mais il indique au compilateur d'insérer du code pour gérer le compte de référence de l'objet automatiquement et de supprimer l'objet lorsque le compte de référence atteint zéro.

Pour créer une structure POD (Plain Old Data), utilisez une classe de valeur ou une structure de valeur. Les types de valeur ont une taille fixe et contiennent uniquement des champs. Contrairement aux types ref, ils ne possèdent pas de propriétés. Windows::Foundation::DateTime et Windows::Foundation::Rect sont deux exemples de types de valeur WinRT. Lorsque vous instanciez des types de valeur, vous n'utilisez pas le modificateur ^ :

Windows::Foundation::Rect bounds(0, 0, 100, 100);

Remarquez également que MainPage est déclaré « sealed ». Le mot clé « sealed », qui s'apparente au mot clé final C++11, empêche que ce type soit encore dérivé. MainPage est déclaré « sealed », car tous les types ref publics ayant un constructeur public doivent également être déclarés « sealed ». Cela tient au fait que, dans le runtime, le langage n'est pas défini initialement et certains langages (par exemple, JavaScript) ne comprennent pas l'héritage.

Concentrez maintenant votre attention sur les membres MainPage. La variable de membre m_processor (la classe GameProcessor est définie dans le projet de composant WinRT ; j'aborderai ce type pus tard) est privée, tout simplement parce que la classe MainPage est sealed et qu'aucune classe dérivée ne peut l'utiliser (et, en général, les membres de données doivent, dans la mesure du possible, être privés, afin d'appliquer l'encapsulation). La méthode OnNavigatedTo est protégée, car la classe Windows::UI::Xaml::Controls::Page, dont dérive MainPage, déclare cette méthode comme protégée. XAML doit accéder au constructeur et à la propriété Processor. Ces éléments sont donc publics.

Vous connaissez déjà les spécificateurs d'accès publics, protégés et privés. Leurs significations en C++/CX sont les mêmes qu'en C++. Pour en savoir plus sur les spécificateurs internes et sur d'autres spécificateurs C++/CX, reportez-vous à bit.ly/Xqb5Xe. Nous verrons un exemple de spécificateur interne plus tard.

Une « ref class » peut avoir des types accessibles uniquement publiquement dans ses sections publiques et protégées, c'est-à-dire des types primitifs, des types ref publics ou valeur publique. Inversement, un type C++ peut contenir des types ref en tant que variables de membre, dans les signatures de méthode et dans les variables de fonction locale. Voici un exemple du projet Hilo C++ :

std::vector<Windows::Storage::IStorageItem^> m_createdFiles;

L'équipe Hilo utilise std::vector au lieu de Platform::Collections::Vector pour sa variable de membre privé, car la collection n'est pas exposée en dehors de la classe. Utiliser std::vector nous aide à utiliser le plus possible de code C++ et définit clairement les intentions.

À propos du constructeur MainPage :

MainPage::MainPage() : m_processor(ref 
  new TicTacToeLibrary::GameProcessor())
{
  InitializeComponent();
  DataContext = m_processor;
}

J'utilise les mots clés ref new pour instancier l'objet GameProcessor. Utilisez ref new plutôt que new pour construire des objets de type de référence WinRT. Lorsque vous créez des objets dans des fonctions, vous pouvez utiliser le mot clé auto C++ pour réduire la nécessité de spécifier le nom du type ou l'utilisation de ^ :

auto processor = ref new TicTacToeLibrary::GameProcessor();

Création de la bibliothèque du Morpion

Le code de bibliothèque pour le jeu du Morpion contient un mélange de C++ et de C++/CX. Pour cette application, j'ai supposé que du code C++ existant avait déjà été écrit et testé. J'ai directement incorporé ce code et seulement ajouté du code C++/CX pour connecter l'implémentation interne à XAML. En d’autres termes, j'ai uniquement utilisé C++/CX pour relier les deux mondes. Examinons certains éléments importants de la bibliothèque et mettons en évidence les fonctionnalités C++/CX qui n'ont pas encore été abordées.

La classe GameProcessor sert de contexte de données pour l'interface utilisateur (pensez au modèle de vue si vous connaissez le modèle MVVM). Lors de la déclaration de cette classe, j'ai utilisé deux attributs, BindableAttribute et WebHostHiddenAttribute (comme pour .NET, vous pouvez omettre la partie « Attribut » lorsque vous déclarez des attributs) :

[Windows::UI::Xaml::Data::Bindable]
[Windows::Foundation::Metadata::WebHostHidden]
public ref class GameProcessor sealed : public Common::BindableBase

L'attribut BindableAttribute produit des métadonnées qui indiquent au Windows Runtime que le type prend en charge la liaison de données. De cette manière, toutes les propriétés publiques du type sont visibles pour les composants XAML. Je dérive de BindableBase pour implémenter la fonctionnalité requise pour que la liaison fonctionne. Étant donné que BindableBase est destiné à être utilisé par XAML et non par JavaScript, il utilise l'attribut WebHost­HiddenAttribute (bit.ly/ZsAOV3). Conformément à la convention, j'ai également marqué la classe GameProcessor avec cet attribut, afin de le masquer pour JavaScript.

J'ai séparé les propriétés de GameProcessor en sections publiques et internes. Les propriétés publiques sont exposées à XAML, tandis que les propriétés internes sont seulement exposées à d'autres types et fonctions dans la bibliothèque. Je pense que cette distinction permet de mieux mettre en évidence l'objectif du code.

La liaison de collections à XAML est un modèle d'utilisation de propriété courant : 

property Windows::Foundation::Collections::IObservableVector<Cell^>^ Cells
{
  Windows::Foundation::Collections::IObservableVector<Cell^>^ get()
    { return m_cells; }
}

Cette propriété définit les données du modèle pour les cases qui apparaissent sur la grille. Lorsque la valeur des cases change, le XAML est automatiquement mis à jour. Le type de la propriété est IObservableVector, qui est l'un des types définis spécifiquement pour C++/CX afin que l'interopérabilité soit entièrement possible avec Windows Runtime. Windows Runtime définit les interfaces de collections indépendantes du langage et chaque langage implémente ces interfaces à sa manière. En C++/CX, l'espace de noms Platform::Collections fournit des types tels que Vector et Map, qui fournissent des implémentations concrètes pour les interfaces de ces collections. Je peux donc déclarer la propriété Cells comme IObservableVector, mais accompagner cette propriété avec un objet Vector, ce qui est spécifique à C++/CX :

Platform::Collections::Vector<Cell^>^ m_cells;

Alors, quand convient-il d'utiliser les collections Platform::String et Platform::Collections plutôt que les types et les collections standard ? Devriez-vous, par exemple, utiliser std::vector ou Platform::Collections::Vector pour stocker vos données ? En règle générale, j'utilise la fonctionnalité Platform lorsque je prévois de travailler principalement avec Windows Runtime et les types standard tels que std::wstring et std::vector pour mon code interne ou consommant énormément de ressources de calcul. Vous pouvez aussi facilement effectuer une conversion entre Vector et std::vector en cas de besoin. Vous pouvez créer un Vector à partir d'un std::vector ou utiliser to_vector pour créer un std::vector à partir d'un Vector :

std::vector<int> more_numbers =
  Windows::Foundation::Collections::to_vector(result);

Un coût de copie est associé au marshaling entre les deux types de vecteur. Vous devez donc vérifier quel type convient à votre code.

La conversion entre std::wstring et Platform::String est une autre tâche courante. Voici comment procéder :

// Convert std::wstring to Platform::String.
std::wstring s1(L"Hello");
auto s2 = ref new Platform::String(s1.c_str());
// Convert back from Platform::String to std::wstring.
// String::Data returns a C-style string, so you don’t need
// to create a std::wstring if you don’t need it.
std::wstring s3(s2->Data());
// Here's another way to convert back.
std::wstring s4(begin(s2), end(s2));

Il faut noter deux éléments intéressants dans l'implémentation de la classe GameProcessor (GameProcessor.cpp). Premièrement, j'utilise uniquement un C++ standard pour implémenter la fonction checkEndOfGame. C'est ici que je voulais illustrer l'intégration de code C++ existant déjà écrit et testé.

Deuxièmement, j'utilise la programmation asynchrone. Lorsqu'il est temps de changer de tour, j'utilise la classe de tâche PPL pour traiter les joueurs virtuels en arrière-plan, comme illustré figure 4.

Figure 4 Utilisation de la classe de tâche PPL pour traiter les joueurs virtuels en arrière-plan

void GameProcessor::SwitchPlayers()
{
  // Switch player by toggling pointer.
  m_currentPlayer = (m_currentPlayer == m_player1) ? m_player2 : m_player1;
  // If the current player is computer-controlled, call the ThinkAsync
  // method in the background, and then process the computer's move.
  if (m_currentPlayer->Player == TicTacToeLibrary::PlayerType::Computer)
  {
    m_currentThinkOp =
      m_currentPlayer->ThinkAsync(ref new Vector<wchar_t>(m_gameBoard));
    m_currentThinkOp->Progress =
      ref new AsyncOperationProgressHandler<uint32, double>([this](
      IAsyncOperationWithProgress<uint32, double>^ asyncInfo, double value)
      {
        (void) asyncInfo; // Unused parameter
        // Update progress bar.
        m_backgroundProgress = value;
        OnPropertyChanged("BackgroundProgress");
      });
      // Create a task that wraps the async operation. After the task
      // completes, select the cell that the computer chose.
      create_task(m_currentThinkOp).then([this](task<uint32> previousTask)
      {
        m_currentThinkOp = nullptr;
        // I use a task-based continuation here to ensure this continuation
        // runs to guarantee the UI is updated. You should consider putting
        // a try/catch block around calls to task::get to handle any errors.
        uint32 move = previousTask.get();
        // Choose the cell.
        m_cells->GetAt(move)->Select(nullptr);
        // Reset the progress bar.
        m_backgroundProgress = 0.0;
        OnPropertyChanged("BackgroundProgress");
      }, task_continuation_context::use_current());
  }
}

Si vous êtes programmeur .NET, pensez à la tâche et à la méthode then comme la version C++ d'async et await en C#. Les tâches sont disponibles à partir de tous les programmes C++, mais vous devez les utiliser dans l'ensemble de votre code C++/CX pour préserver la rapidité et la fluidité de votre application du Windows Store. Pour en savoir plus sur la programmation asynchrone dans les applications du Windows Store, consultez l'article d'Artur Laksberg de février 2012, « Programmation asynchrone en C++ avec PPL » (msdn.microsoft.com/magazine/hh781020), ainsi que l'article de la bibliothèque MSDN à l'adresse suivante : msdn.microsoft.com/library/hh750082.

La classe Cell modélise une case sur la grille. Cette classe démontre deux nouveaux éléments : les événements et les références faibles.

La grille de la zone de jeu du Morpion se compose de contrôles Windows::UI::Xaml::Controls::Button. Un contrôle Button génère un événement Click, mais vous pouvez également répondre à une entrée utilisateur en définissant un objet ICommand qui détermine le contrat pour la commande. J'utilise l'interface ICommand plutôt que l'événement Click afin que les objets Cell puissent répondre directement. Dans le XAML des boutons qui définissent les cases, la propriété Command établit une liaison avec la propriété Cell::SelectCommand :

<Button Width="133" Height="133" Command="{Binding SelectCommand}"
  Content="{Binding Text}" Foreground="{Binding ForegroundBrush}"
  BorderThickness="2" BorderBrush="White" FontSize="72"/>

J'ai utilisé la classe DelegateCommand Hilo pour implémenter l'interface ICommand. DelegateCommand comprend la fonction d'appel lorsque la commande est exécutée, ainsi qu'une fonction facultative qui détermine si la commande peut être exécutée. Voici la façon dont je configure la commande pour chaque case :

m_selectCommand = ref new DelegateCommand(
  ref new ExecuteDelegate(this, &Cell::Select), nullptr);

Lors de la programmation XAML, vous utilisez souvent des événements prédéfinis, mais vous pouvez également définir vos propres événements. J'ai créé un événement qui est déclenché lorsqu'un objet Cell est sélectionné. La classe GameProcessor gère cet événement en vérifiant si le jeu est terminé et en changeant de joueur, si nécessaire.

Pour créer un événement, vous devez d'abord créer un type de délégué. Assimilez le type de délégué à un pointeur de fonction ou à un objet de fonction :

delegate void CellSelectedHandler(Cell^ sender);

Je crée ensuite un événement pour chaque objet Cell :

event CellSelectedHandler^ CellSelected;

Voici comment la classe GameProcessor souscrit à l'événement pour chaque case :

for (auto cell : m_cells)
{
  cell->CellSelected += ref new CellSelectedHandler(
    this, &GameProcessor::CellSelected);
}

Un délégué construit à partir d'un ^ et d'une fonction pointeur vers membre contient seulement une référence faible à l'objet ^. Cette construction n'entraînera donc pas de références circulaires.

Voici comment les objets Cell déclenchent l'événement lorsqu'ils sont sélectionnés :

void Cell::Select(Platform::Object^ parameter)
{
  (void)parameter;
  auto gameProcessor = 
    m_gameProcessor.Resolve<GameProcessor>();
  if (m_mark == L'\0' && gameProcessor != nullptr &&
    !gameProcessor->IsThinking && 
    !gameProcessor->CanCreateNewGame)
  {
    m_mark = gameProcessor->CurrentPlayer->Symbol;
    OnPropertyChanged("Text");
    CellSelected(this);
  }
}

Quel est la fonction de l'appel Resolve dans le code précédent ? La classe GameProcessor contient une collection d'objets Cell, mais je souhaite que chaque objet Cell puisse accéder à sa classe GameProcessor parente. Si Cell contenait une référence forte à son parent, c'est-à-dire un GameProcessor^, je créerais une référence circulaire. Les références circulaires peuvent empêcher la libération des objets, car l'association mutuelle oblige les deux objets à toujours avoir au moins une référence. Pour empêcher cela, je crée une variable de membre Platform::WeakReference et je la définis à partir du constructeur Cell (prêtez une attention particulière à la gestion de la durée de vie et aux relations de propriété des objets) :

Platform::WeakReference m_gameProcessor;

Lorsque j'appelle WeakReference::Resolve, nullptr est retourné si l'objet n'existe plus. Étant donné que GameProcessor possède des objets Cell, je m'attend à ce que l'objet GameProcessor soit toujours valide.

Dans le cas du jeu de Morpion, je peux rompre la référence circulaire chaque fois qu'une nouvelle grille est créée. En général, j'essaie d'éviter de devoir rompre des références circulaires, car cela peut rendre le code moins facile à gérer. Par conséquent, dans le cas d'une relation parent-enfant, lorsque l'enfant doit accéder à son parent, j'utilise des références faibles. 

Utilisation d'interfaces

Pour faire la différence entre les joueurs réels et les joueurs virtuels, j'ai créé une interface IPlayer avec des implémentations HumanPlayer et ComputerPlayer concrètes. La classe GameProcessor contient deux objets IPlayer, un pour chaque joueur, ainsi qu'une référence supplémentaire au joueur actuel :

 

IPlayer^ m_player1;
IPlayer^ m_player2;
IPlayer^ m_currentPlayer;

La figure 5 illustre l'interface IPlayer.

Figure 5 Interface IPlayer

private interface class IPlayer
{
  property PlayerType Player
  {
    PlayerType get();
  }
  property wchar_t Symbol
  {
    wchar_t get();
  }
  virtual Windows::Foundation::IAsyncOperationWithProgress<uint32, double>^
    ThinkAsync(Windows::Foundation::Collections::IVector<wchar_t>^ gameBoard);
};

L'interface IPlayer étant privée, pourquoi n'ai-je pas simplement utilisé des classes C++ ? À vrai dire, cette démonstration a pour but de vous montrer comment créer une interface et comment créer un type privé qui n'est pas publié vers les métadonnées. Si je devais créer une bibliothèque réutilisable, je déclarerais peut-être IPlayer comme une interface publique, afin que d'autres applications puissent l'utiliser. Je pourrais aussi choisir de m'en tenir à C++ et ne pas utiliser d'interface C++/CX.

La classe ComputerPlayer implémente ThinkAsync en exécutant l'algorithme minimax en arrière-plan (pour explorer cette implémentation, consultez le fichier ComputerPlayer.cpp fourni dans le code à télécharger qui accompagne cet article).

L'algorithme minimax est couramment utilisé pour créer des composants d'intelligence artificielle pour des jeux tels que le Morpion. Pour en savoir davantage sur cet algorithme, consultez l'ouvrage « Intelligence artificielle : une approche moderne » (Prentice Hall, 2010), écrit par Stuart Russell et Peter Norvig.

J'ai adapté l'algorithme minimax de Russell et Norvig pour qu'il s'exécute en parallèle en utilisant le PPL (reportez-vous à minimax.h dans le téléchargement de code). C'était une excellente occasion d'utiliser un C++11 pur pour écrire la partie de mon application qui sollicite le processeur. Je n'ai pas encore battu l'ordinateur et je n'ai jamais vu ce dernier l'emporter sur lui-même à l'occasion d'un jeu ordinateur contre ordinateur. Je suis conscient que le jeu pourrait être plus excitant. Je vous propose donc de provoquer l'action : ajoutez une logique supplémentaire pour que le jeu puisse être gagné. La façon la plus simple serait de faire en sorte que l'ordinateur fasse des choix aléatoires à des moments aléatoires. De manière plus sophistiquée, l'ordinateur pourrait délibérément faire un choix moins optimal à des moments aléatoires. Vous pouvez également ajouter un contrôle Slider à l'interface utilisateur afin d'ajuster la difficulté du jeu (pour que le jeu soit moins difficile, l'ordinateur fait délibérément un choix moins optimal ou fait au moins un choix aléatoire).

Pour la classe HumanPlayer, ThinkAsync n'a aucun rôle. Je génère donc l'exception Platform::NotImplementedException. Pour cela, je dois tester la propriété IPlayer::Player au préalable, mais cela m'épargne une tâche :

IAsyncOperationWithProgress<uint32, double>^
  HumanPlayer::ThinkAsync(IVector<wchar_t>^ gameBoard)
{
  (void) gameBoard;
  throw ref new NotImplementedException();
}

Bibliothèque WRL

Votre caisse à outils contient un outil précieux, à utiliser lorsque C++/CX ne fait pas ce que vous voulez ou lorsque vous préférez travailler directement avec COM : la bibliothèque WRL. Par exemple, lorsque vous créez une extension de support pour Microsoft Media Foundation, vous devez créer un composant qui implémente à la fois les interfaces COM et WinRT. Les classes ref C++/CX pouvant seulement implémenter les interfaces WinRT, vous devez utiliser la bibliothèque WRL pour créer une extension de support, car WRL prend en charge l'implémentation des interfaces COM et WinRT. Pour en savoir plus sur la programmation de WRL, consultez bit.ly/YE8Dxu.

Approfondissement

Au début, j'avais quelques doutes sur les extensions C++/CX, mais ils se sont rapidement dissipés. J'apprécie aujourd'hui le fait qu'elles me permettent d'écrire rapidement des applications du Windows Store et d'utiliser des éléments C++ modernes. Si vous êtes développeur C++, je vous recommande vivement de les essayer.

Je n'ai abordé que quelques-uns des modèles communs que vous rencontrerez lors de l'écriture de code C++/CX. Hilo, une application photo qui utilise C++ et XAML, aborde le sujet de façon plus approfondie et exhaustive. J'ai beaucoup apprécié travailler sur le projet C++ Hilo. J'y fait référence le plus souvent possible lorsque j'écris de nouvelles applications. Je vous invite à le découvrir, à l'adresse bit.ly/15xZ5JL.

Thomas Petchel est un rédacteur-programmeur expérimenté dans la division Developer de Microsoft. Il est membre de l'équipe Visual Studio depuis huit ans. Il y élabore des documents et des exemples de code pour les développeurs.

Merci aux experts techniques suivants d'avoir relu cet article : Michael Blome (Microsoft) et James McNellis (Microsoft)
Michael Blome travaille depuis plus de 10 ans chez Microsoft, il participe à la tâche colossale d'écriture et de réécriture de la documentation MSDN pour Visual C++, DirectShow, la référence sur le langage C#, LINQ et la programmation parallèle dans l'environnement .NET Framework.

James McNellis est un passionné de C++ et développeur de logiciels dans l'équipe Visual C++ chez Microsoft. Il crée des bibliothèques C et C++ exceptionnelles. Il contribue de façon productive sur le sujet du dépassement de capacité de la pile. Vous pouvez le joindre par tweet @JamesMcNellis et sur sa page jamesmcnellis.com.