Astuces DotNet (Sébastien Courtois)

08/11/2010

[DirectX 10/11] Tutoriel 1 : Création d’une application Win32

Filed under: 3D, C++, Débutant, DirectX, Win32 — Étiquettes : , , , , , , — sebastiencourtois @ 00:09

J’ai fait une petite coupure avec ce blog compte tenu des préparatifs de mon départ aux USA et de l’avancement de certains projets persos. Au passage, je voulais souligner la création d’un nouveau blog d’un ex-futur-collègue qui vient de rejoindre la communauté des blogueurs .NET : Gilles Peron.

J’ai aussi décidé de me lancer dans une nouvelle série d’articles sur la programmation en parallèle de celle sur XNA. Le sujet est donc DirectX 10/11. L’une des grosses nouveautés est que nous allons faire ça à l’ancienne : en C++ (oui j’ai dû réapprendre à faire du C++ pour vous Sourire).

  • Introduction à DirectX

DirectX est une boite à outil pour la création de jeu vidéo fournie par Microsoft et fonctionnant principalement sur Windows. DirectX est composé de nombreux catégories chacune étant liée à une partie précise d’un jeu (Direct3D pour le graphisme 3D, DirectInput pour la gestion clavier/souris/manette, DirectPlay pour le réseau …).

Nous en sommes aujourd’hui à la onzième version de DirectX (sorti en 2009). Toutefois, DirectX 11 ajoute des nouvelles fonctionnalités à DirectX 10 qui était une évolution majeure de l’API. C’est pourquoi nous étudierons les deux versions 10 et 11.


  • Installation et configuration du kit de développement DirectX

Pour développer en DirectX, il faut :

    • Un environnement de développement C++
    • Le kit de développement Direct X : Page MSDN DirectX SDK.

Une fois le SDK installé, il suffit de créer  un projet C++ vide et d’ajouter les dossiers includes et libs du DirectX SDK (par défaut dans c:\Program Files\) au projet.

blog_dx_t1_0 blog_dx_t1_1 blog_dx_t1_2
Création d’un nouveau projet C++ vide Ajout du répertoire libs DirectX au projet Ajout du répertoire include DirectX au projet
  • La base d’une application DirectX : La fenêtre

Vous êtes maintenant prêt développer en DirectX. Pour cela, il est nécessaire d’avoir une fenêtre dans laquelle nous allons afficher les informations de notre jeu. Pour cela, plusieurs choix sont possibles. Dans ce tutoriel, je ferais ce que recommande Microsoft dans son tutoriel, l’utilisation de l’API Win32. Cette API complexe existe depuis que Windows existe et gère tout ce qui est fenêtre et gestion des entrées/sortie. D’autres méthodes de gestion de fenêtres plus simple existent (notamment QT) mais elles ne sont pas toutes compatible avec DirectX 10/11.

L’objectif de ce tutoriel va donc être de créer une fenêtre que l’on pourra fermer en appuyant sur la croix de la barre de titre ou en appuyant sur Echap.

Remarque : L’API Win32 et DirectX sont des API distinctes. Win32 est une API qu’il n’est pas spécialement nécessaire de maitriser parfaitement si votre objectif est uniquement de faire de la 3D ou du jeu vidéo. Dans le texte qui suit, je vais essayer de décrire au maximum chacune des propriétés. Il n’est pas nécessaire de mémoriser toute ces informations (ou alors uniquement pour votre culture général).

  • Le point d’entrée de notre programme : WinMain

Tout programme commence toujours par un point d’entrée. En C/C++ classique, ce point d’entrée est écrit ainsi :

#include <stdio.h>
int main(int argc,char **argv)
{
    printf("Hello World !!");
    return 0;
}

La même chose, en Win32, s’écrit de la façon suivante :

#include <stdio.h>
#include <Windows.h>
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    MessageBox(NULL,"Hello World","Titre",MB_ICONERROR);
    return 0;
}

Au niveau des changements entre les deux codes, on voit tout d’abord l’ajout d’un fichier windows.h. Ce fichier contient les fonctions principales de l’API Win32.

La signature du point d’entrée est plus complexe. Cette méthode retourne un nombre au système d’exploitation. La valeur 0 correspond à un programme qui s’est terminé correctement. Le mot APIENTRY signifie que la méthode est une méthode qui peut être appelé (CALLBACK). Le nom de la méthode WinMain est le nom du point d’entrée.Ce nom est défini dans windows.h et est obligatoire pour être reconnu comme un point d’entrée valide. Les deux paramètres suivants correspondent aux instances du programme. La première est l’instance pour le programme que le WinMain va lancer. La deuxième correspond à l’instance du programme qui appel ce WinMain. Dans notre cas, hPrevInstance sera NULL car nous lançons directement l’application (nous ne passons pas par un autre programme pour la lancer.). Le paramètre lpCmdLine correspond aux paramètres qui sont ajouté en arguement au lancement de l’application (équivalent du paramètre argv d’un main C/C++ classique).Pour récupérer la commande complète, vous pouvez aussi utiliser la méthode GetCommandLine. Enfin le dernier paramètre indique comment la fenêtre devra être affiché à son lancement (minimisé, plein écran …). Si vous voulez plus d’informations sur le WinMain, vous trouverez les informations voulues sur la MSDN.

Afin de tester que tout fonctionne correctement, nous allons demander l’affichage d’une boite de dialogue d’erreur. Celle ci est disponible gràce à la méthode MessageBox. Cette méthode prend en premier paramètre le handle (sorte de pointeur) sur la fenêtre qui l’a crée. Dans notre exemple, nous n’avons pas de fenêtre donc nous utiliserons la valeur NULL pour ce paramètre. Ensuite, il est nécessaire de fournir le texte puis le titre que l’on souhaite afficher. Enfin, on peut customiser les icones,boutons de la boite de dialogues avec le dernier paramètre. La liste et l’utilisation de ce paramètre est décrit en détail sur la MSDN.

Lorsque l’on lance ce morceau de code, on obtient le résultat suivant :

blog_dx_t1_3

  • Création d’une fenêtre de base

Nous allons maintenant chercher à créer une fenêtre normale. Pour cela nous allons rajouter 3 variables globales : Un Handle sur la fenêtre, deux nombre représentant la largeur et la hauteur de la fenêtre.

//Handle de la fenêtre
HWND hWnd;        
//Largeur de la fenêtre
int windowWidth = 800;    
//Hauteur de la fenêtre
int windowHeight = 600;

On modifie la méthode WinMain afin de créer et gérer un fenêtre.

//Point d'entrée du programme
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    //Initialisation d'une fenêtre
    if(!InitWindow(hWnd,hInstance,nCmdShow)) return 0;

    // Boucle 
    MSG msg = {0};
    while (WM_QUIT != msg.message)
    {
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);            
        }    
    }
    return (int) msg.wParam;
}

Une première étape va être de créer et d’afficher la fenêtre. Cela va être fait par la méthode InitWindow que nous allons créer dans un moment.

Pour l’instant, nous allons nous concentrer sur la boucle. Windows gère ses fenêtres grâce à un système de message. Ainsi lorsqu’un ordre est donné à une fenêtre (minimiser la fenêtre, la fermer …), le système envoie un message à la fenêtre. Cette boucle regarde si des messages sont disponibles avec la méthode PeekMessage puis analyse le message et agit en conséquence (TranslateMessage) puis dispatch le message à d’autres fenêtres (les fenêtres enfants par exemples). On sort de la boucle lorsque le message de fermeture de l’application (Message WM_QUIT) est reçu par a fenêtre.

La valeur retour du WinMain dépend du wParam du dernier message. En effet, les messages peuvent aussi remonter des codes erreurs. Par conséquent, on utilise cette valeur plutôt que donner la valeur 0 en dur.

Concentrons-nous maintenant sur la création de la fenêtre proprement dite :

//Initialisation de la fenêtre Win32
bool InitWindow(HWND hWnd,HINSTANCE hInstance,int nCmdShow)
{
    //Structure d'information sur une fenêtre
    WNDCLASSEX wndStruct;
    ZeroMemory(&wndStruct,sizeof(WNDCLASSEX));
    wndStruct.cbSize        = sizeof(WNDCLASSEX);
    wndStruct.style            = CS_HREDRAW | CS_VREDRAW;
    wndStruct.lpfnWndProc    = (WNDPROC)wndProc;
    wndStruct.hInstance        = hInstance;
    wndStruct.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wndStruct.hbrBackground    = (HBRUSH)COLOR_WINDOW;
    wndStruct.lpszClassName    = TEXT("TutorialWindow");
    RegisterClassEx(&wndStruct);

    hWnd = CreateWindow( "TutorialWindow", 
                         "Tutorial 1 : Win32", 
                         WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                         CW_USEDEFAULT, 
                         CW_USEDEFAULT, 
                         windowWidth, 
                         windowHeight, 
                         NULL, 
                         NULL, 
                         hInstance, 
                         NULL);

    if(!hWnd) return false;

    ShowWindow(hWnd,nCmdShow);
    UpdateWindow(hWnd);

    return true;
}

Afin de créer une fenêtre, il est nécessaire de remplir une structure qui va décrire la fenêtre. Cette structure est WNDCLASSEX. Elle contient un grand nombre de propriété mais nous n’en utiliserons que 7 pour notre exemple.

cbSize Taille de la structure (en octets)
style Indique le comportement de la fenêtre. Dans notre cas, nous demandons à redessiner le contenu de la fenêtre lors qu’il y a  un changement vertical ou horizontal de la fenêtre. Les valeurs possibles sont disponibles ici.
lpfnWndProc Callback permettant de récupérer les évènements de la fenêtre (souris, clavier …). Nous allons créer cette méthode (wndProc) dans le paragraphe suivant.
hInstance Instance de l’application (celle fourni par le WinMain)
hCursor Curseur de souris lorsque celui ci passe sur la fenêtre (IDC_Arrow correspond au curseur de base).
hbrBackground Couleur de fond de la fenêtre
lpszClassName Nom pour la fenêtre. A ne pas confondre avec le titre de la fenêtre. Ce nom de fenêtre sera utilisé lors de la création de fenêtre pour identifier la structure WNDCLASSEX que l’on souhaite utiliser.

Plus d’informations sur la structure WNDCLASSEX sur la MSDN.

Une fois la structure de la fenêtre est définie, il est nécessaire de l’enregistrer dans le système afin de pouvoir la réutiliser. Cela se fait avec la méthode RegisterClassEx.

L’étape suivante est la création du handle de la fenêtre. On utilise pour cela la méthode CreateWindow. Le premier paramètre correspond au nom de la fenêtre comme décrit dans la structure WNDCLASSEX. Le deuxième paramètre est le titre de la fenêtre. Ensuite, on peut choisir le style de la fenêtre (barre de titre, bouton de fermeture …). La liste complète des possibilités se trouve ici. Les quatre paramètres suivants sont la position x,y du coin supérieur gauche de la fenêtre puis sa largeur et hauteur. La constante CW_USEDEFAULT est utilisé ici afin de laisser au système d’exploitation le choix de la position de la fenêtre. Le paramètre suivant indique si cette fenêtre doit être lié à une  autre fenêtre (ce n’est pas notre cas de figure donc NULL). Le NULL suivant correspond au handle d’un menu. Vu que nous ne souhaitons pas de menu, nous mettons une nouvelle fois NULL. Le paramètre qui suit n’est autre que l’instance à laquelle sera lié la fenêtre. Le dernier paramètre n’est pas utile dans notre cas (cas des fenêtres multiples MDI).

La description complète de cette méthode se trouve ici.

Une fois créé, on récupère un handle pour notre fenêtre. Si ce handle est NULL, c’est que la fenêtre n’a pas été créée. Dans le cas contraire, on peut l’afficher avec la méthode ShowWindow auquel on passe le paramètre nCmdShow reçu par le WinMain qui indique comment s’affiche la fenêtre (plein écran, minimisé…).

On appelle enfin la méthode UpdateWindow afin de forcer le rafraichissement de la fenêtre.

  • Réponses aux évènements de la fenêtre

Une fenêtre reçoit des évènements venant des périphériques (clavier,souris). Ce type d’évènements est à prendre en charge dans une méthode indiqué dans la propriété lpfnWndProc de la structure WNDCLASSEX (voir plus haut). Dans notre cas, elle s’appelle wndProc et à toujours la même signature.

LRESULT CALLBACK wndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
        case WM_KEYDOWN    :    
            if(wParam == VK_ESCAPE)
                PostQuitMessage(0);
        break;        
        case WM_DESTROY    :    
            PostQuitMessage(0);
        break;
    }
    
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Cela fonctionne sur un système de message comme pour la boucle principale sauf que celle ci est spécifique à l’application. Lorsqu’un nouveau message pour la fenêtre arrive, la méthode wndProc est appelée. Nous gérons ici deux messages.

WN_DESTROY est reçu lorsque la fenêtre va être détruite. Cela peut provenir d’un clic sur la croix de fermeture de la fenêtre mais cela peut aussi venir d’autres facteurs comme le fermeture de l’application dans le gestionnaire de tâches ou encore l’extinction de l’ordinateur.

WM_KEYDOWN correspond à une touche du clavier qui a été enfoncée. Le code de la touche est disponible grâce au paramètre wParam.

Vous pouvez trouver ici la liste des messages pour wndProc.

On appelle la méthode PostQuitMessage avec un code retour 0 (aucun problème) pour envoyer un message de fermeture de l’application (donc de la fenêtre). Cela enverra un message WM_Quit à la boucle du WinMain qui sortira du programme.

  • La récompense

Nous sommes arrivés au bout de notre périple. Si vous lancez l’application, vous devriez vous retrouver avec la fenêtre suivante.

blog_dx_t1_4

Si ce n’est pas le cas, je vous donne mon projet de solution (VS 2010).

  • Conclusion

J’imagine votre première réaction à la fin de ce tutoriel : “Tout ça pour ….. ça Triste !!!“. Effectivement le code Win32 est assez indigeste, complexe (j’en connais qui aime ça Sourire) pour un résultat très médiocre. Il s’agit pourtant de la base de tout jeu vidéo DirectX et vous êtes obligé de passer par là pour commencer un projet. La bonne nouvelle est qu’une fois que c’est fait, on n’a plus à s’en préoccuper et on peut réutiliser le même code sans problème. Je vous conseille donc de  garder votre projet sous la main car vous pourriez en avoir besoin très bientôt.

Créez un site Web ou un blog gratuitement sur WordPress.com.