Win32 против MFC - часть II

ОГЛАВЛЕНИЕ

В данной части рассматривается оконная процедура в приложении MFC и как команда MFC построила ее для программистов MFC.

Введение

В первой части серии было описано, как MFC инкапсулирует функцию WinMain, и как она создает цикл обработки сообщений внутри. Более того, было сказано, что все программы Windows имеют минимум две функции, WinMain и wndProc, но wndProc детально не разбиралась.

Если вы еще не прочитали первую часть статьи, начните с нее, поскольку она посвящена основам приложений Win32 и MFC.

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

Оконная процедура (или оконная функция) является определенной в приложении функцией, обрабатывающей сообщения, отправленные окну. Сообщения варьируются от сообщений, генерируемых мышью, до сообщений, отправляемых приложению другим процессом. Эта специальная функция не вызывается ни программой, ни MFC, она вызывается Windows.

Программа ждет, пока Windows не отправит ей сообщение. При получении сообщения (через заданную функцию обратного вызова) программа должна предпринять соответствующее действие путем выполнения некоторых процессов, вызова одного-двух API и предоставления конечному пользователю функциональности, которую программа должна предоставлять.
В терминологии Windows функция обратного вызова является функцией, вызываемой Windows. Оконная процедура является прекрасным примером функции обратного вызова, прототип которой создается следующим образом:

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

Где LRESULT объявлен как тип данных long, и CALLBACK задает соглашение о вызовах __stdcall (Чтобы увидеть это, подведите курсор к нужному типу данных и нажмите F12 при взаимодействии в VC++ 6.0).

В первой части статьи написано, как работает оконная процедура и что она должна делать. Но для простоты чтения здесь снова приведена функция обратного вызова:

LRESULT CALLBACK wndProc(HWND hWnd, UINT message,
                         WPARAM wParam, LPARAM lParam)
{
    switch(message)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(hWnd, message, wParam,
            lParam);
    }
    return 0;
}

Эта функция, как сказано выше, вызывается всегда, когда Windows генерирует сообщение, чтобы можно было обработать сообщение или передать его оконной процедуре по умолчанию путем вызова функции DefWindowProc. Функция DefWindowProc вызывает оконную процедуру по умолчанию, чтобы обеспечить стандартную обработку любых сообщений окна, которые приложение не обрабатывает. Эта функция обеспечивает обработку всех сообщений и, что неудивительно, вызывается с такими же параметрами, полученными оконной процедурой.

При получении WM_DESTROY вызывается PostQuitMessage, говорящий системе, что поток сделал запрос на завершение. Это прерывает цикл обработки сообщений, и программа успешно завершается.


Оконная процедура 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, с которыми программисту удобней работать.