Win32 против MFC - часть II - Оконная процедура MFC

ОГЛАВЛЕНИЕ

Оконная процедура MFC

После завершения предыдущего раздела пора задать важный вопрос: "Где находится оконная процедура в приложении MFC?" Ответ несколько сложен.

Оконная процедура в приложении Win32 устанавливается путем присвоения адреса оконной процедуры члену lpfnWndProc из WNDCLASSEX. То же самое происходит в приложении MFC. Но это присваивание может происходить в нескольких местах, в зависимости от того, как создано приложение.

Оконная процедура каркаса приложения, являющаяся основой приложения MFC, называется AfxWndProc, и ее прототип создается так же, как и у знакомой оконной процедуры Win32. Эта оконная функция реализована следующим образом:

LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg,
                            WPARAM wParam, LPARAM lParam)
{
    //специальное сообщение, обозначающее окно как использующее
    AfxWndProc
        if (nMsg == WM_QUERYAFXWNDPROC)
            return 1;

    // все остальные сообщения проходят через карту сообщений
    CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
    ASSERT(pWnd != NULL);
    ASSERT(pWnd->m_hWnd == hWnd);

    return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}

Как видно, AfxWndProc не производит отправку сообщений. Но он вызывает функцию AfxCallWndProc, которая, в свою очередь, вызывает CWnd::WindowProc,  вызывающую метод CWnd::OnWndMsg(...). Фактическая отправка сообщений производится в этой функции, кроме WM_COMMAND или WM_NOTIFY! В случае WM_COMMAND OnWndMsg вызывает функцию OnCommand, а в случае сообщения WM_NOTIFY вызывается функция OnNotify, обе из которых вызывают CCmdTarget::OnCmdMsg(...) для осуществления отправки:

Что понимается под фактической отправкой, и где вступает в действие карта сообщений? Как MFC понимает, что при получении WM_PAINT надо вызвать функцию OnPaint?
Чтобы понять все это, надо знать, что MFC не направляет сообщения по идентификатору сообщения. То есть MFC не опирается на идентификаторы сообщения (WM_PAINT, WM_MOVE и WM_SIZE) при вызове функций-обработчиков. Вместо этого он переключает по прототипу (или сигнатуре) функции-обработчика сообщения.

Если создать диалоговое приложение MFC и назвать его sample, то мастер MFC сгенерирует следующую карту сообщений для класса диалога:

BEGIN_MESSAGE_MAP(CSampleDlg, CDialog)
    //{{AFX_MSG_MAP(CSampleDlg)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

Внимательно посмотрите на эту автоматически сгенерированную карту сообщений! Есть запись, скажем, ON_WM_PAINT, определенная так:

#define ON_WM_PAINT() \
    {WM_PAINT, 0, 0, 0, AfxSig_vv, \
    (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))&OnPaint},

где AfxSig_vv - сигнатура функции обработчика сообщения.

AfxSig_vv означает "Сигнатура каркаса приложения, возвращающая значение void и принимающая параметр void". То есть она гласит, что функция OnPaint имеет следующий прототип:

void OnPaint(void);

Что происходит в случае WM_SIZE? Сигнатура его функции-обработчика определяется как AfxSig_vwii, так как прототип OnSize заявляет это:

void OnSize(UINT nType, int cx, int cy)

поэтому AfxSig_vwii означает, что функция-обработчик возвращает значение void, в то же время, принимая 3 параметра, UINT, int и int. Макрос ON_WM_SIZE определен следующим образом:

#define ON_WM_SIZE() \
    { WM_SIZE, 0, 0, 0, AfxSig_vwii, \
    (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, int, int))&OnSize },

Где определены все эти сигнатуры? Они объявлены в заголовочном файле Afxmsg_.h под именем каталога включения компилятора. Уже было сказано, что MFC переключает по сигнатуре функции-обработчика, а не по ее идентификатору сообщения. Если вы внимательно посмотрите на функцию-член OnWndMsg из CWnd (объявлена в ..\MFC\SRC\WinCore.cpp), то быстро выясните, как MFC отправляет сообщения.

Сначала MFC вызывает функцию GetMessageMap, получающую карту сообщений класса окна. Эта функция возвращает структуру типа AFX_MSGMAP, имеющую две записи:

struct AFX_MSGMAP
{
#ifdef _AFXDLL
    const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
    const AFX_MSGMAP* pBaseMap;
#endif
    const AFX_MSGMAP_ENTRY* lpEntries;
};

Где _AFXDLL показывает, как MFC DLL должны быть подключены к программе - в общей DLL или в статической библиотеке. В то же время, AFX_MSGMAP_ENTRY объявляется так:

struct AFX_MSGMAP_ENTRY
{
    UINT nMessage;  // сообщение окна
    UINT nCode; // контрольный код или код WM_NOTIFY
    UINT nID;       // контрольный идентификатор (или 0 для сообщений windows)
    UINT nLastID;   // применяется для записей, задающих диапазон контрольных идентификаторов
    UINT nSig;  // тип сигнатуры (действие) или указатель на сообщение #
    AFX_PMSG pfn;   // процедура для вызова (или специальное значение)
};

Ниже показано, что происходит при получении сообщения WM_PAINT. Внутри функции-члена CWnd::OnWndMsg извлекается карта сообщений (путем вызова функции GetMessageMap(...)), и член lpEntries структуры AFX_MSGMAP заполняется следующими значениями:

nMessage = 15
nCode = 0
nID = 0
nLastID = 0
nSig = 12
pfn = CSampleDlg::OnPaint()

Отсюда следует, что при получении сообщения WM_PAINT (определенного как 0x000F) вызывается функция-обработчик OnPaint() с сигнатурой 12 (AfxSig_vv). Затем производится переключение по nSig, которая в данном примере равна AfxSig_vv:

union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;

Switch(nSig)
{
    //обрезано
case AfxSig_vv:
    (this->*mmf.pfn_vv)();
    break;
    // обрезано
}

Поэтому вызывается функция CSampleDlg::OnPaint. А как насчет MessageMapFunctions? MessageMapFunctions является union(объединение), обозначающим указатель на функцию всех типов, pfn_bb, pfn_vv, pfn_vw и не только.

Возникает вопрос, почему MFC отправляет по коду сигнатуры, а не по идентификатору сообщения.

Чтобы ответить на этот вопрос, надо внимательно посмотреть на WM_SIZE и на то, как оно обрабатывается не в MFC. При каждом изменении размера окна сообщение WM_SIZE отправляется приложению через его оконную процедуру:

LRESULT CALLBACK WindowProc(HWND hwnd, // описатель окна
    UINT uMsg, // WM_SIZE
    WPARAM wParam, // флаг изменения размера
    LPARAM lParam); // клиентская область

Где wParam указывает тип запрошенного изменения размера. Младший разряд lParam указывает новую ширину клиентской области, а старший разряд lParam указывает новую высоту клиентской области. Теперь представьте, что происходит, если программисту MFC приходится вручную преобразовывать все эти параметры в осмысленные аргументы? В данном случае ему придется взять старший разряд и/или младший разряд lParam, чтобы получить ширину и высоту новой клиентской области. Но при работе с другими сообщениями требуется больше обработки. Более того, было бы ужасно обрабатывать все это вручную, с точки зрения программиста MFC.

Чтобы преодолеть эту проблему, MFC сперва упаковывает каждый аргумент функции-обработчика в более безопасный с точки зрения типов объект, а затем вызывает функцию-обработчик с безопасными с точки зрения типов аргументами. То есть MFC преобразует параметры wParam и lParam в более осмысленные значения. В случае WM_SIZE прототип функции-обработчика выглядит так:

void OnSize(UINT nType, int cx, int cy)

Отсюда следует, что MFC преобразовал wParam и lParam в 3 параметра, nType, cx и cy, с которыми программисту удобней работать.