Retour à la table des matières

Tutorial MFC (Microsoft Foundation Classes)

Nous allons, dans ce document, faire une présentation succincte des MFC (Microsoft Foundation Classes). En effet, il nous faudrait bien quelques mois de travail pour couvrir la totalité des possibilités offertes par les MFC. Au terme de cette présentation, vous aurez les éléments de base nécessaires pour créer une application MFC simple : d'ailleurs il vous est possible de télécharger le code de l'exemple sur lequel je m'appuie durant cette présentation. Cet exemple vous montre comment coder une application permettant de tracer une image, constituée de segments, à la souris.

Introduction

Les MFC constituent une librairie de classes dédiées à la mise en oeuvre d'applications graphiques sur plate-forme Windows. Il s'agit en fait d'une encapsulation objet des fonctionnalités de base proposées par la librairie Win32. L'environnement de développement Visual C++ 6.0 permet, via de nombreux outils, de simplifier sérieusement la mise en oeuvre d'une application MFC. Ce document montre notamment comment utiliser Visual C++ 6.0 pour générer la quasi totalité du code nécessaire à notre exemple.

Dans un premier temps, nous allons utiliser "l'App Wizard" (l'assistant de création d'application) pour générer la base du code sur lequel nous allons travailler. Nous verrons qu'il crée plusieurs classes ayant chacune un rôle bien spécifique. En fait les MFC vous proposent d'utiliser une architecture Document/Vues permettant de séparer les données (le document) du code permettant de les visualiser (les vues).

Ensuite, nous utiliserons un autre assistant : le "Class Wizard". Il permet de pouvoir générer les gestionnaires de messages Windows (les gestionnaires d'événements si vous préférez). Les messages que nous traiterons seront liés à l'utilisation de la souris. Nous verrons aussi comment afficher les données du document au sein de la vue.

Enfin, nous parlerons plus sérieusement du document et de sa persistance. En effet, vos aurez certainement besoin de pouvoir permettre de sauver ou de charger vos documents. Pour ce faire, les MFC proposent un mécanisme de sérialisation, que nous présenterons brièvement.

Création d'une application MFC minimale

Lancez Visual C++ et créez un nouveau projet. Deux informations sont utiles : la nature du projet et son nom. Choisissez respectivement "MFC AppWizard (exe)" et par exemple "Draw" puis cliquez sur "Ok". Six boîtes de dialogue vont se succéder. Pour une grande partie des options demandées, acceptez les choix par défaut.

Au terme de cette étape, plusieurs classes ont été créées. Chacune d'entre elles prend en charge une partie des fonctionnalités de l'application à générer. La classe d'application contient, notamment, du code exécuté au démarrage et à la fermeture de l'application. Elle crée aussi une fenêtre en instanciant un objet à partir de la classe de fenêtre du projet. Dans une fenêtre, au moins deux sous-objets sont créés : l'objet de document de la fenêtre et un objet de vue. L'objet de document contient les données en cours de manipulation. Une vue sert à afficher les données d'un document d'une certaine manière.

Pour mieux comprendre cette séparation document/vues, considérons une partie d'échecs. Ce qui permet de définir cette partie, c'est la connaissance des positions de chaque pièce sur l'échiquier, la valeur de deux chronomètres et à qui c'est le tour de jouer. C'est informations seront stockées dans le document. Par contre, il y a plusieurs façons de représenter cette partie : on peut la représenter en deux dimensions, ce qui est couramment fait, ou en trois dimensions avec les pièces elles-mêmes dessinées en 3D. Deux types de vues distincts peuvent donc être codés pour représenter une telle partie.

Revenons à notre exemple, et cherchons à voir ce qui a été généré. Pour ce faire, compilez le projet et exécutez-le. Il doit être clair que par défaut, l'application est minimale. C'est à vous d'en compléter le code pour en faire quelque chose.

Ajout de gestionnaires de messages

La première chose que nous allons faire, c'est ajouter des gestionnaires de messages (ou d'événements, si vous préférez) pour que votre application réagisse aux actions de l'utilisateur. Pour ce faire, nous allons utiliser le "Class Wizard". La manière la plus simple d'invoquer cet assistant, c'est d'appuyer simultanément sur Ctrl et W. La capture d'écran suivante vous montre cet assistant.

La fenêtre est divisée en différentes zones, chacune affichant une partie de l'information. Prenez soin de vérifier que le projet sélectionné soit bien le bon et choisissez comme nom de classe CDrawView. C'est sur cette classe de vue que nous allons ajouter trois événements. Ensuite, il vous faut choisir l'objet de l'application pour lequel les messages doivent être traités. En effet, un objet C++ est directement associé à votre vue, mais vous avez aussi tous les objets associés aux ressources graphiques (éléments de menu, éléments de dialogue, ...). Vous référencez chacun d'eux via son IDentificateur.

Dans le cas présent, ce sont les messages associés à la vue qui nous intéresse. Puis ensuite, choisissez les messages à gérer. Ils sont au nombre de trois : WM_LBUTTONDOWN, WM_LBUTTONUP et WN_MOUSEMOVE. Pour chacun d'entre eux, cliquez sur "Add Function". Ensuite, placez-vous sur "OnLButtonDown" dans la liste du bas et cliquez sur "Edit Code". Commencez par mettre le code suivant dans vos trois gestionnaires. N'oubliez pas de déclarer les deux attributs de classes utilisés comme étant privés au sein de la classe de vue.

//////////////////////////////////////////
/// Le début du fichier DrawView.cpp /////
//////////////////////////////////////////

CDrawView::CDrawView() {
    this->isDrawing = false;
}

//////////////////////////////////////////
/// La suite du code /////////////////////
//////////////////////////////////////////
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point) {
    this->isDrawing = true;
    this->oldPoint = point;

    CView::OnLButtonDown(nFlags, point);
}

void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) {
    this->isDrawing = false;

    CView::OnLButtonUp(nFlags, point);
}

void CDrawView::OnMouseMove(UINT nFlags, CPoint point) {
    if (this->isDrawing) {
        CClientDC *pdc = (CClientDC *)this->GetDC();
        pdc->MoveTo(this->oldPoint);
        pdc->LineTo(point);

        this->oldPoint = point;
    }
	
    CView::OnMouseMove(nFlags, point);
}
Fichier "DrawView.cpp"

//////////////////////////////////////////
/// Le début du fichier DrawView.h   /////
//////////////////////////////////////////

class CDrawView : public CView {
    bool isDrawing;
    CPoint oldPoint;

protected:

//////////////////////////////////////////
/// La fin du fichier DrawView.h   ///////
//////////////////////////////////////////
Fichier "DrawView.h"

Compilez ce programme et lancez-le ! Normalement, vous devez être en mesure de réaliser à la souris, un tracé sur la vue. Mais des bugs existent dans notre programme. Tout d'abord, utilisez le splitter pour avoir plusieurs vues dans la fenêtre : vous ne pouvez dessiner que dans une unique vue. De plus, si vous iconifiez la fenêtre, puis si vous la faites repasser en avant plan, le tracé devrait disparaitre.

Ces deux problèmes sont tout à fait normaux. En effet, quand la vue se réaffiche, elle est censée retracer les données du document. Or, nous n'avons jamais enrichi notre document.

Mise à jour du document

Pour améliorer notre application, nous allons faire en sorte de sauvegarder chaque segment de notre image dans l'objet de document. Pour ce faire, nous allons avoir besoin d'un conteneur. Les MFC fournissent des classes génériques servant à contenir des données et notamment les classes CArray, CList et CMap. Dans notre application, nous allons utiliser un objet de type CArray. Ces types de conteneurs sont définis dans l'en-tête <afxtempl.h>.

Le modèle (template) CArray requiert deux paramètres de généricité. Le premier indique le type des données contenues (pour nous des CPoint) et le second indique comment ces données seront passées en paramètres des méthodes du conteneur (le mieux sera de les passer par référence, histoire d'optimiser les choses). Le tableau sera défini sous forme d'attribut de la classe de document. Nous aurons donc la déclaration suivante :

CArray<CPoint, const CPoint &> points;

En conséquence, voici les ajouts que je vous propose d'adjoindre à votre classe de document pour permettre le stockage des différents segments constituant la figure.

// . . .

class CDrawDoc : public CDocument
{
// . . .

// Attributes
public:
    CArray<CPoint, const CPoint &> points;

// Operations
public:
    void AddPoint(const CPoint &point1, const CPoint &point2);

// . . .

Fichier "DrawDoc.h"

// . . . Le début du document . . .

void CDrawDoc::AddPoint(const CPoint &point1, const CPoint &point2) {
    this->points.Add(point1);
    this->points.Add(point2);

    // Les données du document sont mises à jour. Il faut donc
    // demander confirmation avant fermeture du document.
    this->SetModifiedFlag(true);
}
Fichier "DrawDoc.cpp"

Il faut ensuite faire en sorte d'ajouter un segment dans le document, si l'utilisateur trace dans la vue. Pour ce faire, il faut mettre à jour la méthode DrawView::OnMouseMove comme le montre l'extrait de code suivant.

void CDrawView::OnMouseMove(UINT nFlags, CPoint point) {
    if (this->isDrawing) {
	CClientDC *pdc = (CClientDC *)this->GetDC();
	pdc->MoveTo(this->oldPoint);
	pdc->LineTo(point);

	CDrawDoc* pDoc = this->GetDocument();
	ASSERT_VALID(pDoc);
	pDoc->AddPoint(this->oldPoint, point);

	this->oldPoint = point;
    }
	
    CView::OnMouseMove(nFlags, point);
}

Retracer le document dans la vue

Mais cela ne règle pas nos problèmes précédents. En effet, il nous faut maintenant redessiner le document dans la vue. Pour ce faire, il nous faut simplement réagir à un message Windows : WM_PAINT. Celui-ci est déclenché à chaque fois qu'une vue doit réafficher son contenu. Le système MFC déclenche alors, en réaction à WM_PAINT, la méthode OnDraw qui se devra de retracer la vue.

La méthode OnDraw prend en paramètre un pointeur sur un CDC (un objet de contexte de périphérique - Device Context). En effet, OnDraw sert à la fois pour le tracé sur l'écran et pour le tracé pour l'impression papier : le CDC garanti l'indépendance vis à vis du matériel utilisé. Cet objet vous fournit un certain nombre de méthodes permettant le tracé (de points, de lignes, de figures, ...) ainsi que des méthodes de manipulation des objets GDI (Graphics Device Interface) tels que les polices, les couleurs de tracé (avant plan et arrière plan), les bitmaps, ... Le code suivant vous montre comment retracer l'ensemble des segments constituant la figure.

void CDrawView::OnDraw(CDC* pDC) {
    CDrawDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    int lastPos = pDoc->points.GetSize();
    //TRACE1("SIZE == %d\n", lastPos);
    for(int i=0; i<lastPos; i+=2) {
	pDC->MoveTo(pDoc->points.ElementAt(i).x,pDoc->points.ElementAt(i).y);
	pDC->LineTo(pDoc->points.ElementAt(i+1).x,pDoc->points.ElementAt(i+1).y);
    }
}

Mais cela ne suffit pas ! En effet, nous avons incorporé un splitter au niveau de nos fenêtres filles. Celui-ci permet de diviser l'espace de la fenêtre en plusieurs vues (dans la capture d'écran qui suit, quatre). Pour l'heure, si vous tracez dans une vue, des segments s'y rajoutent. Mais les autres vues ne changent pas. Ce qu'il faudrait, c'est demander une réactualisation de toutes les vues à partir du document qui leur est rattaché. Cela est possible par l'intermédiaire de la méthode CDocument::UpdateAllViews. Elle est chargée d'envoyer un message WM_PAINT à toutes les vues du document considéré.

Cette méthode peut accepter un paramètre indiquant quelle est la vue ayant permis les modifications du document : elle ne recevra pas le message WM_PAINT. Dans notre cas, cela est fort intéressant : en effet cette vue n'est pas à réactualiser car elle vient d'être mise à jour. Si au contraire, vous passez la valeur NULL, toutes les vues seront remises à jour. Voici donc la nouvelle version de la méthode CDrawView::OnMouseMove, prenant en charge la réactualisation des autres vues rattachées au document.

void CDrawView::OnMouseMove(UINT nFlags, CPoint point) {
    if (this->isDrawing) {
	CClientDC *pdc = (CClientDC *)this->GetDC();
	pdc->MoveTo(this->oldPoint);
	pdc->LineTo(point);

	CDrawDoc* pDoc = this->GetDocument();
	ASSERT_VALID(pDoc);
	pDoc->AddPoint(this->oldPoint, point);

	this->oldPoint = point;
	this->GetDocument()->UpdateAllViews(this);
    }
    CView::OnMouseMove(nFlags, point);
}

Les MFC et la Sérialisation

La sérialisation est un concept objet qui permet la sauvegarde et la relecture d'un objet sur un support de stockage tel qu'un disque dur. Tous les langages de programmation, orientés objet, ne supportent pas forcément ce concept. Le langage Java le supporte de base, alors que C++ non. Pour les langages ne supportant pas cette possibilité, certaines librairies proposent leur propre moteur de sérialisation : c'est le cas des MFC.

Pour être sérialisables, vos objets se doivent de dériver de la classe CObject. De plus deux macros doivent être employées dans le code de vos classes persistantes, dans le but d'y insérer le support minimal utile au concept. Ces deux macros sont : DECLARE_DYNCREATE et IMPLEMENT_DYNCREATE. Votre classe de document répond déjà à ces critères. De plus, notre document contient un tableau (de classe CArray) de points (CPoint). Ces deux types de données sont eux aussi déjà sérialisables. Certes, CPoint ne dérive pas de CObject, mais un opérateur << à été redéfini afin de permettre sa manipulation lors de la sérialisation.

Vous n'avez plus qu'une seule chose à faire. C++ ne vous propose aucun mécanisme pour déterminer le nombre, les types et les noms de vos paramètres : il n'automatise donc pas la sauvegarde des attributs de votre classe. Pour pallier le problème, une méthode, à liaison dynamique (virtuelle), est définie : Serialize. Il vous suffit de la redéfinir et d'y placer le code utile pour la sauvegarde et la lecture de vos attributs.

Il y a principalement deux façons de coder une méthode Serialize. Soit vous utilisez des types de données simples CPoint, CString, ... et dans ce cas vous utiliserez les opérateurs << et >> sur l'objet d'archive (un exemple de code suit). Soit vous utilisez des objets dérivés de CObject : dans ce dernier cas, il suffit d'y cascader un appel à leur méthode Serialize.

void COtherDoc::Serialize(CArchive & ar) {
    if (ar.IsStoring()) {
	ar << this->theString;
    } else {
        ar >> this->theString;
    }
}
Un exemple de sauvegade de CString

void CDrawDoc::Serialize(CArchive & ar) {
	this->points.Serialize(ar);
}
Sérialisons notre figure

Conclusion

Au terme de ce document, nous avons vu que les MFC simplifient la mise en oeuvre d'applications graphiques sous Windows. Il s'agit en fait de bien plus qu'une simple librairie orientée objet : nous avons à faire à un véritable framework de développement. Ce document s'est plus particulièrement focalisé sur l'architecture Document/Vues proposée par les MFC.

Afin de mieux comprendre les choses, nous avons utilisé des assistants (l'assistant d'applications et l'assistant de classes) pour générer une grande partie du code de l'application. Puis nous avons complété les différentes classes générées.

Au final, nous avons vu qu'il est possible, via les MFC, de tirer profit de la sérialisation pour sauvegarder les données comprises dans notre objet de document. Il doit de plus être bien clair que nous n'avons couvert qu'une très petite partie des possibilités offertes par les MFC.

Retour à la table des matières