¿Qué es MFC en Windows? — Conocimientos básicos para mantener activos existentes
· Actualizado el: · Go Komura · Windows, MFC, VisualC++, Cpp, Win32, NativeApp, DesktopApp, LegacyCode, Aprovechamiento de activos existentes
1. Lo primero que conviene entender
Si mantiene aplicaciones de escritorio antiguas de Windows, es posible que se encuentre con nombres como estos.
CWinApp
CWnd
CDialog
CDialogEx
CFrameWnd
CDocument
CView
CString
CFile
CArchive
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_BN_CLICKED
DoDataExchange
UpdateData
Estos son nombres que aparecen con frecuencia en MFC, un framework de aplicaciones Windows para C++. MFC son las siglas de Microsoft Foundation Classes, una biblioteca que facilita el manejo de la API de Win32 mediante clases de C++.
En el desarrollo actual de aplicaciones Windows existen muchas opciones —WinUI, WPF, Windows Forms, Electron, Qt, tecnologías web, entre otras—, por lo que cada vez es menos habitual elegir MFC como primera opción para un desarrollo nuevo. Aun así, no es una tecnología desaparecida. En aplicaciones de negocio, equipos de medición, software de control, CAD/CAM, herramientas internas o paquetes de software con mucha antigüedad, todavía hay ocasiones en las que se mantiene una base de código en MFC.
Antes de continuar, conviene tener presentes estas ideas para entender MFC.
MFC es un mapa esencial para leer las aplicaciones de escritorio antiguas de Windows
MFC no oculta la API de Win32, sino que la envuelve al estilo de C++
Sin conocer las convenciones de MFC, es fácil malinterpretar el comportamiento más allá de lo que muestra el código
Aporta más valor en el mantenimiento, la prolongación de vida y la migración gradual de activos existentes que en una adopción nueva
En este artículo se organiza una visión general de MFC: la estructura de la aplicación, el mapa de mensajes, Document/View, los cuadros de diálogo, DDX/DDV, los recursos, la compilación y los puntos a tener en cuenta durante el mantenimiento.
El lector al que está dirigido este artículo es alguien que sabe programar en C++, pero no tiene experiencia con la API de Win32 ni con MFC. Con este nivel de conocimientos previos es suficiente.
| Área | Nivel esperado |
|---|---|
| C++ | Sabe leer clases, herencia, funciones virtuales, punteros y referencias |
| API de Win32 | No hace falta experiencia. Basta con haber oído hablar de ventanas y mensajes |
| Visual Studio | Ha abierto una solución y la ha compilado alguna vez |
| COM / OLE | No hace falta experiencia. Es suficiente con consultarlo cuando se necesite en el capítulo 27 |
Al final del capítulo 2 se enumeran los «conocimientos necesarios para leer MFC», pero no hace falta reunirlos todos de antemano. Basta con leer el artículo e ir volviendo a esa lista cuando haga falta.
Tampoco es necesario leer del capítulo 1 al 41 en orden. A continuación se indica el camino más corto según el objetivo.
| Objetivo | Capítulos a leer |
|---|---|
| Solo quiero saber qué es MFC y qué posición ocupa hoy | Capítulos 2 a 4, 36 |
| Quiero llegar a leer código MFC existente | Cap. 6 → 9 → 10 → 14 → 12 y 13 → 38 |
| Quiero solucionar un problema de compilación | Capítulos 5, 25, 32, 33 |
| Quiero investigar un fallo | Cap. 34 → 35 → 8 y 22 |
| Quiero decidir una política de mantenimiento o migración | Capítulos 30, 31, 37, 39 |
Lo primero que conviene dominar son dos cosas: el mapa de mensajes (capítulo 9) y Document/View (capítulo 14). Al entender estas dos, se vuelve posible leer «por qué se ejecuta una función cuyo punto de llamada no se encuentra» y «cómo se conectan los datos con la pantalla». Si se saltan estos dos capítulos, las explicaciones del resto quedan en el aire.
Además, los fragmentos de código que aparecen en este artículo están publicados en GitHub como una colección de referencia, organizada en archivos por capítulo.
windows-mfc-overview - komurasoft-blog-samples (GitHub)
2. Qué es MFC
MFC es una biblioteca de clases para crear aplicaciones de escritorio nativas de Windows en C++.
Cuando se usa la API de Win32 directamente, es típico escribir un código como este.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
// Procesamiento de dibujo
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
La API de Win32 es muy potente, pero como se construye principalmente con funciones en C, handles, mensajes y callbacks, en aplicaciones grandes tiende a volverse difícil de seguir. MFC hace que todo esto se pueda tratar como clases de C++: la ventana se representa con CWnd, el cuadro de diálogo con CDialog, la aplicación completa con CWinApp, la ventana de marco con CFrameWnd y la vista con CView, entre otras clases.
class CMainFrame : public CFrameWnd
{
public:
CMainFrame();
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
MFC no sustituye por completo la API de Win32 por algo distinto; es un framework de C++ delgado pero de gran alcance, construido sobre las ideas de la API de Win32. Por eso, para leer MFC hace falta algo más que conocer sus clases: también se necesitan estos otros conocimientos.
Mensajes de Windows
Handles como HWND
GDI/GDI+
Archivos de recursos
COM/OLE
DLL y runtime
Codificación de caracteres
Hilos y bucle de mensajes
Más que una «biblioteca mágica que permite programar sin conocer Windows», la realidad de MFC es la de «una forma de organizar el funcionamiento de Windows mediante tipos y un framework de C++».
3. ¿Se puede seguir usando MFC hoy?
MFC se puede seguir usando actualmente en Visual Studio. Sin embargo, conviene no confundir su posición: aunque sigue teniendo soporte, no es un framework de UI de última generación al que se le añadan funciones activamente, y la propia documentación de MFC de Microsoft señala que, si bien MFC continúa con soporte, no se añaden nuevas funciones ni se actualiza la documentación.
Por eso, la posición de MFC se puede resumir más o menos así.
Mantenimiento de una app MFC existente -> habitual en la práctica
Añadir funciones a una app MFC existente -> posible
Actualizar el entorno de compilación de la app -> importante
Migración gradual de MFC a otra UI -> posible
Adopción en una app GUI genérica totalmente nueva -> decidir con cautela
En particular, en aplicaciones de negocio que llevan mucho tiempo en uso, es habitual que la interfaz de usuario, la impresión, la entrada/salida de archivos, el control de dispositivos, protocolos propietarios y la integración con COM estén todos reunidos dentro de MFC.
En una base de código así, antes de «abandonar MFC» hace falta «llegar a poder leer MFC».
4. Los ámbitos en los que MFC destacaba
El ámbito representativo en el que se ha usado MFC es el de las aplicaciones de escritorio nativas de Windows.
En concreto, se trata de aplicaciones como estas.
Herramientas de negocio centradas en cuadros de diálogo
Apps SDI que abren y editan archivos
Apps MDI que manejan varios documentos
Pantallas de control de equipos de medición o maquinaria
Apps nativas del tipo CAD/CAM
Apps que hacen un uso intensivo de impresión y vista previa
Apps con integración ActiveX u OLE
Apps muy ligadas a la API de Windows antigua o a activos COM
El punto fuerte de MFC es que funciona muy cerca de los componentes nativos de Windows. Permite tratar como clases de C++ elementos como ventanas, menús, barras de herramientas, barras de estado, cuadros de diálogo, controles comunes, impresión, cuadros de diálogo de archivos, el registro y el dibujo GDI.
Por otro lado, el punto débil de MFC es que la construcción de UI moderna, el enlace de datos, la facilidad para hacer pruebas, el procesamiento asíncrono, los diseños modernos, la compatibilidad con alto DPI, la internacionalización y la accesibilidad no se pueden escribir con la misma naturalidad que en los frameworks más recientes.
Si se enumeran sus características, quedan así.
Muy cercano a lo nativo de Windows
Se puede controlar directamente desde C++
Hay muchos activos existentes
Se necesita conocer Win32
Conserva muchas convenciones antiguas
Hay que organizar uno mismo una estructura fácil de probar
5. Preparar Visual Studio para usar MFC
Aunque tenga instalado C++ en Visual Studio, eso no garantiza que MFC esté instalado, porque MFC se trata como un componente individual del Visual Studio Installer. Como referencia, conviene comprobar componentes como estos.
Desarrollo para escritorio con C++
MSVC v143 - VS 2022 C++ x64/x86 build tools
Windows SDK
C++ MFC for latest v143 build tools
C++ ATL for latest v143 build tools
Si se necesita o no la versión de MFC con Spectre Mitigations
Si al compilar no se encuentran los archivos relacionados con MFC, no basta con revisar la configuración del proyecto: también hay que comprobar si el componente MFC está instalado en Visual Studio.
Los pasos para seleccionarlo en el instalador son los siguientes.
1. Abra «Visual Studio Installer» desde el menú Inicio
2. Pulse «Modificar» en la instalación de Visual Studio correspondiente
3. En la pestaña «Cargas de trabajo», active la casilla «Desarrollo para escritorio con C++»
4. En el panel derecho «Detalles de la instalación» de la misma pantalla,
active la casilla «C++ MFC for latest v143 build tools»
5. Si no la encuentra, cambie a la pestaña «Componentes individuales»,
escriba MFC en el cuadro de búsqueda y localícela
6. Pulse «Modificar» para instalarla
Con solo seleccionar la carga de trabajo «Desarrollo para escritorio con C++» a veces no se instala MFC, así que en el paso 4 o el paso 5 hay que comprobar siempre que el propio componente tenga la casilla marcada.
Si se instala desde un script o desde CI, hay que especificar el ID del componente.
| Nombre visible | ID del componente |
|---|---|
| C++ MFC for latest v143 build tools | Microsoft.VisualStudio.Component.VC.ATLMFC |
| C++ ATL for latest v143 build tools | Microsoft.VisualStudio.Component.VC.ATL |
| Desarrollo para escritorio con C++ (carga de trabajo de Build Tools) | Microsoft.VisualStudio.Workload.VCTools |
Se puede comprobar si está instalado revisando si existe la carpeta atlmfc, donde se ubican los encabezados de MFC. Los encabezados y las bibliotecas de MFC se instalan dentro del conjunto de herramientas de MSVC.
<carpeta de instalación de Visual Studio>\VC\Tools\MSVC\<versión del toolset>\atlmfc\include\afxwin.h
Para buscarlo con PowerShell, se hace así.
Get-ChildItem -Path "$env:ProgramFiles\Microsoft Visual Studio\2022" -Recurse -Filter afxwin.h -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty FullName
Si no aparece nada, el componente MFC no está instalado.
Si se compila en ese estado, el proceso se detiene al intentar incluir afxwin.h. Lo habitual es que aparezca este error de compilación.
fatal error C1083: Cannot open include file: 'afxwin.h': No such file or directory
Un rodeo habitual es interpretar este error como «una ruta de inclusión mal configurada» y ponerse a buscar por las propiedades del proyecto. Cuando no se encuentran afxwin.h o afxdialogex.h, lo primero que hay que sospechar es del lado del Visual Studio Installer.
Lo mismo pasa en un entorno de CI o en un servidor de compilación: si la compilación funciona en el Visual Studio local pero falla en CI, la causa puede estar en la presencia o ausencia del componente MFC, o en una diferencia de versión del toolset de destino.
6. Estructura básica de una aplicación MFC
Una aplicación MFC suele tener, a grandes rasgos, la siguiente estructura.
Clase derivada de CWinApp
Se encarga de la inicialización y el cierre de toda la aplicación
Clase derivada de CFrameWnd / CMDIFrameWnd / CDialog
Se encarga de la ventana principal o de los cuadros de diálogo
Clase derivada de CView
Se encarga de la visualización en pantalla y de la interacción del usuario
Clase derivada de CDocument
Se encarga de los datos y de guardar archivos
Archivo de recursos
Contiene menús, cuadros de diálogo, iconos, cadenas de texto, etc.
Mapa de mensajes
Vincula los mensajes de Windows y los comandos con funciones controladoras
Por ejemplo, en una aplicación MFC sencilla aparece una clase derivada de CWinApp como esta.
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 es la clase que representa la aplicación completa.
En una aplicación MFC, normalmente existe un único objeto derivado de CWinApp.
Un objeto global como este theApp puede resultar extraño al principio, pero en MFC es la estructura estándar.
7. Qué hace CWinApp
CWinApp es importante como punto de entrada de una aplicación MFC. En una aplicación Win32 normal, WinMain, el registro de la clase de ventana y el bucle de mensajes se escriben a mano, pero en MFC gran parte de eso lo asume el framework. El desarrollador se dedica principalmente a sobrescribir InitInstance para escribir la inicialización específica de la aplicación.
BOOL CMyApp::InitInstance()
{
CWinApp::InitInstance();
// Cargar la configuración
// Inicializar COM
// Crear la ventana principal
// Registrar la plantilla de documento
return TRUE;
}
En InitInstance es habitual escribir procesos como estos.
Inicialización de los controles comunes
Configuración de las claves del registro
Carga de la lista de archivos usados recientemente
Registro de la plantilla de documento
Creación del marco principal
Procesamiento de los argumentos de la línea de comandos
Inicialización de COM/OLE
Al hacer mantenimiento, revisar primero la clase derivada de CWinApp facilita ver el orden de arranque de toda la aplicación.
8. CWnd, la clase central de MFC
La mayoría de las clases de UI de MFC tienen como base CWnd, que representa una ventana de Windows. Sin embargo, un objeto CWnd y un HWND no son lo mismo.
HWND
El handle de ventana que administra el propio Windows
CWnd
Un objeto contenedor (wrapper) de C++ que facilita el manejo de HWND
En MFC, CWnd mantiene internamente un HWND.
HWND hWnd = m_hWnd;
También se puede obtener así.
HWND hWnd = GetSafeHwnd();
Lo importante en el mantenimiento es que, aunque exista un CWnd*, el HWND correspondiente puede haberse destruido ya.
Por eso, para comprobar si una ventana sigue siendo válida se hace algo así.
if (pWnd != nullptr && ::IsWindow(pWnd->GetSafeHwnd()))
{
pWnd->ShowWindow(SW_SHOW);
}
Al investigar errores en MFC, es importante comprobar si el tiempo de vida del objeto C++ CWnd está desalineado respecto al tiempo de vida real del handle de ventana de Windows.
9. Qué es el mapa de mensajes
Uno de los mecanismos que mejor refleja el carácter de MFC es el mapa de mensajes.
Una aplicación Windows recibe los clics del ratón, la entrada de teclado, el redibujado, el cambio de tamaño de ventana y la selección de menú, entre otras cosas, en forma de mensajes de Windows.
En la API de Win32, los mensajes normalmente se procesan con una instrucción switch dentro de WndProc.
En MFC, eso se vincula a funciones controladoras mediante el mapa de mensajes.
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"));
}
El significado de este código es el siguiente.
Cuando se hace clic en el botón IDC_BUTTON_OK
se llama a CMyDialog::OnClickedButtonOk
Si se representa en un diagrama el flujo desde que llega el mensaje hasta que se llama al controlador, queda así.
flowchart TB
MSG["Mensaje de Windows<br/>WM_COMMAND / WM_PAINT, etc."] --> LOOP["Bucle de mensajes<br/>lo ejecuta el framework de MFC"]
LOOP --> PROC["CWnd::WindowProc<br/>la ventanilla común que prepara MFC"]
PROC --> MAP{"¿Hay una entrada que coincide<br/>en el mapa de mensajes de la propia clase?"}
MAP -->|Sí| HANDLER["Llama a la función controladora<br/>que indica ON_BN_CLICKED, etc."]
MAP -->|No| BASE["Busca en el mapa de mensajes de la clase base<br/>recorre CDialogEx, luego CDialog, luego CWnd"]
BASE --> FOUND{"¿Se encontró en algún punto?"}
FOUND -->|Sí| HANDLER
FOUND -->|No, hasta el final| DEF["DefWindowProc<br/>lo deja en manos del procesamiento predeterminado de Windows"]
No se ve ninguna instrucción switch porque el framework se encarga de esta parte, la de «recorrer el mapa de mensajes desde la propia clase hasta la clase base». Resulta más fácil de entender si se piensa en DECLARE_MESSAGE_MAP y BEGIN_MESSAGE_MAP como macros que preparan, para cada clase, la tabla de correspondencias necesaria para esta búsqueda.
Si no está acostumbrado a leer MFC, puede resultar difícil saber desde dónde se llama a una función. Cuando la búsqueda no encuentra ninguna llamada directa, hay que mirar el mapa de mensajes.
La función no se llama directamente
pero se ejecuta al producirse un evento
-> revise las macros BEGIN_MESSAGE_MAP / ON_...
En una revisión de código de MFC, es importante examinar el mapa de mensajes junto con la función controladora, en vez de mirar solo esta última.
10. Enrutamiento de comandos
En MFC, las acciones sobre menús y barras de herramientas también se tratan como comandos.
El caso representativo es ON_COMMAND.
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
END_MESSAGE_MAP()
void CMainFrame::OnFileOpen()
{
// Procesamiento para abrir el archivo
}
MFC dispone de un mecanismo para enviar los comandos al objeto adecuado.
Por ejemplo, para un mismo ID_EDIT_COPY, quién lo procesa puede variar entre la vista activa en ese momento, el documento, el marco o la aplicación.
Vista activa
Documento
Ventana de marco
Aplicación
El comando va pasando en este orden a los objetos que puedan procesarlo.
Por eso, en MFC a veces es difícil rastrear «qué función se llama al pulsar un menú» con una simple búsqueda de texto.
Los puntos a revisar durante el mantenimiento son estos.
Cuál es el ID del comando
En qué clase está el ON_COMMAND
Dónde está el ON_UPDATE_COMMAND_UI
Cuál es la vista activa en ese momento
Si se usa la estructura Document/View
11. Qué es ON_UPDATE_COMMAND_UI
En MFC, a veces se usa ON_UPDATE_COMMAND_UI para actualizar el estado habilitado/deshabilitado, el estado de marcado o el texto mostrado de los elementos de menú y los botones de la barra de herramientas.
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());
}
De esta manera, el menú o el botón solo se habilitan cuando es posible eliminar.
Al usar una aplicación MFC, si un botón aparece en gris sin razón aparente, un menú no se puede pulsar, o el estado de marcado cambia, buscar ON_UPDATE_COMMAND_UI puede revelar la causa.
12. Aplicaciones MFC basadas en cuadros de diálogo
Una de las formas más fáciles de entender en MFC es la aplicación basada en cuadros de diálogo.
Las pantallas de configuración, las herramientas de negocio sencillas o las pantallas de manejo de equipos suelen estar organizadas en torno a un cuadro de diálogo.
Lo típico es heredar de CDialog o de CDialogEx.
class CSettingsDialog : public CDialogEx
{
public:
CSettingsDialog(CWnd* pParent = nullptr);
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_SETTINGS_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
afx_msg void OnBnClickedOk();
DECLARE_MESSAGE_MAP()
private:
CString m_name;
int m_interval;
};
En el código basado en cuadros de diálogo aparecen a menudo estos elementos.
IDD_... ID del recurso de cuadro de diálogo
IDC_... ID del control
OnInitDialog Procesamiento de inicialización
DoDataExchange Asociación entre controles y variables miembro
UpdateData Sincronización entre la pantalla y las variables
ON_BN_CLICKED Procesamiento del clic en un botón
Un cuadro de diálogo no funciona solo por su apariencia: combina recursos, variables miembro, el mapa de mensajes y el procesamiento de inicialización.
13. DDX y DDV
En los cuadros de diálogo de MFC aparecen a menudo DDX y DDV.
DDX = Dialog Data Exchange
DDV = Dialog Data Validation
DDX es el mecanismo que asocia los controles del cuadro de diálogo con las variables miembro de C++, y DDV es el mecanismo que valida esos valores introducidos.
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);
}
Al llamar a UpdateData(TRUE), los valores introducidos en pantalla se reflejan en las variables miembro.
void CSettingsDialog::OnBnClickedOk()
{
if (!UpdateData(TRUE))
{
return;
}
// En este punto, m_name y m_interval contienen los valores introducidos en pantalla
SaveSettings(m_name, m_interval);
CDialogEx::OnOK();
}
A la inversa, al llamar a UpdateData(FALSE), el valor de las variables miembro se refleja en pantalla.
BOOL CSettingsDialog::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_name = _T("default");
m_interval = 60;
UpdateData(FALSE);
return TRUE;
}
Cuando un valor introducido en un cuadro de diálogo de MFC no es correcto, conviene revisar estos puntos.
Si DDX está definido en DoDataExchange
Si se está llamando a UpdateData(TRUE)
Si el momento en que se llama a UpdateData(FALSE) es correcto
Si DDV está rechazando la entrada
Si el ID del control coincide con el del recurso
14. La arquitectura Document/View
Una de las grandes características de MFC es la arquitectura Document/View.
Es una estructura para separar los datos que maneja la aplicación de su presentación en pantalla.
CDocument
Mantiene los datos
Se encarga de leer y escribir el archivo
Notifica las actualizaciones a varias vistas
CView
Muestra los datos
Gestiona la interacción del usuario
Administra el dibujo y el estado de selección
Si se representa en un diagrama la relación entre el marco, la vista y el documento, queda así.
flowchart TB
APP["Derivada de CWinApp<br/>arranque y cierre de toda la app"] --> TPL["Plantilla de documento<br/>CSingleDocTemplate / CMultiDocTemplate"]
TPL --> FRAME["Ventana de marco<br/>CFrameWnd / CMDIChildWnd"]
TPL --> DOC["Derivada de CDocument<br/>datos y E/S de archivos"]
TPL --> VIEW["Derivada de CView<br/>dibujo e interacción del usuario"]
FRAME --> VIEW
VIEW -->|se obtiene con GetDocument| DOC
DOC -->|notifica con UpdateAllViews| VIEW
El punto clave es que lo que conecta el marco, el documento y la vista es la plantilla de documento. Como esta plantilla se registra dentro de InitInstance, cuando se quiera saber «qué vista muestra qué documento», lo primero es leer InitInstance.
También conviene tener clara la diferencia de dirección: se usa GetDocument cuando la vista consulta el documento, y UpdateAllViews cuando el documento propaga un cambio a todas las vistas; entenderlo facilita rastrear los fallos en los que la pantalla no se actualiza.
Por ejemplo, en aplicaciones como un editor de texto, un editor de figuras, una herramienta de edición de archivos de configuración o un CAD, tiene sentido separar los datos de la presentación.
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;
};
En el lado de la vista, se obtiene el documento y se dibuja.
void CMyView::OnDraw(CDC* pDC)
{
CMyDocument* pDoc = GetDocument();
if (pDoc == nullptr)
{
return;
}
for (const auto& item : pDoc->m_items)
{
// Dibujar usando pDC
}
}
La ventaja de Document/View es que facilita mostrar los mismos datos en varias vistas.
Por ejemplo, los mismos datos se pueden mostrar así.
Vista de tabla
Vista de gráfico
Vista de detalle
Vista previa
Vista de impresión
Sin embargo, si se usa Document/View en una pantalla de configuración sencilla o en una herramienta pequeña, la estructura puede llegar a sentirse innecesariamente pesada.
En el mantenimiento, determinar primero si la aplicación usa Document/View o está centrada en un cuadro de diálogo facilita seguir el código.
15. SDI y MDI
En MFC, combinadas con Document/View, aparecen a menudo las estructuras SDI y MDI.
SDI = Single Document Interface
MDI = Multiple Document Interface
SDI es, básicamente, la forma en la que un solo marco maneja un solo documento.
Ventana principal
Un documento
Una o varias vistas
MDI es la forma en la que una ventana principal contiene varias ventanas hijas, y cada una de ellas maneja un documento.
Marco padre MDI
Marco hijo MDI 1 -> documento 1
Marco hijo MDI 2 -> documento 2
Marco hijo MDI 3 -> documento 3
En las aplicaciones antiguas de Windows, MDI se usaba con frecuencia.
En el mantenimiento, con solo mirar el nombre de la clase se puede intuir la estructura.
CFrameWnd Marco de tipo SDI
CMDIFrameWnd Marco padre MDI
CMDIChildWnd Marco hijo MDI
CSingleDocTemplate Plantilla de documento para SDI
CMultiDocTemplate Plantilla de documento para MDI
En las aplicaciones creadas con el asistente de MFC, es habitual que dentro de InitInstance haya un proceso de registro de CSingleDocTemplate o CMultiDocTemplate.
16. Entender el archivo de recursos
En una aplicación MFC, el archivo .rc es muy importante.
.rc es un archivo de recursos de Windows.
En él se definen elementos como los siguientes.
Plantillas de cuadros de diálogo
Menús
Teclas aceleradoras
Iconos
Mapas de bits
Tablas de cadenas de texto
Información de versión
Barras de herramientas
Además, en resource.h se definen los ID de los recursos.
#define IDD_SETTINGS_DIALOG 101
#define IDC_EDIT_NAME 1001
#define IDC_EDIT_INTERVAL 1002
#define ID_FILE_OPEN 32771
En el código de MFC, estos ID se usan para vincular los recursos con el código C++.
DDX_Text(pDX, IDC_EDIT_NAME, m_name);
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
Un problema habitual en el mantenimiento es la inconsistencia de los ID de recursos.
El ID en resource.h cambió
El ID entró en conflicto al fusionar otra rama
El ID del control en el cuadro de diálogo no coincide con el ID en DDX
Queda un ID de menú que debería haberse eliminado
Un ID de la tabla de cadenas de texto está duplicado
Al investigar el comportamiento de una aplicación MFC, hay que revisar a la vez el .rc y resource.h, no solo el código C++.
17. Class Wizard y el código escrito a mano
MFC tiene una historia profundamente ligada al Class Wizard de Visual Studio.
Con Class Wizard se pueden generar automáticamente controladores de mensajes, variables DDX, sobrescrituras de funciones virtuales y más.
Por eso, en el código de MFC queda mucho código con la forma que genera la herramienta.
//{{AFX_DATA(CSettingsDialog)
//}}AFX_DATA
//{{AFX_MSG(CSettingsDialog)
//}}AFX_MSG
En versiones más nuevas de Visual Studio, la apariencia y la forma generada pueden haber cambiado, pero en bases de código antiguas todavía pueden quedar marcadores de comentario como estos.
Lo importante en el mantenimiento es no romper de forma descuidada el límite entre el código generado y el código escrito a mano.
No elimine el mapa de mensajes
No rompa las correspondencias de DDX
No cambie el ID de recurso sin cuidado
No borre sin necesidad los comentarios que presuponen el antiguo Class Wizard
En MFC no basta con que el código compile como C++. También hay que conservar, hasta cierto punto, la forma que espera el editor de recursos o Class Wizard de Visual Studio.
18. CString y las cadenas de texto
La clase de cadenas de texto que aparece con frecuencia en MFC es CString.
CString name = _T("Komura");
CString message;
message.Format(_T("Hello, %s"), name.GetString());
CString es una clase de cadena de longitud variable que se usa con frecuencia en el código de la familia MFC/ATL. En el C++ moderno es habitual usar std::string o std::wstring, pero en MFC se recurre mucho a CString por su compatibilidad con la API y los controles.
Durante el mantenimiento hay que tener presente la codificación de caracteres.
CString equivale a CStringA o CStringW según la configuración del proyecto
CStringA de la familia ANSI / MBCS
CStringW de la familia Unicode / UTF-16
LPCTSTR puntero de cadena basado en TCHAR
LPCSTR de la familia char
LPCWSTR de la familia wchar_t
std::string normalmente de la familia char
std::wstring de la familia wchar_t
En las aplicaciones Windows actuales, lo más seguro es asumir básicamente Unicode, pero en las aplicaciones MFC antiguas puede quedar código que presupone MBCS.
CString text = _T("Japonés");
std::wstring ws(text.GetString());
Los errores de conversión de cadenas son frecuentes en el mantenimiento de aplicaciones MFC.
Conviene prestar especial atención a casos como estos.
Leer un archivo que presupone Shift_JIS
Cambiar a una compilación Unicode
Un DLL externo que exige char*
COM que exige BSTR
Convertir a la ligera a std::string y que el texto se corrompa
Al encontrarse con CString, en lugar de pensar simplemente «es una clase de cadena antigua», es importante revisarla junto con la configuración del juego de caracteres del proyecto, las API externas y el formato de archivo.
19. CFile y CArchive
MFC también dispone de clases para operar con archivos y para la serialización. Las más representativas son CFile y CArchive.
CFile file;
if (file.Open(path, CFile::modeRead))
{
CArchive ar(&file, CArchive::load);
// Leer desde ar
}
CArchive se usa con frecuencia en el mecanismo de serialización de MFC.
En las clases derivadas de CDocument, es habitual sobrescribir Serialize para escribir la lectura y el guardado en la misma función.
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 serialización de MFC es cómoda, pero requiere cuidado en un uso a largo plazo.
Compatibilidad con formatos de archivo antiguos
Gestión del número de versión
Recuperación cuando falla la lectura
Manejo de excepciones
Codificación de caracteres
Endianness
Si se está guardando una estructura tal cual
En una aplicación MFC que lleva mucho tiempo usando un formato binario propio, Serialize puede haberse convertido de facto en la especificación del archivo.
En ese caso, antes de modificar el código conviene preparar siempre datos de prueba que permitan leer los archivos existentes.
20. El dibujo GDI y CDC
En el dibujo de pantalla de MFC, se usa con frecuencia la clase CDC para manejar el contexto de dispositivo (Device Context) de Windows. En CView::OnDraw se pasa un CDC* como argumento.
void CMyView::OnDraw(CDC* pDC)
{
pDC->TextOut(10, 10, _T("Hello MFC"));
pDC->Rectangle(10, 40, 200, 120);
}
Cuando se usan plumas (pens) o pinceles (brushes), hay que tener cuidado con la selección y su restauración.
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);
}
En torno a los objetos GDI, los errores que suelen dar problemas son estos.
No restaurar el objeto original después de llamar a SelectObject
Crear muchos objetos GDI sin destruirlos
Confundir los roles de OnPaint y OnDraw
Parpadeo por no usar doble búfer
El dibujo con píxeles fijos se rompe en alto DPI
Ante un error de dibujo en MFC, hay que revisar no solo la lógica de C++, sino también los recursos GDI de Windows, el momento del redibujado, el DPI y el tamaño de fuente.
21. Cuadros de diálogo modales y no modales
En MFC también hay que prestar atención a la forma de mostrar los cuadros de diálogo.
Un cuadro de diálogo modal se muestra con DoModal.
CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
// Procesamiento cuando se pulsa Aceptar
}
En este caso, quien realiza la llamada espera hasta que se cierra el cuadro de diálogo.
En cambio, en un cuadro de diálogo no modal, el control vuelve a quien lo llamó justo después de crearlo.
m_pToolDialog = new CToolDialog(this);
m_pToolDialog->Create(IDD_TOOL_DIALOG, this);
m_pToolDialog->ShowWindow(SW_SHOW);
En los cuadros de diálogo no modales, la gestión del tiempo de vida es importante.
Cuándo se hace delete del diálogo creado con new
Si la ventana padre no se destruye antes
Si el diálogo usa PostNcDestroy
Si no se llega a crear por duplicado
Si no queda un puntero colgante después de cerrarlo
En la investigación de bloqueos de MFC, la causa puede estar en un problema de tiempo de vida de un cuadro de diálogo no modal.
22. El tiempo de vida de los objetos C++ y de los handles de Windows
Algo muy importante en MFC es la diferencia entre el tiempo de vida de un objeto C++ y el de un handle de Windows. Por ejemplo, CWnd es un objeto C++, pero la ventana real la administra Windows como un HWND, y ambas cosas no siempre se crean ni se destruyen al mismo tiempo.
Existe el objeto CWnd pero el HWND todavía no
El HWND se destruyó pero el objeto CWnd sigue existiendo
Se ha creado un objeto CWnd contenedor temporal
Se reasigna el handle con Attach/Detach
Por ejemplo, un código como este merece atención.
CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// Guardar pWnd en una variable miembro para usarlo más tarde
Si se conserva durante mucho tiempo el puntero obtenido con GetDlgItem, existe el riesgo de acceder a él después de que se haya destruido la ventana.
Si hace falta, en algunos casos es más seguro llamar a GetDlgItem cada vez, o administrar una variable miembro del control mediante DDX.
DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);
En MFC, que un puntero no sea nulo no garantiza que sea seguro.
if (m_pDialog != nullptr && ::IsWindow(m_pDialog->GetSafeHwnd()))
{
m_pDialog->SetWindowText(_T("Running"));
}
Esta sensibilidad es muy importante en el mantenimiento de MFC.
23. Hilos y actualización de la UI
Básicamente, la UI de Windows debe manipularse desde el hilo de UI en el que se creó, y lo mismo ocurre en una aplicación MFC. Manipular directamente un control de UI desde un hilo de trabajo (worker thread) puede provocar un comportamiento inestable o un bloqueo.
Un ejemplo que conviene evitar.
UINT WorkerThreadProc(LPVOID pParam)
{
CMyDialog* pDlg = static_cast<CMyDialog*>(pParam);
// Evite tocar la UI directamente desde un hilo de trabajo
pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));
return 0;
}
Lo habitual es notificar al hilo de UI mediante PostMessage, entre otras opciones.
constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;
UINT WorkerThreadProc(LPVOID pParam)
{
HWND hWnd = static_cast<HWND>(pParam);
// Procesamiento pesado
::PostMessage(hWnd, WM_APP_WORK_DONE, 0, 0);
return 0;
}
En el lado de la UI, se recibe mediante el mapa de mensajes.
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;
}
En lo relativo a los hilos en MFC, hay que revisar lo siguiente.
Si se está tocando la UI directamente desde un hilo de trabajo
Si se llama a PostMessage después de destruir la ventana
Si se detiene el hilo de UI esperando a que termine otro hilo
Si el bloqueo de los datos compartidos es adecuado
Si se malinterpreta el valor de retorno y el tiempo de vida de AfxBeginThread
24. Los DLL de MFC y el estado del módulo
Al crear un DLL con MFC aparece el concepto de estado del módulo.
En situaciones como cargar recursos desde un DLL de MFC, mostrar un cuadro de diálogo o crear un DLL de extensión, el problema es a qué módulo se va a buscar los recursos.
En el punto de entrada de una función de un DLL de MFC, es posible encontrarse con una macro como esta.
AFX_MANAGE_STATE(AfxGetStaticModuleState());
Esto sirve para que MFC pueda usar el estado de módulo correcto.
Si se olvida esta macro, se producen fallos como estos.
No se encuentra el recurso de cuadro de diálogo dentro del DLL
Los recursos de cadena de texto se leen desde otro módulo
No se encuentran iconos o menús
Funciona solo en depuración y falla en la versión release
Al mantener un DLL de MFC, es importante tener claras las relaciones entre el EXE, el DLL normal, el DLL de extensión y el DLL de recursos.
Hay que tener especial cuidado cuando una aplicación que no es MFC llama a un DLL de MFC, o cuando la configuración es de tipo plugin.
25. Vincular MFC de forma estática o usarlo como DLL compartido
En una aplicación MFC, la configuración del proyecto incluye la opción «Use of MFC».
Las dos opciones representativas son estas.
Use MFC in a Shared DLL
Use MFC in a Static Library
Si se usa un DLL compartido, hace falta que el entorno de ejecución tenga el runtime de MFC y el runtime de Visual C++ correspondientes.
Con la vinculación estática, el paquete de distribución puede parecer más simple, pero hay que tener en cuenta el tamaño del ejecutable, las actualizaciones, la incorporación de correcciones de seguridad y las condiciones de licencia y redistribución.
Ninguna de las dos opciones es siempre la correcta.
Los criterios para decidir son estos.
Si en el destino se puede instalar el Visual C++ Redistributable
Si se quiere que la app se acerque a un único exe
Cómo se van a aplicar las actualizaciones de seguridad
Si varias apps van a compartir el mismo runtime
Si se puede preparar un instalador
Cuál es la versión de Windows de destino
En el mantenimiento, lo primero es comprobar la configuración actual.
Configuration Properties
General
Use of MFC
Además, hay que revisar la configuración de Runtime Library.
/MD Multi-threaded DLL
/MDd Multi-threaded Debug DLL
/MT Multi-threaded
/MTd Multi-threaded Debug
Si se mezclan configuraciones de vinculación entre MFC y el CRT, pueden aparecer problemas de asignación y liberación de memoria en el límite entre bibliotecas.
26. Unicode, MBCS y TCHAR
En el código MFC antiguo aparecen con frecuencia TCHAR, LPCTSTR y la macro _T().
CString title = _T("Configuración");
SetWindowText(title);
Esta forma de escribirlo permite dar soporte tanto a compilaciones Unicode como a compilaciones MBCS.
Compilación Unicode
TCHAR -> wchar_t
LPCTSTR -> const wchar_t*
_T("...") -> L"..."
Compilación MBCS
TCHAR -> char
LPCTSTR -> const char*
_T("...") -> "..."
Actualmente lo habitual es compilar en Unicode, pero en las aplicaciones antiguas puede quedar procesamiento que presupone MBCS.
En particular, hay que comprobar las suposiciones sobre la codificación de caracteres en archivos externos, protocolos de comunicación, DLL antiguos, conexiones a bases de datos y comunicación serie, entre otros.
Conviene no considerar la conversión a Unicode como una simple sustitución mecánica.
Si el tamaño de un array de char está en bytes o en caracteres
Si se está usando strlen
Si se está usando sizeof(buffer) como si fuera el número de caracteres
Si la API externa acepta UTF-16 o Shift_JIS
Si está bien que cambie el formato de guardado del archivo
Al corregir algo relacionado con las cadenas de texto en MFC, hay que revisar no solo la visualización en pantalla, sino también la compatibilidad de archivos y la integración externa.
27. MFC y COM/OLE/ActiveX
MFC también se ha usado en aplicaciones muy relacionadas con COM, OLE y ActiveX.
En aplicaciones de negocio antiguas pueden quedar elementos como los siguientes.
OLE Automation
ActiveX Control
Servidor COM
Cliente COM
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant
En el proceso de arranque de una aplicación MFC puede aparecer un código como este.
if (!AfxOleInit())
{
AfxMessageBox(_T("OLE initialization failed"));
return FALSE;
}
Cuando se usa COM/OLE, un problema que parece de MFC puede en realidad deberse a la inicialización de COM, el modelo de hilos, el conteo de referencias, la información de registro o la diferencia entre 32 y 64 bits.
Conviene prestar especial atención a los componentes ActiveX o COM de 32 bits.
Desde una app MFC de 32 bits se usa COM de 32 bits
Desde una app MFC de 64 bits se usa COM de 64 bits
El registro de COM de 32 y de 64 bits es independiente
Un ActiveX antiguo puede no ser compatible con 64 bits
Al portar una aplicación MFC a x64, hay que revisar siempre las dependencias de COM/OLE, además del código de la UI.
28. La compatibilidad con alto DPI y el Windows actual
Si se ejecuta una aplicación MFC antigua en un Windows actual, la visualización puede romperse en un entorno de alto DPI.
Por ejemplo, problemas como estos.
El texto se corta
Los botones quedan demasiado pequeños
El dibujo con píxeles fijos se desplaza
Se rompe cuando hay varios monitores con distinto factor de escala
Los mapas de bits antiguos se ven borrosos
El diseño del cuadro de diálogo queda apretado
En una aplicación de escritorio de Windows, la propia aplicación debe declarar explícitamente su modo de compatibilidad con DPI.
También en una aplicación MFC hay que revisar el manifiesto, los recursos, el código de dibujo, las fuentes y el diseño.
En particular, un código como este, que escribe coordenadas fijas directamente, tiende a dar problemas en alto DPI.
pDC->TextOut(10, 10, _T("Status"));
pDC->Rectangle(10, 40, 200, 80);
Las coordenadas que presuponen píxeles fijos rompen el aspecto visual cuando cambia el DPI.
Los puntos de revisión durante el mantenimiento son estos.
La configuración de DPI del manifiesto de la aplicación
La fuente de los recursos de cuadro de diálogo
El dibujo con píxeles fijos
La resolución de los recursos de imagen
El comportamiento con varios monitores
La visualización en Windows 10 / Windows 11
La compatibilidad de MFC con alto DPI no siempre termina con solo cambiar la configuración del proyecto. En una UI antigua hace falta comprobar la pantalla real y corregir el diseño.
29. Manejo de excepciones y de errores
MFC tiene sus propias clases de excepción y sus propias convenciones para el manejo de errores.
En código antiguo se pueden encontrar macros como estas.
TRY
{
// Procesamiento
}
CATCH(CFileException, e)
{
e->ReportError();
}
END_CATCH
También hay código en el que se mezcla con el try / catch del C++ moderno.
try
{
DoSomething();
}
catch (const std::exception& ex)
{
// Escribir en el log
}
En el mantenimiento de MFC conviene prestar atención a estos puntos.
Si se están mezclando las excepciones de MFC con las excepciones estándar de C++
Si se está malinterpretando el tiempo de vida del objeto de excepción
Si se entienden las antiguas macros THROW/CATCH
Si se mezclan errores por valor de retorno con excepciones
Si el único registro del error es un AfxMessageBox, sin quedar constancia en el log
En una aplicación de negocio, es importante no limitarse a mostrar el mensaje de error en pantalla, sino dejar constancia del log, el historial de operaciones, los valores introducidos y el estado de la conexión externa.
En una aplicación MFC antigua, el manejo de un error a veces se limita a un AfxMessageBox.
AfxMessageBox(_T("No se pudo guardar."));
Para mejorar la mantenibilidad, conviene separar la visualización en la UI del registro en el log.
LogError(_T("Save failed"), path);
AfxMessageBox(_T("No se pudo guardar. Consulte el registro."));
30. Cómo hacer convivir MFC con el C++ moderno
El hecho de mantener una aplicación MFC no significa que todo tenga que ajustarse al estilo de C++ antiguo.
Se puede respetar las convenciones de MFC en la capa de UI y, al mismo tiempo, organizar la lógica de dominio y los cálculos con C++ moderno.
Por ejemplo, se puede dividir así.
Capa MFC
CDialog
CView
CDocument
CString
Mapa de mensajes
Operaciones sobre recursos
Capa no MFC
std::string / std::wstring
std::vector
std::optional
std::variant
std::filesystem
Clases que se pueden probar con pruebas unitarias
Lógica de negocio
Una mala forma es aquella en la que todo el procesamiento está apilado dentro de la clase del cuadro de diálogo.
void CMainDialog::OnBnClickedExecute()
{
// Obtener la entrada
// Leer el archivo
// Comunicación
// Cálculo
// Actualizar la base de datos
// Actualizar la pantalla
// Escribir en el log
// Manejo de excepciones
}
Un código así es difícil de modificar, difícil de probar y también dificulta la investigación de errores.
Para mejorarlo, hay que sacar la lógica fuera de la clase 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);
}
De esta manera, m_service.Execute se puede probar sin necesidad de MFC.
La mejora más eficaz al mantener activos MFC existentes es ir extrayendo la lógica de la clase de UI poco a poco.
31. Escribir código MFC fácil de probar
Una aplicación MFC, tal cual, suele ser difícil de probar con pruebas unitarias.
La razón es que la UI, Win32, los archivos, las comunicaciones, la base de datos y el estado global tienden a quedar fuertemente acoplados.
El enfoque para facilitar las pruebas es el siguiente.
No intente probar directamente CDialog o CView
Primero extraiga la lógica que no es de UI
Convierta los tipos de MFC en el límite
Convierta los archivos y las comunicaciones en interfaces
Deje los controladores de eventos de pantalla lo más ligeros posible
Por ejemplo, se traslada el procesamiento a una clase C++ pura.
class PriceCalculator
{
public:
int CalculateTotal(const std::vector<int>& prices) const
{
int total = 0;
for (int price : prices)
{
total += price;
}
return total;
}
};
El lado de MFC se encarga únicamente de la entrada y la salida.
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 esta estructura, PriceCalculator y ParsePrices se pueden probar con un framework de pruebas de C++ normal.
No hace falta rehacer toda la aplicación MFC de una vez; ya tiene efecto simplemente empezar sacando del controlador de eventos el procesamiento que se pueda probar.
32. Fijar el entorno de compilación
En el mantenimiento de una aplicación MFC, es importante fijar el entorno de compilación.
En una base de código antigua, el resultado de la compilación puede variar por diferencias como las siguientes.
La versión de Visual Studio
La versión del toolset de MSVC
La versión del Windows SDK
La presencia o ausencia de los componentes MFC/ATL
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
Vinculación estática o DLL compartido de MFC
La configuración de la biblioteca en tiempo de ejecución
Los encabezados precompilados
En una aplicación MFC, stdafx.h o pch.h a veces concentran muchas dependencias.
#include "framework.h"
#include "MyApp.h"
La compilación se puede romper por motivos como que un solo archivo tenga una configuración de compilación distinta, o que la configuración de los encabezados precompilados esté desalineada.
En un proyecto de mantenimiento, dejar esta información documentada de forma explícita en el README facilita las cosas más adelante.
La versión de Visual Studio necesaria
Las cargas de trabajo y componentes individuales necesarios
El Windows SDK necesario
La plataforma de destino
El método de vinculación de MFC
Los pasos de compilación
Cómo ejecutar el CI
Cómo generar el paquete de distribución
Superar el «en mi PC compila» es el primer paso del mantenimiento de MFC.
33. Compilar MFC en CI
Una aplicación MFC también se puede compilar en CI.
Sin embargo, el entorno de CI debe tener instalado el componente MFC.
Si se usa Visual Studio Build Tools, hay que especificar el ID del componente MFC/ATL para instalarlo. El ID que se debe indicar es el mismo que aparece en la tabla del capítulo 5.
Los puntos que conviene comprobar son los siguientes.
Si MFC está instalado en Build Tools
Si el toolset de destino coincide con el del proyecto
Si el Windows SDK está instalado
Si se comprueban tanto la compilación x86 como la x64
Si el compilador de recursos funciona
Si existe un proceso de firma
Si la generación del instalador también está dentro del alcance del CI
En una aplicación MFC no es sencillo llegar a automatizar las pruebas de UI, pero al menos automatizar lo siguiente resulta útil.
Compilación en Debug / Release
Compilación x86 / x64
Análisis estático
Pruebas unitarias
Creación del instalador
Guardar el hash de los artefactos
Comprobación de los DLL de los que depende
En un mantenimiento a largo plazo, mantener la aplicación en un estado en el que compile ya aporta un gran valor por sí solo.
34. Dónde mirar al depurar una aplicación MFC
Al rastrear un fallo en una aplicación MFC, resulta eficiente revisar en este orden.
1. En qué pantalla ocurre
2. Si es un cuadro de diálogo, una View o un Frame
3. Cuál es el ID de recurso que corresponde a la acción
4. A qué función lleva el mapa de mensajes
5. Si la dirección de UpdateData es correcta
6. Si es Document/View, cuál es el estado del Document
7. Si el enrutamiento de comandos no está desviándose a otra clase
8. Si se está tocando la UI desde un hilo de trabajo
9. Si una excepción o un error se está ocultando solo con AfxMessageBox
10. Si hay algún problema de inicialización o de tiempo de vida propio de la compilación release
Por ejemplo, ante un fallo del tipo «no pasa nada al pulsar el botón», hay que comprobar lo siguiente.
Si el IDC del botón es correcto
Si existe el ON_BN_CLICKED
Si la firma del controlador es correcta
Si el recurso de cuadro de diálogo no es otro distinto
Si el botón no está deshabilitado
Si UpdateData no está fallando a mitad del proceso
Si no se está ocultando una excepción
Si se trata de «no se puede pulsar el menú», hay que revisar esto.
Si ON_UPDATE_COMMAND_UI lo está deshabilitando
Si el ID de comando está duplicado
Si la vista activa es la esperada
En cuál de Frame/View/Document/App está el controlador
En MFC, el evento visible y el procesamiento real están conectados mediante macros y enrutamiento, así que hasta que uno se familiariza con ello resulta más fácil entenderlo dibujando la ruta de llamada. El diagrama del capítulo 9 es la forma básica de esa ruta de llamada.
Además, si se quiere partir de los síntomas para orientarse, conviene mirar antes la tabla del capítulo 35, que relaciona los errores habituales con el lugar donde se comprueba cada uno.
35. Errores habituales
Se resumen a continuación los errores habituales en el mantenimiento de MFC.
Buscar solo llamadas a la función sin revisar el mapa de mensajes
Suponer que un CWnd* es válido con solo no ser nulo
Confundir el tiempo de vida de HWND con el del objeto C++
Equivocar la dirección de UpdateData(TRUE/FALSE)
No darse cuenta de que ON_UPDATE_COMMAND_UI lo está deshabilitando
No darse cuenta de un conflicto de ID en resource.h
Tocar la UI directamente desde un hilo de trabajo
Corromper el texto al convertir entre CString y std::string
Romper código que presupone MBCS al pasarlo a Unicode
Olvidar AFX_MANAGE_STATE en un DLL de MFC
Romper un COM/ActiveX que presupone x86 al pasarlo a x64
Equivocarse al seleccionar o liberar objetos GDI
Que el diseño con coordenadas fijas se rompa en alto DPI
Para cada uno de ellos se indica «con qué síntoma se manifiesta» y «dónde hay que mirar para comprobarlo». El punto de entrada para depurar se corresponde con los pasos del capítulo 34.
| Error habitual | Síntoma frecuente | Dónde comprobarlo |
|---|---|---|
| Buscar solo llamadas a la función sin revisar el mapa de mensajes | Se ejecuta algo aunque no se encuentra ningún punto de llamada | Busque BEGIN_MESSAGE_MAP. Cap. 9, paso 4 del cap. 34 |
Suponer que un CWnd* es válido con solo no ser nulo |
Se cae de vez en cuando, se cae al operar sobre una pantalla ya cerrada | Compruébelo con ::IsWindow(pWnd->GetSafeHwnd()). Cap. 8 |
Confundir el tiempo de vida de HWND con el del objeto C++ |
Violación de acceso después de cerrar un cuadro de diálogo | Compruebe si se conserva el valor devuelto por GetDlgItem. Cap. 22 |
Equivocar la dirección de UpdateData |
El valor introducido no se refleja, o el valor inicial no aparece en pantalla | Revise dónde se llama a UpdateData(TRUE) y a UpdateData(FALSE). Cap. 13, paso 5 del cap. 34 |
No darse cuenta de que ON_UPDATE_COMMAND_UI lo está deshabilitando |
El botón o el menú aparecen en gris y no se pueden pulsar | El controlador de ON_UPDATE_COMMAND_UI. Cap. 11, «no se puede pulsar el menú» del cap. 34 |
No darse cuenta de un conflicto de ID en resource.h |
Reacciona un cuadro de diálogo o un elemento de menú distinto | Compare por ID resource.h con el .rc. Cap. 16 |
| Tocar la UI directamente desde un hilo de trabajo | Caídas inestables difíciles de reproducir, se cuelga de vez en cuando | Compruebe si se toca la UI dentro de la función pasada a AfxBeginThread. Cap. 23 |
Corromper el texto al convertir entre CString y std::string |
Solo se corrompe el japonés, se corta el final | La configuración del juego de caracteres del proyecto y el punto de conversión. Cap. 18 |
| Romper código que presupone MBCS al pasarlo a Unicode | Desajuste en la longitud del búfer, dejan de poder leerse archivos existentes | Compruebe si se usa sizeof como si fuera número de caracteres. Cap. 26 |
Olvidar AFX_MANAGE_STATE en un DLL de MFC |
No se encuentran el cuadro de diálogo o los recursos de cadena de texto dentro del DLL | El inicio de las funciones públicas del DLL. Cap. 24 |
| Romper un COM/ActiveX que presupone x86 al pasarlo a x64 | Solo la compilación de 64 bits falla al iniciar o al mostrar la pantalla | Compruebe si el registro de COM solo se hizo para el lado de 32 bits. Cap. 27 |
| Equivocarse al seleccionar o liberar objetos GDI | El dibujo se degrada al ejecutar la app durante mucho tiempo | Compruebe si la columna «Objetos GDI» del Administrador de tareas no deja de crecer. Cap. 20 |
| Que el diseño con coordenadas fijas se rompa en alto DPI | El texto se corta solo en un entorno con escala al 150 % | La configuración de DPI del manifiesto y el dibujo con coordenadas fijas. Cap. 28 |
Un fallo en MFC a veces no se entiende mirando solo la sintaxis de C++; hay que revisar también los mensajes de Windows, los recursos, los handles, los módulos y la configuración del runtime.
36. ¿Conviene elegir MFC para un desarrollo nuevo?
Conviene decidir con cautela si elegir MFC para un desarrollo completamente nuevo.
Si hay una razón para elegir MFC, sería en casos como estos.
Es necesario integrarse estrechamente con código MFC existente
Se quieren reutilizar componentes o pantallas MFC ya existentes
Se necesita un control muy cercano a Win32/GDI/COM
La empresa cuenta con suficiente habilidad interna de mantenimiento de MFC
El objetivo se limita al escritorio de Windows
Dentro de un plan de migración a largo plazo, primero hay que ampliar con MFC
Por el contrario, en los siguientes casos conviene estudiar otras opciones.
Se quiere crear una UI moderna
Se necesita un diseño flexible o animaciones
El centro de la aplicación es la integración web o en la nube
Se quiere dar prioridad a la facilidad de hacer pruebas
Se quiere elegir una tecnología accesible para desarrolladores jóvenes
Se necesita compatibilidad multiplataforma
Se quiere priorizar desde el principio la accesibilidad y el alto DPI
MFC no es una tecnología que «no valga la pena aprender ahora» ni una que se elija «por ser nueva»: es una tecnología para enfrentarse a los activos nativos de Windows ya existentes.
37. Cómo enfocar la migración desde MFC
Si se quiere migrar una aplicación MFC a otra tecnología, apuntar directamente a una renovación completa suele terminar en fracaso.
Primero, conviene descomponer la aplicación de esta manera.
UI
Lógica de negocio
Formato de archivo
Procesamiento de comunicaciones
Procesamiento de base de datos
Control de dispositivos
Impresión
Integración COM/OLE
Gestión de la configuración
Registro (log)
De todo esto, lo que más depende de MFC es la UI.
En cambio, la lógica de negocio y el procesamiento de archivos tienen posibilidades de extraerse.
El orden realista para la migración es el siguiente.
1. Reproducir el entorno de compilación
2. Fijar el comportamiento existente con datos de prueba
3. Extraer la lógica de los controladores de eventos de UI
4. Trasladarla a una biblioteca de C++ que no dependa de MFC
5. Añadir pruebas automatizadas
6. Documentar las especificaciones externas
7. Reemplazar de forma gradual las pantallas necesarias
Es más probable tener éxito si el objetivo no es «dejar de usar MFC» en sí mismo, sino «sacar hacia afuera la lógica importante que está encerrada en MFC».
38. Por dónde empezar a leer código MFC
Al leer por primera vez un proyecto MFC existente, conviene empezar por archivos como estos.
*.vcxproj
Revise el toolset, la configuración de MFC, el juego de caracteres y la configuración del runtime
resource.h
Revise los ID de recursos
*.rc
Revise los cuadros de diálogo, menús, cadenas de texto e iconos
*App.cpp / *App.h
Revise la clase derivada de CWinApp e InitInstance
MainFrm.cpp / MainFrm.h
Revise el marco principal y el menú/la barra de herramientas
*Doc.cpp / *Doc.h
Si es Document/View, revise la estructura de datos y el procesamiento de guardado
*View.cpp / *View.h
Revise el dibujo y la interacción del usuario
*Dlg.cpp / *Dlg.h
Revise el cuadro de diálogo, DDX y el procesamiento de botones
A continuación, las palabras clave de búsqueda más usadas.
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_UPDATE_COMMAND_UI
ON_BN_CLICKED
DoDataExchange
UpdateData
OnInitDialog
OnDraw
Serialize
AfxMessageBox
AfxBeginThread
AFX_MANAGE_STATE
Buscando estas palabras resulta más fácil ver cómo funciona la aplicación.
39. Directrices de diseño para mantener MFC
Si se va a mantener durante mucho tiempo un activo MFC existente, resultan útiles directrices como estas.
Mantener ligeros los controladores de eventos de UI
No dejar que CString o CWnd se filtren demasiado a la capa que no es de UI
Trasladar la lógica de negocio a clases C++ normales
Crear pruebas de compatibilidad del formato de archivo
Dejar claras las diferencias entre x86 y x64
Revisar los cambios de ID de recurso
Comprobar el estado del módulo de los DLL de MFC
Mantener el registro (log) en buen estado
Fijar la compilación en CI
Comprobar periódicamente la pantalla en alto DPI y en Windows 11
En particular, es importante no apilar demasiado procesamiento en CDialog o en CView.
Conviene que las clases de pantalla de MFC se concentren en la entrada, la visualización y el enrutamiento de eventos.
Tomar el valor de la pantalla
Pasarlo al servicio
Reflejar el resultado en la pantalla
Si se mantiene este nivel, aunque sea MFC, resulta bastante más fácil de mantener.
40. Lista de verificación práctica
Esta es la lista de verificación para cuando se trabaja con una aplicación MFC.
¿Está clara la versión de Visual Studio?
¿Están instalados los componentes MFC/ATL?
¿Está claro el destino x86/x64?
¿Se tiene identificada la configuración Unicode/MBCS?
¿MFC está vinculado de forma estática o como DLL compartido?
¿Cuál es el Visual C++ Redistributable necesario?
¿resource.h y .rc forman parte de la revisión de código?
¿Se sigue la ruta de eventos a través del mapa de mensajes?
¿Es correcta la dirección de UpdateData?
¿Se usa la estructura Document/View?
¿Es segura la gestión del tiempo de vida de los cuadros de diálogo no modales?
¿Se está tocando la UI directamente desde un hilo de trabajo?
¿Hay algún punto en un DLL de MFC que necesite AFX_MANAGE_STATE?
¿Se comprueba la pantalla en un entorno de alto DPI?
¿Las dependencias COM/ActiveX son compatibles con 32/64 bits?
¿Queda registro (log)?
¿Se puede probar la lógica que no es de UI?
MFC puede parecer peculiar hasta que uno se acostumbra, pero una vez que se sabe dónde mirar, se puede leer de forma bastante sistemática.
41. Resumen
MFC es un framework con historia para construir aplicaciones de escritorio nativas de Windows en C++. Ya no es la corriente principal para desarrollos nuevos, pero sigue siendo una tecnología importante para el mantenimiento de activos existentes, la actualización del entorno de compilación, la incorporación de funciones y la migración gradual.
Para entender MFC, los puntos especialmente importantes son los siguientes.
MFC hace que la API de Win32 se maneje con facilidad desde C++
CWinApp administra toda la aplicación
El tiempo de vida de CWnd y de HWND no es el mismo
El mapa de mensajes vincula los eventos con las funciones
Document/View es el mecanismo que separa los datos de la presentación
DDX/DDV se usan para la sincronización y la validación de los valores del diálogo
El archivo de recursos y resource.h son muy importantes
CString y TCHAR se entienden junto con la configuración de codificación de caracteres
En un DLL de MFC hay que prestar atención al estado del módulo
Una aplicación MFC antigua merece revisarse desde el punto de vista del alto DPI, x64, CI y las pruebas
Al trabajar con MFC, en lugar de dar por hecho que «es malo por ser antiguo», lo importante es entender primero su estructura. Una aplicación MFC existente puede tener condensados muchos años de conocimiento de negocio, especificaciones particulares de cada cliente, integración con dispositivos y compatibilidad de archivos. Para conservar ese valor mientras se va facilitando el mantenimiento poco a poco, lo realista es entender las convenciones de MFC, extraer la lógica que no es de UI y ordenar la compilación y las pruebas.
Si se tuviera que resumir en una sola frase, sería esta.
Una base para tratar el funcionamiento de las aplicaciones nativas de Windows mediante clases y un framework de C++.
Con esta perspectiva, MFC deja de ser una simple tecnología antigua y se convierte en una clave para interpretar con seguridad los activos existentes de Windows.
Referencias
- Colección de referencia con los fragmentos de código de este artículo organizados por capítulo - komurasoft-blog-samples (GitHub)
- MFC Desktop Applications - Microsoft Learn
- MFC y 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
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo manejar correctamente los tokens de suplantación de Windows — préstamo de privilegios por hilo y una forma segura de revertirlos
Guía práctica sobre los tokens de suplantación de Windows: tokens de acceso, primarios y de hilo, niveles de suplantación, RevertToSelf y...
Las profundidades del I/O en Windows (6.ª entrega, final) ── Controladores de filtro y minifiltros: por qué Procmon y el análisis antivirus pueden interceptar el I/O
Última entrega de la serie sobre controladores de filtro y minifiltros de Windows: el Filter Manager, las altitudes, los callbacks pre/po...
Las profundidades de la E/S de Windows (parte 4) — El administrador de caché: ¿cuándo llega su WriteFile al disco?
Cuarta entrega de la serie que explica con diagramas el administrador de caché de Windows: la caché implementada como asignación de archi...
Los entresijos de E/S de Windows (parte 3) — Puertos de finalización de E/S (IOCP) y el grupo de subprocesos de .NET: el sótano de async/await
Parte 3 de una serie que explica con diagramas el puerto de finalización de E/S (IOCP): la cola de finalización integrada con el control ...
Las profundidades de la E/S de Windows (Parte 2) ── E/S síncrona y E/S asíncrona: el verdadero significado de OVERLAPPED
Segunda entrega de la serie que explica con diagramas la E/S síncrona y la E/S asíncrona (E/S superpuesta) de Windows: el significado de ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué es MFC?
- MFC son las siglas de Microsoft Foundation Classes, un framework de aplicaciones Windows que facilita el manejo de la API de Win32 mediante clases de C++. Las ventanas se representan con CWnd, los cuadros de diálogo con CDialog y la aplicación completa con CWinApp, entre otras clases. No es una "biblioteca mágica que permite programar sin conocer Windows", sino una forma de organizar el funcionamiento de Windows mediante tipos y un framework de C++, por lo que también se necesitan conocimientos de Win32 como los mensajes de Windows y los handles.
- ¿Todavía se puede usar MFC? ¿Sigue teniendo soporte?
- MFC se puede seguir usando actualmente en Visual Studio y continúa teniendo soporte. Sin embargo, la documentación de Microsoft señala que no se añaden nuevas funciones ni se actualiza la documentación. En la práctica, es habitual usarlo para mantener aplicaciones MFC existentes, añadirles funciones, actualizar el entorno de compilación o migrar de forma gradual hacia otra interfaz de usuario; en cambio, para una aplicación GUI genérica completamente nueva conviene decidir su adopción con cautela. Es necesario instalar el componente individual del Visual Studio Installer (C++ MFC for latest build tools).
- ¿Qué es el mapa de mensajes (message map) de MFC?
- Es el mecanismo de MFC que vincula los mensajes de Windows y los comandos con funciones controladoras (handlers). Entre BEGIN_MESSAGE_MAP y END_MESSAGE_MAP se describen correspondencias como "si se hace clic en este botón, llama a esta función" mediante macros como ON_BN_CLICKED u ON_COMMAND. Si al buscar una función no se encuentra ninguna llamada directa pero esta se ejecuta al producirse un evento, hay que revisar el mapa de mensajes. En una revisión de código de MFC es importante examinar el mapa de mensajes junto con la función controladora, no solo esta última.
- ¿Cómo se migra de MFC a otra tecnología?
- Apuntar directamente a una renovación completa suele terminar en fracaso. El orden realista es reproducir el entorno de compilación, fijar el comportamiento existente con datos de prueba, extraer la lógica de los controladores de eventos de UI para trasladarla a una biblioteca de C++ que no dependa de MFC, añadir pruebas automatizadas, documentar las especificaciones externas y, a partir de ahí, ir reemplazando las pantallas necesarias de forma gradual. Es más probable tener éxito si el objetivo no es "dejar de usar MFC" en sí mismo, sino "sacar hacia afuera la lógica importante que está encerrada en MFC".
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.