Utilisation de Boost.serialize dans un contexte MFC

Patrick OTTAVI MVP Visual C++

À travers cet article, je vous propose de découvrir Boost.serialize dans un contexte d'utilisation lié aux MFC.

- Titre : Utilisation de Boost.serialize dans un contexte MFC

- Auteur : Patrick OTTAVI MVP Visual C++

- Parution : 1 octobre 2008

- Mise à jour : 13 juillet 2020

- 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 ® 2008 Farscape. 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. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Introduction

- II. Installer les bibliothèques de Boost

- III. Mise en place de la sérialisation

- IV. La sérialisation binaire MFC

- V. Sérialiser les classes MFC avec Boost

- V-A. Sérialisation d'une CArray

- V-B. Sérialisation d'une CList

- V-C. Sérialisation d'une CMap

- V-D. Sérialisation des classes CString,CStringA,CStringW

- VI. Lancer la sérialisation

- VII. Les Exemples

- VIII. Conclusion

I - Introduction

Les MFC proposent depuis… 1994 un système de sérialisation des données dans un format binaire, le système est simple d'utilisation et prend en charge tous les types de données et classes associées aux MFC, malgré cela il devient très intéressant voir indispensable de disposer d'un tel mécanisme pour le format de données XML , celui-ci étant devenu incontournable depuis quelques années il devient légitime de pouvoir bénéficier de cette fonctionnalité dans nos programmes MFC.
La solution viendra de la bibliothèque Boost ::serialize qui propose une bibliothèque pour sérialiser des données dans plusieurs formats : texte, binaire, et XML.
Le hic c'est que cette implémentation ne concerne que les types de base et les conteneurs de la STL ou de types définis dans boost.
Et ne peut donc fonctionner directement avec les conteneurs spécifiques des MFC : CArray,CMap,CList ou encore la classe CString et ses déclinaisons.
Le présent article se propose donc de vous montrer comment intégrer une sérialisation XML dans votre projet MFC en utilisant dans un premier temps les conteneurs de la STL.
La deuxième partie abordera une solution pour utiliser la sérialisation XML avec les conteneurs des MFC et surtout la classe CString.

II - Installer les bibliothèques de Boost

L'installation et la compilation de Boost sont un peu délicates… heureusement il existe une solution simple et gratuite fournie par la société Boostpro computing qui propose un installateur pour les compilateurs récents. Il sera ainsi très facile d'installer les bibliothèques Boost pour Visual 2008 : http://www.boostpro.com/boost_1_36_0_setup.exe.

Paramétrer les chemins de recherche dans Visual 2008 :
Rajoutez dans le chemin de recherche pour les inclure, les répertoires suivants :
VotreRepInstall\boost\boost_1_36_0 ;
VotreRepInstall \boost\boost_1_36_0\boost.

Pour les bibliothèques :
VotreRepInstall \boost\boost_1_36_0\lib.

Utiliser Boost.serialization :
Vous pouvez vous reporter à cet article de présentation :https://khayyam.developpez.com/articles/cpp/boost/serialization/.
Et consulter la documentation officielle :http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html.

III - Mise en place de la sérialisation

Un utilisateur de la sérialisation MFC ne sera pas trop dépaysé par le système Boost et y verra un air de déjà vu, ce n’est peut-être pas un hasard, mais l'auteur de cette bibliothèque fait référence au système des MFC dans la documentation pour l'avoir utilisée pendant de nombreuses années.
http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html.
L'exemple
Je vais tout de suite aborder l'aspect pratique de l'utilisation de Boost.Serialize à travers un exemple concret.
J'ai écrit une petite classe qui se propose de maintenir dans des listes l'ensemble des libellés des contrôles d'une fenêtre.
Il sera possible de mémoriser un ensemble de fenêtres.
La classe permettra de modifier un libellé spécifique d'un contrôle et de mettre à jour la fenêtre parente du contrôle avec les libellés mémorisés.
Finalement ces informations seront enregistrées dans un fichier XML encodé au format UTF8 et modifiables directement par notepad.
Il sera donc possible au lancement d'une fenêtre de changer dynamiquement les différents libellés établis par défaut dans les ressources…
La classe d'origine à sérialiser :

class CWindowTextManager
{
public:
CWindowTextManager(){}
~CWindowTextManager(){}

// sauvegarde les libellés par fenêtres
bool Load(const TCHAR *szXMLFile);

// lecture des libellés par numéro de fenêtres.
bool Save(const TCHAR *szXMLFile);

// mémorise pour la fenêtre désignée par pWndToExplore l'ensemble des libellés
// nIdWindow correspond a l'identifiant de la fenêtre dans les ressources par exemple MyformView::IDD
int  ExploreCtrl(CWnd *pWndToExplore,UINT nIdWindow);

// fixe le libellé d'un contrôle désigné par son identifiant pour l'objet fenêtre pWnd.
// la table interne est aussi mise à jour.
bool SetWindowText(CWnd *pWnd,UINT nIdControl,TCHAR *szText);

// fixe le libellé d'un contrôle désigné par son identifiant pour l'identifiant de fenêtre nIdWindow.
// la table interne est mise à jour, si une fenêtre est liée a l'identifiant le contrôle est mis à jour graphiquement/
bool SetWindowText(UINT nIdWindow,UINT nIdControl,TCHAR *szText);

// rafraichi les libellés d'un objet fenêtre pWnd d'après la table interne.
bool RefreshWindow(CWnd *pWnd);
// rafraichi les libellés d'une fenêtre désignée par nIdWindow d'après la table interne.
bool RefreshWindow(UINT nIdWindow);
// fixe le lien entre un objet fenêtre et son identifiant dans les ressources.
void SetWndId(CWnd *pWnd,UINT nIdWindow);
// retrouve l'objet fenêtre associé à l'identifiant nIdWindow
CWnd *FindWindow(UINT nIdWindow);
// retrouve l'identifiant fenêtre associé à l'objet fenêtre pWnd
UINT  FindWindow(CWnd *pWnd);

static CStringA UTF8EncodeString(const CStringW input);
static CStringW UTF8DecodeString(const CStringA input);

private:
CWindowTextManager (const CWindowTextManager  &r);
CWindowTextManager   & operator = (const CWindowTextManager  & arg);

protected:
bool FindItem(UINT nIdWindow,UINT nIdControl);
class ItemCtrl
{
public:

ItemCtrl():nIdC(0){}
BOOL operator == (const int &  nid) const
{
return nid==nIdC;
}

CStringW strText;    // Texte Contrôle
UINT    nIdC;        // identifiant
};

bool    m_bSave;
std::map<CWnd*, UINT > m_mapWnd;
std::map<UINT, std::list<ItemCtrl> > m_MapElt;
std::list<ItemCtrl> m_ListItem;
std::list<ItemCtrl>::iterator m_ItListItem;
};

L'ensemble des contrôles d'une fenêtre sont stockés dans une Liste de la classe ItemCtrl.
Celle-ci contient le libellé du contrôle et son identifiant.
Cette liste est contenue dans une map (m_MapElt) dont la clef est l'identifiant de la fenêtre.
C'est ce conteneur map qu'il va falloir sérialiser.

La sérialisation boost
On dispose de plusieurs approches : Globalement la sérialisation peut se faire avec une méthode Serialize à définir, celle-ci peut être intrusive ou non, c'est-à-dire présente au sein de la classe à sérialiser ou en dehors. On peut voir dans la documentation boost un exemple très simple d'implémentation : http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html.
Cette méthode convient bien lorsque les types à sérialiser sont des types simples, int, doubles, etc. ou des éléments de la stl reconnus comme des primitives, exemple le conteneur vector ,list ou encore la classe string.
Elle permet aussi d'écrire en une seule ligne les deux traitements de lecture et d'écriture grâce à l'opérateur 'et'.
En fait on dispose de deux sortes d'opérateurs :
l'opérateur 'et' que je viens d'évoquer, et les opérateurs de flux qui seront utilisés lorsque l'on utilisera la deuxième approche de la sérialisation à savoir une méthode séparée pour la lecture et la sauvegarde dans l'archive.
Ces méthodes seront donc utiles chaque fois que le traitement de sérialisation ne peut être identique sur la lecture et l'écriture des données.

Analysons la sauvegarde des éléments.

Je dois sérialiser mon conteneur map, comme je peux exprimer la sauvegarde et la lecture de manière identique je vais mettre en place une méthode serialize dans la classe CWindowTextManager

friend class boost::serialization::access;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & boost::serialization::make_nvp("WndList",m_MapElt);
}

la function make_nvp fait le travail de sérialisation et utilisera la balise WndList pour décrire les données.
Comme dans le cas de la sérialisation MFC boost ne peut sérialiser directement notre liste qui contient des éléments de la classe ItemCtrl celle-ci ne faisant pas partie des primitives acceptées.
Cette classe devra donc aussi fournir une méthode de sérialisation, cette fois-ci je vais utiliser la deuxième technique d'implémentation avec une méthode save et load :

void save(Archive & ar, const unsigned int version) const
{
std::string str;
str=CWindowTextManager::UTF8EncodeString(strText);
ar << BOOST_SERIALIZATION_NVP(nIdC) << BOOST_SERIALIZATION_NVP(str);
}

Dans ce code, j'ai transformé le libellé du contrôle qui était stocké dans une CString Unicode en une chaine codée en UTF8.
Ensuite j'ai laissé faire boost pour la sauvegarde par défaut des éléments, le nom des balises employées étant ici les noms des variables utilisées.
La lecture suit la même logique :

void load(Archive & ar, const unsigned int version)
{
std::string str;
ar >> BOOST_SERIALIZATION_NVP(nIdC) >> BOOST_SERIALIZATION_NVP(str);
strText=CWindowTextManager::UTF8DecodeString(str.c_str());
}

Vous remarquerez que j'utilise une std::string pour faire l'interface pour la sérialisation de ma CString.
Enfin dans le cas d'utilisation des méthodes séparées de traitement de lecture et d'écriture il faut rajouter à votre code :

friend class boost::serialization::access;

ainsi que la macro :

BOOST_SERIALIZATION_SPLIT_MEMBER()

voilà la sérialisation de ma map est terminée, on obtiendra ce genre d'écriture :

IV - La sérialisation binaire MFC

Pour établir un point de comparaison avec la sérialisation MFC, j'ai écrit le même traitement en utilisant la sérialisation binaire MFC :

class CWindowTextManager
{
public:
CWindowTextManager(){}
~CWindowTextManager(){}

//......

// la methode de serialisation.
void Serialize(CArchive& ar)
{
m_MapElt.Serialize(ar);
}

public:
bool FindItem(UINT nIdWindow,UINT nIdControl);
class ItemCtrl : public CObject
{
public:
ItemCtrl(const TCHAR *sz=_T(""),UINT nId=0)
{
strText=sz;
nIdC=nId;
}
ItemCtrl(const ItemCtrl &rItem)
{
*this = rItem;
}
const ItemCtrl& operator=(const ItemCtrl& Src)
{
CopyFrom(Src);
return *this;
}
void CopyFrom(const ItemCtrl & Src )
{
if(this==&Src) return;
strText=Src.strText;
nIdC=Src.nIdC;
}

BOOL operator == (const ItemCtrl &  item) const
{
return item.nIdC==nIdC;
}
void Serialize(CArchive& ar)
{
if(ar.IsStoring())
ar <<  strText << nIdC;
else ar >> strText >> nIdC;
}
CStringW strText;    // Texte Contrôle
UINT     nIdC;        // identifiant
};

bool    m_bSave;
CMapEx<CWnd *,CWnd *,UINT,UINT> m_mapWnd;
typedef CListEx<ItemCtrl,ItemCtrl> ListCtrl;
CMapEx<UINT,UINT, ListCtrl,ListCtrl &> m_MapElt;
ListCtrl m_ListItem;
ItemCtrl m_Item;
};

template<>
void AFXAPI SerializeElements<CWindowTextManager::ListCtrl > (CArchive& ar, CWindowTextManager::ListCtrl* pElements, int nCount);
template<>
void AFXAPI SerializeElements<CWindowTextManager::ItemCtrl> (CArchive& ar, CWindowTextManager::ItemCtrl* pElements, int nCount);

La sérialisation MFC utilise la méthode Serialize pour effectuer le traitement, on la retrouve donc méthode dans la classe CWindowTextManager et dans la classe ItemCtrl exactement comme dans la sérialisation avec Boost.
Avec les MFC on ne dispose que de la méthode Serialize, pour distinguer le mode de lecture et d'écriture on teste le mode d'ouverture de l'archive avec la méthode IsStoring().
Dans mon exemple j'utilise des classes héritées des conteneurs MFC pour disposer du constructeur de copie et de l'opérateur d'affectation.
Enfin la sérialisation MFC nécessite pour pouvoir fonctionner avec nos types de données la spécialisation de la fonction SerializeElements.

// code de la spécialisation dans le .cpp
template<>
void AFXAPI SerializeElements<CWindowTextManager::ListCtrl> (CArchive& ar, CWindowTextManager::ListCtrl* pElements, int nCount)
{
for ( int i = 0; i < nCount; i++, pElements++ ) pElements->Serialize( ar );
}
template<>
void AFXAPI SerializeElements<CWindowTextManager::ItemCtrl> (CArchive& ar, CWindowTextManager::ItemCtrl* pElements, int nCount)
{
for ( int i = 0; i < nCount; i++, pElements++ ) pElements->Serialize( ar );
}

Pour finir la comparaison, voici le code d'appel de la sauvegarde et de l'écriture des données :

//------------------------------------------------------------------
bool CWindowTextManager::Load(const TCHAR *szXMLFile)
{
m_bSave=false;
CStringA strFileName(szXMLFile);
if(_access(strFileName,0)==-1) return false;
CFile File;
if(File.Open(strFileName, CFile::modeRead ))
{
CArchive ar( &File, CArchive::load);
m_MapElt.Serialize(ar);
}
return true;
}
//------------------------------------------------------------------
bool CWindowTextManager::Save(const TCHAR *szXMLFile)
{
CStringA strFileName(szXMLFile);
m_bSave=true;

CFile File;
if(File.Open(strFileName, CFile::modeCreate | CFile::modeWrite ))
{
CArchive ar( &File, CArchive::store);
m_MapElt.Serialize(ar);
return true;
}
return false;
}

Le code est similaire pour les deux méthodes, la différenciation du mode de lecture et d'écriture se fait par le mode de création de l'archive.
Pour un complément d'information, je vous invite à consulter la FAQ Visual avec les posts suivants :

Comment sérialiser des données avec les MFC ?
Comment sérialiser des données avec les conteneurs templates MFC ?
Vous trouverez l'exemple complet de cette sérialisation binaire MFC dans l'exemple SerialMFC

V - Sérialiser les classes MFC avec Boost

Nous venons de voir qu'il est relativement aisé de mettre en place une sérialisation XML dans un contexte MFC en utilisant des éléments de la STL.
Qu'en est-il avec les classes MFC ?
La classe CString élément important dans un programme MFC ne peut être sérialisée directement. De même que les conteneurs CArray, CList et CMap.
Nous allons voir maintenant comment contourner cet écueil.

V-A - Sérialisation d'une CArray

Je commencerais par la sérialisation d'une langue=« Faq » qui est le pendant MFC du conteneur vector de la stl.
Lors de la sérialisation d'un vector d'entiers, on obtient le résultat suivant :

J'ai donc écrit une nouvelle classe héritée de la classe CArray pour simuler ce résultat :

template<class TYPE, class ARG_TYPE>
class BoostCArray: public CArray<TYPE, ARG_TYPE>
{
public:
BoostCArray():CArray<TYPE, ARG_TYPE>(){}
BoostCArray(const BoostCArray &x){ *this = x;}

BoostCArray &operator = (const BoostCArray& x)
{
if(this!=&x)
{
RemoveAll();
Copy(x);
}
return *this;
}

private:
friend class boost::serialization::access;
template<class Archive>
void save(Archive & ar, const unsigned int version) const
{
const int count=GetCount();
if(!count) return;
ar & boost::serialization::make_nvp("count",count);

for(int n=0;n<count;n++)
{
ar &  boost::serialization::make_nvp("item",GetAt(n));
}
}

template<class Archive>
void load(Archive & ar, const unsigned int version)
{
int count=0;
ar & boost::serialization::make_nvp("count",count);
SetSize(count);
for(int n=0;n<count;n++)
{
ar &  boost::serialization::make_nvp("item",GetAt(n));
}
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
};

On retrouve nos deux méthodes séparées save et load.
J'ai comblé au passage deux manques essentiels de la classe CArray : le constructeur de copie et l'opérateur d'affectation.
Pour sauvegarder un CArray on commence par écrire le nombre d'éléments contenus avec la balise « count ».
Ensuite on itère sur les éléments du CArray pour inscrire les éléments référencés par la balise « item ».
Ce qui correspond au traitement effectué sur son cousin le conteneur vector dans la stl.
Le code est très simple, notez toutefois l'utilisation de SetSize sur la lecture qui permet l'affectation directe dans le bloc de données.
Pour utiliser notre nouveau conteneur, on écrira :

BoostCArray<UINT,UINT> > m_array;

BoostCarray remplacera donc CArray dans votre code.
Le nouveau conteneur reste bien sûr compatible avec la sérialisation MFC.

V-B - Sérialisation d'une CList

Voyons maintenant le cas d'une CList :

template< class TYPE, class ARG_TYPE = const TYPE& >
class BoostCList : public CList< TYPE, ARG_TYPE  >
{
public:
BoostCList (int nBlockSize = 10):CList<TYPE,ARG_TYPE>(nBlockSize){}
BoostCList (const BoostCList &x){*this = x;}
BoostCList &operator = (const BoostCList &x);

private:
friend class boost::serialization::access;
template<class Archive>
void save(Archive & ar, const unsigned int version) const
{
const int count=GetCount();
if(!count) return;
ar & boost::serialization::make_nvp("count",count);

POSITION pos =GetHeadPosition();
for (int n = 0; n < count; n++)
{
ar &  boost::serialization::make_nvp("item",GetNext(pos));
}
}

template<class Archive>
void load(Archive & ar, const unsigned int version)
{
int count=0;
ar & boost::serialization::make_nvp("count",count);
ARG_TYPE var;
for(int n=0;n<count;n++)
{
ar &  boost::serialization::make_nvp("item",var);
AddTail(var);
}
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
};

template< class TYPE, class ARG_TYPE>
inline BoostCList<TYPE,ARG_TYPE> &
BoostCList<TYPE,ARG_TYPE>::operator = (const BoostCList &x)
{
if(this != &x)
{
TYPE tDst;
TYPE tSrc;
POSITION pos;

RemoveAll();
pos = x.GetHeadPosition();
while(pos != NULL)
{
tSrc = x.GetNext(pos);
CopyElements<TYPE>(&tDst,&tSrc,1);
AddTail(tDst);
}
}
return *this;
}

Grosso modo on retrouve le même traitement de sérialisation que pour le CArray.
Comme pour le CArray j'ai rajouté le constructeur de copie et l'opérateur d'affectation
Pour utiliser notre nouveau conteneur, on écrira :

BoostCList<UINT,UINT> > m_list;

BoostClist remplace CList dans votre code.

V-C - Sérialisation d'une CMap

La sérialisation d'une CMap est un peu plus compliquée, car il va falloir enregistrer les informations correspondantes à la clef et aux données :

template<class KEY, class ARG_KEY, class VALUE, class ARG_VALUE>
class BoostCMap : public CMap<KEY, ARG_KEY, VALUE, ARG_VALUE>
{
public:

BoostCMap(int nBlockSize = 10);
BoostCMap(const BoostCMap &x);

BoostCMap &operator = (const BoostCMap& x);

// accès a la donnée par GetAt par un index comme pour un CArray
VALUE GetAt(int nIndex,KEY &rKey) const;

// fixe le nom de la clef pour la sérialisation boost
void SetKeyName(const TCHAR *szKeyName){m_strKeyName=szKeyName;}
// récupère le nom de la clef pour la sérialisation boost
CStringA GetKeyName()const {return m_strKeyName;}

// fixe le nom de la data pour la sérialisation boost
void SetDataName(const TCHAR *szDataName){m_strDataName=szDataName;}
// récupère le nom de la data pour la sérialisation boost
CStringA GetDataName()const {return m_strDataName;}

private:
friend class boost::serialization::access;
template<class Archive>
void save(Archive & ar, const unsigned int version) const
{
BoostCArray<KEY,ARG_KEY> arKeys;
ARG_KEY key;
VALUE val;
for(POSITION pos = GetStartPosition();pos != NULL;)
{
GetNextAssoc(pos, key, val);
arKeys.Add(key);
}
ar << boost::serialization::make_nvp(GetKeyName(),arKeys);
CStringA str;
for(POSITION pos = GetStartPosition();pos != NULL;)
{
GetNextAssoc(pos, key, val);
ar << boost::serialization::make_nvp(BoostSerializeDataName<KEY>(GetDataName(),key),val);
}
}
template<class Archive>
void load(Archive & ar, const unsigned int version)
{
BoostCArray<KEY,ARG_KEY> arKeys;
RemoveAll();
ar >> boost::serialization::make_nvp(GetKeyName(),arKeys);
for(int i=0;i<arKeys.GetCount();i++)
{
VALUE val;
ARG_KEY key=arKeys[i];
ar >> boost::serialization::make_nvp(BoostSerializeDataName<KEY>(GetDataName(),key),val);
SetAt(arKeys[i],val);
}
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
private:
CString m_strDataName,m_strKeyName;
};

template<class TYPE>
CStringA BoostSerializeDataName(CStringA DefaultName, TYPE Element)
{
return DefaultName;
}

Comme pour les autres conteneurs J'ai rajouté le constructeur de copie et l'opérateur d'affectation.
Les méthodes SetKeyName et SetDataName permettent de spécifier le label employé pour nommer la clef et les données.
Pour permettre une identification plus précise, il est possible de spécialiser la fonction BoostSerializeDataName.

V-D - Sérialisation des classes CString,CStringA,CStringW

Jusque-là rien de bien compliqué le problème devient plus épineux avec la CString qui je le rappelle peut se décliner en CString ,CStringA ou CStringW suivant le mode de fonctionnement réglé au niveau du programme (Unicode ou multibyte).
La première approche serait de faire une méthode load et save comme nous venons de voir, cette méthode fonctionnerait, mais aurait l'inconvénient de rajouter un niveau de balise pour identifier la chaine de caractère. La solution idéale serait de pouvoir faire comme pour une string et d'utiliser les opérateurs :

& ou << >>

Après quelques essais infructueux au niveau de la définition des opérateurs de flux la solution finale consistera à les définir comme suit :

template<class PARENTCSTRING>
class  BoostCString : public PARENTCSTRING
{
public:

BoostCString( const char *pszSrc) :PARENTCSTRING(pszSrc){}
BoostCString( const wchar_t *pszSrc) :PARENTCSTRING(pszSrc){}
BoostCString() :PARENTCSTRING(){}
BoostCString( const PARENTCSTRING &s) :PARENTCSTRING(s){}

BoostCString &operator = (const PARENTCSTRING &x)
{
PARENTCSTRING::operator =(x);
return *this;
}
BoostCString &operator = (const char *pszSrc)
{
PARENTCSTRING::operator =(pszSrc);
return *this;
}
BoostCString &operator = (const wchar_t *pszSrc)
{
PARENTCSTRING::operator =(pszSrc);
return *this;
}
friend basic_ostream<char, char_traits<char> > & operator << (basic_ostream<char, char_traits<char> > &flxOut,const BoostCString &item);
friend basic_istream<char, char_traits<char> > & operator >> (basic_istream<char, char_traits<char> >&flxIn ,BoostCString &item);
};

#include <boost/serialization/collection_traits.hpp>
#include <boost/config.hpp>
#include <boost/serialization/level.hpp>
BOOST_CLASS_IMPLEMENTATION(BoostCString<CString>  , boost::serialization::primitive_type)
#ifdef _UNICODE
BOOST_CLASS_IMPLEMENTATION(BoostCString<CStringA> , boost::serialization::primitive_type)
#else
BOOST_CLASS_IMPLEMENTATION(BoostCString<CStringW> , boost::serialization::primitive_type)
#endif

la nouvelle classe doit être déclarée comme étant une primitive pour pouvoir utiliser les opérateurs de flux, cette déclaration est faite avec la macro BOOST_CLASS_IMPLEMENTATION.
trois niveaux de déclaration sont possibles ici j'ai choisi : primitive_type
voir :http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/serialization.html#primitiveoperators
et : http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/traits.html#Traits
le code des opérateurs :

using namespace std;

CStringA  UTF8EncodeString(const CStringW input);
CStringW UTF8DecodeString(const CStringA input);

#ifdef _UNICODE
basic_ostream<char, char_traits<char> > & operator << (basic_ostream<char, char_traits<char> > &flxOut,const BoostCString<CStringA> &item)
{
flxOut << UTF8EncodeString(CStringW(item));
return flxOut;

}
basic_ostream<char, char_traits<char> > & operator << (basic_ostream<char, char_traits<char> > &flxOut,const BoostCString<CString> &item)
{
flxOut << UTF8EncodeString(item);
return flxOut;
}

basic_istream<char, char_traits<char> > & operator >> (basic_istream<char, char_traits<char> > &flxIn,BoostCString<CStringA> &item)
{
std::string s;

char c;
while((c=flxIn.get())!='\n')
{
s+=c;
}

basic_string <char>::size_type n=s.find("</");
if(n!=string::npos)
{
int nSizeBalise=(s.size()-n);
s=s.substr(0,s.size()-nSizeBalise);
flxIn.seekg( -(nSizeBalise+2), ios_base::cur ); // se remettre au début de la balise de fin...
}
item=CStringA(UTF8DecodeString(s.c_str()));
return flxIn;
}
basic_istream<char, char_traits<char> > & operator >> (basic_istream<char, char_traits<char> > &flxIn,BoostCString<CString> &item)
{
std::string s;

char c;
while((c=flxIn.get())!='\n')
{
s+=c;
}

basic_string <char>::size_type n=s.find("</");
if(n!=string::npos)
{
int nSizeBalise=(s.size()-n);
s=s.substr(0,s.size()-nSizeBalise);
flxIn.seekg( -(nSizeBalise+2), ios_base::cur ); // se remettre au début de la balise de fin...
}
item=UTF8DecodeString(s.c_str());
return flxIn;
}
#else
basic_ostream<char, char_traits<char> > & operator << (basic_ostream<char, char_traits<char> > &flxOut,const BoostCString<CStringW> &item)
{
flxOut << UTF8EncodeString(item);
return flxOut;
}
basic_ostream<char, char_traits<char> > & operator << (basic_ostream<char, char_traits<char> > &flxOut,const BoostCString<CString> &item)
{
flxOut << UTF8EncodeString(CStringW(item));
return flxOut;
}
basic_istream<char, char_traits<char> > & operator >> (basic_istream<char, char_traits<char> > &flxIn,BoostCString<CStringW> &item)
{
std::string s;
char c;
while((c=flxIn.get())!='\n')
{
s+=c;
}
basic_string <char>::size_type n=s.find("</");
if(n!=string::npos)
{
int nSizeBalise=(s.size()-n);
s=s.substr(0,s.size()-nSizeBalise);
flxIn.seekg( -(nSizeBalise+2), ios_base::cur ); // se remettre au début de la balise de fin...
}
item=UTF8DecodeString(s.c_str());
return flxIn;
}
basic_istream<char, char_traits<char> > & operator >> (basic_istream<char, char_traits<char> > &flxIn,BoostCString<CString> &item)
{
std::string s;
char c;
while((c=flxIn.get())!='\n')
{
s+=c;
}

basic_string <char>::size_type n=s.find("</");
if(n!=string::npos)
{
int nSizeBalise=(s.size()-n);
s=s.substr(0,s.size()-nSizeBalise);
flxIn.seekg( -(nSizeBalise+2), ios_base::cur ); // se remettre au début de la balise de fin...

}
item=CStringA(UTF8DecodeString(s.c_str()));
return flxIn;
}
#endif

L'écriture ne cause pas de problème, le contenu de la variable est envoyé dans le flux.
Par contre pour la lecture celle-ci entraine la lecture de la fin de balise j'ai donc repositionné le pointeur de lecture au début de la balise.
Comme pour l'encodage la lecture procède à un décodage UTF-8 et stocke la chaine dans le mode adapté : Unicode ou MultiBytes.

VI - Lancer la sérialisation

Nous venons de voir la technique d'implémentation de sérialisation des données, voyons maintenant comment déclencher le traitement d'écriture et de lecture de notre fichier XML.

/------------------------------------------------------------------
bool CWindowTextManager::Load(const TCHAR *szXMLFile)
{
m_bSave=false;
CStringA strFileName(szXMLFile);
if(_access(strFileName,0)==-1) return false;
std::ifstream ifile(strFileName);
if(ifile.get()!='<')
{
int x=1;
while(ifile.get()!='<') x++;
ifile.seekg(x);

}
else ifile.seekg(0);

boost::archive::xml_iarchive Archive(ifile);
serialize(Archive,1);
return true;
}
//------------------------------------------------------------------
bool CWindowTextManager::Save(const TCHAR *szXMLFile)
{
CStringA strFileName(szXMLFile);
m_bSave=true;
std::ofstream ofile(strFileName);
boost::archive::xml_oarchive Archive(ofile);
serialize(Archive,1);
return 1;
}

Méthode générale
On ouvre un fichier on l'associe à une Archive et on appel la fonction serialize.
Dans le cas de la lecture, j'ai testé la marque d'un fichier UTF8 pour me positionner juste après pour que la sérialisation puisse fonctionner.
Cette marque sera rajoutée si le fichier est modifié par notepad par exemple.
Enfin pour sérialiser on dispose de plusieurs modèles d'archives :

// a portable text archive
boost::archive::text_oarchive // saving
boost::archive::text_iarchive // loading

// a portable text archive using a wide character stream
boost::archive::text_woarchive // saving
boost::archive::text_wiarchive // loading

// a portable XML archive
boost::archive::xml_oarchive // saving
boost::archive::xml_iarchive // loading

// a portable XML archive which uses wide characters - use for utf-8 output
boost::archive::xml_woarchive // saving
boost::archive::xml_wiarchive // loading

// a non-portable native binary archive
boost::archive::binary_oarchive // saving
boost::archive::binary_iarchive // loading

Dans mon implémentation je me suis limité à l'archive xml_oarchive , en procédant à l'encodage des CString en UTF-8.

VII - Les Exemples

Au début de l'article j'ai exposé la version STL de la sérialisation de ma classe voici maintenant la version XML MFC, c'est-à-dire utilisant que des classes MFC.

class CWindowTextManager
{
public:
CWindowTextManager(){}
~CWindowTextManager(){}

CWindowTextManager();
~CWindowTextManager(){}

// sauvegarde les libellés par fenêtres
bool Load(const TCHAR *szXMLFile);

// lecture des libellés par numéro de fenêtres.
bool Save(const TCHAR *szXMLFile);

// mémorise pour la fenêtre désignée par pWndToExplore l'ensemble des libellés
// nIdWindow correspond a l'identifiant de la fenêtre dans les ressources par exemple MyformView::IDD
int  ExploreCtrl(CWnd *pWndToExplore,UINT nIdWindow);

// fixe le libellé d'un contrôle désigné par son identifiant pour l'objet fenêtre pWnd.
// la table interne est aussi mise à jour.
bool SetWindowText(CWnd *pWnd,UINT nIdControl,TCHAR *szText);

// fixe le libellé d'un contrôle désigné par son identifiant pour l'identifiant de fenêtre nIdWindow.
// la table interne est mise à jour, si une fenêtre est liée a l'identifiant le contrôle est mis à jour graphiquement/
bool SetWindowText(UINT nIdWindow,UINT nIdControl,TCHAR *szText);

// rafraichi les libellés d'un objet fenêtre pWnd d'après la table interne.
bool RefreshWindow(CWnd *pWnd);
// rafraichi les libellés d'une fenêtre désignée par nIdWindow d'après la table interne.
bool RefreshWindow(UINT nIdWindow);
// fixe le lien entre un objet fenêtre et son identifiant dans les ressources.
void SetWndId(CWnd *pWnd,UINT nIdWindow);
// retrouve l'objet fenêtre associé à l'identifiant nIdWindow
CWnd *FindWindow(UINT nIdWindow);
// retrouve l'identifiant fenêtre associé à l'objet fenêtre pWnd
UINT  FindWindow(CWnd *pWnd);

static CStringA UTF8EncodeString(const CStringW input);
static CStringW UTF8DecodeString(const CStringA input);

private:
CWindowTextManager (const CWindowTextManager  &r);
CWindowTextManager   & operator = (const CWindowTextManager  & arg);

friend class boost::serialization::access;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & boost::serialization::make_nvp("MapWnd",m_MapElt);
}

public:
bool FindItem(UINT nIdWindow,UINT nIdControl);
class ItemCtrl : public CObject
{
public:
ItemCtrl(const TCHAR *sz=_T(""),UINT nId=0)
{
strText=sz;
nIdC=nId;
}
ItemCtrl(const ItemCtrl &rItem)
{
*this = rItem;
}
const ItemCtrl& operator=(const ItemCtrl& Src)
{
CopyFrom(Src);
return *this;
}
void CopyFrom(const ItemCtrl & Src )
{
if(this==&Src) return;
strText=Src.strText;
nIdC=Src.nIdC;
}
friend class boost::serialization::access;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & BOOST_SERIALIZATION_NVP(nIdC) & BOOST_SERIALIZATION_NVP(strText);
}

BOOL operator == (const ItemCtrl &  item) const
{
return item.nIdC==nIdC;
}

BoostCString<CStringW> strText;    // Texte Contrôle
UINT    nIdC;        // identifiant
};

bool    m_bSave;
CMap<CWnd *,CWnd *,UINT,UINT> m_mapWnd;
typedef BoostCList<ItemCtrl,ItemCtrl> ListCtrl;

BoostCMap<UINT,UINT, ListCtrl,ListCtrl &> m_MapElt;
ListCtrl m_ListItem;
ItemCtrl m_Item;
};

template<>
CStringA BoostSerializeDataName(CStringA DefaultName, UINT id);

Tous les conteneurs spécifiques pour la sérialisation Boost sont utilisés, la classe string a laissé la place à son équivalent CString étendu avec le template BoostCString.
J'ai rajouté le constructeur de copie et l'opérateur d'affectation dans ma structure ItemCtrl.
L'encodage UTF-8 est pris en charge directement à l'écriture et à la lecture du flux ce qui me permet d'utiliser une méthode serialize pour la lecture et la sauvegarde.
j'ai rajouté une spécialisation de la fonction BoostSerializeDataName pour spécifier un nom pour la liste des données associées à une clef.
Pour mettre en pratique, j'ai réalisé deux projets réalisant le même travail.
Le premier BoostSerialSTL utilise les classes de la STL, le deuxième BoostSerialMFC uniquement les MFC.
Le projet est une boîte de dialogue avec quelques contrôles positionnés dessus.
Au lancement du programme le fichier Xml ListCtrl.xml est lu s'il existe, en cas d'absence l'ensemble des contrôles de la fenêtre est exploré et maintenu dans l'objet issu de la classe CWindowTextManager. À la fermeture du programme, on lance la sérialisation.

BOOL CBoostSerialMFCApp::InitInstance()
{
//.......................
CBoostSerialMFCDlg dlg;
m_pMainWnd = &dlg;
m_WndTextManager.Load(_T("ListCtrl.xml"));
int nResponse = dlg.DoModal();
//...........
}
BOOL CBoostSerialMFCDlg::OnInitDialog()
{
CDialog::OnInitDialog();
//.......
// TODO: Add extra initialization here
theApp.m_WndTextManager.SetWndId(this,CBoostSerialMFCDlg::IDD);
if(!theApp.m_WndTextManager.RefreshWindow(this))
theApp.m_WndTextManager.ExploreCtrl(this,CBoostSerialMFCDlg::IDD);
//.......
}
void CBoostSerialMFCDlg::OnDestroy()
{
theApp.m_WndTextManager.Save(_T("ListCtrl.xml"));
CDialog::OnDestroy();
// TODO : ajoutez ici le code de votre gestionnaire de messages
}

Il est alors possible de modifier les libellés dans le fichier XML avec notepad et de relancer le programme pour voir le changement d'affichage sur les contrôles.

VIII - Conclusion

- Il y aurait énormément de choses à dire sur cette bibliothèque, je n'ai fait que survoler les aspects essentiels et pratiques pour vous mettre le pied à l'étrier…
Je vous conseille vivement d'explorer la documentation pour en apprécier pleinement toutes les possibilités et subtilités.
- À partir de l'exemple de la CString il devrait être facile d'intégrer la sérialisation des autres objets MFC comme la classe CRect, CSize, CPoint, etc.
- Enfin je n'ai traité que les deux conteneurs MFC CArray et CList , il est tout à fait possible de faire un traitement spécifique pour un CMap en copiant le fonctionnement illustré dans mon exemple de sérialisation d'une map.
- Les exemples mettant en œuvre la sérialisation avec la STL et les classes MFC ont été réalisés avec Visual 2008 SP1.