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.
flowchart LR
accTitle: Mappa della conoscenza della manutenzione di app desktop Windows con MFC
accDescr: Diagramma 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 COM
mfc["MFC (Microsoft Foundation Classes)"]
win32_api["Win32 API"]
cwinapp["CWinApp"]
message_map["Mappa dei messaggi"]
document_view_architecture["Architettura Document/View"]
mfc_ddx_ddv["DDX/DDV"]
resource_script["File di risorse (.rc/resource.h)"]
cstring_class["CString"]
vs_mfc_component["Componente MFC di Visual Studio Installer"]
mfc_shared_dll_linking["Modalità di collegamento di MFC (Use of MFC)"]
mfc_module_state["Stato del modulo MFC"]
activex["ActiveX"]
bitness_match_requirement["Requisito di corrispondenza del bitness"]
high_dpi_support["Supporto DPI elevato"]
new_gui_application_development["Sviluppo da zero di un'app GUI generica"]
legacy_app_maintenance_and_migration["Manutenzione e migrazione graduale di app MFC esistenti"]
mfc -->|"usa"| win32_api
mfc -->|"usa"| cwinapp
cwinapp -->|"automatizza"| win32_api
mfc -->|"implementa"| message_map
message_map -->|"richiede"| win32_api
mfc -->|"implementa"| document_view_architecture
document_view_architecture -->|"richiede"| cwinapp
message_map -.->|"usa"| document_view_architecture
mfc -->|"implementa"| mfc_ddx_ddv
mfc_ddx_ddv -->|"richiede"| resource_script
mfc -->|"usa"| cstring_class
mfc -->|"richiede"| vs_mfc_component
mfc -->|"configurato da"| mfc_shared_dll_linking
mfc -.->|"richiede"| mfc_module_state
mfc -.->|"usa"| activex
mfc -.->|"richiede"| bitness_match_requirement
mfc -.->|"richiede"| high_dpi_support
mfc -->|"sconsigliato per"| new_gui_application_development
mfc -->|"consigliato per"| legacy_app_maintenance_and_migration
mfc_module_state -.->|"richiede"| resource_script
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.
flowchart TB
accTitle: Dal messaggio Windows all'handler MFC
accDescr: Il 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.
MSG["Messaggio Windows<br/>WM_COMMAND / WM_PAINT e altri"] --> LOOP["Loop di messaggi<br/>che il framework MFC fa girare"]
LOOP --> PROC["CWnd::WindowProc<br/>punto di ingresso comune predisposto da MFC"]
PROC --> MAP{"Nella message map della propria classe<br/>c'è una voce corrispondente?"}
MAP -->|c'è| HANDLER["Chiama la funzione handler<br/>indicata da ON_BN_CLICKED e simili"]
MAP -->|non c'è| BASE["Cerca la message map della classe base<br/>da CDialogEx a CDialog, poi a CWnd"]
BASE --> FOUND{"Trovata da qualche parte?"}
FOUND -->|trovata| HANDLER
FOUND -->|assente fino in fondo| DEF["DefWindowProc<br/>delega 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.
12. App MFC basate su dialogo
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.
flowchart TB
accTitle: Relazione tra frame, view e document in MFC
accDescr: Il document template collega CWinApp, frame window, CDocument e CView; la view ottiene il document con GetDocument e il document notifica le view con UpdateAllViews.
APP["Derivata da CWinApp<br/>avvio e chiusura dell'app intera"] --> TPL["Document template<br/>CSingleDocTemplate / CMultiDocTemplate"]
TPL --> FRAME["Frame window<br/>CFrameWnd / CMDIChildWnd"]
TPL --> DOC["Derivata da CDocument<br/>dati e I/O su file"]
TPL --> VIEW["Derivata da CView<br/>disegno e operazioni dell'utente"]
FRAME --> VIEW
VIEW -->|ottenuta con GetDocument| DOC
DOC -->|notifica con UpdateAllViews| VIEW
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
- Raccolta di codice di riferimento, frammenti di questo articolo organizzati per capitolo - komurasoft-blog-samples (GitHub)
- MFC Desktop Applications - Microsoft Learn
- MFC e ATL - Microsoft Learn
- Creating an MFC Application - Microsoft Learn
- CWinApp Class - Microsoft Learn
- CWinApp: The Application Class - Microsoft Learn
- Mapping Messages - Microsoft Learn
- Document-View Architecture - Microsoft Learn
- Dialog Data Exchange and Validation - Microsoft Learn
- MFC Library Versions - Microsoft Learn
- Redistribute the MFC Library - Microsoft Learn
- Visual Studio Build Tools workload and component IDs - Microsoft Learn
- High DPI Desktop Application Development on Windows - Microsoft Learn
Articoli correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
L'API thread pool Win32 — Concorrenza senza creare thread, tramite CreateThreadpoolWork
State spargendo chiamate CreateThread per tutto il codice nativo? Questo articolo spiega l'API thread pool Win32 ridisegnata in Vista — i...
Named pipe in pratica — L'IPC standard di Windows, dalla progettazione alla sicurezza
Guida pratica alle named pipe, il meccanismo standard di comunicazione tra processi su Windows. L'articolo organizza, a partire dalle fon...
DllMain e il loader lock — Il vero motivo per cui vi dicono di «non fare niente nell'inizializzazione della DLL»
Perché non dovete chiamare LoadLibrary o sincronizzarvi con altri thread da DllMain. A partire dalle fonti primarie, l'articolo spiega co...
Sveglie spurie — Perché le condition variable si svegliano «senza essere notificate» e come attendere correttamente su Windows
L'attesa di una condition variable può ritornare anche quando non è arrivata nessuna notifica (una sveglia spuria). L'articolo spiega, a ...
Best practice pratiche sul multithreading: edizione C++ — eliminare gli incidenti strutturalmente con RAII e jthread
In C++ il multithreading è un mondo in cui una data race è comportamento indefinito. L'articolo tratta la trappola del distruttore di std...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
Servizi collegati all’argomento
L’articolo è direttamente collegato ai servizi seguenti.
Sviluppo di applicazioni Windows
Applicazioni aziendali, integrazione di dispositivi e strumenti di comunicazione, dai requisiti allo sviluppo.
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.