Windows 的 MFC 是什麼──維護既有資產的基礎知識

· · Windows, MFC, VisualC++, Cpp, Win32, NativeApp, DesktopApp, LegacyCode, 既有資產活用

1. 首先要掌握的事情

Windows 舊版桌面應用程式維護時,可能會遇到這樣的名稱:

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

這些是 MFC 這個 C++ 用 Windows 應用程式框架中常見的名稱。MFC 是 Microsoft Foundation Classes 的縮寫,是一套讓 Win32 API 更容易以 C++ 類別方式使用的函式庫。

在現今的 Windows 應用程式開發中,有 WinUI、WPF、Windows Forms、Electron、Qt、Web 技術等各種選擇,因此把 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 是一套用 C++ 建立 Windows 原生桌面應用程式的類別函式庫。

若直接使用 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 完全替換成別的東西,而是以 Win32 API 的概念為基礎、範圍廣泛但薄薄一層的 C++ 框架。因此,要閱讀 MFC,不僅需要了解 MFC 的類別,也需要以下這些知識。

Windows訊息
HWND等控制代碼
GDI/GDI+
資源檔案
COM/OLE
DLL與執行環境
字元編碼
執行緒與訊息迴圈

與其說是「不了解 Windows 也能寫的魔法函式庫」,不如說「把 Windows 的機制以 C++ 型別與框架整理出來」才是 MFC 的真實面貌。

3. MFC 現在還能用嗎

MFC 目前仍可在 Visual Studio 中使用。不過切勿誤解它的定位。雖然目前仍受到支援,但它並不是會積極新增功能的最新 UI 框架,Microsoft 的 MFC 文件中也註明,MFC 會持續獲得支援,但不會新增功能或更新文件。

因此,MFC 的定位大致如下。

維護既有MFC應用程式              -> 實務上相當常見
既有MFC應用程式新增功能          -> 有可能
MFC應用程式更新建置環境          -> 重要
從MFC逐步遷移到其他UI            -> 有可能
全新開發一般GUI應用程式時採用    -> 應謹慎判斷

特別是長期使用的業務應用程式,UI、列印、檔案輸出入、裝置控制、自訂通訊協定、COM 整合等,經常整合在 MFC 之中。

在這樣的程式碼庫中,比起「捨棄 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. 在 Visual Studio 中使用 MFC 的準備

即使在 Visual Studio 中已安裝 C++,也不代表 MFC 一定隨附安裝。這是因為 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
是否需要Spectre Mitigations版本的MFC

如果建置時找不到 MFC 相關檔案,除了專案設定,也要確認 Visual Studio 端是否已安裝 MFC 元件。

CI 環境或建置伺服器也一樣,如果本機的 Visual Studio 能建置成功、但在 CI 中卻失敗,原因可能是 MFC 元件是否存在,或目標工具集版本不同所致。

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 的核心類別

MFC 中許多 UI 類別,都以代表 Windows 視窗的 CWnd 為基底。不過,CWnd 物件和 HWND 並不是同一個東西。

HWND
  由Windows作業系統管理的視窗控制代碼

CWnd
  用來方便處理HWND的C++包裝物件

在 MFC 中,CWnd 內部持有一個 HWND

HWND hWnd = m_hWnd;

或者,也可以像下面這樣取得。

HWND hWnd = GetSafeHwnd();

維護時重要的是,即使 CWnd* 存在,對應的 HWND 也可能已經被銷毀。

因此,要確認視窗是否有效時,會用以下這種方式。

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

在調查 MFC 的錯誤時,重要的是要留意 CWnd 這個 C++ 物件的生命週期,和實際 Windows 視窗控制代碼的生命週期是否出現不一致。

9. 訊息映射(Message Map)是什麼

MFC 特色最鮮明的機制之一,就是訊息映射(message map)

Windows 應用程式會以 Windows 訊息的形式,接收滑鼠點擊、按鍵輸入、重繪、視窗大小變更、選單選取等操作。

在 Win32 API 中,通常會用 WndProcswitch 陳述式來處理訊息。

在 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. 命令路由(Command Routing)

在 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 中最容易理解的形式之一,就是以對話框為主的應用程式。

設定畫面、簡單的業務工具、裝置操作畫面等,有時會以對話框為核心來組成。

典型的作法是繼承 CDialogCDialogEx

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 對話框中經常出現的,是 DDXDDV

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 對話框的輸入值出現異常時,可確認以下幾點。

DoDataExchange中是否定義了DDX
是否呼叫了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 精靈建立的應用程式中,InitInstance 中經常會有註冊 CSingleDocTemplateCMultiDocTemplate 的處理。

16. 理解資源檔案

在 MFC 應用程式中,.rc 檔案非常重要。

.rc 是 Windows 的資源檔案。

其中定義了以下這些內容。

對話框範本
選單
快速鍵
圖示
點陣圖
字串表
版本資訊
工具列

此外,resource.h 中則定義了資源 ID。

#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 不一致。

resource.h的ID被改變
不同分支合併時ID發生衝突
對話框上控制項的ID與DDX的ID不一致
應該刪除的選單ID卻還留著
字串表的ID重複

在調查 MFC 應用程式的行為時,除了 C++ 程式碼,也需要同時查看 .rcresource.h

17. Class Wizard 與手寫程式碼

MFC 與 Visual Studio 的 Class Wizard 之間,有著很深的歷史關係。

使用 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::stringstd::wstring,但在 MFC 中,由於與 API 及控制項的相容性較好,CString 仍被大量使用。

維護時,需要留意字元編碼。

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 也提供了用於檔案操作與序列化的類別,代表性的是 CFileCArchive

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 的序列化機制雖然方便,但在長期運作上需要留意。

與舊版檔案格式的相容性
版本編號的管理
讀取失敗時的復原
例外處理
字元編碼
位元組順序(Endian)
是否直接把結構體整個儲存

在長期使用自訂二進位格式的 MFC 應用程式中,Serialize 有時已經事實上成為檔案規格本身。

在這種情況下,變更程式碼之前,務必先準備能讀取既有檔案的測試資料。

20. GDI 繪製與 CDC

在 MFC 的畫面繪製中,經常使用 CDC 類別來處理 Windows 的 Device Context。在 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++ 邏輯,還需要確認 Windows 的 GDI 資源、重繪時機、DPI、字型大小。

21. 模態對話框與非模態對話框

在 MFC 中,顯示對話框的方式也需要注意。

模態對話框是用 DoModal 顯示。

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // OK時的處理
}

在這種情況下,呼叫端會等到對話框關閉為止。

另一方面,非模態對話框在建立之後,呼叫端的處理仍會繼續執行。

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

在非模態對話框中,生命週期管理相當重要。

new出來的對話框要在何時delete
父視窗是否會先被銷毀
對話框端是否使用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 更新

Windows 的 UI 基本上必須在建立該 UI 的執行緒中操作,MFC 應用程式也是一樣。如果從工作執行緒直接操作 UI 控制項,會導致不穩定的行為或當機。

以下是應避免的例子。

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

    // 避免從工作執行緒直接操作UI
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

一般會使用 PostMessage 等方式,把通知傳送給 UI 執行緒。

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 與模組狀態

在使用 MFC 建立 DLL 時,會出現模組狀態(module state)這樣的概念。

在從 MFC DLL 讀取資源、顯示對話框、建立擴充 DLL 這類場合中,要去哪個模組讀取資源,會成為一個問題。

在 MFC DLL 的函式入口處,經常會看到這樣的巨集。

AFX_MANAGE_STATE(AfxGetStaticModuleState());

這是為了讓 MFC 能使用正確的模組狀態。

如果忘記加上這個巨集,會導致以下這類問題。

找不到DLL內的對話框資源
字串資源從別的模組被讀取
找不到圖示或選單
只在偵測時能動作,發行版卻壞掉

在維護 MFC DLL 時,重要的是要整理清楚 EXE、一般 DLL、擴充 DLL、資源 DLL 之間的關係。

特別是從非 MFC 應用程式呼叫 MFC DLL、或採用外掛架構的情況下,需要特別注意。

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 程式碼中,經常出現 TCHARLPCTSTR_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 Control
COM伺服端
COM用戶端
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

在 MFC 應用程式的啟動處理中,有時會出現這樣的程式碼。

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

如果使用了 COM/OLE,有時看起來像是 MFC 的問題,但實際原因可能出在 COM 的初始化、執行緒模型、參照計數、註冊資訊、32bit/64bit 的差異上。

特別需要注意的是 32bit ActiveX 或 COM 元件。

32bit MFC應用程式要使用32bit COM
64bit MFC應用程式要使用64bit COM
32bit/64bit的COM註冊是分開的
舊版ActiveX有時未對應64bit

在把 MFC 應用程式改成 x64 時,除了 UI 程式碼,也一定要確認 COM/OLE 相依性。

28. 高 DPI 支援與現代 Windows

在現代 Windows 上執行舊版 MFC 應用程式時,畫面在高 DPI 環境下可能會顯示異常。

例如,以下這類問題。

文字被截斷
按鈕太小
固定像素繪製產生偏移
多台顯示器縮放比例不同時畫面跑掉
舊版點陣圖變得模糊
對話框版面配置變得擁擠

在 Windows 桌面應用程式中,應用程式必須明確宣告 DPI 對應模式。

即使是 MFC 應用程式,也需要確認資訊清單(manifest)、資源、繪製程式碼、字型、版面配置。

特別是像下面這種直接寫死座標的程式碼,在高 DPI 下容易出問題。

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

以固定像素為前提的座標,會在 DPI 改變時導致外觀跑掉。

維護時的確認觀點如下。

應用程式資訊清單的DPI設定
對話框資源的字型
固定像素繪製
影像資源的解析度
多台顯示器下的動作
Windows 10 / Windows 11上的顯示狀況

MFC 的高 DPI 對應,不一定光靠變更專案設定就能完成。對於舊版 UI,往往還需要實際確認畫面並修正版面配置。

29. 例外處理與錯誤處理

MFC 有自己一套例外類別與錯誤處理的作法。

在較舊的程式碼中,會看到這樣的巨集。

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

也有些程式碼會與現代 C++ 的 try / catch 混用。

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

採用這種結構後,PriceCalculatorParsePrices 就能用一般的 C++ 測試框架來測試。

不需要一次性重建整個 MFC 應用程式,光是先把事件處理器中可測試的處理抽離出來,就會有效果。

32. 固定建置環境

在維護 MFC 應用程式時,固定建置環境非常重要。

在較舊的程式碼庫中,以下差異可能導致建置結果不同。

Visual Studio的版本
MSVC工具集的版本
Windows SDK的版本
是否具備MFC/ATL元件
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
MFC靜態連結 / 共用DLL
執行環境函式庫設定
預先編譯標頭

在 MFC 應用程式中,stdafx.hpch.h 有時會集中大量相依關係。

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

只有某個檔案的編譯設定不同、或預先編譯標頭的設定有偏差,都可能導致建置損毀。

在維護專案中,把這些資訊寫進 README,之後會方便許多。

所需的Visual Studio版本
所需的工作負載與個別元件
所需的Windows SDK
目標平台
MFC的連結方式
建置步驟
CI的執行方式
發佈物的製作方式

擺脫「只有自己的電腦能建置」,是維護 MFC 的第一步。

33. 在 CI 中建置 MFC

MFC 應用程式也可以在 CI 中建置。

不過,CI 環境需要安裝 MFC 元件。

若使用 Visual Studio Build Tools,需要指定 MFC/ATL 的元件 ID 進行安裝。

以下是需要確認的重點。

Build Tools中是否安裝了MFC
目標工具集是否與專案一致
是否安裝了Windows SDK
是否同時確認x86建置與x64建置
資源編譯器是否能執行
是否有簽署處理
安裝程式產生是否也納入CI對象

雖然 MFC 應用程式要把 UI 測試全部自動化並不容易,但至少以下這些自動化是有效的。

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 時常見的陷阱。

不看訊息映射,只搜尋函式呼叫
以為CWnd*非null就代表有效
混淆HWND的生命週期與C++物件的生命週期
弄錯UpdateData(TRUE/FALSE)的方向
沒注意到被ON_UPDATE_COMMAND_UI停用
沒注意到resource.h的ID衝突
從工作執行緒直接操作UI
CString與std::string轉換時發生亂碼
Unicode化時破壞了以MBCS為前提的程式碼
在MFC DLL中忘記AFX_MANAGE_STATE
x64化時破壞了以x86為前提的COM/ActiveX
GDI物件的選取/釋放出錯
高DPI下固定座標版面配置跑掉

MFC 的錯誤,有時光看 C++ 語法無法理解,還需要包含 Windows 的訊息、資源、控制代碼、模組、執行環境設定一併查看。

36. 全新開發是否該選擇 MFC

對於完全全新的開發,是否該選擇 MFC,應謹慎判斷。

如果有理由選擇 MFC,通常是以下這些情況。

需要與既有MFC程式碼緊密整合
想重複使用既有的MFC元件或畫面
需要非常貼近Win32/GDI/COM的控制
公司內部已具備足夠的MFC維護技能
目標僅限於Windows桌面
在長期遷移計畫中,需要先以MFC擴充功能

反過來,若符合以下情況,則建議考慮其他選項。

想建立現代化的UI
需要靈活的版面配置或動畫
以Web整合或雲端整合為主
重視可測試性
希望選用讓年輕開發者容易參與的技術
需要跨平台支援
從一開始就重視無障礙支援或高DPI

MFC 既不是「現在學也沒有價值」的技術,也不是「因為新才選」的技術,而是用來面對既有 Windows 原生資產的技術。

37. 從 MFC 遷移時的思考方式

若想把 MFC 應用程式遷移到其他技術,一開始就想全面翻新,容易失敗。

首先,把應用程式拆解成以下這幾個部分來思考。

UI
業務邏輯
檔案格式
通訊處理
資料庫處理
裝置控制
列印
COM/OLE整合
設定管理
記錄

其中,最依賴 MFC 的是 UI。

另一方面,業務邏輯或檔案處理,則有可能被抽離出來。

實際可行的遷移順序如下。

1. 重現建置環境
2. 用測試資料固定既有行為
3. 從UI事件處理器中抽離邏輯
4. 移到非MFC的C++函式庫
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上確認畫面

特別重要的是,不要把太多處理塞進 CDialogCView 中。

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相依是否對應32bit/64bit
是否留下記錄
非UI邏輯是否可測試

MFC 在習慣之前可能顯得獨特,但只要知道該看哪裡,其實可以相當有規律地閱讀。

41. 總結

MFC 是一套歷史悠久的框架,用於以 C++ 建立 Windows 原生桌面應用程式。雖然已不再是全新開發的主流,但在維護既有資產、更新建置環境、新增功能、逐步遷移等場合,至今仍是重要的技術。

理解 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 邏輯抽離出來,並整頓建置與測試。

若要用一句話來總結,可以這樣說:

用 C++ 的類別與框架,來處理 Windows 原生應用程式運作機制的基礎。

有了這樣的觀點,MFC 就不只是單純的舊技術,而會成為安全解讀既有 Windows 資產的線索。

參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

MFC 是什麼?
MFC 是 Microsoft Foundation Classes 的縮寫,是一套讓 Win32 API 更容易以 C++ 類別方式處理的 Windows 應用程式框架。視窗以 CWnd、對話框以 CDialog、整個應用程式以 CWinApp 這類類別來表示。它並不是「不需要了解 Windows 就能寫程式的魔法函式庫」,而是把 Windows 的運作機制以 C++ 型別與框架整理出來的東西,因此仍然需要 Windows 訊息、控制代碼等 Win32 相關知識。
MFC 現在還能用嗎?還有支援嗎?
MFC 目前仍可在 Visual Studio 中使用,並持續獲得支援。不過 Microsoft 的文件中也註明,不會再新增功能或更新文件。就定位而言,維護既有 MFC 應用程式、新增功能、更新建置環境、逐步遷移到其他 UI,在實務上都相當常見;但若是全新開發的一般 GUI 應用程式,採用 MFC 則應謹慎判斷。此外需要安裝 Visual Studio Installer 中的個別元件(C++ MFC for latest build tools)。
MFC 的訊息映射(message map)是什麼?
這是 MFC 用來把 Windows 訊息或命令連結到處理函式的機制。在 BEGIN_MESSAGE_MAP 到 END_MESSAGE_MAP 之間,會用 ON_BN_CLICKED、ON_COMMAND 等巨集寫出「當這個按鈕被點擊時,呼叫這個函式」的對應關係。如果搜尋函式卻找不到直接呼叫、但事件發生時卻會執行,就要檢查訊息映射。在 MFC 的程式碼審查中,重要的是要把處理函式和訊息映射一起檢查,而不是只看處理函式本身。
要如何從 MFC 遷移到其他技術?
一開始就想著全面翻新,容易失敗。實際可行的順序是:先重現建置環境,用測試資料把既有行為固定下來,把 UI 事件處理器中的邏輯抽離出來、移到非 MFC 的 C++ 函式庫中,再加上自動化測試,並將外部規格文件化,最後再從需要的畫面開始逐步替換。與其把「擺脫 MFC」本身當作目標,把目標放在「把被封閉在 MFC 中的重要邏輯搬到外面」,反而更容易成功。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽