La sérialisation avec vc++6 [mfc] comprendre et mettre en oeuvre

Description

Archiver des données représentant des objets dans un fichier structuré en gérant les versions sans repartir de zéro! Voici, sous la forme d'un tutoriel, de quoi comprendre et mettre en oeuvre la sérialisation.

L'exemple montre comment tirer au MAXIMUM parti du framework MFC pour faire en sorte que nos classes soient vraiment SERIALISABLEs.

Dans le ZIP : les deux projets (sans et avec la gestion des versions)
la V1 fait du noir et la V2 gère la couleur...

Source / Exemple :


I) Création du Squelette

Créer un projet MFC SimpleDocument (SDI) de nom "serialisation"
à l'étape 4, lui donner une extension de fichier (.PTS)
générer le squelette d'application

II) Dessin simple

Faire en sorte qu'à chaque clic de souris, un nouveau point soit dessiné à l'écran à la position du clic
on pourra représenter le point par ses coordonnées sous la forme [x, y]

Pour le débutant, voici la démarche pas à pas :
	il suffit de faire un clic droit dans l'onglet ClassView sur la classe CSerialisationView
	et de demander la génération d'un gestionnaire de message pour windows "Windows Message Handler"
	On choisira le WM_LBUTTONDOWN (clic gauche) et add&edit
	on rajoutera le code suivant dans le gestionnaire :

	void CSerialisationView::OnLButtonDown(UINT nFlags, CPoint point) 
	{
		// TODO: Add your message handler code here and/or call default
		CString s;
		s.Format("[%d,%d]", point.x, point.y);
		CClientDC dc(this);
		dc.TextOut(point.x, point.y,s); 
		CView::OnLButtonDown(nFlags, point);
	}
fin de la démarche pas à pas :

Tester l'application en faisant quelques clics sur le fond de la fenêtre
Et vérifier le mauvais comportement de l'application lors des redimensionnements de la fenêtre
ou de minimisation-restauration.

III) Dessin et Mémorisation
 
On notera que si les points sont simplement dessinés et non archivés dans la classe CDocument
tout rafraîchissement de la fenêtre (un redimensionnement par exemple) fait disparaître tous les points

A) On a donc besoin à chaque clic de souris d'ajouter le point à une collection du document
et de le dessiner
ATTENTION ! la classe CPoint n'est pas dérivée de CObject
Le but de cet article étant de traiter la sérialisation (à la MFC) et le PREMIER PRINCIPE à respecter
pour être une classe sérialisable étant de dériver de CObject :
=> Il faut donc créer notre propre classe de Point, on pourra la nommer CObjectPoint et lui donner
un constructeur acceptant un CPoint et une donnée membre de type CPoint (réutilisation par composition)
Prendre bien soin de conserver le constructeur par défaut car il est exigé en vertu du DEUXIEME PRINCIPE
de la sérialisation.
B) De plus, dans l'événement WM_PAINT de la vue, il faut redessiner tous les points. 
Il faut donc être capable d'itérer sur la collection privée et nous fournirons en prenant exemple
sur la conception (le Design) de la classe CObArray les méthodes GetSize et GetAt.

Pour le débutant, voici la démarche pas à pas :
Créer une nouvelle classe CObjectPoint
Clic droit dans ClassView sur le projet (serialisation classes) puis new class
On doit prendre une classe générique (non MFC) car la classe CObject n'est pas vue de l'assistant
nommer la classe CObjectPoint (les fichiers "objectPoint.h et .cpp" sont automatiquement créés)
On modifiera le constructeur 

Modification du document :
Comme on utilisera le type CObjectPoint, ajouter la ligne suivante en tête de "serialisationDoc.h"
#include "objectPoint.h"
Doter la classe dérivée de CDocument (CSerialisationDoc) d'un attribut privé de type CObArray (tableau dynamique d'objets)
Modifier le code du gestionnaire OnLButtonDown pour ajouter un point dans la collection à chaque clic.
	private:
		CObArray m_points;
Parce que notre collection est privée, on doit fournir une méthode d'encapsulation Add(CPoint* pPoint)
Elle sera donc publique et son contenu :
	void CSerialisationDoc::Add(CObjectPoint *pPoint)
	{
		m_points.Add(pPoint);

	}
Modifier le gestionnaire OnLButtonDown en lui rajoutant la ligne suivante
	GetDocument()->Add(new CObjectPoint(point));
Si vous vous demandez pourquoi faire un new (alloc dynamique)... réfléchissez!

Ajout de GetSize et GetAt (ne pas oublier les déclarations dans  le .h)
	int CSerialisationDoc::GetSize()
	{
		return m_points.GetSize();
	}

	CObjectPoint* CSerialisationDoc::GetAt(int index)
	{
		return (CObjectPoint*)m_points.GetAt(index);
	}

Gérer le rafraîchissement de la vue en traitant le WM_PAINT
Pour cela, il faut parcourir la collection de points et dessiner chacun d'eux.
Il est préférable de demander à chaque point de se dessiner lui-même et nous créons une méthode
Draw(CDC*) pour notre classe CObjectPoint (elle recevra le Device Context de dessin):
	void CObjectPoint::Draw(CDC* pDC)
	{
		CString s;
		s.Format("[%d,%d]", m_point.x, m_point.y);
		pDC->TextOut(m_point.x, m_point.y,s);
	}

Modification de la méthode OnDraw de la vue (c'est elle qui est appelée lors des WM_PAINT)
	void CSerialisationView::OnDraw(CDC* pDC)
	{
		CSerialisationDoc* pDoc = GetDocument();
		ASSERT_VALID(pDoc);
		// TODO: add draw code for native data here
		for (int i=0; i<pDoc->GetSize(); i++)
			pDoc->GetAt(i)->Draw(pDC);
	}

fin de la démarche pas à pas

On peut alors test l'application : elle fonctionne correctement même lors des redimensionnements...
ou presque ... Avez vous essayé le menu Fichier/Nouveau !

Les points ne disparaissent jamais et d'ailleurs ils ne sont jamais détruits (pas de delete => fuite mémoire)
La bonne méthode (officielle MFC) est de redéfinir la méthode virtuelle DeleteContent
Clic droit sur CSerialisationDoc / Add Virtual fonction / DeleteContents / Add & Edit
	void CSerialisationDoc::DeleteContents() 
	{
		// TODO: Add your specialized code here and/or call the base class
		for (int i=0; i<m_points.GetSize(); i++)
			delete m_points[i];	//destruction des CObjectPoint (libération du tas)
		m_points.RemoveAll();	//libération du conteneur 
		CDocument::DeleteContents();
	}

Encore un point nécessaire !
Démarrer l'application, cliquer pour générer quelques points, puis fermer l'application.
Rien d'anormal ? 
Elle a oublié de nous proposer de sauvegarder notre document...
Pour cela, nous devons passer à l'étape suivante de sérialisation mais on peut tout de même
la préparer en appelant SetModifiedFlag() à chaque fois que nous ajoutons un point au document
Donc dans la méthode Add.

IV) Sérialisation de base
 
Si l'on referme l'application, les points étant simplement mis en mémoire vive, ils sont perdus:
Il faut donc les archiver sur disque, le plus simple étant d'utiliser le mécanisme
de sérialisation du framework
Le TROISIEME PRINCIPE de la sérialisation est de posséder une méthode Serialize
Nous pouvons vérifier que notre classe document le vérifie déjà ... mais pas notre CObjectPoint
On doit donc l'ajouter à la classe :
Clic droit sur CObjectPoint dans ClassView => ajouter une méthode...
	void CObjectPoint::Serialize(CArchive &ar)
	{
		if (ar.IsStoring())
		{
			// TODO: add storing code here
			ar << m_point;
		}
		else
		{
			// TODO: add loading code here
			ar >> m_point;
		}
	}

Quand et Comment cette méthode est elle appelée ? 
Lorsque le document a besoin de se sauvegarder... par l'appui sur fichier/Enregistrer par exemple.
C'est donc via le document que l'appel a lieu
On doit modifier le contenu de la méthode Serialize du document ainsi :
	void CSerialisationDoc::Serialize(CArchive& ar)
	{
		m_points.Serialize(ar);
	}

C'est presque fini pour le code ... on teste
- d'abord l'enregistrement d'un document vide ... aucun clic => aucun point => OK
on peut vérifier qu'un fichier à l'extension .PTS a bien été créé
- ensuite l'enreristrement d'un ducument non vide ! et un message d'erreur apparaît
Il nous faut respecter le QUATRIEME PRINCIPE de la sérialisation : utiliser les MACROS
DECLARE_SERIAL et IMPLEMENT_SERIAL
rajouter à la fin de la définition de la classe CObjectPoint (fichier "objectPoint.h")
	DECLARE_SERIAL();

et au début du fichier objectPoint.cpp (mais après les includes)
	IMPLEMENT_SERIAL(CObjectPoint, CObject, 1)

Retester ...

V) Sérialisation avec gestion de version
 
On voit bien l'intérêt de la sérialisation ... conserver de façon persistante (archivée) les informations
qui resteraient volatiles sinon.
Le problème est que les logiciels évoluent ... ils changent de version
Que se passe t-il par exemple lorsque l'on veut traiter des points colorés dans une version 2 ?
Généralement, on ne change pas l'extension de fichier pour autant.
La sérialisation d'un document en version 1 suivi de la désérialisation en version 2 devrait bien se passer !

Pour cette raison, lors de la sérialisation, un numéro de version est archivé avec le contenu
et il peut être récupéré lors de la désérialisation pour savoir si l'on ne lit que des CObjectPoint
ou bien des CObjectPointCouleur !

Voyons ce que nous offre le framework MFC pour traiter la question :

La MACRO IMPLEMENT_SERIAL(CObjectPoint, CObject, 1) contient en troisième paramètre le numéro de version
pour faire évoluer le logiciel, il faut donc le modifier dans la version 2.

Pour le débutant, voici la démarche pas à pas :
Partie traitant les points en couleur
Parsque cet article se focalise sur la sérialisation et non sur les bonnes pratiques d'interfaces utilisateur, 
on gèrera la couleur courante dans la vue et un clic droit pour passer de rouge à bleu et réciproquement
un menu déroulant/menu contextuel/barre d'outil/boite de dialogue serait plus adapté mais plus long à décrire

Ajouter une donnée membre privée de type COLORREF appelée m_couleur à la classe Vue (CSerialisationView)
	private:
		COLORREF m_couleur;
Initialiser le à "bleu" dans le constructeur 
		m_couleur = RGB(0,0,255);

Gérer le clic droit qui change la couleur courante:
	void CSerialisationView::OnRButtonDown(UINT nFlags, CPoint point) 
	{
		// TODO: Add your message handler code here and/or call default
		if (m_couleur==RGB(0,0,255))
			m_couleur=RGB(255,0,0);
		else
			m_couleur=RGB(0,0,255);

		CView::OnRButtonDown(nFlags, point);
	}

Modifier la classe CObjectPoint pour suppporter la couleur
	ajouter une donnée membre m_couleur, ajouter un Constructeur capturant la couleur et initialiser. 
Modifier la méthode Draw du point pour tenir compte de la couleur
	pDC->SetTextColor(m_couleur);
Modifier la méthode Serialize du CObjectPoint
	if (ar.IsStoring())
		{	ar << m_point << m_couleur;	}
	else
		{	ar >> m_point << m_couleur;	}

Modifier la création des points dans la méthode OnLButtonDown
	void CSerialisationView::OnLButtonDown(UINT nFlags, CPoint point) 
	{
		// TODO: Add your message handler code here and/or call default
		CString s;
		s.Format("[%d,%d]", point.x, point.y);
		CClientDC dc(this);
		dc.SetTextColor(m_couleur);
		dc.TextOut(point.x, point.y,s);
		
		GetDocument()->Add(new CObjectPoint(point, m_couleur));

		CView::OnLButtonDown(nFlags, point);
	}

Vérifier qu'il existe alors un problème lors de la relecture des fichiers sauvegardés en Version 1

On utilise pour régler cela la technique suivante :
modifier la macro pour lui passer un numéro de version égal à 2
	IMPLEMENT_SERIAL(CObjectPoint, CObject, VERSIONABLE_SCHEMA|2)
Modifier la sérialisation du point ainsi :
	void CObjectPoint::Serialize(CArchive &ar)
	{
		if (ar.IsStoring())
		{
			ar << m_point << m_couleur;//on décide de toujours re-stocker en version 2
		}
		else
		{
		  int nVersion = ar.GetObjectSchema();

		  switch(nVersion)
		  {
		  case 1:  // lire en version 1 (sans la couleur)
			ar >> m_point ;
			 break;
		  case 2:  // lire en version courante (2)
			ar >> m_point >> m_couleur;
			 break;
		  default:
			 // version inconnue
			 break;
		  }
		}
	}

On pourra tester en ouvrant un fichier sauvegardé en version 1 (les points sont noirs)
Modifier ensuite en rajoutant des points rouges et bleus puis sauvegarder.
Ouvrir ensuite le fichier résultat.
Notez que la compatibilité ascendante est contrôlée en particulier car le constructeur des 
CObjectPoint prend une valeur par défaut à 0 pour la couleur (Noire) correspondant à la couleur de la version 1

Conclusion :


créé pour illustrer la sérialisation suite à diverses questions sur le forum

... remarques et ajouts sont les bienvenus

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.