Win32 против MFC - Часть I

ОГЛАВЛЕНИЕ

Данная статья рассматривает архитектуру документ/вид и структуру обертки MFC.

Введение

Польза MFC упрощает жизнь программиста. Встает вопрос: "Что такое архитектура документ/вид?" Неясно, что это такое, и как MFC инкапсулирует Win32 API внутри. Возникает второй вопрос: "Где функция WinMain находится в приложении MFC?"

Основы приложений Win32

Программа Windows содержит минимум две функции: WinMain и wndProc. Каждой программе Windows нужна функция WinMain, являющаяся точкой входа. Вторая функция является оконной процедурой, обрабатывающей сообщения окна (Хотя название «оконная процедура» не слишком блестящее, так как в действительности она обрабатывает любые сообщения, соответствующие программе).
Сначала рассматривается устройство WinMain и его функциональность, а затем разбирается wndProc.

Функция WinMain делится на три части: объявление процедуры, инициализация программы и цикл обработки сообщений.

Когда программа начинает выполняться, Windows передает ей некоторую информацию, включая, в том числе, описатель текущего экземпляра приложения и описатель предыдущего экземпляра, при его наличии. Рассмотрим Win32 API, сгенерированный компилятором MSVC++:

int APIENTRY WinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)

Первый параметр, hInstance, объявляется как int и является описателем выполняющегося в данный момент экземпляра приложения.
Второй параметр, hPrevInstance, является описателем ранее выполнявшегося экземпляра того же самого приложения. Однако в 32-битном API Windows NT этот параметр всегда равен NULL, независимо от выполнения нескольких экземпляров одного и того же приложения. Причина этого лежит в том, что в 32-битном Windows API каждая копия каждой программы выполняется в своем адресном пространстве и не делит ничего с другими выполняющимися в данный момент экземплярами из соображений безопасности.

Третий параметр, lpCmdLine, содержит параметры командной строки для программы.

Последний параметр, nCmdShow, используется для объявления стиля главного окна при первом запуске программы. Он сообщает Windows, как отобразить главное окно –  т.е. развернутым, свернутым и тому подобное.

Возвращаемое значение функции имеет прототип APIENTRY, определенный следующим образом:

#define APIENTRY WINAPI
и, в свою очередь, WINAPI объявлен так:
#define WINAPI __stdcall

А что такое __stdcall? К сожалению, ответ выходит за рамки данной статьи, поэтому он пропущен и оставлен читателю.

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

Инициализация предусматривает вызов трех процедур Windows API: RegisterClass (или ее расширенная версия RegisterClassEx), CreateWindow (или ее расширенный аналог CreateWindowEx) и ShowWindow (для этого API нет расширения).

Чтобы создать окно, надо заполнить члены структуры WNDCLASSEX и передать экземпляр этой структуры в RegisterClassEx API таким образом:

WNDCLASSEX wcl;
wcl.cbSize = sizeof(WNDCLASSEX);
wcl.hInstance = hInstance;
wcl.lpfnWndProc = (WNDPROC)wndProc;
wcl.style = CS_HREDRAW | CS_VREDRAW;
wcl.hIcon = LoadIcon(hInstance, IDC_ARROW);
wcl.hIconSm = NULL;
wcl.hCursor = LoadCursor(NULL, IDC_ARROW);
wcl.lpszMenuName = NULL;
wcl.cbClsExtra = 0;
wcl.cbWndExtra = 0;
wcl.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wcl.lpszClassName = "myClass";

if(!RegisterClassEx(&wcl))
    return 0;

В результате этого Windows копирует элементы этой структуры куда-то в базу данных класса. Когда программа хочет создать окно, она ссылается на запись в базе данных класса, а затем Windows использует эти данные для создания окна. Ловко, да?

Сейчас пора рассмотреть члены этой структуры, но поскольку в ней есть много членов, выяснение функциональности каждого члена оставлено читателю. Однако нужен один из ее членов, так как он указывает на оконную процедуру - вторую функцию, которая будет в программе. Да, это член lpfnWndProc. Но подождите! Сначала надо закончить часть инициализации программы функции WinMain. После регистрации данных класса вызывается API CreateWindowEx для создания фактического окна.

HWND hWnd = CreateWindow("myClass",
    "WindowTitle",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    NULL,
    NULL,
    hInstance,
    NULL);

Затем демонстрируется окно с помощью API ShowWindow следующим образом:

ShowWindow (hWnd, nCmdShow);

В этом и заключается суть инициализации программы. Теперь вернемся к члену lpfnWndProc структуры WNDCLASSEX. Этот член является "длинным указателем на функцию, именуемую оконной процедурой". Этот член был присвоен функции wndProc, поэтому в программе будет объявлена функция по имени wndProc, обрабатывающая сообщения окна.

Пока перейдем от инициализации программы к циклу обработки сообщений, третьей и последней части функции WinMain.
Каждая программа Windows имеет цикл обработки сообщений, позволяющий ей непрерывно опрашивать на наличие сообщений. Таким образом, каждая программа Windows остается на связи с подачей сообщений, важных для правильной работы программы Windows. Ниже приведен типичный цикл обработки сообщений:

MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

Этот цикл выполняется непрерывно до получения сообщения WM_QUIT. Как только это сообщение получено, программа прерывает свой цикл обработки сообщений и завершается. Достаточно сказать, что каждая итерация через вышеуказанный цикл означает получение сообщения из очереди аппаратных событий или из очереди сообщений приложения.

Цикл обработки сообщений состоит из трех основных API: GetMessage, втаскивающего сообщения в программу, TranslateMessage, преобразующего все сообщения нажатия клавиш в правильные значения символов и помещающего их в очередь приватных сообщений приложения как сообщение WM_CHAR, и последнего API DispatchMessage, проталкивающего извлеченное сообщение (msg) в оконную процедуру для обработки.

Имея данную информацию, изучим функциональность оконной процедуры. Помните, что уже был присвоен член lpfnWndProc структуры WNDCLASSEX в wndProc. Теперь посмотрим, как wndProc выглядит:

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

Где LRESULT объявлен как тип данных long, и CALLBACK задает соглашение о вызовах __stdcall.

Оконная процедура вызывается всегда, когда генерируется сообщение. При необходимости можно обработать сообщение или пропустить сообщение и передать его API DefWindowProc. DefWindowProc делает все, что нужно для обеспечения работы окна. Microsoft предоставил исходный код этой процедуры в Windows SDK (комплект для разработки ПО). Кстати, при получении WM_DESTROY вызывается API PostQuitMessage, обрабатывающий процесс завершения работы приложения.


Основы приложения MFC

Приложение MFC инкапсулирует большую часть Win32 API, упрощая жизнь каждого программиста. Есть много книг и статей о точке входа приложения MFC. Все они в той или иной степени  заявляют, что точкой входа приложения MFC является функция InitInstance программы.
Это порождает новый вопрос: «Где находится функция WinMain, если функция InitInstance является точкой входа?»

Чтобы выяснить, что происходит внутри, создайте программу-пример, чтобы изучить и просмотреть детали, скрытые от программиста MFC. Для создания программы-примера запустите MSVC++ 6.0. В меню «Файл» выберите пункт «Новый». Выбрав вкладку «Проект», выделите Мастер приложений MFC (.exe) и в поле редактирования имени проекта введите "sdisample" и нажмите кнопку Ok. Выберите «Отдельный документ» и нажмите кнопку «Завершить». Нажмите Ok, чтобы позволить мастеру создать каркас приложения.

На первый взгляд выяснится, что приложение содержит следующие классы:
•    CAboutDlg
•    CMainFrame
•    CSdiSampleApp
•    CSdiSampleDoc
•    CSdiSampleView

Один из этих классов по имени CSdiSampleApp унаследован от класса CWinApp:

Вышеуказанный класс имеет функцию-член по имени InitInstance, называемую точкой входа приложения. Встает вопрос, почему эта функция называется точкой входа приложения MFC? Ответ на данный вопрос лежит за архитектурой MFC. Глупо, да?

Было сказано, что каждая программа Windows содержит две функции: WinMain и wndProc. То же самое относится к приложениям MFC. Они тоже имеют функцию WinMain и wndProc.

Запустите программу путем нажатия кнопки F10. Вы вскоре выясните, что выполнение программы останавливается на следующей функции:

extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR lpCmdLine,
    int nCmdShow)
{
    // вызывается общая/экспортированная WinMain
    return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}

Посмотрите внимательно! Функция имеет знакомые параметры, такие же, как у функции WinMain. Но что за мерзость объявлена перед WINAPI? extern "C" сообщает компилятору, как компилировать эту функцию, и WINAPI определено следующим образом:

#define WINAPI __stdcall

А что насчет _tWinMain? Она объявлена следующим образом:

#define _tWinMain WinMain

Оказались снова в том же самом месте! Здесь рассмотренная ранее функция WinMain:

int APIENTRY WinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)

а здесь функция, сгенерированная MFC:

extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR lpCmdLine,
    int nCmdShow)

Сравнение этих двух функций показывает, что они одинаковые. Ловко, да? Теперь тщательней рассмотрим функцию _tWinMain и ее реализацию:

extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR lpCmdLine,
    int nCmdShow)
{
    // вызывается общая/экспортированная WinMain
    return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}

Как видно, она вызывает и возвращает функцию AfxWinMain, функцию WinMain каркаса приложения! Но какой она является и как реализована? Ниже приведена важная часть функции AfxWinMain:

int AFXAPI AfxWinMain(HINSTANCE hInstance, 
                      HINSTANCE hPrevInstance,
                      LPTSTR lpCmdLine,
                      int nCmdShow)
{
    //Обрезано
    //внутренняя реализация AFX
    if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
        goto InitFailure;

    // Глобальные инициализации приложения (редкие)
    if (pApp != NULL && !pApp->InitApplication())
        goto InitFailure;

    // Выполняются специфические инициализации
    if (!pThread->InitInstance())
    {
        if (pThread->m_pMainWnd != NULL)
        {
            TRACE0("Warning: Destroying non-NULL m_pMainWnd\n");
            pThread->m_pMainWnd->DestroyWindow();
        }
        nReturnCode = pThread->ExitInstance();
        goto InitFailure;
    }

    nReturnCode = pThread->Run();

    //Обрезано
}

Сначала AfxWinMain вызывает AfxWinInit, функцию, вызывающую MFCO42D.DLL (если программа выполняется в отладочной версии), а также инициализирующую переменные-члены надлежащим именем файла выполнения, файла справки и файла .ini. (Правда, AfxWinMain делает немного больше, но это опущено, потому что MFC тут не переделывается).

Затем AfxWinMain вызывает функцию InitApplication, которую можно легко переопределить в классе CSdiSampleApp с помощью мастера класса. Эта функция используется для выполнения однократной инициализации приложения, и через несколько строк она вызывает функцию InitInstance, названную точкой входа приложения MFC. Затем функция AfxWinMain вызывает функцию «Выполнить»! Эта функция вызывает надлежащую функцию CWinApp::Run(), которая в свою очередь вызывает функцию CWinThread::Run().
В этом методе представлен цикл обработки сообщений, реализованный следующим образом:

int CWinThread::Run()
{
    ASSERT_VALID(this);

    // для отслеживания состояния простоя
    BOOL bIdle = TRUE;
    LONG lIdleCount = 0;

    // получает и отправляет сообщения, пока не
    // получено сообщение WM_QUIT.
    for (;;)
    {
        // фаза 1: проверяется, можно ли выполнить работу простоя
        while (bIdle && !::PeekMessage
            (&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
        {
            // вызывается OnIdle во время пребывания в состоянии bIdle
            if (!OnIdle(lIdleCount++))
                bIdle = FALSE; // принимается состояние отсутствия простоя
        }

        // фаза 2: качаются сообщения, пока доступны
        do
        {
            // качается сообщение, но завершается при WM_QUIT
            if (!PumpMessage())
                return ExitInstance();

            // сбрасывается состояние отсутствия простоя после
            // перекачки сообщения "нормальный"
            if (IsIdleMessage(&m_msgCur))
            {
                bIdle = TRUE;
                lIdleCount = 0;
            }
        } while (::PeekMessage(&m_msgCur,
            NULL, NULL, NULL, PM_NOREMOVE));
    }

    ASSERT(FALSE); // недоступно
}

Как видно, эта функция содержит цикл, и этот цикл прервется, если и только если будет получено сообщение WM_QUIT, и, следовательно, обладает функциональностью вышеназванного цикла обработки сообщений:

MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

Однако между двумя этими циклами есть ряд отличий. Первое и самое важное заключается в том, что CWinThread::Run() вызывает функцию-член OnIdle приложения, когда программе нечего обрабатывать. Во-вторых, он вызывает метод ExitInstance после получения сообщения WM_QUIT и перед выходом из программы. Итак, программист MFC может сделать все, что нужно путем переопределения метода ExitInstance, следя, чтобы этот код вызывался всегда, когда программа собирается завершиться.

PumpMessage, с другой стороны, инкапсулирует API TranslateMessage и DispatchMessage внутри. Теперь имеется цикл обработки сообщений для приложения MFC. Однако еще ничего не было сказано о wndProc приложения MFC. Эта функция является достоинством MFC и будет рассмотрена во второй части данной статьи.