MFC trên Windows là gì ── Kiến thức nền để bảo trì tài sản hiện có

· Cập nhật ngày: · · Windows, MFC, Visual C++, C++, Win32, Native App, Desktop App, Legacy Code, Tái sử dụng tài sản hiện có

1. Những điểm cần nắm trước

Khi bảo trì các ứng dụng desktop Windows cũ, bạn đôi khi gặp những tên như thế này.

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

Chúng xuất hiện thường xuyên trong MFC, một framework ứng dụng Windows cho C++. MFC là viết tắt của Microsoft Foundation Classes, thư viện giúp làm việc với Win32 API dưới dạng class C++ dễ dùng hơn.

Trong phát triển ứng dụng Windows hiện nay có nhiều lựa chọn — WinUI, WPF, Windows Forms, Electron, Qt, công nghệ Web — nên MFC ít còn là ứng viên đầu tiên cho phát triển mới. Dẫu vậy, đây không phải công nghệ đã biến mất. Ứng dụng nghiệp vụ, thiết bị đo, phần mềm điều khiển, CAD/CAM, công cụ nội bộ, phần mềm đóng gói tồn tại từ lâu… vẫn thường để lại codebase MFC cần bảo trì.

Trước hết, hãy nêu những góc nhìn nên giữ khi tìm hiểu MFC.

MFC là bản đồ quan trọng để đọc ứng dụng desktop Windows cũ
Hãy nghĩ MFC không phải thứ giấu Win32 API, mà là thứ bọc Win32 theo kiểu C++
Không biết cách viết MFC thì dễ đọc sai hành vi hơn cả vẻ bề ngoài của mã
Giá trị nằm ở bảo trì, kéo dài đời, và di chuyển từng bước tài sản hiện có, hơn là chọn mới

Bài viết này sắp xếp tổng quan MFC, cấu trúc ứng dụng, message map, Document/View, hộp thoại, DDX/DDV, resource, build, và những điểm cần chú ý khi bảo trì.

Đối tượng đọc là người đã viết được C++ nhưng chưa từng dùng Win32 API hay MFC. Mức kiến thức nền khoảng như sau là đủ.

Lĩnh vực Mức kỳ vọng
C++ Đọc được class, kế thừa, hàm ảo, pointer và reference
Win32 API Chưa cần kinh nghiệm. Biết qua các từ cửa sổ và message là đủ
Visual Studio Đã từng mở solution và build
COM / OLE Chưa cần kinh nghiệm. Tới chương 27, lúc cần thì tra là đủ

Cuối chương 2 có liệt kê “kiến thức cần để đọc MFC”, nhưng bạn không phải gom đủ hết trước. Đọc rồi lúc cần thì quay lại là ổn.

Thứ tự đọc cũng không bắt buộc đi từ chương 1 tới chương 41. Dưới đây là đường ngắn theo mục đích.

Mục đích Chương nên đọc
Chỉ muốn biết MFC là gì và hiện đứng ở đâu Chương 2–4, chương 36
Muốn đọc được mã MFC hiện có Chương 6 → 9 → 10 → 14 → 12·13 → 38
Muốn sửa chỗ build không qua Chương 5, 25, 32, 33
Muốn điều tra lỗi Chương 34 → 35 → 8·22
Muốn quyết chính sách bảo trì hay di chuyển Chương 30, 31, 37, 39

Hai điểm cần nắm trước hết là message map (chương 9) và Document/View (chương 14). Hiểu hai phần này thì sẽ đọc được “vì sao hàm không tìm thấy nơi gọi lại vẫn chạy” và “dữ liệu nối với màn hình thế nào”. Bỏ hai phần này thì các chương khác sẽ lơ lửng.

Các đoạn mã trong bài được công bố trên GitHub như tập mã tham chiếu, sắp theo từng chương.

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

Bản đồ tri thức của bài viết này

MFC là framework giúp làm việc với Win32 API dưới dạng class C++ dễ dùng hơn: CWinApp đảm nhiệm khởi tạo toàn bộ ứng dụng, message map gắn Windows message với hàm handler, kiến trúc Document/View tách dữ liệu khỏi hiển thị UI, còn DDX/DDV tương ứng control trên hộp thoại với biến thành viên rồi kiểm tra. Để dùng MFC cần cài riêng thành phần MFC trong Visual Studio Installer, và có thể cấu hình dùng shared DLL hay link tĩnh. Bài viết nêu vị trí: với ứng dụng GUI thông thường hoàn toàn mới thì nên cân nhắc thận trọng khi chọn MFC; ngược lại, bảo trì, thêm chức năng và di chuyển từng bước ứng dụng MFC hiện có thì trong thực tế thường rất có giá trị.

Bản đồ tri thức bảo trì ứng dụng desktop Windows bằng MFCSơ đồ cho thấy MFC bọc Win32 API bằng C++ và triển khai các cơ chế như message map hay Document/View, vị trí phù hợp để bảo trì tài sản hiện có nhưng cần thận trọng với phát triển GUI mới, cùng các điểm cần chú ý về môi trường build và liên kết COMsử dụngsử dụngtự động hóatriển khaiyêu cầutriển khaiyêu cầusử dụngtriển khaiyêu cầusử dụngyêu cầucấu hình bằngyêu cầusử dụngyêu cầuyêu cầukhông khuyến nghịkhuyến nghị choyêu cầuMFC (Microsoft Foundation Classes)Win32 APICWinAppMessage MapKiến trúc Document/ViewDDX/DDVTệp resource (.rc/resource.h)CStringThành phần MFC của Visual Studio InstallerCách liên kết MFC (Use of MFC)Trạng thái module MFCActiveXYêu cầu khớp bitnessHỗ trợ DPI caoPhát triển ứng dụng GUI hoàn toàn mớiBảo trì và chuyển dần ứng dụng MFC hiện có

Trong sơ đồ, đường liền nét biểu thị quan hệ luôn đúng và đường nét đứt biểu thị quan hệ có điều kiện (điều kiện nằm trong phần giải thích từng quan hệ trên trang chi tiết). Danh sách đầy đủ các quan hệ (tổng 20, kèm bằng chứng và mức chắc chắn) cùng định nghĩa các khái niệm chính được tập hợp tại trang chi tiết bản đồ tri thức (bằng tiếng Nhật). Dữ liệu: JSON-LD / Turtle

2. MFC là gì

MFC là class library để viết ứng dụng desktop native Windows bằng C++.

Khi dùng Win32 API trực tiếp, bạn thường viết mã kiểu này.

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

Win32 API rất mạnh, nhưng vì được ghép quanh hàm C, handle, message và callback, ứng dụng lớn dễ mất tầm nhìn. MFC cho phép xử lý những thứ đó như class C++: cửa sổ là CWnd, hộp thoại là CDialog, toàn ứng dụng là CWinApp, frame window là CFrameWnd, view là CView.

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

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

MFC không thay Win32 API bằng một thứ hoàn toàn khác. Đó là framework C++ mỏng nhưng trải rộng, lấy cách nghĩ của Win32 API làm nền. Vì thế, để đọc MFC bạn cần không chỉ class MFC mà cả những kiến thức khoảng như sau.

Windows message
Handle như HWND
GDI/GDI+
Tệp resource
COM/OLE
DLL và runtime
Bảng mã
Thread và message loop

Thực chất của MFC không phải “thư viện phép thuật để viết Windows mà không cần biết Windows”, mà là “cơ chế của Windows được tổ chức thành kiểu C++ và framework”.

3. Hiện nay còn dùng MFC được không

MFC hiện vẫn dùng được trong Visual Studio. Nhưng đừng hiểu sai vị trí của nó. Dù vẫn được hỗ trợ, đây không phải framework UI hiện đại đang được thêm tính năng mới sôi nổi — tài liệu MFC của Microsoft cũng ghi chú rằng MFC tiếp tục được hỗ trợ nhưng sẽ không thêm tính năng mới hay cập nhật tài liệu.

Vì thế, vị trí của MFC đại khái như sau.

Bảo trì ứng dụng MFC hiện có              -> rất thường gặp trong thực tế
Thêm chức năng cho ứng dụng MFC hiện có  -> hoàn toàn có thể
Cập nhật môi trường build của ứng dụng MFC -> quan trọng
Di chuyển từng bước từ MFC sang UI khác  -> hoàn toàn có thể
Chọn cho ứng dụng GUI hoàn toàn mới, thông thường -> cân nhắc thận trọng

Đặc biệt với ứng dụng nghiệp vụ dùng lâu năm, UI, in ấn, I/O tệp, điều khiển thiết bị, protocol riêng, liên kết COM… đôi khi gom hết trong MFC.

Với codebase như vậy, trước “bỏ MFC” bạn cần “đưa codebase về trạng thái đọc được MFC”.

4. Những lĩnh vực MFC từng mạnh

Lĩnh vực điển hình mà MFC được dùng là ứng dụng desktop native Windows.

Cụ thể là những ứng dụng như sau.

Công cụ nghiệp vụ lấy hộp thoại làm trung tâm
Ứng dụng SDI mở tệp rồi chỉnh sửa
Ứng dụng MDI xử lý nhiều document
Màn hình điều khiển thiết bị đo hay máy sản xuất
Ứng dụng native thuộc họ CAD/CAM
Ứng dụng dùng nhiều in ấn và xem trước
Ứng dụng gồm liên kết ActiveX hay OLE
Ứng dụng gắn chặt với Windows API cũ hay tài sản COM

Điểm mạnh của MFC là chạy sát các thành phần native của Windows. Cửa sổ, menu, toolbar, status bar, hộp thoại, common control, in ấn, hộp thoại tệp, registry, vẽ GDI… đều dùng được như class C++.

Điểm yếu của MFC là xây UI hiện đại, data binding, khả năng kiểm thử, xử lý bất đồng bộ, layout hiện đại, hỗ trợ high DPI, đa ngôn ngữ, accessibility… không viết tự nhiên bằng framework gần đây.

Xếp đặc điểm lại thì như sau.

Gần với Windows native
Điều khiển trực tiếp bằng C++
Tài sản hiện có nhiều
Cần kiến thức Win32
Nhiều cách viết cũ
Muốn cấu trúc dễ kiểm thử thì phải tự sắp

5. Chuẩn bị dùng MFC trong Visual Studio

Dù đã cài C++ cho Visual Studio, MFC chưa chắc đã có. MFC được xử lý như Individual components của Visual Studio Installer. Điển hình, hãy kiểm các thành phần khoảng như sau.

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
Có cần bản MFC Spectre Mitigations hay không

Khi build mà không tìm thấy tệp liên quan MFC, đừng chỉ xem thiết lập dự án — hãy kiểm Visual Studio đã có thành phần MFC chưa.

Cách chọn trong installer như sau.

1. Mở Visual Studio Installer từ Start menu
2. Nhấn Modify trên bản Visual Studio đã cài
3. Tab Workloads, chọn Desktop development with C++
4. Ở Installation details bên phải cùng màn hình,
   chọn C++ MFC for latest v143 build tools
5. Nếu không thấy, chuyển sang tab Individual components,
   gõ MFC vào ô tìm kiếm
6. Nhấn Modify để cài

Chỉ chọn workload Desktop development with C++ đôi khi vẫn chưa có MFC, nên ở bước 4 hoặc bước 5 hãy chắc bản thân thành phần đã được chọn.

Khi cài từ script hay CI, hãy chỉ định component ID.

Tên hiển thị Component ID
C++ MFC for latest v143 build tools Microsoft.VisualStudio.Component.VC.ATLMFC
C++ ATL for latest v143 build tools Microsoft.VisualStudio.Component.VC.ATL
Desktop development with C++ (workload của Build Tools) Microsoft.VisualStudio.Workload.VCTools

Có hay không có thể kiểm bằng việc thư mục atlmfc — nơi đặt header MFC — có tồn tại không. Header và thư viện MFC nằm dưới MSVC toolset.

<nơi cài Visual Studio>\VC\Tools\MSVC\<phiên bản toolset>\atlmfc\include\afxwin.h

Nếu tìm bằng PowerShell thì như sau.

Get-ChildItem -Path "$env:ProgramFiles\Microsoft Visual Studio\2022" -Recurse -Filter afxwin.h -ErrorAction SilentlyContinue |
    Select-Object -ExpandProperty FullName

Không hiện gì thì thành phần MFC chưa được cài.

Build trong trạng thái đó sẽ dừng ngay lúc include afxwin.h. Điển hình là lỗi biên dịch sau.

fatal error C1083: Cannot open include file: 'afxwin.h': No such file or directory

Đọc lỗi này thành “sai include path” rồi lần mò Properties của dự án là đường vòng thường gặp. Khi không tìm thấy afxwin.h hay afxdialogex.h, hãy nghi Visual Studio Installer trước.

Môi trường CI hay build server cũng vậy: máy local build được mà CI thất bại thì nguyên nhân có thể là thiếu thành phần MFC, hoặc lệch phiên bản toolset đích.

6. Cấu trúc cơ bản của ứng dụng MFC

Ứng dụng MFC thường có cấu trúc khoảng như sau.

Lớp dẫn xuất CWinApp
  Chịu trách nhiệm khởi tạo và kết thúc toàn ứng dụng

Lớp dẫn xuất CFrameWnd / CMDIFrameWnd / CDialog
  Chịu trách nhiệm cửa sổ chính hay hộp thoại

Lớp dẫn xuất CView
  Chịu trách nhiệm hiển thị màn hình và thao tác người dùng

Lớp dẫn xuất CDocument
  Chịu trách nhiệm dữ liệu và lưu tệp

Tệp resource
  Giữ menu, hộp thoại, icon, chuỗi, v.v.

Message map
  Gắn Windows message và command với hàm handler

Ví dụ, ứng dụng MFC đơn giản sẽ có lớp dẫn xuất CWinApp như thế này.

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 là class biểu diễn toàn bộ ứng dụng. Trong ứng dụng MFC, thường có đúng một đối tượng dẫn xuất từ CWinApp.

Đối tượng toàn cục kiểu theApp này lúc đầu có thể hơi lạ, nhưng trong MFC đó là cấu trúc chuẩn.

7. CWinApp đang làm gì

CWinApp quan trọng với tư cách cửa vào của ứng dụng MFC. Ứng dụng Win32 thông thường tự viết WinMain, đăng ký window class, message loop, còn MFC thì framework gánh phần lớn việc đó. Người phát triển chủ yếu override InitInstance để viết khởi tạo riêng của ứng dụng.

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

    // Đọc cấu hình
    // Khởi tạo COM
    // Tạo cửa sổ chính
    // Đăng ký document template

    return TRUE;
}

Những xử lý hay được viết trong InitInstance là kiểu này.

Khởi tạo common control
Thiết lập registry key
Đọc danh sách tệp dùng gần đây
Đăng ký document template
Tạo main frame
Xử lý command-line argument
Khởi tạo COM/OLE

Khi bảo trì, hãy xem lớp dẫn xuất CWinApp trước — thứ tự khởi động của toàn ứng dụng sẽ hiện rõ hơn.

8. CWnd là class nằm ở trung tâm MFC

Phần lớn class UI của MFC lấy CWnd — biểu diễn cửa sổ Windows — làm lớp cơ sở. Tuy nhiên, đối tượng CWndHWND không phải cùng một thứ.

HWND
  Window handle do Windows OS quản lý

CWnd
  Đối tượng wrapper C++ để làm việc với HWND cho dễ

Trong MFC, CWnd giữ HWND bên trong.

HWND hWnd = m_hWnd;

Hoặc lấy như sau.

HWND hWnd = GetSafeHwnd();

Điểm quan trọng khi bảo trì: dù CWnd* còn tồn tại, HWND tương ứng có thể đã bị hủy.

Vì thế, cửa sổ còn hiệu lực hay không thì kiểm kiểu này.

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

Khi điều tra lỗi MFC, điều quan trọng là xem vòng đời đối tượng C++ của CWnd có lệch với vòng đời window handle Windows thực tế hay không.

9. Message map là gì

Một trong những cơ chế thể hiện rõ nhất “chất MFC” là message map.

Ứng dụng Windows nhận click chuột, nhập phím, vẽ lại, đổi kích thước cửa sổ, chọn menu… dưới dạng Windows message.

Với Win32 API, bạn thường xử lý message bằng câu switch trong WndProc.

MFC gắn chúng với hàm handler bằng message map.

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_BN_CLICKED(IDC_BUTTON_OK, &CMyDialog::OnClickedButtonOk)
    ON_WM_CLOSE()
END_MESSAGE_MAP()

void CMyDialog::OnClickedButtonOk()
{
    AfxMessageBox(_T("Clicked"));
}

Nghĩa của đoạn mã này như sau.

Khi nút IDC_BUTTON_OK được bấm
thì gọi CMyDialog::OnClickedButtonOk

Vẽ luồng từ lúc message tới đến lúc handler được gọi thì như sau.

Luồng từ Windows message tới hàm handlerWindows message đi qua message loop và CWnd::WindowProc, rồi lần theo message map từ chính class tới class cơ sở cho đến khi gọi handler hoặc giao cho DefWindowProc.khôngtìm thấykhông có đến cuốiWindows messageWM_COMMAND / WM_PAINT v.v.Message loopFramework MFC chạy vòng lặpCWnd::WindowProcCửa vào chung do MFC chuẩn bịCó mục khớp trong message mapcủa chính class này không?Gọi hàm handler màON_BN_CLICKED v.v. trỏ tớiTìm message map của class cơ sởLần lượt từ CDialogEx tới CDialog, CWndCó tìm thấy ở đâu đó không?DefWindowProcGiao cho xử lý mặc định của Windows

Lý do không thấy câu switch là vì framework gánh phần “lần theo message map từ chính class tới class cơ sở”. Hãy nghĩ DECLARE_MESSAGE_MAPBEGIN_MESSAGE_MAP là macro chuẩn bị bảng tương ứng theo từng class cho cuộc tìm đó.

Người chưa quen đọc MFC thường khó thấy hàm được gọi từ đâu. Nếu tìm mà không thấy chỗ gọi trực tiếp, hãy nhìn message map.

Hàm không được gọi trực tiếp
nhưng vẫn chạy khi có sự kiện
-> Kiểm tra macro BEGIN_MESSAGE_MAP / ON_...

Khi review mã MFC, quan trọng là xem hàm handler cùng với message map, không chỉ riêng hàm.

10. Command routing

Trong MFC, thao tác menu và toolbar cũng được xử lý như command.

Điển hình là ON_COMMAND.

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

void CMainFrame::OnFileOpen()
{
    // Xử lý mở tệp
}

MFC có cơ chế phân phối command tới đối tượng thích hợp.

Ví dụ, cùng một ID_EDIT_COPY, view đang active, document, frame, hay ứng dụng — bên nào xử lý có thể thay đổi.

View đang active
Document
Frame window
Ứng dụng

Command được chuyển theo thứ tự đó tới đối tượng nào xử lý được.

Vì thế, trong MFC đôi khi khó lần “bấm menu thì hàm nào được gọi” chỉ bằng tìm chuỗi đơn giản.

Khi bảo trì, hãy nhìn những điểm khoảng như sau.

Command ID là gì
ON_COMMAND nằm ở class nào
ON_UPDATE_COMMAND_UI nằm ở đâu
View đang active là view nào
Có dùng cấu trúc Document/View không

11. ON_UPDATE_COMMAND_UI là gì

Trong MFC, để cập nhật bật/tắt, trạng thái check, văn bản hiển thị của mục menu hay nút toolbar, đôi khi dùng 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());
}

Nhờ đó, menu hay nút chỉ bật khi đang ở trạng thái xóa được.

Khi dùng ứng dụng MFC mà nút tự nhiên xám, menu không bấm được, trạng thái check đổi… thì tìm ON_UPDATE_COMMAND_UI đôi khi ra nguyên nhân.

12. Ứng dụng MFC dựa trên hộp thoại

Một trong những dạng dễ hiểu nhất của MFC là ứng dụng dialog-based.

Màn hình cấu hình, công cụ nghiệp vụ đơn giản, màn hình thao tác thiết bị… đôi khi được ghép quanh hộp thoại.

Điển hình là kế thừa CDialog hoặc 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;
};

Trong mã dialog-based, các yếu tố sau hay xuất hiện.

IDD_...        ID resource hộp thoại
IDC_...        ID control
OnInitDialog   Xử lý khởi tạo
DoDataExchange Liên kết control với biến thành viên
UpdateData     Đồng bộ màn hình và biến
ON_BN_CLICKED  Xử lý bấm nút

Hộp thoại không chỉ là giao diện: resource, biến thành viên, message map và khởi tạo kết hợp mới chạy.

13. DDX và DDV

Trong hộp thoại MFC, DDXDDV hay xuất hiện.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX là cơ chế tương ứng control trên hộp thoại với biến thành viên C++, DDV là cơ chế kiểm tra giá trị nhập đó.

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

Gọi UpdateData(TRUE) thì giá trị nhập trên màn hình được ghi vào biến thành viên.

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

    // Ở đây m_name và m_interval đã chứa giá trị nhập trên màn hình
    SaveSettings(m_name, m_interval);

    CDialogEx::OnOK();
}

Ngược lại, gọi UpdateData(FALSE) thì giá trị biến thành viên được ghi ra màn hình.

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

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

    return TRUE;
}

Khi giá trị nhập của hộp thoại MFC sai, hãy kiểm những điểm khoảng như sau.

DoDataExchange đã định nghĩa DDX chưa
Đã gọi UpdateData(TRUE) chưa
Thời điểm gọi UpdateData(FALSE) có đúng không
DDV có đang từ chối input không
Control ID có khớp với resource không

14. Kiến trúc Document/View

Một đặc điểm lớn của MFC là kiến trúc Document/View.

Đây là cấu trúc tách dữ liệu ứng dụng xử lý khỏi phần hiển thị dữ liệu đó.

CDocument
  Giữ dữ liệu
  Chịu trách nhiệm đọc/ghi tệp
  Thông báo cập nhật tới nhiều view

CView
  Hiển thị dữ liệu
  Xử lý thao tác người dùng
  Quản lý vẽ và trạng thái chọn

Vẽ quan hệ giữa frame, view và document thì như sau.

Quan hệ giữa frame, view và documentDocument template gắn frame window, document và view; view lấy document bằng GetDocument, document thông báo mọi view bằng UpdateAllViews.Lấy bằng GetDocumentThông báo bằng UpdateAllViewsLớp dẫn xuất CWinAppKhởi động và kết thúc toàn ứng dụngDocument templateCSingleDocTemplate / CMultiDocTemplateFrame windowCFrameWnd / CMDIChildWndLớp dẫn xuất CDocumentDữ liệu và I/O tệpLớp dẫn xuất CViewVẽ và thao tác người dùng

Điểm then chốt: thứ gắn ba phần frame, document và view lại là document template. Template này được đăng ký trong InitInstance, nên muốn biết “view nào nhìn document nào” thì hãy đọc InitInstance trước.

View nhìn document thì dùng GetDocument, phía document muốn báo thay đổi tới mọi view thì dùng UpdateAllViews — nắm khác biệt chiều này cũng giúp lần lỗi màn hình không cập nhật dễ hơn.

Ví dụ, trình soạn văn bản, trình soạn hình, công cụ sửa tệp cấu hình, ứng dụng kiểu CAD… thì tách dữ liệu khỏi hiển thị có ý nghĩa.

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

Phía view lấy document rồi vẽ.

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

    for (const auto& item : pDoc->m_items)
    {
        // Vẽ bằng pDC
    }
}

Ưu điểm của Document/View là cùng một dữ liệu dễ hiển thị bằng nhiều view.

Ví dụ, cùng dữ liệu có thể hiện như sau.

View dạng bảng
View dạng đồ thị
View chi tiết
Xem trước
View in

Tuy nhiên, đưa Document/View vào màn hình cấu hình đơn giản hay công cụ nhỏ đôi khi lại làm cấu trúc nặng hơn.

Khi bảo trì, hãy phân biệt ngay ứng dụng đó dùng Document/View hay lấy hộp thoại làm trung tâm — lần mã sẽ dễ hơn.

15. SDI và MDI

Trong MFC, kết hợp với Document/View, cấu trúc SDI và MDI hay xuất hiện.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI về cơ bản là một frame xử lý một document.

Cửa sổ chính
  Một document
  Một hoặc nhiều view

MDI là một cửa sổ cha chứa nhiều cửa sổ con, mỗi cửa sổ con xử lý một document.

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

Ứng dụng Windows cũ hay dùng MDI.

Khi bảo trì, nhìn tên class là đoán được cấu trúc.

CFrameWnd       Frame họ SDI
CMDIFrameWnd    MDI parent frame
CMDIChildWnd    MDI child frame
CSingleDocTemplate Document template cho SDI
CMultiDocTemplate  Document template cho MDI

Ứng dụng do wizard MFC tạo ra thường có xử lý đăng ký CSingleDocTemplate hoặc CMultiDocTemplate trong InitInstance.

16. Hiểu tệp resource

Trong ứng dụng MFC, tệp .rc rất quan trọng.

.rc là tệp resource của Windows.

Ở đó định nghĩa những thứ như sau.

Dialog template
Menu
Phím accelerator
Icon
Bitmap
String table
Thông tin phiên bản
Toolbar

Ngoài ra, resource.h định nghĩa resource ID.

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

Mã MFC dùng các ID này để gắn resource với mã C++.

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

Vấn đề thường gặp khi bảo trì là ID resource không khớp.

ID trong resource.h đã đổi
Merge nhánh khác làm ID đụng nhau
ID control trên hộp thoại không khớp ID của DDX
ID menu lẽ ra đã xóa vẫn còn
ID trong string table bị trùng

Khi điều tra hành vi ứng dụng MFC, cần nhìn đồng thời mã C++, .rcresource.h, không chỉ mã C++.

17. Class Wizard và mã viết tay

MFC có lịch sử gắn chặt với Class Wizard của Visual Studio.

Dùng Class Wizard thì có thể sinh tự động message handler, biến DDX, override hàm ảo, v.v.

Vì thế, mã MFC còn nhiều đoạn mang hình dạng do công cụ sinh ra.

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

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

Visual Studio mới hơn có thể đổi cách hiện và dạng được sinh, nhưng codebase cũ đôi khi vẫn còn marker comment kiểu này.

Khi bảo trì, điều quan trọng là đừng phá ẩu ranh giới giữa mã sinh và mã viết tay.

Đừng xóa message map
Đừng phá tương ứng DDX
Đừng đổi resource ID một cách tùy tiện
Đừng xóa bừa comment vốn dành cho Class Wizard cũ

Với MFC, chỉ cần biên dịch được như C++ thôi là chưa đủ. Bạn cũng cần giữ, ở mức nhất định, hình dạng mà Resource Editor và Class Wizard của Visual Studio kỳ vọng.

18. CString và chuỗi

Class chuỗi xuất hiện thường xuyên trong MFC là CString.

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

CString là class chuỗi độ dài thay đổi hay dùng trong mã họ MFC/ATL. C++ hiện đại thường dùng std::string hay std::wstring, nhưng MFC dùng nhiều CString vì hợp với API và control.

Khi bảo trì, cần để ý bảng mã.

CString      Tùy thiết lập dự án, tương đương CStringA hoặc CStringW
CStringA     Họ ANSI / MBCS
CStringW     Họ Unicode / UTF-16
LPCTSTR      Con trỏ chuỗi dựa trên TCHAR
LPCSTR       Họ char
LPCWSTR      Họ wchar_t
std::string  Thường là họ char
std::wstring Họ wchar_t

Ứng dụng Windows hiện nay về cơ bản an toàn hơn nếu lấy Unicode làm tiền đề, nhưng ứng dụng MFC cũ đôi khi còn mã lấy MBCS làm tiền đề.

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

Lỗi chuyển chuỗi hay gặp khi bảo trì ứng dụng MFC.

Đặc biệt hãy chú ý những trường hợp sau.

Đọc tệp lấy Shift_JIS làm tiền đề
Chuyển sang build Unicode
DLL bên ngoài đòi char*
COM đòi BSTR
Chuyển ẩu sang std::string rồi bị lỗi font

Thấy CString thì đừng chỉ nghĩ “class chuỗi cũ”, hãy kiểm cùng thiết lập character set của dự án, API bên ngoài, và định dạng tệp.

19. CFile và CArchive

MFC cũng có class cho thao tác tệp và serialize. Điển hình là CFileCArchive.

CFile file;
if (file.Open(path, CFile::modeRead))
{
    CArchive ar(&file, CArchive::load);
    // Đọc từ ar
}

CArchive hay được dùng trong cơ chế serialize của MFC.

Lớp dẫn xuất CDocument đôi khi override Serialize và viết đọc cùng lưu trong cùng một hàm.

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

Serialize của MFC tiện, nhưng vận hành dài hạn thì cần chú ý.

Tương thích với định dạng tệp cũ
Quản lý số phiên bản
Khôi phục khi đọc thất bại
Xử lý exception
Bảng mã
Endian
Có đang lưu nguyên struct hay không

Ứng dụng MFC dùng định dạng nhị phân riêng trong thời gian dài đôi khi lấy Serialize làm đặc tả tệp trên thực tế.

Trong trường hợp đó, trước khi đổi mã, hãy chuẩn bị dữ liệu kiểm thử đọc được tệp hiện có.

20. Vẽ GDI và CDC

Khi vẽ màn hình MFC, class CDC để làm việc với Device Context của Windows hay được dùng. Trong CView::OnDraw, CDC* được truyền như đối số.

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

Khi dùng pen hay brush, hãy chú ý chọn rồi khôi phục.

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

Xung quanh đối tượng GDI, những sai lầm sau thành vấn đề.

SelectObject rồi không trả về đối tượng cũ
Tạo hàng loạt đối tượng GDI mà không hủy
Nhầm vai trò OnPaint và OnDraw
Không double-buffer nên bị nhấp nháy
Vẽ lấy pixel cố định làm tiền đề bị vỡ ở high DPI

Lỗi vẽ MFC không chỉ nằm ở logic C++: cần kiểm resource GDI của Windows, thời điểm vẽ lại, DPI, kích thước font.

21. Hộp thoại modal và modeless

Trong MFC, cách mở hộp thoại cũng cần chú ý.

Hộp thoại modal được hiện bằng DoModal.

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // Xử lý khi OK
}

Trong trường hợp này, bên gọi đợi đến khi hộp thoại đóng.

Ngược lại, hộp thoại modeless sau khi tạo xong vẫn trả điều khiển về bên gọi.

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

Với hộp thoại modeless, quản lý vòng đời là quan trọng.

Hộp thoại đã new thì xóa lúc nào
Cửa sổ cha có bị hủy trước không
Phía hộp thoại có dùng PostNcDestroy không
Có bị tạo trùng không
Pointer sau khi đóng còn sót không

Điều tra crash MFC đôi khi ra nguyên nhân từ vòng đời hộp thoại modeless.

22. Vòng đời đối tượng C++ và Windows handle

Điều rất quan trọng trong MFC là khác biệt vòng đời giữa đối tượng C++ và Windows handle. Ví dụ CWnd là đối tượng C++, còn cửa sổ thực tế do Windows quản lý như HWND, và hai thứ này không phải lúc nào cũng được tạo cùng lúc rồi biến mất cùng lúc.

Có đối tượng CWnd nhưng chưa có HWND
HWND đã bị hủy nhưng đối tượng CWnd còn lại
Wrapper CWnd tạm thời đang được tạo
Đang gắn/tháo handle bằng Attach/Detach

Ví dụ, đoạn mã như sau cần hết sức chú ý.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// Lưu pWnd vào thành viên rồi dùng sau

Giữ lâu pointer lấy từ GetDlgItem thì có nguy cơ tham chiếu sau khi cửa sổ đã bị hủy.

Nếu cần, mỗi lần gọi lại GetDlgItem, hoặc quản lý biến thành viên dành cho control bằng DDX, đôi khi an toàn hơn.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

Trong MFC, pointer khác null chưa chắc đã an toàn.

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

Cảm giác này rất quan trọng khi bảo trì MFC.

23. Thread và cập nhật UI

UI Windows về cơ bản phải được thao tác trên UI thread đã tạo ra nó, và ứng dụng MFC cũng vậy. Worker thread thao tác trực tiếp UI control sẽ gây hành vi không ổn định hoặc crash.

Ví dụ nên tránh.

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

    // Tránh chạm UI trực tiếp từ worker thread
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

Thông thường, hãy thông báo sang UI thread bằng PostMessage chẳng hạn.

constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;

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

    // Xử lý nặng

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

Phía UI nhận bằng 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;
}

Xung quanh thread của MFC, hãy kiểm những điểm sau.

Có đang chạm UI trực tiếp từ worker thread không
Có PostMessage sau khi cửa sổ đã bị hủy không
Có dừng UI thread để đợi thread kết thúc không
Khóa dữ liệu dùng chung có thích hợp không
Có hiểu nhầm giá trị trả về và vòng đời của AfxBeginThread không

24. MFC DLL và module state

Khi làm DLL bằng MFC, khái niệm module state xuất hiện.

Những lúc MFC DLL đọc resource, hiện hộp thoại, làm extension DLL… vấn đề là resource của module nào được tìm.

Ở cửa vào hàm của MFC DLL, đôi khi thấy macro như sau.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

Đây là macro để MFC dùng đúng module state.

Quên macro này sẽ dẫn tới lỗi kiểu sau.

Không tìm thấy dialog resource trong DLL
String resource bị đọc từ module khác
Không tìm thấy icon hay menu
Chỉ chạy được lúc debug, bản release thì hỏng

Khi bảo trì MFC DLL, điều quan trọng là sắp quan hệ giữa EXE, DLL thường, extension DLL và resource DLL.

Đặc biệt cần chú ý khi ứng dụng không MFC gọi MFC DLL, hoặc khi cấu hình plugin.

Ứng dụng MFC có thiết lập “Use of MFC” trong project settings.

Hai lựa chọn điển hình là như sau.

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

Dùng shared DLL thì môi trường chạy cần MFC runtime và Visual C++ runtime tương ứng.

Link tĩnh thì gói phân phối có vẻ đơn giản hơn, nhưng cần nghĩ tới kích thước tệp thực thi, cập nhật, tiếp nhận bản vá bảo mật, giấy phép và điều kiện tái phân phối.

Không bên nào luôn đúng.

Cơ sở để quyết khoảng như sau.

Máy đích có cài được Visual C++ Redistributable không
Có muốn đưa ứng dụng gần tới một file exe không
Phản ánh bản vá bảo mật bằng cách nào
Nhiều ứng dụng có dùng chung cùng runtime không
Có chuẩn bị được installer không
Phiên bản Windows đích là gì

Khi bảo trì, trước hết hãy xác nhận thiết lập hiện tại.

Configuration Properties
  General
    Use of MFC

Thêm nữa, hãy nhìn thiết lập Runtime Library.

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

Thiết lập link của MFC và CRT lẫn lộn thì ranh giới thư viện có thể phát sinh vấn đề cấp phát/giải phóng bộ nhớ.

26. Unicode, MBCS, TCHAR

Mã MFC cũ hay xuất hiện macro TCHAR, LPCTSTR, _T().

CString title = _T("Cài đặt");
SetWindowText(title);

Đây là cách viết để đáp ứng cả build Unicode lẫn build MBCS.

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

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

Hiện nay build Unicode là phổ biến, nhưng ứng dụng cũ đôi khi còn xử lý lấy MBCS làm tiền đề.

Đặc biệt với tệp bên ngoài, protocol truyền thông, DLL cũ, kết nối cơ sở dữ liệu, giao tiếp serial… cần xác nhận tiền đề bảng mã.

Điểm cần chú ý: đừng nghĩ Unicode hóa chỉ là việc thay thế đơn giản.

Kích thước mảng char là số byte hay số ký tự
Có đang dùng strlen không
Có đang lấy sizeof(buffer) làm số ký tự không
API bên ngoài nhận UTF-16 hay Shift_JIS
Định dạng lưu tệp có được phép đổi không

Khi sửa chuỗi MFC, hãy kiểm không chỉ hiển thị màn hình mà cả tương thích tệp và liên kết bên ngoài.

27. MFC và COM/OLE/ActiveX

MFC cũng từng được dùng trong ứng dụng gắn chặt với COM, OLE, ActiveX.

Ứng dụng nghiệp vụ cũ đôi khi còn những yếu tố như sau.

OLE Automation
ActiveX Control
COM server
COM client
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

Khởi động ứng dụng MFC đôi khi có mã như sau.

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

Khi dùng COM/OLE, trông như lỗi MFC nhưng thực ra nguyên nhân có thể là khởi tạo COM, threading model, reference count, thông tin đăng ký, khác biệt 32-bit/64-bit.

Đặc biệt hãy chú ý ActiveX hay COM component 32-bit.

Ứng dụng MFC 32-bit dùng COM 32-bit
Ứng dụng MFC 64-bit dùng COM 64-bit
Đăng ký COM 32-bit/64-bit là tách nhau
ActiveX cũ đôi khi chưa hỗ trợ 64-bit

Khi đưa ứng dụng MFC lên x64, hãy kiểm không chỉ mã UI mà cả phụ thuộc COM/OLE.

28. Hỗ trợ high DPI và Windows hiện đại

Chạy ứng dụng MFC cũ trên Windows hiện đại thì môi trường high DPI đôi khi làm hiển thị vỡ.

Ví dụ những vấn đề như sau.

Chữ bị cắt
Nút quá nhỏ
Vẽ pixel cố định bị lệch
Nhiều màn hình với tỷ lệ phóng khác nhau thì vỡ
Bitmap cũ bị mờ
Layout hộp thoại bị chật

Ứng dụng desktop Windows cần khai báo rõ chế độ hỗ trợ DPI.

Ứng dụng MFC cũng cần kiểm manifest, resource, mã vẽ, font, layout.

Đặc biệt, mã viết thẳng tọa độ cố định như sau dễ thành vấn đề ở high DPI.

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

Tọa độ lấy pixel cố định làm tiền đề sẽ vỡ giao diện khi DPI đổi.

Góc kiểm khi bảo trì như sau.

Thiết lập DPI trong application manifest
Font của dialog resource
Vẽ pixel cố định
Độ phân giải của image resource
Hành vi trên nhiều màn hình
Hiển thị trên Windows 10 / Windows 11

Hỗ trợ high DPI của MFC không phải lúc nào cũng xong chỉ bằng đổi project settings. UI cũ cần kiểm màn hình thực tế và sửa layout.

29. Xử lý exception và xử lý lỗi

MFC có class exception riêng và cách viết xử lý lỗi riêng.

Mã cũ đôi khi thấy macro như sau.

TRY
{
    // Xử lý
}
CATCH(CFileException, e)
{
    e->ReportError();
}
END_CATCH

Cũng có mã lẫn try / catch của C++ hiện đại.

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

Khi bảo trì MFC, hãy chú ý những điểm sau.

Exception MFC và exception chuẩn C++ có đang lẫn không
Có hiểu nhầm vòng đời đối tượng exception không
Có hiểu macro THROW/CATCH cũ không
Lỗi trả về và exception có đang lẫn không
Có đang ở trạng thái chỉ AfxMessageBox, không còn log không

Ứng dụng nghiệp vụ cần không chỉ hiện thông báo lỗi trên màn hình mà còn giữ log, lịch sử thao tác, giá trị nhập, trạng thái kết nối bên ngoài.

Ứng dụng MFC cũ đôi khi lỗi chỉ kết thúc bằng AfxMessageBox.

AfxMessageBox(_T("Lưu thất bại"));

Muốn tăng khả năng bảo trì thì hãy tách hiển thị UI khỏi ghi log.

LogError(_T("Save failed"), path);
AfxMessageBox(_T("Lưu thất bại. Hãy kiểm tra log."));

30. Làm MFC sống cùng C++ hiện đại thế nào

Bảo trì ứng dụng MFC không có nghĩa phải viết hết theo cách C++ cũ.

Tầng UI tôn trọng cách viết MFC, còn domain logic và tính toán thì sắp được bằng C++ hiện đại.

Ví dụ, hãy tách như sau.

Tầng MFC
  CDialog
  CView
  CDocument
  CString
  Message map
  Thao tác resource

Tầng không MFC
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  Class có thể unit test
  Business logic

Dạng xấu là mọi xử lý bị nhồi vào class hộp thoại.

void CMainDialog::OnBnClickedExecute()
{
    // Lấy input
    // Đọc tệp
    // Truyền thông
    // Tính toán
    // Cập nhật DB
    // Cập nhật màn hình
    // Ghi log
    // Xử lý exception
}

Mã như vậy khó đổi, khó kiểm thử, điều tra lỗi cũng khó.

Muốn cải thiện thì hãy đưa logic ra khỏi class 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);
}

Làm vậy thì m_service.Execute kiểm thử được mà không cần MFC.

Cải thiện hiệu quả nhất khi bảo trì tài sản MFC hiện có là tách dần logic ra khỏi class UI.

31. Làm mã MFC dễ kiểm thử

Ứng dụng MFC để nguyên thường khó unit test.

Lý do là UI, Win32, tệp, truyền thông, cơ sở dữ liệu, trạng thái toàn cục dễ gắn chặt với nhau.

Cách nghĩ để dễ kiểm thử như sau.

Đừng cố test trực tiếp CDialog hay CView
Trước hết tách logic không UI
Chuyển kiểu MFC ở ranh giới
Đưa tệp và truyền thông thành interface
Làm event handler màn hình mỏng

Ví dụ, chuyển xử lý sang class C++ thuần.

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

Phía MFC chỉ chịu trách nhiệm input và 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);
}

Với cấu trúc này, PriceCalculatorParsePrices kiểm thử được bằng framework test C++ thông thường.

Không cần làm lại toàn bộ ứng dụng MFC một lần; chỉ cần đưa xử lý kiểm thử được ra khỏi event handler đã có hiệu quả.

32. Cố định môi trường build

Khi bảo trì ứng dụng MFC, cố định môi trường build là quan trọng.

Codebase cũ đôi khi cho kết quả build khác nhau vì những khác biệt sau.

Phiên bản Visual Studio
Phiên bản MSVC toolset
Phiên bản Windows SDK
Có thành phần MFC/ATL hay không
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
Link tĩnh MFC / shared DLL
Thiết lập runtime library
Precompiled header

Ứng dụng MFC đôi khi gom nhiều phụ thuộc vào stdafx.h hay pch.h.

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

Chỉ một tệp lệch thiết lập biên dịch, hoặc lệch thiết lập precompiled header, cũng có thể làm build hỏng.

Dự án bảo trì nên ghi rõ thông tin này trong README cho dễ về sau.

Phiên bản Visual Studio cần
Workload và Individual components cần
Windows SDK cần
Nền tảng đích
Cách link MFC
Quy trình build
Cách chạy CI
Cách tạo gói phân phối

Tốt nghiệp khỏi “máy tôi thì build được” là bước đầu của bảo trì MFC.

33. Build MFC trên CI

Ứng dụng MFC cũng build được trên CI.

Tuy nhiên, môi trường CI cần có thành phần MFC.

Khi dùng Visual Studio Build Tools, cần chỉ định component ID của MFC/ATL rồi cài. ID cần chỉ định giống bảng ở chương 5.

Những điểm nên kiểm như sau.

Build Tools đã có MFC chưa
Toolset đích có khớp dự án không
Đã có Windows SDK chưa
Đã kiểm cả build x86 lẫn x64 chưa
Resource compiler có chạy không
Có bước ký không
Tạo installer có nằm trong phạm vi CI không

Ứng dụng MFC thì tự động hóa tới UI test không dễ, nhưng ít nhất tự động hóa những phần sau vẫn có ích.

Build Debug / Release
Build x86 / x64
Phân tích tĩnh
Unit test
Tạo installer
Lưu hash của artifact
Kiểm DLL phụ thuộc

Bảo trì dài hạn, chỉ giữ được trạng thái build được cũng đã có giá trị lớn.

34. Chỗ cần nhìn khi debug ứng dụng MFC

Khi lần lỗi ứng dụng MFC, đi theo thứ tự này thì hiệu quả.

1. Màn hình nào
2. Hộp thoại, View, hay Frame
3. Resource ID tương ứng thao tác là gì
4. Message map đi tới hàm nào
5. Chiều UpdateData có đúng không
6. Nếu Document/View thì trạng thái Document là gì
7. Command routing có chảy sang class khác không
8. Có chạm UI từ worker thread không
9. Exception hay lỗi có bị nuốt chỉ bằng AfxMessageBox không
10. Có vấn đề chưa khởi tạo hay vòng đời riêng bản Release không

Ví dụ, lỗi “bấm nút mà không có gì xảy ra” thì kiểm những chỗ sau.

IDC của nút có đúng không
ON_BN_CLICKED có tồn tại không
Chữ ký handler có đúng không
Dialog resource có phải cái khác không
Nút có đang bị vô hiệu không
Giữa chừng UpdateData có thất bại không
Exception có bị nuốt không

“Menu không bấm được” thì đây.

ON_UPDATE_COMMAND_UI có đang vô hiệu không
Command ID có trùng không
View active có đúng như kỳ vọng không
Handler nằm ở Frame/View/Document/App chỗ nào

MFC nối sự kiện bề mặt với xử lý thực tế bằng macro và routing, nên lúc chưa quen, vẽ đường gọi sẽ dễ hiểu hơn. Hình ở chương 9 là dạng cơ bản của đường gọi đó.

Nếu muốn chẩn từ triệu chứng, hãy xem bảng chương 35 trước. Bảng đó gắn các bẫy thường gặp với chỗ cần xác nhận từng cái.

35. Những bẫy thường gặp

Sắp các bẫy thường gặp khi bảo trì MFC.

Chỉ tìm chỗ gọi hàm, không nhìn message map
Tưởng CWnd* khác null là còn hiệu lực
Nhầm vòng đời HWND với vòng đời đối tượng C++
Nhầm chiều UpdateData(TRUE/FALSE)
Không nhận ra đang bị vô hiệu bởi ON_UPDATE_COMMAND_UI
Không nhận ra ID trong resource.h đụng nhau
Chạm UI trực tiếp từ worker thread
Chuyển CString và std::string bị lỗi font
Unicode hóa làm hỏng mã lấy MBCS làm tiền đề
Quên AFX_MANAGE_STATE trong MFC DLL
Đưa x64 làm hỏng COM/ActiveX lấy x86 làm tiền đề
Chọn/giải phóng đối tượng GDI sai
Layout tọa độ cố định vỡ ở high DPI

Với từng cái, hãy gắn “triệu chứng hay ra sao” với “nhìn chỗ nào để xác nhận”. Cửa vào debug tương ứng quy trình chương 34.

Bẫy Triệu chứng hay gặp Chỗ xác nhận
Chỉ tìm chỗ gọi hàm, không nhìn message map Không tìm thấy nơi gọi nhưng vẫn chạy Tìm BEGIN_MESSAGE_MAP. Chương 9, bước 4 chương 34
Tưởng CWnd* khác null là còn hiệu lực Thỉnh thoảng crash, thao tác màn hình đã đóng thì crash Đi qua ::IsWindow(pWnd->GetSafeHwnd()). Chương 8
Nhầm vòng đời HWND với vòng đời đối tượng C++ Access violation sau khi đóng hộp thoại Có đang giữ giá trị trả về của GetDlgItem không. Chương 22
Nhầm chiều UpdateData Giá trị nhập không được phản ánh, giá trị khởi tạo không ra màn hình Vị trí gọi UpdateData(TRUE)UpdateData(FALSE). Chương 13, bước 5 chương 34
Không nhận ra đang bị vô hiệu bởi ON_UPDATE_COMMAND_UI Nút hay menu xám, không bấm được Handler của ON_UPDATE_COMMAND_UI. Chương 11, mục “menu không bấm được” chương 34
Không nhận ra ID trong resource.h đụng nhau Hộp thoại hay mục menu khác phản ứng Đối chiếu resource.h.rc theo ID. Chương 16
Chạm UI trực tiếp từ worker thread Crash không ổn định, khó tái hiện, đôi khi hang Trong hàm đưa cho AfxBeginThread có chạm UI không. Chương 23
Chuyển CStringstd::string bị lỗi font Chỉ chữ Nhật bị hỏng, đuôi bị cắt Thiết lập character set của dự án và chỗ chuyển. Chương 18
Unicode hóa làm hỏng mã lấy MBCS làm tiền đề Lệch độ dài buffer, tệp hiện có không đọc được Có lấy sizeof làm số ký tự không. Chương 26
Quên AFX_MANAGE_STATE trong MFC DLL Không tìm thấy hộp thoại hay string resource trong DLL Đầu hàm export của DLL. Chương 24
Đưa x64 làm hỏng COM/ActiveX lấy x86 làm tiền đề Chỉ bản 64-bit thất bại lúc khởi động hay hiện màn hình Đăng ký COM có chỉ nằm phía 32-bit không. Chương 27
Chọn và giải phóng đối tượng GDI sai Chạy lâu thì vẽ bị vỡ Cột “GDI objects” của Task Manager có tăng mãi không. Chương 20
Layout tọa độ cố định vỡ ở high DPI Chỉ môi trường phóng 150% bị cắt chữ Thiết lập DPI trong manifest và vẽ tọa độ cố định. Chương 28

Lỗi MFC đôi khi không thấy được nếu chỉ nhìn cú pháp C++; cần nhìn cả Windows message, resource, handle, module, thiết lập runtime.

36. Phát triển mới có nên chọn MFC không

Chọn MFC cho phát triển hoàn toàn mới thì nên cân nhắc thận trọng.

Nếu có lý do chọn MFC, thì là những trường hợp như sau.

Cần liên kết chặt với mã MFC hiện có
Muốn tái sử dụng thành phần hay màn hình MFC hiện có
Cần điều khiển rất sát Win32/GDI/COM
Trong tổ chức đã có đủ kỹ năng bảo trì MFC
Đối tượng bị giới hạn ở desktop Windows
Theo kế hoạch di chuyển dài hạn, trước hết cần mở rộng bằng MFC

Ngược lại, những trường hợp sau thì nên xem lựa chọn khác.

Muốn làm UI hiện đại
Cần layout linh hoạt hay animation
Lấy liên kết Web hay cloud làm trung tâm
Muốn coi trọng khả năng kiểm thử
Muốn chọn công nghệ để lập trình viên trẻ tham gia dễ hơn
Cần hỗ trợ đa nền tảng
Muốn coi trọng accessibility và high DPI ngay từ đầu

MFC không phải công nghệ “từ giờ học chẳng đáng” cũng không phải công nghệ “mới nên chọn”. Đó là công nghệ để đối diện tài sản native Windows hiện có.

37. Cách nghĩ khi di chuyển khỏi MFC

Muốn đưa ứng dụng MFC sang công nghệ khác, nhắm ngay tới làm mới toàn bộ dễ thất bại.

Trước hết, hãy tách ứng dụng như sau rồi nghĩ.

UI
Business logic
Định dạng tệp
Xử lý truyền thông
Xử lý cơ sở dữ liệu
Điều khiển thiết bị
In ấn
Liên kết COM/OLE
Quản lý cấu hình
Log

Trong số đó, phần phụ thuộc MFC nhất là UI.

Ngược lại, business logic và xử lý tệp có thể tách được.

Thứ tự thực tế khi di chuyển như sau.

1. Tái hiện môi trường build
2. Cố định hành vi hiện có bằng dữ liệu kiểm thử
3. Tách logic ra khỏi UI event handler
4. Đưa sang thư viện C++ không MFC
5. Thêm kiểm thử tự động
6. Ghi lại đặc tả bên ngoài
7. Thay từng bước từ những màn hình cần thiết

Đặt mục tiêu “đưa logic quan trọng đang bị nhốt trong MFC ra ngoài” sẽ thành công hơn là đặt mục tiêu “bỏ MFC”.

38. Cửa vào khi đọc mã MFC

Lần đầu đọc dự án MFC hiện có, hãy bắt đầu từ những tệp khoảng như sau.

*.vcxproj
  Nhìn toolset, thiết lập MFC, character set, runtime

resource.h
  Nhìn resource ID

*.rc
  Nhìn hộp thoại, menu, chuỗi, icon

*App.cpp / *App.h
  Nhìn lớp dẫn xuất CWinApp và InitInstance

MainFrm.cpp / MainFrm.h
  Nhìn main frame và menu/toolbar

*Doc.cpp / *Doc.h
  Nếu Document/View thì nhìn cấu trúc dữ liệu và xử lý lưu

*View.cpp / *View.h
  Nhìn vẽ và thao tác người dùng

*Dlg.cpp / *Dlg.h
  Nhìn hộp thoại, DDX, xử lý nút

Tiếp theo là từ khóa tìm kiếm hay dùng.

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

Tìm những chỗ này thì hành vi ứng dụng hiện rõ hơn.

39. Chính sách thiết kế khi bảo trì MFC

Muốn bảo trì tài sản MFC hiện có lâu dài, những chính sách sau có hiệu quả.

Làm UI event handler mỏng
Đừng để CString hay CWnd rò quá nhiều sang tầng không UI
Chuyển business logic sang class C++ thông thường
Làm kiểm thử tương thích định dạng tệp
Làm rõ khác biệt x86/x64
Review thay đổi resource ID
Xác nhận module state của MFC DLL
Sắp log
Cố định build bằng CI
Định kỳ kiểm màn hình trên high DPI và Windows 11

Đặc biệt, đừng nhồi quá nhiều xử lý vào CDialog hay CView.

Class màn hình MFC hãy tập trung vào nhập, hiển thị, phân phối sự kiện.

Lấy giá trị từ màn hình
Đưa sang service
Phản ánh kết quả lên màn hình

Giữ được ở mức này thì dù là MFC cũng bảo trì được khá dễ.

40. Checklist thực tế

Checklist khi làm việc với ứng dụng MFC.

Phiên bản Visual Studio đã rõ chưa
Thành phần MFC/ATL đã cài chưa
Đích x86/x64 đã rõ chưa
Đã nắm thiết lập Unicode/MBCS chưa
MFC là link tĩnh hay shared DLL
Visual C++ Redistributable cần là cái nào
Đã đưa resource.h và .rc vào đối tượng review chưa
Đã lần đường sự kiện bằng message map chưa
Chiều UpdateData có đúng không
Có dùng cấu trúc Document/View không
Quản lý vòng đời hộp thoại modeless có an toàn không
Có chạm UI trực tiếp từ worker thread không
MFC DLL có chỗ cần AFX_MANAGE_STATE không
Đã kiểm màn hình trên môi trường high DPI chưa
Phụ thuộc COM/ActiveX đã hỗ trợ 32-bit/64-bit chưa
Log có được giữ không
Logic không UI kiểm thử được không

MFC lúc chưa quen trông đặc thù, nhưng biết chỗ cần nhìn thì đọc khá có quy luật.

41. Kết luận

MFC là framework lâu đời để xây ứng dụng desktop native Windows bằng C++. Nó không còn là dòng chính của phát triển mới, nhưng trong bảo trì tài sản hiện có, cập nhật môi trường build, thêm chức năng, di chuyển từng bước, hiện vẫn là công nghệ quan trọng.

Để hiểu MFC, những điểm sau đặc biệt quan trọng.

MFC là thứ làm Win32 API dễ dùng bằng C++
CWinApp quản lý toàn ứng dụng
Vòng đời CWnd và HWND không giống nhau
Message map gắn sự kiện với hàm
Document/View là cơ chế tách dữ liệu khỏi hiển thị
DDX/DDV dùng để đồng bộ giá trị hộp thoại và kiểm tra
Tệp resource và resource.h rất quan trọng
CString và TCHAR hãy hiểu cùng thiết lập bảng mã
MFC DLL thì chú ý module state
Ứng dụng MFC cũ đáng xem lại từ góc high DPI, x64, CI, kiểm thử

Khi làm việc với MFC, điều quan trọng là hiểu cấu trúc trước, hơn là kết luận “cũ nên xấu”. Ứng dụng MFC hiện có đôi khi chứa kiến thức nghiệp vụ nhiều năm, đặc tả từng khách hàng, liên kết thiết bị, tương thích tệp. Để giữ giá trị đó đồng thời làm dần cho dễ bảo trì, cách thực tế là hiểu cách viết MFC, rồi tách logic không UI, sắp build và kiểm thử.

Nếu cố gói lại một câu thì như sau.

Nền tảng để xử lý cơ chế ứng dụng native Windows bằng class C++ và framework.

Có góc nhìn đó, MFC không chỉ là công nghệ cũ, mà thành manh mối để đọc an toàn tài sản Windows hiện có.

Tài liệu tham khảo

Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.

Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.

Bài viết liên quan trực tiếp đến các dịch vụ sau.

Câu hỏi thường gặp

Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.

MFC là gì?
MFC là viết tắt của Microsoft Foundation Classes, một framework ứng dụng Windows giúp làm việc với Win32 API dưới dạng class C++ dễ dùng hơn. Cửa sổ được biểu diễn bằng CWnd, hộp thoại bằng CDialog, toàn bộ ứng dụng bằng CWinApp. Đây không phải "thư viện phép thuật để viết Windows mà không cần biết Windows", mà là cách tổ chức cơ chế của Windows thành kiểu C++ và framework, nên vẫn cần kiến thức Win32 như Windows message và handle.
Hiện nay còn dùng MFC được không? Hỗ trợ còn tiếp tục chứ?
MFC hiện vẫn dùng được trong Visual Studio và vẫn được hỗ trợ. Tuy nhiên tài liệu Microsoft ghi chú rằng sẽ không thêm tính năng mới hay cập nhật tài liệu. Về vị trí, bảo trì ứng dụng MFC hiện có, thêm chức năng, cập nhật môi trường build, và di chuyển từng bước sang UI khác vẫn rất thường gặp trong thực tế; còn chọn MFC cho một ứng dụng GUI hoàn toàn mới, thông thường, thì cần cân nhắc thận trọng. Bạn phải cài riêng thành phần Individual components của Visual Studio Installer (C++ MFC for latest build tools).
Message map trong MFC là gì?
Đó là cơ chế của MFC gắn Windows message và command với hàm handler. Giữa BEGIN_MESSAGE_MAP và END_MESSAGE_MAP, các macro như ON_BN_CLICKED hay ON_COMMAND mô tả tương ứng kiểu "khi nút này được bấm thì gọi hàm này". Nếu tìm hàm mà không thấy chỗ gọi trực tiếp, nhưng hàm vẫn chạy khi có sự kiện, hãy kiểm tra message map. Khi review mã MFC, quan trọng là xem hàm handler cùng với message map, không chỉ riêng hàm.
Muốn chuyển từ MFC sang công nghệ khác thì làm thế nào?
Nhắm ngay tới làm mới toàn bộ dễ thất bại. Thứ tự thực tế là: tái hiện môi trường build, cố định hành vi hiện có bằng dữ liệu kiểm thử, tách logic ra khỏi UI event handler rồi đưa sang thư viện C++ không MFC, thêm kiểm thử tự động, ghi lại đặc tả bên ngoài, rồi thay từng màn hình cần thiết. Đặt mục tiêu "đưa logic quan trọng đang bị nhốt trong MFC ra ngoài" sẽ thành công hơn là đặt mục tiêu "bỏ MFC".

Hồ sơ tác giả

Trang giới thiệu tác giả bài viết.

Go Komura

Đại diện của KomuraSoft LLC

Chuyên về phát triển phần mềm Windows, tư vấn kỹ thuật và điều tra lỗi, đặc biệt trong các dự án có hệ thống hiện hữu và lỗi khó tái hiện.

Liên kết công khai

Quay lại blog