Chose promise, chose due. Voici un petit tuto pour tous ceux qui souhaitent utiliser les fichiers au format X. Je les trouve très pratiques et je les utilise depuis pas mal de temps. Jusqu’à une période récente, j’utilisais la version dite "legacy", qui est dite "deprecated" par Microsoft. J’ai donc entrepris de mettre à niveau mes applis. Comme je suis passé entre temps du Pascal au C++, ça n’a pas été sans mal.
Je n’ai trouvé aucun tuto pour m’aider. Alors pourquoi ne pas en écrire un moi-même ?

Tout d’abord, qu’est-ce qu’un fichier au standard .X ?
Le standard X permet de stocker et d’accéder à des données organisées suivant une structure arborescente.

Préliminaire : les templates de données
Pour utiliser un fichier .X, on commence par définir des templates de données (à noter qu’il existe des templates "prêts-à-porter" correspondant aux principales structures utilisées par DirectX).
Exemple :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 
#define TEMPL1 "xof 0303txt 0032 \
    template d_elem { \
    <BCD66A88-5B1A-47E1-B3EE-07A56BAFC017> \
        DWORD kind; \
        FLOAT we; \
        FLOAT he; \
        UCHAR red; \
        UCHAR green; \
        UCHAR blue; \
        UCHAR alpha; \
    } \ 
    template listelem { \
    <1DA1F62D-8FE7-4515-BD6C-3501F6622723> \
        DWORD nelem; \
        array d_elem elem[nelem]; \
    } \
    template d_conf { \
    <555CE7A5-C452-46BB-9638-B0B544E66044> \
        BYTE blig; \
        BYTE bact; \
        BYTE bvis; \
        BYTE bfold; 
        FLOAT dmin; \
        FLOAT dmax; \
        [...] \
    }"
Dans l’exemple ci-dessus, le premier template (d_elem) correspond à une structure C++ basique :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
 
struct myelem {
        DWORD kind;
        double we;
        double he;
        unsigned char red;
        unsigned char green;
        unsigned char blue;
        unsigned char alpha;
}
Le second correspond à une liste desdites structures.
Le troisième est un format ouvert : il indique que l’on pourra ajouter à la donnée enregistrée une donnée "enfant". Cette donnée "enfant" pourra, à son tour, comporter une liste ou une autre donnée enfant…
L’entête « xof 0303txt 0032 » définit le format du fichier (il existe une version binaire et une version texte).
Les GUID identifiant les templates sont obtenus en utilisant l’utilitaire GuidGen de Visual Studio.
Il faut également créer des objets "GUID" qui permettront de manipuler ces templates plus facilement par la suite :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
 
DEFINE_GUID(GUID_Elem, 0xBCD66A88, 0x5B1A, 0x47E1, 0xB3, 0xEE, \
            0x07, 0xA5, 0x6B, 0xAF, 0xC0, 0x17);
DEFINE_GUID(GUID_Listelem, 0x1DA1F62D, 0x8FE7, 0x4515, 0xBD, 0x6C, \
            0x35, 0x01, 0xF6, 0x62, 0x27, 0x23);
DEFINE_GUID(GUID_Conf, 0x555CE7A5, 0xC452, 0x46BB, 0x96, 0x38, \
            0xB0, 0xB5, 0x44, 0xE6, 0x60, 0x44);
Enregistrement des données
Les étapes de l’enregistrement :
- On crée une interface ID3DXFile.
- On enregistre les templates dans l’objet correspondant à cet interface.
- On crée à partir de cet objet D3DXFile un objet D3DXFileSaveObject (interface ID3DXFileSaveObject).
Il est important de comprendre que c’est cet objet D3DXFileSaveObject qui va recueillir l’ensemble des données que l’on enregistrera ensuite dans un fichier sur le disque dur.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
#include "D3DX9Xof.h"
 
ID3DXFile *pDXFile;
ID3DXFileSaveObject *pSaveObject;
 
// création de l’objet D3DXFile
if (FAILED(D3DXFileCreate(&pDXFile))) return FALSE;
 
// enregistrement des templates
if (FAILED(pDXFile->RegisterTemplates((LPVOID)TEMPL1, sizeof(TEMPL1)-1)) return FALSE;
 
// création de l’objet ID3DXFileSaveObject qui recevra les données à enregistrer  
if (FAILED(pDXFile->CreateSaveObject((LPVOID)Filename, 
           D3DXF_FILESAVE_TOFILE, 
           D3DXF_FILEFORMAT_TEXT, 
           &pSaveObject ))) return FALSE;
La signification de D3DXF_FILESAVE_TOFILE et D3DXF_FILEFORMAT_TEXT est donnée dans la documentation de DirectX.

On dispose paintenant d’un objet qui va recevoir toutes les données à enregistrer. On le conserve tout le temps nécessaire pour lui transférer toutes ces données. Lorsqu’on aura fini, on procèdera au transfert sur le disque :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
 
HRESULT hr = pSaveObject->Save();
hr = pSaveObject->Release();
hr = pDXFile->Release();
Les deux "Release()" sont indispensables pour libérer les objets COM auxquels on accède au travers des interfaces mentionnés plus haut. En particulier, si l’on ne relâche pas pSaveObject, le transfert sur le disque ne sera pas effectué malgré le Save() qui précède.
Une fois le Save() réalisé, il n’est plus possible d’ajouter d’autres données au fichier à moins de recommencer un cycle complet.

Comment ajoute-t’on les données ?
On encapsule les données à enregistrer dans des objets D3DXFileSaveData. On peut créer de tels objets à partir de notre objet D3DXFileSaveObject "racine" mais on peut également créer un objet D3DXFileSaveData comme enfant d’un autre objet D3DXFileSaveData. Ceci permet de créer une arborescence.
Voici le mécanisme général : prenons l’exemple d’une liste de structures myelem (qu’on appelle listelem).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
ID3DXFileSaveData *pSaveData;
 
DWORD nelem = listelem->Nbitem();
DWORD nsize = nelem * sizeof(myelem) + 4;  // dimension en octets
byte *pObj = new byte[nsize];              // on réserve un buffer
DWORD *DWObj = (DWORD*)pObj;               // premier cast -> DWORD*
*DWObj = nelem;                            // le nombre d’éléments
DWObj +=1;
 
myelem *pElem = (myelem*)DWObj;            // deuxième cast -> myelem*
myelem *un_elem;
for (DWORD i = 0; i < nelem; i++) {
    // on recopie tout simplement les structures
    un_elem = (myelem*)listelem->Item(i);
    *pElem = *unelem;
    pElem++;
}
 
bool bOK = (SUCCEEDED(pSaveObject->AddDataObject(
            GUID_Listelem, "listelem", NULL, nsize, pObj, &pSaveData)));
 
return bOK;
Récapitulons :
- on crée un buffer de longueur suffisante pour accueillir toute la liste (y compris le nombre d’éléments),
- on y recopie bout à bout les structures qui constituent la liste,
- on crée un objet D3DXFileSaveData dérivant directement de pSaveObject en précisant le GUID associé à son template, la taille du buffer à transférer et l’adresse de ce buffer. La méthode AddDataObject renvoie un pointeur sur l’objet ainsi créé. Si cet objet n’a pas d’enfant, on ne fera rien de ce pointeur. Si celui-ci a un enfant, on l’utilisera pour dériver un autre objet D3DXFileSaveData.
Reprenons l’exemple du template d_conf (cf ci-dessus). La structure de ce template permet d’ajouter un objet "enfant". Pour ce faire, on procède de la manière suivante :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
 
ID3DXFileSaveData *pSaveData;
 
DWORD nsize = ... 
byte *pObj = new byte[nsize];
 
bool bOK = (SUCCEEDED(pSaveObject->AddDataObject(
            GUID_Conf, "conf", NULL, nsize, pObj, &pSaveData)));
 
if (bOK) StoreChild_conf(pSaveData);
 
return bOK;
Dans l’exemple qui précède, on retrouve dans StoreChild_conf (méthode perso) la même mécanique de création et d’écriture dans un buffer mais, cette fois, le buffer est attribué à un objet D3DXFileSaveData obtenu à partir du pSaveData passé en argument.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
 
ID3DXFileSaveData *pChildData;
 
DWORD nsize = ... 
byte *pObj = new byte[nsize];
 
bool bOK = (SUCCEEDED(pSaveData->AddDataObject(
            GUID_Conf, "conf", NULL, nsize, pObj, &pSaveData)));
 
return bOK;
Récupération des données
Ce n’est pas sorcier : on détricote…
Comme pour la sauvegarde, on passe par une interface D3DXFile. Cette fois, on ne crée pas un objet D3DXFileSaveObject mais un objet D3DXFileEnumObject. On n’a pas non plus besoin d’enregistrer les templates : on peut les récupère à partir de cet objet D3DXFileEnumObject.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
#include "D3DX9Xof.h"
 
ID3DXFile *pDXFile;
ID3DXFileEnumObject *pEnumObject;
 
// création de l’objet x-file
if (FAILED(D3DXFileCreate(&pDXFile))) return FALSE;
 
// création de l’objet ID3DXFileEnumObject qui donnera accès aux données
if (FAILED(pDXFile->CreateEnumObject((LPCVOID)filename,
            D3DXF_FILELOAD_FROMFILE,
            &pEnumObj))) return = FALSE;
 
// récupération des templates à partir du D3DXFileEnumObject
if (FAILED(pDXFile->RegisterEnumTemplates(pEnumObj))) return FALSE;
La suite se comprend facilement si on a pigé le mécanisme de sauvegarde. On commence par récupérer le nombre d’objets "de premier niveau" de l’arborescence.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
 
SIZE_T num;
 
if (FAILED(pEnumObj->GetChildren(&num))) return FALSE;
Ceci permet de rentrer dans une boucle de récupération de ces objets. On utilise pour cela des objets de type D3DXFileData (le pendant des D3DXFileSaveData).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
ID3DXFileData *pDataObj;    // pointeur d’objet D3DXFileData
GUID myguid;                // ...pour récupérer le GUID de ces objets
 
for {DWORD i = 0 ; i < num ; i++) {
 
    // récupération du ième objet de rang 1
    bool bok = SUCCEEDED(pEnumObj->GetChild(i, &pDataObj));
 
    // quel est le GUID du template utilisé lors de l’enregistrement ?
    if (bok) bok = SUCCEEDED(pDataObj->GetType(&myguid));
 
    // ce GUID est-il celui d’un objet listelem ?
    if (bok) bok = IsEqualGUID(myguid, GUID_Listelem);
    if (bok) bok = ParseListElem(pDataObj);
    if (!bok) return FALSE;
}
La fonction ParseListElem (méthode perso) permet d’accéder aux données encapsulées dans l’objet pDataObj. Pour récupérer et recopier ces données, il faut d’abord verrouiller cet objet en mémoire. On récupère alors un pointeur sur les données et la taille du bloc correspondant. On termine en déverrouillant l’objet.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
// verrouillage de l’objet et récupération d’un pointeur sur les données
SIZE_T nsize = 0;            // taille en octets du bloc de données (out)
DWORD *pnelem;               // pointeur (out)
 
bool bok = (SUCCEEDED(pDataObj->Lock(&nsize, (LPCVOID*)&pnelem)));
if (!bok) return FALSE;
 
// nombre d'éléments et vérification de cohérence
// deux précautions valent mieux qu’une !
 
DWORD nelem = *pnelem;
if (nsize != nelem * sizeof(myelem) + 4) return FALSE;
 
// récupération des données
 
pnelem +=1;                              // on saute le 1er DWORD
 
chElem *pElem = (chElem*)pnelem;         // cast DWORD* -> myelem*
for (DWORD i = 0; i < nelem; ++i) {      // création des éléments
    listelem->Copyelem(pElem);           // par copie du buffer
    pElem++;                             // Copyelem est une méthode perso
}
 
// déverrouillage de l’objet
pDataObj->Unlock();
 
return TRUE;
On le voit, tout cela n’est pas sorcier. Les anglo-saxons disent : this is not rocket science.
Si l’objet de rang "1" a des "enfants", on l’analyse de la même façon qu’on l’a fait pour l’énumérateur. Simplement, cette fois on utilise les méthodes de ID3DXFileData et non plus celles de ID3DXFileEnumObject.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
// nombre d’objets "enfant"
DWORD num;
pDataObj->GetChildren(&num);
 
DWORD i = 0;
bool bok = true;
while ((i < num) && bok} {
    GUID myguid;
    ID3DXFileData *pchild;
    // on récupère le ième objet enfant
    bok = (SUCCEEDED(pDataObj->GetChild(i, &pchild)));
 
    // on teste son GUID
    if (bok) bok = SUCCEEDED(pchild->GetType(&myguid));
    if (bok) bok = IsEqualGUID(myguid, GUID_Truc);   // un de mes GUID...
    if (bok) bok = ParseTruc(pchild);                // méthode perso
 
    i++;
}
 
return bok;
On aura compris que l’intérêt du GetType est de permettre de faire un switch pour choisir le "parser" adapté.

Voili voilà. On pensera juste à faire un Release() sur les objets .COM que l’on a créés, on ne sait jamais…

Ce mini tuto est terminé, j’espère qu’il vous aidera. Je ne suis pas un expert du fichier X (au sens propre comme au figuré) mais comme j’ai un peu galéré à mettre au point mon programme, autant en faire profiter les autres !