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: · Go Komura · 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ị.
flowchart LR
accTitle: Bản đồ tri thức bảo trì ứng dụng desktop Windows bằng MFC
accDescr: Sơ đồ 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 COM
mfc["MFC (Microsoft Foundation Classes)"]
win32_api["Win32 API"]
cwinapp["CWinApp"]
message_map["Message Map"]
document_view_architecture["Kiến trúc Document/View"]
mfc_ddx_ddv["DDX/DDV"]
resource_script["Tệp resource (.rc/resource.h)"]
cstring_class["CString"]
vs_mfc_component["Thành phần MFC của Visual Studio Installer"]
mfc_shared_dll_linking["Cách liên kết MFC (Use of MFC)"]
mfc_module_state["Trạng thái module MFC"]
activex["ActiveX"]
bitness_match_requirement["Yêu cầu khớp bitness"]
high_dpi_support["Hỗ trợ DPI cao"]
new_gui_application_development["Phát triển ứng dụng GUI hoàn toàn mới"]
legacy_app_maintenance_and_migration["Bảo trì và chuyển dần ứng dụng MFC hiện có"]
mfc -->|"sử dụng"| win32_api
mfc -->|"sử dụng"| cwinapp
cwinapp -->|"tự động hóa"| win32_api
mfc -->|"triển khai"| message_map
message_map -->|"yêu cầu"| win32_api
mfc -->|"triển khai"| document_view_architecture
document_view_architecture -->|"yêu cầu"| cwinapp
message_map -.->|"sử dụng"| document_view_architecture
mfc -->|"triển khai"| mfc_ddx_ddv
mfc_ddx_ddv -->|"yêu cầu"| resource_script
mfc -->|"sử dụng"| cstring_class
mfc -->|"yêu cầu"| vs_mfc_component
mfc -->|"cấu hình bằng"| mfc_shared_dll_linking
mfc -.->|"yêu cầu"| mfc_module_state
mfc -.->|"sử dụng"| activex
mfc -.->|"yêu cầu"| bitness_match_requirement
mfc -.->|"yêu cầu"| high_dpi_support
mfc -->|"không khuyến nghị"| new_gui_application_development
mfc -->|"khuyến nghị cho"| legacy_app_maintenance_and_migration
mfc_module_state -.->|"yêu cầu"| resource_script
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 CWnd và HWND 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.
flowchart TB
accTitle: Luồng từ Windows message tới hàm handler
accDescr: Windows 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.
MSG["Windows message<br/>WM_COMMAND / WM_PAINT v.v."] --> LOOP["Message loop<br/>Framework MFC chạy vòng lặp"]
LOOP --> PROC["CWnd::WindowProc<br/>Cửa vào chung do MFC chuẩn bị"]
PROC --> MAP{"Có mục khớp trong message map<br/>của chính class này không?"}
MAP -->|có| HANDLER["Gọi hàm handler mà<br/>ON_BN_CLICKED v.v. trỏ tới"]
MAP -->|không| BASE["Tìm message map của class cơ sở<br/>Lần lượt từ CDialogEx tới CDialog, CWnd"]
BASE --> FOUND{"Có tìm thấy ở đâu đó không?"}
FOUND -->|tìm thấy| HANDLER
FOUND -->|không có đến cuối| DEF["DefWindowProc<br/>Giao 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_MAP và BEGIN_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, DDX và DDV 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.
flowchart TB
accTitle: Quan hệ giữa frame, view và document
accDescr: Document 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.
APP["Lớp dẫn xuất CWinApp<br/>Khởi động và kết thúc toàn ứng dụng"] --> TPL["Document template<br/>CSingleDocTemplate / CMultiDocTemplate"]
TPL --> FRAME["Frame window<br/>CFrameWnd / CMDIChildWnd"]
TPL --> DOC["Lớp dẫn xuất CDocument<br/>Dữ liệu và I/O tệp"]
TPL --> VIEW["Lớp dẫn xuất CView<br/>Vẽ và thao tác người dùng"]
FRAME --> VIEW
VIEW -->|Lấy bằng GetDocument| DOC
DOC -->|Thông báo bằng UpdateAllViews| VIEW
Đ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++, .rc và resource.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à CFile và CArchive.
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.
25. Link tĩnh MFC hay dùng shared DLL
Ứ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, PriceCalculator và ParsePrices 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) và 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 và .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 CString và std::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
- Tập mã tham chiếu, sắp từng đoạn mã của bài theo chương - komurasoft-blog-samples (GitHub)
- MFC Desktop Applications - Microsoft Learn
- MFC and ATL - Microsoft Learn
- Creating an MFC Application - Microsoft Learn
- CWinApp Class - Microsoft Learn
- CWinApp: The Application Class - Microsoft Learn
- Mapping Messages - Microsoft Learn
- Document-View Architecture - Microsoft Learn
- Dialog Data Exchange and Validation - Microsoft Learn
- MFC Library Versions - Microsoft Learn
- Redistribute the MFC Library - Microsoft Learn
- Visual Studio Build Tools workload and component IDs - Microsoft Learn
- High DPI Desktop Application Development on Windows - Microsoft Learn
Bài viết liên quan
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.
API thread pool Win32 — Đồng thời mà không tạo luồng, với CreateThreadpoolWork
Bạn đang rải các lời gọi CreateThread khắp mã native? Bài viết này giải thích API thread pool Win32 được thiết kế lại từ Vista — bốn đối ...
Named pipe trong thực tiễn — IPC chuẩn của Windows, từ thiết kế đến bảo mật
Hướng dẫn thực tiễn về named pipe, cơ chế giao tiếp giữa các tiến trình chuẩn của Windows. Bài viết sắp xếp, từ nguồn gốc, việc chọn giữa...
DllMain và Loader Lock — Lý do thật sự bạn bị bảo "đừng làm gì trong khởi tạo DLL"
Vì sao bạn không được gọi LoadLibrary hoặc đồng bộ với luồng khác từ DllMain. Dựa trên nguồn gốc, bài viết giải thích cách loader lock tu...
Thức tỉnh giả — Vì sao biến điều kiện thức "mà không được thông báo" và cách chờ đúng trên Windows
Lần chờ của biến điều kiện có thể trở về ngay cả khi không có thông báo nào tới (thức tỉnh giả). Bài viết này giải thích, từ triển khai W...
Cách Windows tương thích ứng dụng hoạt động — chế độ tương thích, shim và Compatibility Administrator
Vì sao tích chế độ tương thích lại làm ứng dụng cũ chạy được. Bài viết giải thích shim (hook API), các shim tiêu biểu, triển khai bằng Co...
Chủ đề liên quan
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.
Chủ đề kỹ thuật Windows
Cổng vào phát triển Windows, điều tra lỗi và khai thác tài sản hiện có.
Dịch vụ liên quan đến chủ đề này
Bài viết liên quan trực tiếp đến các dịch vụ sau.
Phát triển ứng dụng Windows
Ứng dụng nghiệp vụ, tích hợp thiết bị và công cụ liên lạc, từ yêu cầu đến phát triển.
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".