Учебник по Visual C++ .Net

         

Анализ стартовой заготовки Первые


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

// Текст заголовка окна

TCHAR szWindowClass[MAX_LOADSTRING];

// Текст регистрации

Рассматривайте описатель hlnst как адрес исполняемого модуля в пространстве процесса, соответствующего приложению. Если вы не знакомы с понятиями поток и процесс, то обратитесь к последнему уроку этой книги, где приведены некоторые сведения об архитектуре Windows. Текст регистрации szWindowClass будет загружен из ресурсов при выполнении winMain (см. вызов LoadString).


Этот текст представляет собой строку символов «API», которая хранится в ресурсах. Ее можно увидеть, раскрыв дерево ресурсов в окне Resource View, узел String Table и дважды щелкнув на элементе String Table (group). С помощью этой строки ваше приложение регистрируется в операционной системе.

При вызове функции winMain система передает ей параметры:

hinstance — описатель экземпляра приложения. Это адрес приложения, загруженного в память. В Windows NT/2000 этот адрес для всех приложений имеет одно и то же значение 0x00400000 (4 Мбайт);

hPrevlnstance — описатель предыдущего экземпляра приложения. Этот параметр устарел и теперь не используется в приложениях Win32;

lpCmdLine — указатель на командную строку. Мы не будем использовать этот параметр;

nCmdShow — состояние окна при начальной демонстрации.

Ранее в Win 16 второй параметр использовался в целях экономии ресурсов, но в Win32 — это NULL, так как каждый экземпляр приложения теперь выполняется в своем собственном виртуальном адресном пространстве процесса емкостью 4 Гбайт. Все экземпляры процесса загружаются начиная с одного и того же адреса в этом пространстве (см. последний урок). Теперь рассмотрим алгоритм функции WinMain:



она загружает из ресурсов две рассмотренные выше строки текста;

создает, заполняет и регистрирует структуру типа WNDCLASS;

создает главное окно приложения;

загружает клавиатурные ускорители;

запускает цикл ожидания и обработки сообщений.

Основные атрибуты главного окна приложения задаются в структуре типа WNDCLASSEX. Понятие класс окна появилось до того, как появились классы C++. Поэтому структура WNDCLASSEX не имеет ничего общего с классами MFC. Она является типом структур языка С. Дело в том, что каждое Windows-приложение должно зарегистрировать атрибуты своего окна, а система использует их при создании окна. Структура WNDCLASSEX своими полями определяет некий шаблон или модель для создания окон данного класса. В полях структуры вы указываете необходимые атрибуты окна: адрес исполняемого модуля приложения, .адрес оконной процедуры, имя ресурса меню, набор битов для описания стиля окна, местонахождение изображения курсора, значка и т. д. Эту «неинтересную» работу выполняет большая часть кодов функции MyRegisterClass. Используя классы MFC, вы избавляете себя от подобной рутинной работы.



При регистрации класса окон (точнее, переменной типа WNDCLASSEX) операционная система связывает оконную процедуру (WndProc) с приложением. В winMain вы должны зарегистрировать главное окно приложения, остальные же окна, если они нужны, могут быть зарегистрированы и в других местах программы. Адрес заполненной структуры передается в функцию RegisterClassEx, которая говорит Windows, что от нее ожидается, когда окна подобного класса появляются на экране. Теперь система знает, какой вид курсора использовать при попадании указателя мыши в пределы окна, кому посылать сообщения о событиях, происходящих в области окна, какие значки (большой 32 х 32 и маленький 16 х 16) будут связаны с приложением и т. д. Функция RegisterClassEx возвращает число типа АТОМ (16-ти битное целое без знака), которое соответствует строке регистрации в таблице атомов, поддерживаемой системой.

После регистрации класса главного окна идет вызов функции Initlnstance, которая пытается создать окно (CreateWindow) зарегистрированного класса. Если система нашла класс окна в трех поддерживаемых ею списках зарегистрированных классов окон, то функция CreateWindow возвращает описатель окна (HWND). Мы запоминаем его для того, чтобы использовать в качестве параметра при вызове других функций. Если нет, то функция вернет нулевое значение и приложение молча завершает свою работу. Попробуйте при вызове CreateWindow вставить пустую строку, вместо szWindowClass. Запустив приложение, вы поймете, что в ветвь if (ihwnd) неплохо бы вставить вызов:

MessageBox(0,"Не нашла класс окна","Ошибка",МВ_ОК);

При успешном создании окна происходит вызов функций

//====== Показать окно

ShowWindow(hWnd, nCmdShow);

//====== Сделать это, минуя очередь

UpdateWindow(hWnd);

Далее в коде winMain загружается таблица акселераторов (соответствий клавиатурных комбинаций командам меню), которая используется в цикле ожидания и обработки сообщений. Функция GetMessage выбирает сообщение из очереди сообщений потока и помещает его в структуру типа MSG, служащей для хранения информации о сообщении Windows. Функция TranslateAccelerator пытается транслировать (преобразовать) сообщения WM_KEYDOWN (нажата клавиша) или WM_SYSKEYDOWN (нажата F10 или ALT+другая клавиша) в сообщения WM_COMMAND или WM_SYSCOMMAND, но только в том случае, если в таблице акселераторов присутствует код клавиши.



Преобразованное сообщение направляется непосредственно в оконную процедуру. Характерно то, что TranslateAccelerator ждет завершения обработки сообщения. Функция TranslateMessage транслирует сообщения, поступившие в виде виртуальных кодов клавиш, в символьные сообщения и снова отправляет их в очередь сообщений потока для того, чтобы отреагировать на него на следующей итерации цикла. Например, сообщение WM_KEYDOWN (virtual key message) она может преобразовать в WM_CHAR (character message) или в WM_DEADCHAR (см. MSDN). Функция DispatchMessage отправляет сообщение оконной процедуре.

Коротко алгоритм работы winMain может быть сформулирован так. После выполнения инициализирующих действий функция WinMain входит в цикл обработки сообщений. После выхода из этого цикла работа приложения завершается. Выход происходит, когда придет сообщение WM_QUIT. Обычно его посылает оконная процедура, когда пользователь закрывает главное окно.

Стартовая заготовка иллюстрирует стандартную последовательность действий при создании Windows-приложения на базе API-функций. Обратите внимание на то, что функция WndProc нигде явно не вызывается, хотя именно она выполняет всю полезную работу. Для проверки усвоения прочитанного ответьте на вопрос: «Когда и кем она вызывается?»


Геометрическое перо Косметические


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

LineTo(hdc, iXMax,iYPos);

SelectObject(hdc, hold);

DeleteObject(hp);

TextOut(hdc, 10, iYPos, brush[i].c_str(), brush[i].size());

}

Геометрическое перо может быть создано на основе заданного пользователем или программистом узора (PS_USERSTYLE). Узор задается с помощью массива переменных типа DWORD. Элементы массива определяют циклический алгоритм закраски линии по схеме «играем — не играем». Например, массив DWORD а [ ] = { 2,3,4 } определяет линию, у которой 2 пиксела закрашены, 3 — не закрашены, 4 — закрашены. Затем цикл (2,3,4) повторяется. Для моделирования этой возможности введите в WndProc еще один двухмерный массив (так как линия будет не одна):


//======= три узора геометрического пера

static DWORD dwUser[3][4] =

{

{ 8, 3, 3, 3),

{ 3, 3, 3, 3),

(15, 4, 3, 4}

};

Затем добавьте вслед за фрагментом, моделирующим штриховую линию, следующий код:

//======= Геометричесое перо (user-defined)

Ib.lbStyle - BS_SOLID;

sText = "Стили на основе узора (Geometric pen)";

GetTextExtentPoint(hdc,sText.c_str(), sText.size(),SszText);

iYPos += 2 * szText.cy;

iXPos = iXCenter - szText.cx/2;

TextOutfhdc, iXPos, iYPos,

sText.c_str(), sText .size () } ,

nLines = sizeof(dwUser)/sizeof(dwUser[0]) ;

//====== Перебираем узоры пользователя

//====== (строки массива dwUser)

for (i = 0; i < nLines; i++)

{

DWORD dw = PS_GEOMETRIC | PS_USERSTYLE | PS_ENDCAP_FLAT;

HPEN hp = ExtCreatePen(dw, i+2, Sib, 4, dwUser[i]);

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, user[i].c_str(), user[i].size());

}

Запустите и проверьте результат. Здесь следует отметить, что узор имеет возможность развиться (разогнаться) только на достаточно большом промежутке между точками. Для вывода графиков функциональных зависимостей он, к сожалению, не пригоден, так как графики имеют большое количество точек. Точки расположены тесно, а узор начинается заново после каждой пары точек. Как ни странно, косметическое перо толщиной в 1 пиксел выдерживает подобное испытание и его можно использовать для графиков функций.


Косметическое перо Сначала исследуем


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 ());


Оконная процедура Теперь рассмотрим


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.


Перья на основе растровых изображений


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_*. Вы, вероятно, захотите их исследовать самостоятельно. При этом можете использовать уже надоевшую, но достаточно эффективную схему, которой мы пользуемся сейчас.

Рис. 3.3. Стили пера в Win32

Теперь ответим на один из вопросов, которые были заданы по ходу текста этой главы. Для того чтобы изменился цвет текста, надо вставить вызов API-функции SetTextColor. И сделать это надо в ветви обработки сообщения WM_PAINT перед вызовом функции TextOut.

SetTextColor(hdc, color) ;

Подведем итоги. Наш пример иллюстрирует характерные особенности строения приложения, управляемого событиями:

оно должно состоять как минимум из двух функций: winMain и регистрируемой оконной процедуры;

последняя имеет вид отдельных ветвей, обрабатывающих те команды пользователя или сообщения Windows, которые выбрал разработчик;

в более сложных случаях для обработки событий, конечно же, необходимо предусмотреть множество отдельных функций или модулей программы;

приложения Win32, разработанные по рассмотренной схеме, имеют то преимущество, что они не несут дополнительной нагрузки в виде скрытого от ваших глаз каркаса приложения, поэтому дают очень быстрый код;

недостатком приложения Win32 является то, что для их разработки необходимо ориентироваться в мире из тысяч API-функций, вспомогательных структур, макросов и интерфейсов.



Программы, управляемые событиями


В этом уроке мы с помощью 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.


Прохождение сообщений в системе


Путь прохождения сообщений от клавиатуры

Здесь буфер клавиатуры служит связующим звеном между прикладной программой и одним из сервисов ОС. Точно так же формируют (или могут формировать) свои специфические данные обработчики других событий. При этом используется универсальная структура данных 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 при Санкт-Петербургском государственном техническом университете.


Каждый обработчик события (драйвер) помещает сформированное сообщение в определенную динамическую структуру данных в памяти. Другие аппаратные и программные обработчики точно так же формируют свои сообщения, ставя их в очередь за уже существующими. Так формируется системная очередь сообщений.

Операционная система постоянно работает с очередью и, анализируя текущее сообщение, решает, какому приложению следует его передать. Она переписывает его в другую структуру данных в памяти — очередь сообщений конкретного приложения. Приложение реагирует или не реагирует на сообщение, но в любом случае удаляет его из очереди сообщений. Здесь важно понять, что по отношению к приложению сообщения появляются случайным образом и невозможно предсказать, какое сообщение появится в следующий момент.



Рис. 3.2. Путь прохождения сообщений Windows


Структура Windows-приложения Рассмотренная


msg.wParam;

}

//

// FUNCTION: MyRegisterClass ()

//

// НАЗНАЧЕНИЕ: Регистрирует оконный класс

//

// COMMENTS: //

// Эта функция нужна только если вы хотите, чтобы код

// был совместим с Win32 системами, которые

// существовали до создания функции 'RegisterClassEx ' ,

// введенной в Windows 95.

// Вызов 'RegisterClassEx' необходим для правильного

// создания маленького (small) значка, ассоциированного

// с приложением.

//

ATOM MyRegisterClass (HINSTANCE hlnstance)

{

WNDCLASSEX wcex;

wcex.cbSize = sizeof (WNDCLASSEX) ;

wcex. style = CS_HREDRAW | CS_VREDRAW;

wcex.lpfnWndProc = (WNDPROC) WndProc;

wcex. cbClsExtra = 0;

wcex.cbWndExtra = 0;

wcex. hlnstance = hlnstance;

wcex.hlcon = Loadlcon (hlnstance,

(LPCTSTR) IDI_API) ;

wcex.hCursor = LoadCursor (NULL, IDC_ARROW) ;

wcex.hbrBackground = (HBRUSH) (COLOR_WINDOW+1) ;

wcex.lpszMenuName = (LPCSTR) IDC_API;

wcex. IpszClassName = szWindowClass;

wcex.hlconSm = Loadlcon (wcex. hlnstance, (LPCTSTR) IDI_SMALL)

return RegisterClassEx (&wcex) ;

}

//

// FUNCTION: Initlnstance (HANDLE, int)

//

// НАЗНАЧЕНИЕ: Запоминание описателя экземпляра

// приложения и создание главного окна приложения

//

// COMMENTS:

// В этой функции мы запоминаем описатель экземпляра

// приложения в глобальной переменной и создаем

// главное окно приложения.

//

BOOL Initlnstance(HINSTANCE hlnstance, int nCmdShow)

{

HWND hWnd;

//======= Запоминаем экземпляр приложения

hlnst = hlnstance;

//======= Создаем главное окно

hWnd = CreateWindow(szWindowClass, szTitle, WSJDVERLAPPEDWINDOW,CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hlnstance, NULL),

if (IhWnd) {

return FALSE; }

ShowWindow(hWnd, nCmdShow);

UpdateWindow(hWnd) ;

return TRUE; }

//

// FUNCTION: WndProc(HWND, unsigned, WORD, LONG)

//

// НАЗНАЧЕНИЕ: Обработка сообщений главного окна.

//

// WM_COMMAND - обработка команд меню

// WM_PAINT - перерисовка окна

// WM_DESTROY - посылка сообщения о завершении и выход


//

//

LRESULT CALLBACK WndProc (HWND hWnd, UINT message,

WPARAM wParam, LPARAM IParam)

{

int wmld, wmEvent;

PAINTSTRUCT ps;

HDC hdc;

switch (message)

{

case WM_COMMAND:

wmld = LOWORD (wParam) ;

wmEvent - HIWORD (wParam) ;

//====== Расшифровка выбора в меню:

switch (wmld)

{

case IDM_ABOUT:

DialogBox (hlnst, (LPCTSTR) IDD_ABOUTBOX, hWnd,

(DLGPROC)About) ;

break;

case IDM_EXIT:

DestroyWindow(hWnd);

break;

default:

return DefWindowProc(hWnd, message, wParam, IParara);

{

break;

//======= Ветвь перерисовки содержимого окна

case WM_PAINT:

hdc = BeginPaint(hWnd, sps);

//======= TODO: Вставьте сюда рисующий код

EndPaint(hWnd, Sps);

break; //======= Ветвь закрытия окна

case WM_DESTROY:

PostQuitMessage(0);

break; default:

return DefWindowProc(hWnd, message, wParam, IParam);

}

return 0;

}

//======= Обработчик команды вызова диалога About

LRESULT CALLBACK About(HWND hDlg, UINT message,

WPARAM wParam, LPARAM IParam)

{

switch (message)

{

//======= Ветвь инициализации окна диалога

case WM_INITDIALOG:

return TRUE;

//======= Ветвь обработки команд, исходящих

//======= от элементов управления диалога

case WM_COMMAND:

if (LOWORD(wParam) == IDOK

LOWORD(wParam) == IDCANCEL)

EndDialog(hDlg, LOWORD(wParam));

return TRUE;

}

break;

}

return FALSE;

}


Традиционное Windows-приложение


Программы, управляемые событиями

Прохождение сообщений в системе

Структура Windows-приложения

Стартовая заготовка приложения Win32 и ее анализ

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

Меню и диалог

Развитие начальной заготовки

Управление пером Windows