1. สิ่งที่ควรจับให้ได้ก่อน
ตอนบำรุงรักษาแอปเดสก์ท็อป Windows รุ่นเก่า คุณอาจเจอชื่อแบบนี้
CWinApp
CWnd
CDialog
CDialogEx
CFrameWnd
CDocument
CView
CString
CFile
CArchive
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_BN_CLICKED
DoDataExchange
UpdateData
ชื่อเหล่านี้โผล่บ่อยในเฟรมเวิร์กแอป Windows สำหรับ C++ ที่เรียกว่า MFC MFC ย่อมาจาก Microsoft Foundation Classes เป็นไลบรารีที่ทำให้ใช้ Win32 API เป็นคลาส C++ ได้ง่ายขึ้น
ในงานพัฒนาแอป Windows วันนี้มีทางเลือกหลายทาง เช่น WinUI, WPF, Windows Forms, Electron, Qt และเทคโนโลยีเว็บ จึงแทบไม่ค่อยมีใครตั้ง MFC เป็นตัวเลือกแรกของงานใหม่ แต่ก็ไม่ใช่เทคโนโลยีที่หายไปแล้ว แอปธุรกิจ เครื่องวัด ซอฟต์แวร์ควบคุม CAD/CAM เครื่องมือภายในองค์กร และแพ็กเกจซอฟต์แวร์ที่อยู่มานาน ยังมีโค้ดเบส MFC ที่ต้องบำรุงรักษาอยู่
มุมมองที่ควรมีไว้ก่อนเมื่อจะเข้าใจ MFC มีดังนี้
MFC คือแผนที่สำคัญสำหรับอ่านแอปเดสก์ท็อป Windows รุ่นเก่า
อย่ามองว่า MFC ซ่อน Win32 API แต่ให้คิดว่าห่อให้ใช้แบบ C++
ถ้าไม่รู้จักธรรมเนียมของ MFC จะอ่านพฤติกรรมผิดมากกว่าที่หน้าตาโค้ดบอก
คุณค่าอยู่ที่การบำรุงรักษา ยืดอายุ และย้ายสินทรัพย์เดิมทีละขั้น มากกว่าการเลือกใช้กับงานใหม่
บทความนี้จัดระเบียบภาพรวมของ MFC โครงสร้างแอป message map, Document/View, ไดอะล็อก, DDX/DDV, รีซอร์ส, การ build และจุดที่ต้องระวังตอนบำรุงรักษา
กลุ่มผู้อ่านที่สมมติไว้คือคนที่ เขียน C++ ได้ แต่ยังไม่เคยใช้ Win32 API หรือ MFC ความรู้พื้นฐานประมาณนี้ก็พอ
| ด้าน | ระดับที่คาดหวัง |
|---|---|
| C++ | อ่านคลาส การสืบทอด ฟังก์ชันเสมือน พอยน์เตอร์และเรฟเฟอเรนซ์ได้ |
| Win32 API | ไม่มีประสบการณ์ก็ได้ แค่เคยได้ยินคำว่า window กับ message |
| Visual Studio | เคยเปิดโซลูชันแล้ว build |
| COM / OLE | ไม่มีประสบการณ์ก็ได้ ค้นเพิ่มเมื่อถึงบทที่ 27 ก็พอ |
ท้ายบทที่ 2 มีรายการ “ความรู้ที่ต้องมีเพื่ออ่าน MFC” แต่ไม่ต้องรวบให้ครบก่อนอ่าน อ่านไปแล้วค่อยย้อนกลับเมื่อจำเป็นก็พอ
ลำดับการอ่านก็ไม่ต้องไล่จากบทที่ 1 ถึง 41 ทั้งเล่ม นี่คือเส้นทางสั้นสุดแยกตามเป้าหมาย
| เป้าหมาย | บทที่ควรอ่าน |
|---|---|
| อยากรู้แค่ว่า MFC คืออะไร และตอนนี้อยู่ตรงไหน | บทที่ 2–4, บทที่ 36 |
| อยากอ่านโค้ด MFC เดิมได้ | บทที่ 6 → บทที่ 9 → บทที่ 10 → บทที่ 14 → บทที่ 12 และ 13 → บทที่ 38 |
| อยากแก้ให้ build ผ่าน | บทที่ 5, 25, 32, 33 |
| อยากสอบสวนบั๊ก | บทที่ 34 → บทที่ 35 → บทที่ 8 และ 22 |
| อยากตัดสินนโยบายบำรุงรักษาหรือการย้าย | บทที่ 30, 31, 37, 39 |
สิ่งที่ควรจับก่อนคือสองอย่าง message map (บทที่ 9) กับ Document/View (บทที่ 14) ถ้าสองอย่างนี้เข้าใจ คุณจะอ่านได้ว่า “ทำไมฟังก์ชันที่หา caller ไม่เจอถึงรันได้” และ “ข้อมูลกับหน้าจอต่อกันอย่างไร” ถ้าข้ามสองบทนี้ คำอธิบายบทอื่นจะลอย
ชิ้นส่วนโค้ดในบทความนี้เผยแพร่บน GitHub เป็นชุดโค้ดอ้างอิงที่แยกไฟล์ตามบท
windows-mfc-overview - komurasoft-blog-samples (GitHub)
แผนที่ความรู้ของบทความนี้
MFC เป็นเฟรมเวิร์กที่ทำให้ใช้ Win32 API เป็นคลาส C++ ได้ง่ายขึ้น โดย CWinApp รับหน้าที่เริ่มต้นแอปทั้งก้อน message map ผูก Windows message เข้ากับฟังก์ชัน handler สถาปัตยกรรม Document/View แยกข้อมูลออกจากการแสดงผลของ UI และ DDX/DDV จับคู่คอนโทรลบนไดอะล็อกกับตัวแปรสมาชิกแล้วตรวจค่า การจะใช้ MFC ต้องติดตั้งคอมโพเนนต์ MFC แยกใน Visual Studio Installer และเลือกได้ด้วยว่าจะใช้แบบ shared DLL หรือ static link บทความจัดวางตำแหน่งว่าแอป GUI ใหม่ทั้งหมดแบบทั่วไปควรตัดสินใจเลือกใช้ MFC อย่างระมัดระวัง ในขณะที่การบำรุงรักษาแอป MFC เดิม การเพิ่มฟีเจอร์ และการย้ายทีละขั้น มักมีคุณค่าจริงในทางปฏิบัติ
flowchart LR
accTitle: แผนที่ความรู้การบำรุงรักษาแอปเดสก์ท็อป Windows ด้วย MFC
accDescr: แผนภาพที่แสดงว่า MFC ห่อ Win32 API เป็น C++ และอิมพลีเมนต์กลไกอย่าง message map กับ Document/View จัดวางตำแหน่งว่าเหมาะกับการบำรุงรักษาสินทรัพย์เดิมแต่ควรระมัดระวังกับการพัฒนา GUI ใหม่ และความสัมพันธ์ของจุดที่ต้องระวังเรื่องสภาพแวดล้อมการ build กับการทำงานร่วมกับ COM
mfc["MFC (Microsoft Foundation Classes)"]
win32_api["Win32 API"]
cwinapp["CWinApp"]
message_map["แมปข้อความ (message map)"]
document_view_architecture["สถาปัตยกรรม Document/View"]
mfc_ddx_ddv["DDX/DDV"]
resource_script["ไฟล์รีซอร์ส (.rc/resource.h)"]
cstring_class["CString"]
vs_mfc_component["คอมโพเนนต์ MFC ของ Visual Studio Installer"]
mfc_shared_dll_linking["วิธีลิงก์ MFC (Use of MFC)"]
mfc_module_state["สถานะโมดูล MFC"]
activex["ActiveX"]
bitness_match_requirement["ข้อกำหนดให้ bitness ตรงกัน"]
high_dpi_support["การรองรับ DPI สูง"]
new_gui_application_development["การพัฒนาแอป GUI ทั่วไปจากศูนย์"]
legacy_app_maintenance_and_migration["การบำรุงรักษาและโยกย้ายทีละขั้นของแอป MFC เดิม"]
mfc -->|"ใช้"| win32_api
mfc -->|"ใช้"| cwinapp
cwinapp -->|"ทำอัตโนมัติ"| win32_api
mfc -->|"อิมพลีเมนต์"| message_map
message_map -->|"ต้องมี"| win32_api
mfc -->|"อิมพลีเมนต์"| document_view_architecture
document_view_architecture -->|"ต้องมี"| cwinapp
message_map -.->|"ใช้"| document_view_architecture
mfc -->|"อิมพลีเมนต์"| mfc_ddx_ddv
mfc_ddx_ddv -->|"ต้องมี"| resource_script
mfc -->|"ใช้"| cstring_class
mfc -->|"ต้องมี"| vs_mfc_component
mfc -->|"กำหนดค่าด้วย"| mfc_shared_dll_linking
mfc -.->|"ต้องมี"| mfc_module_state
mfc -.->|"ใช้"| activex
mfc -.->|"ต้องมี"| bitness_match_requirement
mfc -.->|"ต้องมี"| high_dpi_support
mfc -->|"ไม่แนะนำให้ใช้กับ"| new_gui_application_development
mfc -->|"แนวทางที่แนะนำสำหรับ"| legacy_app_maintenance_and_migration
mfc_module_state -.->|"ต้องมี"| resource_script
ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 20 รายการ พร้อมหลักฐานและระดับความเชื่อมั่น) และนิยามของแนวคิดหลักรวบรวมไว้ที่ หน้าละเอียดของแผนที่ความรู้ (เป็นภาษาญี่ปุ่น) ข้อมูล: JSON-LD / Turtle
2. MFC คืออะไร
MFC คือไลบรารีคลาสสำหรับสร้างแอปเดสก์ท็อป native ของ Windows ด้วย C++
ถ้าใช้ Win32 API โดยตรง โดยทั่วไปจะเขียนโค้ดแบบนี้
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
// การวาด
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
Win32 API ทรงพลังมาก แต่ประกอบรอบฟังก์ชัน C, handle, message และ callback จึงมักมองภาพรวมยากเมื่อแอปใหญ่ MFC ทำให้จัดการสิ่งเหล่านี้เป็นคลาส C++ ได้ หน้าต่างคือ CWnd ไดอะล็อกคือ CDialog แอปทั้งก้อนคือ CWinApp หน้าต่างเฟรมคือ CFrameWnd วิวคือ CView
class CMainFrame : public CFrameWnd
{
public:
CMainFrame();
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
MFC ไม่ได้แทนที่ Win32 API ด้วยของคนละชุดทั้งหมด แต่เป็นเฟรมเวิร์ก C++ ที่บางแต่ครอบคลุมกว้าง วางบนแนวคิดของ Win32 API ดังนั้นการอ่าน MFC ต้องรู้ไม่ใช่แค่คลาสของ MFC แต่รวมถึงเรื่องพวกนี้ด้วย
Windows message
handle เช่น HWND
GDI/GDI+
ไฟล์รีซอร์ส
COM/OLE
DLL และรันไทม์
รหัสอักขระ
เธรดกับ message loop
ภาพจริงของ MFC จึงไม่ใช่ “ไลบรารีวิเศษที่เขียน Windows ได้โดยไม่ต้องรู้ Windows” แต่เป็น “กลไกของ Windows ที่ถูกจัดเป็นชนิดข้อมูลและเฟรมเวิร์ก C++”
3. ทุกวันนี้ยังใช้ MFC ได้หรือไม่
MFC ยังใช้ใน Visual Studio ได้ แต่ห้ามเข้าใจตำแหน่งผิด แม้ยังมีซัพพอร์ต มันไม่ใช่เฟรมเวิร์ก UI สมัยใหม่ที่ยังเพิ่มฟีเจอร์ใหม่อย่างคึกคัก ในเอกสาร MFC ของ Microsoft ก็มีหมายเหตุว่า MFC ยังซัพพอร์ตต่อไป แต่จะไม่มีการเพิ่มฟีเจอร์ใหม่หรืออัปเดตเอกสาร
ตำแหน่งของ MFC จึงประมาณนี้
บำรุงรักษาแอป MFC เดิม -> พบได้บ่อยในงานจริง
เพิ่มฟีเจอร์ให้แอป MFC เดิม -> เป็นไปได้
อัปเดตสภาพแวดล้อมการ build ของแอป MFC -> สำคัญ
ย้ายจาก MFC ไป UI อื่นทีละขั้น -> เป็นไปได้
เลือกใช้กับแอป GUI ใหม่ทั่วไป -> ตัดสินใจอย่างระมัดระวัง
โดยเฉพาะแอปธุรกิจที่ใช้มานาน UI การพิมพ์ ไฟล์ I/O การควบคุมอุปกรณ์ โปรโตคอลเฉพาะ และการเชื่อม COM มักมัดรวมอยู่ใน MFC
กับโค้ดเบสแบบนั้น สิ่งที่ต้องทำก่อน “ทิ้ง MFC” คือทำให้ “อ่าน MFC ได้”
4. ขอบเขตที่ MFC ถนัด
ขอบเขตคลาสสิกที่ MFC ถูกใช้คือแอปเดสก์ท็อป native ของ Windows
ตัวอย่างคือแอปแบบนี้
เครื่องมือธุรกิจที่ศูนย์กลางอยู่ที่ไดอะล็อก
แอป SDI ที่เปิดไฟล์แล้วแก้ไข
แอป MDI ที่จัดการหลายเอกสาร
หน้าจอควบคุมเครื่องวัดหรือเครื่องจักรผลิต
แอป native ฝั่ง CAD/CAM
แอปที่ใช้การพิมพ์และพรีวิวหนัก
แอปที่มี ActiveX หรือ OLE
แอปที่ผูกแน่นกับ Windows API หรือสินทรัพย์ COM รุ่นเก่า
จุดแข็งของ MFC คือทำงานใกล้ชิ้นส่วน native ของ Windows คุณจัดการหน้าต่าง เมนู แถบเครื่องมือ แถบสถานะ ไดอะล็อก common control การพิมพ์ ไดอะล็อกไฟล์ รีจิสทรี และการวาด GDI เป็นคลาส C++ ได้
จุดอ่อนคือการประกอบ UI สมัยใหม่ data binding ความง่ายในการทดสอบ งานอะซิงก์ เลย์เอาต์สมัยใหม่ การรองรับ high DPI การทำหลายภาษา และ accessibility เขียนได้ไม่เป็นธรรมชาติเท่าเฟรมเวิร์กรุ่นใหม่
จัดลักษณะได้แบบนี้
ใกล้ Windows native
ควบคุมตรง ๆ ด้วย C++ ได้
มีสินทรัพย์เดิมมาก
ต้องมีความรู้ Win32
มีธรรมเนียมเก่าจำนวนมาก
ต้องจัดโครงสร้างให้ทดสอบง่ายด้วยตัวเอง
5. เตรียมใช้ MFC ใน Visual Studio
แม้ติดตั้ง C++ ใน Visual Studio แล้ว MFC ก็ไม่ได้รับประกันว่าจะมากับเครื่อง เพราะ MFC ถูกจัดเป็น individual component ของ Visual Studio Installer โดยทั่วไปให้ตรวจคอมโพเนนต์พวกนี้
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
ว่าต้องการรุ่น Spectre Mitigations ของ MFC หรือไม่
ถ้าตอน build หาไฟล์ที่เกี่ยวกับ MFC ไม่เจอ นอกจากตั้งค่าโปรเจกต์ ให้ตรวจด้วยว่าฝั่ง Visual Studio มีคอมโพเนนต์ MFC หรือยัง
วิธีเลือกในตัวติดตั้งมีดังนี้
1. เปิด Visual Studio Installer จาก Start menu
2. กด Modify ของ Visual Studio ที่ติดตั้งไว้
3. ที่แท็บ Workloads ให้ติ๊ก Desktop development with C++
4. ที่ Installation details ด้านขวาของหน้าจอเดียวกัน
ให้ติ๊ก C++ MFC for latest v143 build tools
5. ถ้าไม่เจอ ให้สลับไปแท็บ Individual components
แล้วพิมพ์ MFC ในช่องค้นหา
6. กด Modify เพื่อติดตั้ง
การเลือก workload Desktop development with C++ อย่างเดียวบางครั้งยังไม่ติดตั้ง MFC ให้ ดังนั้นต้องยืนยันในขั้นที่ 4 หรือ 5 ว่าติ๊กตัวคอมโพเนนต์เองแล้ว
ถ้าติดตั้งจากสคริปต์หรือ CI ให้ระบุ component ID
| ชื่อที่แสดง | 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 ของ Build Tools) | Microsoft.VisualStudio.Workload.VCTools |
ตรวจว่ามีหรือไม่ได้จากโฟลเดอร์ atlmfc ที่วางเฮดเดอร์ของ MFC เฮดเดอร์และไลบรารีของ MFC อยู่ใต้ MSVC toolset
<ที่ติดตั้ง Visual Studio>\VC\Tools\MSVC\<เวอร์ชัน toolset>\atlmfc\include\afxwin.h
ถ้าค้นด้วย PowerShell ใช้แบบนี้
Get-ChildItem -Path "$env:ProgramFiles\Microsoft Visual Studio\2022" -Recurse -Filter afxwin.h -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty FullName
ถ้าไม่พิมพ์อะไร แปลว่ายังไม่มีคอมโพเนนต์ MFC
ถ้า build ในสภาพนี้ จะสะดุดตอน include afxwin.h ข้อผิดพลาดคอมไพล์แบบฉบับคือ
fatal error C1083: Cannot open include file: 'afxwin.h': No such file or directory
การอ่านข้อความนี้ว่า “ตั้ง include path ผิด” แล้วไล่หาในคุณสมบัติโปรเจกต์เป็นทางอ้อมที่พบบ่อย เมื่อหา afxwin.h หรือ afxdialogex.h ไม่เจอ ให้สงสัย Visual Studio Installer ก่อน
สภาพแวดล้อม CI หรือเซิร์ฟเวอร์ build ก็เหมือนกัน ถ้าเครื่องท้องถิ่น build ได้แต่ CI ล้มเหลว สาเหตุอาจเป็นว่าไม่มีคอมโพเนนต์ MFC หรือเวอร์ชัน target toolset ไม่ตรง
6. โครงสร้างพื้นฐานของแอป MFC
แอป MFC โดยประมาณมีโครงสร้างแบบนี้
คลาสลูกของ CWinApp
รับผิดชอบการเริ่มต้นและจบแอปทั้งก้อน
คลาสลูกของ CFrameWnd / CMDIFrameWnd / CDialog
รับผิดชอบหน้าต่างหลักหรือไดอะล็อก
คลาสลูกของ CView
รับผิดชอบการแสดงผลและการทำงานของผู้ใช้
คลาสลูกของ CDocument
รับผิดชอบข้อมูลและการบันทึกไฟล์
ไฟล์รีซอร์ส
เก็บเมนู ไดอะล็อก ไอคอน สตริง ฯลฯ
message map
ผูก Windows message และคำสั่งเข้ากับฟังก์ชัน handler
ตัวอย่างแอป MFC แบบง่ายจะมีคลาสลูกของ CWinApp แบบนี้
class CMyApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
CMyApp theApp;
BOOL CMyApp::InitInstance()
{
CWinApp::InitInstance();
CMainFrame* pFrame = new CMainFrame;
m_pMainWnd = pFrame;
pFrame->Create(nullptr, _T("My MFC Application"));
pFrame->ShowWindow(SW_SHOW);
pFrame->UpdateWindow();
return TRUE;
}
CWinApp คือคลาสที่แทนแอปทั้งก้อน ในแอป MFC โดยปกติมีอ็อบเจ็กต์ที่สืบทอดจาก CWinApp อยู่หนึ่งตัว
อ็อบเจ็กต์โกลบอลอย่าง theApp นี้อาจดูแปลกตอนแรก แต่ใน MFC นี่คือโครงสร้างมาตรฐาน
7. CWinApp ทำอะไร
CWinApp สำคัญในฐานะทางเข้าของแอป MFC ในแอป Win32 ปกติคุณเขียน WinMain การลงทะเบียน window class และ message loop เอง แต่ใน MFC เฟรมเวิร์กรับงานพวกนี้ไปเป็นส่วนใหญ่ ผู้พัฒนาจะ override InitInstance เป็นหลัก แล้วเขียนการเริ่มต้นเฉพาะแอป
BOOL CMyApp::InitInstance()
{
CWinApp::InitInstance();
// โหลดการตั้งค่า
// เริ่มต้น COM
// สร้างหน้าต่างหลัก
// ลงทะเบียน document template
return TRUE;
}
สิ่งที่มักถูกเขียนใน InitInstance คืองานแบบนี้
เริ่มต้น common control
ตั้งค่าคีย์รีจิสทรี
โหลดรายการไฟล์ที่ใช้ล่าสุด
ลงทะเบียน document template
สร้างเฟรมหลัก
ประมวลผลอาร์กิวเมนต์บรรทัดคำสั่ง
เริ่มต้น COM/OLE
ตอนบำรุงรักษา ถ้าเริ่มจากคลาสลูกของ CWinApp จะเห็นลำดับการสตาร์ตของแอปทั้งก้อนได้ง่ายขึ้น
8. CWnd คือคลาสที่เป็นศูนย์กลางของ MFC
คลาส UI ของ MFC ส่วนใหญ่มีฐานที่ CWnd ซึ่งแทนหน้าต่างของ Windows แต่ CWnd กับ HWND ไม่ใช่ของเดียวกัน
HWND
window handle ที่ Windows OS จัดการ
CWnd
อ็อบเจ็กต์ wrapper ฝั่ง C++ ที่ทำให้ใช้ HWND ง่ายขึ้น
ใน MFC CWnd ถือ HWND ไว้ภายใน
HWND hWnd = m_hWnd;
หรือดึงแบบนี้
HWND hWnd = GetSafeHwnd();
จุดสำคัญตอนบำรุงรักษาคือ แม้มี CWnd* อยู่ HWND ที่คู่กันอาจถูกทำลายไปแล้ว
ดังนั้นการตรวจว่าหน้าต่างยังใช้ได้ มักเขียนแบบนี้
if (pWnd != nullptr && ::IsWindow(pWnd->GetSafeHwnd()))
{
pWnd->ShowWindow(SW_SHOW);
}
ตอนสอบสวนบั๊ก MFC สิ่งที่สำคัญคือดูว่าอายุขัยของอ็อบเจ็กต์ C++ ฝั่ง CWnd กับอายุขัยของ window handle จริงของ Windows ไม่ได้คลาดกัน
9. message map คืออะไร
กลไกที่ “ความเป็น MFC” ออกชัดที่สุดอย่างหนึ่งคือ message map
แอป Windows รับการคลิกเมาส์ การกดคีย์ การวาดใหม่ การเปลี่ยนขนาดหน้าต่าง และการเลือกเมนู ในรูป Windows message
ใน Win32 API โดยปกติประมวลผล message ด้วย switch ใน WndProc
ใน MFC คุณผูกสิ่งนั้นเข้ากับฟังก์ชัน handler ผ่าน 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"));
}
ความหมายของโค้ดนี้คือ
เมื่อกดปุ่ม IDC_BUTTON_OK
ให้เรียก CMyDialog::OnClickedButtonOk
เส้นทางจากตอนที่ message มาถึงจนเรียก handler วาดเป็นรูปได้แบบนี้
flowchart TB
accTitle: เส้นทางจาก Windows message ถึง handler ใน MFC
accDescr: แสดงว่า message loop ของ MFC ส่ง Windows message ไป CWnd::WindowProc แล้วไล่ message map จากคลาสตัวเองไปคลาสฐาน จนพบ handler หรือส่งต่อให้ DefWindowProc
MSG["Windows message<br/>เช่น WM_COMMAND / WM_PAINT"] --> LOOP["message loop<br/>ที่เฟรมเวิร์ก MFC หมุนให้"]
LOOP --> PROC["CWnd::WindowProc<br/>จุดรับร่วมที่ MFC จัดไว้"]
PROC --> MAP{"ใน message map ของคลาสตัวเอง<br/>มีรายการที่ตรงหรือไม่"}
MAP -->|มี| HANDLER["เรียก handler ที่<br/>ON_BN_CLICKED ฯลฯ ชี้ไว้"]
MAP -->|ไม่มี| BASE["ค้น message map ของคลาสฐาน<br/>ไล่จาก CDialogEx ไป CDialog, CWnd"]
BASE --> FOUND{"พบที่ใดที่หนึ่งหรือไม่"}
FOUND -->|พบ| HANDLER
FOUND -->|ไม่มีจนสุด| DEF["DefWindowProc<br/>ปล่อยให้ Windows ประมวลผลตามค่าเริ่มต้น"]
ที่คุณไม่เห็น switch เพราะเฟรมเวิร์กรับช่วงส่วน “ไล่ message map จากคลาสตัวเองไปคลาสฐาน” แทน DECLARE_MESSAGE_MAP กับ BEGIN_MESSAGE_MAP คือแมโครที่เตรียมตารางจับคู่ต่อคลาสเพื่อการค้นนี้
คนที่ยังไม่คุ้น MFC มักงงว่าฟังก์ชันถูกเรียกจากไหน ถ้าค้นแล้วไม่พบการเรียกตรง ๆ ให้ดูที่ message map
ฟังก์ชันไม่ถูกเรียกตรง ๆ
แต่รันตอนมีอีเวนต์
-> ตรวจแมโคร BEGIN_MESSAGE_MAP / ON_...
ตอนรีวิวโค้ด MFC สิ่งสำคัญคืออย่าดูแค่ฟังก์ชัน handler แต่ให้ดูคู่กับ message map
10. Command routing
ใน MFC การทำงานของเมนูและแถบเครื่องมือก็ถูกจัดการเป็นคำสั่งเช่นกัน
ตัวแทนคือ ON_COMMAND
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
END_MESSAGE_MAP()
void CMainFrame::OnFileOpen()
{
// ประมวลผลเปิดไฟล์
}
MFC มีกลไกส่งคำสั่งไปยังอ็อบเจ็กต์ที่เหมาะสม
ตัวอย่างเช่น แม้เป็น ID_EDIT_COPY เดียวกัน ว่าวิวที่แอ็กทีฟอยู่ เอกสาร เฟรม หรือแอปตัวไหนจะประมวลผล อาจเปลี่ยนได้
วิวที่แอ็กทีฟ
เอกสาร
หน้าต่างเฟรม
แอปพลิเคชัน
คำสั่งถูกส่งตามลำดับนี้ไปยังอ็อบเจ็กต์ที่ประมวลผลได้
ดังนั้นใน MFC การตามว่า “กดเมนูแล้วฟังก์ชันไหนถูกเรียก” ด้วยการค้นสตริงอย่างเดียวอาจยาก
จุดที่ดูตอนบำรุงรักษามีประมาณนี้
command ID คืออะไร
ON_COMMAND อยู่ที่คลาสไหน
ON_UPDATE_COMMAND_UI อยู่ที่ไหน
วิวที่แอ็กทีฟอยู่ตอนนี้คืออันไหน
ใช้โครงสร้าง Document/View หรือไม่
11. ON_UPDATE_COMMAND_UI คืออะไร
ใน MFC การอัปเดตว่าเมนูหรือปุ่มแถบเครื่องมือจะเปิด/ปิด สถานะเช็ก และข้อความที่แสดง มักใช้ ON_UPDATE_COMMAND_UI
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_EDIT_DELETE, &CMainFrame::OnEditDelete)
ON_UPDATE_COMMAND_UI(ID_EDIT_DELETE, &CMainFrame::OnUpdateEditDelete)
END_MESSAGE_MAP()
void CMainFrame::OnUpdateEditDelete(CCmdUI* pCmdUI)
{
pCmdUI->Enable(CanDeleteCurrentItem());
}
ด้วยวิธีนี้ เมนูหรือปุ่มจะเปิดได้เฉพาะตอนที่ลบได้
ตอนใช้แอป MFC แล้วปุ่มเทาโดยไม่ทราบสาเหตุ กดเมนูไม่ได้ หรือสถานะเช็กเปลี่ยน การค้น ON_UPDATE_COMMAND_UI มักเจอต้นเหตุ
12. แอป MFC แบบ dialog-based
รูปแบบที่เข้าใจง่ายที่สุดอย่างหนึ่งของ MFC คือแอปแบบ dialog-based
หน้าจอตั้งค่า เครื่องมือธุรกิจเล็ก ๆ และหน้าจอควบคุมอุปกรณ์ มักประกอบรอบไดอะล็อก
โดยทั่วไปจะสืบทอด CDialog หรือ CDialogEx
class CSettingsDialog : public CDialogEx
{
public:
CSettingsDialog(CWnd* pParent = nullptr);
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_SETTINGS_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
afx_msg void OnBnClickedOk();
DECLARE_MESSAGE_MAP()
private:
CString m_name;
int m_interval;
};
ในโค้ดแบบ dialog-based มักเจอองค์ประกอบเหล่านี้
IDD_... ID ของไดอะล็อกรีซอร์ส
IDC_... ID ของคอนโทรล
OnInitDialog การเริ่มต้น
DoDataExchange การผูกคอนโทรลกับตัวแปรสมาชิก
UpdateData การซิงก์หน้าจอกับตัวแปร
ON_BN_CLICKED การประมวลผลคลิกปุ่ม
ไดอะล็อกไม่ได้ทำงานแค่หน้าตา แต่เป็นการรวมรีซอร์ส ตัวแปรสมาชิก message map และการเริ่มต้นเข้าด้วยกัน
13. DDX และ DDV
สิ่งที่โผล่บ่อยในไดอะล็อกของ MFC คือ DDX กับ DDV
DDX = Dialog Data Exchange
DDV = Dialog Data Validation
DDX คือกลไกจับคู่คอนโทรลบนไดอะล็อกกับตัวแปรสมาชิก C++ ส่วน DDV คือกลไกตรวจค่าที่กรอก
void CSettingsDialog::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_NAME, m_name);
DDX_Text(pDX, IDC_EDIT_INTERVAL, m_interval);
DDV_MinMaxInt(pDX, m_interval, 1, 3600);
}
เมื่อเรียก UpdateData(TRUE) ค่าที่กรอกบนหน้าจอจะถูกคัดลอกไปยังตัวแปรสมาชิก
void CSettingsDialog::OnBnClickedOk()
{
if (!UpdateData(TRUE))
{
return;
}
// ตรงนี้ m_name และ m_interval มีค่าที่กรอกบนหน้าจอแล้ว
SaveSettings(m_name, m_interval);
CDialogEx::OnOK();
}
กลับกัน เมื่อเรียก UpdateData(FALSE) ค่าในตัวแปรสมาชิกจะถูกคัดลอกไปยังหน้าจอ
BOOL CSettingsDialog::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_name = _T("default");
m_interval = 60;
UpdateData(FALSE);
return TRUE;
}
ถ้าค่าที่กรอกในไดอะล็อก MFC ผิดปกติ ให้ตรวจจุดเหล่านี้
มี DDX นิยามใน DoDataExchange หรือไม่
เรียก UpdateData(TRUE) หรือไม่
จังหวะเรียก UpdateData(FALSE) ถูกต้องหรือไม่
DDV ดีดค่าที่กรอกทิ้งหรือไม่
control ID ตรงกับรีซอร์สหรือไม่
14. สถาปัตยกรรม Document/View
ลักษณะใหญ่ของ MFC อย่างหนึ่งคือ สถาปัตยกรรม Document/View
นี่คือโครงสร้างที่แยกข้อมูลที่แอปจัดการ ออกจากการแสดงผล
CDocument
เก็บข้อมูล
รับผิดชอบการอ่านเขียนไฟล์
แจ้งอัปเดตไปยังหลายวิว
CView
แสดงข้อมูล
จัดการการทำงานของผู้ใช้
จัดการการวาดและสถานะการเลือก
ความสัมพันธ์ระหว่างเฟรม วิว และเอกสาร วาดเป็นรูปได้แบบนี้
flowchart TB
accTitle: ความสัมพันธ์ระหว่างเฟรม วิว และเอกสารใน MFC
accDescr: แสดงว่า document template ผูก CWinApp หน้าต่างเฟรม CDocument และ CView เข้าด้วยกัน และวิวกับเอกสารสื่อสารผ่าน GetDocument กับ UpdateAllViews
APP["คลาสลูกของ CWinApp<br/>เริ่มและจบแอปทั้งก้อน"] --> TPL["document template<br/>CSingleDocTemplate / CMultiDocTemplate"]
TPL --> FRAME["หน้าต่างเฟรม<br/>CFrameWnd / CMDIChildWnd"]
TPL --> DOC["คลาสลูกของ CDocument<br/>ข้อมูลและไฟล์ I/O"]
TPL --> VIEW["คลาสลูกของ CView<br/>การวาดและการทำงานของผู้ใช้"]
FRAME --> VIEW
VIEW -->|ดึงด้วย GetDocument| DOC
DOC -->|แจ้งด้วย UpdateAllViews| VIEW
จุดสำคัญคือสิ่งที่ผูกเฟรม เอกสาร และวิวทั้งสามเข้าด้วยกันคือ document template เทมเพลตนี้ถูกลงทะเบียนใน InitInstance ดังนั้นถ้าอยากรู้ว่าวิวไหนมองเอกสารไหน ให้เริ่มอ่าน InitInstance
ทิศทางก็ควรจำไว้ วิวมองเอกสารด้วย GetDocument ส่วนเอกสารบอกทุกวิวเมื่อมีการเปลี่ยนด้วย UpdateAllViews ถ้าจำทิศนี้ได้ จะตามบั๊กที่หน้าจอไม่อัปเดตได้ง่ายขึ้น
ตัวอย่างเช่นในตัวแก้ไขข้อความ ตัวแก้ไขรูปทรง เครื่องมือแก้ไฟล์ตั้งค่า หรือแอปแบบ CAD การแยกข้อมูลกับการแสดงผลมีความหมาย
class CMyDocument : public CDocument
{
public:
std::vector<Item> m_items;
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
};
class CMyView : public CView
{
protected:
virtual void OnDraw(CDC* pDC);
CMyDocument* GetDocument() const;
};
ฝั่งวิวจะดึงเอกสารแล้ววาด
void CMyView::OnDraw(CDC* pDC)
{
CMyDocument* pDoc = GetDocument();
if (pDoc == nullptr)
{
return;
}
for (const auto& item : pDoc->m_items)
{
// วาดด้วย pDC
}
}
ข้อดีของ Document/View คือแสดงข้อมูลชุดเดียวกันด้วยหลายวิวได้ง่าย
ตัวอย่างเช่น ข้อมูลชุดเดียวกันอาจแสดงแบบนี้
วิวตาราง
วิวกราฟ
วิวรายละเอียด
พรีวิว
วิวพิมพ์
แต่ถ้าเอา Document/View ไปใช้กับหน้าจอตั้งค่าธรรมดาหรือเครื่องมือเล็ก ๆ โครงสร้างอาจรู้สึกหนักเกินไป
ตอนบำรุงรักษา ถ้าแยกให้ได้ตั้งแต่ต้นว่าแอปนี้ใช้ Document/View หรือศูนย์กลางอยู่ที่ไดอะล็อก จะตามโค้ดง่ายขึ้น
15. SDI และ MDI
ใน MFC โครงสร้าง SDI และ MDI มักโผล่คู่กับ Document/View
SDI = Single Document Interface
MDI = Multiple Document Interface
SDI โดยพื้นฐานคือหนึ่งเฟรมจัดการหนึ่งเอกสาร
หน้าต่างหลัก
หนึ่งเอกสาร
หนึ่งหรือหลายวิว
MDI มีหลายหน้าต่างลูกในหน้าต่างแม่หนึ่งอัน และแต่ละลูกจัดการเอกสารของตัวเอง
เฟรมแม่ MDI
เฟรมลูก MDI 1 -> เอกสาร 1
เฟรมลูก MDI 2 -> เอกสาร 2
เฟรมลูก MDI 3 -> เอกสาร 3
ในแอป Windows รุ่นเก่า MDI ถูกใช้บ่อย
ตอนบำรุงรักษา ดูชื่อคลาสก็พอเดาโครงสร้างได้
CFrameWnd เฟรมสาย SDI
CMDIFrameWnd เฟรมแม่ MDI
CMDIChildWnd เฟรมลูก MDI
CSingleDocTemplate document template สำหรับ SDI
CMultiDocTemplate document template สำหรับ MDI
ในแอปที่ตัวช่วยของ MFC สร้างให้ มักมีโค้ดลงทะเบียน CSingleDocTemplate หรือ CMultiDocTemplate ใน InitInstance
16. ทำความเข้าใจไฟล์รีซอร์ส
ในแอป MFC ไฟล์ .rc สำคัญมาก
.rc คือไฟล์รีซอร์สของ Windows
ที่นี่นิยามสิ่งพวกนี้
เทมเพลตไดอะล็อก
เมนู
คีย์เร่ง (accelerator)
ไอคอน
บิตแมป
ตารางสตริง
ข้อมูลเวอร์ชัน
แถบเครื่องมือ
และใน resource.h จะนิยาม resource ID
#define IDD_SETTINGS_DIALOG 101
#define IDC_EDIT_NAME 1001
#define IDC_EDIT_INTERVAL 1002
#define ID_FILE_OPEN 32771
โค้ด MFC ใช้ ID เหล่านี้ผูกรีซอร์สกับโค้ด C++
DDX_Text(pDX, IDC_EDIT_NAME, m_name);
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
ปัญหาที่พบบ่อยตอนบำรุงรักษาคือ resource ID ไม่ตรงกัน
ID ใน resource.h เปลี่ยน
ID ชนกันตอน merge จากอีกสาขา
control ID บนไดอะล็อกไม่ตรงกับ ID ของ DDX
menu ID ที่ควรลบแล้วยังค้างอยู่
ID ในตารางสตริงซ้ำกัน
ตอนสอบสวนพฤติกรรมของแอป MFC ต้องดูไม่ใช่แค่โค้ด C++ แต่ดู .rc กับ resource.h พร้อมกันด้วย
17. Class Wizard กับโค้ดที่เขียนเอง
MFC มีประวัติผูกกับ Class Wizard ของ Visual Studio อย่างลึก
Class Wizard สร้าง message handler, ตัวแปร DDX และการ override ฟังก์ชันเสมือนให้อัตโนมัติได้
ดังนั้นในโค้ด MFC มักเหลือรูปที่เครื่องมือสร้างไว้
//{{AFX_DATA(CSettingsDialog)
//}}AFX_DATA
//{{AFX_MSG(CSettingsDialog)
//}}AFX_MSG
Visual Studio รุ่นใหม่หน้าตาและการสร้างอาจเปลี่ยนไป แต่ในโค้ดเบสรุ่นเก่ามักยังเหลือเครื่องหมายคอมเมนต์แบบนี้
จุดสำคัญตอนบำรุงรักษาคืออย่าทำลายเส้นแบ่งระหว่างโค้ดที่ถูกสร้างกับโค้ดที่เขียนเองอย่างเลินเล่อ
อย่าลบ message map
อย่าทำลายการจับคู่ DDX
อย่าเปลี่ยน resource ID โดยไม่จำเป็น
อย่าลบคอมเมนต์ที่สมมติ Class Wizard รุ่นเก่าทิ้งมั่ว ๆ
ใน MFC การคอมไพล์ผ่านในฐานะ C++ อย่างเดียวไม่พอ ต้องรักษาโครงที่ตัวแก้ไขรีซอร์สและ Class Wizard ของ Visual Studio คาดไว้ในระดับหนึ่งด้วย
18. CString และสตริง
คลาสสตริงที่โผล่บ่อยใน MFC คือ CString
CString name = _T("Komura");
CString message;
message.Format(_T("Hello, %s"), name.GetString());
CString คือคลาสสตริงความยาวแปรได้ที่ใช้บ่อยในโค้ดสาย MFC/ATL ใน C++ สมัยนี้มักใช้ std::string หรือ std::wstring แต่ใน MFC มักใช้ CString เพราะเข้ากันได้กับ API และคอนโทรล
ตอนบำรุงรักษาต้องคำนึงถึงรหัสอักขระ
CString เทียบเท่า CStringA หรือ CStringW ตามการตั้งค่าโปรเจกต์
CStringA สาย ANSI / MBCS
CStringW สาย Unicode / UTF-16
LPCTSTR พอยน์เตอร์สตริงฐาน TCHAR
LPCSTR สาย char
LPCWSTR สาย wchar_t
std::string โดยปกติสาย char
std::wstring สาย wchar_t
แอป Windows ปัจจุบันโดยพื้นฐานปลอดภัยกว่าถ้าสมมติ Unicode แต่แอป MFC รุ่นเก่าอาจยังมีโค้ดที่สมมติ MBCS เหลืออยู่
CString text = _T("日本語");
std::wstring ws(text.GetString());
บั๊กแปลงสตริงพบบ่อยตอนบำรุงรักษาแอป MFC
เคสที่ควรระวังเป็นพิเศษคือ
อ่านไฟล์ที่สมมติ Shift_JIS
สลับไป Unicode build
DLL ภายนอกขอ char*
COM ขอ BSTR
แปลงไป std::string อย่างง่ายแล้วตัวอักษรเพี้ยน
เมื่อเห็น CString อย่าคิดแค่ว่า “คลาสสตริงเก่า” แต่ให้ตรวจคู่กับการตั้งค่า character set ของโปรเจกต์, API ภายนอก และรูปแบบไฟล์
19. CFile และ CArchive
MFC มีคลาสสำหรับงานไฟล์และซีเรียลไลซ์ด้วย ตัวแทนคือ CFile กับ CArchive
CFile file;
if (file.Open(path, CFile::modeRead))
{
CArchive ar(&file, CArchive::load);
// อ่านจาก ar
}
CArchive ถูกใช้บ่อยในกลไกซีเรียลไลซ์ของ MFC
ในคลาสลูกของ CDocument มัก override Serialize แล้วเขียนทั้งการอ่านและการบันทึกในฟังก์ชันเดียวกัน
void CMyDocument::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_title;
ar << static_cast<int>(m_items.size());
for (const auto& item : m_items)
{
ar << item.Name;
ar << item.Value;
}
}
else
{
int count = 0;
ar >> m_title;
ar >> count;
m_items.clear();
for (int i = 0; i < count; ++i)
{
Item item;
ar >> item.Name;
ar >> item.Value;
m_items.push_back(item);
}
}
}
ซีเรียลไลซ์ของ MFC สะดวก แต่ตอนใช้งานระยะยาวต้องระวัง
ความเข้ากันได้กับรูปแบบไฟล์เก่า
การจัดการหมายเลขเวอร์ชัน
การกู้เมื่ออ่านล้มเหลว
exception handling
รหัสอักขระ
endian
ว่าได้เซฟ struct ตรง ๆ หรือไม่
ในแอป MFC ที่ใช้รูปแบบไบนารีของตัวเองมานาน Serialize อาจกลายเป็นสเปกไฟล์จริง
ในกรณีนี้ ก่อนเปลี่ยนโค้ด ควรเตรียมข้อมูลทดสอบที่อ่านไฟล์เดิมได้เสมอ
20. การวาดด้วย GDI และ CDC
ตอนวาดหน้าจอใน MFC มักใช้คลาส CDC สำหรับ Device Context ของ Windows ใน CView::OnDraw จะได้รับ CDC* เป็นอาร์กิวเมนต์
void CMyView::OnDraw(CDC* pDC)
{
pDC->TextOut(10, 10, _T("Hello MFC"));
pDC->Rectangle(10, 40, 200, 120);
}
ถ้าใช้ปากกาหรือแปรง ให้ระวังการเลือกและการคืนค่า
void CMyView::OnDraw(CDC* pDC)
{
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
CPen* pOldPen = pDC->SelectObject(&pen);
pDC->MoveTo(10, 10);
pDC->LineTo(100, 100);
pDC->SelectObject(pOldPen);
}
รอบ GDI object ความผิดแบบนี้กลายเป็นปัญหา
SelectObject แล้วไม่คืนของเดิม
สร้าง GDI object จำนวนมากแล้วไม่ทำลาย
ปนบทบาทของ OnPaint กับ OnDraw
ไม่ได้ double buffer จึงกะพริบ
วาดแบบสมมติพิกเซลคงที่แล้วพังที่ high DPI
บั๊กการวาดของ MFC ต้องดูไม่ใช่แค่โลจิก C++ แต่รวมถึงรีซอร์ส GDI ของ Windows จังหวะการวาดใหม่ DPI และขนาดฟอนต์
21. Modal dialog กับ modeless dialog
ใน MFC วิธีเปิดไดอะล็อกก็ต้องระวัง
modal dialog แสดงด้วย DoModal
CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
// ประมวลผลเมื่อกด OK
}
ในกรณีนี้ ผู้เรียกรอจนกว่าไดอะล็อกจะปิด
ส่วน modeless dialog หลังจากสร้างแล้ว การประมวลผลของผู้เรียกกลับมาต่อได้
m_pToolDialog = new CToolDialog(this);
m_pToolDialog->Create(IDD_TOOL_DIALOG, this);
m_pToolDialog->ShowWindow(SW_SHOW);
กับ modeless dialog การจัดการอายุขัยสำคัญ
จะ delete ไดอะล็อกที่ new เมื่อไร
หน้าต่างแม่จะถูกทำลายก่อนหรือไม่
ฝั่งไดอะล็อกใช้ PostNcDestroy หรือไม่
ถูกสร้างซ้ำซ้อนหรือไม่
พอยน์เตอร์หลังปิดยังค้างอยู่หรือไม่
ตอนสอบสวนแครชของ MFC ปัญหาอายุขัยของ modeless dialog เป็นต้นเหตุได้
22. อายุขัยของอ็อบเจ็กต์ C++ กับ Windows handle
สิ่งที่สำคัญมากใน MFC คืออายุขัยของอ็อบเจ็กต์ C++ กับ Windows handle ไม่ใช่เรื่องเดียวกัน ตัวอย่างเช่น CWnd คืออ็อบเจ็กต์ C++ แต่หน้าต่างจริง Windows จัดการเป็น HWND และทั้งสองไม่ได้ถูกสร้างและหายไปพร้อมกันเสมอ
มีอ็อบเจ็กต์ CWnd แต่ยังไม่มี HWND
HWND ถูกทำลายแล้ว แต่อ็อบเจ็กต์ CWnd ยังอยู่
มีการสร้าง CWnd wrapper ชั่วคราว
มีการสลับ handle ด้วย Attach/Detach
ตัวอย่างโค้ดแบบนี้ต้องระวัง
CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// เก็บ pWnd เป็นสมาชิกแล้วใช้ทีหลัง
ถ้าถือพอยน์เตอร์จาก GetDlgItem ไว้นาน มีความเสี่ยงไปอ้างอิงหลังหน้าต่างถูกทำลาย
ถ้าจำเป็น เรียก GetDlgItem ทุกครั้ง หรือจัดการตัวแปรสมาชิกของคอนโทรลด้วย DDX จะปลอดภัยกว่าในหลายกรณี
DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);
ใน MFC แม้พอยน์เตอร์ไม่เป็น null ก็ไม่ได้รับประกันว่าปลอดภัย
if (m_pDialog != nullptr && ::IsWindow(m_pDialog->GetSafeHwnd()))
{
m_pDialog->SetWindowText(_T("Running"));
}
ความรู้สึกนี้สำคัญมากตอนบำรุงรักษา MFC
23. เธรดกับการอัปเดต UI
UI ของ Windows โดยพื้นฐานต้องถูกจัดการบน UI thread ที่สร้างมัน และแอป MFC ก็เช่นกัน ถ้า worker thread ไปจับคอนโทรล UI โดยตรง จะทำให้พฤติกรรมไม่เสถียรหรือแครชได้
ตัวอย่างที่ควรเลี่ยง
UINT WorkerThreadProc(LPVOID pParam)
{
CMyDialog* pDlg = static_cast<CMyDialog*>(pParam);
// อย่าแตะ UI โดยตรงจาก worker thread
pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));
return 0;
}
โดยทั่วไปให้แจ้ง UI thread ด้วย PostMessage เป็นต้น
constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;
UINT WorkerThreadProc(LPVOID pParam)
{
HWND hWnd = static_cast<HWND>(pParam);
// งานหนัก
::PostMessage(hWnd, WM_APP_WORK_DONE, 0, 0);
return 0;
}
ฝั่ง UI รับด้วย 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;
}
รอบเธรดของ MFC ให้ตรวจสิ่งเหล่านี้
ไม่ได้แตะ UI โดยตรงจาก worker thread หรือไม่
ไม่ได้ PostMessage หลังหน้าต่างถูกทำลายหรือไม่
ไม่ได้หยุด UI thread เพื่อรอเธรดจบหรือไม่
ล็อกข้อมูลร่วมเหมาะสมหรือไม่
เข้าใจค่าที่คืนและอายุขัยของ AfxBeginThread ผิดหรือไม่
24. MFC DLL กับ module state
เมื่อสร้าง DLL ด้วย MFC จะเจอแนวคิด module state
ตอนโหลดรีซอร์สจาก MFC DLL แสดงไดอะล็อก หรือสร้าง extension DLL คำถามคือจะไปดูรีซอร์สของโมดูลไหน
ที่ทางเข้าฟังก์ชันของ MFC DLL มักเห็นแมโครแบบนี้
AFX_MANAGE_STATE(AfxGetStaticModuleState());
แมโครนี้ทำให้ MFC ใช้ module state ที่ถูกต้องได้
ถ้าลืมแมโครนี้ จะนำไปสู่ความผิดแบบนี้
หาไดอะล็อกรีซอร์สใน DLL ไม่เจอ
สตริงรีซอร์สถูกอ่านจากโมดูลอื่น
หาไอคอนหรือเมนูไม่เจอ
ดีบักแล้วรันได้ แต่รีลีสพัง
ตอนบำรุงรักษา MFC DLL สิ่งสำคัญคือจัดความสัมพันธ์ระหว่าง EXE, DLL ปกติ, extension DLL และ resource DLL
ต้องระวังเป็นพิเศษเมื่อแอปที่ไม่ใช่ MFC เรียก MFC DLL หรือเมื่อเป็นโครงแบบปลั๊กอิน
25. จะ static link MFC หรือใช้ shared DLL
ในแอป MFC มีการตั้งค่า “Use of MFC” ในโปรเจกต์
ตัวเลือกหลักมีสองแบบ
Use MFC in a Shared DLL
Use MFC in a Static Library
ถ้าใช้ shared DLL สภาพแวดล้อมรันต้องมี MFC runtime และ Visual C++ runtime ที่ตรงกัน
ถ้า static link สิ่งที่แจกอาจดูเรียบง่าย แต่ต้องคิดเรื่องขนาดไฟล์ปฏิบัติการ การอัปเดต การรับแพตช์ความปลอดภัย รวมถึงสัญญาอนุญาตและเงื่อนไขการแจกจ่ายซ้ำ
ไม่มีฝั่งไหนถูกเสมอ
ปัจจัยตัดสินใจมีประมาณนี้
ใส่ Visual C++ Redistributable ที่ปลายทางได้หรือไม่
อยากให้แอปเข้าใกล้ exe เดี่ยวหรือไม่
จะนำการอัปเดตความปลอดภัยมาใช้ได้อย่างไร
จะแชร์รันไทม์เดียวกันระหว่างหลายแอปหรือไม่
จัดตัวติดตั้งได้หรือไม่
เวอร์ชัน Windows เป้าหมายคืออะไร
ตอนบำรุงรักษา สิ่งสำคัญคือตรวจการตั้งค่าปัจจุบันก่อน
Configuration Properties
General
Use of MFC
นอกจากนี้ให้ดูการตั้งค่า Runtime Library ด้วย
/MD Multi-threaded DLL
/MDd Multi-threaded Debug DLL
/MT Multi-threaded
/MTd Multi-threaded Debug
ถ้าการลิงก์ MFC กับ CRT ปนกัน อาจเกิดปัญหาจอง/คืนหน่วยความจำที่ขอบไลบรารี
26. Unicode, MBCS, TCHAR
ในโค้ด MFC รุ่นเก่ามักเจอ TCHAR, LPCTSTR และแมโคร _T()
CString title = _T("設定");
SetWindowText(title);
นี่คือวิธีเขียนที่รองรับทั้ง Unicode build และ MBCS build
Unicode build
TCHAR -> wchar_t
LPCTSTR -> const wchar_t*
_T("...") -> L"..."
MBCS build
TCHAR -> char
LPCTSTR -> const char*
_T("...") -> "..."
ปัจจุบัน Unicode build เป็นเรื่องทั่วไป แต่แอปเก่าอาจยังมีโค้ดที่สมมติ MBCS เหลืออยู่
โดยเฉพาะไฟล์ภายนอก โปรโตคอลสื่อสาร DLL เก่า การเชื่อมฐานข้อมูล และการสื่อสารอนุกรม ต้องตรวจสมมติฐานเรื่องรหัสอักขระ
จุดที่ควรระวังคืออย่ามองว่าการทำ Unicode เป็นงานค้นแทนที่ธรรมดา
ขนาดอาร์เรย์ char นับเป็นไบต์หรือจำนวนตัวอักษร
ยังใช้ strlen อยู่หรือไม่
ใช้ sizeof(buffer) เป็นจำนวนตัวอักษรหรือไม่
API ภายนอกรับ UTF-16 หรือ Shift_JIS
รูปแบบการบันทึกไฟล์เปลี่ยนได้หรือไม่
ตอนแก้สตริงของ MFC อย่าดูแค่การแสดงบนหน้าจอ แต่ให้ตรวจถึงความเข้ากันได้ของไฟล์และการเชื่อมภายนอกด้วย
27. MFC กับ COM/OLE/ActiveX
MFC ถูกใช้กับแอปที่สัมพันธ์ลึกกับ COM, OLE และ ActiveX ด้วย
ในแอปธุรกิจรุ่นเก่า อาจยังเหลือองค์ประกอบแบบนี้
OLE Automation
ActiveX Control
COM server
COM client
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant
ในโค้ดสตาร์ตของแอป MFC อาจเจอแบบนี้
if (!AfxOleInit())
{
AfxMessageBox(_T("OLE initialization failed"));
return FALSE;
}
เมื่อใช้ COM/OLE สิ่งที่ดูเหมือนปัญหา MFC จริง ๆ อาจมาจากการเริ่มต้น COM, เธรดโมเดล, reference count, ข้อมูลการลงทะเบียน และความต่าง 32-bit/64-bit
ที่ควรระวังเป็นพิเศษคือ ActiveX หรือคอมโพเนนต์ COM แบบ 32-bit
แอป MFC 32-bit ใช้ COM 32-bit
แอป MFC 64-bit ใช้ COM 64-bit
การลงทะเบียน COM ของ 32-bit/64-bit แยกกัน
ActiveX เก่าอาจยังไม่รองรับ 64-bit
ตอนย้ายแอป MFC ไป x64 ต้องตรวจไม่ใช่แค่โค้ด UI แต่ตรวจการพึ่ง COM/OLE ด้วยเสมอ
28. การรองรับ high DPI กับ Windows สมัยนี้
ถ้าเอาแอป MFC รุ่นเก่าไปรันบน Windows ปัจจุบัน การแสดงผลอาจพังในสภาพ high DPI
ตัวอย่างปัญหาแบบนี้
ตัวอักษรถูกตัด
ปุ่มเล็กเกินไป
การวาดพิกเซลคงที่เลื่อน
หลายจอที่สเกลต่างกันแล้วพัง
บิตแมปเก่าเบลอ
เลย์เอาต์ไดอะล็อกแน่นเกินไป
ในแอปเดสก์ท็อป Windows แอปต้องประกาศโหมดรองรับ DPI ให้ชัด
แม้เป็นแอป MFC ก็ต้องตรวจแมนิเฟสต์ รีซอร์ส โค้ดการวาด ฟอนต์ และเลย์เอาต์
โค้ดที่เขียนพิกัดคงที่ตรง ๆ แบบนี้พังง่ายที่ high DPI
pDC->TextOut(10, 10, _T("Status"));
pDC->Rectangle(10, 40, 200, 80);
พิกัดที่สมมติพิกเซลคงที่จะเพี้ยนเมื่อ DPI เปลี่ยน
มุมตรวจตอนบำรุงรักษาคือ
การตั้งค่า DPI ใน application manifest
ฟอนต์ของไดอะล็อกรีซอร์ส
การวาดพิกเซลคงที่
ความละเอียดของรีซอร์สรูปภาพ
พฤติกรรมบนหลายจอ
การแสดงผลบน Windows 10 / Windows 11
การรองรับ high DPI ของ MFC ไม่จบแค่เปลี่ยนการตั้งค่าโปรเจกต์ UI รุ่นเก่าต้องตรวจหน้าจอจริงและแก้เลย์เอาต์
29. Exception handling และการจัดการข้อผิดพลาด
MFC มีคลาสข้อยกเว้นและธรรมเนียมจัดการข้อผิดพลาดของตัวเอง
ในโค้ดเก่ามักเจอแมโครแบบนี้
TRY
{
// ประมวลผล
}
CATCH(CFileException, e)
{
e->ReportError();
}
END_CATCH
บางโค้ดปนกับ try / catch ของ C++ สมัยนี้ด้วย
try
{
DoSomething();
}
catch (const std::exception& ex)
{
// เขียนล็อก
}
ตอนบำรุงรักษา MFC ควรระวังจุดเหล่านี้
ข้อยกเว้นของ MFC กับข้อยกเว้นมาตรฐาน C++ ปนกันหรือไม่
เข้าใจอายุขัยของอ็อบเจ็กต์ข้อยกเว้นผิดหรือไม่
เข้าใจแมโคร THROW/CATCH รุ่นเก่าหรือไม่
ข้อผิดพลาดแบบค่าที่คืนกับข้อยกเว้นปนกันหรือไม่
จบแค่ AfxMessageBox โดยไม่เหลือล็อกหรือไม่
ในแอปธุรกิจ นอกจากโชว์ข้อความผิดพลาดบนหน้าจอ สิ่งสำคัญคือเหลือล็อก ประวัติการทำงาน ค่าที่กรอก และสถานะการเชื่อมภายนอก
แอป MFC รุ่นเก่าบางตัวจบข้อผิดพลาดแค่ AfxMessageBox
AfxMessageBox(_T("บันทึกไม่สำเร็จ"));
ถ้าจะเพิ่มความง่ายในการบำรุงรักษา ควรแยกการแสดง UI กับการบันทึกล็อก
LogError(_T("Save failed"), path);
AfxMessageBox(_T("บันทึกไม่สำเร็จ กรุณาดูล็อก"));
30. ให้ MFC กับ C++ สมัยใหม่อยู่ด้วยกันอย่างไร
แม้กำลังบำรุงรักษาแอป MFC ก็ไม่จำเป็นต้องเขียนทุกอย่างตามแบบ C++ เก่า
ชั้น UI ให้เคารพธรรมเนียมของ MFC ส่วน domain logic และการคำนวณจัดด้วย C++ สมัยใหม่ได้
ตัวอย่างการแยก
ชั้น MFC
CDialog
CView
CDocument
CString
message map
การจัดการรีซอร์ส
ชั้นที่ไม่ใช่ MFC
std::string / std::wstring
std::vector
std::optional
std::variant
std::filesystem
คลาสที่ทดสอบหน่วยได้
business logic
รูปแบบที่ไม่ดีคือยัดการประมวลผลทั้งหมดลงคลาสไดอะล็อก
void CMainDialog::OnBnClickedExecute()
{
// ดึงอินพุต
// อ่านไฟล์
// สื่อสาร
// คำนวณ
// อัปเดต DB
// อัปเดตหน้าจอ
// เขียนล็อก
// จัดการข้อยกเว้น
}
โค้ดแบบนี้เปลี่ยนยาก ทดสอบยาก และสอบสวนบั๊กก็ยาก
ถ้าจะปรับปรุง ให้ดึงโลจิกออกจากคลาส MFC
void CMainDialog::OnBnClickedExecute()
{
if (!UpdateData(TRUE))
{
return;
}
ExecuteRequest request;
request.Name = ToStdWString(m_name);
request.Interval = m_interval;
ExecuteResult result = m_service.Execute(request);
m_status = ToCString(result.Message);
UpdateData(FALSE);
}
แบบนี้ m_service.Execute ทดสอบได้โดยไม่ต้องมี MFC
การปรับปรุงที่ได้ผลที่สุดตอนบำรุงรักษาสินทรัพย์ MFC เดิม คือค่อย ๆ ดึงโลจิกออกจากคลาส UI
31. ทำให้โค้ด MFC ทดสอบได้ง่าย
แอป MFC ตามสภาพมักทดสอบหน่วยยาก
เหตุคือ UI, Win32, ไฟล์, การสื่อสาร, ฐานข้อมูล และสถานะโกลบอลมักผูกแน่น
แนวคิดให้ทดสอบง่ายขึ้นคือ
อย่าพยายามทดสอบ CDialog หรือ CView โดยตรง
ดึงโลจิกที่ไม่ใช่ UI ออกก่อน
แปลงชนิดของ MFC ที่ขอบ
ทำอินเทอร์เฟซให้ไฟล์และการสื่อสาร
ทำ event handler ของหน้าจอให้บาง
ตัวอย่างคือย้ายการประมวลผลไปคลาส C++ บริสุทธิ์
class PriceCalculator
{
public:
int CalculateTotal(const std::vector<int>& prices) const
{
int total = 0;
for (int price : prices)
{
total += price;
}
return total;
}
};
ฝั่ง MFC รับผิดชอบแค่อินพุตกับเอาต์พุต
void CPriceDialog::OnBnClickedCalculate()
{
if (!UpdateData(TRUE))
{
return;
}
std::vector<int> prices = ParsePrices(ToStdWString(m_input));
int total = m_calculator.CalculateTotal(prices);
m_result.Format(_T("%d"), total);
UpdateData(FALSE);
}
โครงนี้ทำให้ทดสอบ PriceCalculator และ ParsePrices ด้วยเฟรมเวิร์กทดสอบ C++ ปกติได้
ไม่จำเป็นต้องสร้างแอป MFC ทั้งก้อนใหม่ในครั้งเดียว แค่ดึงงานที่ทดสอบได้ออกจาก event handler ก็ได้ผลแล้ว
32. ตรึงสภาพแวดล้อมการ build
ตอนบำรุงรักษาแอป MFC การตรึงสภาพแวดล้อมการ build สำคัญ
ในโค้ดเบสรุ่นเก่า ความต่างเหล่านี้ทำให้ผล build เปลี่ยนได้
เวอร์ชัน Visual Studio
เวอร์ชัน MSVC toolset
เวอร์ชัน Windows SDK
มีคอมโพเนนต์ MFC/ATL หรือไม่
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
static link ของ MFC / shared DLL
การตั้งค่า runtime library
precompiled header
ในแอป MFC การพึ่งพาจำนวนมากอาจไปรวมที่ stdafx.h หรือ pch.h
#include "framework.h"
#include "MyApp.h"
build อาจพังเพราะไฟล์เดียวตั้งค่าคอมไพล์ต่าง หรือการตั้งค่า precompiled header คลาด
ในโปรเจกต์บำรุงรักษา ถ้าเขียนข้อมูลนี้ไว้ใน README งานหลังจะง่ายขึ้น
เวอร์ชัน Visual Studio ที่ต้องใช้
workload และ individual component ที่ต้องใช้
Windows SDK ที่ต้องใช้
แพลตฟอร์มเป้าหมาย
วิธีลิงก์ MFC
ขั้นตอน build
วิธีรัน CI
วิธีสร้างสิ่งที่แจก
ก้าวแรกของการบำรุงรักษา MFC คือเลิกสถานะ “บนพีซีของฉัน build ได้”
33. build MFC ใน CI
แอป MFC ก็ build ใน CI ได้
แต่สภาพแวดล้อม CI ต้องมีคอมโพเนนต์ MFC
ถ้าใช้ Visual Studio Build Tools ต้องระบุ component ID ของ MFC/ATL ตอนติดตั้ง ID ที่ระบุคือชุดเดียวกับตารางในบทที่ 5
จุดที่ควรตรวจมีดังนี้
Build Tools มี MFC หรือไม่
toolset เป้าหมายตรงกับโปรเจกต์หรือไม่
มี Windows SDK หรือไม่
ตรวจทั้ง x86 build และ x64 build หรือไม่
resource compiler ทำงานหรือไม่
มีขั้นตอนเซ็นชื่อหรือไม่
การสร้างตัวติดตั้งอยู่ในขอบ CI หรือไม่
ในแอป MFC การทำ UI test อัตโนมัติไม่ใช่เรื่องง่าย แต่การอัตโนมัติระดับนี้ได้ผล
build Debug / Release
build x86 / x64
การวิเคราะห์เชิงสถิต
การทดสอบหน่วย
การสร้างตัวติดตั้ง
เก็บแฮชของผลลัพธ์
ตรวจ DLL ที่พึ่งพา
ในการบำรุงรักษาระยะยาว แค่รักษาสภาพที่ build ได้ก็มีคุณค่ามากแล้ว
34. จุดที่ต้องดูเมื่อดีบักแอป MFC
ตอนตามบั๊กในแอป MFC ลำดับนี้มักมีประสิทธิภาพ
1. เป็นหน้าจอไหน
2. เป็นไดอะล็อก วิว หรือเฟรม
3. resource ID ที่ตรงกับการทำงานคืออะไร
4. message map พาไปฟังก์ชันไหน
5. ทิศทางของ UpdateData ถูกหรือไม่
6. ถ้าเป็น Document/View สถานะของ Document คืออะไร
7. command routing ไหลไปคลาสอื่นหรือไม่
8. worker thread ไปแตะ UI หรือไม่
9. ข้อยกเว้นหรือข้อผิดพลาดถูกกลืนด้วย AfxMessageBox อย่างเดียวหรือไม่
10. มีปัญหาค่าที่ยังไม่เริ่มต้นหรืออายุขัยเฉพาะรีลีส build หรือไม่
ตัวอย่างถ้าบั๊กคือ “กดปุ่มแล้วไม่มีอะไรเกิด” ให้ตรวจจุดนี้
IDC ของปุ่มถูกหรือไม่
มี ON_BN_CLICKED หรือไม่
ซิกเนเจอร์ของ handler ถูกหรือไม่
ไดอะล็อกรีซอร์สเป็นคนละอันหรือไม่
ปุ่มถูกปิดใช้งานหรือไม่
UpdateData ล้มเหลวกลางทางหรือไม่
ข้อยกเว้นถูกกลืนหรือไม่
ถ้าเป็น “กดเมนูไม่ได้” ให้ดูฝั่งนี้
ถูกปิดด้วย ON_UPDATE_COMMAND_UI หรือไม่
command ID ซ้ำหรือไม่
วิวที่แอ็กทีฟตรงที่คาดหรือไม่
handler อยู่ที่ Frame/View/Document/App ที่ไหน
ใน MFC อีเวนต์บนผิวกับการประมวลผลจริงถูกต่อด้วยแมโครและการส่งต่อ จึงเข้าใจง่ายขึ้นถ้าวาดเส้นทางเรียกจนกว่าจะคุ้น รูปในบทที่ 9 คือรูปพื้นฐานของเส้นทางเรียกนั้น
ถ้าอยากกะอาการก่อน ให้ดูตารางในบทที่ 35 ก่อน ตารางนั้นจับคู่หลุมพรางที่พบบ่อยกับจุดที่ต้องตรวจ
35. หลุมพรางที่พบบ่อย
จัดระเบียบหลุมพรางที่พบบ่อยตอนบำรุงรักษา MFC
ค้นแค่การเรียกฟังก์ชันโดยไม่ดู message map
คิดว่า CWnd* ที่ไม่เป็น null ใช้ได้เสมอ
ปนอายุขัยของ HWND กับอ็อบเจ็กต์ C++
ทิศทางของ UpdateData(TRUE/FALSE) ผิด
ไม่ทันสังเกตว่าถูกปิดด้วย ON_UPDATE_COMMAND_UI
ไม่ทันสังเกต ID ชนกันใน resource.h
แตะ UI โดยตรงจาก worker thread
ตัวอักษรเพี้ยนตอนแปลง CString กับ std::string
ทำลายโค้ดที่สมมติ MBCS ตอนทำ Unicode
ลืม AFX_MANAGE_STATE ใน MFC DLL
ทำลาย COM/ActiveX ที่สมมติ x86 ตอนย้าย x64
เลือก/คืน GDI object ผิด
เลย์เอาต์พิกัดคงที่พังที่ high DPI
สำหรับแต่ละข้อ จะจับคู่ “อาการที่มักออก” กับ “ดูที่ไหนเพื่อยืนยัน” ทางเข้าการดีบักสอดคล้องกับขั้นตอนในบทที่ 34
| หลุมพราง | อาการที่มักออก | จุดที่ต้องตรวจ |
|---|---|---|
| ค้นแค่การเรียกฟังก์ชันโดยไม่ดู message map | หา caller ไม่เจอแต่ฟังก์ชันกลับรัน | ค้น BEGIN_MESSAGE_MAP ขั้นตอนที่ 4 ในบทที่ 9 และ 34 |
คิดว่า CWnd* ที่ไม่เป็น null ใช้ได้เสมอ |
แครชเป็นครั้งคราว แครชเมื่อไปจับหน้าจอที่ปิดแล้ว | ผ่าน ::IsWindow(pWnd->GetSafeHwnd()) บทที่ 8 |
ปนอายุขัยของ HWND กับอ็อบเจ็กต์ C++ |
access violation หลังปิดไดอะล็อก | ว่าได้ถือค่าที่คืนจาก GetDlgItem ไว้หรือไม่ บทที่ 22 |
ทิศทางของ UpdateData ผิด |
ค่าที่กรอกไม่อัปเดต ค่าเริ่มต้นไม่โผล่บนหน้าจอ | จุดเรียก UpdateData(TRUE) กับ UpdateData(FALSE) บทที่ 13 และขั้นตอนที่ 5 ในบทที่ 34 |
ไม่ทันสังเกตว่าถูกปิดด้วย ON_UPDATE_COMMAND_UI |
ปุ่มหรือเมนูเทา กดไม่ได้ | handler ของ ON_UPDATE_COMMAND_UI บทที่ 11 และหัวข้อ “กดเมนูไม่ได้” ในบทที่ 34 |
ไม่ทันสังเกต ID ชนกันใน resource.h |
ไดอะล็อกหรือรายการเมนูอื่นตอบสนอง | จับคู่ resource.h กับ .rc ตาม ID บทที่ 16 |
| แตะ UI โดยตรงจาก worker thread | แครชไม่เสถียร ทำซ้ำยาก บางครั้งค้าง | ว่าฟังก์ชันที่ส่งให้ AfxBeginThread ไปแตะ UI หรือไม่ บทที่ 23 |
ตัวอักษรเพี้ยนตอนแปลง CString กับ std::string |
เพี้ยนเฉพาะภาษาญี่ปุ่น ถูกตัดท้าย | การตั้งค่า character set ของโปรเจกต์และจุดแปลง บทที่ 18 |
| ทำลายโค้ดที่สมมติ MBCS ตอนทำ Unicode | ความยาวบัฟเฟอร์คลาด อ่านไฟล์เดิมไม่ได้ | ว่าใช้ sizeof เป็นจำนวนตัวอักษรหรือไม่ บทที่ 26 |
ลืม AFX_MANAGE_STATE ใน MFC DLL |
หาไดอะล็อกหรือสตริงรีซอร์สใน DLL ไม่เจอ | ต้นฟังก์ชันที่เปิดออกของ DLL บทที่ 24 |
| ทำลาย COM/ActiveX ที่สมมติ x86 ตอนย้าย x64 | ล้มเหลวตอนสตาร์ตหรือโชว์หน้าจอเฉพาะ 64-bit build | ว่าการลงทะเบียน COM อยู่แค่ฝั่ง 32-bit หรือไม่ บทที่ 27 |
| เลือกและคืน GDI object ผิด | วาดเพี้ยนเมื่อรันนาน | คอลัมน์ “GDI objects” ใน Task Manager เพิ่มขึ้นเรื่อยหรือไม่ บทที่ 20 |
| เลย์เอาต์พิกัดคงที่พังที่ high DPI | ตัวอักษรถูกตัดเฉพาะสภาพสเกล 150% | การตั้งค่า DPI ในแมนิเฟสต์และการวาดพิกัดคงที่ บทที่ 28 |
ความผิดของ MFC บางครั้งดูแค่ไวยากรณ์ C++ ไม่พอ ต้องดูถึง Windows message, รีซอร์ส, handle, โมดูล และการตั้งค่ารันไทม์ด้วย
36. ควรเลือก MFC สำหรับงานพัฒนาใหม่หรือไม่
การเลือก MFC สำหรับงานพัฒนาใหม่ทั้งก้อน ควรตัดสินใจอย่างระมัดระวัง
เหตุผลที่พอจะเลือก MFC ได้คือกรณีแบบนี้
ต้องเชื่อมแน่นกับโค้ด MFC เดิม
อยากนำชิ้นส่วนหรือหน้าจอ MFC เดิมกลับมาใช้
ต้องควบคุมใกล้ Win32/GDI/COM มาก
ในองค์กรมีทักษะบำรุงรักษา MFC พอ
เป้าหมายจำกัดที่เดสก์ท็อป Windows
ตามแผนย้ายระยะยาว ต้องต่อเติมด้วย MFC ก่อน
กลับกัน กรณีต่อไปนี้ควรพิจารณาทางเลือกอื่น
อยากทำ UI สมัยใหม่
ต้องการเลย์เอาต์ยืดหยุ่นหรือแอนิเมชัน
ศูนย์กลางอยู่ที่การเชื่อมเว็บหรือคลาวด์
ให้ความสำคัญกับความง่ายในการทดสอบ
อยากเลือกเทคโนโลยีที่นักพัฒนารุ่นใหม่เข้ามาง่าย
ต้องรองรับข้ามแพลตฟอร์ม
อยากให้ความสำคัญกับ accessibility และ high DPI ตั้งแต่ต้น
MFC ไม่ใช่เทคโนโลยีที่ “ไม่มีค่าให้เรียนรู้ตั้งแต่วันนี้” และก็ไม่ใช่เทคโนโลยีที่ “เลือกเพราะใหม่” แต่เป็นเทคโนโลยีสำหรับเผชิญกับสินทรัพย์ native ของ Windows เดิม
37. แนวคิดตอนย้ายออกจาก MFC
ถ้าอยากย้ายแอป MFC ไปเทคโนโลยีอื่น การมุ่งรีไรต์ทั้งก้อนทันทีมักล้มเหลว
ก่อนอื่นให้แยกแอปแบบนี้
UI
business logic
รูปแบบไฟล์
การสื่อสาร
การประมวลผลฐานข้อมูล
การควบคุมอุปกรณ์
การพิมพ์
การเชื่อม COM/OLE
การจัดการการตั้งค่า
ล็อก
ในนี้ สิ่งที่พึ่ง MFC มากที่สุดคือ UI
ในทางกลับกัน business logic และงานไฟล์มีโอกาสดึงออกได้
ลำดับที่เป็นจริงของการย้ายคือ
1. ทำให้สภาพแวดล้อมการ build ทำซ้ำได้
2. ตรึงพฤติกรรมเดิมด้วยข้อมูลทดสอบ
3. ดึงโลจิกออกจาก UI event handler
4. ย้ายไปไลบรารี C++ ที่ไม่ผูก MFC
5. เพิ่มการทดสอบอัตโนมัติ
6. ทำเอกสารสเปกภายนอก
7. แทนที่จากหน้าจอที่จำเป็นทีละขั้น
ถ้าตั้งเป้าที่ “เลิกใช้ MFC” เอง มักยากกว่าการตั้งเป้าที่ “ดึงโลจิกสำคัญที่ถูกขังใน MFC ออกมา” จะสำเร็จง่ายกว่า
38. จุดเริ่มต้นเมื่ออ่านโค้ด MFC
เมื่ออ่านโปรเจกต์ MFC เดิมครั้งแรก ให้เริ่มจากไฟล์พวกนี้
*.vcxproj
ดู toolset, การตั้งค่า MFC, character set, การตั้งค่ารันไทม์
resource.h
ดู resource ID
*.rc
ดูไดอะล็อก เมนู สตริง ไอคอน
*App.cpp / *App.h
ดูคลาสลูกของ CWinApp และ InitInstance
MainFrm.cpp / MainFrm.h
ดูเฟรมหลักกับเมนู/แถบเครื่องมือ
*Doc.cpp / *Doc.h
ถ้าเป็น Document/View ให้ดูโครงสร้างข้อมูลและการบันทึก
*View.cpp / *View.h
ดูการวาดและการทำงานของผู้ใช้
*Dlg.cpp / *Dlg.h
ดูไดอะล็อก, DDX, การประมวลผลปุ่ม
ถัดไปคือคำค้นที่ใช้บ่อย
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_UPDATE_COMMAND_UI
ON_BN_CLICKED
DoDataExchange
UpdateData
OnInitDialog
OnDraw
Serialize
AfxMessageBox
AfxBeginThread
AFX_MANAGE_STATE
ถ้าค้นจุดเหล่านี้ จะเห็นการเคลื่อนของแอปได้ง่ายขึ้น
39. แนวออกแบบตอนบำรุงรักษา MFC
ถ้าจะบำรุงรักษาสินทรัพย์ MFC เดิมระยะยาว นโยบายแบบนี้ได้ผล
ทำ UI event handler ให้บาง
อย่าให้ CString หรือ CWnd รั่วไปชั้นที่ไม่ใช่ UI มากเกินไป
ย้าย business logic ไปคลาส C++ ปกติ
สร้างการทดสอบความเข้ากันได้ของรูปแบบไฟล์
ทำให้ความต่าง x86/x64 ชัด
รีวิวการเปลี่ยน resource ID
ตรวจ module state ของ MFC DLL
จัด log ให้เป็นระบบ
ตรึงการ build ด้วย CI
ตรวจหน้าจอบน high DPI และ Windows 11 เป็นระยะ
โดยเฉพาะอย่างยิ่ง อย่ายัดการประมวลผลลง CDialog หรือ CView มากเกินไป
คลาสหน้าจอของ MFC ควรโฟกัสที่อินพุต การแสดงผล และการส่งอีเวนต์
ดึงค่าจากหน้าจอ
ส่งให้บริการ
แสดงผลกลับบนหน้าจอ
ถ้าคุมไว้ระดับนี้ แม้เป็น MFC ก็บำรุงรักษาง่ายขึ้นมาก
40. เช็กลิสต์ภาคปฏิบัติ
เช็กลิสต์ตอนทำงานกับแอป MFC
เวอร์ชัน Visual Studio ชัดหรือไม่
ติดตั้งคอมโพเนนต์ MFC/ATL แล้วหรือไม่
เป้าหมาย x86/x64 ชัดหรือไม่
รู้การตั้งค่า Unicode/MBCS หรือไม่
MFC เป็น static link หรือ shared DLL
Visual C++ Redistributable ที่ต้องใช้คืออะไร
เอา resource.h กับ .rc เข้ารีวิวโค้ดหรือไม่
ตามเส้นทางอีเวนต์ผ่าน message map หรือไม่
ทิศทางของ UpdateData ถูกหรือไม่
ใช้โครงสร้าง Document/View หรือไม่
การจัดการอายุขัยของ modeless dialog ปลอดภัยหรือไม่
ไม่ได้แตะ UI โดยตรงจาก worker thread หรือไม่
มีจุดใน MFC DLL ที่ต้องใช้ AFX_MANAGE_STATE หรือไม่
ตรวจหน้าจอในสภาพ high DPI หรือไม่
การพึ่ง COM/ActiveX รองรับ 32-bit/64-bit หรือไม่
ล็อกเหลือหรือไม่
ทดสอบโลจิกที่ไม่ใช่ UI ได้หรือไม่
MFC ดูแปลกจนกว่าจะคุ้น แต่เมื่อรู้ว่าต้องดูตรงไหน จะอ่านได้ค่อนข้างเป็นแบบแผน
41. สรุป
MFC คือเฟรมเวิร์กที่มีประวัติยาวนานสำหรับสร้างแอปเดสก์ท็อป native ของ Windows ด้วย C++ มันไม่ใช่กระแสหลักของงานใหม่อีกต่อไป แต่ในการบำรุงรักษาสินทรัพย์เดิม การอัปเดตสภาพแวดล้อมการ build การเพิ่มฟีเจอร์ และการย้ายทีละขั้น มันยังเป็นเทคโนโลยีที่สำคัญอยู่
จุดที่สำคัญเป็นพิเศษเมื่อจะเข้าใจ MFC คือ
MFC ทำให้ใช้ Win32 API จาก C++ ได้ง่ายขึ้น
CWinApp จัดการแอปทั้งก้อน
อายุขัยของ CWnd กับ HWND ไม่ใช่เรื่องเดียวกัน
message map ผูกอีเวนต์กับฟังก์ชัน
Document/View คือกลไกแยกข้อมูลกับการแสดงผล
DDX/DDV ใช้ซิงก์ค่าและตรวจค่าของไดอะล็อก
ไฟล์รีซอร์สกับ resource.h สำคัญมาก
เข้าใจ CString และ TCHAR คู่กับการตั้งค่ารหัสอักขระ
ใน MFC DLL ให้ระวัง module state
แอป MFC เก่าคุ้มที่จะทบทวนจากมุม high DPI, x64, CI และการทดสอบ
ตอนทำงานกับ MFC สิ่งสำคัญก่อนคือเข้าใจโครงสร้าง มากกว่าการตัดสินว่า “เก่าจึงไม่ดี” แอป MFC เดิมมักบรรจุความรู้ธุรกิจหลายปี สเปกเฉพาะลูกค้า การเชื่อมอุปกรณ์ และความเข้ากันได้ของไฟล์ เพื่อรักษาค่านั้นไว้แล้วค่อยทำให้บำรุงรักษาง่ายขึ้นทีละนิด เส้นทางที่เป็นจริงคือเข้าใจธรรมเนียมของ MFC ดึงโลจิกที่ไม่ใช่ UI ออกมา แล้วจัดระเบียบการ build และการทดสอบ
ถ้าสรุปเป็นบรรทัดเดียว จะเป็นแบบนี้
รากฐานสำหรับจัดการกลไกของแอป native Windows ผ่านคลาส C++ และเฟรมเวิร์ก
เมื่อมีมุมนี้ MFC จะไม่ใช่แค่เทคโนโลยีเก่า แต่เป็นกุญแจสำหรับถอดรหัสสินทรัพย์ Windows เดิมอย่างปลอดภัย
อ้างอิง
- ชุดโค้ดอ้างอิงที่จัดชิ้นส่วนโค้ดของบทความนี้ตามบท - 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
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
Win32 Thread Pool API — คอนเคอร์เรนซีโดยไม่สร้างเธรดเอง ด้วย CreateThreadpoolWork
โค้ดเนทีฟของคุณกระจายการเรียก CreateThread ไปทั่วหรือไม่ บทความนี้อธิบาย Win32 thread pool API ที่ออกแบบใหม่ใน Vista — ออบเจ็กต์สี่ชนิด w...
Named Pipe ในทางปฏิบัติ — IPC มาตรฐานของ Windows ตั้งแต่การออกแบบถึงความปลอดภัย
คู่มือเชิงปฏิบัติเรื่อง named pipe ซึ่งเป็นการสื่อสารระหว่างโปรเซสมาตรฐานของ Windows บทความนี้จัดระเบียบจากแหล่งปฐมภูมิ ทั้งการเลือกระหว่...
DllMain และ Loader Lock — เหตุผลจริงที่ถูกบอกให้ "อย่าทำอะไรในการเริ่มต้น DLL"
ทำไมคุณต้องไม่เรียก LoadLibrary หรือซิงโครไนซ์กับเธรดอื่นจาก DllMain บทความนี้อธิบายจากแหล่งปฐมภูมิว่า loader lock ทำให้การแจ้ง DLL ทุกคร...
Spurious Wakeup — ทำไม condition variable จึงตื่น "โดยไม่ถูกแจ้ง" และวิธีรออย่างถูกต้องบน Windows
การรอของ condition variable สามารถกลับได้แม้ไม่มีการแจ้งมาถึง (spurious wakeup) บทความนี้อธิบายจากการอิมพลีเมนต์ของ Windows ว่าทำไมข้อกำห...
แนวปฏิบัติมัลติเธรดเชิงปฏิบัติ: ฉบับ C++ — ตัดอุบัติเหตุด้วยโครงสร้างด้วย RAII และ jthread
ใน C++ มัลติเธรดคือโลกที่ data race คือพฤติกรรมไม่กำหนด บทความนี้ไล่กับดักของดีสตรักเตอร์ std::thread การออกแบบการหยุดด้วย jthread และ st...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้
พัฒนาแอป Windows
แอปธุรกิจ การเชื่อมต่ออุปกรณ์ และเครื่องมือสื่อสาร ตั้งแต่ความต้องการจนถึงการพัฒนา
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- MFC คืออะไร?
- MFC ย่อมาจาก Microsoft Foundation Classes เป็นเฟรมเวิร์กแอป Windows ที่ทำให้ใช้ Win32 API เป็นคลาส C++ ได้ง่ายขึ้น หน้าต่างคือ CWnd ไดอะล็อกคือ CDialog แอปทั้งก้อนคือ CWinApp ไม่ใช่ไลบรารีวิเศษที่เขียนได้โดยไม่ต้องรู้ Windows แต่เป็นการจัดกลไกของ Windows เป็นชนิดข้อมูลและเฟรมเวิร์ก C++ จึงยังต้องมีความรู้ Win32 เช่น Windows message และ handle
- ทุกวันนี้ยังใช้ MFC ได้หรือไม่ ยังมีซัพพอร์ตอยู่หรือไม่?
- MFC ยังใช้ใน Visual Studio ได้ และยังมีซัพพอร์ตอยู่ แต่เอกสารของ Microsoft มีหมายเหตุว่าจะไม่เพิ่มฟีเจอร์ใหม่หรืออัปเดตเอกสาร ในทางปฏิบัติมักใช้ตอนบำรุงรักษาแอป MFC เดิม เพิ่มฟีเจอร์ อัปเดตสภาพแวดล้อมการ build และย้ายไป UI อื่นทีละขั้น ส่วนงาน GUI ใหม่ทั่วไปควรตัดสินใจอย่างระมัดระวัง ต้องติดตั้ง individual component ใน Visual Studio Installer (C++ MFC for latest build tools)
- message map ของ MFC คืออะไร?
- เป็นกลไกของ MFC ที่ผูก Windows message และคำสั่งเข้ากับฟังก์ชัน handler ระหว่าง BEGIN_MESSAGE_MAP กับ END_MESSAGE_MAP คุณเขียนแมโครอย่าง ON_BN_CLICKED และ ON_COMMAND เพื่อบอกว่าเมื่อกดปุ่มนี้ให้เรียกฟังก์ชันนี้ ถ้าค้นฟังก์ชันแล้วไม่พบการเรียกตรง ๆ แต่ฟังก์ชันกลับรันตอนมีอีเวนต์ ให้ดูที่ message map ตอนรีวิวโค้ด MFC ต้องดูทั้งฟังก์ชัน handler และ message map เป็นชุด
- จะย้ายจาก MFC ไปเทคโนโลยีอื่นอย่างไร?
- ถ้ามุ่งรีไรต์ทั้งก้อนทันที มักล้มเหลว ลำดับที่เป็นจริงคือทำให้สภาพแวดล้อมการ build ทำซ้ำได้ ตรึงพฤติกรรมเดิมด้วยข้อมูลทดสอบ แยกโลจิกออกจาก UI event handler ไปไว้ไลบรารี C++ ที่ไม่ผูก MFC เพิ่มการทดสอบอัตโนมัติ ทำเอกสารสเปกภายนอก แล้วค่อยแทนที่หน้าจอที่จำเป็นทีละขั้น เป้าหมายไม่ควรเป็นเลิกใช้ MFC แต่เป็นดึงโลจิกสำคัญที่ถูกขังใน MFC ออกมา จะสำเร็จง่ายกว่า