Операционная система Microsoft Windows 3.1 для программиста© Александр Фролов, Григорий ФроловТом 11, М.: Диалог-МИФИ, 1993, 269 стр. 1.5. Приложение с обработкой сообщенийВ этом разделе мы рассмотрим простейшее приложение Windows, содержащее цикл обработки сообщений. Это приложение имеет только одно окно и одну функцию окна, однако это только начало. Алгоритм работы приложенияЕсли вы, забегая вперед, посмотрите листинги файлов нашего приложения, то обратите внимание на то, что оно имеет несколько необычную (с точки зрения программиста, составляющего программы для MS-DOS) структуру. В частности, функция WinMain после выполнения инициализирующих действий входит в цикл обработки сообщений, после выхода из которого работа приложения завершается. Функция WndProc вообще не вызывается ни из какой другой функции приложения, хотя именно она выполняет всю "полезную" работу. Составляя программы для MS-DOS, вы привыкли к тому, что за весь сценарий работы программы отвечает функция main. Эта функция выполняет вызов всех остальных функций (за исключением функций обработки прерываний), из которых и состоит программа. Логика работы приложения Windows другая. Прежде всего выполняются инициализирующие действия, связанные, например, с определением классов, на базе которых в дальнейшем (или сразу) будут создаваться окна приложения. Для каждого класса необходимо указать адрес функции окна. Эта функция будет обрабатывать сообщения, направляемые окнам, создаваемым на базе класса. Однако процесс распределения сообщений функциям окон, созданных приложением, происходит не сам по себе. Приложение должно само организовать этот процесс, для чего после выполнения инициализирующих действий в функции WinMain запускается цикл обработки сообщений. Обработка сообщений, которые операционная система Windows посылает в очередь приложения (во время цикла обработки сообщений), выполняется соответствующей функцией окна. Наше первое приложение с обработкой сообщений определяет один класс окна и на его базе создает одно, главное окно. Для обработки сообщений, поступающих в это окно, приложение определяет одну функцию окна. Функция окна обрабатывает три сообщения с кодами WM_LBUTTONDOWN, WM_RBUTTONDOWN и WM_DESTROY. Сообщение WM_LBUTTONDOWN записывается в очередь приложения и передается функции окна, когда вы устанавливаете курсор мыши внутри главного окна приложения и нажимаете левую клавишу мыши. В ответ на это сообщение функция окна выводит диалоговую панель с сообщением о том, что нажата левая клавиша мыши. Сообщение WM_RBUTTONDOWN аналогично предыдущему, но оно записывается в очередь сообщений приложения, когда вы нажимаете правую кнопку мыши. В ответ на это сообщение функция выводит диалоговую панель с сообщением и дополнительно выдает звуковой сигнал. Последнее сообщение, WM_DESTROY, передается приложению при разрушении структуры данных, связанной с окном. В нашем примере при завершении работы приложения разрушается главное (и единственное) окно. В ответ на это сообщение функция окна помещает в очередь приложения специальное сообщение с идентификатором WM_QUIT. Выборка этого сообщения в цикле обработки сообщений приводит к завершению цикла и, соответственно, к завершению работы приложения. Схематически алгоритм работы функции WinMain приложения можно представить следующим образом: if(приложение уже было запущено) Завершение работы текущей копии приложения; Создание класса окна; Создание главного окна приложения на основе созданного ранее класса; Отображение окна на экране; while(в очереди нет сообщения WM_QUIT) Выборка сообщения и распределение его функции окна; Завершение работы приложения; Адрес функции окна указывается при создании класса окна. Этот адрес используется операционной системой Windows для вызова функции окна (напомним, что приложение само не вызывает функцию окна). Приведем алгоритм работы функции окна нашего простейшего приложения: switch(Идентификатор сообщения) { case WM_LBUTTONDOWN: Вывод диалоговой панели с сообщением о том, что была нажата левая кнопка мыши; case WM_RBUTTONDOWN: Выдача звукового сигнала; Вывод диалоговой панели с сообщением о том, что была нажата правая кнопка мыши; case WM_DESTROY: Запись в очередь приложения сообщения WM_QUIT, при выборке которого завершается цикл обработки сообщений; } Вызов функции DefWindowProc, обрабатывающей все остальные сообщения, поступающие в функцию окна; Из приведенного выше алгоритма работы функции окна видно, что наша функция обрабатывает только три сообщения. Все остальные сообщения, которые попадают в очередь приложения и распределяются функции окна (а их очень много), также должны быть обработаны. Для этого необходимо использовать функцию программного интерфейса Windows с именем DefWindowProc. Если ваша функция окна проигнорирует вызов функции DefWindowProc для тех сообщений, которые она сама не обрабатывает, Windows не сможет обработать такие сообщения. Это может привести к неправильной работе или блокировке как приложения, так и всей операционной системы Windows. Листинги файлов приложенияВ отличие от первого приложения (листинг 1.1) новое приложение состоит из двух файлов - файла window.cpp (листинг 1.2), содержащего исходный текст функций приложения, и файла window.def (листинг 1.3), который является файлом определения модуля. В файле window.cpp определены функции WinMain, InitApp и функция окна WndProc, то есть все функции, из которых состоит приложение. Файл window.def содержит инструкции редактору связей. Эти инструкции используются при создании загрузочного exe-файла приложения. Рассмотрим подробно файл window.cpp. Листинг 1.2. Файл window\window.cpp // ---------------------------------------- // Простейшее приложение Windows // с циклом обработки сообщений // ---------------------------------------- #define STRICT #include <windows.h> #include <mem.h> // Прототипы функций BOOL InitApp(HINSTANCE); LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM); // Имя класса окна char const szClassName[] = "WindowAppClass"; // Заголовок окна char const szWindowTitle[] = "Window Application"; // ===================================== // Функция WinMain // Получает управление при запуске // приложения // ===================================== #pragma argsused int PASCAL WinMain(HINSTANCE hInstance, // идентификатор текущей // копии приложения HINSTANCE hPrevInstance, // идентификатор предыдущей // копии приложения LPSTR lpszCmdLine, // указатель на командную // строку int nCmdShow) // способ отображения // главного окна приложения { MSG msg; // структура для работы с сообщениями HWND hwnd; // идентификатор главного окна приложения // Проверяем, не запускалось ли это приложение ранее if(!hPrevInstance) { // Если не запускалось, вызываем функцию InitApp // для инициализации приложения. // Если инициализацию выполнить не удалось, // завершаем приложение if(!InitApp(hInstance)) return FALSE; } // Если данное приложение уже работает, // выводим сообщение о том, что допускается // запуск только одной копии приложения, и // затем завершаем работу приложения else { MessageBox(NULL, "Можно запускать только одну копию приложения", "Ошибка", MB_OK | MB_ICONSTOP); return FALSE; } // После успешной инициализации приложения создаем // главное окно приложения hwnd = CreateWindow( szClassName, // имя класса окна szWindowTitle, // заголовок окна WS_OVERLAPPEDWINDOW, // стиль окна CW_USEDEFAULT, // задаем размеры и расположение CW_USEDEFAULT, // окна, принятые по умолчанию CW_USEDEFAULT, CW_USEDEFAULT, 0, // идентификатор родительского окна 0, // идентификатор меню hInstance, // идентификатор приложения NULL); // указатель на дополнительные // параметры // Если создать окно не удалось, завершаем приложение if(!hwnd) return FALSE; // Рисуем окно. Для этого после функции ShowWindow, // рисующей окно, вызываем функцию UpdateWindows, // посылающую сообщение WM_PAINT в функцию окна ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // Запускаем цикл обработки сообщений while(GetMessage(&msg, 0, 0, 0)) { DispatchMessage(&msg); } // Возвращаем значение WParam, переданное // в качестве параметра функции PostQuitMessage // в процессе инициирования завершения работы // приложения из функции окна. // Затем завершаем работу приложения return msg.wParam; } // ===================================== // Функция InitApp // Вызывается из функции WinMain для // инициализации приложения. // Выполняет регистрацию класса окна // ===================================== BOOL InitApp(HINSTANCE hInstance) { ATOM aWndClass; // атом для кода возврата WNDCLASS wc; // структура для регистрации // класса окна memset(&wc, 0, sizeof(wc)); // Стиль окна wc.style = 0; // Указатель на функцию окна, обрабатывающую // сообщения, предназначенные для всех окон, // созданных на основе данного класса wc.lpfnWndProc = (WNDPROC) WndProc; // Размер дополнительной области данных, // зарезервированной в описании класса окна wc.cbClsExtra = 0; // Размер дополнительной области данных, // зарезервированной для каждого окна, // созданного на основе данного класса wc.cbWndExtra = 0; // Идентификатор приложения, которое // создало данный класс wc.hInstance = hInstance; // Идентификатор пиктограммы, используемой // для окна данного класса wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); // Идентификатор курсора, используемого // для окна данного класса wc.hCursor = LoadCursor(NULL, IDC_ARROW); // Цвет фона окна wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // Идентификатор меню wc.lpszMenuName = (LPSTR)NULL; // Имя, которое присваивается создаваемому // классу и используется при создании // окон данного класса wc.lpszClassName = (LPSTR)szClassName; // Регистрация класса aWndClass = RegisterClass(&wc); // Возвращаем результат регистрации класса return (aWndClass != 0); } // ===================================== // Функция WndProc // НЕ ВЫЗЫВАЕТСЯ ни из одной функции приложения. // Эту функцию вызывает Windows в процессе // обработки сообщений. Для этого адрес функции WndProc // указывается при регистрации класса окна. // Функция выполняет обработку сообщений главного // окна приложения // ===================================== LRESULT CALLBACK _export WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { // Выполняем обработку сообщений. Идентификатор // сообщения передается через параметр msg switch (msg) { // Это сообщение приходит, когда вы поместили курсор // мыши в область главного окна приложения и нажали // левую клавишу мыши case WM_LBUTTONDOWN: { MessageBox(NULL, "Нажата левая клавиша мыши", "Сообщение", MB_OK | MB_ICONINFORMATION); return 0; } // Это сообщение приходит, когда вы поместили курсор // мыши в область главного окна приложения и нажали // правую клавишу мыши case WM_RBUTTONDOWN: { MessageBeep(-1); // звуковой сигнал MessageBox(NULL, "Нажата правая клавиша мыши", "Сообщение", MB_OK | MB_ICONINFORMATION); return 0; } // Это сообщение приходит, когда вы завершаете // работу приложения стандартным для // Windows способом case WM_DESTROY: { // Инициируем завершение работы приложения, // помещая в очередь приложения сообщение // WM_QUIT. Это приведет к завершению // цикла обработки сообщений в функции WinMain PostQuitMessage(0); return 0; } } // Все сообщения, которые не обрабатываются нашей // функцией окна, ДОЛЖНЫ передаваться функции // DefWindowProc return DefWindowProc(hwnd, msg, wParam, lParam); } Определения типов, констант и функцийПервые две строки используются для определения типов данных и констант: #define STRICT #include <windows.h> Определение символа STRICT, сделанное до включения файла windows.h, позволит выполнить более строгую проверку типов данных. Просмотрев исходный текст файла windows.h (этот файл находится в подкаталоге include каталога, в который вы выполняли установку транслятора), вы обнаружите, что способ определения многих типов и функций сильно зависит от того, был ли ранее определен символ STRICT. Далее в программе приведены прототипы двух функций - InitApp и WndProc. После прототипов определены две строки символов, содержащие имя класса окна (szClassName) и заголовок окна (szWindowTitle). Имя класса окна используется при создании класса окна, а заголовок нужен, разумеется, для того, чтобы озаглавить создаваемое главное окно приложения. Так как мы в приложении не собираемся изменять эти строки, они описаны как const. Функция WinMain определена так же, как и в нашем самом первом приложении (листинг 1.1.). В области стека функции созданы две переменные с именами msg и hwnd: MSG msg; // структура для работы с сообщениями HWND hwnd; // идентификатор главного окна приложения Переменная msg представляет собой структуру типа MSG, описанную в файле windows.h следующим образом: typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG; Эта переменная предназначена для временного хранения сообщений и используется в цикле обработки сообщений. Переменная hwnd имеет тип HWND, также описанный в файле windows.h, и используется для хранения идентификатора (handle) главного окна приложения. Заметьте, что в структуре MSG также присутствует поле с типом HWND. Это поле используется для хранения идентификатора окна, к которому относится сообщение. В операционной системе Windows очень широко практикуется использование идентификаторов для ссылки на различные ресурсы. При определении символа STRICT все такие идентификаторы имеют различный тип, поэтому вы не сможете по ошибке выполнить присвоение значений идентификаторов, относящихся к разным ресурсам. Вы уже знакомы с двумя типами идентификаторов - идентификатор копии приложения HINSTANCE и идентификатор окна HWND. Существуют десятки других типов идентификаторов, о которых мы будем рассказывать по мере изучения программного интерфейса Windows. Инициализация приложенияПервое, что делает функция WinMain после запуска приложения, это проверяет наличие уже запущенной ранее копии этого же приложения: if(!hPrevInstance) { if(!InitApp(hInstance)) return FALSE; } else { MessageBox(NULL, "Можно запускать только одну копию приложения", "Ошибка", MB_OK | MB_ICONSTOP); return FALSE; } Как мы уже говорили, параметр hPrevInstance функции WinMain содержит нуль, если приложение запустили в первый раз, или идентификатор предыдущей копии приложения, запущенной ранее и работающей на момент запуска текущей копии. Если параметр hPrevInstance равен нулю, функция WinMain вызывает функцию инициализации приложения InitApp. В противном случае на экран выводится сообщение о невозможности запуска второй копии приложения. Вы можете разрешить запуск нескольких копий приложения, убрав соответствующую проверку. В данном случае нет никаких причин запрещать параллельную работу нескольких приложений. Мы сделали это только для иллюстрации использования параметра hPrevInstance . Обратите внимание на вызов функции MessageBox. В качестве последнего параметра указано значение MB_OK | MB_ICONSTOP. Последний параметр функции представляет собой набор битовых флагов, которые мы рассмотрим позже. Флаг MB_OK предназначен для создания на диалоговой панели кнопки с надписью "OK", флаг MB_ICONSTOP нужен для изображения пиктограммы с надписью "STOP" (рис. 1. 10).
Рис. 1.10. Сообщение об ошибке Регистрация класса окнаЗадача функции InitApp - регистрация класса окна. На базе этого класса будет создано главное окно приложения. Если допускается одновременная работа нескольких копий одного приложения, регистрация класса окна должна выполняться только один раз первой копией приложения. В области локальных переменных функции определены две переменные - aWndClass и wc: ATOM aWndClass; // атом для кода возврата WNDCLASS wc; // структура для регистрации // класса окна Переменная aWndClass используется для временного хранения кода возврата функции RegisterClass. Эта функция относится к функциям программного интерфейса Windows, она и выполняет регистрацию класса. В качестве единственного параметра функции необходимо указать адрес соответствующим образом подготовленной структуры типа WNDCLASS: aWndClass = RegisterClass(&wc); Приведем прототип функции RegisterClass: ATOM WINAPI RegisterClass(const WNDCLASS FAR* lpwc); Таким образом, процедура регистрации класса окна является несложной. Вам достаточно подготовить одну структуру и вызвать функцию RegisterClass. Для вас, возможно, непривычно использование переменной специального типа ATOM для передачи результата выполнения функции. Однако такое использование не создает никаких дополнительных трудностей. Тип ATOM отображается на тип UINT, который, в свою очередь, отображается на тип unsigned int (см. файл windows.h): typedef UINT ATOM; typedef unsigned int UINT; Переменные типа ATOM используются как идентификаторы текстовых строк (атомы), хранящихся в области памяти, принадлежащей операционной системе Windows. Существует набор функций для работы с этими идентификаторами (для работы с атомами), который мы сейчас не будем рассматривать. Отметим только, что в этом наборе есть функции для получения адреса строки, соответствующей идентификатору, для создания и удаления, а также поиска идентификаторов. В нашем приложении функция InitApp использует переменную типа ATOM для формирования кода возврата: return (aWndClass != 0); Если регистрация класса произошла успешно, функция RegisterClass возвращает атом с ненулевым значением, при этом функция InitApp возвращает значение TRUE. Последнее означает, что инициализация приложения выполнена без ошибок. Теперь займемся структурой WNDCLASS, которая используется для регистрации класса окна. Эта структура определена в файле windows.h: typedef struct tagWNDCLASS { UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCSTR lpszMenuName; LPCSTR lpszClassName; } WNDCLASS; Перед регистрацией вам необходимо заполнить все поля в этой структуре. Поле style определяет стиль класса и задается в виде констант (описанных, как всегда, в файле windows.h), имя которых начинается с префикса CS_, например CS_HREDRAW, CS_VREDRAW. Стиль задает реакцию окна на изменение его размера, на выполнение в окне операции двойного щелчка мышью (double click), а также позволяет определить другие характеристики окна, создаваемого на базе данного класса. Например, если для стиля задать значение CS_HREDRAW | CS_VREDRAW, при изменении вертикального или горизонтального размера окна приложение должно его перерисовать, то есть нарисовать заново все или часть того, что было изображено в окне до изменения размера. В нашем приложении стиль класса не используется, поэтому для него задается нулевое значение: wc.style = 0; В поле lpfnWndProc необходимо записать адрес функции окна, которая будет выполнять обработку сообщений, поступающих во все окна, созданные на базе данного класса. Имя функции окна можно выбрать любое. В нашем случае используется имя WndProc, хотя вы можете использовать другое имя. Запись адреса функции окна должна выполняться следующим образом: wc.lpfnWndProc = (WNDPROC) WndProc; Поле lpfnWndProc имеет тип WNDPROC (дальний указатель на функцию), который мы рассмотрим чуть позже, при описании функции окна. Для того чтобы избежать получения от компилятора предупреждающего сообщения о несоответствии типов, вы должны использовать явное преобразование типа. Поле cbClsExtra используется для резервирования дополнительной памяти, общей и доступной для всех окон, создаваемых на базе данного класса. Чтобы это было понятно, отметим, что при регистрации класса окна в памяти, принадлежащей операционной системе Windows, резервируется и заполняется некоторая область (структура данных). В этой области хранится вся информация о классе, необходимая для создания окон на базе этого класса. Вы можете увеличить размер области описания класса для хранения своей собственной информации, предназначенной для всех создаваемых на базе этого класса окон. Поле cbClsExtra определяет размер дополнительной памяти в байтах. В программном интерфейсе Windows имеются специальные функции, предназначенные для работы с дополнительной областью памяти. Наше приложение не создает в описании класса никаких дополнительных областей, поэтому для заполнения поля cbClsExtra используется нулевое значение: wc.cbClsExtra = 0; Так же как и при создании нового класса, при создании нового окна Windows резервирует в своей памяти область, описывающую окно. С помощью параметра cbWndExtra вы можете увеличить размер этой области для хранения информации, имеющей отношение к создаваемому окну. В нашем случае размер области описания окна не увеличивается, поэтому для заполнения поля cbWndExtra используется нулевое значение: wc.cbWndExtra = 0; Поле hInstance перед регистрацией класса окна должно содержать идентификатор приложения, создающего класс окна. В качестве этого идентификатора следует использовать значение, полученное функцией WinMain в параметре hInstance: wc.hInstance = hInstance; Следующее поле имеет имя hIcon и тип HICON. Это идентификатор пиктограммы, в которую превращается окно при уменьшении его размеров до предела (при минимизации окна). В нашем приложении мы указываем пиктограмму, используемую Windows по умолчанию. Для Microsoft Windows версии 3.1 вид этой пиктограммы приведен на рис. 1.11.
Рис. 1.11. Пиктограмма приложения Для загрузки пиктограммы в приложении вызывается функция программного интерфейса Windows с именем LoadIcon: wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); Прототип функции LoadIcon: HICON WINAPI LoadIcon(HINSTANCE hinst, LPCSTR pszicon); Первый параметр функции (hinst) содержит идентификатор приложения, второй (pszicon) - имя ресурса-пиктограммы. Позже мы научим вас определять для окон собственные пиктограммы, нарисованные с помощью приложения Resource Workshop, входящего в комплект поставки Borland C++ for Windows. В поле hCursor (имеющем тип HCURSOR) вы можете задать вид курсора мыши при его прохождении над окном. Вы знаете, что курсор мыши меняет свою форму при перемещении над различными окнами приложений Windows. При регистрации класса окна вы можете указать форму курсора, для чего и используется поле hCursor. В нашем приложении мы задаем курсор в виде стандартной стрелки, для чего вызываем функцию LoadCursor и указываем в качестве второго параметра константу IDC_ARROW: wc.hCursor = LoadCursor(NULL, IDC_ARROW); Прототип функции LoadCursor: HCURSOR WINAPI LoadCursor(HINSTANCE hinst, LPCSTR pszCursor); Вы можете определить для окна свой курсор, нарисовав его аналогично пиктограмме при помощи такого приложения, как Resource Workshop или Microsoft SDKPaint. Однако пока для простоты мы будем использовать стандартный курсор. Далее нам необходимо заполнить поле hbrBackground, имеющее тип HBRUSH. Это поле позволяет определить кисть (brush), которая будет использована для закрашивания фона окна. В качестве кисти можно использовать "чистые" цвета или небольшую пиктограмму размером 8 х 8 точек. В нашем приложении мы использовали системный цвет, который Windows использует для закрашивания фона окон: wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); Системный цвет можно изменять при помощи приложения Control Panel. Позже мы научим вас задавать для фона окна другие цвета и раскрашивать окно при помощи пиктограмм. Поле lpszMenuName (указатель на строку типа LPCSTR) определяет меню, располагающееся в верхней части окна. Если меню не используется, при заполнении этого поля необходимо использовать значение NULL: wc.lpszMenuName = (LPSTR)NULL; Тип LPCSTR определяется как константный дальний указатель на строку символов: typedef const char FAR* LPCSTR; Очень важно поле lpszClassName. В это поле необходимо записать указатель на текстовую строку, содержащую имя регистрируемого класса окон: wc.lpszClassName = (LPSTR)szClassName; На этом подготовку структуры wc к регистрации класса окна можно считать законченной. Можно вызывать функцию RegisterClass. После регистрации функция InitApp возвращает управление обратно в функцию WinMain. Создание главного окна приложенияДалее приложение вызывает функцию CreateWindow для того, чтобы создать главное окно приложения: hwnd = CreateWindow( szClassName, // имя класса окна szWindowTitle, // заголовок окна WS_OVERLAPPEDWINDOW, // стиль окна CW_USEDEFAULT, // задаем размеры и расположение CW_USEDEFAULT, // окна, принятые по умолчанию CW_USEDEFAULT, CW_USEDEFAULT, 0, // идентификатор родительского окна 0, // идентификатор меню hInstance, // идентификатор приложения NULL); // указатель на дополнительные // параметры В случае успеха функция CreateWindow возвращает идентификатор окна (типа HWND). Если окно создать не удалось, функция возвращает нулевое значение. Приведем прототип функции CreateWindow: HWND CreateWindow(LPCSTR lpszClassName, LPCSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hwndParent, HMENU hmenu, HINSTANCE hinst, void FAR* lpvParam); Многочисленные параметры функции CreateWindow дополняют описание окна, сделанное при создании класса окна. Первый параметр функции (lpszClassName) - указатель на строку, содержащую имя класса, на базе которого создается окно. В процессе инициализации приложения мы зарегистрировали класс с именем "WindowAppClass" (переменная szClassName). Второй параметр функции (lpszWindowName) - указатель на строку, содержащую заголовок окна (Title Bar). В нашем случае окно будет иметь заголовок "Window Application" (переменная szWindowTitle). Третий параметр (dwStyle) - стиль создаваемого окна. Этот параметр задается как логическая комбинация отдельных битов. Константа WS_OVERLAPPEDWINDOW соответствует окну, которое может перекрывать другие окна, имеет заголовок, системное меню, кнопки для минимизации и максимизации окна, а также рамку вокруг окна, с помощью которой можно изменять размер окна. Операционная система Windows позволяет задавать различные стили для создаваемых окон. Мы их рассмотрим в дальнейшем. Четвертый и пятый параметры функции CreateWindow для окна данного стиля определяют горизонтальную (x) и вертикальную (y) координату относительно верхнего левого угла экрана видеомонитора. Шестой и седьмой параметры определяют ширину (nWidth) и высоту (nHeight) создаваемого окна. Наше приложение в качестве координат окна и его размеров использует константу CW_USEDEFAULT. При этом операционная система Windows сама определяет положение и размеры создаваемого окна. Восьмой параметр (hwndParent) определяет индекс родительского окна. Для нашего приложения в качестве значения используется нуль, так как в приложении создается только одно окно. Девятый параметр (hmenu) - идентификатор меню или идентификатор порожденного (child) окна. В нашем случае никакого меню или порожденного окна нет, поэтому в качестве значения используется нуль. Десятый параметр (hinst) - идентификатор приложения, которое создает окно. Необходимо использовать значение, передаваемое функции WinMain через параметр hInstance. Одиннадцатый, последний параметр функции (lpvParam) представляет собой дальний указатель на область данных, определяемых приложением. Этот параметр передается в функцию окна вместе с сообщением WM_CREATE при создании окна. Наше приложение не пользуется этим параметром. Отображение окна на экранеИтак, окно создано. Однако на экране оно еще не появилось, в чем вы можете убедиться, запустив приложение под управлением отладчика. Поэтому, проверив, что создание окна выполнено успешно (функция CreateWindow вернула ненулевое значение), необходимо сделать окно видимым (нарисовать его на экране). Это можно сделать с помощью функции ShowWindow: ShowWindow(hwnd, nCmdShow); Прототип функции: BOOL ShowWindow(HWND hwnd, int nCmdShow); Функция отображает окно, идентификатор которого задан первым параметром (hwnd), в нормальном, максимально увеличенном или уменьшенном до пиктограммы виде, в зависимости от значения второго параметра (nCmdShow). Наше приложение использует в качестве второго параметра значение, передаваемое функции WinMain через параметр nCmdShow. После отображения окна в нормальном или максимально увеличенном виде внутренняя поверхность окна закрашивается кистью, определенной при регистрации класса. Внешний вид окна, создаваемого нашим приложением, показан на рис. 1.12.
Рис. 1.12. Главное окно приложения Сразу после функции ShowWindow в приложении вызывается функция UpdateWindow. UpdateWindow(hwnd); Прототип функции: void UpdateWindow(HWND hwnd); Функция UpdateWindow вызывает функцию окна, заданного идентификатором, передаваемым в качестве параметра hwnd, и передает ей сообщение WM_PAINT. Получив это сообщение, функция окна должна перерисовать все окно или его часть. Наше приложение не обрабатывает это сообщение, передавая его функции DefWindowProc. Сообщение WM_PAINT и способ его обработки будут описаны позже, когда мы займемся рисованием в окне. Цикл обработки сообщенийПосле отображения окна функция WinMain запускает цикл обработки сообщений: while(GetMessage(&msg, 0, 0, 0)) { DispatchMessage(&msg); } Функция GetMessage предназначена для выборки сообщений из очереди приложения и имеет следующий прототип: BOOL GetMessage(LPMSG lpmsg, HWND hwnd, WORD uMsgFilterMin, WORD uMsgFilterMax); Первый параметр функции (lpmsg) является дальним указателем на структуру типа MSG, в которую будет записано выбранное из очереди сообщение. Тип LPMSG определен в файле windows.h следующим образом: typedef MSG FAR* LPMSG; Второй параметр (hwnd) является идентификатором окна, для которого необходимо выбрать сообщение из очереди приложения. Очередь приложения содержит сообщения, предназначенные для всех окон, созданных приложением. Если в качестве второго параметра функции GetMessage указать нуль, будет выполняться выборка всех сообщений, предназначенных для всех окон приложения. В нашем случае приложение создает только одно окно, поэтому можно указать либо идентификатор созданного окна, либо нуль. Третий (uMsgFilterMin) и четвертый (uMsgFilterMax) параметры функции GetMessage позволяют определить диапазон сообщений, выбираемых из очереди приложения, задавая соответственно минимальное и максимальное значение кодов выбираемых сообщений. Если для этих параметров указать нулевые значения (как это сделано в нашем приложении), из очереди приложения будут выбираться все сообщения. Как работает функция GetMessage? Эта функция может выбирать сообщения только из очереди того приложения, которое ее вызывает. Если очередь сообщений приложения пуста или содержит только сообщения с низким приоритетом, функция GetMessage передает управление другим работающим приложениям, обеспечивая невытесняющую мультизадачность. Таким образом, проверяя очередь сообщений, приложение может передать управление другим приложениям. Эти приложения, в свою очередь, тоже вызывают функцию GetMessage. Таким образом, приложения распределяют между собой процессорное время. Выбранное функцией GetMessage сообщение удаляется из очереди сообщений приложения и записывается в структуру, адрес которой задан первым параметром функции. Если из очереди выбирается сообщение с кодом WM_QUIT, функция GetMessage возвращает значение FALSE. В этом случае приложение должно завершить цикл обработки сообщений. При выборке из очереди любых других сообщений функция GetMessage возвращает значение TRUE. После выборки сообщения из очереди в цикле обработки сообщений его необходимо распределить функции окна, для которой это сообщение предназначено. Для этого должна быть использована функция программного интерфейса Windows с именем DispatchMessage. Эта функция имеет следующий прототип: DWORD DispatchMessage(LPMSG lpmsg); Единственный параметр функции (lpmsg) является указателем на структуру, содержащую сообщение. Функция DispatchMessage возвращает значение, полученное при возврате из функции окна. Обычно это значение игнорируется приложением. Даже если ваше приложение содержит только одно окно и одну функцию окна, вы не должны вызывать функцию окна самостоятельно. Функция окна имеет нестандартный пролог и эпилог, поэтому ее прямой вызов может привести к аварийному завершению приложения. Единственный правильный способ вызова функции окна в цикле обработки сообщений - косвенный вызов при помощи функции DispatchMessage. С помощью специальных функций, таких, как SendMessage или CallWindowProc, вы все же можете при необходимости вызвать функцию окна. Однако в цикле обработки сообщений следует использовать именно функцию DispatchMessage, так как она для каждого сообщения вызывает именно ту функцию окна, которой это сообщение предназначено. Завершение работы приложенияПриложение обычно завершает свою работу, когда вы нажимаете комбинацию клавиш <Alt+F4> или выбираете строку "Close" в системном меню главного окна приложения. При этом в его функцию окна попадает сообщение WM_DESTROY. В ответ на это сообщение функция окна помещает в очередь сообщение WM_QUIT, вызывая для этого функцию PostQuitMessage. Как вы уже знаете, выборка этого сообщения приводит к завершению цикла обработки сообщений и, следовательно, к завершению работы приложения. Если операционная система Windows завершает свою работу, функциям окна каждого работающего приложения передается сообщение WM_QUERYENDSESSION. Обрабатывая это сообщение соответствующим образом, приложение может завершить свою работу, предварительно сохранив все необходимые данные. Если, например, вы создали документ в текстовом процессоре, а затем, не сохранив его, попытаетесь завершить работу Windows, текстовый процессор получит сообщение WM_QUERYENDSESSION, в ответ на которое он может попросить вас сохранить документ, отказаться от сохранения или от завершения работы Windows. Функция окнаВ нашем приложении был создан класс окна, в котором определена функция окна с именем WndProc: LRESULT CALLBACK _export WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_LBUTTONDOWN: { MessageBox(NULL, "Нажата левая клавиша мыши", "Сообщение", MB_OK | MB_ICONINFORMATION); return 0; } case WM_RBUTTONDOWN: { MessageBeep(-1); // звуковой сигнал MessageBox(NULL, "Нажата правая клавиша мыши", "Сообщение", MB_OK | MB_ICONINFORMATION); return 0; } case WM_DESTROY: { PostQuitMessage(0); return 0; } } return DefWindowProc(hwnd, msg, wParam, lParam); } Мы уже говорили, что эта функция нестандартна и ее нельзя вызывать напрямую из функции WinMain или из какой-либо другой функции. Функция окна должна иметь следующий прототип (имя функции окна может быть любым, а не только WndProc): LRESULT CALLBACK _export WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); Тип LRESULT определен в файле windows.h следующим образом: typedef signed long LONG; typedef LONG LRESULT; Таким образом, функция окна возвращает значение с типом LONG, что в текущей реализации Windows (версия 3.1) соответствует двойному слову. Модификатор CALLBACK указывает на соглашения о передаче параметров _pascal и определяет функцию окна как функцию _far: #define CALLBACK _far _pascal Функция окна, так же как и функция WinMain (и все функции программного интерфейса Windows), использует для передачи параметров соглашения языка Паскаль. Вы могли бы описать функцию окна как long _far _pascal, однако для обеспечения возможности переноса вашей программы в 32-разрядные версии Windows (Windows NT) или для использования 32-разрядного расширения Win32s текущей версии Windows следует пользоваться символами LRESULT и CALLBACK. Обратите внимание на ключевое слово _export, которое используется в определении функции окна. Если описать функцию с этим ключевым словом, ее имя станет экспортируемым. При этом функция будет иметь специальный пролог и эпилог. Пролог и эпилог экспортируемых функций представляет собой небольшое добавление в начало и конец функции, обеспечивающее возможность вызова функции из ядра операционной системы Windows. В частности, пролог обеспечивает экспортируемой функции доступ к сегменту данных приложения при вызове этой функции из модулей операционной системы Windows. В отличие от обычных функций, вызываемых вашим приложением, функция окна вызывается не приложением, а операционной системой Windows. Для обеспечения возможности такого вызова функция должна быть определена с ключевым словом _export или описана специальным образом в модуле определения файла, который будет рассмотрен позже. Займемся теперь параметрами функции окна. Первый параметр является индексом окна, для которого предназначено сообщение. Напомним, что адрес функции окна указывается при регистрации класса окна: wc.lpfnWndProc = (WNDPROC) WndProc; На базе одного класса может быть создано несколько окон, каждое из которых имеет собственный идентификатор. Для обработки сообщений, поступающих в окна, созданные на базе одного класса, используется общая функция окна. Функция окна может определить окно, для которого предназначено сообщение, анализируя свой первый параметр. В нашем случае имеется только одно окно, поэтому идентификатор окна не используется. Следующие три параметра функции окна соответствуют полям msg, wParam и lParam структуры MSG. В поле msg записывается код сообщения, поля wParam и lParam описывают дополнительную информацию, передаваемую в функцию окна вместе с сообщением. Формат этой информации зависит от кода сообщения. В нашем приложении функция окна представляет собой переключатель, выполняющий различные действия для сообщений с разными кодами. Сообщения WM_LBUTTONDOWN, WM_RBUTTONDOWN и WM_DESTROY обрабатываются функцией окна, остальные передаются функции DefWindowProc. Сообщение WM_LBUTTONDOWN попадает в функцию окна, когда вы устанавливаете курсор мыши в окно приложения и нажимаете левую клавишу мыши. При этом функция окна вызывает функцию MessageBox и на экране появляется диалоговая панель с сообщением о том, что была нажата левая клавиша мыши (рис. 1.13).
Рис. 1.13. Диалоговое окно с сообщением Аналогично сообщение WM_RBUTTONDOWN попадает в функцию окна, когда вы устанавливаете курсор мыши в окно приложения и нажимаете правую клавишу мыши. В этом случае функция окна вызывает функцию MessageBeep с параметром -1 и затем функцию MessageBox для вывода сообщения о том, что была нажата правая клавиша мыши. Приведем прототип функции MessageBeep: void WINAPI MessageBeep(UINT uAlert); Параметр функции MessageBeep позволяет выбрать один из нескольких системных звуковых сигналов:
Функцию MessageBeep удобно использовать для отладки, задавая ей в качестве параметра значение -1. При завершении работы приложения функции окна передается сообщение WM_DESTROY, в ответ на которое функция окна помещает в очередь приложения сообщение WM_QUIT, вызывая функцию PostQuitMessage. При выборке сообщения WM_QUIT завершается цикл обработки сообщений и работа приложения. Все остальные сообщения передаются без изменения функции DefWindowProc для дальнейшей обработки. Обратим ваше внимание на тот факт, что в ответ на сообщение WM_DESTROY приложение помещает в свою собственную очередь сообщение WM_QUIT. При этом получается, что одно сообщение, появившееся в очереди сообщений, порождает другое. Такая практика широко используется приложениями Windows. Через функцию окна вашего простейшего приложения проходят многочисленные сообщения, некоторые из которых порождают новые сообщения после того, как достигают функции DefWindowProc. Весь этот поток сообщений незаметен для вашего приложения, однако оно может перехватить любое сообщение и обработать его самостоятельно. В этом заключается сила механизма обработки сообщения. Фактически приложение может подменить частично или полностью любой метод, используемый Windows для реализации той или иной операции с окном или приложением. Файл определения модуляДля того чтобы создать программу MS-DOS, вам достаточно было иметь ее исходный текст, компилятор и редактор связей. Компилятор создавал один или несколько объектных модулей, транслируя файлы исходных текстов программы, редактор собирал из этих модулей с использованием библиотек загрузочный модуль. Наше самое первое приложение Windows (листинг 1.1) также состояло из одного файла, однако при сборке загрузочного модуля вы получали предупреждающее сообщение о том, что в проекте недостает файла определения модуля. Файл определения модуля (мы будем называть его def-файлом) используется редактором связей при создании exe-файла: в нем указывается имя загрузочного модуля приложения, тип exe-файла, атрибуты сегментов кода и данных, необходимый объем оперативной памяти для стека и кучи, а также имена экспортируемых функций, определенных в приложении. Наше приложение использует def-файл, представленный в листинге 1.3. Листинг 1.3. Файл window\window.def ; ============================= ; Файл определения модуля ; ============================= ; Имя приложения NAME WINDOW ; Описание приложения DESCRIPTION 'Приложение WINDOW, (C) 1994, Frolov A.V.' ; Определение типа загрузочного модуля как ; приложения Windows EXETYPE windows ; Программа, которая будет записана в начало файла ; приложения. Эта программа получит управление ; при попытке запуска приложения в среде MS-DOS STUB 'winstub.exe' ; Размер стека в байтах STACKSIZE 5120 ; Размер локальной кучи памяти (local heap) ; приложения в байтах HEAPSIZE 1024 ; Атрибуты сегмента кода CODE preload moveable discardable ; Атрибуты сегмента данных DATA preload moveable multiple Файл содержит отдельные операторы, такие, как NAME, DESCRIPTION или EXETYPE, причем практически все эти операторы имеют дополнительные параметры. Оператор NAME определяет имя приложения, которое используется операционной системой Windows для идентификации приложения в своих сообщениях. К этому имени предъявляются такие же требования, как и к имени любого файла MS-DOS. Дополнительно после имени приложения может быть указано ключевое слово WINDOWAPI, означающее, что данное приложение предназначено для работы в среде Windows. Это ключевое слово используется по умолчанию и может быть опущено (что мы и сделали). Оператор DESCRIPTION (необязательный) помещает в exe-файл дополнительную информацию, такую, как название приложения и сведения о разработчике. Эта информация никогда не загружается в оперативную память. Оператор EXETYPE отмечает exe-файл как предназначенный для работы в среде Windows. Если для создания приложения вы используете среду разработки, способную создавать приложения OS/2, вам следует указать в def-файле нужный тип загрузочного модуля. При использовании транслятора Borland C++ версии 3.1 этот оператор может быть опущен. Оператор STUB определяет имя файла с программой, подготовленной для работы в среде MS-DOS, которая получит управление при попытке запустить приложение Windows из командной строки MS-DOS. В составе Borland C++ поставляется программа с именем winstub.exe, которая выводит сообщение о том, что данный модуль предназначен для работы в среде Windows: This program requires Microsoft Windows. Однако в принципе вы можете подготовить два варианта своей программы - для MS-DOS и для Windows. В зависимости от того, в какой среде будет запущено ваше приложение, будет выбран соответствующий вариант. Другой способ использования программы, описанной при помощи оператора STUB, заключается в том, что эта программа запускает Windows, передавая ей при запуске в качестве параметра имя файла, в котором она находится. При попытке запуска приложения из среды MS-DOS будет запущена операционная система Windows и затем нужное приложение. Но будьте осторожны - вы можете случайно запустить приложение из виртуальной машины MS-DOS, работающей под управлением Windows. Поэтому перед запуском Windows следует проверить среду, в которой был выполнен запуск приложения. Оператор STACKSIZE определяет размер стека для приложения. Стандартное значение параметра для этого оператора составляет 5120 байт. Однако при использовании некоторых функций Windows версии 3.1 может потребоваться увеличение размера стека до 9 - 10 Кбайт. Такой большой размер стека (по сравнению с размером стека программ MS-DOS) связан с широко распространенной в Windows практикой неявного рекурсивного вызова функций. Оператор HEAPSIZE определяет размер кучи приложения. Однако размер, указанный в качестве параметра, не является критичным. Можно указать любое число, большее нуля. При попытке заказать дополнительную память из кучи приложения операционная система Windows динамически увеличивает размер кучи до необходимой величины (если это возможно). Для кучи, стека и хранения ближних (описанных с ключевым словом near), глобальных, а также статических переменных может быть использовано максимально 64 Кбайт оперативной памяти. Оператор CODE определяет атрибуты стандартного сегмента кода с именем _TEXT. Атрибут preloaded используется для обеспечения автоматической загрузки сегмента в оперативную память сразу после запуска приложения. Атрибут movable позволяет Windows перемещать сегмент в памяти, например для освобождения большого сплошного участка памяти. И наконец, атрибут discardable позволяет Windows при необходимости уничтожить сегмент и отдать занимаемую им память другим приложениям. При необходимости данный сегмент будет автоматически прочитан из exe-файла и восстановлен. Оператор DATA определяет атрибуты стандартного сегмента данных, принадлежащего к группе с именем DGROUP. Наш сегмент данных описан как предварительно загружаемый и перемещаемый. Дополнительно указан атрибут multiple, означающий, что для каждой копии приложения необходимо создать отдельный сегмент данных. Файл определения модуля может содержать и другие операторы, которые мы опишем позже по мере необходимости. |