מה זה MFC ב-Windows — יסודות לתחזוקת אפליקציות קיימות

· עודכן בתאריך: · · Windows, MFC, Visual C++, C++, Win32, Native App, Desktop App, Legacy Code, שימוש בקוד קיים

1. מה כדאי להבין קודם

כשמתחזקים אפליקציות desktop ישנות ב-Windows, לפעמים נתקלים בשמות כאלה.

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

אלה שמות שמופיעים שוב ושוב ב-MFC, framework לאפליקציות Windows ב-C++. MFC הוא קיצור של Microsoft Foundation Classes, ספרייה שהופכת את Win32 API לנוח יותר לעבודה כמחלקות C++.

בפיתוח אפליקציות Windows היום יש הרבה אפשרויות — WinUI, WPF, Windows Forms, Electron, Qt, טכנולוגיות Web — ולכן פחות מתייחסים ל-MFC כמועמד ראשון לפיתוח חדש. אבל זו לא טכנולוגיה שנעלמה. באפליקציות עסקיות, מכשירי מדידה, תוכנות בקרה, CAD/CAM, כלי עבודה פנימיים וחבילות ותיקות, עדיין מתחזקים codebases של MFC.

לפני שנכנסים לפרטים, כדאי לגשת לנושא עם נקודות המבט האלה.

MFC הוא מפה חשובה לקריאת אפליקציות desktop ישנות ב-Windows
MFC לא מסתיר את Win32 API, אלא עוטף אותו בצורה טבעית ל-C++
בלי להכיר את המוסכמות של MFC, קל לטעות בהבנת ההתנהגות יותר ממה שנראה מהקוד
הערך הוא בתחזוקה, בהארכת חיים ובמעבר הדרגתי של קוד קיים, לא באימוץ חדש

המאמר סוקר את MFC: מבנה האפליקציה, message map, Document/View, dialogs, DDX/DDV, resources, build, ונקודות שחשוב לשים לב אליהן בתחזוקה.

קהל היעד הוא מי שיודע לכתוב C++ אבל אין לו ניסיון ב-Win32 API או ב-MFC. ידע מוקדם ברמה הזו מספיק.

תחום הרמה המצופה
C++ יודעים לקרוא classes, inheritance, virtual functions, pointers ו-references
Win32 API אין צורך בניסיון. מספיק ששמעתם על window ו-message
Visual Studio פתחתם solution ועשיתם 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)

knowledge map של המאמר

MFC הוא framework שהופך את Win32 API לנוח יותר לעבודה כמחלקות C++: CWinApp אחראי לאתחול של האפליקציה כולה, ה-message map קושר Windows messages לפונקציות handler, ארכיטקטורת Document/View מפרידה בין הנתונים לבין תצוגת ה-UI, ו-DDX/DDV מקשרים controls של dialog למשתני חבר ומוודאים את הקלט. כדי להשתמש ב-MFC צריך להתקין בנפרד את רכיב ה-MFC ב-Visual Studio Installer, ואפשר גם להגדיר לינק כ-shared DLL או לינק סטטי. המאמר מציג את המעמד הזה: באפליקציית GUI כללית חדשה לגמרי כדאי לשקול בזהירות אם לאמץ MFC, ואילו בתחזוקה, בהוספת פיצ’רים ובמעבר הדרגתי של אפליקציות MFC קיימות יש לזה בפועל ערך לעתים קרובות.

knowledge map: תחזוקת אפליקציות desktop ב-Windows עם MFCDiagram שמראה ש-MFC עוטף את Win32 API ב-C++ ומממש מנגנונים כמו message map ו-Document/View, שהוא מתאים לתחזוקה של קוד קיים אבל בפיתוח GUI חדש כדאי לשקול בזהירות, ואת הנקודות שחשוב לשים לב אליהן בסביבת ה-build ובשילוב COM.משתמש במשתמש במאוטמט אתמממש אתדורשמממש אתדורשמשתמש במממש אתדורשמשתמש בדורשמוגדר בדורשמשתמש בדורשדורשלא מומלץ למומלץ לדורשMFC (Microsoft Foundation Classes)Win32 APICWinAppmessage mapארכיטקטורת Document/ViewDDX/DDVקובץ resource (.rc / resource.h)CStringרכיב MFC ב-Visual Studio InstallerUse of MFC (shared DLL / static)module state של MFC (AFX_MANAGE_STATE)ActiveXדרישה ל-bitness תואםתמיכה ב-High DPIפיתוח אפליקציית GUI חדשה לגמריתחזוקה ומיגרציה הדרגתית של אפליקציות MFC קיימות

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 20, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

2. מה זה MFC

MFC הוא class library לבניית אפליקציות desktop 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, handles, messages ו-callbacks, ולכן באפליקציה גדולה קשה לשמור על מבנה ברור. MFC מאפשר לעבוד עם זה כמחלקות C++: חלון הוא CWnd, dialog הוא CDialog, האפליקציה כולה היא CWinApp, frame window הוא CFrameWnd, ו-view הוא CView.

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

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

MFC לא מחליף את Win32 API במשהו אחר לגמרי. זה framework דק אבל רחב ב-C++, שיושב על אופן החשיבה של Win32 API. לכן כדי לקרוא MFC צריך לא רק את המחלקות של MFC, אלא גם ידע מהסוג הזה.

Windows messages
handles כמו HWND
GDI/GDI+
קבצי resource
COM/OLE
DLL ו-runtime
קידוד תווים
threads ו-message loop

התמונה האמיתית של MFC היא לא ספריית קסם שמאפשרת לכתוב בלי לדעת Windows, אלא ארגון של מנגנוני Windows לטיפוסים ול-framework של C++.

3. האם אפשר עדיין להשתמש ב-MFC

MFC עדיין זמין ב-Visual Studio. אבל חשוב לא לטעות במעמד שלו. הוא עדיין נתמך, אבל זה לא UI framework מודרני שמקבל פיצ’רים חדשים באופן פעיל. גם בתיעוד MFC של Microsoft יש הערה ש-MFC ממשיך להיות נתמך, אבל לא יתווספו פיצ’רים חדשים ולא יעודכן התיעוד.

לכן המעמד של MFC נראה בערך כך.

תחזוקת אפליקציות MFC קיימות              -> נפוץ בפועל
הוספת פיצ'רים לאפליקציית MFC קיימת        -> אפשרי
עדכון סביבת ה-build של אפליקציית MFC      -> חשוב
מעבר הדרגתי מ-MFC ל-UI אחר                -> אפשרי
אימוץ באפליקציית GUI כללית חדשה לגמרי     -> לשקול בזהירות

במיוחד באפליקציות עסקיות ותיקות, UI, הדפסה, קריאה וכתיבה לקבצים, בקרת התקנים, פרוטוקולים ייעודיים ושילוב COM יכולים להיות כולם ארוזים בתוך MFC.

ב-codebase כזה, לפני שזורקים את MFC צריך קודם להגיע למצב שאפשר לקרוא MFC.

4. באילו תחומים MFC היה חזק

התחום הקלאסי שבו השתמשו ב-MFC הוא אפליקציות desktop native של Windows.

בפועל, אפליקציות מהסוג הזה.

כלי עבודה עסקיים שמרוכזים סביב dialog
אפליקציות SDI שפותחות קובץ ועורכות אותו
אפליקציות MDI שמטפלות בכמה documents
מסכי בקרה למכשירי מדידה ולציוד ייצור
אפליקציות native בתחום CAD/CAM
אפליקציות עם הרבה הדפסה ו-preview
אפליקציות עם שילוב ActiveX או OLE
אפליקציות שצמודות ל-Windows API ישן ולנכסי COM

החוזק של MFC הוא שהוא רץ קרוב לרכיבים native של Windows. windows, menus, toolbars, status bars, dialogs, common controls, הדפסה, file dialogs, Registry וציור GDI אפשר לטפל בהם כמחלקות C++.

החולשה היא שבנייה מודרנית של UI, data binding, קלות בדיקה, עיבוד אסינכרוני, layout מודרני, תמיכה ב-high DPI, לוקליזציה ונגישות לא נכתבים באופן טבעי כמו ב-frameworks חדשים יותר.

אם שמים את המאפיינים זה לצד זה, מקבלים את זה.

קרוב ל-Windows native
שליטה ישירה מ-C++
הרבה קוד קיים
דורש ידע ב-Win32
הרבה מוסכמות ישנות
מבנה שקל לבדוק — צריך לבנות אותו בעצמכם

5. הכנה לשימוש ב-MFC ב-Visual Studio

גם אם התקנתם C++ ב-Visual Studio, MFC לא בהכרח נכנס. MFC מטופל כ-individual component ב-Visual Studio Installer. בדרך כלל בודקים את ה-components האלה.

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, בודקים לא רק את הגדרות הפרויקט, אלא גם אם ה-component של MFC מותקן בצד Visual Studio.

איך בוחרים ב-installer:

1. מפעילים Visual Studio Installer מתפריט Start
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 חייבים לוודא שה-component עצמו מסומן.

כשמתקינים מסקריפט או מ-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, שבה יושבים ה-headers של MFC. ה-headers והספריות של 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

אם לא מודפס כלום, ה-component של MFC לא מותקן.

build במצב הזה נעצר כבר בשלב שבו מנסים לכלול את afxwin.h. שגיאת הקומפילציה הטיפוסית היא זו.

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

לפרש את השגיאה כטעות בהגדרת include path, ואז לחפש הרבה זמן ב-project properties, זה סיבוב מיותר שנפוץ. כש-afxwin.h או afxdialogex.h לא נמצאים, קודם חושדים ב-Visual Studio Installer.

אותו דבר ב-CI ובשרתי build. אם מקומית ב-Visual Studio ה-build עובר וב-CI הוא נכשל, הסיבה יכולה להיות חסר של component של MFC, או אי-התאמה בגרסת ה-toolset.

6. המבנה הבסיסי של אפליקציית MFC

לאפליקציית MFC יש בדרך כלל מבנה כזה.

מחלקה נגזרת מ-CWinApp
  אחראית לאתחול ולסגירה של האפליקציה כולה

מחלקות נגזרות מ-CFrameWnd / CMDIFrameWnd / CDialog
  אחראיות לחלון הראשי ול-dialogs

מחלקה נגזרת מ-CView
  אחראית לתצוגה ולפעולות משתמש

מחלקה נגזרת מ-CDocument
  אחראית לנתונים ולשמירת קבצים

קבצי resource
  מחזיקים menus, dialogs, icons, מחרוזות וכו'

message map
  קושר Windows messages ו-commands לפונקציות 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 ה-framework לוקח על עצמו חלק גדול מזה. המפתח בעיקר עושה override ל-InitInstance וכותב שם את האתחול הייחודי לאפליקציה.

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

    // טעינת הגדרות
    // אתחול COM
    // יצירת החלון הראשי
    // רישום document template

    return TRUE;
}

ב-InitInstance נוטים לכתוב טיפול מהסוג הזה.

אתחול common controls
הגדרת מפתחות Registry
טעינת רשימת הקבצים האחרונים
רישום document template
יצירת ה-main frame
טיפול ב-command-line arguments
אתחול COM/OLE

בתחזוקה, אם מתחילים ממחלקה נגזרת מ-CWinApp, סדר ההפעלה של כל האפליקציה נהיה קריא יותר.

8. CWnd היא המחלקה במרכז MFC

רוב מחלקות ה-UI של MFC יושבות על CWnd, שמייצגת חלון של Windows. אבל אובייקט CWnd ו-HWND הם לא אותו דבר.

HWND
  window handle שמנוהל על ידי Windows

CWnd
  אובייקט wrapper ב-C++ שמקל לעבוד עם HWND

ב-MFC, ל-CWnd יש HWND בפנים.

HWND hWnd = m_hWnd;

או שמקבלים אותו כך.

HWND hWnd = GetSafeHwnd();

נקודה חשובה בתחזוקה: גם אם CWnd* קיים, ה-HWND המתאים כבר יכול להיות destroyed.

לכן בודקים אם החלון עדיין תקף בצורה כזו.

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

בחקירת באגים ב-MFC חשוב לבדוק שאין פער בין ה-lifetime של אובייקט ה-C++ של CWnd לבין ה-lifetime של ה-window handle האמיתי של Windows.

9. מה זה message map

אחד המנגנונים שהכי מאפיינים את MFC הוא message map.

אפליקציית Windows מקבלת לחיצת עכבר, קלט מקלדת, ציור מחדש, שינוי גודל חלון, בחירה מ-menu וכו’ כ-Windows messages.

ב-Win32 API בדרך כלל מטפלים בזה ב-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

הזרימה מהרגע ש-Windows message מגיע עד שה-handler נקרא נראית כך.

זרימת Windows messages דרך message map ב-MFCWindows message נכנס ל-message loop של MFC, עובר ב-CWnd::WindowProc, ואז מחפשים התאמה ב-message map של המחלקה ושל מחלקות הבסיס עד שקוראים ל-handler או מעבירים ל-DefWindowProc.ישאיןנמצאהאין עד הסוףWindows messageWM_COMMAND / WM_PAINT וכו'message loopה-framework של MFC מריץ אותוCWnd::WindowProcנקודת כניסה משותפת שמספק MFCיש רשומה תואמתב-message map של המחלקה עצמה?קוראים ל-handlerש-ON_BN_CLICKED וכדומה מצביעים עליומחפשים ב-message map של מחלקות הבסיסמ-CDialogEx ל-CDialog ואז ל-CWndנמצאה התאמה איפשהו?DefWindowProcמעבירים לטיפול ברירת המחדל של Windows

הסיבה שלא רואים switch היא שה-framework לוקח על עצמו את החלק הזה: מעבר על ה-message map מהמחלקה עצמה אל מחלקות הבסיס. קל יותר לחשוב על DECLARE_MESSAGE_MAP ו-BEGIN_MESSAGE_MAP כ-macros שמכינים טבלת התאמה לכל מחלקה לצורך החיפוש הזה.

מי שלא רגיל לקרוא MFC מתקשה לראות מאיפה הפונקציה נקראת. אם חיפוש לא מוצא קריאה ישירה, מסתכלים על ה-message map.

הפונקציה לא נקראת ישירות
אבל היא רצה באירוע
-> בודקים את ה-macros BEGIN_MESSAGE_MAP / ON_...

ב-code review של MFC חשוב לא להסתכל רק על פונקציית ה-handler, אלא לבדוק אותה יחד עם ה-message map.

10. command routing

ב-MFC גם פעולות על menu ועל toolbar מטופלות כ-commands.

הדוגמה הטיפוסית היא ON_COMMAND.

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

void CMainFrame::OnFileOpen()
{
    // פתיחת קובץ
}

ל-MFC יש מנגנון שמפיץ command לאובייקט המתאים.

למשל, גם עבור אותו ID_EDIT_COPY, מי שמטפל יכול להיות ה-view הפעיל, ה-document, ה-frame או האפליקציה.

ה-view הפעיל
document
frame window
האפליקציה

ה-command עובר בסדר הזה עד שהוא מגיע לאובייקט שיכול לטפל בו.

לכן ב-MFC קשה לפעמים לעקוב אחרי “איזו פונקציה רצה אחרי לחיצה ב-menu” עם חיפוש מחרוזת פשוט בלבד.

בתחזוקה כדאי לבדוק את הנקודות האלה.

מה ה-command ID
באיזו מחלקה נמצא ON_COMMAND
איפה נמצא ON_UPDATE_COMMAND_UI
איזה view פעיל עכשיו
האם משתמשים במבנה Document/View

11. מה זה ON_UPDATE_COMMAND_UI

ב-MFC משתמשים לפעמים ב-ON_UPDATE_COMMAND_UI כדי לעדכן enable/disable של פריטי menu וכפתורי toolbar, מצב check, וטקסט תצוגה.

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

כך אפשר להפעיל את ה-menu או הכפתור רק כשאפשר למחוק.

כשבודקים באפליקציית MFC למה כפתור אפור, למה אי אפשר ללחוץ על menu, או למה מצב ה-check משתנה, לפעמים מוצאים את הסיבה ב-ON_UPDATE_COMMAND_UI.

12. אפליקציות MFC מבוססות dialog

אחת הצורות הברורות ביותר ב-MFC היא אפליקציה מבוססת dialog.

מסכי הגדרות, כלי עבודה עסקיים פשוטים ומסכי הפעלה של מכשירים לפעמים בנויים סביב dialog.

בדרך כלל יורשים מ-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 מופיעים הרבה האלמנטים האלה.

IDD_...        ID של dialog resource
IDC_...        ID של control
OnInitDialog   אתחול
DoDataExchange קישור בין controls למשתני חבר
UpdateData     סנכרון בין המסך למשתנים
ON_BN_CLICKED  טיפול בלחיצה על כפתור

dialog לא חי רק מהמראה. הוא רץ משילוב של resource, משתני חבר, message map ואתחול.

13. DDX ו-DDV

ב-dialogs של MFC מופיעים הרבה DDX ו-DDV.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX מקשר בין controls על ה-dialog למשתני חבר ב-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;
}

כשקלט ב-dialog של MFC מתנהג מוזר, בודקים את הנקודות האלה.

האם מוגדר DDX ב-DoDataExchange
האם קוראים ל-UpdateData(TRUE)
האם התזמון של UpdateData(FALSE) נכון
האם DDV דוחה את הקלט
האם ה-control IDs תואמים ל-resource

14. ארכיטקטורת Document/View

מאפיין גדול של MFC הוא ארכיטקטורת Document/View.

זה מבנה שמפריד בין הנתונים שהאפליקציה מטפלת בהם לבין התצוגה שלהם.

CDocument
  מחזיק נתונים
  אחראי לקריאה ולכתיבה של קבצים
  מודיע לכמה views על עדכון

CView
  מציג נתונים
  מטפל בפעולות משתמש
  מנהל ציור ומצב בחירה

הקשר בין frame, view ו-document נראה כך.

קשר בין frame, view ו-document ב-MFCCWinApp רושם document template, והתבנית מחברת frame window, document ו-view. ה-view מקבל את ה-document ב-GetDocument, וה-document מודיע לכל ה-views ב-UpdateAllViews.מקבלים עם GetDocumentמודיעים עם UpdateAllViewsמחלקה נגזרת מ-CWinAppהפעלה וסגירה של כל האפליקציהdocument templateCSingleDocTemplate / CMultiDocTemplateframe windowCFrameWnd / CMDIChildWndמחלקה נגזרת מ-CDocumentנתונים וקריאה/כתיבה לקבציםמחלקה נגזרת מ-CViewציור ופעולות משתמש

הנקודה היא שה-document template הוא מה שמחבר בין frame, document ו-view. את התבנית רושמים בתוך InitInstance, ולכן כשרוצים לדעת איזה view רואה איזה document, קודם קוראים את InitInstance.

גם כיוון הקריאה חשוב: מה-view אל ה-document משתמשים ב-GetDocument, ומה-document אל כל ה-views משתמשים ב-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;
};

בצד ה-view מקבלים את ה-document ומציירים.

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

    for (const auto& item : pDoc->m_items)
    {
        // ציור באמצעות pDC
    }
}

היתרון של Document/View הוא שקל יותר להציג את אותם נתונים בכמה views.

למשל אפשר להראות את אותם נתונים כך.

view טבלאי
view גרף
view פירוט
preview
view הדפסה

אבל במסך הגדרות פשוט או בכלי קטן, Document/View יכול להרגיש כבד מדי.

בתחזוקה, אם מזהים קודם אם האפליקציה משתמשת ב-Document/View או שהיא מרוכזת סביב dialog, קל יותר לעקוב אחרי הקוד.

15. SDI ו-MDI

ב-MFC, יחד עם Document/View, מופיעים הרבה המבנים SDI ו-MDI.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI מטפל בעיקרון ב-document אחד בתוך frame אחד.

חלון ראשי
  document אחד
  view אחד או כמה views

MDI מחזיק כמה child windows בתוך parent window אחד, וכל אחד מטפל ב-document.

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

באפליקציות Windows ישנות MDI היה נפוץ.

בתחזוקה אפשר לנחש את המבנה משמות המחלקות.

CFrameWnd          frame בסגנון SDI
CMDIFrameWnd       MDI parent frame
CMDIChildWnd       MDI child frame
CSingleDocTemplate document template ל-SDI
CMultiDocTemplate  document template ל-MDI

באפליקציות שנוצרו ב-wizard של MFC, בתוך InitInstance לרוב יש רישום של CSingleDocTemplate או CMultiDocTemplate.

16. להבין קבצי resource

באפליקציית MFC, קובץ .rc חשוב מאוד.

.rc הוא קובץ resource של Windows.

מוגדרים בו דברים כמו אלה.

תבניות dialog
menus
accelerator keys
icons
bitmaps
string table
version info
toolbars

ב-resource.h מוגדרים מזהי ה-resource.

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

בקוד MFC משתמשים ב-IDs האלה כדי לחבר בין resource לקוד C++.

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

בעיה נפוצה בתחזוקה היא אי-התאמה של resource IDs.

ה-ID ב-resource.h השתנה
אחרי merge בין branches יש התנגשות IDs
ה-control ID על ה-dialog לא תואם ל-ID ב-DDX
נשאר menu ID שאמור היה להימחק
יש כפילות IDs ב-string table

כשבודקים התנהגות של אפליקציית MFC, צריך להסתכל לא רק על קוד C++, אלא גם על .rc ועל resource.h באותו זמן.

17. Class Wizard וקוד שנכתב ידנית

ל-MFC יש היסטוריה צמודה ל-Class Wizard של Visual Studio.

עם Class Wizard אפשר לייצר אוטומטית message handlers, משתני DDX, ו-overrides לפונקציות וירטואליות.

לכן בקוד MFC נשאר הרבה קוד בצורת הקוד שהכלי ייצר.

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

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

ב-Visual Studio חדש המראה וצורת הייצור יכולים להיות שונים, אבל ב-codebases ישנים עדיין נשארים סימוני הערה כאלה.

בתחזוקה חשוב לא לשבור בגסות את הגבול בין קוד שיוצר לבין קוד שנכתב ידנית.

לא למחוק message map
לא לשבור את קישורי DDX
לא לשנות resource IDs בלי סיבה
לא למחוק סתם הערות שמניחות Class Wizard ישן

ב-MFC לא מספיק שהקוד מתקמפל כ-C++. צריך גם לשמור במידה מסוימת על הצורה ש-resource editor ו-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 ול-controls.

בתחזוקה צריך לשים לב לקידוד תווים.

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 יש גם מחלקות לפעולות קבצים ול-serialization. המחלקות הטיפוסיות הן CFile ו-CArchive.

CFile file;
if (file.Open(path, CFile::modeRead))
{
    CArchive ar(&file, CArchive::load);
    // קריאה מ-ar
}

CArchive משמש הרבה במנגנון ה-serialization של 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);
        }
    }
}

ה-serialization של MFC נוח, אבל בשימוש לאורך זמן צריך זהירות.

תאימות לפורמט קבצים ישן
ניהול מספרי גרסה
שחזור אחרי כשל בקריאה
טיפול ב-exceptions
קידוד תווים
endianness
האם שומרים struct כמו שהוא

באפליקציות MFC שמשתמשות שנים בפורמט בינארי ייעודי, Serialize לפעמים הופך בפועל למפרט הקובץ.

במקרה כזה, לפני שינוי קוד, כדאי להכין נתוני בדיקה שקוראים קבצים קיימים.

20. ציור GDI ו-CDC

בציור מסך ב-MFC משתמשים הרבה במחלקה CDC, שמטפלת ב-Device Context של Windows. ב-CView::OnDraw מועבר CDC* כ-argument.

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

כשמשתמשים ב-pen או ב-brush, צריך לשים לב ל-select ולשחזור.

void CMyView::OnDraw(CDC* pDC)
{
    CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
    CPen* pOldPen = pDC->SelectObject(&pen);

    pDC->MoveTo(10, 10);
    pDC->LineTo(100, 100);

    pDC->SelectObject(pOldPen);
}

סביב אובייקטי GDI הטעויות האלה הופכות לבעיה.

אחרי SelectObject לא מחזירים את האובייקט הקודם
יוצרים הרבה אובייקטי GDI ולא משחררים אותם
מבלבלים בין התפקיד של OnPaint לבין OnDraw
אין double buffering ויש הבהוב
ציור שמניח פיקסלים קבועים נשבר ב-high DPI

בבאג ציור ב-MFC צריך לבדוק לא רק לוגיקת C++, אלא גם משאבי GDI של Windows, תזמון ציור מחדש, DPI וגודל גופן.

21. modal dialog ו-modeless dialog

ב-MFC גם אופן הצגת ה-dialog דורש תשומת לב.

dialog מודאלי מציגים עם DoModal.

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // טיפול בלחיצה על OK
}

במקרה הזה הקורא מחכה עד שה-dialog נסגר.

לעומת זאת, dialog מסוג modeless מחזיר שליטה לקורא גם אחרי היצירה.

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

ב-dialog מסוג modeless, ניהול ה-lifetime חשוב.

מתי עושים delete ל-dialog שנוצר ב-new
האם החלון האב נהרס קודם
האם בצד ה-dialog משתמשים ב-PostNcDestroy
האם אין יצירה כפולה
האם נשאר pointer אחרי הסגירה

בחקירת crash ב-MFC, לפעמים הסיבה היא בעיית lifetime של dialog מסוג modeless.

22. ה-lifetime של אובייקט C++ ושל handle של Windows

נקודה חשובה מאוד ב-MFC היא ההבדל בין lifetime של אובייקט C++ לבין lifetime של handle של Windows. למשל CWnd הוא אובייקט C++, אבל החלון עצמו מנוהל על ידי Windows כ-HWND, ושני אלה לא תמיד נוצרים יחד ונעלמים יחד.

אובייקט CWnd קיים אבל HWND עוד אין
HWND כבר נהרס אבל אובייקט CWnd נשאר
נוצר wrapper זמני של CWnd
מחליפים handle ב-Attach/Detach

למשל הקוד הזה דורש זהירות.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// שמירת pWnd כחבר מחלקה לשימוש מאוחר

אם שומרים לאורך זמן pointer שהתקבל מ-GetDlgItem, יש סיכון לגשת אליו אחרי שהחלון נהרס.

לפעמים בטוח יותר לקרוא ל-GetDlgItem בכל פעם, או לנהל משתנה חבר ל-control דרך DDX.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

ב-MFC, pointer שאינו null לא אומר שהשימוש בטוח.

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

האינטואיציה הזו חשובה מאוד בתחזוקת MFC.

23. threads ועדכון UI

UI של Windows צריך בעיקרון להיות מופעל מ-UI thread שבו הוא נוצר, וזה נכון גם לאפליקציות MFC. אם worker thread נוגע ישירות ב-UI controls, זה גורם להתנהגות לא יציבה או ל-crash.

דוגמה שכדאי להימנע ממנה.

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

סביב threads ב-MFC בודקים את הנקודות האלה.

האם לא נוגעים ב-UI ישירות מ-worker thread
האם לא עושים PostMessage אחרי שהחלון נהרס
האם לא עוצרים את ה-UI thread בהמתנה לסיום thread
האם הנעילה על נתונים משותפים נכונה
האם מבינים נכון את ערך החזרה ואת ה-lifetime של AfxBeginThread

24. MFC DLL ו-module state

כשבונים DLL ב-MFC מופיע המושג module state.

במצבים שבהם טוענים resource מ-MFC DLL, מציגים dialog, או בונים extension DLL, השאלה היא מאיזה מודול הולכים לקרוא את ה-resource.

בכניסה לפונקציה של MFC DLL לפעמים רואים מקרו כזה.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

זה מה שמאפשר ל-MFC להשתמש ב-module state הנכון.

אם שוכחים את המקרו, מגיעים לבאגים כאלה.

לא נמצא dialog resource בתוך ה-DLL
string resource נקרא ממודול אחר
לא נמצאים icons או menus
עובד רק ב-debug ונשבר ב-release

כשמתחזקים MFC DLL חשוב להבין את הקשר בין EXE, DLL רגיל, extension DLL ו-resource DLL.

במיוחד צריך זהירות כשאפליקציה שאינה MFC קוראת ל-MFC DLL, או כשיש מבנה plugins.

25. לינק סטטי של MFC או shared DLL

באפליקציית MFC יש בהגדרות הפרויקט את Use of MFC.

שתי האפשרויות הטיפוסיות הן אלה.

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

בשימוש ב-shared DLL, בסביבת הריצה צריך את ה-runtime המתאים של MFC ושל Visual C++.

בלינק סטטי חבילת ההפצה יכולה להיראות פשוטה יותר, אבל צריך לחשוב על גודל ה-exe, על עדכונים, על קליטת תיקוני אבטחה, ועל תנאי רישיון והפצה מחדש.

אין תשובה שנכונה תמיד.

השיקולים הם בערך אלה.

אפשר להתקין Visual C++ Redistributable אצל הלקוח
רוצים להתקרב ל-exe יחיד
איך קולטים עדכוני אבטחה
האם כמה אפליקציות חולקות את אותו runtime
אפשר לספק installer
מה גרסת 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.

במיוחד בקבצים חיצוניים, בפרוטוקולי תקשורת, ב-DLLs ישנים, בחיבור למסד נתונים ובתקשורת סריאלית, צריך לבדוק איזה קידוד מניחים.

חשוב לא לחשוב על מעבר ל-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, threading model, 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.

למשל בעיות כאלה.

טקסט נחתך
כפתורים קטנים מדי
ציור בפיקסלים קבועים זז
נשבר כשיש יחסי הגדלה שונים בין מסכים
bitmaps ישנים מטושטשים
ה-layout של ה-dialog נדחס

באפליקציית desktop של Windows, האפליקציה צריכה להצהיר במפורש על מצב תמיכה ב-DPI.

גם באפליקציית MFC צריך לבדוק manifest, resources, קוד ציור, גופנים ו-layout.

במיוחד קוד שכותב קואורדינטות קבועות כך נוטה להישבר ב-high DPI.

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

קואורדינטות שמניחות פיקסלים קבועים נראות שבורות כש-DPI משתנה.

נקודות הבדיקה בתחזוקה הן אלה.

הגדרת DPI ב-application manifest
הגופן ב-dialog resource
ציור בפיקסלים קבועים
רזולוציית משאבי תמונה
התנהגות עם כמה מסכים
תצוגה ב-Windows 10 / Windows 11

תמיכה ב-high DPI ב-MFC לא בהכרח מסתיימת בשינוי הגדרת פרויקט. ב-UI ישן צריך בדיקת מסך אמיתית ותיקון layout.

29. טיפול ב-exceptions ובשגיאות

ל-MFC יש מחלקות exception משלו ומוסכמות לטיפול בשגיאות.

בקוד ישן רואים macros כאלה.

TRY
{
    // טיפול
}
CATCH(CFileException, e)
{
    e->ReportError();
}
END_CATCH

יש גם קוד שמערבב את זה עם try / catch של C++ מודרני.

try
{
    DoSomething();
}
catch (const std::exception& ex)
{
    // כתיבה ל-log
}

בתחזוקת MFC כדאי לשים לב לנקודות האלה.

האם exceptions של MFC ושל C++ סטנדרטי לא מעורבבים
האם לא מבינים לא נכון את ה-lifetime של אובייקט ה-exception
האם מבינים את ה-macros הישנים THROW/CATCH
האם לא מעורבבים קודי שגיאה כערך חזרה יחד עם exceptions
האם לא נשאר מצב שבו יש רק AfxMessageBox בלי log

באפליקציה עסקית חשוב לא רק להציג הודעת שגיאה במסך, אלא גם להשאיר log, היסטוריית פעולות, ערכי קלט ומצב חיבור חיצוני.

באפליקציות MFC ישנות, לפעמים השגיאה נגמרת ב-AfxMessageBox בלבד.

AfxMessageBox(_T("השמירה נכשלה"));

כדי לשפר תחזוקה, עדיף להפריד בין תצוגת UI לבין רישום ל-log.

LogError(_T("Save failed"), path);
AfxMessageBox(_T("השמירה נכשלה. בדקו את ה-log."));

30. איך משלבים MFC עם C++ מודרני

זה שמתחזקים אפליקציית MFC לא אומר שחייבים לכתוב הכול בסגנון C++ ישן.

אפשר לכבד את המוסכמות של MFC בשכבת ה-UI, ובמקביל לכתוב את ה-domain logic ואת החישובים ב-C++ מודרני.

למשל מחלקים כך.

שכבת MFC
  CDialog
  CView
  CDocument
  CString
  message map
  פעולות resource

שכבה בלי MFC
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  מחלקות שאפשר לבדוק ב-unit tests
  business logic

הצורה הגרועה היא שכל הטיפול דחוס במחלקת ה-dialog.

void CMainDialog::OnBnClickedExecute()
{
    // קריאת קלט
    // קריאת קובץ
    // תקשורת
    // חישוב
    // עדכון DB
    // עדכון המסך
    // כתיבה ל-log
    // טיפול ב-exceptions
}

קוד כזה קשה לשנות, קשה לבדוק, וקשה לחקור באגים.

שיפור אפשרי הוא להוציא את הלוגיקה ממחלקות 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, כמו שהיא, לרוב קשה לבדוק ב-unit tests.

הסיבה היא ש-UI, Win32, קבצים, תקשורת, מסד נתונים ומצב גלובלי נוטים להיות קשורים חזק.

דרך החשיבה כדי להקל על בדיקות היא זו.

לא לנסות לבדוק ישירות CDialog או CView
קודם מוציאים לוגיקה בלי UI
ממירים טיפוסי MFC בגבול
שמים קבצים ותקשורת מאחורי interface
משאירים את ה-UI event handlers דקים

למשל מעבירים טיפול למחלקת 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 ב-test framework רגיל של C++.

אין צורך לבנות מחדש את כל אפליקציית MFC בבת אחת. גם רק להוציא טיפול שניתן לבדיקה מתוך event handlers כבר עוזר.

32. לקבע את סביבת ה-build

בתחזוקת אפליקציית MFC, קיבוע סביבת ה-build חשוב.

ב-codebase ישן, ההבדלים האלה יכולים לשנות את תוצאת ה-build.

גרסת Visual Studio
גרסת MSVC toolset
גרסת Windows SDK
האם יש components של MFC/ATL
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
לינק סטטי של MFC / shared DLL
הגדרת runtime library
precompiled headers

באפליקציות MFC לפעמים הרבה תלויות מתרכזות ב-stdafx.h או ב-pch.h.

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

build יכול להישבר כי לקובץ אחד יש הגדרות קומפילציה שונות, או כי הגדרת precompiled headers לא מיושרת.

בפרויקט תחזוקה, כדאי לרשום את המידע הזה ב-README.

גרסת Visual Studio הנדרשת
ה-workloads וה-individual components הנדרשים
Windows SDK הנדרש
פלטפורמת היעד
אופן הלינק של MFC
שלבי ה-build
איך מריצים CI
איך בונים את חבילת ההפצה

הצעד הראשון בתחזוקת MFC הוא להפסיק להסתפק בזה ש-build עובר אצלכם במחשב.

33. לבנות MFC ב-CI

גם אפליקציית MFC אפשר לבנות ב-CI.

אבל בסביבת CI צריך שיהיה מותקן ה-component של MFC.

אם משתמשים ב-Visual Studio Build Tools, צריך להתקין עם ציון ה-component IDs של MFC/ATL. ה-IDs הם אותם IDs שבטבלה בפרק 5.

נקודות שכדאי לבדוק.

האם MFC מותקן ב-Build Tools
האם ה-toolset תואם לפרויקט
האם Windows SDK מותקן
האם בודקים גם build של x86 וגם של x64
האם resource compiler רץ
האם יש שלב חתימה
האם גם יצירת installer נכנסת ל-CI

באפליקציית MFC לא קל לבצע אוטומציה עד UI tests, אבל האוטומציה הזו כבר מועילה.

build של Debug / Release
build של x86 / x64
ניתוח סטטי
unit tests
יצירת installer
שמירת hash של ה-artifacts
בדיקת DLLs תלויים

בתחזוקה ארוכת טווח, עצם השמירה על מצב שבו אפשר לבנות כבר שווה הרבה.

34. איפה להסתכל כשמדבגים אפליקציית MFC

כשרודפים אחרי באג באפליקציית MFC, הסדר הזה יעיל.

1. באיזה מסך
2. האם זה dialog, View או Frame
3. מה ה-resource ID שמתאים לפעולה
4. לאיזו פונקציה הולכים ב-message map
5. האם כיוון UpdateData נכון
6. אם זה Document/View, מה מצב ה-Document
7. האם command routing זרם למחלקה אחרת
8. האם worker thread נוגע ב-UI
9. האם exception או שגיאה נבלעים רק ב-AfxMessageBox
10. האם יש בעיית אתחול או lifetime ייחודית ל-release build

למשל, לבאג שבו לוחצים על כפתור ולא קורה כלום בודקים כאן.

האם ה-IDC של הכפתור נכון
האם קיים ON_BN_CLICKED
האם ה-signature של ה-handler נכון
האם זה לא dialog resource אחר
האם הכפתור לא מנוטרל
האם UpdateData לא נכשל באמצע
האם exception לא נבלע

אם אי אפשר ללחוץ על menu, בודקים כאן.

האם ON_UPDATE_COMMAND_UI מנטרל
האם אין כפילות command ID
האם ה-view הפעיל הוא הצפוי
איפה ה-handler: Frame / View / Document / App

ב-MFC האירוע על פני השטח והטיפול בפועל מחוברים ב-macros וב-routing, ולכן עד שמתרגלים כדאי לשרטט את נתיב הקריאה. האיור בפרק 9 הוא הצורה הבסיסית של הנתיב הזה.

אם רוצים לכוון לפי סימפטום, כדאי קודם להסתכל בטבלה בפרק 35. היא מקשרת מלכודות נפוצות למקום שבו בודקים כל אחת.

35. מלכודות נפוצות

הנה מלכודות נפוצות בתחזוקת MFC.

מחפשים רק קריאות לפונקציה בלי להסתכל על message map
מניחים ש-CWnd* שאינו null הוא תקף
מבלבלים בין lifetime של HWND לבין lifetime של אובייקט C++
טועים בכיוון של UpdateData(TRUE/FALSE)
לא שמים לב ש-ON_UPDATE_COMMAND_UI מנטרל
לא שמים לב להתנגשות IDs ב-resource.h
נוגעים ב-UI ישירות מ-worker thread
המרה בין CString ל-std::string יוצרת ג'יבריש
שוברים קוד שמניח MBCS במעבר ל-Unicode
שוכחים AFX_MANAGE_STATE ב-MFC DLL
שוברים COM/ActiveX שמניחים x86 במעבר ל-x64
טועים ב-select/release של אובייקטי GDI
layout בקואורדינטות קבועות נשבר ב-high DPI

לכל אחת משייכים איזה סימפטום נפוץ ואיפה בודקים. נקודת הכניסה ל-debug תואמת את הסדר בפרק 34.

מלכודת סימפטום נפוץ איפה בודקים
מחפשים רק קריאות לפונקציה בלי להסתכל על message map רץ בלי שמוצאים caller מחפשים BEGIN_MESSAGE_MAP. פרק 9, שלב 4 בפרק 34
מניחים ש-CWnd* שאינו null הוא תקף נופל מדי פעם, נופל אחרי שסוגרים מסך מעבירים דרך ::IsWindow(pWnd->GetSafeHwnd()). פרק 8
מבלבלים בין lifetime של HWND לבין lifetime של אובייקט C++ access violation אחרי סגירת dialog האם שומרים את ערך החזרה של GetDlgItem. פרק 22
טועים בכיוון של UpdateData קלט לא נכנס, ערך התחלתי לא מופיע במסך מיקום הקריאות ל-UpdateData(TRUE) ול-UpdateData(FALSE). פרק 13, שלב 5 בפרק 34
לא שמים לב ש-ON_UPDATE_COMMAND_UI מנטרל כפתור או menu אפורים ואי אפשר ללחוץ ה-handler של ON_UPDATE_COMMAND_UI. פרק 11, אי אפשר ללחוץ על menu בפרק 34
לא שמים לב להתנגשות IDs ב-resource.h dialog או פריט menu אחר מגיב משווים IDs בין resource.h ל-.rc. פרק 16
נוגעים ב-UI ישירות מ-worker thread נפילות לא יציבות שקשה לשחזר, לפעמים hang האם הפונקציה שמועברת ל-AfxBeginThread נוגעת ב-UI. פרק 23
המרה בין CString ל-std::string יוצרת ג’יבריש רק טקסט ביפנית נשבר, או שהסוף נחתך הגדרת character set של הפרויקט ומקומות ההמרה. פרק 18
שוברים קוד שמניח MBCS במעבר ל-Unicode אי-התאמה באורך buffer, קבצים קיימים לא נקראים האם משתמשים ב-sizeof כמספר תווים. פרק 26
שוכחים AFX_MANAGE_STATE ב-MFC DLL לא נמצאים dialog או string resource בתוך ה-DLL תחילת פונקציות ה-export של ה-DLL. פרק 24
שוברים COM/ActiveX שמניחים x86 במעבר ל-x64 נכשל בהפעלה או בתצוגה רק ב-build של 64-bit האם רישום COM נעשה רק בצד 32-bit. פרק 27
טועים ב-select ובשחרור של אובייקטי GDI אחרי ריצה ארוכה הציור נשבר האם עמודת GDI objects ב-Task Manager ממשיכה לעלות. פרק 20
layout בקואורדינטות קבועות נשבר ב-high DPI טקסט נחתך רק בהגדלה של 150% הגדרת DPI ב-manifest וציור בקואורדינטות קבועות. פרק 28

באגים ב-MFC לפעמים לא מובנים מתוך תחביר C++ בלבד. צריך להסתכל גם על messages של Windows, resources, handles, מודולים והגדרות runtime.

36. האם לבחור MFC בפיתוח חדש

האם לבחור MFC בפיתוח חדש לגמרי כדאי לשקול בזהירות.

אם יש סיבה לבחור MFC, אלה המקרים.

צריך שילוב הדוק עם קוד MFC קיים
רוצים לעשות reuse לרכיבי MFC או למסכים קיימים
צריך שליטה קרובה מאוד ל-Win32/GDI/COM
בארגון יש מספיק מיומנות תחזוקה של MFC
היעד מוגבל ל-desktop של Windows
בתוכנית מעבר ארוכת טווח צריך קודם להרחיב ב-MFC

להפך, במקרים האלה עדיף לבחון אפשרות אחרת.

רוצים UI מודרני
צריך layout גמיש או אנימציות
המרכז הוא שילוב Web או ענן
שמים דגש על קלות בדיקה
רוצים טכנולוגיה שקל יותר למפתחים צעירים להיכנס אליה
צריך תמיכה ב-cross-platform
רוצים לשים דגש מראש על נגישות ועל high DPI

MFC הוא לא טכנולוגיה שאין טעם ללמוד עכשיו, וגם לא טכנולוגיה שבוחרים כי היא חדשה. זו טכנולוגיה כדי לעבוד מול נכסי Windows native קיימים.

37. איך לחשוב על מעבר מ-MFC

כשרוצים להעביר אפליקציית MFC לטכנולוגיה אחרת, אם מכוונים מיד להחלפה מלאה קל להיכשל.

קודם מפרקים את האפליקציה כך.

UI
business logic
פורמט קבצים
טיפול בתקשורת
טיפול במסד נתונים
בקרת התקנים
הדפסה
שילוב COM/OLE
ניהול הגדרות
log

מתוך אלה, מה שתלוי ביותר ב-MFC הוא ה-UI.

לעומת זאת, business logic וטיפול בקבצים לפעמים אפשר להוציא החוצה.

הסדר המציאותי למעבר הוא זה.

1. משחזרים את סביבת ה-build
2. מקבעים את ההתנהגות הקיימת בנתוני בדיקה
3. מוציאים לוגיקה מ-UI event handlers
4. מקרבים לספריית C++ בלי MFC
5. מוסיפים בדיקות אוטומטיות
6. מתעדים את המפרט החיצוני
7. מחליפים בהדרגה מהמסכים שצריך

במקום להציב מטרה לעזוב את MFC, עדיף להציב מטרה להוציא החוצה את הלוגיקה החשובה שכלואה בתוך MFC. כך הסיכוי להצליח גבוה יותר.

38. מאיפה מתחילים לקרוא קוד MFC

כשקוראים לראשונה פרויקט MFC קיים, מתחילים מהקבצים האלה.

*.vcxproj
  מסתכלים על toolset, הגדרות MFC, character set והגדרות runtime

resource.h
  מסתכלים על resource IDs

*.rc
  מסתכלים על dialogs, menus, מחרוזות ו-icons

*App.cpp / *App.h
  מסתכלים על המחלקה הנגזרת מ-CWinApp ועל InitInstance

MainFrm.cpp / MainFrm.h
  מסתכלים על ה-main frame ועל menu/toolbar

*Doc.cpp / *Doc.h
  אם זה Document/View, מסתכלים על מבנה הנתונים ועל השמירה

*View.cpp / *View.h
  מסתכלים על ציור ועל פעולות משתמש

*Dlg.cpp / *Dlg.h
  מסתכלים על dialog, 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 handlers דקים
לא מדליפים CString ו-CWnd יותר מדי לשכבות בלי UI
מעבירים business logic למחלקות C++ רגילות
בונים בדיקות תאימות לפורמט קבצים
מבהירים את ההבדל בין x86 ל-x64
עושים review לשינוי resource IDs
בודקים module state ב-MFC DLL
מסדרים log
מקבעים build ב-CI
בודקים מסכים באופן קבוע ב-high DPI וב-Windows 11

במיוחד חשוב לא לדחוס יותר מדי טיפול ב-CDialog או ב-CView.

מחלקות המסך של MFC מתרכזות בקלט, בתצוגה ובהפצת אירועים.

לוקחים ערכים מהמסך
מעבירים לשירות
מציגים את התוצאה במסך

אם מצליחים לשמור על הרמה הזו, גם MFC נהיה קל הרבה יותר לתחזוקה.

40. checklist מעשי

checklist לטיפול באפליקציית MFC.

האם גרסת Visual Studio מוגדרת בבירור
האם ה-components של MFC/ATL מותקנים
האם יעדי x86/x64 מוגדרים בבירור
האם מבינים את הגדרות Unicode/MBCS
האם MFC בלינק סטטי או ב-shared DLL
איזה Visual C++ Redistributable נדרש
האם resource.h ו-.rc נכנסים ל-review
האם עוקבים אחרי נתיב האירוע דרך message map
האם כיוון UpdateData נכון
האם משתמשים במבנה Document/View
האם ניהול ה-lifetime של dialog מסוג modeless בטוח
האם לא נוגעים ב-UI ישירות מ-worker thread
האם יש מקומות ב-MFC DLL שצריכים AFX_MANAGE_STATE
האם בודקים מסכים בסביבת high DPI
האם תלויות COM/ActiveX תומכות ב-32-bit/64-bit
האם נשאר log
האם אפשר לבדוק לוגיקה בלי UI

MFC נראה ייחודי עד שמתרגלים אליו, אבל ברגע שיודעים איפה להסתכל, אפשר לקרוא אותו באופן די שיטתי.

41. סיכום

MFC הוא framework ותיק לבניית אפליקציות desktop native של Windows ב-C++. הוא כבר לא הבחירה המרכזית לפיתוח חדש, אבל בתחזוקת קוד קיים, בעדכון סביבת build, בהוספת פיצ’רים ובמעבר הדרגתי, זו עדיין טכנולוגיה חשובה.

הנקודות החשובות במיוחד להבנת MFC הן אלה.

MFC הופך את Win32 API לנוח יותר לעבודה מ-C++
CWinApp מנהל את האפליקציה כולה
ה-lifetime של CWnd ושל HWND אינו זהה
message map קושר אירועים לפונקציות
Document/View הוא מנגנון שמפריד נתונים מתצוגה
DDX/DDV משמשים לסנכרון ערכים ב-dialog ולבדיקתם
קבצי resource ו-resource.h חשובים מאוד
מבינים CString ו-TCHAR יחד עם הגדרת character set
ב-MFC DLL שמים לב ל-module state
באפליקציות MFC ישנות יש ערך לבדיקה מחדש מזווית high DPI, x64, CI ובדיקות

כשעובדים עם MFC, במקום לקבוע שזה ישן ולכן גרוע, קודם חשוב להבין את המבנה. באפליקציות MFC קיימות לפעמים דחוסים שנים של ידע עסקי, מפרטים לפי לקוח, שילוב התקנים ותאימות קבצים. כדי לשמור על הערך הזה ולהקל בהדרגה על התחזוקה, הדרך המציאותית היא להבין את המוסכמות של MFC, להוציא לוגיקה בלי UI, ולהכין build ובדיקות.

אם צריך משפט אחד, זה המשפט.

תשתית לעבודה עם מנגנוני אפליקציות Windows native דרך מחלקות C++ ו-framework.

עם נקודת המבט הזו, MFC הוא לא רק טכנולוגיה ישנה, אלא מפתח לקריאה בטוחה של נכסי Windows קיימים.

מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מה זה MFC?
MFC הוא קיצור של Microsoft Foundation Classes, framework לאפליקציות Windows שהופך את Win32 API לנוח יותר לעבודה כמחלקות C++. חלון מיוצג ב-CWnd, dialog ב-CDialog, והאפליקציה כולה ב-CWinApp. זה לא ספריית קסם שמאפשרת לכתוב בלי לדעת Windows, אלא ארגון של מנגנוני Windows לטיפוסים ול-framework של C++. לכן צריך גם ידע ב-Win32, כמו Windows messages ו-handles.
אפשר עדיין להשתמש ב-MFC? האם יש תמיכה?
MFC עדיין זמין ב-Visual Studio ועדיין נתמך. עם זאת, בתיעוד של Microsoft יש הערה שלא יתווספו פיצ'רים חדשים ושלא יעודכן התיעוד. בפועל זה נפוץ בתחזוקה של אפליקציות MFC קיימות, בהוספת פיצ'רים, בעדכון סביבת build ובמעבר הדרגתי ל-UI אחר. לאימוץ באפליקציית GUI כללית חדשה לגמרי כדאי לשקול בזהירות. צריך להתקין את ה-component הבודד ב-Visual Studio Installer (C++ MFC for latest build tools).
מה זה message map ב-MFC?
זה המנגנון ב-MFC שקושר Windows messages ו-commands לפונקציות handler. בין BEGIN_MESSAGE_MAP ל-END_MESSAGE_MAP כותבים עם macros כמו ON_BN_CLICKED ו-ON_COMMAND את ההתאמה: כשלוחצים על הכפתור הזה, קוראים לפונקציה הזו. אם חיפוש לא מוצא קריאה ישירה לפונקציה, אבל היא רצה באירוע, בודקים את ה-message map. ב-code review של MFC חשוב לבדוק את פונקציית ה-handler יחד עם ה-message map.
איך עוברים מ-MFC לטכנולוגיה אחרת?
אם מכוונים מיד להחלפה מלאה, קל להיכשל. הסדר המציאותי: משחזרים את סביבת ה-build, מקבעים את ההתנהגות הקיימת בנתוני בדיקה, מוציאים לוגיקה מ-UI event handlers לספריית C++ בלי MFC, מוסיפים בדיקות אוטומטיות, מתעדים את המפרט החיצוני, ואז מחליפים מסכים בהדרגה לפי הצורך. במקום להציב מטרה לעזוב את MFC, עדיף להוציא החוצה את הלוגיקה החשובה שכלואה בתוך MFC.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג