Qu'est-ce que MFC sous Windows ? ── Les bases pour maintenir un actif existant

· Mis à jour le: · · Windows, MFC, Visual C++, C++, Win32, Application native, Application de bureau, Code legacy, Valorisation des actifs existants

1. Ce qu’il faut comprendre en premier

Lorsqu’on assure la maintenance d’anciennes applications de bureau Windows, on tombe parfois sur des noms comme ceux-ci.

CWinApp
CWnd
CDialog
CDialogEx
CFrameWnd
CDocument
CView
CString
CFile
CArchive
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_BN_CLICKED
DoDataExchange
UpdateData

Ces éléments reviennent constamment dans MFC, un framework d’application Windows pour C++. MFC est l’abréviation de Microsoft Foundation Classes, une bibliothèque qui facilite la manipulation de l’API Win32 sous forme de classes C++.

Dans le développement d’applications Windows actuel, de nombreuses options existent — WinUI, WPF, Windows Forms, Electron, Qt, les technologies web — si bien que MFC est de moins en moins le premier choix pour un nouveau développement. Ce n’est pourtant pas une technologie disparue pour autant. Les applications métier, les instruments de mesure, les logiciels de contrôle, le CAD/CAM, les outils internes ou les anciens progiciels amènent encore aujourd’hui à maintenir des bases de code MFC.

Voici d’abord les points de vue à garder à l’esprit pour comprendre MFC.

MFC est une carte essentielle pour lire d'anciennes applications de bureau Windows
Il ne faut pas voir MFC comme masquant l'API Win32, mais comme l'enveloppant à la manière de C++
Sans connaître les usages de MFC, on interprète mal le comportement du code, bien plus que ne le laisse penser son apparence
Sa valeur réside dans la maintenance, la prolongation et la migration progressive de l'existant, plutôt que dans l'adoption pour du neuf

Cet article présente une vue d’ensemble de MFC : la structure des applications, les cartes de messages, Document/View, les boîtes de dialogue, DDX/DDV, les ressources, la compilation et les points de vigilance pour la maintenance.

Les extraits de code de cet article sont par ailleurs publiés sur GitHub sous forme de collection de code de référence, organisée par fichier et par chapitre.

windows-mfc-overview - komurasoft-blog-samples (GitHub)

2. Qu’est-ce que MFC ?

MFC est une bibliothèque de classes permettant de créer des applications de bureau natives Windows en C++.

Lorsqu’on utilise directement l’API Win32, on écrit typiquement du code de ce genre.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
        // Dessin
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

L’API Win32 est extrêmement puissante, mais comme elle s’organise autour de fonctions C, de handles, de messages et de callbacks, les grandes applications deviennent vite difficiles à suivre. MFC permet de manipuler tout cela sous forme de classes C++ : une fenêtre est représentée par CWnd, une boîte de dialogue par CDialog, l’application dans son ensemble par CWinApp, une fenêtre de cadre (frame window) par CFrameWnd, et une vue par CView.

class CMainFrame : public CFrameWnd
{
public:
    CMainFrame();

protected:
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    DECLARE_MESSAGE_MAP()
};

MFC ne remplace pas entièrement l’API Win32 par autre chose : c’est un framework C++ fin mais de vaste portée, construit sur la logique de l’API Win32. Pour lire du code MFC, il faut donc, au-delà des classes MFC elles-mêmes, des connaissances dans ces domaines.

Les messages Windows
Les handles tels que HWND
GDI/GDI+
Les fichiers de ressources
COM/OLE
Les DLL et les runtimes
Les encodages de caractères
Les threads et la boucle de messages

Plutôt qu’une « bibliothèque magique permettant d’écrire du code sans connaître Windows », MFC est en réalité « les mécanismes de Windows organisés en types et en framework C++ ».

3. MFC est-il encore utilisable aujourd’hui ?

MFC reste aujourd’hui disponible dans Visual Studio. Mais attention à ne pas se méprendre sur sa position actuelle. Il continue certes d’être supporté, mais ce n’est pas un framework UI récent recevant activement de nouvelles fonctionnalités : la documentation MFC de Microsoft précise elle-même que MFC continue d’être supporté, mais qu’aucune nouvelle fonctionnalité ni mise à jour de documentation n’est prévue.

La position de MFC se résume donc à peu près ainsi.

Maintenance d'une application MFC existante               -> réaliste et courant
Ajout de fonctionnalités à une application MFC existante  -> envisageable
Mise à jour de l'environnement de build d'une application MFC -> important
Migration progressive de MFC vers une autre UI             -> envisageable
Adoption pour une application GUI totalement nouvelle      -> décision à prendre avec prudence

En particulier, dans les applications métier utilisées de longue date, l’UI, l’impression, les entrées-sorties fichier, le contrôle de périphériques, les protocoles propriétaires ou l’intégration COM se retrouvent souvent regroupés à l’intérieur de MFC.

Pour ce type de base de code, avant de « se débarrasser de MFC », il faut d’abord « être capable de lire MFC ».

4. Les domaines où MFC excellait

Le domaine emblématique où MFC a été utilisé est celui des applications de bureau natives Windows.

Concrètement, il s’agit d’applications comme celles-ci.

Outils métier centrés sur des boîtes de dialogue
Applications SDI qui ouvrent et modifient des fichiers
Applications MDI qui gèrent plusieurs documents
Écrans de contrôle d'instruments de mesure ou d'équipements de fabrication
Applications natives de type CAD/CAM
Applications faisant un usage intensif de l'impression et de l'aperçu
Applications intégrant ActiveX ou OLE
Applications étroitement liées à d'anciennes API Windows ou à des actifs COM

La force de MFC est de fonctionner tout près des composants natifs de Windows. Fenêtres, menus, barres d’outils, barres d’état, boîtes de dialogue, contrôles communs, impression, boîtes de dialogue de fichiers, Registre, dessin GDI : tout cela peut être manipulé comme des classes C++.

En contrepartie, la faiblesse de MFC est que la construction d’UI moderne, la liaison de données (data binding), la testabilité, le traitement asynchrone, les dispositions modernes, la prise en charge du haut DPI, l’internationalisation ou l’accessibilité ne s’expriment pas aussi naturellement que dans les frameworks récents.

En résumé, ses caractéristiques sont les suivantes.

Proche du natif Windows
Contrôle direct depuis C++
Une grande quantité d'actifs existants
Connaissances Win32 nécessaires
Beaucoup de conventions anciennes
La structure testable doit être organisée soi-même

5. Se préparer à utiliser MFC dans Visual Studio

Même en ayant installé C++ dans Visual Studio, MFC n’est pas forcément inclus. MFC est en effet géré comme un composant individuel de Visual Studio Installer. On vérifie typiquement les composants suivants.

Développement Desktop en C++
MSVC v143 - VS 2022 C++ x64/x86 build tools
Windows SDK
C++ MFC for latest v143 build tools
C++ ATL for latest v143 build tools
Si la version MFC avec Spectre Mitigations est nécessaire

Si des fichiers liés à MFC sont introuvables à la compilation, il faut vérifier non seulement les réglages du projet, mais aussi si le composant MFC est bien installé côté Visual Studio.

C’est la même chose pour les environnements de CI ou les serveurs de build : si le projet compile en local sous Visual Studio mais échoue sur la CI, la cause peut être l’absence du composant MFC ou une différence de version du toolset ciblé.

6. La structure de base d’une application MFC

Une application MFC a en général la structure suivante.

Classe dérivée de CWinApp
  Prend en charge l'initialisation et la fermeture de l'application dans son ensemble

Classes dérivées de CFrameWnd / CMDIFrameWnd / CDialog
  Prennent en charge la fenêtre principale et les boîtes de dialogue

Classes dérivées de CView
  Prennent en charge l'affichage à l'écran et les interactions utilisateur

Classes dérivées de CDocument
  Prennent en charge les données et l'enregistrement des fichiers

Fichiers de ressources
  Contiennent les menus, boîtes de dialogue, icônes, chaînes de caractères, etc.

Cartes de messages
  Relient les messages Windows et les commandes à des fonctions gestionnaires

Par exemple, une application MFC simple contient une classe dérivée de CWinApp de ce genre.

class CMyApp : public CWinApp
{
public:
    virtual BOOL InitInstance();
};

CMyApp theApp;

BOOL CMyApp::InitInstance()
{
    CWinApp::InitInstance();

    CMainFrame* pFrame = new CMainFrame;
    m_pMainWnd = pFrame;

    pFrame->Create(nullptr, _T("My MFC Application"));
    pFrame->ShowWindow(SW_SHOW);
    pFrame->UpdateWindow();

    return TRUE;
}

CWinApp est la classe représentant l’application dans son ensemble. Dans une application MFC, il existe normalement un seul objet dérivé de CWinApp.

Un objet global comme ce theApp peut sembler étrange au premier abord, mais c’est la structure standard en MFC.

7. Que fait CWinApp ?

CWinApp joue un rôle essentiel en tant que point d’entrée d’une application MFC. Dans une application Win32 classique, on écrit soi-même WinMain, l’enregistrement de la classe de fenêtre et la boucle de messages, mais en MFC, le framework prend en charge l’essentiel de tout cela. Le développeur se contente le plus souvent de surcharger InitInstance pour y écrire l’initialisation propre à l’application.

BOOL CMyApp::InitInstance()
{
    CWinApp::InitInstance();

    // Chargement des réglages
    // Initialisation de COM
    // Création de la fenêtre principale
    // Enregistrement des modèles de document

    return TRUE;
}

Voici les traitements que l’on écrit couramment dans InitInstance.

Initialisation des contrôles communs
Configuration de la clé de Registre
Chargement de la liste des fichiers récemment utilisés
Enregistrement des modèles de document
Création de la fenêtre principale (main frame)
Traitement des arguments de ligne de commande
Initialisation de COM/OLE

Lors de la maintenance, examiner d’abord la classe dérivée de CWinApp permet de voir plus facilement l’ordre de démarrage global de l’application.

8. CWnd, la classe au centre de MFC

La plupart des classes UI de MFC dérivent de CWnd, qui représente une fenêtre Windows. Un objet CWnd et un HWND, cependant, ne sont pas la même chose.

HWND
  Le handle de fenêtre géré par le système d'exploitation Windows

CWnd
  L'objet wrapper C++ qui facilite la manipulation du HWND

En MFC, CWnd détient un HWND en interne.

HWND hWnd = m_hWnd;

Ou bien on l’obtient de la manière suivante.

HWND hWnd = GetSafeHwnd();

Ce qui importe lors de la maintenance, c’est que même quand un CWnd* existe, le HWND correspondant peut déjà avoir été détruit.

On vérifie donc la validité d’une fenêtre de cette manière.

if (pWnd != nullptr && ::IsWindow(pWnd->GetSafeHwnd()))
{
    pWnd->ShowWindow(SW_SHOW);
}

Dans l’investigation de bugs MFC, il est important de vérifier si la durée de vie de l’objet C++ CWnd et celle du handle de fenêtre Windows réel ne sont pas décalées.

9. Qu’est-ce qu’une carte de messages ?

L’un des mécanismes où le caractère propre à MFC transparaît le plus est la carte de messages (message map).

Une application Windows reçoit les clics de souris, les saisies clavier, les demandes de réaffichage, les redimensionnements de fenêtre, les sélections de menu, etc., sous forme de messages Windows.

Avec l’API Win32, on traite normalement les messages dans une instruction switch au sein de WndProc.

En MFC, la carte de messages relie cela à des fonctions gestionnaires.

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_BN_CLICKED(IDC_BUTTON_OK, &CMyDialog::OnClickedButtonOk)
    ON_WM_CLOSE()
END_MESSAGE_MAP()

void CMyDialog::OnClickedButtonOk()
{
    AfxMessageBox(_T("Clicked"));
}

Le sens de ce code est le suivant.

Quand le bouton IDC_BUTTON_OK est cliqué
appeler CMyDialog::OnClickedButtonOk

Sans habitude de lecture de MFC, il est difficile de voir d’où une fonction est appelée. Si une recherche ne révèle aucun appel direct, il faut regarder la carte de messages.

La fonction n'est appelée nulle part directement
Mais elle s'exécute lors d'un événement
-> Vérifier les macros BEGIN_MESSAGE_MAP / ON_...

Dans une revue de code MFC, il est important de ne pas se limiter à la fonction gestionnaire, mais de la vérifier conjointement avec la carte de messages.

10. Le routage des commandes

En MFC, les actions sur les menus et les barres d’outils sont elles aussi traitées comme des commandes.

ON_COMMAND en est l’exemple représentatif.

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
END_MESSAGE_MAP()

void CMainFrame::OnFileOpen()
{
    // Ouverture du fichier
}

MFC dispose d’un mécanisme qui achemine les commandes vers l’objet approprié.

Par exemple, un même ID_EDIT_COPY peut être traité tantôt par la vue active, tantôt par le document, la fenêtre de cadre ou l’application, selon le contexte.

Vue active
Document
Fenêtre de cadre (frame window)
Application

La commande est ainsi transmise dans cet ordre à l’objet capable de la traiter.

C’est pourquoi, en MFC, il peut être difficile de suivre « quelle fonction est appelée quand on clique sur tel menu » avec une simple recherche de texte.

Voici les points à examiner lors de la maintenance.

Quel est l'ID de commande ?
Dans quelle classe se trouve le ON_COMMAND ?
Où se trouve le ON_UPDATE_COMMAND_UI ?
Quelle est la vue actuellement active ?
L'application utilise-t-elle la structure Document/View ?

11. Qu’est-ce que ON_UPDATE_COMMAND_UI ?

En MFC, ON_UPDATE_COMMAND_UI est parfois utilisé pour mettre à jour l’état actif/inactif, l’état de coche ou le texte affiché d’un élément de menu ou d’un bouton de barre d’outils.

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_COMMAND(ID_EDIT_DELETE, &CMainFrame::OnEditDelete)
    ON_UPDATE_COMMAND_UI(ID_EDIT_DELETE, &CMainFrame::OnUpdateEditDelete)
END_MESSAGE_MAP()

void CMainFrame::OnUpdateEditDelete(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(CanDeleteCurrentItem());
}

Cela permet de n’activer le menu ou le bouton que lorsque la suppression est réellement possible.

Lorsqu’on utilise une application MFC et qu’on observe un bouton mystérieusement grisé, un menu impossible à cliquer, ou un état de coche qui change, chercher ON_UPDATE_COMMAND_UI permet souvent de trouver la cause.

12. Les applications MFC basées sur des boîtes de dialogue

L’une des formes les plus faciles à comprendre en MFC est l’application basée sur une boîte de dialogue (dialog-based).

Les écrans de configuration, les outils métier simples ou les écrans de pilotage d’équipement sont souvent construits autour d’une boîte de dialogue.

On hérite typiquement de CDialog ou de CDialogEx.

class CSettingsDialog : public CDialogEx
{
public:
    CSettingsDialog(CWnd* pParent = nullptr);

#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_SETTINGS_DIALOG };
#endif

protected:
    virtual void DoDataExchange(CDataExchange* pDX);
    virtual BOOL OnInitDialog();

    afx_msg void OnBnClickedOk();
    DECLARE_MESSAGE_MAP()

private:
    CString m_name;
    int m_interval;
};

Dans du code basé sur une boîte de dialogue, les éléments suivants reviennent fréquemment.

IDD_...        ID de ressource de la boîte de dialogue
IDC_...        ID de contrôle
OnInitDialog   traitement d'initialisation
DoDataExchange association des contrôles et des variables membres
UpdateData     synchronisation entre l'écran et les variables
ON_BN_CLICKED  traitement du clic sur un bouton

Une boîte de dialogue ne se résume pas à son apparence : elle fonctionne grâce à la combinaison des ressources, des variables membres, de la carte de messages et du traitement d’initialisation.

13. DDX et DDV

Deux éléments qui reviennent constamment dans les boîtes de dialogue MFC sont DDX et DDV.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX est le mécanisme qui associe les contrôles d’une boîte de dialogue aux variables membres C++, et DDV est le mécanisme qui valide les valeurs saisies.

void CSettingsDialog::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Text(pDX, IDC_EDIT_NAME, m_name);
    DDX_Text(pDX, IDC_EDIT_INTERVAL, m_interval);
    DDV_MinMaxInt(pDX, m_interval, 1, 3600);
}

Appeler UpdateData(TRUE) reporte les valeurs saisies à l’écran dans les variables membres.

void CSettingsDialog::OnBnClickedOk()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    // À ce stade, m_name et m_interval contiennent les valeurs saisies à l'écran
    SaveSettings(m_name, m_interval);

    CDialogEx::OnOK();
}

À l’inverse, appeler UpdateData(FALSE) reporte les valeurs des variables membres vers l’écran.

BOOL CSettingsDialog::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_name = _T("default");
    m_interval = 60;
    UpdateData(FALSE);

    return TRUE;
}

Quand une valeur saisie se comporte mal dans une boîte de dialogue MFC, on vérifie les points suivants.

Le DDX est-il défini dans DoDataExchange ?
UpdateData(TRUE) est-il appelé ?
Le moment où UpdateData(FALSE) est appelé est-il correct ?
La saisie est-elle rejetée par DDV ?
Les ID de contrôle correspondent-ils aux ressources ?

14. L’architecture Document/View

L’une des grandes caractéristiques de MFC est l’architecture Document/View.

C’est une structure destinée à séparer les données manipulées par l’application de leur affichage.

CDocument
  Détient les données
  Prend en charge la lecture et l'écriture des fichiers
  Notifie les mises à jour aux différentes vues

CView
  Affiche les données
  Gère les interactions utilisateur
  Gère le dessin et l'état de sélection

Par exemple, dans des applications comme un éditeur de texte, un éditeur de formes, un outil d’édition de fichiers de configuration ou un CAD, séparer les données de leur affichage a du sens.

class CMyDocument : public CDocument
{
public:
    std::vector<Item> m_items;

    virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
    virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
};

class CMyView : public CView
{
protected:
    virtual void OnDraw(CDC* pDC);

    CMyDocument* GetDocument() const;
};

Du côté de la vue, on récupère le document et on dessine.

void CMyView::OnDraw(CDC* pDC)
{
    CMyDocument* pDoc = GetDocument();
    if (pDoc == nullptr)
    {
        return;
    }

    for (const auto& item : pDoc->m_items)
    {
        // Dessiner avec pDC
    }
}

L’avantage de Document/View est de faciliter l’affichage des mêmes données dans plusieurs vues.

Par exemple, les mêmes données peuvent être présentées de ces différentes façons.

Vue sous forme de tableau
Vue graphique
Vue détaillée
Aperçu
Vue d'impression

Cependant, appliquer Document/View à un simple écran de configuration ou à un petit outil peut, à l’inverse, alourdir inutilement la structure.

Lors de la maintenance, déterminer d’abord si l’application utilise Document/View ou si elle est centrée sur des boîtes de dialogue facilite grandement le suivi du code.

15. SDI et MDI

En MFC, les structures SDI et MDI apparaissent souvent en combinaison avec Document/View.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI est, par essence, la forme où une seule fenêtre de cadre gère un seul document.

Fenêtre principale
  Un document
  Une ou plusieurs vues

MDI est la forme où une fenêtre parente contient plusieurs fenêtres enfants, chacune gérant son propre document.

Fenêtre de cadre parente MDI
  Fenêtre de cadre enfant MDI 1 -> document 1
  Fenêtre de cadre enfant MDI 2 -> document 2
  Fenêtre de cadre enfant MDI 3 -> document 3

Les anciennes applications Windows utilisaient fréquemment MDI.

Lors de la maintenance, les noms de classe donnent une bonne idée de la structure.

CFrameWnd          fenêtre de cadre de type SDI
CMDIFrameWnd       fenêtre de cadre parente MDI
CMDIChildWnd       fenêtre de cadre enfant MDI
CSingleDocTemplate modèle de document pour SDI
CMultiDocTemplate  modèle de document pour MDI

Dans une application générée par l’assistant MFC, InitInstance contient souvent l’enregistrement de CSingleDocTemplate ou CMultiDocTemplate.

16. Comprendre les fichiers de ressources

Dans les applications MFC, le fichier .rc est extrêmement important.

.rc est un fichier de ressources Windows.

On y définit des éléments comme ceux-ci.

Modèles de boîtes de dialogue
Menus
Touches accélératrices
Icônes
Bitmaps
Tables de chaînes de caractères
Informations de version
Barres d'outils

Par ailleurs, resource.h définit les ID de ressources.

#define IDD_SETTINGS_DIALOG  101
#define IDC_EDIT_NAME        1001
#define IDC_EDIT_INTERVAL    1002
#define ID_FILE_OPEN         32771

Le code MFC utilise ces ID pour relier les ressources au code C++.

DDX_Text(pDX, IDC_EDIT_NAME, m_name);
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)

Un problème fréquent lors de la maintenance est une incohérence des ID de ressources.

Un ID dans resource.h a changé
Une fusion d'une autre branche a provoqué une collision d'ID
L'ID du contrôle sur la boîte de dialogue ne correspond pas à celui du DDX
Un ID de menu censé avoir été supprimé subsiste encore
Des ID de la table de chaînes de caractères sont dupliqués

Pour investiguer le comportement d’une application MFC, il faut examiner en même temps le fichier .rc et resource.h, pas seulement le code C++.

17. Le Class Wizard et le code écrit à la main

MFC a une histoire profondément liée au Class Wizard de Visual Studio.

Le Class Wizard permet de générer automatiquement des gestionnaires de messages, des variables DDX, des surcharges de fonctions virtuelles, et bien d’autres éléments.

C’est pourquoi le code MFC conserve souvent des traces abondantes de code généré par l’outil.

//{{AFX_DATA(CSettingsDialog)
//}}AFX_DATA

//{{AFX_MSG(CSettingsDialog)
//}}AFX_MSG

Dans les versions récentes de Visual Studio, l’apparence et la forme générée peuvent différer, mais les anciennes bases de code conservent parfois ce genre de marqueurs de commentaire.

Ce qui compte lors de la maintenance, c’est de ne pas rompre négligemment la frontière entre code généré et code écrit à la main.

Ne pas supprimer la carte de messages
Ne pas casser les associations DDX
Ne pas modifier les ID de ressources à la légère
Ne pas effacer sans réfléchir les anciens commentaires prévus pour le Class Wizard

En MFC, il ne suffit pas que le code compile simplement en tant que C++. Il faut aussi préserver, dans une certaine mesure, la forme attendue par l’éditeur de ressources et le Class Wizard de Visual Studio.

18. CString et les chaînes de caractères

La classe de chaînes de caractères qui revient constamment en MFC est CString.

CString name = _T("Komura");
CString message;
message.Format(_T("Hello, %s"), name.GetString());

CString est une classe de chaînes de longueur variable très utilisée dans le code de type MFC/ATL. Le C++ moderne privilégie souvent std::string ou std::wstring, mais en MFC, CString est largement utilisé en raison de sa bonne compatibilité avec les API et les contrôles.

Lors de la maintenance, il faut garder à l’esprit les encodages de caractères.

CString      équivalent à CStringA ou CStringW selon les réglages du projet
CStringA     famille ANSI / MBCS
CStringW     famille Unicode / UTF-16
LPCTSTR      pointeur de chaîne basé sur TCHAR
LPCSTR       famille char
LPCWSTR      famille wchar_t
std::string  généralement famille char
std::wstring famille wchar_t

Dans les applications Windows actuelles, il est fondamentalement plus sûr de partir du principe Unicode, mais les anciennes applications MFC conservent parfois du code qui suppose MBCS.

CString text = _T("日本語");
std::wstring ws(text.GetString());

Les bugs de conversion de chaînes reviennent fréquemment dans la maintenance des applications MFC.

Il faut particulièrement se méfier des cas suivants.

Lire un fichier qui suppose Shift_JIS
Basculer vers une build Unicode
Une DLL externe qui exige un char*
COM qui exige un BSTR
Une conversion négligente vers std::string qui produit du texte corrompu

En voyant un CString, il ne faut pas se contenter de penser « c’est une vieille classe de chaînes » : il est important de le vérifier conjointement avec les réglages de jeu de caractères du projet, les API externes et les formats de fichier.

19. CFile et CArchive

MFC comprend aussi des classes dédiées aux opérations sur fichiers et à la sérialisation. Les plus représentatives sont CFile et CArchive.

CFile file;
if (file.Open(path, CFile::modeRead))
{
    CArchive ar(&file, CArchive::load);
    // Lire depuis ar
}

CArchive est largement utilisé dans le mécanisme de sérialisation de MFC.

Dans une classe dérivée de CDocument, on surcharge souvent Serialize pour écrire la lecture et l’enregistrement dans la même fonction.

void CMyDocument::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        ar << m_title;
        ar << static_cast<int>(m_items.size());
        for (const auto& item : m_items)
        {
            ar << item.Name;
            ar << item.Value;
        }
    }
    else
    {
        int count = 0;
        ar >> m_title;
        ar >> count;
        m_items.clear();
        for (int i = 0; i < count; ++i)
        {
            Item item;
            ar >> item.Name;
            ar >> item.Value;
            m_items.push_back(item);
        }
    }
}

La sérialisation MFC est pratique, mais elle demande de la prudence sur le long terme.

Compatibilité avec les anciens formats de fichier
Gestion des numéros de version
Récupération en cas d'échec de lecture
Gestion des exceptions
Encodage des caractères
Boutisme (endianness)
Le fait d'enregistrer une structure telle quelle ou non

Dans une application MFC utilisant depuis longtemps un format binaire propriétaire, Serialize en vient parfois à constituer de facto la spécification du fichier.

Dans ce cas, avant toute modification du code, il vaut mieux impérativement préparer des données de test permettant de lire les fichiers existants.

20. Le dessin GDI et CDC

Pour le dessin à l’écran en MFC, la classe CDC, qui gère le Device Context de Windows, est très utilisée. CView::OnDraw reçoit un CDC* en argument.

void CMyView::OnDraw(CDC* pDC)
{
    pDC->TextOut(10, 10, _T("Hello MFC"));
    pDC->Rectangle(10, 40, 200, 120);
}

Lors de l’utilisation de stylos (pens) ou de pinceaux (brushes), il faut faire attention à la sélection et à la restauration.

void CMyView::OnDraw(CDC* pDC)
{
    CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
    CPen* pOldPen = pDC->SelectObject(&pen);

    pDC->MoveTo(10, 10);
    pDC->LineTo(100, 100);

    pDC->SelectObject(pOldPen);
}

Autour des objets GDI, les erreurs suivantes posent problème.

Ne pas restaurer l'objet d'origine après un SelectObject
Créer un grand nombre d'objets GDI sans les détruire
Confondre les rôles de OnPaint et de OnDraw
Un scintillement dû à l'absence de double buffering
Un dessin supposant des pixels fixes qui se déforme en haut DPI

Pour un bug de dessin MFC, il faut vérifier non seulement la logique C++, mais aussi les ressources GDI de Windows, le moment du réaffichage, le DPI et la taille des polices.

21. Boîtes de dialogue modales et non modales

En MFC, la manière d’afficher une boîte de dialogue mérite aussi une attention particulière.

Une boîte de dialogue modale s’affiche avec DoModal.

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // Traitement du OK
}

Dans ce cas, l’appelant attend que la boîte de dialogue se ferme.

À l’inverse, avec une boîte de dialogue non modale (modeless), le contrôle revient à l’appelant dès la création.

m_pToolDialog = new CToolDialog(this);
m_pToolDialog->Create(IDD_TOOL_DIALOG, this);
m_pToolDialog->ShowWindow(SW_SHOW);

Pour une boîte de dialogue non modale, la gestion de la durée de vie est essentielle.

Quand supprimer (delete) la boîte de dialogue créée avec new ?
La fenêtre parente n'est-elle pas détruite en premier ?
La boîte de dialogue utilise-t-elle PostNcDestroy ?
N'est-elle pas créée deux fois ?
Le pointeur ne reste-t-il pas actif après la fermeture ?

Dans une investigation de crash MFC, un problème de durée de vie d’une boîte de dialogue non modale peut en être la cause.

22. La durée de vie des objets C++ et des handles Windows

Un point extrêmement important en MFC est la différence de durée de vie entre un objet C++ et un handle Windows. CWnd, par exemple, est un objet C++, mais la fenêtre réelle est gérée par Windows sous forme de HWND : les deux ne sont pas toujours créés et détruits en même temps.

L'objet CWnd existe mais le HWND n'existe pas encore
Le HWND a été détruit mais l'objet CWnd subsiste
Un wrapper CWnd temporaire a été créé
Le handle a été réattaché via Attach/Detach

Par exemple, un code comme celui-ci mérite une attention particulière.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// Stocker pWnd dans un membre pour l'utiliser plus tard

Si l’on conserve longtemps un pointeur obtenu via GetDlgItem, on risque de le référencer après la destruction de la fenêtre.

Si nécessaire, il peut être plus sûr d’appeler GetDlgItem à chaque fois, ou de gérer une variable membre pour le contrôle via DDX.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

En MFC, un pointeur non nul n’est pas forcément sûr.

if (m_pDialog != nullptr && ::IsWindow(m_pDialog->GetSafeHwnd()))
{
    m_pDialog->SetWindowText(_T("Running"));
}

Ce réflexe est extrêmement important dans la maintenance MFC.

23. Threads et mise à jour de l’UI

L’UI Windows doit fondamentalement être manipulée depuis le thread UI qui l’a créée, et il en va de même pour les applications MFC. Manipuler directement des contrôles UI depuis un thread de travail (worker thread) provoque des comportements instables ou des crashes.

Voici un exemple à éviter.

UINT WorkerThreadProc(LPVOID pParam)
{
    CMyDialog* pDlg = static_cast<CMyDialog*>(pParam);

    // Éviter de manipuler l'UI directement depuis un thread de travail
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

En général, on notifie le thread UI via, par exemple, PostMessage.

constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;

UINT WorkerThreadProc(LPVOID pParam)
{
    HWND hWnd = static_cast<HWND>(pParam);

    // Traitement lourd

    ::PostMessage(hWnd, WM_APP_WORK_DONE, 0, 0);
    return 0;
}

Côté UI, on reçoit cela via la carte de messages.

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_MESSAGE(WM_APP_WORK_DONE, &CMyDialog::OnWorkDone)
END_MESSAGE_MAP()

LRESULT CMyDialog::OnWorkDone(WPARAM, LPARAM)
{
    SetDlgItemText(IDC_STATUS, _T("Done"));
    return 0;
}

Autour des threads en MFC, on vérifie les points suivants.

L'UI n'est-elle pas manipulée directement depuis un thread de travail ?
PostMessage n'est-il pas appelé après la destruction de la fenêtre ?
Le thread UI n'est-il pas bloqué en attendant la fin d'un thread ?
Le verrouillage des données partagées est-il approprié ?
La valeur de retour et la durée de vie de AfxBeginThread ne sont-elles pas mal comprises ?

24. Les DLL MFC et l’état de module

Quand on crée une DLL avec MFC, la notion d’état de module (module state) entre en jeu.

Charger des ressources depuis une DLL MFC, afficher une boîte de dialogue, ou créer une DLL d’extension : dans ces situations, la question se pose de savoir dans quel module aller chercher les ressources.

On rencontre parfois cette macro au point d’entrée des fonctions d’une DLL MFC.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

Elle permet à MFC d’utiliser le bon état de module.

Oublier cette macro entraîne des dysfonctionnements comme ceux-ci.

Les ressources de boîte de dialogue de la DLL sont introuvables
Les ressources de chaînes de caractères sont lues depuis un autre module
Les icônes ou les menus sont introuvables
Cela fonctionne en débogage mais casse en release

Lors de la maintenance d’une DLL MFC, il est important de clarifier les relations entre l’EXE, les DLL ordinaires, les DLL d’extension et les DLL de ressources.

Il faut être particulièrement prudent lorsqu’une application non-MFC appelle une DLL MFC, ou lorsque l’architecture est de type plugin.

25. Lier MFC statiquement ou utiliser la DLL partagée

Dans les projets MFC, les réglages de projet comportent une option « Use of MFC ».

Les deux options principales sont les suivantes.

Use MFC in a Shared DLL
Use MFC in a Static Library

En utilisant la DLL partagée, l’environnement d’exécution a besoin du runtime MFC et du runtime Visual C++ correspondants.

Avec la liaison statique, la distribution peut sembler plus simple, mais il faut prendre en compte la taille de l’exécutable, les mises à jour, l’intégration des correctifs de sécurité, ainsi que les conditions de licence et de redistribution.

Aucune des deux options n’est systématiquement la bonne réponse.

Voici les éléments à prendre en compte pour décider.

Peut-on installer le Visual C++ Redistributable sur le poste cible ?
Souhaite-t-on rapprocher l'application d'un exécutable unique ?
Comment appliquer les mises à jour de sécurité ?
Plusieurs applications partagent-elles le même runtime ?
Peut-on fournir un installateur ?
Quelle version de Windows est ciblée ?

En maintenance, il est important de vérifier d’abord le réglage actuel.

Configuration Properties
  General
    Use of MFC

Il faut également vérifier le réglage Runtime Library.

/MD   Multi-threaded DLL
/MDd  Multi-threaded Debug DLL
/MT   Multi-threaded
/MTd  Multi-threaded Debug

Si les réglages de liaison de MFC et du CRT sont mélangés, des problèmes d’allocation/libération de mémoire peuvent apparaître aux frontières des bibliothèques.

26. Unicode, MBCS et TCHAR

Dans le code MFC ancien, TCHAR, LPCTSTR et la macro _T() reviennent fréquemment.

CString title = _T("設定");
SetWindowText(title);

C’est une manière d’écrire compatible à la fois avec les builds Unicode et les builds MBCS.

Build Unicode
  TCHAR    -> wchar_t
  LPCTSTR  -> const wchar_t*
  _T("...") -> L"..."

Build MBCS
  TCHAR    -> char
  LPCTSTR  -> const char*
  _T("...") -> "..."

Les builds Unicode sont aujourd’hui la norme, mais les anciennes applications conservent parfois des traitements qui supposent MBCS.

En particulier pour les fichiers externes, les protocoles de communication, les anciennes DLL, les connexions à des bases de données ou les communications série, il faut vérifier les hypothèses d’encodage de caractères.

Il faut se garder de considérer l’unicodisation comme un simple travail de remplacement.

La taille d'un tableau char est-elle exprimée en octets ou en caractères ?
strlen est-il utilisé ?
sizeof(buffer) est-il utilisé comme un nombre de caractères ?
L'API externe accepte-t-elle de l'UTF-16 ou du Shift_JIS ?
Est-il acceptable que le format d'enregistrement du fichier change ?

En corrigeant la gestion des chaînes de caractères en MFC, il faut vérifier non seulement l’affichage à l’écran, mais aussi la compatibilité des fichiers et les intégrations externes.

27. MFC et COM/OLE/ActiveX

MFC a aussi été utilisé dans des applications étroitement liées à COM, OLE et ActiveX.

Les anciennes applications métier conservent parfois des éléments comme ceux-ci.

OLE Automation
Contrôle ActiveX
Serveur COM
Client COM
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

Le traitement de démarrage d’une application MFC peut contenir du code comme celui-ci.

if (!AfxOleInit())
{
    AfxMessageBox(_T("OLE initialization failed"));
    return FALSE;
}

Quand COM/OLE est utilisé, ce qui ressemble à un problème MFC peut en réalité être causé par l’initialisation de COM, le modèle de threading, le comptage de références, les informations d’enregistrement, ou des différences entre 32 bits et 64 bits.

Il faut particulièrement se méfier des composants ActiveX ou COM en 32 bits.

Une application MFC 32 bits utilise du COM 32 bits
Une application MFC 64 bits utilise du COM 64 bits
Les enregistrements COM en 32 bits et en 64 bits sont distincts
D'anciens composants ActiveX peuvent ne pas être compatibles 64 bits

Lors du portage d’une application MFC vers x64, il faut impérativement vérifier non seulement le code UI, mais aussi les dépendances COM/OLE.

28. Le support du haut DPI et le Windows moderne

Faire fonctionner une ancienne application MFC sur un Windows moderne peut entraîner un affichage dégradé dans un environnement à haut DPI.

Voici, par exemple, le genre de problèmes rencontrés.

Le texte est tronqué
Les boutons sont trop petits
Le dessin en pixels fixes est décalé
L'affichage se casse quand plusieurs moniteurs ont des taux d'agrandissement différents
Les anciens bitmaps apparaissent flous
La disposition de la boîte de dialogue est trop compressée

Dans une application de bureau Windows, l’application doit déclarer explicitement son mode de prise en charge du DPI.

Pour une application MFC aussi, il faut vérifier le manifeste, les ressources, le code de dessin, les polices et la disposition.

En particulier, un code comme celui-ci, qui écrit en dur des coordonnées fixes, pose facilement problème en haut DPI.

pDC->TextOut(10, 10, _T("Status"));
pDC->Rectangle(10, 40, 200, 80);

Des coordonnées supposant des pixels fixes se déforment visuellement dès que le DPI change.

Voici les points à vérifier lors de la maintenance.

Le réglage DPI du manifeste d'application
La police des ressources de boîte de dialogue
Le dessin en pixels fixes
La résolution des ressources d'image
Le comportement avec plusieurs moniteurs
L'affichage sous Windows 10 / Windows 11

Le support du haut DPI en MFC ne se limite pas nécessairement à modifier les réglages du projet. Pour une interface ancienne, une vérification concrète à l’écran et des corrections de disposition sont souvent nécessaires.

29. Gestion des exceptions et des erreurs

MFC possède ses propres classes d’exceptions et ses propres conventions de gestion des erreurs.

Dans le code ancien, on rencontre des macros comme celles-ci.

TRY
{
    // Traitement
}
CATCH(CFileException, e)
{
    e->ReportError();
}
END_CATCH

Il existe aussi du code où cela se mélange avec le try / catch du C++ moderne.

try
{
    DoSomething();
}
catch (const std::exception& ex)
{
    // Écriture dans le journal
}

Voici les points à surveiller dans la maintenance MFC.

Les exceptions MFC et les exceptions standard C++ ne sont-elles pas mélangées ?
La durée de vie de l'objet d'exception n'est-elle pas mal comprise ?
Les anciennes macros THROW/CATCH sont-elles bien comprises ?
Les erreurs par valeur de retour et les exceptions ne sont-elles pas mélangées ?
Les erreurs ne se limitent-elles pas à AfxMessageBox sans laisser aucune trace dans un journal ?

Dans une application métier, il est important de ne pas se contenter d’afficher un message d’erreur à l’écran, mais de conserver aussi le journal, l’historique des opérations, les valeurs saisies et l’état des connexions externes.

Dans une ancienne application MFC, il arrive que la gestion d’une erreur se limite à un simple AfxMessageBox.

AfxMessageBox(_T("Échec de l'enregistrement."));

Pour améliorer la maintenabilité, il vaut mieux séparer l’affichage UI de l’enregistrement dans le journal.

LogError(_T("Échec de l'enregistrement"), path);
AfxMessageBox(_T("Échec de l'enregistrement. Veuillez consulter le journal."));

30. Faire coexister MFC et le C++ moderne

Ce n’est pas parce qu’on assure la maintenance d’une application MFC qu’il faut aligner tout le code sur l’ancien style C++.

On peut respecter les conventions de MFC dans la couche UI, tout en organisant la logique métier et les calculs en C++ moderne.

On peut par exemple répartir ainsi.

Couche MFC
  CDialog
  CView
  CDocument
  CString
  Cartes de messages
  Manipulation des ressources

Couche non-MFC
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  Classes testables unitairement
  Logique métier

La mauvaise pratique consiste à entasser tous les traitements dans la classe de la boîte de dialogue.

void CMainDialog::OnBnClickedExecute()
{
    // Récupération des entrées
    // Lecture des fichiers
    // Communication
    // Calcul
    // Mise à jour de la base de données
    // Mise à jour de l'écran
    // Écriture des journaux
    // Gestion des exceptions
}

Un tel code devient difficile à modifier, difficile à tester, et difficile à investiguer en cas de bug.

Pour l’améliorer, on extrait la logique hors de la classe MFC.

void CMainDialog::OnBnClickedExecute()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    ExecuteRequest request;
    request.Name = ToStdWString(m_name);
    request.Interval = m_interval;

    ExecuteResult result = m_service.Execute(request);

    m_status = ToCString(result.Message);
    UpdateData(FALSE);
}

Ainsi, m_service.Execute peut être testé sans MFC.

L’amélioration la plus efficace pour la maintenance d’un actif MFC existant est d’extraire progressivement la logique hors des classes UI.

31. Rendre le code MFC testable

Une application MFC est souvent difficile à tester unitairement telle quelle.

La raison est que l’UI, Win32, les fichiers, les communications, la base de données et l’état global ont tendance à être fortement couplés.

Voici la démarche à suivre pour rendre le code testable.

Ne pas chercher à tester directement CDialog ou CView
Extraire d'abord la logique non-UI
Convertir les types MFC aux frontières
Passer les fichiers et les communications par des interfaces
Alléger les gestionnaires d'événements de l'écran

Par exemple, on déplace le traitement vers une classe C++ pure.

class PriceCalculator
{
public:
    int CalculateTotal(const std::vector<int>& prices) const
    {
        int total = 0;
        for (int price : prices)
        {
            total += price;
        }
        return total;
    }
};

Le côté MFC ne prend en charge que l’entrée et la sortie.

void CPriceDialog::OnBnClickedCalculate()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    std::vector<int> prices = ParsePrices(ToStdWString(m_input));
    int total = m_calculator.CalculateTotal(prices);

    m_result.Format(_T("%d"), total);
    UpdateData(FALSE);
}

Avec cette structure, PriceCalculator et ParsePrices peuvent être testés avec un framework de test C++ ordinaire.

Il n’est pas nécessaire de reconstruire l’ensemble de l’application MFC d’un coup : extraire simplement, pour commencer, les traitements testables des gestionnaires d’événements produit déjà un effet.

32. Fixer l’environnement de build

Dans la maintenance d’une application MFC, fixer l’environnement de build est important.

Dans une ancienne base de code, le résultat de la compilation peut varier selon les différences suivantes.

Version de Visual Studio
Version du toolset MSVC
Version du Windows SDK
Présence ou non des composants MFC/ATL
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
Liaison statique de MFC / DLL partagée
Réglage de la bibliothèque runtime
En-têtes précompilés

Dans une application MFC, de nombreuses dépendances se retrouvent parfois concentrées dans stdafx.h ou pch.h.

#include "framework.h"
#include "MyApp.h"

La compilation peut se casser parce qu’un seul fichier a des réglages différents, ou parce que les réglages des en-têtes précompilés sont décalés.

Dans un projet de maintenance, consigner ces informations dans le README facilite grandement la suite.

Version de Visual Studio nécessaire
Charges de travail et composants individuels nécessaires
Windows SDK nécessaire
Plateformes cibles
Mode de liaison de MFC
Procédure de build
Mode d'exécution de la CI
Manière de produire les livrables

Dépasser le stade du « ça compile sur mon PC » est la première étape de la maintenance MFC.

33. Compiler MFC sur la CI

Une application MFC peut elle aussi être compilée sur la CI.

Mais l’environnement de CI doit disposer du composant MFC.

En utilisant Visual Studio Build Tools, il faut installer en spécifiant les ID des composants MFC/ATL.

Voici les points à vérifier.

MFC est-il inclus dans les Build Tools ?
Le toolset ciblé correspond-il au projet ?
Le Windows SDK est-il installé ?
Les builds x86 et x64 sont-elles toutes deux vérifiées ?
Le compilateur de ressources fonctionne-t-il ?
Y a-t-il une étape de signature ?
La génération de l'installateur fait-elle aussi partie de la CI ?

Pour une application MFC, automatiser jusqu’aux tests d’UI n’est pas simple, mais automatiser au moins les éléments suivants reste utile.

Build Debug / Release
Build x86 / x64
Analyse statique
Tests unitaires
Création de l'installateur
Enregistrement du hash des livrables
Vérification des DLL dépendantes

Dans une maintenance à long terme, simplement préserver la capacité à compiler a déjà une grande valeur.

34. Où regarder pour déboguer une application MFC

Pour traquer un dysfonctionnement dans une application MFC, examiner les éléments dans cet ordre est efficace.

1. De quel écran s'agit-il ?
2. Est-ce une boîte de dialogue, une View ou une Frame ?
3. Quel est l'ID de ressource correspondant à l'action ?
4. Vers quelle fonction la carte de messages achemine-t-elle ?
5. Le sens de UpdateData est-il correct ?
6. Pour Document/View, quel est l'état du Document ?
7. Le routage de commande ne dévie-t-il pas vers une autre classe ?
8. Un thread de travail ne manipule-t-il pas l'UI ?
9. Une exception ou une erreur n'est-elle pas étouffée par un simple AfxMessageBox ?
10. Y a-t-il un problème d'initialisation ou de durée de vie propre à la build release ?

Par exemple, pour un dysfonctionnement du type « rien ne se passe quand on clique sur le bouton », on vérifie ici.

L'IDC du bouton est-il correct ?
Un ON_BN_CLICKED existe-t-il ?
La signature du gestionnaire est-elle correcte ?
La ressource de boîte de dialogue n'est-elle pas une autre que prévu ?
Le bouton n'est-il pas désactivé ?
UpdateData n'échoue-t-il pas en cours de traitement ?
Une exception n'est-elle pas étouffée ?

Pour « le menu ne peut pas être cliqué », voici les points à vérifier.

N'est-il pas désactivé par ON_UPDATE_COMMAND_UI ?
Les ID de commande ne sont-ils pas dupliqués ?
La vue active est-elle bien celle attendue ?
Où se trouve le gestionnaire : Frame, View, Document ou App ?

En MFC, les événements apparents et le traitement réel sont reliés par des macros et un routage : tant qu’on n’y est pas habitué, schématiser le chemin d’appel facilite la compréhension.

35. Les pièges courants

Voici un récapitulatif des pièges courants dans la maintenance MFC.

Rechercher uniquement les appels de fonction sans regarder la carte de messages
Supposer qu'un CWnd* non nul est valide
Confondre la durée de vie du HWND avec celle de l'objet C++
Se tromper de sens pour UpdateData(TRUE/FALSE)
Ne pas remarquer qu'un élément est désactivé via ON_UPDATE_COMMAND_UI
Ne pas remarquer une collision d'ID dans resource.h
Manipuler directement l'UI depuis un thread de travail
Produire du texte corrompu lors de la conversion entre CString et std::string
Casser du code supposant MBCS en le convertissant vers Unicode
Oublier AFX_MANAGE_STATE dans une DLL MFC
Casser du COM/ActiveX supposant x86 lors d'un portage x64
Mal gérer la sélection/libération des objets GDI
Voir une disposition en coordonnées fixes se casser en haut DPI

Un dysfonctionnement MFC peut rester invisible en n’examinant que la syntaxe C++ : il faut aussi regarder les messages Windows, les ressources, les handles, les modules et les réglages du runtime.

36. Faut-il choisir MFC pour un nouveau développement ?

Choisir MFC pour un développement entièrement nouveau est une décision qui mérite d’être prise avec prudence.

S’il existe des raisons de choisir MFC, ce sont les suivantes.

Une intégration étroite avec du code MFC existant est nécessaire
On souhaite réutiliser des composants ou des écrans MFC existants
Un contrôle très proche de Win32/GDI/COM est nécessaire
L'entreprise dispose déjà de compétences suffisantes en maintenance MFC
La cible se limite au bureau Windows
Le plan de migration à long terme exige de d'abord étendre l'existant en MFC

À l’inverse, dans les cas suivants, il vaut mieux envisager une autre option.

On souhaite créer une UI moderne
Une disposition flexible ou des animations sont nécessaires
L'intégration web ou cloud est au centre du projet
On souhaite privilégier la testabilité
On veut choisir une technologie facile d'accès pour les jeunes développeurs
Le support multiplateforme est nécessaire
On souhaite privilégier dès le départ l'accessibilité et le haut DPI

MFC n’est ni une technologie « qu’il ne vaut plus la peine d’apprendre aujourd’hui », ni une technologie « à choisir parce qu’elle est nouvelle » : c’est une technologie destinée à affronter les actifs natifs Windows existants.

37. Comment penser la migration hors de MFC

Si l’on souhaite migrer une application MFC vers une autre technologie, viser d’emblée une refonte complète mène facilement à l’échec.

On commence par décomposer l’application ainsi.

UI
Logique métier
Format de fichier
Traitement des communications
Traitement de la base de données
Contrôle de périphériques
Impression
Intégration COM/OLE
Gestion de la configuration
Journalisation

Parmi ces éléments, celui qui dépend le plus de MFC est l’UI.

La logique métier et le traitement des fichiers, en revanche, peuvent potentiellement être extraits.

L’ordre réaliste de la migration se présente ainsi.

1. Reproduire l'environnement de build
2. Figer le comportement existant avec des données de test
3. Extraire la logique des gestionnaires d'événements UI
4. La déplacer vers une bibliothèque C++ non-MFC
5. Ajouter des tests automatisés
6. Documenter les spécifications externes
7. Remplacer progressivement les écrans qui en ont besoin

Plutôt que de viser « abandonner MFC » comme un objectif en soi, viser « faire sortir la logique importante enfermée dans MFC » facilite davantage la réussite.

38. Points d’entrée pour lire du code MFC

Pour lire un projet MFC existant pour la première fois, on commence par examiner des fichiers comme ceux-ci.

*.vcxproj
  Examiner le toolset, les réglages MFC, le jeu de caractères, les réglages du runtime

resource.h
  Examiner les ID de ressources

*.rc
  Examiner les boîtes de dialogue, menus, chaînes de caractères, icônes

*App.cpp / *App.h
  Examiner la classe dérivée de CWinApp et InitInstance

MainFrm.cpp / MainFrm.h
  Examiner la fenêtre principale et les menus/barres d'outils

*Doc.cpp / *Doc.h
  Pour Document/View, examiner la structure de données et le traitement de sauvegarde

*View.cpp / *View.h
  Examiner le dessin et les interactions utilisateur

*Dlg.cpp / *Dlg.h
  Examiner les boîtes de dialogue, le DDX et le traitement des boutons

Ensuite, voici les mots-clés de recherche fréquemment utilisés.

BEGIN_MESSAGE_MAP
ON_COMMAND
ON_UPDATE_COMMAND_UI
ON_BN_CLICKED
DoDataExchange
UpdateData
OnInitDialog
OnDraw
Serialize
AfxMessageBox
AfxBeginThread
AFX_MANAGE_STATE

Rechercher ces éléments permet de voir plus facilement le fonctionnement de l’application.

39. Les principes de conception pour la maintenance de MFC

Pour maintenir durablement un actif MFC existant, les orientations suivantes sont efficaces.

Alléger les gestionnaires d'événements UI
Ne pas laisser trop fuir CString ou CWnd vers la couche non-UI
Déplacer la logique métier vers des classes C++ ordinaires
Créer des tests de compatibilité pour le format de fichier
Clarifier les différences entre x86 et x64
Passer en revue les modifications des ID de ressources
Vérifier l'état de module des DLL MFC
Structurer la journalisation
Fixer le build via la CI
Vérifier régulièrement l'écran en haut DPI et sous Windows 11

Il est particulièrement important de ne pas entasser trop de traitements dans CDialog ou CView.

Les classes d’écran de MFC doivent se concentrer sur la saisie, l’affichage et la distribution des événements.

Récupérer les valeurs depuis l'écran
Les transmettre à un service
Reporter le résultat sur l'écran

En maintenant ce niveau de sobriété, même MFC devient nettement plus facile à maintenir.

40. Liste de contrôle pratique

Voici une liste de contrôle pour travailler avec une application MFC.

La version de Visual Studio est-elle clairement définie ?
Les composants MFC/ATL sont-ils installés ?
Les cibles x86/x64 sont-elles clairement définies ?
Les réglages Unicode/MBCS sont-ils bien connus ?
MFC est-il en liaison statique ou en DLL partagée ?
Quel Visual C++ Redistributable est nécessaire ?
resource.h et le fichier .rc font-ils partie de la revue de code ?
Le chemin des événements est-il suivi à travers la carte de messages ?
Le sens de UpdateData est-il correct ?
La structure Document/View est-elle utilisée ?
La gestion de la durée de vie des boîtes de dialogue non modales est-elle sûre ?
L'UI n'est-elle pas manipulée directement depuis un thread de travail ?
Y a-t-il des endroits où AFX_MANAGE_STATE est nécessaire dans une DLL MFC ?
L'écran est-il vérifié dans un environnement haut DPI ?
Les dépendances COM/ActiveX prennent-elles en charge le 32 bits/64 bits ?
Les journaux sont-ils conservés ?
La logique non-UI peut-elle être testée ?

MFC peut sembler particulier tant qu’on n’y est pas habitué, mais une fois qu’on sait où regarder, il se lit de façon assez régulière.

41. Résumé

MFC est un framework historique pour construire des applications de bureau natives Windows en C++. Il n’est plus le choix dominant pour un nouveau développement, mais il reste aujourd’hui une technologie importante pour la maintenance de l’existant, la mise à jour de l’environnement de build, l’ajout de fonctionnalités et la migration progressive.

Voici les points particulièrement importants pour comprendre MFC.

MFC facilite la manipulation de l'API Win32 depuis C++
CWinApp gère l'application dans son ensemble
CWnd et HWND n'ont pas la même durée de vie
La carte de messages relie les événements aux fonctions
Document/View est le mécanisme séparant les données de leur affichage
DDX/DDV servent à synchroniser et valider les valeurs d'une boîte de dialogue
Les fichiers de ressources et resource.h sont extrêmement importants
CString et TCHAR doivent être compris conjointement avec les réglages d'encodage
Il faut être attentif à l'état de module dans les DLL MFC
Une ancienne application MFC mérite d'être réexaminée sous l'angle du haut DPI, de x64, de la CI et des tests

Quand on aborde MFC, plutôt que de le juger d’emblée comme « mauvais parce qu’ancien », il est important de commencer par comprendre sa structure. Une application MFC existante peut concentrer des années de connaissance métier, des spécifications propres à chaque client, une intégration matérielle et une compatibilité de fichiers. Pour préserver cette valeur tout en rendant progressivement l’application plus facile à maintenir, l’approche réaliste consiste à comprendre les conventions de MFC, à extraire la logique non-UI, puis à mettre en ordre le build et les tests.

Si l’on devait le résumer en une phrase, ce serait ceci.

Un socle pour appréhender les mécanismes des applications natives Windows au moyen de classes et d’un framework C++.

Avec ce point de vue, MFC cesse d’être une simple technologie ancienne : il devient une clé pour déchiffrer en toute sécurité les actifs Windows existants.

Références

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Qu'est-ce que MFC ?
MFC est l'abréviation de Microsoft Foundation Classes, un framework d'application Windows qui facilite l'utilisation de l'API Win32 sous forme de classes C++. Une fenêtre est représentée par CWnd, une boîte de dialogue par CDialog, et l'application dans son ensemble par CWinApp. Ce n'est pas une « bibliothèque magique permettant d'écrire du code sans connaître Windows », mais plutôt les mécanismes de Windows organisés en types et en framework C++ : des connaissances Win32 comme les messages Windows ou les handles restent donc nécessaires.
Peut-on encore utiliser MFC aujourd'hui ? Le support continue-t-il ?
MFC est toujours disponible dans Visual Studio et continue d'être supporté. Cependant, la documentation de Microsoft précise qu'aucune nouvelle fonctionnalité ni mise à jour de documentation n'est prévue. Concrètement, MFC reste couramment utilisé pour la maintenance d'applications MFC existantes, l'ajout de fonctionnalités, la mise à jour de l'environnement de build ou une migration progressive vers une autre UI, mais son adoption pour une application GUI totalement nouvelle mérite une décision prudente. L'installation du composant individuel de Visual Studio Installer (« C++ MFC for latest build tools ») est nécessaire.
Qu'est-ce que la carte de messages (message map) de MFC ?
C'est le mécanisme de MFC qui relie les messages Windows et les commandes à des fonctions gestionnaires (handlers). Entre BEGIN_MESSAGE_MAP et END_MESSAGE_MAP, des macros comme ON_BN_CLICKED ou ON_COMMAND décrivent des correspondances du type « quand ce bouton est cliqué, appeler cette fonction ». Si une recherche de fonction ne révèle aucun appel direct alors qu'elle s'exécute bien lors d'un événement, il faut vérifier la carte de messages. Dans une revue de code MFC, il est important d'examiner non seulement la fonction gestionnaire, mais aussi la carte de messages qui l'accompagne.
Comment migrer de MFC vers une autre technologie ?
Viser d'emblée une refonte complète mène généralement à l'échec. L'ordre réaliste consiste à reproduire l'environnement de build, à figer le comportement existant avec des données de test, à extraire la logique des gestionnaires d'événements UI pour la déplacer vers une bibliothèque C++ non-MFC, à ajouter des tests automatisés, à documenter les spécifications externes, puis à remplacer progressivement les écrans qui en ont besoin. Plutôt que de viser « abandonner MFC » comme objectif en soi, il est plus efficace de viser « faire sortir la logique importante enfermée dans MFC ».

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog