DirectX 11 - Tutoriel 2 : Création du cadre de travail et d'une fenêtre

RasterTek

Traducteur

Jérôme Marsaguet

Avant de commencer à coder avec DirectX 11, je vous recommande de construire un squelette de code simple. Cette ossature prendra en charge les fonctionnalités de base des fenêtres et vous fournira un moyen simple d'étendre le code de façon organisée et lisible.

Commentez

- Titre : DirectX 11 - Tutoriel 2 : Création du cadre de travail et d'une fenêtre

- Auteur : RasterTek

- Traducteur : Jérôme Marsaguet

- Parution : 7 avril 2014

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Craig. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- II. Introduction

- III. Le cadre de travail

- IV. WinMain

- V. Systemclass.h

- VI. Systemclass.cpp

- VII. Inputclass.h

- VIII. Inputclass.cpp

- IX. Graphicsclass.h

- X. Graphicsclass.cpp

- XI. Résumé

- XII. Exercices à faire

- XIII. Code source

- XIV. Remerciements

- Navigation

Navigation

Tutoriel précédent : Mise en place de DirectX 11 avec Visual Studio

Sommaire

Tutoriel suivant : Initialisation de DirectX 11

II - Introduction

Avant de commencer à coder avec DirectX 11, je vous recommande de construire un squelette de code simple. Cette ossature prendra en charge les fonctionnalités de base des fenêtres et vous fournira un moyen simple d'étendre le code de façon organisée et lisible. Comme le but de ces tutoriels est juste d'essayer différentes fonctionnalités de DirectX 11, nous garderons une base aussi claire que possible.

III - Le cadre de travail

Le cadre de travail débutera par quatre éléments. Une fonction WinMain pour traiter le point d'entrée de l'application. Une classe System encapsulant l'ensemble de l'application qui sera appelée au sein de la fonction WinMain. Dans la classe System, nous aurons une classe Input pour la gestion des entrées de l'utilisateur et une classe Graphics pour manipuler le code graphique DirectX. Voici un diagramme de cette configuration :

Maintenant que nous voyons comment le squelette est organisé, commençons par regarder la fonction WinMain dans le fichier main.cpp.

IV - WinMain

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : main.cpp
////////////////////////////////////////////////////////////////////////////////
#include "systemclass.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow)
{
SystemClass* System;
bool result;

// Créer l'objet System.
System = new SystemClass;
if(!System)
{
return 0;
}

// Initialiser et exécuter l'objet System.
result = System->Initialize();
if(result)
{
System->Run();
}

// Fermer et libérer l'objet System.
System->Shutdown();
delete System;
System = 0;

return 0;
}

Comme vous pouvez le voir nous avons gardé la fonction WinMain assez simple. Nous créons un objet System, puis l'initialisons. S'il s'initialise sans problème, nous appelons sa méthode Run qui exécutera sa propre boucle ainsi que tout le code de l'application jusqu'à ce qu'elle se termine. Après cela, nous fermons l'objet System et le libérons. Nous avons donc fait très simple et encapsulé toute l'application à l'intérieur de la classe System. Jetons maintenant un œil au fichier d'en-tête de la classe System.

V - Systemclass.h

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : systemclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_

Nous définissons ici WIN32_LEAN_AND_MEAN. Nous faisons cela pour accélérer la compilation, réduisant la taille des fichiers d'en-tête Win32 en excluant certaines bibliothèques moins utilisées.

///////////////////////////////
// PRE-PROCESSING DIRECTIVES //
///////////////////////////////
#define WIN32_LEAN_AND_MEAN

Windows.h est inclus afin que nous puissions appeler les fonctions pour créer/détruire des fenêtres et utiliser les autres fonctions Win32 utiles.

//////////////
// INCLUDES //
//////////////
#include <windows.h>

Nous avons inclus les en-têtes des deux autres classes de notre ensemble de composants ici afin que nous puissions les utiliser dans la classe System.

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "graphicsclass.h"

La définition de la classe est assez simple. Nous voyons la définition des méthodes Initialize, Shutdown, et Run qui sont appelées dans WinMain, ainsi que quelques fonctions privées qui seront appelées à l'intérieur de ces fonctions. Nous avons également une fonction MessageHandler dans la classe pour gérer les messages du système de fenêtres envoyés à l'application durant son exécution. Enfin, nous avons quelques variables privées m_Input et m_Graphics qui pointeront vers les deux objets se chargeant des graphiques et des entrées.

////////////////////////////////////////////////////////////////////////////////
// Nom de la classe : SystemClass
////////////////////////////////////////////////////////////////////////////////
class SystemClass
{
public:
SystemClass();
SystemClass(const SystemClass&);
~SystemClass();

bool Initialize();
void Shutdown();
void Run();

LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);

private:
bool Frame();
void InitializeWindows(int&, int&);
void ShutdownWindows();

private:
LPCWSTR m_applicationName;
HINSTANCE m_hinstance;
HWND m_hwnd;

InputClass* m_Input;
GraphicsClass* m_Graphics;
};

/////////////////////////
// FUNCTION PROTOTYPES //
/////////////////////////
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

/////////////
// GLOBALS //
/////////////
static SystemClass* ApplicationHandle = 0;

#endif

La fonction WndProc et le pointeur ApplicationHandle sont également inclus dans ce fichier de classe afin que nous puissions rediriger les messages du système de fenêtres dans notre méthode MessageHandler de la classe System.

Jetons maintenant un œil au fichier source de la classe System :

VI - Systemclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : systemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "systemclass.h"

Dans le constructeur de la classe, j'initialise les pointeurs d'objet à NULL. Ceci est important parce que si l'initialisation de ces objets échoue, la fonction Shutdown tentera plus tard de libérer ces objets. S'ils ne sont pas NULL, alors cela suppose qu'ils pointent vers des objets valides créés et ayant besoin d'être libérés. Il est également recommandé d'initialiser tous les pointeurs et les variables à NULL dans vos applications. Certaines compilations en release échoueront si vous ne le faites pas.

SystemClass::SystemClass()
{
m_Input = 0;
m_Graphics = 0;
}

Je crée ici un destructeur et un constructeur de copie vides. Je n'ai pas besoin d'eux dans cette classe, mais s'ils ne sont pas définis, certains compilateurs vont les générer pour vous, et je préfère ici qu'ils soient vides.

Vous remarquerez aussi que je ne libère aucun objet dans le destructeur de la classe. Je fais tout cela dans la méthode Shutdown que vous verrez plus tard. La raison est que je n'ai pas la certitude qu'elle est appelée. Certaines fonctions de Windows comme ExitThread() sont connues pour ne pas appeler vos destructeurs de classe, produisant des fuites de mémoire. Vous pouvez dorénavant bien sûr appeler des versions plus sûres de ces fonctions, mais je préfère être très prudent lorsque je programme sous Windows.

SystemClass::SystemClass(const SystemClass& other)
{
}

SystemClass::~SystemClass()
{
}

La fonction Initialize qui suit effectue l'installation de l'application. Elle appelle d'abord InitializeWindows qui crée la fenêtre, puis crée et initialise les objets Input et Graphics que l'application utilisera pour la gestion des entrées de l'utilisateur et du rendu.

bool SystemClass::Initialize()
{
int screenWidth, screenHeight;
bool result;

// Initialise la largeur et hauteur de l'écran à zéro avant d'envoyer les variables à la fonction.
screenWidth = 0;
screenHeight = 0;

// Initialise la fenêtre de l'interface
InitializeWindows(screenWidth, screenHeight);

// Crée l'objet Input. Cet objet sera utilisé pour gérer la saisie au clavier par l'utilisateur.
m_Input = new InputClass;
if(!m_Input)
{
return false;
}

// Initialise l'objet Input.
m_Input->Initialize();

// Crée l'objet Graphics. Cet objet gérera tous les graphiques de l'application.
m_Graphics = new GraphicsClass;
if(!m_Graphics)
{
return false;
}

// Initialise l'objet Graphics.
result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd);
if(!result)
{
return false;
}

return true;
}

La fonction Shutdown fait le nettoyage. Elle arrête et libère tout ce qui est associé aux objets Graphics et Input. Elle ferme également la fenêtre et libère les identifiants qui lui sont associés.

void SystemClass::Shutdown()
{
// Libère l'objet Graphics.
if(m_Graphics)
{
m_Graphics->Shutdown();
delete m_Graphics;
m_Graphics = 0;
}

// Libère l'objet Input.
if(m_Input)
{
delete m_Input;
m_Input = 0;
}

// Ferme la fenêtre.
ShutdownWindows();

return;
}

La méthode Run est celle qui met en boucle notre application et effectue tous les traitements jusqu'à ce que nous décidions de quitter l'application. Les traitements sont effectués dans la fonction Frame appelée à chaque itération. Il s'agit d'un concept important à comprendre car le reste de notre application doit être écrit dans cet esprit. Le pseudo-code ressemble à ce qui suit :

tant que la variable 'done' est à false
vérifier les messages système de la fenêtre
traiter les messages système
traiter la boucle de l'application
vérifier si l'utilisateur a voulu fermer l'application durant le traitement

void SystemClass::Run()
{
MSG msg;
bool done, result;

// Initialise la structure de message.
ZeroMemory(&msg, sizeof(MSG));

// Boucler jusqu'à obtenir un message de sortie de la fenêtre ou de l'utilisateur.
done = false;
while(!done)
{
// Gérer les messages de la fenêtre.
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

// Si la fenêtre demande à fermer l'application, alors on sort.
if(msg.message == WM_QUIT)
{
done = true;
}
else
{
// Autrement, effectuer le traitement de la trame.
result = Frame();
if(!result)
{
done = true;
}
}

}

return;

}

La fonction Frame qui suit est celle qui effectue les traitements de notre application. Jusque-là c'est assez simple, nous interrogeons l'objet Input pour contrôler si l'utilisateur a appuyé sur la touche Échap, voulant fermer l'application. S'il ne veut pas, alors nous utilisons l'objet Graphics pour effectuer les traitements de rendu de cette trame. Au fur et à mesure que notre application évoluera, nous y rajouterons du code.

bool SystemClass::Frame()
{
bool result;

// Vérifie si l'utilisateur a appuyé sur Échap pour fermer l'application.
if(m_Input->IsKeyDown(VK_ESCAPE))
{
return false;
}

// Effectue le traitement de la trame pour les objets Graphics.
result = m_Graphics->Frame();
if(!result)
{
return false;
}

return true;
}

C'est dans la méthode MessageHandler où nous traitons les messages système de la fenêtre. De cette façon, nous pouvons écouter certains évènements qui nous intéressent. Actuellement nous allons juste vérifier si une touche est enfoncée ou relâchée et transmettre cette information à l'objet Input. Tous les autres évènements seront passés au gestionnaire de messages par défaut de Windows.

LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch(umsg)
{
// Vérifie si une touche a été pressée.
case WM_KEYDOWN:
{
// Si oui, on l'envoie à l'objet Input afin qu'il sauvegarde cet état.
m_Input->KeyDown((unsigned int)wparam);
return 0;
}

// Vérifie si une touche a été relâchée.
case WM_KEYUP:
{
// Si oui, on l'envoie à l'objet Input afin qu'il supprime l'état de cette touche.
m_Input->KeyUp((unsigned int)wparam);
return 0;
}

// Les autres messages sont envoyés au gestionnaire de message par défaut. Nous n'en avons pas besoin dans notre application.
default:
{
return DefWindowProc(hwnd, umsg, wparam, lparam);
}
}
}

Dans la méthode InitializeWindows, nous mettons le code pour créer la fenêtre utilisée pour l'affichage. Elle retourne les valeurs screenWidth et screenHeight à la fonction qui l'appelle afin que nous puissions les utiliser dans l'application. À l'aide de paramètres par défaut, nous créons une fenêtre noire sans bordures et l'initialisons. La fonction créera une fenêtreen plein écran ou non selon la variable globale FULL_SCREEN. Si elle est fixée à true alors l'écran couvre toute la fenêtre du bureau de l'utilisateur ; si elle est définie à false nous avons juste une fenêtre 800x600 au milieu de l'écran. J'ai placé la variable globale FULL_SCREEN au début du fichier graphicsclass.h au cas où vous souhaiteriez la modifier. Ce pourquoi je l'ai placée dans ce fichier plutôt que dans l'en-tête de la classe prendra tout son sens plus tard.

void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight)
{
WNDCLASSEX wc;
DEVMODE dmScreenSettings;
int posX, posY;

// Obtention d'un pointeur externe de cet objet.
ApplicationHandle = this;

// Obtention de l'instance de cette application.
m_hinstance = GetModuleHandle(NULL);

// Nom de l'application.
m_applicationName = L"Engine";

// Configure la classe de la fenêtre avec les réglages par défaut.
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = m_hinstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hIconSm = wc.hIcon;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = m_applicationName;
wc.cbSize = sizeof(WNDCLASSEX);

// Enregistre la classe de fenêtre.
RegisterClassEx(&wc);

// Détermine la résolution de l'écran.
screenWidth  = GetSystemMetrics(SM_CXSCREEN);
screenHeight = GetSystemMetrics(SM_CYSCREEN);

// Configuration, des paramètres de l'écran suivant s'il est exécuté en mode plein écran ou en mode fenêtré.
if(FULL_SCREEN)
{
// En plein écran, on règle l'écran à la taille maximale du bureau en 32 bits.
memset(&dmScreenSettings, 0, sizeof(dmScreenSettings));
dmScreenSettings.dmSize       = sizeof(dmScreenSettings);
dmScreenSettings.dmPelsWidth  = (unsigned long)screenWidth;
dmScreenSettings.dmPelsHeight = (unsigned long)screenHeight;
dmScreenSettings.dmBitsPerPel = 32;
dmScreenSettings.dmFields     = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;

// Modification des paramètres d'affichage en plein écran.
ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);

// Positionnement de la fenêtre dans le coin supérieur gauche..
posX = posY = 0;
}
else
{
// En fenêtré, fixer une résolution de 800x600.
screenWidth  = 800;
screenHeight = 600;

// Centre la fenêtre sur l'écran.
posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth)  / 2;
posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2;
}

// Crée la fenêtre selon les paramètres précédents et récupère son identifiant.
m_hwnd = CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName,
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP,
posX, posY, screenWidth, screenHeight, NULL, NULL, m_hinstance, NULL);

// Affiche la fenêtre à l'écran et lui donne la main.
ShowWindow(m_hwnd, SW_SHOW);
SetForegroundWindow(m_hwnd);
SetFocus(m_hwnd);

// Cache le pointeur de la souris.
ShowCursor(false);

return;
}

ShutdownWindows fait juste ce quelle signifie. Elle remet les paramètres de l'écran à la normale et libère la fenêtre et les identifiants qui lui sont associés.

void SystemClass::ShutdownWindows()
{
// Montre le curseur de la souris.
ShowCursor(true);

// Change les paramètres d'affichage si l'on quitte le mode plein écran.
if(FULL_SCREEN)
{
ChangeDisplaySettings(NULL, 0);
}

// Détruit la fenêtre.
DestroyWindow(m_hwnd);
m_hwnd = NULL;

// Détruit l'instance de l'application.
UnregisterClass(m_applicationName, m_hinstance);
m_hinstance = NULL;

// Efface toute trace de la classe.
ApplicationHandle = NULL;

return;
}

La méthode WndProc est celle dans laquelle la fenêtre envoie ses messages. Vous remarquerez que nous donnons le nom de la fenêtre lorsque l'on initialise la classe de la fenêtre avec wc.lpfnWndProc = WndProc dans la méthode InitializeWindows ci-dessus. Je l'ai incluse dans ce fichier de classe puisque nous l'utilisons directement dans la classe System en lui faisant envoyer tous les messages à la méthode MessageHandler définie dans SystemClass. Cela nous permet d'attacher les fonctionnalités de messagerie directement dans notre classe et conserver un code propre.

LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
switch(umessage)
{
// Vérifie si la fenêtre va être détruite.
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}

// Vérifie si la fenêtre va être fermée.
case WM_CLOSE:
{
PostQuitMessage(0);
return 0;
}

// Tous les autres messages sont passés au gestionnaire de messages de la classe système.
default:
{
return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
}
}
}

VII - Inputclass.h

Pour garder les tutoriels simples, j'ai réservé les entrées fenêtre jusqu'au moment où je ferai un tutoriel sur DirectInput (qui est de loin supérieur). La classe Input gère la saisie au clavier de l'utilisateur. Cette classe est donnée en entrée de la fonction SystemClass::MessageHandler. L'objet Input va stocker l'état de chaque touche dans un tableau de touches. Lorsqu'interrogé, il renseignera les fonctions appelantes si une certaine touche est enfoncée. Voici l'en-tête :

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : inputclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_

////////////////////////////////////////////////////////////////////////////////
// Nom de la classe : InputClass
////////////////////////////////////////////////////////////////////////////////
class InputClass
{
public:
InputClass();
InputClass(const InputClass&);
~InputClass();

void Initialize();

void KeyDown(unsigned int);
void KeyUp(unsigned int);

bool IsKeyDown(unsigned int);

private:
bool m_keys[256];
};

#endif

VIII - Inputclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : inputclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "inputclass.h"

InputClass::InputClass()
{
}

InputClass::InputClass(const InputClass& other)
{
}

InputClass::~InputClass()
{
}

void InputClass::Initialize()
{
int i;

// Initialise toutes les touches comme relâchées, et non pressées.
for(i=0; i<256; i++)
{
m_keys[i] = false;
}

return;
}

void InputClass::KeyDown(unsigned int input)
{
// Si une touche est pressée, on sauvegarde son état dans le tableau.
m_keys[input] = true;
return;
}

void InputClass::KeyUp(unsigned int input)
{
// Si une touche est relâchée, on supprime cet état du tableau.
m_keys[input] = false;
return;
}

bool InputClass::IsKeyDown(unsigned int key)
{
// Retourne l'état de la touche (pressée/relâchée).
return m_keys[key];
}

IX - Graphicsclass.h

La classe System crée également un objet Graphics. Toutes les fonctionnalités graphiques de cette application seront encapsulées dans cette classe. J'utiliserai aussi l'en-tête de ce fichier pour tous les paramètres graphiques globaux qui nous souhaiterons changer comme le mode plein écran ou fenêtré. Actuellement, cette classe est vide, mais dans les tutoriels à venir, elle contiendra tous les objets graphiques.

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_

//////////////
// INCLUDES //
//////////////
#include <windows.h>

/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;

Pour commencer, nous aurons besoin de ces quatre variables globales.

////////////////////////////////////////////////////////////////////////////////
// Nom de la classe : GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
GraphicsClass();
GraphicsClass(const GraphicsClass&);
~GraphicsClass();

bool Initialize(int, int, HWND);
void Shutdown();
bool Frame();

private:
bool Render();

private:

};

#endif

X - Graphicsclass.cpp

J'ai gardé cette classe entièrement vide pour l'instant, étant donné que nous sommes en train de construire le squelette dans ce tutoriel.

////////////////////////////////////////////////////////////////////////////////
// Nom du fichier : graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"

GraphicsClass::GraphicsClass()
{
}

GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}

GraphicsClass::~GraphicsClass()
{
}

bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{

return true;
}

void GraphicsClass::Shutdown()
{

return;
}

bool GraphicsClass::Frame()
{

return true;
}

bool GraphicsClass::Render()
{

return true;
}

XI - Résumé

Nous avons donc maintenant une base de travail et une fenêtre qui apparaît à l'écran. Ce cadre va servir de base pour tous les tutoriels à venir. Sa compréhension est donc assez importante. S'il vous plaît essayez de faire l'exercice afin de vous assurer que le code compile et fonctionne avant de passer au prochain tutoriel. Si vous ne comprenez pas cette base de travail, avancez sur les autres tutoriels, ils pourront vous aider à mieux comprendre ce squelette lors de son utilisation.

XII - Exercices à faire

Changez le paramètre FULL_SCREEN à true dans l'en-tête graphicsclass.h puis recompilez et exécutez le programme. Appuyez sur la touche Échap pour quitter l'application une fois la fenêtre affichée.

XIII - Code source

Projet Visual Studio 2010 : dx11tut02.zip

Source seule : dx11src02.zip

Executable seul : dx11exe02.zip

XIV - Remerciements

Cet article est une traduction autorisée de l'article paru sur RasterTek.

Navigation

Tutoriel précédent : Mise en place de DirectX 11 avec Visual Studio

Sommaire

Tutoriel suivant : Initialisation de DirectX 11