Что такое MFC в Windows: основы для сопровождения существующих приложений

· · Windows, MFC, Visual C++, C++, Win32, Нативное приложение, Настольное приложение, Легаси-код, Использование существующих ресурсов

1. Что нужно усвоить в первую очередь

При сопровождении старых десктопных приложений для Windows иногда встречаются такие имена.

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

Всё это часто встречается в MFC — фреймворке для Windows-приложений на C++. MFC — сокращение от Microsoft Foundation Classes, библиотеки, которая делает Win32 API удобнее в виде C++-классов.

В современной разработке Windows-приложений есть множество альтернатив — WinUI, WPF, Windows Forms, Electron, Qt, веб-технологии, — поэтому MFC всё реже становится первым выбором для новой разработки. Тем не менее эта технология не исчезла. В бизнес-приложениях, измерительном оборудовании, программах управления, CAD/CAM-системах, внутренних инструментах и давно существующем коробочном ПО до сих пор нередко приходится сопровождать кодовые базы на MFC.

Прежде чем идти дальше, перечислим взгляды на MFC, которые стоит держать в голове.

MFC — важная карта для чтения старых Windows-приложений
MFC не скрывает Win32 API, а оборачивает его по-C++-ному
Не зная приёмов MFC, легко неверно прочитать поведение кода — сильнее, чем кажется по одному его виду
Ценность MFC не в новых проектах, а в сопровождении, продлении жизни и поэтапной миграции существующих активов

В этой статье мы разберём обзор MFC, структуру приложения, карты сообщений, Document/View, диалоги, DDX/DDV, ресурсы, сборку и нюансы, на которые стоит обращать внимание при сопровождении.

Фрагменты кода из этой статьи опубликованы на GitHub как справочный набор, разложенный по файлам согласно главам.

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

2. Что такое MFC

MFC — это библиотека классов для создания нативных десктопных приложений Windows на C++.

При прямом использовании Win32 API типично пишут такой код.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
        // Отрисовка
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

Win32 API очень мощный, но строится вокруг C-функций, дескрипторов, сообщений и обратных вызовов, поэтому в крупных приложениях легко потерять общую картину. MFC делает всё это доступным как C++-классы: окно представлено CWnd, диалог — CDialog, приложение целиком — CWinApp, окно-фрейм — CFrameWnd, а представление — CView.

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

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

MFC не заменяет Win32 API чем-то принципиально другим — это тонкий, но широкий по охвату C++-фреймворк, построенный на идеях Win32 API. Поэтому, чтобы читать MFC, нужны не только знания классов MFC, но и знания в следующих областях.

Сообщения Windows
Дескрипторы (handle), такие как HWND
GDI/GDI+
Файлы ресурсов
COM/OLE
DLL и среды выполнения
Кодировки символов
Потоки и цикл обработки сообщений

MFC на деле не «волшебная библиотека, позволяющая писать код, не зная Windows», а «механика Windows, упорядоченная через типы и фреймворк C++».

3. Можно ли использовать MFC сегодня

MFC по-прежнему доступен в Visual Studio. Но не стоит заблуждаться насчёт его роли. Хотя поддержка продолжается, это не современный UI-фреймворк, куда активно добавляют новые возможности: в документации Microsoft по MFC отмечается, что поддержка сохраняется, но новых функций и обновлений документации ждать не стоит.

Поэтому роль MFC можно описать примерно так.

Сопровождение существующих MFC-приложений              -> реалистично и часто встречается
Добавление функциональности в существующие MFC-приложения -> вполне возможно
Обновление среды сборки MFC-приложения                  -> важно
Поэтапная миграция с MFC на другой UI                    -> вполне возможно
Выбор MFC для совершенно нового обычного GUI-приложения  -> требует осторожного решения

Особенно в долгоживущих бизнес-приложениях UI, печать, файловый ввод-вывод, управление устройствами, собственные протоколы и интеграция с COM часто оказываются собраны внутри MFC.

В таких кодовых базах прежде чем «избавляться от MFC», нужно добиться того, чтобы код можно было «читать».

4. Области, в которых MFC был силён

Классическая область применения MFC — нативные десктопные приложения Windows.

Конкретнее, речь о таких приложениях.

Бизнес-инструменты, построенные вокруг диалогов
SDI-приложения, открывающие и редактирующие файлы
MDI-приложения, работающие с несколькими документами
Экраны управления измерительным оборудованием и производственными установками
Нативные приложения CAD/CAM
Приложения, активно использующие печать и предпросмотр
Приложения с интеграцией ActiveX или OLE
Приложения, тесно связанные со старыми Windows API и активами COM

Сила MFC в том, что он работает рядом с нативными компонентами Windows. Окна, меню, панели инструментов, строки состояния, диалоги, стандартные элементы управления, печать, диалоги выбора файлов, реестр и отрисовку GDI можно использовать как C++-классы.

Слабость же MFC в том, что современное построение UI, привязку данных, тестируемость, асинхронную обработку, современную вёрстку, поддержку высокого DPI, локализацию и доступность в нём не получится выразить так же естественно, как в новых фреймворках.

Если перечислить характеристики, получится так.

Близость к нативной Windows
Прямой контроль из C++
Большой объём существующих активов
Требуются знания Win32
Много устаревших приёмов
Тестируемую структуру нужно выстраивать самостоятельно

5. Подготовка к использованию MFC в Visual Studio

Даже если в Visual Studio установлен C++, MFC не обязательно будет доступен: он идёт как отдельный компонент Visual Studio Installer. Обычно нужно проверить наличие таких компонентов.

Разработка классических приложений 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
Нужна ли версия MFC со Spectre Mitigations

Если при сборке не находятся файлы, связанные с MFC, стоит проверить не только настройки проекта, но и то, установлен ли компонент MFC в самой Visual Studio.

То же самое касается CI-окружений и билд-серверов: если сборка проходит локально в Visual Studio, но падает на CI, причиной может быть отсутствие компонента MFC или несовпадение версии целевого набора инструментов (toolset).

6. Базовая структура MFC-приложения

MFC-приложение обычно имеет примерно такую структуру.

Класс, производный от CWinApp
  Отвечает за инициализацию и завершение работы приложения в целом

Классы, производные от CFrameWnd / CMDIFrameWnd / CDialog
  Отвечают за главное окно и диалоги

Классы, производные от CView
  Отвечают за отображение экрана и работу с пользователем

Классы, производные от CDocument
  Отвечают за данные и сохранение файлов

Файлы ресурсов
  Хранят меню, диалоги, значки, строки и так далее

Карты сообщений
  Связывают сообщения Windows и команды с функциями-обработчиками

Например, в простом MFC-приложении встречается такой класс, производный от CWinApp.

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 — класс, представляющий приложение целиком. В MFC-приложении обычно существует ровно один объект, производный от CWinApp.

Глобальный объект вроде theApp поначалу может показаться странным, но в MFC это стандартная структура.

7. Что делает CWinApp

CWinApp важен как точка входа MFC-приложения. В обычном Win32-приложении WinMain, регистрацию класса окна и цикл обработки сообщений вы пишете сами, а в MFC бо́льшую часть этой работы берёт на себя фреймворк. Разработчик в основном переопределяет InitInstance и пишет там инициализацию, специфичную для приложения.

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

    // Загрузка настроек
    // Инициализация COM
    // Создание главного окна
    // Регистрация шаблонов документов

    return TRUE;
}

В InitInstance чаще всего оказывается такая обработка.

Инициализация стандартных элементов управления
Настройка ключа реестра
Загрузка списка недавно использованных файлов
Регистрация шаблонов документов
Создание главного фрейма
Обработка аргументов командной строки
Инициализация COM/OLE

При сопровождении удобно сначала посмотреть на класс, производный от CWinApp, — так проще увидеть общий порядок запуска приложения.

8. CWnd — класс в центре MFC

Большинство UI-классов MFC базируются на CWnd, представляющем окно Windows. Однако объект CWnd и HWND — не одно и то же.

HWND
  Дескриптор окна, которым управляет Windows OS

CWnd
  C++-объект-обёртка, делающий работу с HWND удобнее

В MFC CWnd хранит HWND внутри себя.

HWND hWnd = m_hWnd;

Либо получают его так.

HWND hWnd = GetSafeHwnd();

При сопровождении важно помнить: даже если CWnd* существует, соответствующий HWND уже может быть уничтожен.

Поэтому валидность окна проверяют примерно так.

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

При расследовании ошибок в MFC важно проверять, не разошлись ли время жизни C++-объекта CWnd и реального дескриптора окна Windows.

9. Что такое карта сообщений

Один из механизмов, в которых характер MFC проявляется сильнее всего, — это карта сообщений (message map).

Windows-приложение получает клики мыши, нажатия клавиш, перерисовку, изменение размера окна, выбор пункта меню и так далее в виде сообщений Windows.

В Win32 API сообщения обычно обрабатываются в операторе switch внутри WndProc.

В MFC это делается через карту сообщений, которая связывает их с функциями-обработчиками.

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

Смысл этого кода такой.

Если нажата кнопка IDC_BUTTON_OK
вызывается CMyDialog::OnClickedButtonOk

Если вы не привыкли читать MFC, бывает трудно понять, откуда вызывается функция. Если поиск не находит прямого вызова, стоит посмотреть на карту сообщений.

Функция нигде не вызывается напрямую
но выполняется при возникновении события
-> проверьте макросы BEGIN_MESSAGE_MAP / ON_...

В код-ревью MFC важно смотреть не только на функции-обработчики, но и на карту сообщений вместе с ними.

10. Маршрутизация команд

В MFC действия с меню и панелью инструментов тоже обрабатываются как команды.

Самый распространённый вариант — ON_COMMAND.

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

void CMainFrame::OnFileOpen()
{
    // Открытие файла
}

В MFC есть механизм доставки команды нужному объекту.

Например, одна и та же команда ID_EDIT_COPY может обрабатываться то активным представлением, то документом, то фреймом, то приложением — в зависимости от ситуации.

Активное представление
Документ
Окно-фрейм
Приложение

Команда передаётся в этом порядке, пока не найдётся объект, способный её обработать.

Поэтому в MFC бывает сложно отследить, «какая функция вызывается при нажатии пункта меню», с помощью одного лишь текстового поиска.

При сопровождении стоит обращать внимание на такие моменты.

Какой это ID команды
В каком классе находится ON_COMMAND
Где находится ON_UPDATE_COMMAND_UI
Какое представление сейчас активно
Использует ли приложение структуру Document/View

11. Что такое ON_UPDATE_COMMAND_UI

В MFC для обновления состояния пункта меню или кнопки панели инструментов — доступности, отметки, отображаемого текста — иногда используют ON_UPDATE_COMMAND_UI.

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

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

Так можно сделать меню или кнопку активными только тогда, когда удаление действительно возможно.

Когда в MFC-приложении кнопка почему-то серая, пункт меню не нажимается или меняется отметка — при разборе такого поведения поиск по ON_UPDATE_COMMAND_UI часто помогает найти причину.

12. MFC-приложения на основе диалога

Одна из самых понятных форм MFC-приложения — приложение на основе диалога.

Экраны настроек, простые бизнес-инструменты, панели управления оборудованием нередко строятся вокруг диалога.

Обычно наследуются от CDialog или 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;
};

В коде диалоговых приложений часто встречаются такие элементы.

IDD_...        ID ресурса диалога
IDC_...        ID элемента управления
OnInitDialog   инициализация
DoDataExchange связывает элементы управления и переменные-члены
UpdateData     синхронизирует экран и переменные
ON_BN_CLICKED  обработка клика по кнопке

Диалог — это не только внешний вид: он работает благодаря сочетанию ресурсов, переменных-членов, карты сообщений и кода инициализации.

13. DDX и DDV

В диалогах MFC постоянно встречаются DDX и DDV.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX — механизм, связывающий элементы управления на диалоге с переменными-членами C++, а DDV — механизм проверки введённых значений.

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

При вызове UpdateData(TRUE) введённые на экране значения переносятся в переменные-члены.

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

    // На этом этапе m_name и m_interval уже содержат введённые на экране значения
    SaveSettings(m_name, m_interval);

    CDialogEx::OnOK();
}

И наоборот, при вызове UpdateData(FALSE) значения переменных-членов переносятся на экран.

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

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

    return TRUE;
}

Если во введённых в MFC-диалоге значениях что-то не так, стоит проверить следующее.

Определён ли DDX в DoDataExchange
Вызывается ли UpdateData(TRUE)
В правильный ли момент вызывается UpdateData(FALSE)
Не отклоняет ли ввод DDV
Совпадает ли ID элемента управления с ресурсом

14. Архитектура Document/View

Одна из главных особенностей MFC — архитектура Document/View.

Это структура, разделяющая данные, с которыми работает приложение, и их отображение.

CDocument
  Хранит данные
  Отвечает за чтение и запись файлов
  Уведомляет несколько представлений об обновлениях

CView
  Отображает данные
  Обрабатывает действия пользователя
  Управляет отрисовкой и состоянием выделения

Например, в текстовых редакторах, редакторах фигур, инструментах для редактирования конфигурационных файлов и в CAD-приложениях разделение данных и отображения оправдано.

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

На стороне представления документ получают и отрисовывают.

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

    for (const auto& item : pDoc->m_items)
    {
        // Отрисовка с использованием pDC
    }
}

Преимущество Document/View в том, что одни и те же данные легко показать в нескольких представлениях.

Например, одни и те же данные можно показать так.

Табличное представление
Представление в виде графика
Детальное представление
Предпросмотр
Представление для печати

Впрочем, если применить Document/View к простому экрану настроек или небольшому инструменту, структура может показаться, наоборот, слишком тяжёлой.

При сопровождении полезно сразу определить, использует ли приложение Document/View или построено вокруг диалога, — тогда код будет проще отслеживать.

15. SDI и MDI

В MFC вместе с Document/View часто встречаются структуры SDI и MDI.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI — по сути форма, при которой один фрейм обрабатывает один документ.

Главное окно
  Один документ
  Одно или несколько представлений

MDI — форма, при которой внутри одного родительского окна есть несколько дочерних окон, и каждое из них работает со своим документом.

MDI-родительский фрейм
  MDI-дочерний фрейм 1 -> документ 1
  MDI-дочерний фрейм 2 -> документ 2
  MDI-дочерний фрейм 3 -> документ 3

В старых Windows-приложениях MDI использовался очень часто.

При сопровождении по именам классов можно понять структуру.

CFrameWnd       фрейм в стиле SDI
CMDIFrameWnd    MDI-родительский фрейм
CMDIChildWnd    MDI-дочерний фрейм
CSingleDocTemplate шаблон документа для SDI
CMultiDocTemplate  шаблон документа для MDI

В приложениях, созданных мастером MFC, регистрация CSingleDocTemplate или CMultiDocTemplate часто находится внутри InitInstance.

16. Разбираемся с файлами ресурсов

В MFC-приложениях файл .rc играет чрезвычайно важную роль.

.rc — это файл ресурсов Windows.

В нём определяются такие вещи.

Шаблоны диалогов
Меню
Клавиши-акселераторы
Значки
Растровые изображения
Таблицы строк
Информация о версии
Панели инструментов

Кроме того, в resource.h определяются идентификаторы ресурсов.

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

В коде MFC эти ID используются, чтобы связать ресурсы с C++-кодом.

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

Частая проблема при сопровождении — рассогласование ID ресурсов.

ID в resource.h изменился
При слиянии другой ветки произошёл конфликт ID
ID элемента управления на диалоге не совпадает с ID в DDX
Остался ID меню, который должен был быть удалён
ID в таблице строк дублируются

При разборе поведения MFC-приложения нужно смотреть не только на C++-код, но и одновременно на .rc и resource.h.

17. Class Wizard и код, написанный вручную

У MFC долгая история, тесно связанная с Class Wizard из Visual Studio.

С помощью Class Wizard можно автоматически генерировать обработчики сообщений, переменные DDX, переопределения виртуальных функций и многое другое.

Поэтому в коде MFC часто остаются следы кода, сгенерированного инструментом.

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

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

В новых версиях Visual Studio внешний вид и форма генерируемого кода могут отличаться, но в старых кодовых базах такие комментарии-маркеры нередко остаются.

При сопровождении важно не разрушать неосторожно границу между сгенерированным и написанным вручную кодом.

Не удалять карту сообщений
Не ломать соответствия DDX
Не менять ID ресурсов без необходимости
Не удалять без нужды старые комментарии, рассчитанные на Class Wizard

В MFC недостаточно, чтобы код просто компилировался как валидный C++. Нужно в определённой мере сохранять и ту форму, которую ожидают редактор ресурсов Visual Studio и Class Wizard.

18. CString и строки

Класс строк, который постоянно встречается в MFC, — CString.

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

CString — класс строк переменной длины, широко используемый в коде MFC/ATL. В современном C++ чаще используют std::string и std::wstring, но в MFC CString применяется активно из-за удобства работы с API и элементами управления.

При сопровождении нужно держать в уме кодировку символов.

CString      в зависимости от настроек проекта эквивалентен CStringA или CStringW
CStringA     семейство ANSI / MBCS
CStringW     семейство Unicode / UTF-16
LPCTSTR      указатель на строку на основе TCHAR
LPCSTR       семейство char
LPCWSTR      семейство wchar_t
std::string  обычно семейство char
std::wstring семейство wchar_t

В современных Windows-приложениях в целом безопаснее исходить из Unicode, но в старых MFC-приложениях может оставаться код, рассчитанный на MBCS.

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

Ошибки преобразования строк — частая проблема при сопровождении MFC-приложений.

Особое внимание стоит обратить на такие случаи.

Чтение файлов, рассчитанных на Shift_JIS
Переход на сборку с Unicode
Внешняя DLL требует char*
COM требует BSTR
Неосторожное преобразование в std::string приводит к искажению текста

Увидев CString, не стоит просто считать его «старым классом строк» — важно проверять его вместе с настройками кодировки проекта, внешними API и форматами файлов.

19. CFile и CArchive

В MFC есть классы для работы с файлами и сериализации. Основные из них — CFile и CArchive.

CFile file;
if (file.Open(path, CFile::modeRead))
{
    CArchive ar(&file, CArchive::load);
    // Чтение из ar
}

CArchive часто используется в механизме сериализации MFC.

В классах, производных от CDocument, часто переопределяют Serialize, размещая код чтения и сохранения в одной функции.

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 удобна, но при длительной эксплуатации требует осторожности.

Совместимость со старыми форматами файлов
Управление номером версии
Восстановление при ошибке чтения
Обработка исключений
Кодировка символов
Порядок байтов (endianness)
Не сохраняется ли структура «как есть», без преобразования

В MFC-приложениях, давно использующих собственный бинарный формат, Serialize порой фактически становится спецификацией файла.

В этом случае перед изменением кода обязательно стоит подготовить тестовые данные, проверяющие чтение существующих файлов.

20. Отрисовка GDI и CDC

Для отрисовки экрана в MFC часто используется класс CDC, работающий с Device Context Windows. В CView::OnDraw в качестве аргумента передаётся CDC*.

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

При использовании перьев и кистей нужно внимательно следить за выбором и восстановлением объекта.

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

Вокруг объектов GDI проблемы вызывают такие ошибки.

После SelectObject не восстановлен исходный объект
Создано множество объектов GDI, но они не уничтожены
Перепутаны роли OnPaint и OnDraw
Мерцание из-за отсутствия двойной буферизации
Отрисовка, рассчитанная на фиксированные пиксели, ломается при высоком DPI

При отладке ошибок отрисовки в MFC нужно проверять не только логику C++, но и GDI-ресурсы Windows, момент перерисовки, DPI и размер шрифта.

21. Модальные и немодальные диалоги

В MFC нужно внимательно относиться и к тому, как показывается диалог.

Модальный диалог показывают через DoModal.

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // Обработка при OK
}

В этом случае вызывающий код ждёт, пока диалог не закроется.

Немодальный (modeless) диалог, напротив, после создания сразу возвращает управление вызывающему коду.

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

Для немодальных диалогов критически важно управление временем жизни.

Когда удалять (delete) диалог, созданный через new
Не будет ли родительское окно уничтожено раньше
Используется ли на стороне диалога PostNcDestroy
Не создаётся ли диалог повторно
Не остаётся ли висячий указатель после закрытия

При расследовании падений в MFC причиной нередко оказываются проблемы со временем жизни немодальных диалогов.

22. Время жизни C++-объектов и дескрипторов Windows

Очень важный момент в MFC — разница между временем жизни C++-объекта и дескриптора Windows. Например, CWnd — это C++-объект, но реальным окном управляет Windows в виде HWND, и эти два объекта не всегда создаются и уничтожаются одновременно.

Объект CWnd уже есть, а HWND ещё нет
HWND уничтожен, а объект CWnd остался
Создана временная обёртка CWnd
Дескриптор переподключается через Attach/Detach

Например, такой код требует внимания.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// Сохраняем pWnd в поле для использования позже

Если долго хранить указатель, полученный через GetDlgItem, есть риск обратиться к нему уже после уничтожения окна.

При необходимости безопаснее каждый раз заново вызывать GetDlgItem либо управлять переменной-членом для элемента управления через DDX.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

В MFC не-null указатель не гарантирует безопасность.

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

Это чутьё крайне важно при сопровождении MFC.

23. Потоки и обновление UI

UI Windows в общем случае нужно изменять из того потока, в котором он был создан, — это касается и MFC-приложений. Прямое обращение к элементам управления UI из рабочего потока приводит к нестабильному поведению и падениям.

Пример того, чего стоит избегать.

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

    // Не следует напрямую обращаться к UI из рабочего потока
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

Обычно UI-поток уведомляют через PostMessage и подобные средства.

constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;

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

    // Тяжёлая обработка

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

На стороне UI сообщение принимают через карту сообщений.

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

В части многопоточности в MFC стоит проверить следующее.

Не обращается ли код к UI напрямую из рабочего потока
Не вызывается ли PostMessage после уничтожения окна
Не блокируется ли UI-поток в ожидании завершения другого потока
Правильно ли блокируются общие данные
Не перепутаны ли возвращаемое значение и время жизни AfxBeginThread

24. MFC DLL и состояние модуля

При создании DLL на MFC появляется понятие состояния модуля (module state).

При загрузке ресурсов из MFC DLL, показе диалогов или создании расширяющей (extension) DLL встаёт вопрос, ресурсы какого модуля будут использоваться.

На входе функций MFC DLL иногда встречается такой макрос.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

Он нужен для того, чтобы MFC использовал правильное состояние модуля.

Если забыть этот макрос, это приводит к таким проблемам.

Ресурсы диалогов внутри DLL не находятся
Строковые ресурсы читаются из другого модуля
Значки и меню не находятся
Работает только в отладочной сборке, а в релизе ломается

При сопровождении MFC DLL важно упорядочить отношения между EXE, обычной DLL, расширяющей DLL и ресурсной DLL.

Особая осторожность нужна, когда MFC DLL вызывается из не-MFC-приложения или используется плагинная архитектура.

25. Линковать MFC статически или использовать общую DLL

В настройках проекта MFC-приложения есть параметр «Use of MFC».

Основные варианты — эти два.

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

При использовании общей DLL в среде выполнения должны быть соответствующие среды выполнения MFC и Visual C++.

При статической линковке распространение приложения может выглядеть проще, но нужно учитывать размер исполняемого файла, обновления, применение исправлений безопасности, а также условия лицензирования и повторного распространения.

Ни один из вариантов не является правильным всегда и везде.

Вот что помогает принять решение.

Можно ли установить Visual C++ Redistributable у получателя
Хотите ли вы приблизить приложение к единому exe-файлу
Как будут применяться обновления безопасности
Будут ли несколько приложений делить одну и ту же среду выполнения
Можно ли подготовить установщик
Какие версии Windows являются целевыми

При сопровождении важно прежде всего проверить текущие настройки.

Configuration Properties
  General
    Use of MFC

Кроме того, стоит посмотреть настройку Runtime Library.

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

Если настройки линковки MFC и CRT смешаны, на границах библиотек могут возникать проблемы с выделением и освобождением памяти.

26. Unicode, MBCS и TCHAR

В старом коде MFC постоянно встречаются TCHAR, LPCTSTR и макрос _T().

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

Такой стиль написания нужен, чтобы поддерживать и Unicode-сборку, и MBCS-сборку.

Unicode-сборка
  TCHAR    -> wchar_t
  LPCTSTR  -> const wchar_t*
  _T("...") -> L"..."

MBCS-сборка
  TCHAR    -> char
  LPCTSTR  -> const char*
  _T("...") -> "..."

Сейчас обычно используется Unicode-сборка, но в старых приложениях может оставаться код, рассчитанный на MBCS.

Особенно для внешних файлов, протоколов связи, старых DLL, подключений к базе данных и последовательной связи нужно проверять предположения о кодировке.

Важно не считать переход на Unicode простой заменой одного на другое.

Размер массива char — это количество байт или символов
Не используется ли strlen
Не используется ли sizeof(buffer) как количество символов
Принимает ли внешний API UTF-16 или Shift_JIS
Допустимо ли изменение формата сохраняемого файла

При исправлении работы со строками в MFC нужно проверять не только отображение на экране, но и совместимость файлов, и внешние интеграции.

27. MFC и COM/OLE/ActiveX

MFC также применялся в приложениях, тесно связанных с COM, OLE и ActiveX.

В старых бизнес-приложениях могут сохраняться такие элементы.

OLE Automation
элементы управления ActiveX
COM-серверы
COM-клиенты
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

В коде запуска MFC-приложения иногда встречается такой фрагмент.

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

Если используется COM/OLE, то, что выглядит как проблема MFC, на самом деле может быть вызвано инициализацией COM, моделью потоков, подсчётом ссылок, регистрационными данными или различиями между 32-битной и 64-битной версией.

Особое внимание стоит уделить 32-битным компонентам ActiveX и COM.

32-битное MFC-приложение использует 32-битный COM
64-битное MFC-приложение использует 64-битный COM
Регистрация COM для 32-бит и 64-бит выполняется отдельно
Старый ActiveX может не поддерживать 64-бит

При переводе MFC-приложения на x64 обязательно нужно проверять не только UI-код, но и зависимости от COM/OLE.

28. Поддержка высокого DPI и современная Windows

При запуске старого MFC-приложения на современной Windows отображение может ломаться в средах с высоким DPI.

Например, встречаются такие проблемы.

Текст обрезается
Кнопки слишком маленькие
Отрисовка с фиксированными пикселями смещается
Всё ломается, если у разных мониторов разный масштаб
Старые растровые изображения выглядят размыто
Раскладка диалога становится тесной

В десктопных приложениях Windows приложение должно явно указывать режим поддержки DPI.

В MFC-приложениях тоже нужно проверять манифест, ресурсы, код отрисовки, шрифты и раскладку.

Особенно проблемным при высоком DPI становится код с жёстко прописанными координатами вроде такого.

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

Координаты, рассчитанные на фиксированные пиксели, при изменении DPI визуально ломаются.

Вот на что стоит смотреть при сопровождении.

Настройка DPI в манифесте приложения
Шрифт в ресурсах диалога
Отрисовка с фиксированными пикселями
Разрешение графических ресурсов
Поведение на нескольких мониторах
Отображение на Windows 10 / Windows 11

Поддержка высокого DPI в MFC не всегда сводится к простому изменению настройки проекта. Для старого UI обычно требуются реальная проверка на экране и исправление раскладки.

29. Обработка исключений и ошибок

В MFC есть собственные классы исключений и приёмы обработки ошибок.

В старом коде встречаются такие макросы.

TRY
{
    // Обработка
}
CATCH(CFileException, e)
{
    e->ReportError();
}
END_CATCH

Встречается и код, где это смешано с современными try / catch из C++.

try
{
    DoSomething();
}
catch (const std::exception& ex)
{
    // Запись в лог
}

При сопровождении MFC стоит обратить внимание на следующее.

Не смешаны ли исключения MFC и стандартные исключения C++
Не перепутано ли время жизни объекта исключения
Понимаете ли вы старые макросы THROW/CATCH
Не смешаны ли ошибки через возвращаемое значение и исключения
Не сводится ли всё к AfxMessageBox без записи в лог

В бизнес-приложениях важно не просто показать сообщение об ошибке на экране, но и сохранить лог, историю действий, введённые значения и состояние внешних подключений.

В старых MFC-приложениях обработка ошибки иногда заканчивается одним AfxMessageBox.

AfxMessageBox(_T("Не удалось сохранить."));

Чтобы повысить сопровождаемость, стоит разделить отображение в UI и запись в лог.

LogError(_T("Save failed"), path);
AfxMessageBox(_T("Не удалось сохранить. Проверьте лог."));

30. Как совместить MFC и современный C++

То, что вы сопровождаете MFC-приложение, не означает, что весь код нужно писать в старом стиле C++.

Слой UI можно оставить в рамках приёмов MFC, а доменную логику и вычисления — организовать на современном C++.

Например, разделение может выглядеть так.

Слой MFC
  CDialog
  CView
  CDocument
  CString
  Карты сообщений
  Работа с ресурсами

Слой не-MFC
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  Классы, поддающиеся модульному тестированию
  Бизнес-логика

Плохой вариант — когда вся обработка втиснута в класс диалога.

void CMainDialog::OnBnClickedExecute()
{
    // Получение входных данных
    // Чтение файлов
    // Обмен данными по сети
    // Вычисления
    // Обновление БД
    // Обновление экрана
    // Запись в лог
    // Обработка исключений
}

Такой код трудно менять, трудно тестировать, а расследовать баги в нём непросто.

Чтобы улучшить ситуацию, логику выносят из 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);
}

Тогда m_service.Execute можно протестировать без MFC.

Самое действенное улучшение при сопровождении существующих активов на MFC — постепенно выносить логику из UI-классов.

31. Делаем код MFC тестируемым

MFC-приложения в исходном виде часто плохо поддаются модульному тестированию.

Причина в том, что UI, Win32, файлы, сетевое взаимодействие, база данных и глобальное состояние легко оказываются тесно связаны друг с другом.

Подход к повышению тестируемости такой.

Не пытаться тестировать CDialog или CView напрямую
Сначала выносить логику, не относящуюся к UI
Преобразовывать типы MFC на границе
Скрывать файлы и сетевое взаимодействие за интерфейсами
Делать обработчики экранных событий тонкими

Например, обработку переносят в чистый C++-класс.

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

Сторона MFC отвечает только за ввод и вывод.

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

При такой структуре PriceCalculator и ParsePrices можно протестировать обычным фреймворком тестирования C++.

Полностью переписывать MFC-приложение сразу не нужно — уже вынос тестируемой обработки из обработчиков событий даёт эффект.

32. Фиксируем среду сборки

При сопровождении MFC-приложений важно зафиксировать среду сборки.

В старых кодовых базах результат сборки может меняться из-за таких различий.

Версия Visual Studio
Версия набора инструментов MSVC
Версия Windows SDK
Наличие компонентов MFC/ATL
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
Статическая линковка MFC / общая DLL
Настройки библиотеки времени выполнения
Предкомпилированные заголовки

В MFC-приложениях в stdafx.h или pch.h нередко собирается множество зависимостей.

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

Сборка может ломаться из-за того, что у одного файла отличаются настройки компиляции или разошлись настройки предкомпилированного заголовка.

В проекте на сопровождении стоит явно зафиксировать эту информацию в README — потом будет проще.

Необходимая версия Visual Studio
Необходимые нагрузки (workloads) и отдельные компоненты
Необходимый Windows SDK
Целевые платформы
Способ линковки MFC
Порядок сборки
Как запускать CI
Как собирать дистрибутив

Первый шаг в сопровождении MFC — перестать полагаться на «у меня на компьютере всё собирается».

33. Сборка MFC в CI

MFC-приложения тоже можно собирать в CI.

Но в CI-окружении обязательно должен быть установлен компонент MFC.

При использовании Visual Studio Build Tools нужно устанавливать их, явно указав ID компонентов MFC/ATL.

Вот моменты, которые стоит проверить.

Установлен ли MFC в составе Build Tools
Совпадает ли целевой набор инструментов с проектом
Установлен ли Windows SDK
Проверены ли и x86-, и x64-сборки
Работает ли компилятор ресурсов
Есть ли этап подписи
Входит ли генерация установщика в область CI

Автоматизировать даже UI-тесты в MFC-приложении непросто, но как минимум следующее автоматизировать полезно.

Сборка Debug / Release
Сборка x86 / x64
Статический анализ
Модульные тесты
Создание установщика
Сохранение хешей артефактов
Проверка зависимых DLL

При долгосрочном сопровождении уже сама возможность собирать проект — большая ценность.

34. Куда смотреть при отладке MFC-приложения

При разборе дефекта в MFC-приложении эффективно смотреть в таком порядке.

1. На каком это экране
2. Диалог, View или Frame
3. Какой ID ресурса соответствует действию
4. К какой функции ведёт карта сообщений
5. Правильно ли направление UpdateData
6. Если это Document/View — каково состояние Document
7. Не уходит ли обработка в другой класс через маршрутизацию команд
8. Не обращается ли рабочий поток к UI напрямую
9. Не проглатываются ли исключения или ошибки одним AfxMessageBox
10. Нет ли проблем с неинициализированными данными или временем жизни, характерных именно для релизной сборки

Например, для дефекта «нажатие кнопки ничего не даёт» стоит проверить следующее.

Правильный ли IDC у кнопки
Существует ли ON_BN_CLICKED
Правильна ли сигнатура обработчика
Не подключён ли на самом деле другой ресурс диалога
Не отключена ли кнопка
Не завершается ли UpdateData ошибкой посреди обработки
Не проглатывается ли исключение

Для «пункт меню не нажимается» — вот эти моменты.

Не отключён ли пункт через ON_UPDATE_COMMAND_UI
Не дублируется ли ID команды
Соответствует ли активное представление ожиданиям
В каком месте — Frame, View, Document или App — находится обработчик

В MFC внешне видимое событие и реальная обработка связаны через макросы и маршрутизацию, поэтому, пока не выработалась привычка, путь вызова проще понять, изобразив его схемой.

35. Частые ловушки

Соберём типичные ловушки сопровождения MFC.

Искать только прямые вызовы функций, не заглядывая в карту сообщений
Считать, что не-null CWnd* всегда валиден
Путать время жизни HWND и время жизни C++-объекта
Путать направление UpdateData(TRUE/FALSE)
Не замечать, что элемент отключён через ON_UPDATE_COMMAND_UI
Не замечать конфликт ID в resource.h
Обращаться к UI напрямую из рабочего потока
Получать искажённый текст при преобразовании CString и std::string
Ломать код, рассчитанный на MBCS, при переходе на Unicode
Забывать AFX_MANAGE_STATE в MFC DLL
Ломать COM/ActiveX, рассчитанные только на x86, при переходе на x64
Ошибаться в выборе/освобождении объектов GDI
Ломать раскладку с фиксированными координатами при высоком DPI

Дефекты MFC порой невозможно понять, глядя только на синтаксис C++, — нужно смотреть также на сообщения Windows, ресурсы, дескрипторы, модули и настройки среды выполнения.

36. Стоит ли выбирать MFC для новой разработки

Стоит ли выбирать MFC для совершенно нового проекта — вопрос, который лучше решать осторожно.

Если у выбора MFC и есть основания, то вот такие.

Требуется тесная интеграция с существующим кодом на MFC
Хочется повторно использовать существующие компоненты и экраны на MFC
Нужен контроль, максимально близкий к Win32/GDI/COM
В компании достаточно навыков сопровождения MFC
Целевая платформа ограничена десктопом Windows
По долгосрочному плану миграции сначала нужно нарастить функциональность именно на MFC

И наоборот, в следующих случаях лучше рассмотреть другие варианты.

Нужен современный UI
Требуется гибкая раскладка и анимация
В центре — интеграция с вебом или облаком
Важна тестируемость
Хочется выбрать технологию, к которой легко подключаются молодые разработчики
Нужна кроссплатформенность
С самого начала важны доступность (accessibility) и высокий DPI

MFC — не технология, «не стоящая изучения сегодня», и не технология, которую «выбирают за новизну»: это технология для работы с существующими нативными активами Windows.

37. Как подходить к миграции с MFC

Если вы хотите перевести MFC-приложение на другую технологию, попытка сразу нацелиться на полную переделку обычно заканчивается неудачей.

Сначала стоит разложить приложение вот так.

UI
Бизнес-логика
Форматы файлов
Сетевое взаимодействие
Работа с базой данных
Управление устройствами
Печать
Интеграция с COM/OLE
Управление настройками
Логирование

Из этого списка сильнее всего от MFC зависит UI.

А вот бизнес-логику и обработку файлов часто можно вынести.

Реалистичный порядок миграции выглядит так.

1. Воспроизвести среду сборки
2. Зафиксировать существующее поведение с помощью тестовых данных
3. Вынести логику из обработчиков UI-событий
4. Перенести её в C++-библиотеку, не зависящую от MFC
5. Добавить автоматические тесты
6. Задокументировать внешние спецификации
7. Поэтапно заменять экраны, начиная с тех, что нужны в первую очередь

Успеха проще добиться, если целью станет не «отказ от MFC» сам по себе, а «извлечение важной логики, запертой внутри MFC».

38. С чего начать чтение кода MFC

При первом знакомстве с существующим MFC-проектом стоит начинать с таких файлов.

*.vcxproj
  Смотрим набор инструментов, настройки MFC, кодировку, настройки среды выполнения

resource.h
  Смотрим ID ресурсов

*.rc
  Смотрим диалоги, меню, строки, значки

*App.cpp / *App.h
  Смотрим класс, производный от CWinApp, и InitInstance

MainFrm.cpp / MainFrm.h
  Смотрим главный фрейм и меню/панели инструментов

*Doc.cpp / *Doc.h
  Для Document/View смотрим структуру данных и код сохранения

*View.cpp / *View.h
  Смотрим отрисовку и работу с пользователем

*Dlg.cpp / *Dlg.h
  Смотрим диалоги, DDX, обработку кнопок

Далее — часто используемые ключевые слова для поиска.

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

Поиск по этим словам помогает лучше понять, как работает приложение.

39. Принципы проектирования при сопровождении MFC

Если предстоит долго сопровождать существующие активы на MFC, полезны такие принципы.

Делать обработчики UI-событий тонкими
Не пропускать CString и CWnd слишком глубоко в не-UI слои
Переносить бизнес-логику в обычные классы C++
Создавать тесты совместимости форматов файлов
Явно разграничивать различия x86/x64
Проводить ревью изменений ID ресурсов
Проверять состояние модуля в MFC DLL
Наладить логирование
Фиксировать сборку в CI
Регулярно проверять экраны при высоком DPI и на Windows 11

Особенно важно не перегружать обработкой CDialog и CView.

Экранные классы MFC стоит сосредоточить на вводе, отображении и передаче событий.

Взять значения с экрана
Передать их в сервис
Отразить результат на экране

Если удаётся удерживать код на таком уровне, MFC-приложение становится вполне удобным для сопровождения.

40. Практический чек-лист

Чек-лист для работы с MFC-приложением.

Чётко ли определена версия Visual Studio
Установлены ли компоненты MFC/ATL
Чётко ли определены целевые платформы x86/x64
Известны ли настройки Unicode/MBCS
Линкуется ли MFC статически или через общую DLL
Какой Visual C++ Redistributable требуется
Входят ли resource.h и .rc в область ревью
Отслеживается ли путь событий через карту сообщений
Правильно ли направление UpdateData
Используется ли структура Document/View
Безопасно ли устроено управление временем жизни немодальных диалогов
Не обращается ли рабочий поток к UI напрямую
Нет ли в MFC DLL мест, где нужен AFX_MANAGE_STATE
Проверяются ли экраны в средах с высоким DPI
Поддерживают ли зависимости от COM/ActiveX 32-бит/64-бит
Ведётся ли лог
Можно ли протестировать не-UI логику

Пока не привыкнешь, MFC выглядит своеобразно, но стоит понять, куда смотреть, — и его можно читать вполне закономерно.

41. Итоги

MFC — фреймворк с долгой историей для создания нативных десктопных приложений Windows на C++. Он перестал быть основным выбором для новой разработки, но по-прежнему остаётся важной технологией для сопровождения существующих активов, обновления среды сборки, добавления функциональности и поэтапной миграции.

Особенно важные моменты для понимания MFC — следующие.

MFC делает Win32 API удобнее в виде C++-классов
CWinApp управляет приложением целиком
Время жизни CWnd и HWND не совпадает
Карта сообщений связывает события с функциями
Document/View — механизм разделения данных и отображения
DDX/DDV используются для синхронизации и проверки значений в диалоге
Файлы ресурсов и resource.h чрезвычайно важны
CString и TCHAR нужно понимать вместе с настройками кодировки
В MFC DLL нужно внимательно следить за состоянием модуля
Старые MFC-приложения стоит пересматривать с точки зрения высокого DPI, x64, CI и тестирования

Работая с MFC, важнее сначала разобраться в структуре, чем сразу решать, что «раз старое — значит плохое». В существующих MFC-приложениях нередко накоплены многолетние знания о бизнесе, специфика под конкретных заказчиков, интеграция с устройствами и совместимость файлов. Чтобы сохранить эту ценность и постепенно облегчить сопровождение, реалистичный путь — понять приёмы MFC, вынести не-UI логику наружу и привести в порядок сборку и тесты.

Если попытаться свести всё к одной фразе, получится так.

Основа для работы с механикой нативных Windows-приложений через классы и фреймворк C++.

С этой точки зрения MFC — не просто устаревшая технология, а ключ к безопасному прочтению существующих активов Windows.

Справочные материалы

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое MFC?
MFC — это сокращение от Microsoft Foundation Classes, каркас (фреймворк) для Windows-приложений на C++, который делает Win32 API удобнее в виде классов. Окно представлено классом CWnd, диалог — CDialog, а приложение целиком — CWinApp. Это не «волшебная библиотека, которая позволяет писать код, не зная Windows», а способ упорядочить механику Windows через типы и фреймворк C++, поэтому по-прежнему требуются знания Win32 — сообщения Windows, дескрипторы (handle) и так далее.
MFC ещё можно использовать? Он всё ещё поддерживается?
MFC по-прежнему доступен в Visual Studio и продолжает поддерживаться. Однако в документации Microsoft отмечается, что новые функции и обновления документации для него больше не выпускаются. По сути, MFC — технология, которую реалистично применять для сопровождения существующих MFC-приложений, добавления в них функциональности, обновления среды сборки и постепенной миграции на другой UI, но для совершенно нового обычного GUI-приложения выбор в его пользу стоит делать осторожно. Для работы требуется установить отдельный компонент Visual Studio Installer — C++ MFC for latest build tools.
Что такое карта сообщений (message map) в MFC?
Это механизм MFC, который связывает сообщения Windows и команды с функциями-обработчиками. Между BEGIN_MESSAGE_MAP и END_MESSAGE_MAP с помощью макросов вроде ON_BN_CLICKED и ON_COMMAND описывается соответствие вида «при клике на эту кнопку вызвать эту функцию». Если поиск по коду не находит прямого вызова функции, но при этом она выполняется при возникновении события, нужно смотреть карту сообщений. В код-ревью MFC важно проверять не только сами функции-обработчики, но и карту сообщений вместе с ними.
Как перейти с MFC на другую технологию?
Попытка сразу нацелиться на полную переделку обычно заканчивается неудачей. Реалистичный порядок действий такой: сначала воспроизвести среду сборки, зафиксировать существующее поведение с помощью тестовых данных, вынести логику из обработчиков UI-событий и перенести её в C++-библиотеку, не зависящую от MFC, добавить автоматические тесты, задокументировать внешние спецификации и только затем поэтапно заменять экраны по мере необходимости. Успеха проще достичь, если целью ставить не «отказ от MFC» как таковой, а «извлечение важной логики, запертой внутри MFC» наружу.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог