Komponentenerweiterungen

Einführung in C++/CX

Thomas Petchel

Möchten Sie Ihre erste Windows Store-App schreiben? Oder haben Sie bereits Windows Store-Apps mit HTML/JavaScript, C# oder Visual Basic geschrieben und möchten wissen, was es mit C++ auf sich hat?

Mit den Visual C++-Komponentenerweiterungen (C++/CX) können Sie mit Ihren vorhandenen Fähigkeiten eine neue Stufe erreichen, indem Sie C++-Code mit den umfassenden Steuerelementen und Bibliotheken der Windows-Runtime (WinRT) kombinieren. Und wenn Sie Direct3D verwenden, können Ihre Apps im Windows Store wirklich herausstechen.

Wenn von C++/CX die Rede ist, denken manche Entwickler, dass sie eine ganz neue Sprache lernen müssten. Tatsächlich haben Sie in den meisten Fällen nur mit einer Handvoll von nicht standardsprachlichen Elementen zu tun, beispielsweise dem ^-Modifizierer oder den ref new-Schlüsselwörtern. Diese Elemente verwenden Sie außerdem nur am Rand der App, also nur, wenn eine Interaktion mit Windows-Runtime erforderlich ist. Ihr portabler ISO-C++ dient weiterhin als Arbeitspferd der App. Und das vielleicht Beste ist, dass C++/CX zu 100 Prozent systemeigener Code ist. Obwohl die Syntax der von C++/Common Language Infrastructure (CLI) ähnelt, bringt Ihre App die CLR nur dann ein, wenn Sie dies möchten.

Sie können sicher sein, dass Sie für C++/CX keine komplett neue Sprache lernen müssen, ob Sie nun vorhandenen, bereits getesteten C++-Code haben oder einfach die Flexibilität und Leistung von C++ bevorzugen. In diesem Artikel erfahren Sie, was die C++/CX-Spracherweiterungen zum Erstellen von Windows Store-Apps so einzigartig macht, und wann Sie C++/CX verwenden, um eine Windows Store-App zu erstellen.

Gründe für C++/CX

Jede App hat ihre eigenen, einmaligen Anforderungen, genau wie alle Entwickler ihre eigenen, jeweils einzigartigen Kenntnisse und Fähigkeiten haben. Sie können eine Windows Store-App mit C++, HTML/JavaScript oder Microsoft .NET Framework erstellen. Es gibt aber auch einige Gründe, warum Sie sich für C++ entscheiden könnten:

  • Sie bevorzugen C++ und haben C++-Kenntnisse.
  • Sie möchten Code nutzen, den Sie bereits geschrieben und getestet haben.
  • Sie möchten Bibliotheken wie Direct3D und C++ AMP verwenden, um das gesamte Potenzial der Hardware auszuschöpfen.

Sie müssen sich nicht für das eine oder das andere entscheiden – Sie können auch Sprachen kombinieren. So habe ich im Beispiel zum Reise-Optimierer von Bing Maps (bit.ly/13hkJhA) die Benutzeroberfläche mit HTML und JavaScript definiert und C++ verwendet, um die Hintergrundverarbeitung auszuführen. Der Hintergrundprozess löst im Grunde das Problem des Handelsreisenden. Ich habe die Parallel Patterns Library (PPL, siehe bit.ly/155DPtQ) in einer WinRT-C++-Komponente verwendet, um den Algorithmus parallel auf allen verfügbaren CPUs auszuführen und dadurch die Gesamtleistung zu verbessern. Nur mit JavaScript wäre das schwierig gewesen!

Funktionsweise von C++/CX

Der Kern jeder Windows Store-App ist Windows-Runtime, und der Kern von Windows-Runtime ist die binäre Anwendungsschnittstelle (Application Binary Interface, ABI). WinRT-Bibliotheken definieren Metadaten durch Windows-Metadatendateien (.WINMD). Eine WINMD-Datei beschreibt die verfügbaren öffentlichen Typen. Ihr Format ähnelt dem, das in .NET Framework-Assemblys verwendet wird. In einer C++-Komponente enthält die WINMD-Datei nur Metadaten, der ausführbare Code befindet sich einer separaten Datei. Das trifft auf die WinRT-Komponenten zu, die in Windows enthalten sind. (Bei .NET Framework-Sprachen enthält die WINMD-Datei sowohl den Code als auch die Metadaten, genau wie eine .NET Framework-Assembly.) Sie können diese Metadaten im MSIL-Disassembler (ILDASM) oder einem anderen CLR-Metadaten-Leser anzeigen. Abbildung 1 zeigt, wie „Windows.Foundation.winmd“, die viele der grundlegenden WinRT-Typen enthält, in ILDASM aussieht.

Inspecting Windows.Foundation.winmd with ILDASMAbbildung 1: Prüfen von „Windows.Foundation.winmd“ mit ILDASM

Die ABI wird mithilfe einer COM-Teilmenge erstellt, um die Interaktion von Windows-Runtime mit mehreren Sprachen zu ermöglichen. .NET Framework und JavaScript benötigen zum Aufrufen von WinRT-APIs Projektionen, die für jede Sprachumgebung spezifisch sind. Zum Beispiel wird der zugrunde liegende WinRT-Zeichenfolgetyp HSTRING in .NET als „System.String“, in JavaScript als String-Objekt und in C++/CX als Verweisklasse „Platform::String“ dargestellt.

C++ kann zwar direkt mit COM interagieren, aber C++/CX zielt mithilfe der folgenden Punkte auf eine Vereinfachung dieser Aufgabe ab:

  • Automatische Verweiszählung. WinRT-Objekte verwenden eine Verweiszählung und normalerweise eine Heapzuweisung (unabhängig davon, welche Sprache sie verwenden). Die Objekte werden zerstört, wenn ihre Verweiszählung null erreicht. Der Vorteil bei C++/CX besteht darin, dass die Verweiszählung sowohl automatisch als auch einheitlich ist. Beides wird durch die ^-Syntax ermöglicht.
  • Ausnahmebehandlung. C++/CX stützt sich nicht auf Fehlercodes, sondern auf Ausnahmen, um Fehler anzugeben. Die zugrunde liegenden COM-HRESULT-Werte werden in WinRT-Ausnahmetypen übersetzt.
  • Eine benutzerfreundliche Syntax zur Verarbeitung der WinRT-APIs, während eine hohe Leistung beibehalten wird.
  • Eine benutzerfreundliche Syntax zum Erstellen neuer WinRT-Typen.
  • Eine benutzerfreundliche Syntax zum Ausführen von Typkonvertierungen, Arbeiten mit Ereignissen und für weitere Aufgaben.

Und wie Sie schon wissen, leiht sich C++/CX zwar die C++/CLI-Syntax, produziert aber rein systemeigenen Code. Für die Interaktion mit Windows-Runtime können Sie auch die C++-Vorlagenbibliothek für Windows-Runtime (Windows Runtime C++ Template Library, WRL) verwenden, die ich später noch vorstelle. Jedenfalls hoffe ich, dass Sie nach der Verwendung von C++/CX meiner Meinung sind, dass C++/CX sinnvoll ist. Sie erhalten die Leistung und Kontrolle von systemeigenem Code, das Erlernen von COM entfällt, und Ihr Code für die Interaktion mit Windows-Runtime ist so kompakt wie möglich. So können Sie sich auf die Kernlogik konzentrieren, die Ihre App auszeichnet. 

C++/CX wird durch die Compileroption „/ZW“ aktiviert. Dieser Schalter wird automatisch festgelegt, wenn Sie Visual Studio zum Erstellen eines Windows Store-Projekts verwenden.

Ein Tic-Tac-Toe-Spiel

Die beste Methode zum Lernen einer neuen Sprache ist meiner Meinung nach, tatsächlich etwas damit zu erstellen. Um die gebräuchlichsten Teile von C++/CX zu zeigen, habe ich eine Windows Store-App geschrieben, die Tic-Tac-Toe spielt (möglicherweise nennen Sie das Spiel „Kreis und Kreuz“ oder „XXO“, je nachdem, wo Sie aufgewachsen sind).

Für diese App habe ich die Visual Studio-Vorlage „Leere App (XAML)“ verwendet. Ich habe das Projekt TicTacToe genannt. Die Benutzeroberfläche der App wird im Projekt mit XAML definiert. Ich vertiefe das Thema XAML hier nicht weiter. Weitere Informationen dazu erhalten Sie in der Windows 8-Sonderausgabe von 2012 im von Andy Rich verfassten Artikel „Einführung in C++/CX und XAML“ (msdn.microsoft.com/magazine/jj651573).

Ich habe auch die Vorlage „Komponente für Windows-Runtime“ verwendet, um eine WinRT-Komponente zu erstellen, die die Logik der App definiert. Ich finde die Wiederverwendung von Code hervorragend. Daher habe ich ein separates Komponentenprojekt erstellt, sodass jeder die Kernlogik des Spiels in einer beliebigen Windows Store-App, die XAML und C#, Visual Basic oder C++ verwendet, nutzen kann.

Abbildung 2 zeigt, wie die App aussieht.

The TicTacToe ApAbbildung 2: Die TicTacToe-App

Während meiner Arbeit am Hilo-C++-Projekt (bit.ly/Wy5E92) habe ich festgestellt, wie großartig das Model-View-ViewModel(MVVM)-Muster ist. MVVM ist ein Architekturmuster, mit dem Sie die Darstellung bzw. Ansicht der App von deren zugrunde liegenden Daten bzw. dem Modell trennen können. Das Ansichtsmodell verbindet die Ansicht mit dem Modell. Obwohl ich MVVM für mein Tic-Tac-Toe-Spiel nicht vollständig verwendet haben, war die App doch dadurch einfacher zu schreiben, besser zu lesen und zukünftig einfacher zu verwalten, dass ich die Benutzeroberfläche durch Datenbindung von der App-Logik getrennt habe. Weitere Informationen dazu, wie wir MVVM im Hilo-C++-Projekt verwendet haben, erhalten Sie hier: bit.ly/XxigPg.

Um die App mit der WinRT-Komponente zu verbinden, habe ich im Dialogfeld „Eigenschaftenseiten“ des TicTacToe-Projekts einen Verweis auf das TicTacToeLibrary-Projekt hinzugefügt.

Das TicTacToe-Projekt hat einfach durch das Einrichten des Verweises Zugriff auf alle öffentlichen C++/CX-Typen im TicTacToeLibrary-Projekt. Sie müssen keine #include-Direktiven angeben; es ist nichts weiter erforderlich.

Erstellen der TicTacToe-Benutzeroberfläche

Wie bereits erwähnt, vertiefe ich den XAML-Code nicht weiter. In meinem vertikalen Layout habe ich aber einen Bereich zum Anzeigen des Punktestands eingerichtet, einen für den Hauptspielbereich und einen, um das nächste Spiel einzurichten. Den XAML-Code finden Sie im dazugehörigen Codedownload in der Datei „MainPage.xaml“. Ich mache hier wieder umfassenden Gebrauch von der Datenbindung.

Abbildung 3 zeigt die Definition der MainPage-Klasse („MainPage.h“).

Abbildung 3: Die Definition der MainPage-Klasse

#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;
  };
}

Was ist nun in „MainPage.g.h“ enthalten? Eine .g.h-Datei enthält eine compilergenerierte partielle Klassendefinition für XAML-Seiten. Diese Teildefinitionen definieren im Grunde die erforderlichen Basisklassen und Membervariablen für alle XAML-Elemente, die das x:Name-Attribut aufweisen. Hier sehen Sie „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;
  };
}

Das partial-Schlüsselwort ist wichtig, da es eine Typdeklaration ermöglicht, um mehrere Dateien zu umfassen. In diesem Fall enthält „MainPage.g.h“ compilergenerierte Teile, und „MainPage.h“ enthält die zusätzlichen Teile, die ich definiere.

In der MainPage-Deklaration verwende ich die public- und ref class-Schlüsselwörter. Ein Unterschied zwischen C++/CX und C++ ist das Konzept der Klassen-Barrierefreiheit. Wenn Sie .NET-Programmierer sind, ist Ihnen dies bereits bekannt. Klassen-Barrierefreiheit bedeutet, dass ein Typ oder eine Methode in den Metadaten sichtbar ist, und dass externe Komponenten daher darauf zugreifen können. Ein C++/CX-Typ kann öffentlich oder privat sein. Öffentlich bedeutet, dass Komponenten außerhalb des Moduls (z. B. Windows-Runtime oder eine andere WinRT-Komponente) auf die MainPage-Klasse zugreifen können. Auf einen privaten Typ kann nur innerhalb des Moduls zugegriffen werden. Private Typen geben Ihnen mehr Freiheit, C++-Typen in öffentlichen Methoden zu verwenden, was mit öffentlichen Typen nicht möglich ist. In diesem Fall ist die MainPage-Klasse öffentlich, sodass XAML auf sie zugreifen kann. Ich gehe später auf einige Beispiele für private Typen ein.

Die ref class-Schlüsselwörter teilen dem Compiler mit, dass dies ein WinRT-Typ und kein C++-Typ ist. Eine Verweisklasse wird im Heap zugeordnet, und für ihre Gültigkeitsdauer wird die Verweiszählung verwendet. Aufgrund der Verweiszählung der Verweistypen ist deren Gültigkeitsdauer deterministisch. Wenn der letzte Verweis auf ein Objekt des Verweistyps veröffentlicht wird, wird dessen Destruktor aufgerufen, und der Speicher für dieses Objekt wird freigegeben. Vergleichen wir dies mit .NET, ist dort die Gültigkeitsdauer weniger deterministisch, und Speicher wird mit der Garbage Collection freigegeben.

Wenn Sie einen Verweistyp instanziieren, verwenden Sie normalerweise den Modifizierer „^“ (ausgesprochen wie das englische „hat“). Der ^-Modifizierer entspricht einem C++-Zeiger („*“), weist den Compiler aber an, Code einzufügen, mit dem die Verweiszählung des Objekts automatisch verwaltet und das Objekt gelöscht wird, wenn seine Verweiszählung null erreicht.

Um eine Plain Old Data(POD)-Struktur zu erstellen, verwenden Sie eine Werteklasse oder Wertestruktur. Werttypen haben eine feste Größe und umfassen nur Felder. Anders als Verweistypen haben sie keine Eigenschaften. Zwei Beispiele für WinRT-Werttypen sind „Windows::Foundation::DateTime“ und „Windows::Foundation::Rect“. Wenn Sie Werttypen instanziieren, verwenden Sie keinen ^-Modifizierer:

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

Beachten Sie auch, dass „MainPage“ als versiegelt deklariert ist. Das sealed-Schlüsselwort, das dem final-Schlüsselwort in C++11 ähnelt, verhindert eine weitere Ableitung von diesem Typ. „MainPage“ ist versiegelt, da jeder öffentliche Verweistyp, der einen öffentlichen Konstruktor hat, auch als versiegelt deklariert werden muss. Der Grund dafür ist, dass die Laufzeit sprachagnostisch ist und nicht alle Sprachen (z. B. JavaScript) Vererbung verstehen.

Sehen Sie sich nun die MainPage-Member an. Die m_processor-Membervariable (die GameProcessor-Klasse wird im WinRT-Komponentenprojekt definiert, worauf ich später eingehe) ist einfach deshalb privat, weil die MainPage-Klasse versiegelt ist und keine Möglichkeit besteht, dass eine abgeleitete Klasse sie verwenden kann (und im Allgemeinen müssen Datenmember privat sein, sofern möglich, um die Kapselung zu erzwingen). Die OnNavigatedTo-Methode ist geschützt, da die Windows::UI::Xaml::Controls::Page-Klasse, von der „MainPage“ abgeleitet ist, diese Methode als geschützt deklariert. XAML muss auf den Konstruktor und die Processor-Eigenschaft zugreifen, und daher sind beide öffentlich.

Mit öffentlichen, geschützten und privaten Zugriffsspezifizierern sind Sie bereits vertraut; sie haben in C++/CX dieselbe Bedeutung wie in C++. Informationen über interne und andere C++/CX-Spezifizierer erhalten Sie unter bit.ly/Xqb5Xe. Ich gebe später ein Beispiel für einen internen Spezifizierer.

Eine Verweisklasse kann in ihren öffentlichen und geschützten Abschnitten nur Typen haben, auf die öffentlich zugegriffen werden kann, d. h. nur primitive Typen, öffentliche Verweistypen oder öffentliche Werttypen. Umgekehrt kann ein C++-Typ Verweistypen als Membervariablen enthalten, in Methodensignaturen und in lokalen Funktionsvariablen. Hier ein Beispiel aus dem Hilo-C++-Projekt:

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

Das Hilo-Team verwendet „std::vector“ anstelle von „Platform::Collections::Vector“ für diese private Membervariable, da wir die Auflistung nicht außerhalb der Klasse verfügbar machen. Wir verwenden „std::vector“, um so viel C++-Code wie möglich zu verwenden, und um die Absicht des Codes deutlich zu machen.

Weiter geht es mit dem MainPage-Konstruktor:

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

Ich verwende die ref new-Schlüsselwörter, um das GameProcessor-Objekt zu instanziieren. Verwenden Sie „ref new“ anstelle von „new“, um WinRT-Verweistypobjekte zu erstellen. Wenn Sie Objekte in Funktionen erstellen, können Sie das C++-Schlüsselwort „auto“ verwenden, um so die Notwendigkeit zu verringern, den Typnamen zu spezifizieren oder „^“ zu verwenden.

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

Erstellen der TicTacToe-Bibliothek

Der Bibliothekscode für das TicTacToe-Spiel enthält eine Mischung aus C++ und C++/CX. Für diese App habe ich vorgegeben, über vorhandenen C++-Code zu verfügen, den ich schon geschrieben und getestet habe. Ich habe diesen Code direkt integriert und C++/CX-Code nur hinzugefügt, um die interne Implementierung mit XAML zu verbinden. Mit anderen Worten, ich habe C++/CX nur verwendet, um eine Brücke zwischen den beiden Welten zu bauen. Lassen Sie uns einige wichtige Teile der Bibliothek Schritt für Schritt ansehen und besonders auf einige C++/CX-Features eingehen, die noch nicht besprochen wurden.

Die GameProcessor-Klasse dient als Datenkontext für die Benutzeroberfläche (denken Sie an das Ansichtsmodell, wenn Sie mit MVVM vertraut sind). Ich habe beim Deklarieren dieser Klasse die zwei Attribute „BindableAttribute“ und „WebHostHiddenAttribute“ verwendet (wie bei .NET können Sie den Attribute-Teil beim Deklarieren von Attributen auslassen):

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

„BindableAttribute“ produziert Metadaten, die Windows-Runtime mitteilen, dass der Typ die Datenbindung unterstützt. Dadurch wird gewährleistet, dass alle öffentlichen Eigenschaften des Typs für die XAML-Komponenten sichtbar sind. Ich leite von „BindableBase“ ab, um die erforderliche Funktionalität zu implementieren, damit die Bindung funktioniert. Da „BindableBase“ für die Verwendung von XAML und nicht von JavaScript vorgesehen ist, verwendet „BindableBase“ das Attribut „WebHostHiddenAttribute“ (bit.ly/ZsAOV3). Gemäß der Konvention habe ich auch die GameProcessor-Klasse mit diesem Attribut versehen, um sie im Prinzip für JavaScript auszublenden.

Ich habe die GameProcessor-Eigenschaften in öffentliche und interne Abschnitte getrennt. Die öffentlichen Eigenschaften werden für XAML verfügbar gemacht, die internen Eigenschaften werden nur für andere Typen und Funktionen in der Bibliothek verfügbar gemacht. Ich finde, dass diese Unterscheidung dazu beiträgt, die Absicht des Codes zu verdeutlichen.

Ein verbreitetes Verwendungsmuster für Eigenschaften ist die Bindung von Auflistungen an XAML: 

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

Diese Eigenschaft definiert die Modelldaten für die Zellen, die im Raster angezeigt werden. Ändert sich der Wert von „Cells“, wird der XAML-Code automatisch aktualisiert. Der Typ der Eigenschaft ist „IObservableVector“. Dies ist einer von mehreren Typen, die speziell für C++/CX definiert wurden, um eine vollständige Interoperabilität mit Windows-Runtime zu ermöglichen. Windows-Runtime definiert sprachunabhängige Auflistungsschnittstellen, und jede Sprache implementiert diese Schnittstellen auf ihre eigene Art. In C++/CX stellt der Platform::Collections-Namespace Typen wie beispielsweise „Vector“ und „Map“ bereit, die konkrete Implementierungen für diese Auflistungsschnittstellen bieten. Ich kann daher die Cells-Eigenschaft als „IObservableVector“ deklarieren, diese Eigenschaft aber durch ein Vector-Objekt stützen, welches spezifisch für C++/CX ist:

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

Wann werden nun also die Auflistungen „Platform::String“ und „Platform::Collections“ anstelle der Standardtypen und -auflistungen verwendet? Ist es zum Beispiel empfehlenswert, „std::vector“ oder „Platform::Collections::Vector“ zum Speichern Ihrer Daten zu nutzen? Als Faustregel verwende ich die Plattformfunktionalität, wenn ich hauptsächlich mit Windows-Runtime arbeiten möchte, und Standardtypen wie „std::wstring“ und „std::vector“ für internen oder rechenintensiven Code. Sie können im Bedarfsfall auch einfach eine Konvertierung zwischen „Vector“ und „std::vector“ ausführen. Sie können „Vector“ aus „std::vector“ erstellen, oder Sie verwenden „to_vector“, um „std::vector“ aus „Vector“ zu erstellen:

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

Das Marshalling zwischen den zwei Vektortypen bedeutet Kopieraufwand. Überlegen Sie daher auch hier, welcher Typ für Ihren Code geeignet ist.

Eine weitere häufige Aufgabe ist die Konvertierung zwischen „std::wstring“ und „Platform::String“. So funktioniert es:

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

In der Implementierung der GameProcessor-Klasse („GameProcessor.cpp“) möchte ich auf zwei interessante Punkte hinweisen. Erstens verwende ich nur Standard-C++, um die checkEndOfGame-Funktion zu implementieren. Dies ist eine Stelle, an der ich zeigen möchte, wie vorhandener C++-Code integriert wird, den ich bereits geschrieben und getestet haben.

Und zweitens verwende ich asynchrone Programmierung. Wenn gewechselt werden soll, wer am Zug ist, verwende ich die task-Klasse der PPL, um die Computerspieler im Hintergrund zu verarbeiten, wie in Abbildung 4 gezeigt wird.

Abbildung 4: Verwenden der task-Klasse der PPL, um die Computerspieler im Hintergrund zu verarbeiten

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

Wenn Sie .NET-Programmierer sind, stellen Sie sich „task“ und die dazugehörige Methode „then“ als die C++-Version von „async“ und „await“ in C# vor. Tasks sind in allen C++-Programmen verfügbar, aber Sie verwenden sie in Ihrem gesamten C++/CX-Code, damit die Windows Store-Apps schnell und fließend bleiben. Weitere Informationen zur asynchronen Programmierung in Windows Store-Apps erhalten Sie in Artur Laksbergs Artikel vom Februar 2012, „Asynchrone Programmierung in C++ mit PPL“ (msdn.microsoft.com/magazine/hh781020) und im MSDN-Bibliotheksartikel unter msdn.microsoft.com/library/hh750082.

Die Cell-Klasse modelliert eine Zelle auf der Spieleoberfläche. Diese Klasse zeigt zwei neue Aspekte: Ereignisse und schwache Verweise.

Das Raster für den TicTacToe-Spielebereich besteht aus Windows::UI::Xaml::Controls::Button-Steuerelementen. Ein Button-Steuerelement löst ein Click-Ereignis aus. Sie können aber auch auf eine Benutzereingabe reagieren, indem Sie ein ICommand-Objekt definieren, das den Vertrag für Befehle definiert. Ich verwende die ICommand-Schnittstelle anstelle des Click-Ereignisses, sodass Cell-Objekte direkt antworten können. Im XAML für die Schaltflächen, die die Zellen definieren, wird die Command-Eigenschaft an die Cell::SelectCommand-Eigenschaft gebunden:

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

Ich habe die Hilo-DelegateCommand-Klasse verwendet, um die ICommand-Schnittstelle zu implementieren. „DelegateCommand“ umfasst die Funktion, die aufzurufen ist, wenn der Befehl ausgegeben wird, und eine optionale Funktion, die bestimmt, ob der Befehl ausgegeben werden kann. Den Befehl für die jeweilige Zelle habe ich folgendermaßen eingerichtet:

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

In der Regel verwenden Sie vordefinierte Ereignisse, wenn Sie XAML-Code programmieren, aber Sie können auch eigene Ereignisse definieren. Ich habe ein Ereignis erstellt, das ausgelöst wird, wenn ein Cell-Objekt ausgewählt wird. Die GameProcessor-Klasse verarbeitet dieses Ereignis, indem sie prüft, ob das Spiel beendet ist, und den aktuellen Spieler wechselt, falls erforderlich.

Zum Erstellen eines Ereignisses müssen Sie zuerst einen Delegattyp erstellen. Stellen Sie sich einen Delegattyp als einen Funktionszeiger oder ein Funktionsobjekt vor:

delegate void CellSelectedHandler(Cell^ sender);

Ich erstelle anschließend ein Ereignis für jedes Cell-Objekt:

event CellSelectedHandler^ CellSelected;

Die GameProcessor-Klasse wird folgendermaßen mit dem Ereignis für jede Zelle verknüpft:

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

Ein Delegat, der aus einem „^“ und einer pointer-to-member-Funktion (PMF) erstellt wird, besitzt nur einen schwachen Verweis zum ^-Objekt. Dieses Konstrukt führt daher nicht zu Zirkelverweisen.

Die Cell-Objekte lösen, wenn sie ausgewählt werden, das Ereignis wie folgt aus:

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

Wozu dient der Resolve-Aufruf im vorstehenden Code? Die GameProcessor-Klasse umfasst eine Auflistung von Cell-Objekten, aber ich möchte, dass jedes Cell-Objekt auf seine übergeordnete GameProcessor-Klasse zugreifen kann. Wenn „Cell“ einen starken Verweis auf seine übergeordnete Klasse enthielte – mit anderen Worten, ein „GameProcessor^“ – würde ich einen Zirkelverweis erstellen. Zirkelverweise können dazu führen, dass Objekte nie freigegeben werden, da beide Objekte durch die gegenseitige Zuordnung immer mindestens einen Verweis haben. Um dies zu vermeiden, erstelle ich eine Platform::WeakReference-Membervariable und richte sie im Cell-Konstruktor ein (berücksichtigen Sie die Lebensdauerverwaltung und überlegen Sie sorgfältig, welche Objekte was besitzen).

Platform::WeakReference m_gameProcessor;

Wenn ich „WeakReference::Resolve“ aufrufe, wird für den Fall, dass das Objekt nicht mehr vorhanden ist, „nullptr“ zurückgegeben. Da „GameProcessor“ Cell-Objekte besitzt, erwarte ich, dass das GameProcessor-Objekt immer gültig ist.

Im Falle meines TicTacToe-Spiels kann ich den Zirkelverweis jedes Mal unterbrechen, wenn eine neue Spieleoberfläche erstellt wird. Im Allgemeinen versuche ich zu vermeiden, dass das Unterbrechen von Zirkelverweisen erforderlich ist, da der Code dadurch schlechter zu verwalten sein kann. Wenn ich eine Beziehung zwischen über- und untergeordneten Elementen habe und die untergeordneten auf die übergeordneten zugreifen müssen, verwende ich daher schwache Verweise. 

Arbeiten mit Schnittstellen

Um zwischen menschlichen Spielern und Computerspielern zu unterscheiden, habe ich eine IPlayer-Schnittstelle mit den konkreten Implementierungen „HumanPlayer“ und „ComputerPlayer“ erstellt. Die GameProcessor-Klasse besitzt zwei IPlayer-Objekte – eines für jeden Spieler – und einen zusätzlichen Verweis auf den aktuellen Spieler:

 

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

Abbildung 5 zeigt die IPlayer-Benutzeroberfläche.

Abbildung 5: Die IPlayer-Benutzeroberfläche

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

Die IPlayer-Schnittstelle ist privat, also warum habe ich nicht einfach C++-Klassen verwendet? Ehrlich gesagt deshalb, weil ich zeigen wollte, wie eine Schnittstelle erstellt wird, und wie ein privater Typ erstellt wird, der nicht für die Metadaten veröffentlicht wird. Wenn ich eine wiederverwendbare Bibliothek erstellen würde, könnte ich „IPlayer“ als öffentliche Schnittstelle deklarieren, damit andere Apps sie nutzen könnten. Andernfalls könnte ich mich dafür entscheiden, bei C++ zu bleiben und keine C++/CX-Schnittstelle zu verwenden.

Die ComputerPlayer-Klasse implementiert „ThinkAsync“, indem sie einen Algorithmus namens Minimax im Hintergrund ausführt (diese Implementierung können Sie im dazugehörigen Codedownload in der Datei „ComputerPlayer.cpp“ untersuchen).

Minimax ist ein verbreiteter Algorithmus bei der Erstellung von Komponenten mit künstlicher Intelligenz für Spiele wie Tic-Tac-Toe. Weitere Informationen über Minimax erhalten Sie im Buch „Künstliche Intelligenz: Ein moderner Ansatz“, Prentice Hall 2010, von Stuart Russell und Peter Norvig.

Ich habe den Minimax-Algorithmus von Russell und Norvig für die parallele Ausführung mithilfe der PPL angepasst (siehe „minimax.h“ im Codedownload). Dies war eine großartige Möglichkeit, um reinen C++11-Code zu verwenden und damit den prozessorintensiven Teil meiner App zu schreiben. Ich muss den Computer immer noch schlagen und habe noch nicht erlebt, dass der Computer sich selbst in einem Computer-gegen-Computer-Spiel schlägt. Ich gebe zu, dass das Spiel dadurch nicht besonders spannend ist. Also können Sie jetzt tätig werden: Fügen Sie zusätzliche Logik hinzu, damit das Spiel zu gewinnen ist. Die Basismethode hierfür ist, den Computer zu zufälligen Zeitpunkten eine zufällige Auswahl treffen zu lassen. Eine ausgeklügeltere Methode ist, dass Sie den Computer zu zufälligen Zeitpunkten absichtlich nicht den besten Zug machen lassen. Wenn Sie Bonuspunkte sammeln möchten, fügen Sie der Benutzeroberfläche einen Schieberegler hinzu, der die Schwierigkeit des Spiels anpasst (je einfacher, desto häufiger macht der Computer nicht den besten Zug oder zumindest einen zufällig ausgewählten).

Für die HumanPlayer-Klasse muss „ThinkAsync“ nichts tun, daher löse ich die Platform::NotImplementedException-Ausnahme aus. Das erfordert, das ich zuerst die IPlayer::Player-Eigenschaft teste, aber ich spare eine Aufgabe:

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

Die WRL

Wenn C++/CX nicht Ihren Anforderungen entspricht, oder wenn Sie lieber direkt mit COM arbeiten möchten, findet sich in Ihrer Toolbox ein großartiges, umfassendes Werkzeug: die WRL. Wenn Sie beispielsweise eine Medienerweiterung für Microsoft Media Foundation erstellen, müssen Sie eine Komponente erstellen, die sowohl COM- als auch WinRT-Schnittstellen implementiert. Da C++/CX-Verweisklassen nur WinRT-Schnittstellen implementieren können, müssen Sie zum Erstellen einer Medienerweiterung die WRL verwenden. Diese unterstützt die Implementierung von COM- und WinRT-Schnittstellen. Mehr über die WRL-Programmierung erfahren Sie unter bit.ly/YE8Dxu.

Vertiefung

Trotz anfänglicher Bedenken wurden mir die C++/CX-Erweiterungen schnell zur zweiten Natur, und ich verwende sie gerne, da ich mit ihnen Windows Store-Apps schnell schreiben und moderne C++-Idiome verwenden kann. Wenn Sie C++-Entwickler sind, kann ich nur empfehlen, die C++/CX-Erweiterungen zumindest auszuprobieren.

Ich habe mir gerade einige der häufigen Muster angesehen, die Ihnen beim Schreiben von C++/CX-Code begegnen. Hilo, eine Foto-App, die C++ und XAML verwendet, ist detailreicher und viel vollständiger. Die Arbeit am Hilo-C++-Projekt war hochinteressant, und ich komme oft darauf zurück, wenn ich neue Apps schreibe. Sehen Sie es sich auf jeden Fall unter bit.ly/15xZ5JL an.

Thomas Petchelarbeitet als leitender Redakteur in der Microsoft Developer Division. In den letzten acht Jahren hat er zusammen mit dem Visual Studio-Team Dokumentation und Codebeispiele für die Zielgruppe der Entwickler erstellt.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Michael Blome (Microsoft) und James McNellis (Microsoft)
Michael Blome arbeitet seit über 10 Jahren bei Microsoft. Er widmet sich der Sysiphusaufgabe, MSDN-Dokumentation zu Visual C++, DirectShow, zur C#-Sprachreferenz, zu LINQ und zur parallelen Programmierung in .NET Framework zu schreiben und umzuschreiben.

James McNellis ist ein begeisterter Anhänger von C++ und Softwareentwickler im Visual C++-Team bei Microsoft, wo er herausragende C- und C++-Bibliotheken erstellt. Er schreibt viele Beiträge für Stack Overflow, twittert unter @JamesMcNellis und kann online auch über jamesmcnellis.com erreicht werden.