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.