Первые две строки являются директивами препроцессора, которые сообщают ему, что до того, как начать процесс компиляции модуля, следует вставить в указанное место файлы заголовков (stdafx.h и API.h). Первый файл мы обсуждали в уроке 1. Он содержит директивы подключения библиотечных файлов-заголовков. Директива
//======== Исключает редко используемые элементы
//======== Windows-заголовков
#define WIN32_LEAN_AND_MEAN
уменьшает размер исполняемого модуля, так как исключает те фрагменты каркаса приложения, которые редко используются. Второй файл (API.h) создал мастер. Вы можете открыть его с помощью окна Solution Explorer и увидеть, что он содержит лишь две строки:
#pragma once
#include"resource.h"
Директива fpragma once сообщает компилятору, что данный файл (API.h) должен быть использован при построении кода приложения только один раз, несмотря на возможные повторы (вида #include "API.h"). Вторая директива подключает файл с идентификаторами ресурсов. Сами ресурсы вы видите в окне Resource View. Все ресурсы приложения и их отдельные элементы должны быть идентифицированы, то есть пронумерованы. Новичкам рекомендуется открыть файл resource.h с помощью окна Solution Explorer и просмотреть его содержимое. В этом файле символическим именам (идентификаторам) IDS_APP_TITLE, IDR_MAINFRAME и т. д. соответствуют числовые константы, которые препроцессор вставит вместо идентификаторов еще до процесса компиляции. В конце файла содержатся пометки Studio.Net, определяющие дальнейший способ нумерации ресурсов различного типа. Рассматриваемый файл не рекомендуется редактировать вручную, так как в случае ошибок вы получите труднолокализуемые отказы. Studio.Net сама следит за состоянием resource.h, вставляя и удаляя макроподстановки #def ine по мере того, как вы редактируете ресурсы с помощью специальных редакторов.
Возвращаясь к коду заготовки, отметим, что далее следует объявление глобальных переменных
HINSTANCE hlnst; // Текущий экземпляр
TCHAR szTitle[MAX_LOADSTRING];
Косметические перья работают значительно быстрее, чем другие, но это имеет значение только для сложных рисунков. Геометрическое перо может иметь любую толщину и любые атрибуты Windows-кисти (dither и pattern). Введем дополнения, которые позволят исследовать свойства геометрического пера. В число локальных переменных функции WndProc введите новые сущности:
//====== Узоры штрихов (hatch) кисти, на основе
//====== которых будет основано перо
static UINT uHatch[] =
{
HS_BDIAGONAL, HS_CROSS, HS_DIAGCROSS,
HS_FDIAGONAL, HS_HORIZONTAL, HS_VERTICAL
};
//===== Строки текста для пояснений
static string brush[] =
{
"HS_BDIAGONAL", "HS_CROSS", "HS_DIAGCROSS",
"HS_FDIAGONAL", "HS_HORIZONTAL", "HS_VERTICAL"
};
Вставьте следующий код в ветвь WM_PAINT перед вызовом EndPaint. Этот фрагмент по структуре такой же, как и предыдущий, но здесь мы создаем перо, используя штриховую (hatched) кисть. Запустите и проверьте, что получилось. Попробуйте объяснить, почему линия со штрихом типа HS_HORIZONTAL невидима. Замените строку
LineTo(hdc, iXMax, iYPos);
на
LineTo(hdc, iXMax, iYPos + 3);
и запустите вновь. Теперь линия должна быть видна. Найдите объяснение и попробуйте обойтись без последнего изменения кода, то есть уберите +3:
//======== геометричесое перо
Ib.lbStyle = BS_HATCHED; // Узорная кисть
sText = "Стили на основе кисти (Geometric pen)";
GetTextExtentPoint(hdc,sText.c_str(), sText.size(),SszText);
//======= Сдвиг позиции вывода
iYPos += 2 * szText.cy;
iXPos = iXCenter - szText.cx/2;
TextOut(hdc, iXPos, iYPos, sText.c_str() , sText.size ());
nLines = sizeof(brush)/sizeof(brush[0]);
for (i = 0; i < nLines; i ++ )
{
//======= Выбираем узор штриха кисти
Ib.lbHatch = uHatch[i];
//== Создаем на его основе перо тощиной 5 пиксел
HPEN hp = ExtCreatePen(PS_GEOMETRIC, 5, Sib,0,0);
HPEN hOld = (HPEN)SelectObject(hdc, hp) ;
iYPos += szText.cy; MoveToEx(hdc, 10, iYPos, NULL);
Сначала исследуем косметическое перо. Некоторые его стили, задаваемые символьными константами, занесем в массив. Введем внутрь тела оконной процедуры (после объявления CustColors) объявления новых локальных переменных:
//====== х-координаты:
static int iXCenter; // центра окна,
static int iXPos; // текущей позиции
static int iXMax; // допустимой позиции
int iYPos =0; // Текущая у-координата вывода
int nLines; // Количество линий
SIZE szText; // Экранные размеры строки текста
//====== Стили пера Windows
static DWORD dwPenStyle[] =
{
PS_NULL, PS_SOLID, PS_DOT, PS_DASH,
PS__DASHDOT, PS_DASHDOTDOT
};
//====== Строки текста для вывода в окно
static string style[] =
{
"PS_NULL","PS_SOLID","PS_DOT","PS_DASH",
"PS_DASHDOT","PS_DASHDOTDOT"
};
string sText; // Дежурная строка текста
//===== Логическая кисть — как основа для создания пера
LOGBRUSH lb = { BS_SOLID, color, 0 };
Если вы хотите, чтобы ваш вывод в окно реагировал на изменения пользователем размеров окна, то всегда вводите в оконную процедуру ветвь обработки WM_SIZE. Сделайте это сейчас вместе с изменениями в ветви WM_PAINT:
case WM_SIZE:
//==== В IParam упрятаны размеры окна.
//==== Нас интересует только ширина окна
iXMax = LOWORD(IParam) - 50;
iXCenter = LOWORD(IParam)/2; break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
//===== Режим выравнивания текста (см. MSDN)
SetTextAlign(hdc, TA_NOUPDATECP | TA_LEFT | TA_BASELINE) ;
sText = "Стили линий в Win32 (Cosmetic pen)";
//== Выясняем размеры строки с текстом заголовка GetTextExtentPoint(hdc,sText.c_str(), sText.size(),
//== Сдвигаем точку вывода вниз на одну строку
iYPos += szText.cy;
iXPos = iXCenter - szText.cx/2;
//==== Выводим заголовок
TextOut(hdc,iXPos, iYPos, sText.c_str(), sText. size ()
}
//==== Перебираем массив стилей пера
nLines = sizeof(style)/sizeof(style[0]);
for (int i = 0; i < nLines; i++)
{
//===== Устанавливаем биты стиля пера
DWORD dw = PS_COSMETIC | dwPenStyle[i];
// Создаем перо толщиной в 1 пиксел
HPEN hp = ExtCreatePen(dw, 1, Sib, 0,NULL);
//===== Выбираем перо в контекст устройства
HPEN hOld = (HPEN)SelectObject(hdc,hp); iYPos += szText.cy;
// Сдвиг позиции
//===== Помещаем перо в точку (10, iYPos)
MoveToEx(hdc, 10, iYPos, NULL);
//==== Проводим линию до точки (iXMax, iYPos)
LineTo(hdc, iXMax, iYPos);
//== Возвращаем старое перо в контекст устройства
SelectObject(hdc, hold);
//=== Освобождаем ресурс пера DeleteObject(hp);
//=== Выводим поясняющий текст
TextOut(hdc, 10, iYPos, style[i].c_str(), style [i] .size ()
} ;
EndPaint(hWnd, &ps) ;
break;
Комментарии в тексте поясняют суть происходящего. Отметьте, что здесь применена стандартная тактика работы с ресурсами GDI, которая состоит из последовательности следующих шагов:
создаем свой инструмент;
выбираем его в контекст устройства (SelectObject) и одновременно запоминаем тот инструмент, который используется в контексте в настоящий момент;
рисуем с помощью нашего инструмента;
возвращаем в контекст прежний инструмент;
освобождаем память, занимаемую нашим инструментом.
Так как система работает с ресурсами GDI динамически, то нарушение этой тактики может привести к недостатку памяти и непредсказуемому поведению приложения. Перед тем как запустить проект, попробуйте ответить на вопросы:
Будет ли изменяться цвет линий при пользовании стандартным диалогом, который мы уже реализовали?
Будет ли изменяться цвет текста при тех же условиях?
Теперь запустите приложение и протестируйте его, изменяя размеры окна и пользуясь диалогом. Как вы узнали из документации, косметическое перо может иметь толщину только в 1 пиксел. Если косметическое перо имеет еще один атрибут PS_ALTERNATE, то каждый второй пиксел линии пропускается (не выводится) и создается иллюзия, что перо стало тоньше, чем 1 пиксел. Опробуем эту возможность в нашем примере. Для этого введите в функцию WndProc еще один локальный массив подсказок.
static string alt[] = {"PS_ALTERNATE", "PS_COSMETIC" };
Вставьте следующий код в ветвь WM_PAINT перед вызовом EndPaint, затем запустите и проверьте результат:
//======= Косметическое перо (alternate - solid)
Ib.lbStyle = BS_SOLID;
sText = "Косметическое перо alternate или solid";
GetTextExtentPoint(hdc,sText.c_str(), sText.size(),SszText);
iYPos += 2 * szText.cy;
iXPos = iXCenter - szText.cx/2;
TextOut(hdc, iXPos, iYPos, sText.c_str(), sText.size());
for (i = 0; i < 2; i+ + ) {
DWORD dw = i ? PS_COSMETIC : PS_COSMETIC I PS_ALTERNATE;
HPEN hp = ExtCreatePen(dw, 1, &lb, 0, NULL);
HPEN hOld = (HPEN)SelectObject(hdc, hp) ;
iYPos += szText.cy;
MoveToEx(hdc, 10, iYPos, NULL);
LineTo(hdc, iXMax,iYPos);
SelectObject(hdc, hold);
DeleteObject(hp);
TextOut(hdc, 10, iYPos, alt[i].c str(), alt [i] . size ());
При выборе пользователем какой-либо команды меню система посылает в оконную процедуру сообщение WM_COMMAND, в коротком (wParam) параметре которого будет спрятан идентификатор команды. В обработке сообщения WM_COMMAND содержится распаковка короткого параметра и разветвление в зависимости от идентификатора команды. В ответ на команду About вызывается диалог, шаблон которого вы можете найти в ресурсах приложения.
Запуск диалога в модальном режиме обеспечивает API-функция DialogBox, последним параметром которой является адрес функции About. Он явно приводится к типу DLGPROC (диалоговые процедуры). Этот тип определен как указатель на функцию обратного вызова (реакцию на сообщение) с определенным прототипом. Функция About будет вызываться системой для обработки сообщений, посылаемых уже не главному окну приложения, а окну диалога. Отметим, что описатели CALLBACK, WINAPi и FAR PASCAL идентичны. Они появились на разных этапах развития Windows и одновременно используются системой для обеспечения совместимости со старыми версиями.
Параметры функции About имеют следующий смысл:
HWND hDlg — Windows-описатель окна диалога;
UINT message — код сообщения;
WPARAM wParam, LPARAM IParam — два параметра, сопровождающих сообщение.
Диалоговая процедура имеет характерную, давно устоявшуюся структуру. Первая ветвь switch-блока (WM_INITDIALOG) вызывается при открытии диалога, а вторая (WM_COMMAND) — при нажатии кнопок, расположенных в нем. Вместе с сообщением WM_COMMAND приходят два параметра, в которых запакована сопровождающая информация. В нашем случае это идентификатор (ШОК) кнопки ОК, так как другой традиционной кнопки Cancel (IDCANCEL) просто нет в шаблоне диалога. В Win32 идентификатор элемента управления спрятан в младших 16 битах wParam, и его приходится распаковывать. Функция EndDialog закрывает окно диалога.
Теперь рассмотрим, как устроена оконная процедура wndProc. Ее имя уже дважды появлялось в тексте программы. Сначала был объявлен ее прототип, затем оно было присвоено одному из полей структуры типа WNDCLASSEX. Поле имеет тип указателя на функцию с особым прототипом оконной функции. Здесь полезно вспомнить, что имя функции трактуется компилятором C++ как ее адрес.
Оконная процедура должна «просеивать» все посылаемые ей сообщения и обрабатывать те из них, которые были выбраны программистом для обеспечения желаемой функциональности. Типичной структурой оконной процедуры является switch-блок, каждая ветвь которого содержит обработку одного сообщения. В первом приближении наша оконная процедура реагирует только на три сообщения:
WM_COMMAND — о выборе пользователем одной из команд меню;
WM_PAINT — о необходимости перерисовать клиентскую область окна;
WM_DESTROY — о необходимости закрыть окно.
Сообщение WM_DESTROY (уничтожить окно) посылается системой уже после того, как окно исчезло с экрана. Мы реагируем на него вызовом функции PostQuitMessage, которая указывает системе, что поток приложения требует своего завершения, путем посылки сообщения WM_QUIT. Его параметром является код завершения, который мы указываем при вызове PostQuitMessage.
Примечание
Рассмотренная структура приложения Win32 позволяет сделать вывод, что в подавляющем числе случаев развитие приложения сосредоточено внутри оконной процедуры, а не в функции WinMain. Развитие приложения заключается в том, что в число обрабатываемых сообщений (messages) включаются новые. Для этого программист должен вставлять новые case-ветви в оператор switch (msg).
Если оконная процедура не обрабатывает какое-либо сообщение, то управление передается в ветвь default. Вы видите, что в этой ветви мы вызываем функцию DefWindowProc, которая носит название оконной процедуры по умолчанию. Эта функция гарантирует, что все сообщения будут обработаны, то есть, удалены из очереди. Возвращаемое значение зависит от посланного сообщения.
Вы, конечно, обратили внимание на обилие новых типов данных, которые используются в приложениях Win32. Многие из них имеют префикс Н, который является сокращением слова Handle — дескриптор, описатель. Описатели разных типов (HWND, HPEN, HBITMAP и т. д.) являются посредниками, которые помогают найти нужную структуру данных в виртуальном мире Windows. Объекты Windows или ее ресурсы, такие как окна, файлы, потоки, перья, кисти, области, представлены в системе структурами языка С, и адреса этих структур могут изменяться. В случае нехватки реальной памяти Windows выгружает из памяти ненужные в данный момент времени объекты и загружает на их место объекты, требуемые приложением. В системной области оперативной памяти Windows поддерживает таблицу, в которой хранятся физические адреса объектов. Для поиска объекта и управления им сначала следует получить у системы его описатель (место в таблице, индекс). Важно иметь в виду, что физический адрес объекта — понятие для Windows, а не для программиста. Описатель типа HANDLE можно уподобить номеру мобильного телефона, с помощью которого вы отыскиваете объект, перемещающийся в виртуальном мире Windows.
Последним испытанием для геометрического пера будет произвольное bitmap-изображение. Если задать BS_PATTERN в качестве стиля кисти, на основе которой создается перо, то линия рисунка может иметь произвольный узор и толщину, что дает волю фантазии разработчика. Однако сначала следует создать ресурс bitmap-изображения, загрузить его и задать его описатель в поле IbHatch логической кисти. Для создания изображения:
Перейдите в окно Resource View.
Раскройте узел дерева под именем API.rc и убедитесь, что в дереве нет узла с именем Bitmap.
Вызовите контекстное меню на узле API.rc и дайте команду Add Resource.
В диалоге Add Resource выберите тип Bitmap и нажмите кнопку New.
Откройте окно Properties и в поле справа от текста ID задайте идентификатор IDB_PAT1 новому точечному изображению.
Измените размер изображения, уменьшив его до 5x5. Используйте для этой цели элементами управления (resize handles) рамки.
Создайте произвольное изображение с помощью контрастирующих цветов.
В окне Resource View вызовите контекстное меню на узле дерева IDВ_РАТ1 и выберите команду Insert Copy.
Измените язык копии на тот, который поддерживается системой, например English (United States), иначе не получится, и нажмите ОК.
Теперь вы имеете копию изображения с теми же размерами и идентификатором. Здесь удобно перевести окно Properties в режим Floating или сдвинуть его нижнюю границу вверх. Измените идентификатор нового изображения на юв_РАТ2 и, при желании, возвратите язык ресурса.
Измените узор второго изображения и повторите пункты 7-10, задав идентификатор ЮВ_РАТЗ для третьего изображения.
Возвратитесь в окно API.CPP и введите в число локальных переменных функции wndProc новые массивы:
//===== Массив идентификаторов bitmap
static UINT nPatterns[] =
{
IDB_PAT1, IDB_PAT2, IDB_PAT3
};
static string bitmap!] =
{
"BS_PATTERN, 1", "BS_PATTERN, 2", "BS_PATTERN, 3"
);
Вслед за фрагментом, моделирующим стиль PS_USERSTYLE , вставьте следующий код:
//======= Геометричесое перо (bitmap)
Ib.lbStyle = BS_PATTERN;
sText = "Стили на основе bitmap-узора (Geometric pen)";
GetTextExtentPoint(hdc,sText.c_str(), sText.size 0,SszText);
iYPos += 2 * szText.cy;
iXPos = iXCenter - szText.cx/2;
TextOut(hdc, iXPos, iYPos, sText.c_str(), sText. size () ) ;
nLines = sizeof(nPatterns)/sizeof(nPatterns[0]);
for (i =0; i < nLines; i++)
{
HBITMAP hBmp;
hBmp = LoadBitmap(GetModuleHandle(NULL), (char*)nPatterns[i]);
Ib.lbHatch = long(hBmp);
HPEN hp = ExtCreatePen(PS_GEOMETRIC, 5, &lb, 0, 0) ;
HPEN hOld = (HPEN)SelectObject(hdc, hp) ;
iYPos += szText.cy;
MoveToEx(hdc, 10, iYPos, NULL);
LineTofhdc, iXMax,iYPos);
SelectObject(hdc, hOld);
DeleteObject(hp);
TextOut(hdc, 10, iYPos, bitmap[i].c_str(),
bitmap[i] .size () ) ;
}
Запустите на выполнение и проверьте результат. Вы должны получить окно, которое выглядит так, как показано на рис. 3.3. Отметьте, что остались неисследованными еще несколько возможностей по управлению пером — это стили типа PS_ENDCAP_* и PS_JOIN_*. Вы, вероятно, захотите их исследовать самостоятельно. При этом можете использовать уже надоевшую, но достаточно эффективную схему, которой мы пользуемся сейчас.
В этом уроке мы с помощью Studio.Net научимся разрабатывать традиционные приложения Win32, основанные на использовании функций API (Application Programming Interface). Вы, конечно, знаете, что приложения для Windows можно разрабатывать как с использованием библиотеки классов MFC, так и на основе набора инструментов, объединенных в разделе SDK (Software Development Kit) студии разработчика. Обширную документацию по SDK вы можете найти в MSDN (Microsoft Developer's Network), которая поставляется вместе со студией. Отдельные выпуски MSDN, зачастую содержат еще более свежую информацию по SDK. Без MSDN успешная деятельность в рамках студии разработчика просто немыслима.
Использование всей мощи MFC облегчает процесс разработки приложений, однако далеко не все возможности Win32 API реализованы в библиотеке MFC, многие из них доступны только средствами API. Поэтому каждому разработчику необходимо иметь представление о структуре и принципах функционирования традиционного Windows-приложения, созданного на основе API-функций. Другими доводами в пользу того, что существует необходимость знать и постоянно углублять свои познания в технологии разработки приложений с помощью SDK, могут быть следующие:
каркас MFC-приложения, так или иначе, содержит внутри себя структуру традиционного Windows-приложения;
многие методы классов MFC содержат, инкапсулируют вызовы API-функций;
накоплен огромный банк готовых решений на основе SDK, которые достаточно просто внедряются в приложения на основе MFC, и не пользоваться которыми означает обеднять себя.
В состав API входят не только функции, более 2000, но и множество структур, более 800 сообщений, макросы и интерфейсы. Цель настоящей главы:
показать традиционную структуру Windows-приложения;
продемонстрировать способы управления таким инструментом подсистемы GDI (Graphics Driver Interface), как перо Windows.
Основной чертой всех Windows-приложений является то, что они поддерживают оконный интерфейс, используя при этом множество стандартных элементов управления (кнопки, переключатели, линейки, окна редактирования, списки и т.
д.). Эти элементы поддерживаются с помощью динамических библиотек (DLL), которые являются частью операционной системы (ОС). Именно поэтому элементы доступны любым приложениям, и ваше первое приложение имеет почти такой же облик, как и любое другое. Принципиально важным отличием Windows-приложений от приложений DOS является то, что все они — программы, управляемые событиями (event-driven applications). Приложения DOS — программы с фиксированной последовательностью выполнения. Разработчик программы последовательность выполнения операторов, и система строго ее соблюдает. В случае программ, управляемых событиями, разработчик не может заранее предсказать последовательность вызовов функций и даже выполнения операторов своего приложения, так как эта последовательность определяется на этапе выполнения кода.
Программы, управляемые событиями, обладают большей гибкостью в смысле выбора пользователем порядка выполнения операций. Характерно то, что последовательность действий часто определяется операционной системой и зависит от потока сообщений о событиях в системе. Большую часть времени приложение, управляемое событиями, находится в состоянии ожидания событий, точнее сообщений о них. Сообщения могут поступать от различных источников, но все они попадают в одну очередь системных сообщений. Только некоторые из них система передаст в очередь сообщений вашего приложения. В случае многопотокового приложения сообщение приходит активному потоку (thread) приложения. Приложение постоянно выполняет цикл ожидания сообщений. Как только придет адресованное ему сообщение, управление будет передано его окопной процедуре.
Примечание
Если вы хотите получить более полное представление о процессах и потоках в Windows, то обратитесь к последней главе этой книги, которая носит более познавательный, чем практический характер.
Наступление события обозначается поступлением сообщения. Все сообщения Windows имеют стандартные имена, многие из которых начинаются с префикса WM_ (Windows Message). Например, WM_PAINT именует сообщение о том, что необходимо перерисовать содержимое окна того приложения, которое получило это сообщение.
Идентификатор сообщения WM_PAINT — это символьная константа, обозначающая некое число. Другой пример: при создании окна система посылает сообщение WM_CREATE. Вы можете ввести в оконную процедуру реакцию на это сообщение для того, чтобы произвести какие-то однократные действия.
Программист может создать и определить какие-то свои собственные сообщения, действующие в пределах зарегистрированного оконного класса. В этом случае каждое новое сообщение должно иметь идентификатор, превышающий зарезервированное системой значение WM_USER (0x400). Допустим, вы хотите создать сообщение о том, что пользователь нажал определенную клавишу в тот момент, когда клавиатурный фокус находится в особом окне редактирования с уже зарегистрированным классом. В этом случае новое сообщение можно идентифицировать так:
#define WM_MYEDIT_PRESSED WM_USER + 1
Каждое новое сообщение должно увеличивать значение идентификатора по сравнению с WM_MYEDIT_PRESSED. Максимально-допустимым значением для идентификаторов такого типа является число 0x7 FFF. Если вы хотите создать сообщение, действующее в пределах всего приложения и не конфликтующее'с системными сообщениями, то вместо константы WM_USER следует использовать другую константу WM_APP (0x8000). В этом случае можно наращивать идентификатор вплоть до 0xBFFF.
Рассмотрим ситуацию, когда пользователь приложения нажимает клавишу, а система вырабатывает сообщение об этом событии. Вы знаете, что Windows обеспечивает поддержку клавиатуры, не зависящую от типа устройства (device-independent support). Для каждого типа клавиатуры она устанавливает соответствующий драйвер, то есть специальную программу, которая служит посредником между клавиатурой и операционной системой. Клавиатурная поддержка Windows не зависит от языка общения с системой. Это достигается использованием специальной клавиатурной раскладки (layout), которую пользователь выбрал в данный момент. Каждой клавише на уровне аппаратуры присвоено уникальное значение — идентификатор клавиши, зависящий от типа устройства и называемый скан-кодом.
Примечание
На самом деле, когда пользователь вводит символ, то клавиатура генерирует два события и два скан-кода — один, когда он нажимает клавишу, и другой, когда отпускает. Скан-коды с клавиатуры поступают в клавиатурный драйвер, который, используя текущую раскладку, транслирует их и преобразовывает в сообщения.
Клавиатурный драйвер интерпретирует скан-код и преобразует его в определяемый Windows код виртуальной клавиши (virtual-key code), не зависящий от типа устройства и идентифицирующий функциональный смысл клавиши. После этого преобразования скан-кода драйвер создает сообщение, в которое включает: скан-код, виртуальный код и другую сопутствующую информацию. Затем он помещает сообщение в специальную очередь системных сообщений.
Windows выбирает сообщение из этой очереди и посылает в очередь сообщений соответствующего потока (thread). В конце концов, цикл выборки сообщений данного потока передает его соответствующей оконной процедуре для обработки. Модель ввода с клавиатуры в системе Windows представлена на рис. 3.1.
Рис. 3.1. Путь прохождения сообщений от клавиатуры
Здесь буфер клавиатуры служит связующим звеном между прикладной программой и одним из сервисов ОС. Точно так же формируют (или могут формировать) свои специфические данные обработчики других событий.
При этом используется универсальная структура данных MSG (сообщение), описывающая любое событие. Она содержит сопровождающую информацию, достаточную для того, чтобы сообщением можно было воспользоваться. Например, для сообщения от клавиатуры это должен быть код нажатой клавиши, для сообщения от мыши — координаты ее указателя, для сообщения WM_SIZE — размеры окна. Тип структур MSG определен в одном из файлов заголовков следующим образом:
//======= Ярлык типа
typedef struct tagMSG
{
//===== Описатель окна, чья оконная процедура
//===== получает сообщение
HWND hwnd;
UINT message; // Код сообщения
// Дополнительная информация, зависящая от сообщения
WPARAM wParam;
LPARAM iParam; // Тоже
DWORD time; // Время посылки сообщения
//==== Точка экрана, где был курсор
//==== в момент посылки сообщения
POINT pt;
}
MSG; //===== Тип структур, эквивалентный ярлыку
Универсальные параметры wParam и IParam используются различным образом в различных сообщениях. Например, в сообщении WM_LBUTTONDOWN первый из них содержит идентификатор одновременно нажатой клавиши (Ctrl, Shift и т.д.), а второй (IParam) — упакованные экранные координаты (х, у) курсора мыши. Чтобы выделить координаты, программист должен расщепить «длинный параметр» (4 байта) на два коротких (по 2 байта). В нижнем слове, которое можно выделить с помощью макроподстановки, например,
int х = LOWORD(IParam);
хранится координата х, а в верхнем — координата у, которую вы можете выделить с помощью макроса:
int у = HIWORD(IParam);
Отметьте, что классы библиотеки MFC избавляют вас от необходимости распаковывать параметры сообщения.
Следующая схема (рис. 3.2) в общих чертах иллюстрирует путь прохождения сообщений. Она любезно предоставлена Мариной Полубенцевой, вместе с которой мы ведем курс Visual C++ в Microsoft Certified Educational Center при Санкт-Петербургском государственном техническом университете.
Каждый обработчик события (драйвер) помещает сформированное сообщение в определенную динамическую структуру данных в памяти.Другие аппаратные и программные обработчики точно так же формируют свои сообщения, ставя их в очередь за уже существующими. Так формируется системная очередь сообщений.
Операционная система постоянно работает с очередью и, анализируя текущее сообщение, решает, какому приложению следует его передать. Она переписывает его в другую структуру данных в памяти — очередь сообщений конкретного приложения. Приложение реагирует или не реагирует на сообщение, но в любом случае удаляет его из очереди сообщений. Здесь важно понять, что по отношению к приложению сообщения появляются случайным образом и невозможно предсказать, какое сообщение появится в следующий момент.
Мы хотим показать способы развития начальной заготовки приложения Win32. В наши планы входит создание пера Windows и управление его стилями. Кроме того, мы хотим показать, как можно изменять цвет пера с помощью стандартного диалога Windows. Существует множество стандартных диалогов для управления разными объектами системы, например открытие файла, выбор шрифта, поиск и замена текста. Мы будем управлять стандартным диалогом по выбору цвета. Обычным приемом при работе с каким-либо из стандартных диалогов является использование подходящей вспомогательной структуры. Поля структуры помогают инициализировать элементы диалога при его открытии, а также извлечь результат после его завершения.
В нашем случае диалог вызывается ЛР1-функцией chooseColor, которая требует задать в качестве параметра адрес структуры типа CHOOSECOLOR. Ее надо предварительно создать и использовать для хранения текущего цвета, а также цвета, выбранного пользователем в рамках диалога. Цвет должен храниться в поле rgbResult этой структуры. Вы помните, что оконная процедура многократно, при обработке каждого сообщения, получает и вновь отдает управление системе. Ее локальные (автоматические) переменные будут каждый раз создаваться и погибать. Следовательно, они не в состоянии запомнить текущий выбранный цвет. Выход — использовать либо глобальные, либо статические переменные. Используя второй способ, вставьте в начало тела функции WndProc следующие переменные:
//===== Структура для работы со стандартным диалогом
CHOOSECOLOR cc;
//===== Переменная для хранения текущего цвета
static COLORREF color = RGB(255,0,0);
//===== Массив цветов, выбираемых пользователем
static COLORREF CustColors[16];
Структура CHOOSECOLOR определена в библиотеке, которая сейчас недоступна, поэтому вставьте в конец файла stdafx.h директиву #include <CommDlg.h>. Заодно добавьте туда еще две строки:
#include <string>
using namespace std;
так как ниже мы будем пользоваться объектами типа string из библиотеки STL.
Затем в блок switch (wmld) функции WndProc введите ветвь обработки команды меню ID_EDIT_COLOR (саму команду создадим позже):
// Если выбрана команда с идентификатором ID_EDIT_COLOR
case ID_EDIT_COLOR:
// Подготовка структуры для обмена с диалогом
ZeroMemory(Sec, sizeof(CHOOSECOLOR));
//====== Ее размер
cc.lStructSize = sizeof(CHOOSECOLOR);
//====== Адрес массива с любимыми цветами
cc.lpCustColors = (LPDWORD)CustColors;
if (ChooseColor (ice)) // Вызов диалога
{
// Если нажата кнопка OK,
// то запоминаем выбранный цвет
color = cc.rgbResult;
// Объявляем недействительной
// клиентскую область окна
InvalidateRect(hWnd, NULL, TRUE);
}
break;
Функция ChooseColor запускает диалог в модальном режиме. Это означает, что пользователь не может управлять приложением, пока не завершит диалог. Тактика работы с диалогом такого типа стандартна:
Подготовка данных, инициализирующих поля диалога.
Запуск диалога, ожидание его завершения и проверка результата.
В случае выхода по кнопке ОК, выбор данных из полей вспомогательной структуры.
Использование результатов диалога, например перерисовка окна с учетом нового цвета.
Функция InvalidateRect сообщает системе, что часть окна стала недействительной, то есть требует перерисовки. В ответ на это система посылает приложению сообщение WM_PAINT. Наша оконная процедура уже реагирует на это сообщение, но пока еще не рисует. Теперь создадим команду меню, при выборе которой диалог должен появится на экране. Для этого:
Перейдите в окно Resource View.
Раскройте узел дерева ресурсов под именем Menu.
Выполните двойной щелчок на идентификаторе всей планки меню IDC_API.
В окне редактора меню переведите фокус ввода в окно на планке меню с надписью Type here (Внимание, там два таких окна!).
Введите имя меню Edit и переведите курсор вниз в пустое поле для команды.
Введите имя команды меню Color.
Откройте окно Properties и убедитесь, что команда получила идентификатор ID_EDIT_COLOR.
Перетащите мышью меню Edit на одну позицию влево.
Запустите приложение (Ctrl+F5) и опробуйте команду меню Edit > Color. Диалог имеет две страницы. Для того чтобы убедиться в правильном функционировании статического массива любимых цветов (custColors), раскройте вторую страницу, выберите несколько цветов в ее правой части, нажимая кнопку Add to Custom Colors. Обратите внимание на то, что выбранные цвета попадают в ячейки левой части диалога. Закройте и вновь откройте диалог. Новые цвета должны остаться на месте, так как они сохранились в массиве CustColors.
Рассмотренная модель выработки и прохождения сообщений поможет вам понять структуру, принятую для всех Windows-приложений. Последние два блока в рассмотренной схеме (рис. 3.1) определяют особенности строения любого Windows-приложения. Простейшее из них должно состоять как минимум из двух функций:
функции winMain, с которой начинается выполнение программы и которая «закручивает» цикл ожидания сообщений (message pump);
оконной процедуры, которую вызывает система, направляя ей соответствующие сообщения.
Каждое приложение в системе, основанной на сообщениях, должно уметь получать и обрабатывать сообщения из своей очереди. Основу такого приложения в системе Windows представляет функция winMain, которая содержит стандартную последовательность действий. Однако обрабатывается большинство сообщений окном — объектом операционной системы Windows.
Примечание
C точки зрения пользователя, окно — это прямоугольная область экрана, соответствующая какому-то приложению или его части. Вы знаете, что приложение может управлять несколькими окнами, среди которых обычно выделяют одно главное окно-рамку (Frame Window). С точки зрения операционной системы, окно — это в большинстве случаев конечный пункт, которому направляются сообщения. С точки зрения программиста, окно —это объект, атрибуты которого (тип, размер, положение на экране, вид курсора, меню, зна-чек, заголовок) должны быть сначала сформированы, а затем зарегистрированы системой. Манипуляция окном осуществляется посредством специальной оконной функции, которая имеет вполне определенную, устоявшуюся структуру.
Функция winMain выполняется первой в любом приложении. Ее имя зарезервировано операционной системой. Она в этом смысле является аналогом функции main, с которой начинается выполнение С-программы для DOS-платформы. Имя оконной процедуры произвольно и выбирается разработчиком. Система Windows регистрирует это имя, связывая его с приложением. Главной целью функции winMain является регистрация оконного класса, создание окна и запуск цикла ожидания сообщений.
Программы, управляемые событиями
Прохождение сообщений в системе
Структура Windows-приложения
Стартовая заготовка приложения Win32 и ее анализ
Оконная процедура
Меню и диалог
Развитие начальной заготовки
Управление пером Windows
Если вы хотите самостоятельно освоить какой-либо технологический прием или способ управления ресурсами, а так же инструментами Windows, то лучше всего обратиться к разделу Platform SDK документации (MSDN). В блоке страничных окон, которыми вы успешно пользуетесь, имеется страница Dynamic Help, которая помогает быстро отыскать необходимую информацию в море документации, сопровождающей Studio.Net. Предположим, вы хотите научиться создавать перо Windows и начать с получения справки. Надо открыть вкладку Dynamic Help и набрать в окне редактора текст, который, как вам кажется, имеет отношение к искомой теме, например Реn.
Окно динамической справки следит за вашим вводом и пытается найти подходящий раздел в документации. В нашем случае вы должны увидеть пару тем, связанных с пером Windows. Открыв первую из них (Pen Class), вы убеждаетесь, что попали в раздел Visual Basic Help, то есть не туда. Второй попыткой может быть выбор строки СРеп или CreatePen. Теперь динамическая справка приводит вас ближе к цели. Если вы вспомните, что сейчас мы пользуемся функциями API, то выбор темы CreatePen будет точным.
Примечание
При работе с MSDN вы можете создать свое собственное подмножество документов и сократить количество тем, предлагаемых ядром MSDN.
Внимательно прочтя всю страницу текста справки из раздела Platform SDK, вы поймете, что перо Windows — это достаточно сложный и гибкий инструмент рисования. Не пренебрегайте также гипертекстовыми ссылками внизу экрана под рубрикой See Also. Выберите там ссылку ExtCreatePen для одноименной функции, которую мы собираемся использовать. Правила игры с функцией ExtCreatePen не так просты, как хотелось бы, но они позволяют управлять атрибутами пера в широком диапазоне. Оказывается кроме «простых» перьев можно создавать перья на основе кисти.