לדלג לתוכן

חלון תור ולולאת הודעות

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

נתחיל בלי framework כדי לראות מי קורא למי. אחרי שנבין את הלולאה, יהיה קל יותר להבין מה WPF או WinUI מנהלים בשבילנו בפרקים הבאים.

הכתובת של חלון אינה האובייקט שלנו

הhandle מסוג HWND מזהה חלון במערכת. הוא אינו מצביע לclass של C++, והוא אינו COM reference. השמירה שלו במשתנה אינה מונעת הריסת חלון. גם בדיקת IsWindow ואחריה שימוש אינן נעילה על חיי החלון; בין שתי הפעולות הוא יכול להיהרס, ואפילו ערך handle יכול לשמש מאוחר יותר חלון אחר.

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

בונים חלון שמחזיק מצב

הקובץ window.cpp הוא תוכנית מלאה. נבנה אותה כתוכנית console כדי שאפשר יהיה להוסיף הדפסות בזמן הניסוי. הentry point אינו מה שהופך אותה לתוכנית שמותר לה ליצור חלונות.

#define UNICODE
#define _UNICODE
#include <windows.h>
#include <strsafe.h>
#include <cstdio>

struct AppState { DWORD clicks = 0; };

LRESULT CALLBACK window_proc(HWND window, UINT message,
                             WPARAM wparam, LPARAM lparam) noexcept {
    auto state = reinterpret_cast<AppState*>(GetWindowLongPtrW(window, GWLP_USERDATA));
    if (message == WM_NCCREATE) {
        auto create = reinterpret_cast<CREATESTRUCTW*>(lparam);
        state = static_cast<AppState*>(create->lpCreateParams);
        if (!state) return FALSE;
        SetLastError(ERROR_SUCCESS);
        const LONG_PTR previous = SetWindowLongPtrW(
            window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
        if (!previous && GetLastError() != ERROR_SUCCESS) return FALSE;
        return TRUE;
    }
    switch (message) {
    case WM_LBUTTONDOWN:
        if (state) {
            ++state->clicks;
            InvalidateRect(window, nullptr, FALSE);
        }
        return 0;
    case WM_PAINT: {
        PAINTSTRUCT paint{};
        HDC dc = BeginPaint(window, &paint);
        if (dc) {
            FillRect(dc, &paint.rcPaint, GetSysColorBrush(COLOR_WINDOW));
            RECT client{};
            GetClientRect(window, &client);
            wchar_t text[80]{};
            StringCchPrintfW(text, 80, L"Click inside the window. Count: %lu",
                             state ? state->clicks : 0);
            SetBkMode(dc, TRANSPARENT);
            DrawTextW(dc, text, -1, &client, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
        }
        EndPaint(window, &paint);
        return 0;
    }
    case WM_CLOSE:
        DestroyWindow(window);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_NCDESTROY:
        SetWindowLongPtrW(window, GWLP_USERDATA, 0);
        break;
    }
    return DefWindowProcW(window, message, wparam, lparam);
}

int main() {
    HINSTANCE instance = GetModuleHandleW(nullptr);
    const wchar_t class_name[] = L"AmitTechCourseWindow";
    WNDCLASSEXW klass{sizeof(klass)};
    klass.lpfnWndProc = window_proc;
    klass.hInstance = instance;
    klass.lpszClassName = class_name;
    klass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
    if (!RegisterClassExW(&klass)) return 1;
    AppState state;
    HWND window = CreateWindowExW(0, class_name, L"AmitTech window lab",
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 360,
        nullptr, nullptr, instance, &state);
    if (!window) { UnregisterClassW(class_name, instance); return 2; }
    ShowWindow(window, SW_SHOW);
    int exit_code = 0;
    for (;;) {
        MSG message{};
        const BOOL result = GetMessageW(&message, nullptr, 0, 0);
        if (result == -1) {
            std::fprintf(stderr, "GetMessage error=%lu\n", GetLastError());
            DestroyWindow(window);
            exit_code = 3;
            break;
        }
        if (result == 0) { exit_code = static_cast<int>(message.wParam); break; }
        TranslateMessage(&message);
        DispatchMessageW(&message);
    }
    UnregisterClassW(class_name, instance);
    return exit_code;
}
cl /std:c++20 /EHsc /W4 window.cpp user32.lib gdi32.lib
window.exe

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

הלולאה אינה בודקת רק true או false

הפונקציה GetMessage יכולה להחזיר הודעה, סיום בעקבות WM_QUIT, או שגיאה שמיוצגת ב־1-. לכן לולאה שמתייחסת לכל ערך שאינו אפס כהצלחה עלולה להמשיך עם הודעה לא תקינה. אנחנו בודקים את שלושת המצבים בנפרד, בהתאם לחוזה GetMessage.

הפונקציה TranslateMessage מסייעת לתרגם הודעות מקלדת להודעות תווים; היא אינה מפעילה את WndProc. הפעולה DispatchMessage היא שמנתבת את ההודעה לפונקציית החלון המתאימה. הודעת WM_QUIT שייכת ללולאה של הthread ואינה נשלחת לWndProc כמו הודעת חלון רגילה.

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

מתי המצב שלנו נעשה נגיש

הקריאה לCreateWindowExW יכולה להפעיל את WndProc עוד לפני שהיא חוזרת. לכן אנחנו מעבירים את כתובת AppState כפרמטר יצירה ומחברים אותה כבר בWM_NCCREATE. אי אפשר להסתמך על השמה למשתנה גלובלי שתקרה רק אחרי קבלת הHWND.

המצב המקומי בmain חי עד סוף לולאת ההודעות, והחלון נהרס לפני סוף חייו. בWM_NCDESTROY אנחנו מנתקים את הקישור. זהו חוזה חיים פשוט ומפורש; אם נעביר בעלות על state לחלון ונקצה אותו דינמית, נצטרך לטפל גם בכשל יצירה באמצע ובשחרור יחיד במסלול הריסה.

הפונקציה מסומנת noexcept והפעולות שבה אינן זורקות חריגות C++. אם נוסיף containers, callbacks או קוד עסקי שעלולים לזרוק, נצטרך לתפוס חריגות בגבול ולהחליט על מדיניות כשל. אסור לתת לexception לנדוד דרך callback של מערכת ההפעלה ולצפות שלקוח ABI זר יטפל בו.

למה לא מציירים בתוך הלחיצה

הלחיצה משנה את המודל ומסמנת אזור כלא מעודכן. בהמשך WM_PAINT מאפשר לצייר את המצב הנוכחי. כמה בקשות invalidation יכולות להתאחד; אין התחייבות לקריאת paint אחת לכל לחיצה.

הזוג BeginPaint וEndPaint מנהל את הdevice context של פעולת הציור ואת אזור העדכון. אם נטפל בהודעת paint בלי לאמת את האזור, נוכל לקבל עוד הודעות paint שוב ושוב. מצד שני, ציור חד פעמי מחוץ למסלול הזה יכול להיעלם כשחלון אחר מסתיר אותנו. נבחן את אזור העדכון ואת חוזה BeginPaint.

שליחה לעומת הוספה לתור

הפעולה SendMessage היא קריאה סינכרונית מבחינת השולח, ויכולה להפעיל WndProc לפני שהשולח ממשיך. במעבר בין threadים היא יכולה גם להכניס תלות המתנה. אם נחזיק lock שהנמען צריך, נוכל ליצור את אותו מעגל שפגשנו בCOM.

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