What Is MFC on Windows? Foundational Knowledge for Maintaining Existing Assets

· · Windows, MFC, Visual C++, C++, Win32, Native App, Desktop App, Legacy Code, Legacy Asset Reuse

1. What to Understand First

When maintaining older Windows desktop applications, you sometimes come across names like these.

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

These come up constantly in MFC, a Windows application framework for C++. MFC stands for Microsoft Foundation Classes, a library that makes the Win32 API easier to work with as C++ classes.

In today’s Windows app development there are many options — WinUI, WPF, Windows Forms, Electron, Qt, web technologies — so MFC is rarely the first choice for new development. Yet it is not a dead technology either. Business applications, measurement instruments, control software, CAD/CAM, internal tools, and long-lived packaged software still leave us maintaining MFC codebases today.

Let me state up front the perspectives worth holding when trying to understand MFC.

MFC is an essential map for reading older Windows desktop apps
Think of MFC not as hiding the Win32 API, but as wrapping it in a C++-friendly way
Without knowing MFC's conventions, you will misread behavior more than the code's appearance suggests
Its value lies in maintaining, extending the life of, and incrementally migrating existing assets, not in new adoption

This article walks through an overview of MFC: application structure, message maps, Document/View, dialogs, DDX/DDV, resources, builds, and the points to watch during maintenance.

The code fragments in this article are published on GitHub as a reference code collection, organized into files by chapter.

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

2. What Is MFC?

MFC is a class library for building native Windows desktop applications in C++.

When using the Win32 API directly, you typically write code like this.

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

The Win32 API is extremely powerful, but because everything is built around C functions, handles, messages, and callbacks, large applications quickly become hard to navigate. MFC lets you work with these as C++ classes: a window is a CWnd, a dialog is a CDialog, the application as a whole is a CWinApp, a frame window is a CFrameWnd, and a view is a CView.

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

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

MFC does not replace the Win32 API with something entirely different. It is a thin but wide-ranging C++ framework built on the Win32 API’s way of thinking. So to read MFC, you need knowledge of not just the MFC classes but also areas like these.

Windows messages
Handles such as HWND
GDI/GDI+
Resource files
COM/OLE
DLLs and runtimes
Character encodings
Threads and message loops

Rather than “a magic library that lets you write Windows apps without knowing Windows,” the true nature of MFC is “the mechanics of Windows organized into C++ types and a framework.”

3. Is MFC Still Usable Today?

MFC is still available in Visual Studio. But do not misunderstand its position. While it remains supported, it is not a modern UI framework receiving active new features — Microsoft’s MFC documentation notes that MFC continues to be supported but will not receive new features or documentation updates.

So MFC’s position works out roughly like this.

Maintaining existing MFC apps           -> realistically common
Adding features to existing MFC apps    -> plausible
Updating an MFC app's build environment -> important
Incremental migration from MFC to another UI -> plausible
Adopting it for a brand-new general GUI app  -> judge carefully

In long-lived business applications in particular, the UI, printing, file I/O, device control, proprietary protocols, and COM integration are often all bundled up inside MFC.

For such codebases, before “throwing MFC away,” what you need first is “getting to a state where you can read MFC.”

4. The Areas Where MFC Excelled

The classic domain where MFC has been used is native Windows desktop applications.

Concretely, applications like these.

Dialog-centric business tools
SDI apps that open and edit files
MDI apps that handle multiple documents
Control screens for measurement instruments and manufacturing equipment
Native CAD/CAM applications
Apps that make heavy use of printing and previews
Apps involving ActiveX or OLE integration
Apps tightly coupled to old Windows APIs and COM assets

MFC’s strength is that it operates close to native Windows components. Windows, menus, toolbars, status bars, dialogs, common controls, printing, file dialogs, the registry, and GDI drawing can all be handled as C++ classes.

Its weakness, on the other hand, is that modern UI construction, data binding, testability, asynchronous processing, modern layout, high-DPI support, localization, and accessibility cannot be expressed as naturally as in recent frameworks.

Listing its characteristics, we get this.

Close to native Windows
Direct control from C++
A wealth of existing assets
Requires Win32 knowledge
Full of old conventions
You must shape a testable structure yourself

5. Preparing to Use MFC in Visual Studio

Even if you have C++ installed in Visual Studio, MFC is not necessarily included. MFC is handled as an individual component in the Visual Studio Installer. Typically, check components in this area.

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
Whether the Spectre Mitigations variant of MFC is needed

If MFC-related files cannot be found at build time, check not only the project settings but also whether the MFC component is installed in Visual Studio itself.

The same applies to CI environments and build servers. If a project builds in local Visual Studio but fails on CI, the cause may be the presence or absence of the MFC component, or a mismatch in the target toolset version.

6. The Basic Structure of an MFC Application

An MFC application generally has the following structure.

CWinApp-derived class
  Responsible for application-wide initialization and shutdown

CFrameWnd / CMDIFrameWnd / CDialog-derived classes
  Responsible for the main window and dialogs

CView-derived classes
  Responsible for screen display and user interaction

CDocument-derived classes
  Responsible for data and file saving

Resource files
  Hold menus, dialogs, icons, strings, and so on

Message maps
  Bind Windows messages and commands to handler functions

For example, a simple MFC app contains a CWinApp-derived class like this.

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 is the class that represents the application as a whole. An MFC application normally has exactly one object derived from CWinApp.

A global object like theApp may feel odd at first, but in MFC this is the standard structure.

7. What Does CWinApp Do?

CWinApp matters as the entry point of an MFC application. In a plain Win32 application you write WinMain, window class registration, and the message loop yourself; in MFC, the framework takes over most of that. Developers mainly override InitInstance and write the application-specific initialization there.

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

    // Load settings
    // Initialize COM
    // Create the main window
    // Register document templates

    return TRUE;
}

The kinds of processing that tend to live in InitInstance include these.

Initializing common controls
Setting the registry key
Loading the most-recently-used file list
Registering document templates
Creating the main frame
Processing command-line arguments
Initializing COM/OLE

During maintenance, looking at the CWinApp-derived class first makes the application’s overall startup sequence much easier to see.

8. CWnd Is the Class at the Center of MFC

Most of MFC’s UI classes derive from CWnd, which represents a Windows window. But a CWnd object and an HWND are not the same thing.

HWND
  A window handle managed by the Windows OS

CWnd
  A C++ wrapper object that makes the HWND easier to work with

In MFC, the CWnd holds the HWND internally.

HWND hWnd = m_hWnd;

Or you obtain it like this.

HWND hWnd = GetSafeHwnd();

What matters during maintenance is that even when a CWnd* exists, the corresponding HWND may already have been destroyed.

So you check whether a window is valid like this.

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

When investigating MFC bugs, it is important to check whether the C++ object lifetime of the CWnd and the lifetime of the actual Windows window handle have drifted apart.

9. What Is a Message Map?

One of the mechanisms where MFC’s character shows most clearly is the message map.

A Windows application receives mouse clicks, key input, repaints, window resizes, menu selections, and so on as Windows messages.

With the Win32 API, you normally handle messages in the switch statement of WndProc.

In MFC, a message map binds them to handler functions instead.

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

The meaning of this code is as follows.

When the button IDC_BUTTON_OK is clicked
call CMyDialog::OnClickedButtonOk

If you are not used to reading MFC, it can be hard to tell where a function is being called from. When a search turns up no direct call, look at the message map.

The function is not called directly anywhere
Yet it runs when an event occurs
-> Check the BEGIN_MESSAGE_MAP / ON_... macros

In MFC code reviews, it is important to look not just at the handler functions but at the message map together with them.

10. Command Routing

In MFC, menu and toolbar interactions are also treated as commands.

The most common form is ON_COMMAND.

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

void CMainFrame::OnFileOpen()
{
    // Open the file
}

MFC has a mechanism for routing commands to the appropriate object.

For example, the same ID_EDIT_COPY may be handled by the currently active view, the document, the frame, or the application, depending on the situation.

Active view
Document
Frame window
Application

The command is passed along in this order to whichever object can handle it.

As a result, in MFC it can be hard to trace “which function runs when this menu item is clicked” with a simple text search.

The points to look at during maintenance are these.

What is the command ID?
Which class has the ON_COMMAND?
Where is the ON_UPDATE_COMMAND_UI?
Which view is currently active?
Is the app using the Document/View structure?

11. What Is ON_UPDATE_COMMAND_UI?

In MFC, ON_UPDATE_COMMAND_UI is sometimes used to update the enabled/disabled state, check state, or display text of menu items and toolbar buttons.

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

This lets you enable the menu item or button only when deletion is actually possible.

When investigating behavior in an MFC app — a button that is mysteriously grayed out, a menu that cannot be clicked, a check state that changes — searching for ON_UPDATE_COMMAND_UI often reveals the cause.

12. Dialog-Based MFC Applications

One of the most approachable forms of MFC is the dialog-based application.

Settings screens, simple business tools, and equipment operation panels are often built around dialogs.

Typically, you derive from CDialog or 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;
};

In dialog-based code, the following elements come up frequently.

IDD_...        dialog resource ID
IDC_...        control ID
OnInitDialog   initialization
DoDataExchange associates controls with member variables
UpdateData     synchronizes the screen and the variables
ON_BN_CLICKED  button click handling

A dialog is not just its appearance — it works through the combination of resources, member variables, the message map, and initialization code.

13. DDX and DDV

Two things that come up constantly in MFC dialogs are DDX and DDV.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX is the mechanism that associates controls on a dialog with C++ member variables; DDV is the mechanism that validates the input values.

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

Calling UpdateData(TRUE) copies the values entered on screen into the member variables.

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

    // At this point m_name and m_interval hold the on-screen input values
    SaveSettings(m_name, m_interval);

    CDialogEx::OnOK();
}

Conversely, calling UpdateData(FALSE) copies the member variable values to the screen.

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

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

    return TRUE;
}

When input values misbehave in an MFC dialog, check these points.

Is the DDX defined in DoDataExchange?
Is UpdateData(TRUE) being called?
Is UpdateData(FALSE) being called at the right time?
Is DDV rejecting the input?
Do the control IDs match the resources?

14. The Document/View Architecture

One of MFC’s defining features is the Document/View architecture.

This is a structure for separating the data an application handles from how it is displayed.

CDocument
  Holds the data
  Responsible for reading and writing files
  Notifies multiple views of updates

CView
  Displays the data
  Handles user interaction
  Manages drawing and selection state

For example, in apps like text editors, shape editors, configuration file editors, or CAD, separating data from presentation pays off.

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

On the view side, you fetch the document and draw it.

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

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

The benefit of Document/View is that it is easy to present the same data in multiple views.

For example, the same data can be shown like this.

A table view
A chart view
A detail view
A preview
A print view

That said, applying Document/View to a simple settings screen or a small tool can make the structure feel heavier than necessary.

During maintenance, first determining whether the app uses Document/View or is dialog-centric makes the code much easier to follow.

15. SDI and MDI

In MFC, two structures often appear in combination with Document/View: SDI and MDI.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI is essentially the form where one frame handles one document.

Main window
  One document
  One or more views

MDI has multiple child windows inside one parent window, each handling its own document.

MDI parent frame
  MDI child frame 1 -> document 1
  MDI child frame 2 -> document 2
  MDI child frame 3 -> document 3

Old Windows applications used MDI heavily.

During maintenance, the class names give you a good idea of the structure.

CFrameWnd       SDI-style frame
CMDIFrameWnd    MDI parent frame
CMDIChildWnd    MDI child frame
CSingleDocTemplate document template for SDI
CMultiDocTemplate  document template for MDI

In apps created by the MFC wizard, the registration of CSingleDocTemplate or CMultiDocTemplate usually appears inside InitInstance.

16. Understanding Resource Files

In MFC apps, the .rc file is extremely important.

.rc is a Windows resource file.

It defines things like these.

Dialog templates
Menus
Accelerator keys
Icons
Bitmaps
String tables
Version information
Toolbars

In addition, resource.h defines the resource IDs.

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

MFC code uses these IDs to bind the resources to the C++ code.

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

A common problem during maintenance is resource ID inconsistency.

An ID in resource.h changed
A branch merge caused an ID collision
The control ID on the dialog and the DDX ID do not match
A menu ID that should have been deleted is still there
String table IDs are duplicated

When investigating an MFC app’s behavior, you need to look at the .rc file and resource.h alongside the C++ code.

17. The Class Wizard and Hand-Written Code

MFC has a history deeply intertwined with Visual Studio’s Class Wizard.

The Class Wizard can auto-generate message handlers, DDX variables, virtual function overrides, and more.

As a result, MFC code retains a lot of tool-generated patterns.

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

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

In newer versions of Visual Studio the appearance and the generated shape may differ, but old codebases often still contain comment markers like these.

What matters during maintenance is not to carelessly destroy the boundary between generated and hand-written code.

Do not delete the message map
Do not break the DDX associations
Do not change resource IDs carelessly
Do not casually delete old Class Wizard comment markers

In MFC, merely compiling as valid C++ is not enough. You also need to preserve, to a degree, the shape that the Visual Studio resource editor and Class Wizard expect.

18. CString and Strings

The string class that appears constantly in MFC is CString.

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

CString is a variable-length string class widely used in MFC/ATL-style code. Modern C++ favors std::string and std::wstring in most places, but in MFC, CString is used heavily because of its affinity with the APIs and controls.

During maintenance, you have to keep character encodings in mind.

CString      effectively CStringA or CStringW depending on project settings
CStringA     ANSI / MBCS family
CStringW     Unicode / UTF-16 family
LPCTSTR      TCHAR-based string pointer
LPCSTR       char family
LPCWSTR      wchar_t family
std::string  normally char family
std::wstring wchar_t family

Modern Windows apps are basically safer when built on the Unicode assumption, but old MFC apps often retain MBCS-based code.

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

String conversion bugs come up frequently in MFC maintenance.

Be especially careful in cases like these.

Reading files that assume Shift_JIS
Switching to a Unicode build
An external DLL that requires char*
COM that requires BSTR
Naive conversion to std::string producing garbled text

When you see a CString, do not just think “that’s an old string class.” Check it together with the project’s character set setting, external APIs, and file formats.

19. CFile and CArchive

MFC also includes classes for file operations and serialization. The representative ones are CFile and CArchive.

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

CArchive is commonly used in MFC’s serialization machinery.

In CDocument-derived classes, Serialize is often overridden so that loading and saving live in the same function.

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

MFC serialization is convenient, but it requires care over long-term operation.

Compatibility with old file formats
Managing version numbers
Recovery when loading fails
Exception handling
Character encodings
Endianness
Whether structs are being saved verbatim

In MFC apps that have used a proprietary binary format for a long time, Serialize can effectively be the file format specification.

In that case, before changing the code, you should always prepare test data that exercises loading the existing files.

20. GDI Drawing and CDC

For screen drawing in MFC, the CDC class, which wraps the Windows Device Context, is used heavily. CView::OnDraw receives a CDC* as its argument.

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

When using pens and brushes, pay attention to selection and restoration.

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

Around GDI objects, mistakes like these become problems.

Not restoring the original object after SelectObject
Creating large numbers of GDI objects without destroying them
Confusing the roles of OnPaint and OnDraw
Flickering due to a lack of double buffering
Fixed-pixel drawing falling apart at high DPI

For MFC drawing bugs, you need to check not just the C++ logic but also Windows GDI resources, repaint timing, DPI, and font sizes.

21. Modal and Modeless Dialogs

In MFC, how a dialog is shown also requires attention.

A modal dialog is displayed with DoModal.

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

In this case, the caller waits until the dialog closes.

A modeless dialog, on the other hand, returns control to the caller after creation.

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

For modeless dialogs, lifetime management is critical.

When do you delete the dialog you newed?
Could the parent window be destroyed first?
Does the dialog use PostNcDestroy?
Could it be created twice?
Is a pointer left dangling after the dialog closes?

In MFC crash investigations, modeless dialog lifetime problems are a recurring cause.

22. The Lifetimes of C++ Objects and Windows Handles

Something extremely important in MFC is the difference between the lifetimes of C++ objects and Windows handles. CWnd, for example, is a C++ object, but the actual window is managed by Windows as an HWND, and the two are not always created and destroyed together.

The CWnd object exists but the HWND does not yet
The HWND was destroyed but the CWnd object remains
A temporary CWnd wrapper has been created
The handle was swapped via Attach/Detach

For example, code like this deserves caution.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// Storing pWnd in a member for later use

If you hold on to a pointer obtained from GetDlgItem for a long time, you risk referencing it after the window has been destroyed.

If needed, it can be safer to call GetDlgItem each time, or to manage a member variable for the control through DDX.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

In MFC, a non-null pointer is not necessarily a safe pointer.

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

This instinct is essential in MFC maintenance.

23. Threads and UI Updates

Windows UI must fundamentally be manipulated on the UI thread that created it, and the same applies to MFC apps. Manipulating UI controls directly from a worker thread causes instability and crashes.

An example to avoid.

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

    // Avoid touching the UI directly from a worker thread
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

Generally, you notify the UI thread via something like PostMessage.

constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;

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

    // Heavy processing

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

The UI side receives it through the 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;
}

Around MFC threading, check the following.

Is the UI being touched directly from a worker thread?
Is PostMessage being called after the window was destroyed?
Is the UI thread blocked waiting for a thread to finish?
Is shared data locked appropriately?
Is the return value and lifetime of AfxBeginThread misunderstood?

24. MFC DLLs and Module State

When building DLLs with MFC, the concept of module state comes up.

When loading resources from an MFC DLL, showing dialogs, or building extension DLLs, the question becomes which module’s resources are consulted.

At the entry points of MFC DLL functions, you sometimes see this macro.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

This exists so that MFC can use the correct module state.

Forgetting this macro leads to problems like these.

Dialog resources inside the DLL cannot be found
String resources get read from a different module
Icons and menus cannot be found
Works in debug but breaks in release

When maintaining MFC DLLs, it is important to map out the relationships among the EXE, regular DLLs, extension DLLs, and resource DLLs.

Take particular care when a non-MFC app calls an MFC DLL, or when the system has a plugin architecture.

25. Linking MFC Statically or Using the Shared DLL

MFC projects have a “Use of MFC” project setting.

The main options are these two.

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

When using the shared DLL, the target environment needs the corresponding MFC runtime and Visual C++ runtime.

With static linking, distribution may look simpler, but you have to consider executable size, updates, picking up security fixes, and licensing and redistribution terms.

Neither is always the right answer.

The decision factors are roughly these.

Can you install the Visual C++ Redistributable on target machines?
Do you want the app to approach a single exe?
How will you roll out security updates?
Will multiple apps share the same runtime?
Can you provide an installer?
Which Windows versions are you targeting?

In maintenance, the first thing is to check the current setting.

Configuration Properties
  General
    Use of MFC

Also look at the Runtime Library setting.

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

If MFC and CRT link settings are mixed inconsistently, memory allocation/deallocation problems can appear at library boundaries.

26. Unicode, MBCS, and TCHAR

In old MFC code, TCHAR, LPCTSTR, and the _T() macro appear constantly.

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

This style exists to support both Unicode builds and MBCS builds.

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

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

Unicode builds are the norm today, but old apps often retain MBCS-based processing.

In particular, for external files, communication protocols, old DLLs, database connections, and serial communication, you need to verify the character encoding assumptions.

The thing to be careful about is not treating Unicode conversion as a simple find-and-replace job.

Is the char array size a byte count or a character count?
Is strlen being used?
Is sizeof(buffer) being used as a character count?
Does the external API accept UTF-16 or Shift_JIS?
Is it acceptable for the file save format to change?

When fixing MFC string handling, check not just the screen display but file compatibility and external integrations as well.

27. MFC and COM/OLE/ActiveX

MFC has also been used in applications deeply involved with COM, OLE, and ActiveX.

Old business apps may retain elements like these.

OLE Automation
ActiveX controls
COM servers
COM clients
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

Code like this sometimes appears in an MFC app’s startup path.

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

When COM/OLE is in use, what looks like an MFC problem may actually be caused by COM initialization, the threading model, reference counting, registration data, or 32-bit/64-bit differences.

Pay particular attention to 32-bit ActiveX and COM components.

A 32-bit MFC app uses 32-bit COM
A 64-bit MFC app uses 64-bit COM
32-bit and 64-bit COM registrations are separate
Old ActiveX components may not support 64-bit

When porting an MFC app to x64, always check not just the UI code but the COM/OLE dependencies as well.

28. High-DPI Support and Modern Windows

Running an old MFC app on modern Windows, the display can fall apart in high-DPI environments.

For example, problems like these.

Text gets clipped
Buttons are too small
Fixed-pixel drawing is misaligned
Layouts break when monitors have different scaling factors
Old bitmaps look blurry
Dialog layouts get cramped

In Windows desktop apps, the application needs to declare its DPI-awareness mode explicitly.

In MFC apps too, you have to check the manifest, the resources, the drawing code, the fonts, and the layout.

In particular, code like this with hard-coded fixed coordinates is prone to problems at high DPI.

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

Coordinates that assume fixed pixels fall apart visually when the DPI changes.

The maintenance checkpoints are these.

The DPI settings in the application manifest
The font in the dialog resources
Fixed-pixel drawing
The resolution of image resources
Behavior with multiple monitors
Display on Windows 10 / Windows 11

High-DPI support in MFC is not necessarily finished by just changing a project setting. With old UIs, actual on-screen verification and layout fixes are required.

29. Exception Handling and Error Handling

MFC has its own exception classes and error-handling conventions.

In old code you encounter macros like these.

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

There is also code where these are mixed with modern C++ try / catch.

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

In MFC maintenance, watch for the following.

Are MFC exceptions and standard C++ exceptions mixed?
Is the lifetime of exception objects misunderstood?
Do you understand the old THROW/CATCH macros?
Are return-value errors and exceptions mixed?
Are errors ending at AfxMessageBox with no log left behind?

In business applications, it is important not only to show an error message on screen but also to record logs, operation history, input values, and external connection state.

In old MFC apps, errors sometimes end at an AfxMessageBox and nothing more.

AfxMessageBox(_T("Failed to save."));

To improve maintainability, separate UI display from log recording.

LogError(_T("Save failed"), path);
AfxMessageBox(_T("Failed to save. Please check the log."));

30. Making MFC Coexist with Modern C++

Maintaining an MFC app does not mean everything has to conform to old C++ style.

You can respect MFC’s conventions in the UI layer while organizing domain logic and computation in modern C++.

For example, divide it like this.

MFC layer
  CDialog
  CView
  CDocument
  CString
  Message maps
  Resource handling

Non-MFC layer
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  Unit-testable classes
  Business logic

The bad shape is the one where all the processing is crammed into the dialog class.

void CMainDialog::OnBnClickedExecute()
{
    // Get input
    // Read files
    // Communicate
    // Compute
    // Update the DB
    // Update the screen
    // Write logs
    // Handle exceptions
}

Code like this is hard to change, hard to test, and hard to debug.

To improve it, move the logic out of the MFC class.

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

With this shape, m_service.Execute can be tested without MFC.

The single most effective improvement in maintaining existing MFC assets is gradually extracting logic out of the UI classes.

31. Making MFC Code Testable

MFC apps are often hard to unit test as-is.

The reason is that the UI, Win32, files, communication, databases, and global state tend to be tightly coupled.

The way to think about making them testable is this.

Do not try to test CDialog or CView directly
First extract the non-UI logic
Convert MFC types at the boundary
Put files and communication behind interfaces
Keep UI event handlers thin

For example, move the processing into a pure C++ class.

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

The MFC side handles only input and 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);
}

With this structure, PriceCalculator and ParsePrices can be tested with an ordinary C++ test framework.

There is no need to rebuild the whole MFC app at once. Just moving testable processing out of the event handlers already pays off.

32. Pinning Down the Build Environment

In MFC app maintenance, pinning down the build environment is critical.

In old codebases, the build result can change based on differences like these.

Visual Studio version
MSVC toolset version
Windows SDK version
Presence of the MFC/ATL components
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
MFC static link / shared DLL
Runtime library settings
Precompiled headers

In MFC apps, many dependencies often gather in stdafx.h or pch.h.

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

Builds can break because one file has different compile settings, or the precompiled header settings have drifted.

In a maintenance project, writing this information down in the README pays dividends later.

Required Visual Studio version
Required workloads and individual components
Required Windows SDK
Target platforms
MFC link method
Build steps
How to run CI
How to produce the distribution

Graduating from “it builds on my PC” is the first step in MFC maintenance.

33. Building MFC on CI

MFC apps can be built on CI too.

However, the CI environment must have the MFC components installed.

When using the Visual Studio Build Tools, you need to install them with the MFC/ATL component IDs specified.

Here are the points to verify.

Is MFC included in the Build Tools?
Does the target toolset match the project?
Is the Windows SDK installed?
Are both x86 and x64 builds verified?
Does the resource compiler run?
Is there a signing step?
Is installer generation also covered by CI?

Automating UI tests for an MFC app is not easy, but automating at least the following is worthwhile.

Debug / Release builds
x86 / x64 builds
Static analysis
Unit tests
Installer creation
Saving artifact hashes
Verifying dependent DLLs

In long-term maintenance, just keeping the project buildable already has substantial value.

34. Where to Look When Debugging an MFC App

When chasing a defect in an MFC app, this order of investigation is efficient.

1. Which screen is it?
2. Is it a dialog, a View, or a Frame?
3. What resource ID corresponds to the action?
4. Which function does the message map route to?
5. Is the direction of UpdateData correct?
6. If Document/View, what is the Document's state?
7. Is command routing sending it to another class?
8. Is a worker thread touching the UI?
9. Are exceptions or errors being swallowed by a lone AfxMessageBox?
10. Are there release-build-specific uninitialized or lifetime problems?

For example, for a “nothing happens when I click the button” defect, check here.

Is the button's IDC correct?
Does an ON_BN_CLICKED exist?
Is the handler's signature correct?
Is the dialog resource actually a different one?
Is the button disabled?
Is UpdateData failing partway through?
Is an exception being swallowed?

For “the menu item cannot be clicked,” check here.

Is it disabled by ON_UPDATE_COMMAND_UI?
Are command IDs duplicated?
Is the active view what you expect?
Where is the handler: Frame, View, Document, or App?

Because MFC connects surface-level events to the actual processing through macros and routing, drawing the call paths as a diagram helps a great deal until you get used to it.

35. Common Pitfalls

Let’s organize the common pitfalls in MFC maintenance.

Searching only for function calls without looking at the message map
Assuming a non-null CWnd* is valid
Confusing HWND lifetime with C++ object lifetime
Getting the direction of UpdateData(TRUE/FALSE) wrong
Not noticing something is disabled via ON_UPDATE_COMMAND_UI
Not noticing ID collisions in resource.h
Touching the UI directly from a worker thread
Garbled text from CString/std::string conversion
Breaking MBCS-based code during Unicode conversion
Forgetting AFX_MANAGE_STATE in an MFC DLL
Breaking x86-only COM/ActiveX during an x64 port
Mishandling GDI object selection/release
Fixed-coordinate layouts breaking at high DPI

MFC defects often cannot be understood by looking at C++ syntax alone; you need to look at Windows messages, resources, handles, modules, and runtime settings as well.

36. Should You Choose MFC for New Development?

Whether to choose MFC for a completely new project deserves careful judgment.

If there are reasons to choose MFC, they look like this.

Tight integration with existing MFC code is required
You want to reuse existing MFC components and screens
You need control very close to Win32/GDI/COM
Your organization has ample MFC maintenance skill
The target is strictly Windows desktop
The long-term migration plan requires building onto MFC first

Conversely, consider other options in cases like these.

You want a modern UI
You need flexible layout and animation
Web or cloud integration is the focus
You value testability
You want technology that younger developers can join easily
You need cross-platform support
You want accessibility and high DPI as first-class concerns

MFC is neither a technology “not worth learning anymore” nor one “to choose because it’s new” — it is a technology for engaging with existing Windows native assets.

37. How to Think About Migrating Away from MFC

If you want to migrate an MFC app to another technology, aiming for a complete rewrite from the start is a recipe for failure.

First, decompose the app like this.

UI
Business logic
File formats
Communication
Database access
Device control
Printing
COM/OLE integration
Configuration management
Logging

Of these, the part most dependent on MFC is the UI.

Business logic and file handling, on the other hand, can potentially be extracted.

The realistic migration order looks like this.

1. Reproduce the build environment
2. Pin down existing behavior with test data
3. Extract logic out of UI event handlers
4. Move it into non-MFC C++ libraries
5. Add automated tests
6. Document the external specifications
7. Replace screens incrementally, starting with the ones that need it

Rather than making “getting rid of MFC” the goal itself, making “freeing the important logic trapped inside MFC” the goal is far more likely to succeed.

38. Entry Points for Reading MFC Code

When reading an existing MFC project for the first time, start with files in this area.

*.vcxproj
  Check the toolset, MFC settings, character set, runtime settings

resource.h
  Check the resource IDs

*.rc
  Check dialogs, menus, strings, icons

*App.cpp / *App.h
  Check the CWinApp-derived class and InitInstance

MainFrm.cpp / MainFrm.h
  Check the main frame and the menus/toolbars

*Doc.cpp / *Doc.h
  For Document/View, check the data structures and save logic

*View.cpp / *View.h
  Check drawing and user interaction

*Dlg.cpp / *Dlg.h
  Check dialogs, DDX, and button handling

Next, the commonly used search keywords.

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

Searching for these makes the application’s behavior much easier to see.

39. Design Policies for Maintaining MFC

If you will be maintaining existing MFC assets for the long haul, policies like these are effective.

Keep UI event handlers thin
Do not let CString or CWnd leak too far into non-UI layers
Move business logic into ordinary C++ classes
Build compatibility tests for the file formats
Make the x86/x64 differences explicit
Review changes to resource IDs
Check module state in MFC DLLs
Put logging in order
Pin the build down with CI
Regularly verify the screens at high DPI and on Windows 11

Above all, it is important not to cram too much processing into CDialog and CView.

Keep MFC’s screen classes focused on input, display, and event dispatch.

Take values from the screen
Pass them to a service
Reflect the result back on the screen

If you can keep them at this level, even MFC becomes considerably easier to maintain.

40. A Practical Checklist

Here is a checklist for working with MFC apps.

Is the Visual Studio version clearly defined?
Are the MFC/ATL components installed?
Are the x86/x64 targets clearly defined?
Do you know the Unicode/MBCS settings?
Is MFC statically linked or using the shared DLL?
Which Visual C++ Redistributable is required?
Are resource.h and the .rc file part of code review?
Are you tracing event paths through the message maps?
Is the direction of UpdateData correct?
Is the Document/View structure in use?
Is modeless dialog lifetime management safe?
Is the UI being touched directly from worker threads?
Are there places in MFC DLLs that need AFX_MANAGE_STATE?
Are screens verified in high-DPI environments?
Do the COM/ActiveX dependencies support 32-bit/64-bit?
Are logs being recorded?
Can the non-UI logic be tested?

MFC looks idiosyncratic until you get used to it, but once you know where to look, it reads quite predictably.

41. Summary

MFC is a long-established framework for building native Windows desktop applications in C++. It is no longer the mainstream choice for new development, but for maintaining existing assets, updating build environments, adding features, and incremental migration, it remains an important technology today.

The most important points for understanding MFC are these.

MFC makes the Win32 API easier to work with from C++
CWinApp manages the application as a whole
CWnd and HWND do not share the same lifetime
Message maps bind events to functions
Document/View is the mechanism that separates data from display
DDX/DDV handle dialog value synchronization and validation
Resource files and resource.h are extremely important
Understand CString and TCHAR together with the character set settings
Watch out for module state in MFC DLLs
Old MFC apps are worth revisiting from the angles of high DPI, x64, CI, and testing

When working with MFC, rather than dismissing it as “old, therefore bad,” what matters first is understanding its structure. Existing MFC apps often contain years of business knowledge, customer-specific specifications, device integration, and file compatibility. To protect that value while gradually making things easier to maintain, the realistic path is to understand MFC’s conventions, extract the non-UI logic, and get builds and tests in order.

If we had to sum it up in one line, it would be this.

A foundation for working with the mechanics of native Windows apps through C++ classes and a framework.

With this perspective, MFC is not merely an old technology — it becomes a key to safely deciphering existing Windows assets.

References

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog