Che cos'è MFC su Windows ── Basi per mantenere gli asset esistenti

· Aggiornato il: · · Windows, MFC, Visual C++, C++, Win32, App native, App desktop, Codice legacy, Riuso degli asset esistenti

1. Cosa capire per primi

Quando si mantengono vecchie applicazioni desktop Windows, si incontrano a volte nomi come questi.

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

Sono elementi che compaiono di continuo in MFC, un framework per applicazioni Windows in C++. MFC è l’acronimo di Microsoft Foundation Classes, una libreria che rende più comodo trattare le Win32 API come classi C++.

Nello sviluppo Windows di oggi ci sono molte alternative — WinUI, WPF, Windows Forms, Electron, Qt, tecnologie Web — quindi le occasioni in cui MFC è il primo candidato per uno sviluppo nuovo sono diminuite. Non è però una tecnologia scomparsa. Nelle applicazioni aziendali, negli strumenti di misura, nel software di controllo, nel CAD/CAM, negli strumenti interni e nei packaged software di lunga data capita ancora di mantenere code base MFC.

Prima di procedere, elenco i punti di vista da tenere presenti per capire MFC.

MFC è una mappa essenziale per leggere le vecchie app desktop Windows
Pensate a MFC non come a qualcosa che nasconde le Win32 API, ma come a un involucro in stile C++
Senza conoscere le convenzioni MFC, si legge male il comportamento, più di quanto l'aspetto del codice suggerisca
Il valore sta nella manutenzione, nel prolungamento della vita e nella migrazione graduale degli asset esistenti, non nell'adozione nuova

Questo articolo riordina la panoramica di MFC, la struttura dell’applicazione, le message map, Document/View, i dialoghi, DDX/DDV, le risorse, la build e i punti da osservare in manutenzione.

Il lettore che ho in mente è chi sa scrivere C++, ma non ha esperienza di Win32 API né di MFC. Come livello di partenza basta circa questo.

Ambito Livello atteso
C++ Sapere leggere classi, ereditarietà, funzioni virtuali, puntatori e riferimenti
Win32 API L’inesperienza va bene. Basta aver sentito le parole finestra e messaggio
Visual Studio Aver aperto una solution e averla compilata
COM / OLE L’inesperienza va bene. Basta approfondire al capitolo 27, quando serve

In fondo al capitolo 2 elenco «le conoscenze necessarie per leggere MFC», ma non occorre averle tutte prima. Basta una lettura in cui si torna indietro nel momento in cui servono.

Nemmeno l’ordine di lettura deve coprire i capitoli da 1 a 41 di seguito. Ecco i percorsi più corti per obiettivo.

Obiettivo Capitoli da leggere
Sapere solo che cos’è MFC e quale sia oggi il suo posizionamento Capitoli 2–4, capitolo 36
Arrivare a saper leggere codice MFC esistente Capitolo 6 → 9 → 10 → 14 → 12 e 13 → 38
Sistemare una build che non passa Capitoli 5, 25, 32, 33
Investigare un malfunzionamento Capitolo 34 → 35 → 8 e 22
Decidere una linea di manutenzione o di migrazione Capitoli 30, 31, 37, 39

I due punti da fissare per primi sono la message map (capitolo 9) e Document/View (capitolo 14). Capiti questi due, si riesce a leggere «perché viene eseguita una funzione di cui non si trova il chiamante» e «come sono collegati i dati e lo schermo». Se invece li si salta, le spiegazioni degli altri capitoli restano sospese.

I frammenti di codice di questo articolo sono pubblicati su GitHub come raccolta di riferimento, organizzata in file per capitolo.

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

Mappa della conoscenza di questo articolo

MFC è un framework che rende più comodo trattare le Win32 API come classi C++: CWinApp si occupa dell’inizializzazione dell’applicazione intera, la message map lega i messaggi Windows a funzioni handler, l’architettura Document/View separa i dati dalla visualizzazione dell’UI, e DDX/DDV associa i control del dialogo alle variabili membro e ne convalida i valori. Per usare MFC occorre installare a parte il componente MFC in Visual Studio Installer, e si può configurare se usarlo come DLL condivisa o con linking statico. L’articolo mostra il posizionamento per cui, in un’applicazione GUI generale del tutto nuova, l’adozione di MFC va valutata con cautela, mentre nella manutenzione, nell’aggiunta di funzioni e nella migrazione graduale di app MFC esistenti ha spesso un valore pratico.

Mappa della conoscenza della manutenzione di app desktop Windows con MFCDiagramma che mostra le relazioni tra il wrapping in C++ delle Win32 API da parte di MFC e l'implementazione di meccanismi come la message map e Document/View, il posizionamento per cui è adatto alla manutenzione degli asset esistenti ma va valutato con cautela per lo sviluppo GUI nuovo, e i punti di attenzione su ambiente di build e integrazione COMusausaautomatizzaimplementarichiedeimplementarichiedeusaimplementarichiedeusarichiedeconfigurato darichiedeusarichiederichiedesconsigliato perconsigliato perrichiedeMFC (Microsoft Foundation Classes)Win32 APICWinAppMappa dei messaggiArchitettura Document/ViewDDX/DDVFile di risorse (.rc/resource.h)CStringComponente MFC di Visual Studio InstallerModalità di collegamento di MFC (Use of MFC)Stato del modulo MFCActiveXRequisito di corrispondenza del bitnessSupporto DPI elevatoSviluppo da zero di un'app GUI genericaManutenzione e migrazione graduale di app MFC esistenti

Nel diagramma, una linea continua indica una relazione che vale sempre e una linea tratteggiata indica una relazione condizionale (le condizioni sono nella spiegazione di ciascuna relazione nella pagina di dettaglio). L’elenco completo delle relazioni (in totale 20, con evidenza e livello di certezza) e le definizioni dei concetti principali sono raccolti nella pagina di dettaglio della mappa della conoscenza (in giapponese). Dati: JSON-LD / Turtle

2. Che cos’è MFC

MFC è una class library per costruire applicazioni desktop native Windows in C++.

Quando si usano direttamente le Win32 API, in genere si scrive codice come questo.

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

Le Win32 API sono molto potenti, ma si costruiscono intorno a funzioni C, handle, messaggi e callback, quindi in un’applicazione grande la visione d’insieme si perde facilmente. MFC rende possibile trattarle come classi C++: la finestra è CWnd, il dialogo è CDialog, l’applicazione intera è CWinApp, la frame window è CFrameWnd, la view è CView.

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

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

MFC non sostituisce le Win32 API con qualcosa di completamente diverso: è un framework C++ sottile ma di ampia copertura, fondato sul modo di pensare delle Win32 API. Per leggere MFC, quindi, oltre alle sue classi servono anche conoscenze di questo tipo.

Messaggi Windows
Handle come HWND
GDI/GDI+
File di risorse
COM/OLE
DLL e runtime
Codifica dei caratteri
Thread e message loop

La realtà di MFC non è «una libreria magica con cui si scrive senza conoscere Windows», ma «un riordino dei meccanismi di Windows in tipi C++ e in un framework».

3. Si può ancora usare MFC?

MFC si può ancora usare in Visual Studio. Non va però frainteso il suo posizionamento. Resta supportato, ma non è un framework UI di punta a cui si aggiungono di continuo nuove funzionalità; nella documentazione Microsoft su MFC si trova la nota che, pur restando supportato, non riceverà nuove funzionalità né aggiornamenti della documentazione.

Per questo il posizionamento di MFC è, all’incirca, il seguente.

Manutenzione di app MFC esistenti              -> realistica e frequente
Aggiunta di funzioni a un'app MFC esistente    -> possibile
Aggiornamento dell'ambiente di build MFC       -> importante
Migrazione graduale da MFC a un'altra UI       -> possibile
Adozione in un'app GUI generale del tutto nuova -> da valutare con cautela

In particolare, nelle applicazioni aziendali usate a lungo, UI, stampa, I/O su file, controllo dei dispositivi, protocolli proprietari e integrazione COM possono essere concentrati dentro MFC.

In una code base di quel tipo, prima di «abbandonare MFC» occorre «mettere la code base in uno stato in cui MFC si può leggere».

4. I domini in cui MFC è stato forte

Il dominio rappresentativo in cui MFC è stato usato sono le applicazioni desktop native Windows.

In concreto, applicazioni come queste.

Strumenti aziendali centrati sui dialoghi
App SDI che aprono e modificano file
App MDI che gestiscono più documenti
Schermate di controllo di strumenti di misura o macchine di produzione
App native CAD/CAM
App che usano molto stampa e anteprima
App che includono integrazione ActiveX o OLE
App strettamente legate a vecchie API Windows o ad asset COM

Il punto di forza di MFC è lavorare vicino ai pezzi nativi di Windows. Finestre, menu, toolbar, status bar, dialoghi, common control, stampa, file dialog, Registry, disegno GDI si possono trattare come classi C++.

Il punto debole, invece, è che costruzione UI moderna, data binding, testabilità, elaborazione asincrona, layout moderni, supporto high DPI, internazionalizzazione e accessibilità non si scrivono in modo altrettanto naturale quanto nei framework più recenti.

Mettendo in fila le caratteristiche, il quadro è questo.

Vicino al nativo Windows
Controllo diretto in C++
Molti asset esistenti
Serve conoscenza di Win32
Molte convenzioni datate
Una struttura facile da testare va allestita da sé

5. Preparare Visual Studio per usare MFC

Anche se in Visual Studio è installato C++, MFC non è detto che ci sia. MFC è trattato come componente individuale di Visual Studio Installer. In pratica si verificano componenti di questo tipo.

Desktop development with 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
Se serve la variante Spectre Mitigations di MFC

Se in build non si trovano i file legati a MFC, oltre alle impostazioni del progetto si verifica se il componente MFC è presente lato Visual Studio.

La scelta nell’installer segue questi passi.

1. Avviare «Visual Studio Installer» dal menu Start
2. Premere «Modifica» sull'installazione Visual Studio di destinazione
3. Nella scheda «Workload» spuntare «Desktop development with C++»
4. Nello stesso schermo, a destra in «Dettagli installazione»,
   spuntare «C++ MFC for latest v143 build tools»
5. Se non lo trovate, passare alla scheda «Singoli componenti»
   e cercarlo digitando MFC nella casella di ricerca
6. Premere «Modifica» e installare

Scegliere il solo workload «Desktop development with C++» a volte non installa MFC, quindi al passo 4 o al passo 5 va verificato che il componente stesso sia spuntato.

Se si installa da script o da CI, si specifica il component ID.

Nome visualizzato Component ID
C++ MFC for latest v143 build tools Microsoft.VisualStudio.Component.VC.ATLMFC
C++ ATL for latest v143 build tools Microsoft.VisualStudio.Component.VC.ATL
Desktop development with C++ (workload di Build Tools) Microsoft.VisualStudio.Workload.VCTools

Se è installato lo si può verificare dalla presenza della cartella atlmfc, dove stanno gli header MFC. Header e librerie MFC finiscono sotto il toolset MSVC.

<destinazione di installazione di Visual Studio>\VC\Tools\MSVC\<versione del toolset>\atlmfc\include\afxwin.h

Per cercarlo in PowerShell, si fa così.

Get-ChildItem -Path "$env:ProgramFiles\Microsoft Visual Studio\2022" -Recurse -Filter afxwin.h -ErrorAction SilentlyContinue |
    Select-Object -ExpandProperty FullName

Se non compare nulla, il componente MFC non è installato.

Se si compila in questo stato, ci si ferma già all’inclusione di afxwin.h. L’errore di compilazione tipico è il seguente.

fatal error C1083: Cannot open include file: 'afxwin.h': No such file or directory

Leggere questo errore come «percorso di include sbagliato» e girare a vuoto tra le proprietà del progetto è un giro largo frequente. Quando non si trovano afxwin.h o afxdialogex.h, il primo sospetto è Visual Studio Installer.

Lo stesso vale per CI e build server: se in locale Visual Studio compila e in CI no, la causa può essere l’assenza del componente MFC o una versione diversa del toolset di destinazione.

6. Struttura di base di un’applicazione MFC

Un’applicazione MFC ha, all’incirca, questa struttura.

Classe derivata da CWinApp
  si occupa di inizializzazione e chiusura dell'applicazione intera

Classe derivata da CFrameWnd / CMDIFrameWnd / CDialog
  si occupa della finestra principale o del dialogo

Classe derivata da CView
  si occupa della visualizzazione a schermo e delle operazioni dell'utente

Classe derivata da CDocument
  si occupa dei dati e del salvataggio su file

File di risorse
  tiene menu, dialoghi, icone, stringhe e simili

Message map
  lega messaggi Windows e comandi a funzioni handler

In un’app MFC semplice, per esempio, compare una classe derivata da CWinApp di questo tipo.

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 è la classe che rappresenta l’applicazione intera. In un’applicazione MFC di solito esiste un solo oggetto derivato da CWinApp.

Un oggetto globale come theApp all’inizio può sembrare strano, ma in MFC è la struttura standard.

7. Che cosa fa CWinApp

CWinApp è importante come ingresso dell’applicazione MFC. In un’applicazione Win32 ordinaria si scrivono da sé WinMain, la registrazione della window class, il message loop e così via; in MFC gran parte di questo lo prende in carico il framework. Lo sviluppatore, in sostanza, esegue l’override di InitInstance e ci scrive l’inizializzazione specifica dell’applicazione.

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

    // lettura delle impostazioni
    // inizializzazione COM
    // creazione della finestra principale
    // registrazione del document template

    return TRUE;
}

In InitInstance finiscono spesso trattamenti di questo tipo.

Inizializzazione dei common control
Impostazione della chiave di Registry
Lettura dell'elenco dei file usati di recente
Registrazione del document template
Creazione del main frame
Trattamento degli argomenti da riga di comando
Inizializzazione COM/OLE

In manutenzione, partendo dalla classe derivata da CWinApp si vede più facilmente l’ordine di avvio dell’applicazione intera.

8. CWnd è la classe al centro di MFC

Molte classi UI di MFC hanno come base CWnd, che rappresenta una finestra Windows. Un oggetto CWnd e un HWND, però, non sono la stessa cosa.

HWND
  handle di finestra gestito dal sistema operativo Windows

CWnd
  oggetto wrapper C++ per trattare più comodamente un HWND

In MFC, CWnd tiene internamente un HWND.

HWND hWnd = m_hWnd;

Oppure lo si ottiene così.

HWND hWnd = GetSafeHwnd();

Il punto importante in manutenzione è che un CWnd* può esistere anche quando l’HWND corrispondente è già stato distrutto.

Per questo, se la finestra è valida lo si verifica in una forma come questa.

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

Nell’indagine dei bug MFC è importante vedere se la durata dell’oggetto C++ CWnd e quella dell’handle di finestra Windows reale non siano sfasate.

9. Che cos’è una message map

Uno dei meccanismi in cui MFC si riconosce di più è la message map.

Un’applicazione Windows riceve clic del mouse, input da tastiera, ridisegno, ridimensionamento della finestra, scelta di un menu e così via come messaggi Windows.

Con le Win32 API, di solito i messaggi si trattano nello switch di WndProc.

In MFC li si lega a funzioni handler con la message map.

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"));
}

Il senso di questo codice è il seguente.

Quando si fa clic sul pulsante IDC_BUTTON_OK
chiama CMyDialog::OnClickedButtonOk

Il flusso dal momento in cui arriva il messaggio a quello in cui viene chiamato l’handler, in figura, è questo.

Dal messaggio Windows all'handler MFCIl loop di messaggi del framework MFC passa il messaggio a CWnd::WindowProc, cerca una voce nella message map della classe e delle classi base e, se non la trova, delega a DefWindowProc.c'ènon c'ètrovataassente fino in fondoMessaggio WindowsWM_COMMAND / WM_PAINT e altriLoop di messaggiche il framework MFC fa girareCWnd::WindowProcpunto di ingresso comune predisposto da MFCNella message map della propria classec'è una voce corrispondente?Chiama la funzione handlerindicata da ON_BN_CLICKED e similiCerca la message map della classe baseda CDialogEx a CDialog, poi a CWndTrovata da qualche parte?DefWindowProcdelega all'elaborazione predefinita di Windows

Che non si veda uno switch dipende dal fatto che il framework si fa carico di questa parte: «seguire la message map dalla propria classe verso le classi base». DECLARE_MESSAGE_MAP e BEGIN_MESSAGE_MAP si capiscono meglio se le si pensa come macro che preparano, per ciascuna classe, la tabella di corrispondenza usata in questa ricerca.

Chi non ha l’abitudine di leggere MFC ha spesso difficoltà a capire da dove viene chiamata una funzione. Se la ricerca non trova una chiamata diretta, si guarda la message map.

La funzione non è chiamata direttamente
eppure viene eseguita all'evento
-> controllare le macro BEGIN_MESSAGE_MAP / ON_...

Nella code review MFC è importante verificare non le sole funzioni handler, ma la message map insieme a esse.

10. Command routing

In MFC anche le operazioni su menu e toolbar sono trattate come comandi.

Il caso rappresentativo è ON_COMMAND.

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

void CMainFrame::OnFileOpen()
{
    // apertura del file
}

MFC ha un meccanismo che consegna il comando all’oggetto appropriato.

Per esempio, lo stesso ID_EDIT_COPY può essere trattato dalla view attualmente attiva, dal document, dal frame o dall’applicazione, e quale dei quattro lo tratti può cambiare.

View attiva
Document
Frame window
Applicazione

In questo ordine il comando viene passato all’oggetto in grado di trattarlo.

Per questo, in MFC, «quale funzione viene chiamata quando si preme un menu» a volte è difficile da seguire con la sola ricerca testuale.

I punti da guardare in manutenzione sono questi.

Qual è il command ID
In quale classe sta ON_COMMAND
Dove sta ON_UPDATE_COMMAND_UI
Qual è la view attualmente attiva
Si usa la struttura Document/View

11. Che cos’è ON_UPDATE_COMMAND_UI

In MFC, per aggiornare abilitazione/disabilitazione, stato di check e testo visibile di voci di menu e pulsanti della toolbar si usa a volte ON_UPDATE_COMMAND_UI.

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());
}

Così si può abilitare il menu o il pulsante solo quando l’eliminazione è possibile.

Quando si usa un’app MFC e si indaga un comportamento del tipo «il pulsante è grigio senza motivo», «il menu non si può premere», «lo stato di check cambia», a volte la causa si trova cercando ON_UPDATE_COMMAND_UI.

Una delle forme più chiare di MFC è l’applicazione dialog-based.

Nelle schermate di impostazione, negli strumenti aziendali semplici, nelle schermate di comando di un apparato, la struttura è a volte centrata sui dialoghi.

In genere si eredita da CDialog o da 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;
};

Nel codice dialog-based compaiono spesso questi elementi.

IDD_...        ID della risorsa dialogo
IDC_...        ID del control
OnInitDialog   inizializzazione
DoDataExchange associazione tra control e variabili membro
UpdateData     sincronizzazione tra schermo e variabili
ON_BN_CLICKED  trattamento del clic sul pulsante

Un dialogo non è solo l’aspetto: funziona dalla combinazione di risorse, variabili membro, message map e inizializzazione.

13. DDX e DDV

Nei dialoghi MFC compaiono di frequente DDX e DDV.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX è il meccanismo che associa i control sul dialogo alle variabili membro C++; DDV è il meccanismo che convalida i valori di input.

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);
}

Chiamando UpdateData(TRUE), i valori inseriti a schermo vengono copiati nelle variabili membro.

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

    // qui m_name e m_interval contengono i valori inseriti a schermo
    SaveSettings(m_name, m_interval);

    CDialogEx::OnOK();
}

Al contrario, chiamando UpdateData(FALSE), i valori delle variabili membro vengono copiati sullo schermo.

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

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

    return TRUE;
}

Quando i valori di input di un dialogo MFC sono strani, si controlla questo.

DDX è definito in DoDataExchange
Si chiama UpdateData(TRUE)
Il momento in cui si chiama UpdateData(FALSE) è corretto
DDV non sta scartando l'input
Gli ID dei control coincidono con le risorse

14. Architettura Document/View

Uno dei tratti distintivi di MFC è l’architettura Document/View.

È una struttura per separare i dati che l’applicazione tratta dalla loro visualizzazione.

CDocument
  tiene i dati
  si occupa di lettura e scrittura su file
  notifica l'aggiornamento a più view

CView
  visualizza i dati
  tratta le operazioni dell'utente
  gestisce disegno e stato di selezione

La relazione tra frame, view e document, in figura, è questa.

Relazione tra frame, view e document in MFCIl document template collega CWinApp, frame window, CDocument e CView; la view ottiene il document con GetDocument e il document notifica le view con UpdateAllViews.ottenuta con GetDocumentnotifica con UpdateAllViewsDerivata da CWinAppavvio e chiusura dell'app interaDocument templateCSingleDocTemplate / CMultiDocTemplateFrame windowCFrameWnd / CMDIChildWndDerivata da CDocumentdati e I/O su fileDerivata da CViewdisegno e operazioni dell'utente

Il punto è che a collegare i tre — frame, document e view — è il document template. Si registra questo template dentro InitInstance, quindi se si vuole sapere «quale view guarda quale document» si parte da InitInstance.

Anche la differenza di direzione — dalla view si guarda il document con GetDocument, dal document si comunica la modifica a tutte le view con UpdateAllViews — va tenuta presente: rende più facile inseguire un malfunzionamento in cui lo schermo non si aggiorna.

Per esempio, in un editor di testo, in un editor di figure, in uno strumento di modifica di file di impostazione, in un’app CAD, ha senso separare dati e visualizzazione.

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;
};

Dal lato view si ottiene il document e si disegna.

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

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

Il vantaggio di Document/View è che gli stessi dati si visualizzano facilmente in più view.

Per esempio, gli stessi dati si possono mostrare così.

View tabellare
View a grafico
View di dettaglio
Anteprima
View di stampa

Su una semplice schermata di impostazione o su un piccolo strumento, però, usare Document/View può far sentire la struttura più pesante del necessario.

In manutenzione, capire per primi se l’app usa Document/View o è centrata sui dialoghi rende più facile seguire il codice.

15. SDI e MDI

In MFC, combinati con Document/View, compaiono spesso le strutture SDI e MDI.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI è, in sostanza, la forma in cui un frame tratta un documento.

Finestra principale
  un document
  una o più view

MDI è la forma in cui una finestra padre contiene più finestre figlie, e ciascuna tratta un document.

Frame padre MDI
  Frame figlio MDI 1 -> document 1
  Frame figlio MDI 2 -> document 2
  Frame figlio MDI 3 -> document 3

Nelle vecchie applicazioni Windows, MDI era usato di frequente.

In manutenzione, dal nome della classe si intuisce la struttura.

CFrameWnd          frame di tipo SDI
CMDIFrameWnd       frame padre MDI
CMDIChildWnd       frame figlio MDI
CSingleDocTemplate document template per SDI
CMultiDocTemplate  document template per MDI

Nelle app generate dal wizard MFC, dentro InitInstance c’è spesso la registrazione di CSingleDocTemplate o CMultiDocTemplate.

16. Capire i file di risorse

In un’app MFC il file .rc è molto importante.

.rc è il file di risorse di Windows.

Vi sono definiti elementi di questo tipo.

Template di dialogo
Menu
Tasti acceleratore
Icone
Bitmap
String table
Informazioni di versione
Toolbar

Inoltre, in resource.h sono definiti gli ID delle risorse.

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

Nel codice MFC, con questi ID si legano risorse e codice C++.

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

Un problema frequente in manutenzione è l’incoerenza degli ID delle risorse.

L'ID in resource.h è cambiato
In un merge tra branch gli ID sono andati in collisione
L'ID del control sul dialogo e l'ID di DDX non coincidono
Resta l'ID di un menu che si pensava eliminato
Gli ID della string table sono duplicati

Quando si indaga il comportamento di un’app MFC, oltre al codice C++ occorre guardare insieme .rc e resource.h.

17. Class Wizard e codice scritto a mano

MFC ha una storia strettamente legata al Class Wizard di Visual Studio.

Con Class Wizard si possono generare automaticamente handler di messaggi, variabili DDX, override di funzioni virtuali e così via.

Per questo, nel codice MFC resta spesso codice nella forma generata dallo strumento.

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

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

Nelle Visual Studio più recenti l’aspetto e la forma generata possono essere cambiati, ma nelle code base vecchie marcatori di commento di questo tipo restano a volte.

Il punto importante in manutenzione è non rompere alla leggera il confine tra codice generato e codice scritto a mano.

Non cancellare la message map
Non rompere la corrispondenza DDX
Non cambiare gli ID delle risorse senza motivo
Non cancellare a caso i commenti pensati per il vecchio Class Wizard

In MFC non basta che il C++ compili. Va conservata, almeno in parte, anche la forma che l’editor delle risorse e Class Wizard di Visual Studio si aspettano.

18. CString e le stringhe

La class stringa che in MFC compare di continuo è CString.

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

CString è una classe stringa a lunghezza variabile usata spesso nel codice MFC/ATL. Nel C++ moderno si usano di frequente std::string e std::wstring, ma in MFC, per l’affinità con API e control, si usa molto CString.

In manutenzione occorre tenere presente la codifica dei caratteri.

CString      a seconda delle impostazioni del progetto, equivalente a CStringA o CStringW
CStringA     famiglia ANSI / MBCS
CStringW     famiglia Unicode / UTF-16
LPCTSTR      puntatore a stringa basato su TCHAR
LPCSTR       famiglia char
LPCWSTR      famiglia wchar_t
std::string  di solito famiglia char
std::wstring famiglia wchar_t

Nelle app Windows attuali è più sicuro presupporre Unicode, ma nelle vecchie app MFC resta a volte codice pensato per MBCS.

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

I bug di conversione delle stringhe, nella manutenzione delle app MFC, escono di frequente.

I casi da osservare con particolare attenzione sono questi.

Si legge un file presupposto Shift_JIS
Si passa a una build Unicode
Una DLL esterna chiede char*
COM chiede BSTR
Una conversione sbrigativa a std::string produce testo corrotto

Quando si vede CString, non basta pensarla come «una vecchia classe stringa»: va verificata insieme all’impostazione del set di caratteri del progetto, alle API esterne e al formato dei file.

19. CFile e CArchive

MFC ha anche classi per operazioni su file e serializzazione. Le rappresentative sono CFile e CArchive.

CFile file;
if (file.Open(path, CFile::modeRead))
{
    CArchive ar(&file, CArchive::load);
    // si legge da ar
}

CArchive è usata di frequente nel meccanismo di serializzazione MFC.

Nelle classi derivate da CDocument si fa a volte l’override di Serialize e si scrivono lettura e salvataggio nella stessa funzione.

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 serializzazione MFC è comoda, ma nell’esercizio a lungo termine va usata con attenzione.

Compatibilità con i formati di file vecchi
Gestione del numero di versione
Ripristino quando la lettura fallisce
Gestione delle eccezioni
Codifica dei caratteri
Endianness
Se si sta salvando una struct così com'è

Nelle app MFC che da tempo usano un formato binario proprietario, Serialize è a volte, di fatto, la specifica del file.

In quel caso, prima di cambiare il codice, conviene sempre preparare dati di test che caricano i file esistenti.

20. Disegno GDI e CDC

Nel disegno a schermo di MFC si usa di frequente la classe CDC, per trattare il Device Context di Windows. In CView::OnDraw viene passato come argomento un CDC*.

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

Quando si usano pen e brush, si sta attenti a selezione e ripristino.

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);
}

Intorno agli oggetti GDI, errori di questo tipo diventano problemi.

Dopo SelectObject non si è tornati all'oggetto precedente
Si creano molti oggetti GDI e non li si distrugge
Si confondono i ruoli di OnPaint e OnDraw
Non si fa double buffering e lo schermo sfarfalla
A high DPI il disegno presupposto a pixel fissi si rompe

Nei bug di disegno MFC, oltre alla logica C++, occorre verificare le risorse GDI di Windows, il timing del ridisegno, il DPI e la dimensione del font.

21. Dialoghi modali e modeless

In MFC va osservato anche il modo in cui si mostra un dialogo.

Un dialogo modale si mostra con DoModal.

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // trattamento su OK
}

In questo caso il chiamante attende finché il dialogo non si chiude.

Un dialogo modeless, invece, dopo la creazione restituisce il controllo al chiamante.

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

Nei dialoghi modeless la gestione della durata è importante.

Quando si fa delete del dialogo creato con new
La finestra padre non viene distrutta prima
Sul lato dialogo si usa PostNcDestroy
Non viene creato due volte
Dopo la chiusura non resta un puntatore

Nell’indagine dei crash MFC, a volte la causa è un problema di durata di un dialogo modeless.

22. Durata degli oggetti C++ e degli handle Windows

In MFC è molto importante la differenza di durata tra oggetti C++ e handle Windows. Per esempio CWnd è un oggetto C++, ma la finestra reale è gestita da Windows come HWND, e i due non nascono e spariscono sempre insieme.

L'oggetto CWnd c'è, ma l'HWND ancora no
L'HWND è stato distrutto, ma l'oggetto CWnd resta
È stato creato un wrapper CWnd temporaneo
Con Attach/Detach si sta sostituendo l'handle

Per esempio, un codice come questo va trattato con attenzione.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// salvare pWnd in un membro e usarlo dopo

Se si tiene a lungo un puntatore ottenuto con GetDlgItem, c’è il rischio di riferirlo dopo la distruzione della finestra.

Se serve, a volte è più sicuro chiamare GetDlgItem ogni volta, o gestire la variabile membro del control con DDX.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

In MFC, un puntatore non null non è di per sé sicuro.

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

Questo istinto, nella manutenzione MFC, è molto importante.

23. Thread e aggiornamento della UI

La UI di Windows va operata, in sostanza, sul UI thread in cui è stata creata, e lo stesso vale per le app MFC. Operare i control UI direttamente da un worker thread è causa di comportamento instabile e di crash.

Un esempio da evitare.

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

    // evitare di toccare la UI direttamente dal worker thread
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

In generale si notifica il UI thread con PostMessage o simile.

constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;

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

    // elaborazione pesante

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

Sul lato UI si riceve con la message map.

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;
}

Intorno ai thread MFC si verifica quanto segue.

Non si sta toccando la UI direttamente dal worker thread
Non si fa PostMessage dopo la distruzione della finestra
Non si ferma il UI thread aspettando la fine di un thread
Il lock sui dati condivisi è appropriato
Non si fraintendono valore di ritorno e durata di AfxBeginThread

24. DLL MFC e module state

Quando si fa una DLL con MFC, entra in gioco il concetto di module state.

Nei casi in cui si caricano risorse da una DLL MFC, si mostra un dialogo, si fa una extension DLL, il problema è da quale modulo si vanno a prendere le risorse.

All’ingresso di una funzione di una DLL MFC si vede a volte una macro come questa.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

Serve a far sì che MFC possa usare il module state corretto.

Dimenticarla porta a malfunzionamenti di questo tipo.

Non si trova la risorsa dialogo dentro la DLL
Le risorse stringa vengono lette da un altro modulo
Non si trovano icone o menu
In debug funziona e in release si rompe

Quando si mantiene una DLL MFC, è importante riordinare la relazione tra EXE, DLL ordinaria, extension DLL e resource DLL.

Va osservata con particolare attenzione la chiamata a una DLL MFC da un’app non MFC, e il caso in cui la struttura è a plugin.

25. Collegare MFC in statico o usarlo come DLL condivisa

Nelle app MFC, nelle impostazioni del progetto c’è «Use of MFC».

Le scelte rappresentative sono queste due.

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

Se si usa la DLL condivisa, sull’ambiente di esecuzione servono il runtime MFC e il runtime Visual C++ corrispondenti.

Con il linking statico, il materiale da distribuire può sembrare più semplice, ma vanno considerati la dimensione dell’eseguibile, gli aggiornamenti, l’applicazione delle correzioni di sicurezza, la licenza e le condizioni di redistribuzione.

Non esiste una risposta sempre corretta.

Gli elementi di giudizio sono all’incirca questi.

Sul destinatario della distribuzione si può installare Visual C++ Redistributable
Si vuole avvicinare l'app a un unico exe
Come si applicano gli aggiornamenti di sicurezza
Più app condividono lo stesso runtime
Si può predisporre un installer
Quali versioni di Windows sono il bersaglio

In manutenzione, il primo passo è verificare l’impostazione attuale.

Configuration Properties
  General
    Use of MFC

Inoltre si guarda l’impostazione di Runtime Library.

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

Se le impostazioni di linking di MFC e CRT sono mescolate, al confine tra librerie possono uscire problemi di allocazione/rilascio della memoria.

26. Unicode, MBCS e TCHAR

Nel vecchio codice MFC compaiono di frequente TCHAR, LPCTSTR e la macro _T().

CString title = _T("Impostazioni");
SetWindowText(title);

È un modo di scrivere pensato per coprire sia la build Unicode sia la build MBCS.

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

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

Oggi la build Unicode è la norma, ma nelle app vecchie restano a volte trattamenti presupposti MBCS.

In particolare, su file esterni, protocolli di comunicazione, vecchie DLL, connessioni a database, comunicazione seriale e simili, va verificato il presupposto sulla codifica.

Il punto da non fare è trattare la conversione a Unicode come una semplice sostituzione.

La dimensione di un array di char è in byte o in numero di caratteri
Non si sta usando strlen
Non si sta usando sizeof(buffer) come numero di caratteri
L'API esterna riceve UTF-16 o Shift_JIS
Il formato di salvataggio dei file può cambiare

Quando si toccano le stringhe MFC, oltre alla visualizzazione a schermo si verificano anche la compatibilità dei file e l’integrazione esterna.

27. MFC e COM/OLE/ActiveX

MFC è stato usato anche in applicazioni strettamente legate a COM, OLE e ActiveX.

Nelle vecchie app aziendali restano a volte elementi di questo tipo.

OLE Automation
ActiveX Control
COM server
COM client
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

Nell’avvio di un’app MFC compare a volte un codice come questo.

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

Quando si usa COM/OLE, ciò che sembra un problema MFC è a volte in realtà inizializzazione COM, threading model, reference count, informazioni di registrazione, differenza 32-bit/64-bit.

Da osservare con particolare attenzione sono ActiveX e componenti COM a 32 bit.

Da un'app MFC a 32 bit si usa COM a 32 bit
Da un'app MFC a 64 bit si usa COM a 64 bit
La registrazione COM 32-bit/64-bit è distinta
Un vecchio ActiveX a volte non è pronto per il 64 bit

Quando si porta un’app MFC a x64, oltre al codice UI si verificano sempre anche le dipendenze COM/OLE.

28. Supporto high DPI e Windows moderno

Se si fa girare una vecchia app MFC su un Windows attuale, in un ambiente high DPI la visualizzazione a volte si rompe.

Per esempio, problemi di questo tipo.

Il testo viene tagliato
I pulsanti sono troppo piccoli
Il disegno a pixel fissi si disallinea
Con monitor multipli a fattori di scala diversi si rompe
Le bitmap vecchie diventano sfocate
Il layout del dialogo si comprime

Nelle app desktop Windows, l’applicazione deve dichiarare esplicitamente la modalità di supporto DPI.

Anche in un’app MFC vanno verificati manifest, risorse, codice di disegno, font e layout.

In particolare, un codice come questo, con coordinate fisse scritte a mano, a high DPI diventa facilmente un problema.

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

Le coordinate presupposte a pixel fissi, se il DPI cambia, rompono l’aspetto.

I punti da verificare in manutenzione sono questi.

Impostazione DPI del manifest dell'applicazione
Font della risorsa dialogo
Disegno a pixel fissi
Risoluzione delle risorse immagine
Comportamento su più monitor
Visualizzazione su Windows 10 / Windows 11

Il supporto high DPI di MFC non finisce necessariamente col solo cambio delle impostazioni del progetto. Su una UI vecchia servono verifica a schermo reale e correzione del layout.

29. Gestione delle eccezioni e degli errori

MFC ha classi di eccezione proprie e convenzioni di gestione degli errori.

Nel codice vecchio si vedono macro di questo tipo.

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

C’è anche codice mescolato con try / catch del C++ moderno.

try
{
    DoSomething();
}
catch (const std::exception& ex)
{
    // scrittura del log
}

Nella manutenzione MFC i punti da osservare sono i seguenti.

Eccezioni MFC ed eccezioni standard C++ non sono mescolate
Non si fraintende la durata dell'oggetto eccezione
Si capiscono le vecchie macro THROW/CATCH
Errori per valore di ritorno ed eccezioni non sono mescolati
Non si è nello stato in cui resta solo AfxMessageBox e non resta un log

Nelle app aziendali è importante, oltre a mostrare il messaggio di errore a schermo, lasciare log, cronologia delle operazioni, valori di input e stato delle connessioni esterne.

Nelle vecchie app MFC, a volte l’errore finisce con il solo AfxMessageBox.

AfxMessageBox(_T("Salvataggio non riuscito"));

Per alzare la manutenibilità, conviene separare visualizzazione UI e registrazione del log.

LogError(_T("Save failed"), path);
AfxMessageBox(_T("Salvataggio non riuscito. Controllare i log."));

30. Come far coesistere MFC e C++ moderno

Il fatto di mantenere un’app MFC non obbliga ad allineare tutto al modo di scrivere del C++ vecchio.

Lo strato UI rispetta le convenzioni MFC; la logica di dominio e i calcoli si possono riordinare in C++ moderno.

Per esempio, si divide così.

Strato MFC
  CDialog
  CView
  CDocument
  CString
  Message map
  Operazioni sulle risorse

Strato non MFC
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  Classi unit-testabili
  Business logic

La forma cattiva è quella in cui ogni trattamento è stipato nella classe del dialogo.

void CMainDialog::OnBnClickedExecute()
{
    // lettura dell'input
    // lettura del file
    // comunicazione
    // calcolo
    // aggiornamento del DB
    // aggiornamento dello schermo
    // scrittura del log
    // gestione delle eccezioni
}

Un codice di questo tipo è difficile da cambiare, difficile da testare, e anche l’indagine dei bug diventa dura.

Per migliorarlo, si fa uscire la logica dalle classi 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);
}

Così m_service.Execute si può testare senza MFC.

Il miglioramento che, nel mantenere gli asset MFC esistenti, dà il maggiore effetto è estrarre a poco a poco la logica dalle classi UI.

31. Rendere il codice MFC testabile

Un’app MFC, così com’è, è spesso difficile da unit-testare.

La ragione è che UI, Win32, file, comunicazione, database e stato globale tendono ad accoppiarsi in modo stretto.

Il modo di ragionare per renderla testabile è questo.

Non tentare di testare direttamente CDialog o CView
Prima si estrae la logica non UI
Si convertono i tipi MFC al confine
Si interfacciano file e comunicazione
Si tengono sottili gli handler degli eventi a schermo

Per esempio, si sposta il trattamento in una classe C++ pura.

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

Il lato MFC si occupa solo di input e output.

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);
}

Con questa struttura, PriceCalculator e ParsePrices si possono testare con un framework di test C++ ordinario.

Non occorre rifare di colpo l’app MFC intera: anche solo far uscire dagli handler degli eventi i trattamenti testabili ha già un effetto.

32. Fissare l’ambiente di build

Nella manutenzione di un’app MFC, fissare l’ambiente di build è importante.

Nelle code base vecchie, differenze di questo tipo possono cambiare il risultato della build.

Versione di Visual Studio
Versione del toolset MSVC
Versione del Windows SDK
Presenza dei componenti MFC/ATL
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
Linking statico MFC / DLL condivisa
Impostazione della runtime library
Header precompilati

Nelle app MFC, in stdafx.h o pch.h si concentrano a volte molte dipendenze.

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

La build si rompe a volte perché un solo file ha impostazioni di compilazione diverse, o perché le impostazioni degli header precompilati sono sfasate.

In un progetto di manutenzione, scrivere queste informazioni nel README rende più facile il seguito.

Versione di Visual Studio necessaria
Workload e singoli componenti necessari
Windows SDK necessario
Piattaforma di destinazione
Modo di linking di MFC
Procedura di build
Come eseguire la CI
Come produrre il materiale da distribuire

Uscire da «sul mio PC la build passa» è il primo passo della manutenzione MFC.

33. Compilare MFC in CI

Anche un’app MFC si può compilare in CI.

Sull’ambiente CI, però, deve esserci il componente MFC.

Se si usano Visual Studio Build Tools, va installato specificando i component ID di MFC/ATL. Gli ID da specificare sono gli stessi della tabella del capitolo 5.

I punti da verificare sono i seguenti.

MFC è presente in Build Tools
Il toolset di destinazione coincide con quello del progetto
Il Windows SDK è installato
Si verificano sia la build x86 sia la build x64
Il resource compiler funziona
C'è un passaggio di code signing
Anche la generazione dell'installer è nel perimetro CI

In un’app MFC automatizzare fino ai test UI non è facile, ma almeno l’automazione di questo tipo è efficace.

Build Debug / Release
Build x86 / x64
Analisi statica
Unit test
Creazione dell'installer
Salvataggio dell'hash dei prodotti
Verifica delle DLL dipendenti

Nella manutenzione a lungo termine, anche solo tenere lo stato in cui si può compilare ha già un grande valore.

34. Dove guardare nel debug di un’app MFC

Quando si insegue un malfunzionamento in un’app MFC, questo ordine è efficiente.

1. Quale schermata
2. Dialogo, View o Frame
3. Qual è l'ID di risorsa corrispondente all'operazione
4. A quale funzione va la message map
5. La direzione di UpdateData è corretta
6. Se è Document/View, qual è lo stato del Document
7. Il command routing non viene instradato verso un'altra classe
8. Non si sta toccando la UI da un worker thread
9. Eccezioni ed errori non sono inghiottiti dal solo AfxMessageBox
10. Non ci sono problemi di mancata inizializzazione o di durata specifici della build Release

Per esempio, su un malfunzionamento «premo il pulsante e non succede nulla», si verifica questo.

L'IDC del pulsante è corretto
Esiste ON_BN_CLICKED
La signature dell'handler è corretta
La risorsa dialogo non è un'altra
Il pulsante non è disabilitato
UpdateData non sta fallendo a metà del trattamento
Un'eccezione non è stata inghiottita

Se «il menu non si può premere», è questo.

ON_UPDATE_COMMAND_UI non lo sta disabilitando
Gli ID di comando non sono duplicati
La view attiva è quella prevista
In quale di Frame/View/Document/App sta l'handler

In MFC, evento in superficie e trattamento reale sono collegati da macro e routing, quindi finché non ci si abitua è più facile capire disegnando il percorso di chiamata. La figura del capitolo 9 è la forma di base di quel percorso.

Se volete partire dal sintomo, guardate prima la tabella del capitolo 35. Mette in corrispondenza le insidie frequenti e dove verificarle ciascuna.

35. Insidie ricorrenti

Riordino le insidie frequenti nella manutenzione MFC.

Si cercano solo le chiamate di funzione senza guardare la message map
Si crede che un CWnd* non null sia valido
Si confondono la durata di HWND e quella dell'oggetto C++
Si sbaglia la direzione di UpdateData(TRUE/FALSE)
Non si nota che ON_UPDATE_COMMAND_UI sta disabilitando
Non si nota una collisione di ID in resource.h
Si tocca la UI direttamente da un worker thread
La conversione tra CString e std::string produce testo corrotto
Si rompe codice presupposto MBCS con la conversione a Unicode
In una DLL MFC si dimentica AFX_MANAGE_STATE
Si rompe COM/ActiveX presupposto x86 portando a x64
Si sbaglia selezione/rilascio degli oggetti GDI
A high DPI il layout a coordinate fisse si rompe

Per ciascuna metto in corrispondenza «con quale sintomo esce» e «dove si guarda per verificarlo». L’ingresso al debug corrisponde alla procedura del capitolo 34.

Insidia Sintomo tipico Dove verificare
Si cercano solo le chiamate di funzione senza guardare la message map Viene eseguita anche se non si trova il chiamante Cercare BEGIN_MESSAGE_MAP. Capitolo 9, passo 4 del capitolo 34
Si crede che un CWnd* non null sia valido Va in crash a intermittenza; va in crash se si opera su una schermata già chiusa Verificare con ::IsWindow(pWnd->GetSafeHwnd()). Capitolo 8
Si confondono la durata di HWND e quella dell’oggetto C++ Access violation dopo aver chiuso un dialogo Se si sta tenendo il valore di ritorno di GetDlgItem. Capitolo 22
Si sbaglia la direzione di UpdateData I valori inseriti non vengono applicati, i valori iniziali non compaiono a schermo Punto di chiamata di UpdateData(TRUE) e UpdateData(FALSE). Capitolo 13, passo 5 del capitolo 34
Non si nota che ON_UPDATE_COMMAND_UI sta disabilitando Pulsante o menu grigio, non si può premere Handler di ON_UPDATE_COMMAND_UI. Capitolo 11, «il menu non si può premere» del capitolo 34
Non si nota una collisione di ID in resource.h Reagisce un altro dialogo o un’altra voce di menu Incrociare resource.h e .rc per ID. Capitolo 16
Si tocca la UI direttamente da un worker thread Crash instabili e difficili da riprodurre, a volte hang Se dentro la funzione passata a AfxBeginThread si tocca la UI. Capitolo 23
La conversione tra CString e std::string produce testo corrotto Si corrompe solo il testo giapponese, la coda viene tagliata Impostazione del set di caratteri del progetto e punti di conversione. Capitolo 18
Si rompe codice presupposto MBCS con la conversione a Unicode Sfasamento della lunghezza del buffer, i file esistenti non si leggono più Se si sta usando sizeof come numero di caratteri. Capitolo 26
In una DLL MFC si dimentica AFX_MANAGE_STATE Non si trovano dialogo o risorse stringa dentro la DLL All’inizio delle funzioni esportate della DLL. Capitolo 24
Si rompe COM/ActiveX presupposto x86 portando a x64 Solo la build a 64 bit fallisce all’avvio o nella visualizzazione Se la registrazione COM è solo sul lato 32 bit. Capitolo 27
Si sbaglia selezione e rilascio degli oggetti GDI Dopo un’esecuzione prolungata il disegno si degrada Se la colonna «Oggetti GDI» di Gestione attività continua a crescere. Capitolo 20
A high DPI il layout a coordinate fisse si rompe Solo all’ingrandimento 150% il testo viene tagliato Impostazione DPI del manifest e disegno a coordinate fisse. Capitolo 28

I malfunzionamenti MFC a volte non si capiscono guardando solo la grammatica C++: occorre guardare anche messaggi Windows, risorse, handle, moduli e impostazioni del runtime.

36. Conviene scegliere MFC per uno sviluppo nuovo?

Se scegliere MFC per uno sviluppo del tutto nuovo va valutato con cautela.

Se c’è una ragione per scegliere MFC, è in casi come questi.

Serve un'integrazione stretta con codice MFC esistente
Si vogliono riusare pezzi o schermate MFC esistenti
Serve un controllo molto vicino a Win32/GDI/COM
In azienda c'è già competenza sufficiente di manutenzione MFC
Il bersaglio è limitato al desktop Windows
Nel piano di migrazione a lungo termine, per ora occorre ampliare in MFC

Al contrario, nei casi seguenti conviene considerare un’altra scelta.

Si vuole una UI moderna
Servono layout flessibili o animazioni
Il centro è l'integrazione Web o cloud
Si dà peso alla testabilità
Si vuole una tecnologia a cui i giovani sviluppatori possano unirsi facilmente
Serve il supporto cross-platform
Si vuole dare peso fin dall'inizio ad accessibilità e high DPI

MFC non è una tecnologia «che da ora non vale la pena imparare», né una tecnologia «da scegliere perché è nuova»: è una tecnologia per confrontarsi con gli asset nativi Windows esistenti.

37. Come ragionare sulla migrazione da MFC

Se si vuole migrare un’app MFC verso un’altra tecnologia, puntare subito a una riscrittura totale è facile che fallisca.

Prima si scompone l’app in questo modo.

UI
Business logic
Formato dei file
Trattamento della comunicazione
Trattamento del database
Controllo dei dispositivi
Stampa
Integrazione COM/OLE
Gestione delle impostazioni
Log

Di queste, quella che dipende di più da MFC è la UI.

La business logic e il trattamento dei file, invece, si possono a volte estrarre.

L’ordine realistico della migrazione è questo.

1. Riprodurre l'ambiente di build
2. Fissare il comportamento esistente con dati di test
3. Estrarre la logica dagli handler degli eventi UI
4. Avvicinarla a una libreria C++ non MFC
5. Aggiungere test automatici
6. Documentare le specifiche esterne
7. Sostituire gradualmente a partire dalle schermate necessarie

Si ha più successo se lo scopo non è «abbandonare MFC» in sé, ma «far uscire la logica importante chiusa dentro MFC».

38. Da dove entrare quando si legge codice MFC

Quando si legge per la prima volta un progetto MFC esistente, si parte da file di questo tipo.

*.vcxproj
  si guardano toolset, impostazioni MFC, set di caratteri, impostazioni del runtime

resource.h
  si guardano gli ID delle risorse

*.rc
  si guardano dialoghi, menu, stringhe, icone

*App.cpp / *App.h
  si guardano la classe derivata da CWinApp e InitInstance

MainFrm.cpp / MainFrm.h
  si guardano il main frame e menu/toolbar

*Doc.cpp / *Doc.h
  se è Document/View, si guardano struttura dei dati e salvataggio

*View.cpp / *View.h
  si guardano disegno e operazioni dell'utente

*Dlg.cpp / *Dlg.h
  si guardano dialogo, DDX, trattamento dei pulsanti

Poi, le parole chiave di ricerca usate di frequente.

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

Cercando queste, il movimento dell’applicazione si vede più facilmente.

39. Linee di progettazione per mantenere MFC

Se si vogliono mantenere a lungo gli asset MFC esistenti, una linea di questo tipo è efficace.

Tenere sottili gli handler degli eventi UI
Non far trapelare troppo CString o CWnd nello strato non UI
Spostare la business logic in classi C++ ordinarie
Fare test di compatibilità del formato dei file
Rendere esplicita la differenza x86/x64
Revisionare i cambi di ID delle risorse
Verificare il module state delle DLL MFC
Allestire i log
Fissare la build in CI
Verificare periodicamente lo schermo a high DPI e su Windows 11

In particolare, è importante non stipare troppo trattamento in CDialog o CView.

Le classi schermo MFC si concentrano su input, visualizzazione e consegna degli eventi.

Prendere i valori dallo schermo
Passarli al servizio
Aggiornare lo schermo con il risultato

Se si riesce a tenerle a questo livello, anche MFC diventa piuttosto manutenibile.

40. Checklist operativa

Una checklist per quando si tratta un’app MFC.

La versione di Visual Studio è esplicita
I componenti MFC/ATL sono installati
Il target x86/x64 è esplicito
Si ha chiara l'impostazione Unicode/MBCS
MFC è linking statico o DLL condivisa
Quale Visual C++ Redistributable serve
Si stanno mettendo resource.h e .rc nel perimetro di review
Si sta seguendo il percorso degli eventi con la message map
La direzione di UpdateData è corretta
Si usa la struttura Document/View
La gestione della durata dei dialoghi modeless è sicura
Non si sta toccando la UI direttamente da un worker thread
In una DLL MFC ci sono punti in cui serve AFX_MANAGE_STATE
Si verifica lo schermo in un ambiente high DPI
Le dipendenze COM/ActiveX coprono 32-bit/64-bit
Il log resta
Si può testare la logica non UI

MFC, finché non ci si abitua, sembra peculiare; una volta capito dove guardare, si legge in modo piuttosto regolare.

41. Conclusione

MFC è un framework di lunga data per costruire applicazioni desktop native Windows in C++. Non è più il mainstream dello sviluppo nuovo, ma nella manutenzione degli asset esistenti, nell’aggiornamento dell’ambiente di build, nell’aggiunta di funzioni e nella migrazione graduale resta una tecnologia importante.

Per capire MFC, i punti particolarmente importanti sono i seguenti.

MFC rende più comodo trattare le Win32 API in C++
CWinApp gestisce l'applicazione intera
La durata di CWnd e quella di HWND non sono la stessa
La message map lega eventi e funzioni
Document/View è il meccanismo che separa dati e visualizzazione
DDX/DDV si usano per sincronizzare e convalidare i valori del dialogo
I file di risorse e resource.h sono molto importanti
CString e TCHAR si capiscono insieme all'impostazione della codifica
Nelle DLL MFC si sta attenti al module state
Le vecchie app MFC vale la pena rivederle da high DPI, x64, CI e test

Quando si tratta MFC, più che decidere «è vecchio quindi è cattivo», è importante prima capire la struttura. Nelle app MFC esistenti sono a volte condensati anni di conoscenza aziendale, specifiche per cliente, integrazione con i dispositivi, compatibilità dei file. Per proteggere quel valore e renderle a poco a poco più manutenibili, il percorso realistico è capire le convenzioni MFC, estrarre la logica non UI, e allestire build e test.

Se lo si riassume in una frase, è questa.

Una base per trattare i meccanismi delle app native Windows con classi C++ e un framework.

Con questo punto di vista, MFC non è una tecnologia semplicemente vecchia: diventa un indizio per leggere in sicurezza gli asset Windows esistenti.

Riferimenti

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Che cos'è MFC?
MFC è l'acronimo di Microsoft Foundation Classes, un framework per applicazioni Windows che rende più comodo trattare le Win32 API come classi C++. La finestra è rappresentata da CWnd, il dialogo da CDialog, l'applicazione intera da CWinApp. Non è «una libreria magica con cui si può scrivere senza conoscere Windows»: è un riordino dei meccanismi di Windows in tipi C++ e in un framework, quindi restano necessari anche i concetti Win32 come i messaggi Windows e gli handle.
Si può ancora usare MFC oggi? Il supporto continua?
MFC è ancora disponibile in Visual Studio e resta supportato. Nella documentazione Microsoft, però, si trova la nota che non verranno aggiunte nuove funzionalità né aggiornamenti della documentazione. Nel posizionamento attuale, la manutenzione delle app MFC esistenti, l'aggiunta di funzioni, l'aggiornamento dell'ambiente di build e la migrazione graduale verso un'altra UI sono casi realistici e frequenti; per un'app GUI generale del tutto nuova la scelta va valutata con cautela. Occorre installare il componente individuale di Visual Studio Installer (C++ MFC for latest build tools).
Che cos'è una message map in MFC?
È il meccanismo MFC che lega messaggi Windows e comandi a funzioni handler. Tra BEGIN_MESSAGE_MAP e END_MESSAGE_MAP, con macro come ON_BN_CLICKED e ON_COMMAND si descrive la corrispondenza «quando si fa clic su questo pulsante, chiama questa funzione». Se cercando una funzione non trovate una chiamata diretta, eppure viene eseguita all'evento, controllate la message map. Nella code review MFC è importante verificare la message map insieme alle funzioni handler, non le sole funzioni.
Come si passa da MFC a un'altra tecnologia?
Puntare subito a una riscrittura totale è facile che fallisca. L'ordine realistico è: riprodurre l'ambiente di build, fissare il comportamento esistente con dati di test, estrarre la logica dagli handler degli eventi UI e spostarla in una libreria C++ non MFC, aggiungere test automatici, documentare le specifiche esterne e poi sostituire gradualmente a partire dalle schermate necessarie. Si ha più successo se lo scopo non è «abbandonare MFC» in sé, ma «far uscire la logica importante chiusa dentro MFC».

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog