Азбука программирования в Win32 API

         

Азбука программирования в Win32 API

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

По моему мнению, такой подход к изложению основ Win32 API методологически неверен. Я решил пойти по другому пути: сначала рассказать читателю о том, какие возможности предоставляет Win32 API, и только потом научить его создавать пользовательский интерфейс. Тем самым я постараюсь достичь того, что программист будет понимать возможности системы. Это позволит ему создавать более интересные программы.


Когда я начал изучение программирования под Windows, в поле моего зрения была одна единственная книга - «Programming Windows» Чарльза Петцольда (Charles Petzold). Сейчас я могу сказать, что эта книга просто великолепна! Буквально первые несколько глав позволили мне, как говорится, «въехать» и далее работать самостоятельно, используя впоследствии эту книгу как справочное пособие.

При написании своей книги я предполагал дать быстрое и возможно более полное введение в программирование в Windows NT и Windows'95. При этом мне хотелось, чтобы материал, изложенный в ней, был полезен как начинающему программисту, так и специалисту, имеющему опыт в написании программ для Windows (в этой книге понятие «Windows» соответствует, как правило, Windows NT и Windows'95). Кроме этого, мне хотелось избежать влияния на фантазию программистов, поэтому примеры, приведенные в книге, не являются законченными. В них показано, каким образом можно использовать возможности Win32 API. Предполагается, что читатель знаком с языком программирования С и имеет опыт работы с Windows'95 или Windows NT в качестве пользователя.

Автор рассчитывает на то, что


А теперь - ВНИМАНИЕ! Автор рассчитывает на то, что в момент чтения этой книги читатель будет сидеть за компьютером и вслед за автором пройдет по заголовочным файлам Win32, файлам ее системы помощи, и будет экспериментировать с теми заготовками программ,

которые приводятся автором. В книге дано описание некоторых типов, применяемых в Win32, но, тем не менее, автор предполагает, что при описании переменных, типы которых не относятся к основным типам, определенным в языке C/C++, читатель проявит любознательность и найдет описание переменной в одном из заголовочных файлов Win32.

В книге неоднократно делаются ссылки на «заголовочные файлы» (header'ы) Win32. Одним из отличий Win32 от Windows 3.x является наличие в SDK не одного файла заголовков windows.h, а множества заголовочных файлов. Их объем по сравнению с Windows 3.x вырос не менее чем на порядок. Изучайте их! Ответы на множество вопросов вы найдете только там! По возможности, упоминаемые макросы и значения приведены в виде таблиц. Как правило, таблицы состоят из трех колонок - макрос, числовое значение и описание. Это сделано для того, чтобы читатель смог сам определить, в каком виде ему использовать то или иное значение - в числовом (скажем, для использования в цикле) или в виде макроса. Если в таблице пропуск, то это означает, что у автора нет полной информации о том или ином макросе.

Автор обращает внимание читателя на одну из особенностей про­граммирования в Win32 API . Windows нельзя знать частично. Даже самые первые программы уже требуют глубоких знаний. Поэтому читатель должен с пониманием отнестись к многочисленным ссылкам на последующие разделы книги типа «А об этом мы поговорим попозже». На каком-то этапе чтения книги все станет на свое место.

Под словом «Win32 API» автор понимает совокупность функций, предоставляющих программисту возможность создавать программы (или приложения, что то же самое) для Windows NT и Windows'95. Естествен­но, что эти платформы разнятся между собой. Но набор функций, состав­ляющих API, для них один и тот же. Все функции этого набора являются 32-битными, что отражено в названии интерфейса. При употреблении термина «Win32 API» подразумевается именно набор функций. Когда в тексте встречается термин «Win32», читатель должен понимать, что речь идет о совокупности платформ (Windows NT и Windows'95), поддержи­вающих 32-битный интерфейс программирования. В тех случаях, когда говорится о Windows, автор говорит о двух упоминаемых выше операци­онных системах. Случаи упоминания Windows 3.x оговорены особо.



GETTING STARTED - ДАВАЙТЕ НАЧНЕМ! «HELLO, WORLD!» ДЛЯ WIN32 API

После появления книги Кернигана и Рнтчи «Язык программирования С» в мире программирования одним стандартом стало больше. С их легкой руки сейчас практически каждое руководство по обучению про­граммированию начинается с написания программы, осуществляющей вывод на экран строки «Hello, world!». Я не буду нарушать эту традицию и изучение программирования дня Win32 мы начнем с программы, выводящей строку «Hello, world!» не просто на экран, а в окно.

ЧТО НЕОБХОДИМО ДЛЯ ПОЛУЧЕНИЯ ИСПОЛНЯЕМОГО МОДУЛЯ?



Для получения исполняемого модуля необходимо иметь: установленную на вашем компьютере операционную система Windows'95 или Windows NT;

систему подготовки программ для Win32 (автор для написания про­грамм, приведенных в книге, использовал Borland С++ 5.0);

отладчик для отладки программ (автор пользовался Turbo Debugger'oм для 32-битовых программ, входящим в комплект поставки Borland C++ 5.0).



Для повседневной работы было бы неплохо иметь под рукой рас­печатки стандартных файлов заголовков, которые используются при программировании в Win32. С этим связана определенная трудность. Если в Windows 3.x был один файл заголовков «windows.h», то в Win32 число файлов возросло минимум на порядок. Соответственно, вырос и объем этих файлов. Сейчас он приближается к мегабайту.

ФАЙЛЫ ПРОГРАММЫ ДЛЯ WINDOWS



Перед началом любого дела необходимо представлять, с чего начать, что необходимо получить в результате и каким образом можно выпол­нить эту работу.

Желаемый результат очевиден - мы хотим получить программу для Windows NT или Windows'95, осуществляющую вывод в окно строки «Hello, world!». Но если в DOS для получения исполняемого файла нужен, как минимум, один файл исходного модуля, то в Windows дело обстоит несколько иначе. Как минимум, в Windows проект состоит из двух, а иногда - из трех файлов. Этими файлами являются:

программа на C/C++. Является основным файлом программы и, как правило, имеет расширение .С или .СРР. После его успешной компиля-



ции возникает файл с расширением .OBJ, который используется линке­ром для получения исполняемого модуля;

файл ресурсов. Иногда может не присутствовать в простом проекте. Как правило, имеет расширение .RC. После успешной компиляции его компилятором ресурсов возникает файл с расширение .RES, который используется линкером для получения исполняемого модуля;

файл определения модуля. Обычно имеет расширение .DEF и компи­ляции не подлежит. Используется линкером для определения некоторых характеристик исполняемого модуля. С появлением Win32 файл опреде­ления модуля почти не используется.

Для программиста, привыкшего к DOS, все это выглядит непривычно и громоздко. Тем не менее, в самом ближайшем будущем мы увидим, какие громадные возможности предоставляют файл ресурсов и файл определения модуля!

ТИПЫ ДАННЫХ, ПРИМЕНЯЕМЫЕ В WINDOWS



При первом взгляде на программу, написанную для Windows, броса­ется в глаза странный внешний вид этой программы. В программе ис­пользуются переменные каких-то необычных типов, например, H1NSTANCE, HWND, LPSTR и так далее. Разобраться в них совсем не сложно. Все они определены в заголовочных файлах Win32, общим «предком» которых является знаменитый «windows.h». Возникает зако­номерный вопрос: для чего были определены столько новых типов? Почему для определения переменных нельзя было воспользоваться стандартными типами, определенными в С/С—? Во-первых, что станет очевидно даже при небольшом опыте программирования для Win32, это очень удобно. Использование типов, специально «изобретенных» для Windows, упрощает написание программы, не заставляя запоминать последовательности многочисленных описаний, а применять один описа­тель. Во-вторых, программы, написанные с применением такого стиля, легко читаются и ошибки, связанные с типами переменных, легче обна­ружить.

Возможно, истинная причина подобных нововведений лежит несколь­ко глубже. Дело в том, что применение такого двухступенчатого опреде­ления типов (стандартный тип->заголовки Win32-^nporpaMMa) облегчает перенос программ для Windows в другие операционные системы. При переходе на новую систему достаточно будет изменить только файл заголовков. Изменять отлаженное программное обеспечение пет необ­ходимости. Поэтому один и тот же код можно использовать в различных системах.



ВЕНГЕРСКАЯ НОТАЦИЯ



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

каждое слово в имени переменной пишется с прописной буквы и слитно с другими словами, например, идентификатор для обозначения какой то переменной может выглядеть следующим образом - MyVariable, YourVariable, VariableForSavingAnotherVariable и т. д.;

каждый идентификатор предваряется несколькими строчными буквами, определяющими его тип. Например, целая перемятая MyVariable должна выглядеть как nMyVariable (n - общепринятая для целых переменных), символьная (char) переменная YourVariable превратиться в cYourVariable. Указатель на строку символов заканчивающуюся нулевым байтом, VariableForSavingAnotherVariable, pszVanableForSavingAnotherVariable (psz - сокращение от Point то String with Zero). Примеры подобных префиксов приведены в табл. 1.

Это позволяет упростить процесс чтения и понимания программ, а также делает переменные в некотором смысле самоопределенпыми - имя переменной определяется ее типом. Говорят, когда Симонаи спрашивали о странном внешнем виде его программ, он невозмутимо отвечал, что эти программы написаны по-венгерски.

WINDOWS КАК ОБЪЕКТНО-ОРИЕНТИРОВАННАЯ СИСТЕМА



Когда вы начинаете программировать для Win32, необходимо уяснить, что хотя формально Windows не является объектно-ориентированной систе­мой, она придерживается объектно- ориентиро- ванной идеологии. Каждое окно фактически является объектом. Что такое объект? Фактически объект есть совокупность полей (данных) и методов (процедур и функций управле­ния полями). У окна есть масса полей, об этом мы еще будем говорить. Функция окна фактически является совокупностью методов.



Таблица  1. Префиксы, применяемые в венгерской нотации

Префикс

Тип данных

b сх, су

dw In h i I

n s sz

BYTE (unsigned char)

short (используются как ширина и длина объектов типа

RECT и окон)

DWORD (unsigned long)

function

HANDLE

int

EONG (long)

int или short

string

string terminated by zero

WORD (unsigned int)

short (используются как координаты)

char

«КРОВЕНОСНАЯ СИСТЕМА» ПРОГРАММЫ ДЛЯ WINDOWS



Для программиста, привыкшего к DOS, непривычной является и ор­ ганизация взаимодействия между операционной системой Windows (NT или 95) и другими программами. В DOS только программы обращаются к операционной системе. Обратной связи (вызов системой прикладной программы) в DOS нет (исключения типа перехвата прерываний не в счет). В Windows с момента создания окна и до момента его уничтожения не только программа обращается к Windows, но и самое операционная система при возникновении определенных событий обращается к окну, вызывая связанную с ним оконную процедуру, или, как говорят, посылая окну сообщение. Что значит послать окну сообщение? Это, значит, записать определенную информацию о каком-либо событии в область памяти, доступную оконной процедуре. Эта область памяти, которая вмещает в себя несколько сообщений, действует по принципу стека FIFO (First Input - First Output) и называется очередью программы. В Windows прикладная программа тоже вызывает систему не напрямую, а посылает сообщения системе. Но раз системе, как и прикладной программе, посы­лаются сообщения, то, значит, существует и общесистемная очередь сообщений! Итак, мы пришли к выводу о существовании одной обще­системной очереди сообщений и очереди сообщений у каждого окна.

Неясной остается одна деталь. Откуда система знает о том, что при­шло сообщение? Каким образом сообщение из очереди становится из-

8

вестным программе? Вероятно, как программа, так и система с какой-то периодичностью обращаются к очереди и проверяют, нет ли в очереди сообщений. Здесь мы приходим ко второму выводу - у каждой програм­мы, а также и у системы должны существовать (и существуют!) циклы, в ходе которых опрашивается очередь и выбирается инфор­мация о сообщениях в ней. Остановка цикла опроса очереди приведет к «зависанию» программы, программа «умрет», если сравнивать програм­му для Windows с человеческим организмом. Если продолжать сравне­ние, то будет видно, что сообщения протекают через функцию окна, как кровь по организму. Кровь в организме прокачивается сердцем, а сооб­щения «качаются» циклом обработки сообщений.



WINMAIN () + ФУНКЦИЯ ОКНА = МИНИМАЛЬНАЯ ПРОГРАММА ДЛЯ WINDOWS



Теперь мы уже знаем, что при запуске программы должны происхо­дить, по меньшей мере, два события - должно быть создано окно и запу­щен цикл обработки сообщений, из которого с наступлением какого-то события должен быть осуществлен выход и работа программы должна завершиться. Все это происходит, как правило, в функции WinMamQ, которая является стандартной точкой входа во все программы для Windows. (Обратите внимание - функция mainQ является точкой входа DOS'oBCKiix программ, функция WinMain() - программ, написанных для Windows). Для удобства функция окна отделена от WinMain() (к функ­ции окна постоянно обращается система). Она может находиться либо в той же программе, что и WinMain(), либо вызываться из какой-либо библиотеки. Тем самым становится возможным создавать множество окон, использующих одну и ту же оконную функцию (другими словами, объектов, использующих одни и те же методы!), но имеющих разные характеристики (но имеющих разные значения полей!). А не напоминает ли это каким-то образом полиморфизм объектов? Попутно отмечу, что совокупность окон, использующих одну и ту же оконную функцию, представляет собой класс окон. Каждое окно принадлежит какому-либо классу. Примером такого ктосса могут быть кнопки, работающие совер­шенно одинаково, но имеющие разные размеры, надписи и так далее. Так что же получается'? Мы сначала должны создать класс, а только потом создавать окно созданного класса'? Да! Попробуем резюмировать сказанное.

Па некотором псевдоязыке программу для Windows можно записать следующим образом:

WinMain (список аргументов)

Подготовка и создание класса окон с заданными характеристиками Создание экземпляра окна только что созданного класса; Пока не произошло необходимое для выхода событие

Опрашивать очередь сообщений и передавать сообщения

оконной функции; Возврат из программы;

WindowFunction (список аргументов)

{

Обработать полученное сообщение; Возврат;

ПЕРВАЯ ПРОГРАММА ДЛЯ WINDOWS





Ниже приведен текст программы (вывод в окно строки, о ней говори­лось ранее). Мне бы хотелось, чтобы читатель быстро просмотрел про­грамму и попытался разделить ее на части, соответствующие операторам псевдоязыка. Вот текст этой программы:

#includc <windows.h>

ERESUET CAEEBACK HelloWorldWndProc ( HWND, UINT, UINT, LONG ); hit WINAPI WinMain ( HTNSTANCF, hlnstance, HINSTANCE hPrevinstance,

EPSTR IpszCmdParam, int nCmdShow )

i

HWND hWnd ;

WNDCLASS WndClass ;

MSG Msg;

char szClassNamef] - «HclloWorld»; /* Регистрируем создаваемый класс */ /* Заполняем структуру типа WNDCLASS */

WndClass.stylc = CS_HREDRAW | CS_VREDRAW;

WndClass.lpfnWndProc - HelloWorldWndProc;

WndClass.cbClsExtra = 0;

WndClass.cbWndExtra = 0;

WndClass.hlnstance = hlnstance ;

WndClass.hlcon = Eoadlcon (NULL,IDI_APPLICATION);

WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);

WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);

WndClass.IpsxMenuName = NUEE;

WndClass.IpszClassName = szClassName;

if ( !RegistcrClass(&WndClass))

McssageBox(NULL,«Cannot register class»,»Error»,MB _OK); return 0;

hWnd = CreateWindow(szClassName, «Program No 1», WS_OVERLAPPEDWINDOW, CWJJSEDEFAUET, CWJJSEDEFAUET, CWJJSEDEFAUET, CWJJSEDEFAULT, NUEE, NUEE, hlnstance, NUEE);

if(!liWncl)

MessageBox(NULL,»C'annot create window»,»F,rror»,MB_OK); return 0;

/* Show our window */

Sho\vWindow(hWnd,nCmdShow); UpdaleWindow(hWnd);

/* Beginning of messages cycle */

whi!e(GetMcssagc(&Msg, NUEE, 0, 0)) i

TranslateMessage(&Msg); DispatchMcssage(&Msg); } return Msg.wParam;

ERESUET CALLBACK HelloWorldWndProc (HWND hWnd, UINT Message,

DINT wParam, LONG IParam ) !

HOC HUG;

PAINTSTRUCT PaintSlrucl; RECT Reel;

switch(Message)

i t

case WMJ'AINT:

hDC = BcginPaint(hWnd, &PaintStnict);

GctClicntRect(hWnd,&Rect);

DrawText (hDC,»Hello. World!», -1, &Rect,

DT_SINGLELINE i DE_CENTER j DT VCENTER);

EndPainUhWnd.&PamtStruct);

return 0; case WM_DF.STROY:

PostQuitMessagc(O),



return 0;

return DefWindowProc(hWnd,Message.wParam, IParam);

11

Листинг № 1. Основной файл программы «Hello, world».

Предлагаю читателю откомпилировать основной файл программы. Для начинающих программистов проще всего воспользоваться интегри­рованной средой, например, Borland IDE. При этом не имеет значения, какой системой подготовки программ вы пользуетесь. Единственное, эта система должна позволять разработку программ для Win32. (В этой программе нет ничего, что присуще только Win32 и не присуще Windows 3.x. Но это только в первой программе.) Надеюсь, что эта программа пройдет у вас без ошибок с первого раза. Если же появятся ошибки, сверьте набранный вами текст модуля с приведенным в книге. По всей вероятности, вы допустили ошибку при наборе программы.

МИНИМУМ КОДА ПРИ МАКСИМУМЕ ВОЗМОЖНОСТЕЙ

На рис. 1 приведен результат работы этой программы. Естественно, что для простого вывода строки на экран эта программа велика. Но в том-то и состоит прелесть окна «Hello, world!», что оно обладает всеми харак­теристиками нормального окна, «умеет» изменять свой размер, миними­зироваться (отображаться в виде иконки), максимизироваться (занимать все пространство экрана). У него есть системное меню в левом верхнем углу и три кнопки (максимизации, минимизации и закрытия) вверху справа. Окно может перемещаться по экрану. При изменении размеров окна строка «Hello, world!» автоматически перемещается в новый центр окна. Попробуйте в DOS достичь того же самого программой такого же объема! В DOS для достижения таких же результатов потребуются либо месяцы упорного труда для реализации собствен ной оконной библиоте­ки, либо придется использовать чужую оконную библиотеку, что приве­дет ко многим нежелательным эффектам, типа резкого увеличения объе­ма исполняемого кода. С этой позиции объем «Hello, world!» кажется слишком компактным по сравнению с обычной программой, обладающей такой же функциональностью!

Теперь, когда мы увидели возможности «Hello, world!», попробуем разобрать ее построчно.



Практически каждая программа ( а наша программа исключением не является) начинается со строки

#include <windows.h>

Думаю, что к этому моменту строка в пояснениях уже не нуждается. В тело программы включается файл заголовков «windows.h».

Следом за строкой идет объявление оконной процедуры:

LRESULT CALLBACK HelloWorldWndProc (HWND, TJINT, UINT, LONG);

12

ЙЧЦЧМ Ho 1

Hellfl.Wort*

Рис. l. Результат работы «HelloWorld»

Его мы разберем при рассмотрении непосредственно оконной функ-

Третьей строкой является функция WinMain(), о которой мы сейчас и поговорим.

ФУНКЦИЯ WINMATNO И ЕЕ АРГУМЕНТЫ

С легкой руки автора одной из книг, посвященных программированию для Windows, функция WinMainQ называется «стандартным заклинани­ем». Без этих или подобных строк не обходится почти ни одна програм­ма. Как правило, программирующие для Windows хранят это «заклинание» в отдельном файле. В начале разработки нового проекта в этом файле просто изменяют несколько слов или строк - и функция WinMainQ вновь готова к работе! Определение WinMainQ всегда имеет вид, подобный следующему:

ii.t WINAPI WinMain (HINSTANCF. hlnstancc. HINSTANCE hPrevInstance, Ll'STK ipszCmdParam. int nCmdShow).

Сразу видно, что функция возвращает вызвавшей ее системе целое значение и ничего интересного этот момент собой не представляет. Следующая характеристика - WINAPI - определяет порядок передачи

параметров при вызове процедуры. Наименование характеристики гово­рит само за себя - WINdows Application Programming Interface - применя­ются соглашения о передаче параметров, принятые в системах Windows NT и Windows'95. Если вы не планируете писать приложения на ассемб­лере, вам нужно это просто запомнить.

А вот переменные hlnsbnce и hPrevInstance заслуживают более под­робного обсуждения. Так как Windows NT и Windows'95 являются мно­гозадачными системами, то очевидно, что одна и та же программа может быть запущена несколько раз. Для того чтобы различать экземпляры программ, каждому экземпляру присваивается условный номер - хэндл (handle). Справедливости ради, надо отметить, что в Win32 присваивают­ся хэндлы чему угодно - окну, меню, курсору, иконке и т. д. Фактически хэндл - это указатель на блок памяти, в котором размешен тот или иной объект. В заголовочных файлах тип HANDLE определен как void*, а тип HINSTANCE как HANDLE. Согласно венгерской нотации, идентифика­торы переменных типа HANDLE



должны начинаться с буквы h.

Уважаемый читатель! Обратите внимание на вытекающее из этого по­ложения следствие. Раз уж объект имеет хэндл, который является УКАЗАТЕЛЕМ, то, значит, этот объект сам расположен в памяти! Дру­гими словами, в тех случаях, когда мы должны получить хэндл того или иного объекта, фактически мы должны получить адрес загруженного в память объекта!

Но вернемся к hlnstance. Когда вызывается WinMain(), Windows через эту переменную сообщает программе хэндл экземпляра программы. В Windows 3.1 hPrevInstance являлся хэндлом предыдущего экземпляра программы. Если запускался первый экземпляр программы, то параметр hPrevInstance был равен нулю. Этот факт можно было использовать для того, чтобы не позволять системе запускать более одного экземпляра программы. В Win32 hPrevInstance оставлен ИСКЛЮЧИТЕЛЬНО для совместимости с предыдущими версиями Windows, он не несет никакой нагрузки и постоянно равен нулю. Так просто, как в более ранних верси­ях Windows, определить наличие ранее запущенного экземпляра про­граммы не удастся. Придется нам и этот вопрос оставить на потом, до изучения основ многозадачности Windows.

Следующий параметр - pszCmdLine - представляет собой указатель на строку, ту командую строку, которая набирается после имени запускае­мой программы. При необходимости программа может проанализировать этот аргумент и выполнить те или иные действия.

И последний параметр - nCmdShow - определяет, в каком виде созда­ваемое окно будет появляться на экране. Окно может появляться в максимизированном виде либо в виде иконки (минимизированном),

может иметь произвольный размер, определяемый программой и другие характеристики. В Win32 API определяются десять возможных значений этого параметра. Их идентификаторы начинаются с SW (вероятно, от названия функции ShowWindow, которая использует эти значения). Наиболее часто используются значения SWJSHOWNORJvIAL и SW_ SHOWMINNOACTIVE. Возможные значения этого параметра приведены в табл. 2. Большинство идентификаторов являются самоопре­деленными (вряд ли, скажем, SW_SHOWMAXIMIZED приводит к ото­бражению окна в виде иконки!). Вы можете поэкспериментировать с ними. Их полное описание можно найти в файлах системы помощи. Теперь вспомним, что перед созданием окна мы должны сначала опреде­лить его класс, поэтому у нас на очереди



Регистрация класса окна

Сразу после входа в WinMainQ нам необходимо создать класс окна и сообщить о нем системе. Класс создается и регистрируется функцией RcgistcrClassQ. Единственным аргументом этой функции является указатель на структуру типа WNDCLASS, в которой хранятся характери­стики создаваемого класса. Из этого следует, что у нас добавилось голов­ной боли - перед регистрацией класса заполнить процедуру типа WNDCLASS. В приведенной выше программе структура была определе­на следующим образом:

WNDCLASS WndClass;

Т а б л и п а 2. Возможные значения второго парамелра функции ShowWindowQ

Параметр

Значение

 

Параметр

 

Значение

 

SW HIDE

 

0

 

SW SHOWNOACTIVE

 

4

 

HIDE WINDOW

 

0

 

SHOW OPENNOACTIVE

 

4

 

SW SHOWNORMAL

 

I

 

SW SHOW

 

5

 

SW NORMAL

 

I

 

SW MINIMIZE

 

6

 

SHOW OPENWINDOW

 

I

 

SW SHOWMINNOACTIVE

 

7

 

SW SHOWMINIMIZKD

 

2

 

SW^SHOWNA

 

8

 

SHOW ICONWINDOW

 

2

 

SW RESTORE

 

9

 

SW SHOWMAXIMIZED

 

3

 

SW SHOWDEFAULT

 

Ю

 

SHOW FULL SCREEN

 

3

 

SW MAX

 

Ю

 

SW MAXIMIZE

 

3

 

 

 

 

 

15

Не забывайте, что в языке С, в отличие, скажем, от PASCALS, про­писные и строчные буквы различаются. Для того чтобы заполнить эту структуру, нам необходимо знать тип и назначение каждого ее поля. Посмотрим, как описана эта структура в заголовочных файлах (winuser.h):

typedef struct tagWNDCLASSA {

UTNT       style;

WNDPROC     IpfnWndProc;

ml         cbClsExtra;

int         cbWndExtra;

HINSTANCE   hlnstance;

HICON       hlcon;

HCURSOR     hCursor;

HBRUSH      hbrBackgroimd;

LPCSTR      IpszMcnuNamc;

LPCSTR      IpszClassName: } WNDCLASSA, *PWNDCLASSA, NEAR *NPWNDCLASSA,



FAR *LPWNDCLASSA; typedef struct tagWNDCLASSW {

UINT        style;

WNDPROC     IpfnWndProc;

int         cbClsExtra;

int         cbWndExtra;

HINSTANCE   hlnstance;

HICON       hlcon;

HCURSOR     hCursor;

HBRUSH      hbrBackgroimd;

EPCWSTR     Ips/MenuName; LPCWSTR     IpszClassName; } WNDCLASSW, *P\VNDCLASSW, NEAR *NPWNDCLASSW,

FAR "LPWNDCLASSW; tfifdef UNICODE

typcdcf WNDCLASSW WNDCLASS; typedcfPWNDCLASSWPWNDCLASS; typedef NPWNDCLASSW NPWNDCLASS; tvpedef LPWNDCLASSW LPWNDCLASS;

#clsc

typedef WNDCLASSA WNDCLASS; typedcf PWNDCLASSA PWNDCLASS; typedef NPWNDCLASSA NPWNDCLASS; typedef LPWNDCLASSA LPWNDCLASS;

#cndif/7 UNICODE

О том, почему структура объявляется так странно и что такое Unicode, мы поговорим в разделе, посвященном Unicode. А пока давайте считать, что это просто разные описания одной и той же структуры.

Особое внимание следует обратить на первое, второе и последнее по­ля. Почему? - Сейчас станет ясно.

Стиль окна я определяю оператором

WndCIass.style = CS HREDRAW   CS VREDRAW

В winuser.h описаны тринадцать стилей окна. Наименования их иден­тификаторов начинаются с CS, что, вероятно, означает «Class style». Для спим окна отведено 16 битов и только один из этих битой установлен в единицу. Другими словами, стили, упомянутые в winuser.h, используются как битовые флаги, т. е. с этими стилями можно производить операции логического сложения п логического умножения для получения комбинированных стилей. Перечень шагов приведен в табл. 3.

Я отдаю себе отчет в том, что сейчас описания многих флагов совер­шенно непонятны, но через несколько разделов все будет ясно.

По причине, известной только Microsoft, отсутствуют стили со значениями 0x0010 и 0x0400. Те флаги, которые использует программа «Hello, world!», означают, что окну необходимо полностью перерисовать­ся (запомните это слово. О перерисовке мы еще не раз вспомним!) при изменении его размеров по горизонтали и по вертикали. Попробуйте поиграть с размерами окна и сделать так, чтобы строка появилась не в середине! Надеюсь, вам это не удастся. Как бы вы не дергали его, текст постоянно будет оставаться в центре экрана.



Не будет преувеличением сказать, что второе поле структуры WNDCLASS особенно значимо, - в нем хранится указатель на оконную функцию создаваемого в программе класса окон. Эта функция будет производить обработку абсолютно всех сообщений, получаемых окном, другими словами, значение этого поля полностью определяет все поведение окна. В программе это поле инициализируется следую­щим образом:

WndClass.lptiiWndProc - IlelloWorldWndProc;

Два следующих поля оставлены нулевыми. Дело в том, что для каждо­го класса Windows создает где-то в недрах своей памяти структуру с характеристиками класса. Другая структура создается для каждого окна. При создании этих структур может быть зарезервировано некоторое количество памяти для нужд программиста. Поля cbClsExtra и cbWndExtra указывают размер резервируемой памяти в структурах класса и окна соответственно. Эти поля и раньше использовались доста­точно редко, а с появлением Windows'95 и Windows NT будут использо­ваться еще реже.

Таблица 3. Перечень битовых флагов

Флаг

Значение

 

Описание

 

 

 

CSJVREDRAW

 

0x000 1

 

Перерисовать окно при изменении высоты

 

 

 

 

 

 

 

окна

 

 

 

CS_HREDRAW

 

0x0002

 

Перерисовать окно при изменении

 

 

 

 

 

 

 

ширины окна

 

 

 

CS KEYCVTWINDOW

 

0x0004

 

 

 

 

 

CS_DBLCLKS

 

0x0008

 

Посылать сообщение оконной функции

 

 

 

 

 

 

 

при двойном щелчке мышью, если курсор

 

 

 

 

 

 

 

находится в пределах окна

 

 

 

CS_OWNDC

 

0x0020

 

Для каждого окна класса выделяется

 

 

 

 

 

 

 

собственный контекст

 

 

 

CS_CLASSDC

 

0x0040

 

Один и тот же контекст устройства

 

 

 

 

 

 

 

разделяется всеми окнами этого класса

 

 

 

CS_PARENTDC

 

0x0080

 

Дочерние окна наследуют контекст

 

 

 

 

 

 

 

родительского окна

 

 

 

CS NOKEYCVT

 

0x0 1 00

 

 

 

 

 

CSJMOCLOSE

 

0x0200

 

Убрать команду «Close» из системного

 

 

 

 

 

 

 

меню

 

 

 

CS_SAVEBITS

 

0x0800

 

Сохранять часть области экрана, закры-

 

 

 

 

 

 

 

тую окном, как bitmap, при удалении

 

 

 

 

 

 

 

восстанавливать перекрытую область

 

 

 

CSJ3YTEALIGNCLIENT

 

Ox 1 000

 

Выравнивает границу рабочей области

 

 

 

 

 

 

 

окна (в горизонтальном направлении)

 

 

 

 

 

 

 

таким образом, чтобы для отображения

 

 

 

 

 

 

 

строки требовалось целое число байтов

 

 

 

CS BYTEALIGNWINDOW

 

0x2000

 

То же, но действие затрагивает все окно

 

 

 

CS_GLOBALCLASS

 

0x4000

 

Разрешается создавать класс, не завися-

 

 

 

 

 

 

 

щий от текущего hlnstancc

 

 

 

CSJME

 

OxOOOIOOOOL

 

 

 

 

 

<


Поле hlnstance в объяснении не нуждается - классу окна сообщается хэндл программы.

Оператор

WndClass.hlcon - LoadIcon(NULL, IDI_APPLICATION);

определяет хэндл иконки, которая будет символом окна данного класса. Действие, производимое функцией LoadlconQ, очевидно из ее названия -загрузить иконку. Заметим, что программист может использовать собст­венную иконку, которую он сам разработал, а может применить одну из иконок, хранящихся в глубинах системы (они называются предопреде­ленными). В случае использования собственной иконки первый параметр функции LoadlconQ должен быть равным хэндлу программы (hlnstance). Если мы используем предопределенную иконку, первый параметр равен

пулю (забегая вперед, отметим, что если при загрузке в память какого-либо объекта хэндл программы равен нулю, то объект загружается либо из «глубин» Windows, либо из внешнего файла). Второй параметр - это идентификатор иконки. Все идентификаторы предопределенных иконок начинаются с букв IDI

(возможно, «IDentificator of Icon»). Пока еще мы не знаем, как формировать иконки, воспользуемся одной из предопре­деленных иконок.

Сказанное об иконке можно полностью отнести и к курсору мыши, которым будут пользоваться окна создаваемого класса (не путать с курсором, применяемым при редактировании текстовых файлов). Поле WndClass.hCursor определяет хэндл курсора. Все идентификаторы предопределенных курсоров начинаются с IDC (возможно, «IDentificator of Cursor»).

Поле WndCIass.hbrBackground определяет хэндл та называемой кис­ти (brush), которой будет закрашен фон окна.

К иконкам, курсорам, кистям и перьям мы еще неоднократно будем возвращаться. А сейчас неплохо было бы попробовать поменять иденти­фикаторы иконок, курсоров, кистей и посмотреть, к чему это приведет. Для этого в табл. 4 приведен список объектов этих типов, индификаторы которых я нашел.

Поле WndClass.lps/MenuName храпит указатель на строку, содер­жащую имя меню для данной программы. Наша программа с меню не работает, поэтому мы сделали его нулевым.



И последнее, завершающее поле - WndClass.Ips/ClassName. Как яв­ ствует из его названия, поле содержит указатель на строку, содержащую имя создаваемого нами класса окна. Указав это имя, мы тем самым поставили логическую точку в формировании структуры WNDCLASS.

Указатель на эту структуру передается функции RegistcrClassQ. С вызовом этой функции, данные о создаваемом нами классе становятся известными системе Windows, и с этого момента мы можем создавать окна этого класса. Пожалуйста, не забывайте в своих программах прове­рять, зарегистрировался класс или нет. Если класс не зарегистрирован, работать ваша программа не будет, как бы правильно она не была напи­сана. В нашей программе, в случае, если класс окна не зарегистрирован, просто выдается сообщение об ошибке (функция MessageBoxQ) и осуще­ствляется выход из программы. Кстати, мы уже говорили о предопреде­ленных иконках и курсорах? В Win32 API существует множество предо­пределенных классов окоп, например класс кнопок, списков и т. д. При необходимости создания окна предопределенного класса регистрировать класс окна уже не нужно.

18

Таблица 4. Список предопределенных объектов в Win32 АРГ

Иконка

 

Перо

 

Курсор

 

Кисть

 

IDI_APPLICATION

 

WHITE PEN

 

IDC_ARROW

 

WHITE BRUSH

 

IDI HAND

 

BLACK PEN

 

IDC IBEAM

 

LTGRAY BRUSH

 

IDI QUESTION

 

NULL PEN

 

me wait

 

GRAY BRUSH

 

IDl" EXCLAMATION

 

 

 

IDC CROSS

 

DKGRAY BRUSH

 

IDI ASTERISK

 

 

 

IDC UPARROW

 

BLACKJiRUSH

 

1DI_WINLOGO

 

 

 

IDC_SIZE

 

NULL BRUSH

 

IDI WARNING

 

 

 

IDC ICON

 

HOLLOW BRUSH

 

IDI ERROR

 

 

 

IDC SIZENWSC

 

 

 

IDI INFORMATION

 

 

 

IDC""SIZUNF,SW

 

 

 

 

 

 

 

IDC SIZEWE

 

 

 

 

 

 

 

IDC SIZENS

 

 

 

 

 

 

 

IDC SIZEALL

 

 

 

 

 

 

 

IDC~NO

 

 

 

 

 

 

 

IDC APPSTARTING

 

 

 

 

 

 

 

IDCMIELP

 

 

 

<


Создание экземпляра окна

Следующим шагом на нашем большом пути является создание экзем­пляра окна. Как и экземпляр программы, каждое окно в системе имеет свой уникальный номер хэндл (handle). Обычно окно создается посред­ством функции CrcatcWindowQ, которая и возвращает хэндл созданного окна. Если (увы!) функция CreateWindowQ вернула нуль, то по каким-то причинам окно не создано. Причины могут быть как внутри вашей про­граммы, так и в системе. Но, в отличие от регистрации класса, о том, что окно не создано, вы можете узнать, просто взглянув на экран. Теперь подошло время рассказать о каждом из одиннадцати аргументов функции Create Window().

Первый аргумент - указатель на строку с именем класса, к которому будет принадлежать создаваемое нами окно. В большинстве случаев значение этого аргумента совпадает со значением последнего поля структуры типа WNDCLASS, передаваемой RegisterClass() (Может быть целесообразно использовать одну и ту же переменную в функциях RegisterClass() и CreateWindow()?). Второй аргумент - указатель на стро­ку, содержащую тот текст, который появится в заголовке окна.

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

это поле отводится 32 бита. В файле winuser.h определены несколько десятков стилей окон. Их идентификаторы начинаются с букв WS. Как и в случае со стилями класса, эти значения используются как битовые флаги, т. е. комбинируя их с помощью логических операций, можно получить тот стиль окна, который требуется нам в программе. Рекомен­дую поэкспериментировать с различными стилями окна. Их список приведен в табл. 5.

Некоторые стили, приведенные в winuser.h, представляют собой комби­нации из других стилей. В частности, тот стиль, который используем мы, \VS__OVERLAPPEDW1NDOW, тоже является комбинацией. Выбирая этот стиль, мы определяем наличие у нашего окна заголовка, системною меню, ограничивающей рамки, а также кнопок минимизации и максимизации.



Следующие четыре api- умента определяют положение окна на экране. Значение этих полей представляют измеренные в пикселах отступы левого верхнего угла окна от левого края экрана, от верхней границы экрана, ширину и высоту окна соответственно. Особых пояснений эти параметры не требуют. Используемые нами идентификаторы CW USEDEFAULT, допустимые, кстати, только для окон со стилем WS

OVERLAPPED, позволяют Win32 API установить размер окна самостоятельно.

На очереди следующий аргумент - хэндл окна, являющимся родитель­ским по отношению к нашему. Пока оставим это поле нулевым, а о взаимоотношениях окон типа предок - потомок - сосед мы узнаем, когда будем изучать иерархию окон.

Windows присваивает хэндлы чему угодно, в том числе и меню. Очередной аргумент - это хендл меню нашего окна. До разговора о меню в нашей программе меню не будет, поэтому оставим его нулевым.

Предпоследний аргумент - hlnstance - должен быть понятен из преды­дущих объяснений. Да-да, именно тот самый хэндл экземпляра програм­мы, который мы запускаем.

Последний аргумент - данные, которые используются в некоторых случаях для создания окна. Как правило, в это поле записывается указа­тель на структуры с дополнительной информацией. Для того чтобы доб­раться до него, мы потратили столько сил! А оно используется достаточно редко i i в нашем примере, естественно, остается без дела.

Напоминаю: не забудьте в программе проверить факт создания окна и огреагировать на него соответствующим образом! Справедливости ради, в одном из примеров, поставляемых с Borland C++ v. 5.0, все эти провер­ки называются параноидальными, но я пришел к выводу, что на этапе отладки программы лучше все эти проверки оставить. Когда программа заработает полностью - это дело другое.

Таблица 5. Список различных стилен окна

Стиль

WS OVERLAPPED WS_TABSTOP

WS_MAXIMIZEBOX WS GROUP WS_MINIMIZEBOX WS THICKFRAME

WS_SYSMENU WSJTSCROLL WSJVSCROLL WSJDLGFRAME

WSJBORUER WS_CAPTION WS_MAXIMIZE WS CEIPCHIEDREN

WS CEIPSIBLINGS



WS_DISABLED WS_VISIBLE \VS_MINIMIZE WS CHILD

WS_POPUP

WS_TILED

WS_ICONIC

\VS_SIZEBOX

WS TILEDWINDOW

\VS~OVERLAPPED-

WINDOW

WS POPUP-WINDOW WS CHILDWINDOW

Значение

OxOOOOOOOOL 0x00010000L

0x00010000L Ox00020000L Ox00020000L 0x000400001.

OxOOOSOOOOL OxOOIOOOOOL Ox00200000L Ox00400000L

OxOOSOOOOOL OxOOCOOOOOL OxOlOOOOOOL Ox02000000L

Ox04000000L

OxOSOOOOOOL OxlOOOOOOOL Ox20000000L Ox4000000()L

OxSOOOOOOOL

( iiihcohhc

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

У окна есть тонкая ограничивающая рамка WS_BORDER   WS_DLGFRAME Создается изначально максимизированное окно При прорисовке родительского окна область, занимаемая дочерними окнами, не прорисовыва­ется

Дочернее окно, имеющее yroi стиль, и перекры­вающее другое дочернее окно, при прорисовке перекрываемой области не прорисовывается Создается изначально запрещенное окно Создается изначально отображаемое окно Создается изначально минимизированное окно Создается дочернее окно, имеющее по умолчанию только рабочую область, меню окна этого стиля не имеют никогда Создастся всплывающее (popup) окно \VS_OVERLAPPED \VS_MINIMIZED WS_THICKFRAME WS_OVERLAPPEDWIINDOW WSJWERLAPPED | WS CAPTION WS_SYSMENU | WS THICKFRAME WS_MINIMIZEBOX7WS_MAXIMIZEBOX WS_SYSMENU | WS_BORDER j WSJ'OPUP

WS CHILD

Но что значит создать окно? Это, значит, создать внутренние для Win32 API структуры, которые будут использоваться в течение всего

22

периода существования окна. Но это не означает, что окно тут же появит­ся на экране. Для того чтобы увидеть окно, мы должны осуществить



Отображение окна на экране

Теперь мы подошли к функции ShowWindowQ. Функция отображает окно на экране (отметьте - отображает окно как набор атрибутов, таких как заголовок, рамка, кнопки и т. д.). Первый аргумент этой функции -хэндл созданного только что окна. Второй аргумент определяет, в каком виде окно будет отображено на экране. В нашем случае мы просто взяли и подставили значение nCmdShow, указанное при вызове WinMain(). Как правило, при первом запуске окна функции WinMain() передается значение SW_SHOWDEFAULT, при последующих запусках значение этого параметра может изменяться в соответствии со сложившимися обстоятельствами. Я не рекомендую указывать в качестве второго пара­метра функции ShowWindowQ значение, отличное от передаваемого при вызове WinMain(). Тем самым вы лишите Win32 API некоторых возмож­ностей по управлению окном.

Функция UpdateWindowQ посылает функции окна сообщение WM_PAINT, которое заставляет окно ПЕРЕРИСОВАТЬСЯ, т. е. прори­совать не набор атрибутов, за прорисовку которых отвечает Windows, a изображение в рабочей области окна, за что должна отвечать непосредст­венно программа.

Итак, класс окна зарегистрирован, экземпляр окна создан и выдан на отображение. На очереди -

Запуск и завершение цикла обработки сообщений

Мы уже говорили, что по аналогии с человеческим организмом, со­общения играют роль крови, а цикл обработки - роль сердца, которое эту кровь прокачивает.

Что такое сообщение? Сообщение - это небольшая структура, опреде­ленная в заголовочном файле следующим образом:

typcdcf struct tagMSG )

HWND        hwnd;

UINT        message;

WPARAM      wParam;

LPARAM      IParam;

DWORD       lime;

POINT       pi; } MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

23

Первое поле этой структуры вопросов не вызывает - это хэндл окна, которому адресовано сообщение. Второе поле - номер сообщения. Каж­дое сообщение имеет свой идентификатор. Все идентификаторы сообще­ний начинаются с букв WM, что, возможно, означает «Windows Message». Третье и четвертое поля содержат параметры сообщения. Для каждого сообщения они различны. Назначение поля time в объяснении не нуждается - оно очевидно из названия. В последнее поле (pt) записывает­ся позиция, на которой находился курсор в момент выработки сообще­ния. Все эти поля могут обрабатываться оконной процедурой.



Из очереди приложений сообщение выбирается с помощью функции GetMessageQ. Первый аргумент этой функции - указатель на структуру типа MSG, в которую будет записана информация о сообщении. Второй -хэндл окна, созданного программой. Сообщения, адресованные только этому окну, будут выбираться функцией GetMessageQ и передаваться оконной функции. Если вы хотите, чтобы обрабатывались сообщения для всех созданных программой окон, установите этот параметр равным NULL.

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

Функция GetMessage() всегда, за исключением одного случая, воз­вращает непулевое значение. Исключение в данном случае очевидно -при получении этого единственного сообщения работа цикла, а следова­тельно, и программы, прекращается. Это сообщение называется WM QUIT и приводит к нормальному завершению программы. Но давайте пока оставим WM__QUIT в покое и посмотрим, что происходит внутри цикла при получении им нормального - не WM_QUIT - сообщения.

Давайте на время представим себе, что для унификации процесса об­работки сообщений некоторые из них необходимо преобразовать в более удобный для обработки вид. Эти преобразования выполняет функция TranslateMessage(). К этой функции мы еще вернемся при изучении системы обработки ввода с клавиатуры.

После преобразования сообщение готово к обработке. Функция DispatchMessageQ передает сообщение на обработку в оконную процедуру.

ВСЕ! Мы сформировали структуру типа WNDCLASS, зарегистриро­вали класс окна и создали экземпляр окна этого класса. Мы выдали окно на отображение, после чего запустили цикл обработки сообщений и предусмотрели условие, при котором работа цикла закончится. Теперь все зависит от того, как сообщения будут обрабатываться оконной про-

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



ФУНКЦИЯ ОКНА И ЕЕ АРГУМЕНТЫ

Мы закончили изучение функции WinMain. Теперь нашей ближайшей задачей будет рассмотрение оконной процедуры.

Как обработать множество различных сообщений?

Первый вопрос, возникающий при изучении функции окна, касается реакции этой функции на огромное число сообщений. Ведь в файле заголовков прямо записано, что Майкрософт резервирует для использо­вания в Win32 все сообщения с номерами менее 1024! Неужели функция окна должна обрабатывать все эти сообщения и в программе должен быть код, обеспечивающий эту обработку? Ответ на этот вопрос достаточно парадоксален. Да, оконная функция должна обрабатывать все сообщения, приходящие в адрес окна. Нет, программист не должен писать кода для обработки всех сообщений! Дело в том, что в Windows предусмотрена обработка всех сообщений по умолчанию. В программе должен присутствовать код обработки только тех сообщений, обработка которых по умолчанию не устраивает программу. Все остальные «протекают» сквозь функцию и передаются на обработку по умолчанию. Делается это простым обращением к функции DefWindowProc (Default Window Procedure). Именно эта функция, спрятанная глубоко в «недрах» Windows, производит обработку подавляющего большинства сообщений, получаемых окном. Обратите внимание на предпоследнюю строчку текста программы. В этой строке все необработанные сообщения пере­даются процедуре обработки по умолчанию. На долю оконной процедуры остается «самая малость» - обработать сообщения, которые нуждаются в нестандартной обработке. В нашей программе их два - WM_PAINT и WM_DESTROY.

О сообщении WM PAINT стоит сказать особо. В большинстве окон­ных библиотек при создании окна в памяти формировался буфер, в который записывалось то, что отображалось на закрываемой окном части экрана. Содержимое менялось при изменении положения и размеров окна При этом, кстати, одно окно (перекрывающее) управляло отобра­жением другого (перекрываемого). И, если этот подход был приемлемым для текстового режима, то можно представить, сколько памяти «пожирали» бы подобные буферы в графических режимах! В Windows пошли по другому пути. В Windows каждое окно «знает» о том, что оно



должно проделать в тех случаях, когда условия его отображения измени­лись. К этим случаям я отношу изменение размеров окна, его положения на экране, восстановления на экране после полного или частичного перекрытия другим окном. В этих случаях окну посылается сообщение WM_PAINT, говорящее о том, что окно должно само перерисоваться полностью или частично, обработав сообщение WM_PAINT. Таким образом, помимо сокращения объема требуемой памяти (намного легче хранить небольшую процедуру обработки сообщения WM_PAINT, чем буфер с графической информацией), в Windows решается ее одна про­блема - выполняется одно из требований объектно-ориентированного программирования - полями объекта управляют методы того же (и только того же) объекта. Таким образом, мы пришли еще к одному важному выводу - каждое окно должно обрабатывать сообщение WM_PAINT, иначе оно не сможет восстановить изображение в своей рабочей области. Упомяну еще одно сообщение, которое в нашей программе не присут­ствует, но которое иногда бывает очень полезным. Дело в том, что это сообщение окно получает после создания, но до отображения, точнее, до прорисовки рабочей области. Оно называется WM_CREATE и обычно используется для инициализации окна. В некотором смысле оно является антиподом сообщения WM_DESTROY, применяемого для деинициали-зации окна.

Громадный switch

Фактически вся оконная процедура состоит из одного единственного оператора switch. Он пользуется недоброй славой и, наверное, заслужен­но. Иногда человек просто не в силах осмыслить многочисленные сазе'ы, внутри которых спрятаны очередные switch'n и т. д. По ходу изучения мы увидим, что написаны макрокоманды, позволяющие отказаться от гро­мадного switch первого уровня. Но пока давайте придерживаться «классического» стиля. При написании небольших программ оператор switch очень наглядно (почти графически!) показывает ход обработки сообщений. На данном этапе я не буду рассматривать оконную процеду­ру столь же подробно, как и WinMain(). Обработка сообщения WM_PAINT просто приводит к выводу сообщения на экран. До этого сообщения мы еще дойдем. Структура оконной процедуры ясна практически любому, хоть немного знакомому с языком С. Остановимся на моменте, касающемся завершения работы программы.



WMJ)ESTROYu \VM_QUIT

При необходимости закрыть окно, Windows дает окну возможность «осмотреться» и провести процедуру деинициализации. За счет чего это достигается? В ходе закрытия окна (я напоминаю, что окно - это не только прямоугольная область, видимая на экране, но и совокупность структур данных в глубине системы) сразу после снятия его с отображе­ния оконная функция получает сообщение WM DESTROY, которое является сигналом о необходимости произвести процедуру деинициали­зации. Получив это сообщение и произведя все необходимые действия, функция окна, как правило, вызывает функцию PostQuitMessage(), кото­рая, как следует из ее названия, посылает окну сообщение WM_QUIT, которое, в свою очередь, попав в цикл обработки сообщений, вызывает его прекращение. А посему - ура! Мы прошли путь от начала до заверше­ния программы. Мы узнали достаточно много о структуре программы для Windows, научились регистрировать классы окон, создавать экземпляры окон зарегистрированного класса, запускать и прекращать цикл обработки сообще­ний. Мы получили первоначальные знания об обработке сообщений и написа­нии оконной процедуры.

Следующим шагом в разработке полноцепной программы являет­ся подключение к программе ресурсов. В последующих главах мы остановимся на этом. Это еще на шаг приблизит нас к концу пути. Но на этом пути есть несколько камней, которые нам придется убрать с дороги.

Давайте первым уберем камень под названием

UNICODE

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

ЧТО ТАКОЕ UNICODE



В обычной кодировке ANSI каждый символ определяется восемью оптами, г. е. всего допускается 256 символов, что, конечно, очень и очень



27

мало. Каждому известно, что при русификации компьютер приобретает символы кириллицы, но теряет некоторые стандартные символы, опреде­ленные в коде ANSI. В некоторых алфавитах, например, в японской кане, столько символов, что одного байта для их кодировки просто недоста­точно. В таких случаях приходится искусственно вводить всякие услов­ности (типа двухбайтовых наборов символов, double byte character sets -DBCS) и представлять символы то одним, то двумя байтами. К чему мог привести программиста этот кошмар! Для поддержки таких языков, и, в свою очередь, для облегчения «перевода» программ на другие языки, была создана кодировка Unicode.

Каждый символ в Unicode состоит из двух байтов. С одной стороны, это позволяет преодолеть все сложности по искусственному представле­нию символов двумя байтами. С другой стороны, это позволяет расши­рить набор допустимых символов до 65 536 символов. Разница с 256 символами ANSI достаточно ощутима, не так ли?

Windows NT - это первая операционная система, полностью построен­ная на Unicode. Если функции передается ANSI-строка, она преобразует­ся в Unicode. Если программа пользователя ожидает результат в виде ANSI-строки, то перед возвращением строка Unicode преобразуется в ANSI.

К сожалению, Windows'95, которая выросла из 16-битной Windows 3.x, поддерживает Unicode не в полной мере. Поэтому внутри Windows'95 вся обработка идет в ANSI коде, хотя допускается написание приложений, поддерживающих Unicode. Тем не менее, уважаемый читатель, если вы захотите перейти к работе с Unicode, то придется изучить эту кодировку и способы работы с ней.

UNICODE В WINDOWS NT И WINDOWS'95



Win 32 в большинстве случаев позволяет работу с символами как в традиционной ANSI кодировке, так и в кодировке Unicode. Почти все функции, получающие в качестве аргумента символы или строки, имеют ANSI и Unicode версии. Программист может выбрать, с каким набором символов будет работать его программа. Более того, программист может написать программу таким образом, что она сможет быть откомпилиро­вана для работы как с ANSI, так и с Unicode. Для этого программисту достаточно знать два-три макроса, несколько новых типов данных, и, естественно, новые наименования некоторых хорошо знакомых функций. Но обо всем по порядку.



РАБОТА С UNICODE ПРИ ИСПОЛЬЗОВАНИИ

C/C++ RUN-TIME LIBRARY

Все функции RTL (Run-Time Library - библиотека времени выполне­ния), работающие со строками и символами, имеют ANSI- и Unicode-Персии. Unicode- версии функций используют новый тип данных, введен­ный для описания символов Unicode. Этот тип описан следующим обра­зом:

typedef unsigned short wchar^t;

ANSI-версии, которые по старинке применяют старый добрый char, используют те имена функций, к которым привыкли программисты на языке С. В то же время, имена функций, использующих Unicode, не совпадают с привычными нам старыми именами типа printfQ, strcatQ и т. д. Для того чтобы написать приложение, которое легко адаптировалось оы к обеим кодировкам, нужно уметь манипулировать именами функций и типами данных. Принцип понятен - условная компиляция. В RTL для гого, чтобы определить, какую версию программы строить, введен сим­вол препроцессора UNICODE. В зависимости от того, определен этот символ или нет, строится та или иная версия программы.

Кроме этого, вместо файла string.h используется файл tchar.h, кото­рый обеспечивает универсальность. В нем определен громадный список макросов, которые необходимо использовать для того, чтобы препроцес­сор знал, какой набор функций ему необходимо вызывать, ANSI или Unicode. Этот список макросов приведен в приложении. Предлагаю читателю обратить внимание на то, что для написания кода, который мог бы компилироваться как для ANSI, так и для Unicode, необходимо вместо функций, приведенных в правых колонках, использовать имена, приво­димые в левых колонках.

Для того чтобы указать препроцессору, как нужно строить компили­руемый файл, применяется символ JJNICODE. Этот тип данных исполь­зуется при работе с символами Unicode. Для того чтобы писать приложе­ния, работающие как с ANSI, так и с Uncode, пррименяется другой макрос - FCHAR, который в зависимости от факта определения UNICODE определяется либо

lypcdcfwcharj ТС'НЛК;

typixief unsigned char TCHAR.



Таким образом, используя этот тип, мы можем объявлять данные, ко­торые будут восприниматься в разных обстоятельствах то как ANSI-, то как Unicode-строки или символы. Например, строка

TCHAR* pszMyStnng = «This is my string»;

в зависимости от того, определен ли символ ^UNICODE, будет считаться либо состоящей из символов ANSI и занимать 18 бантов, либо состоящей из символов ANSI и занимать памяти в два раза больше.

Теперь возникает проблема с компилятором. По умолчанию, компи­лятору почему-то наплевать (извините за такое слово, но я долго не мог понять, в чем же состоит моя ошибка при определении строки), что мы описываем строку как состоящую из символов Unicode. Для него если строка, так уж обязательно ANSI! Попробуйте откомпилировать следую­щую «программу»:

#define JJNICODE

#include <windows.h>

#include <tchar.h>

int WINAPI WinMain (HINSTANCE hlnstance, H1NSTANCE hPrevInstance,

LPSTR IpszCmdParam, int nCmdShow ) i

TCHAR* pszMyString = "This is my string"; return 0;

Кажется, полученный .      -

GXG

что   все   сделано   правильно,   но   попробуйте  посмотреть -файл обычным    ^ в ,,оиои е. Вы увидите (<This is my string     в   обычной кодировке,   т.   е.'  определение   _„.. никакого влияния на представление строки не оказывает. Таким of!

ГЖет",

необходимо явно объявлять строку, как состоящую из символов Попробуйте внести небольшое изменение.

TCHAR* pszMyString = L'This is my string";

Буква L перед строкой указывает компилятору, что строка состоит из символов Unicode. В .ехе-файле мы получим символы Unicode. Но в таком случае мы не можем получить ANSI-строку! Замкнутый круг! Проблема разрешается введением еще одного макроса - _ТЕХТ. Опреде­лен он примерно так:

#ifdef_UNICODE typedef _ТЕХТ(х) L ## х

#elsc typedef _TF.XT(x) x

Пользуясь этим макросом, можено описать наши данные таким обра­зом, что они будут компилироваться в обоих случаях так, как мы того хотим.

Попробуйте записать



TCHAR* pszMyString - _TEXT("This is my string");

и откомпилировать программу с определением _UNICODE и без таково­го. Ну и как? Получилось?

К этому моменту трудности совместной работы Unicode и Run-Time Library уже преодолены. На очереди -

РАБОТА С UNICODE В WIN32 API

Предлагаю читателю обратить внимание на то, каким образом опреде­ляются типы данных и указатели в заголовочных файлах Win 32 (обратите внимание - символ UNICODE без знака подчеркивания, со знаком подчеркивания он используется в RTL):

#ifdcf UNICODE

typcdcf wcharj TCHAR; if/else

typcdef unsigned char TCHAR;

#cndif

typcdef TCHAR * LPTSTR, *LPTCH; typedef unsigned char CHAR; typedefCHAR *LPSTR, *LPCH; typcdcf unsigned wchar_t WCHAR, typedef WCHAR *LPWSTR, *LPWCH;

Что же касается работы с функциями, то в Win32, по сравнению с RTL, при работе с функциями программист может чувствовать себя более комфортно. Здесь все сделано намного проще и, по-моему, более изящно. При работе с обеими кодировками используются одинаковые имена функций. Достигается это следующим образом:

каждая функция, как и в RTL, имеет ANSI- и Unicode-версии. При этом имена функций формируются так:

к обычному имени (думаю, не нужно объяснять, что я под­разумеваю под обычным именем?) в случае ANSI-версии добав­ляется символ А (латинская буква, не путать с русской), напри­мер для функции DispatchMessageQ имя ANSI-версии будет DispatchMessageAQ;

для Unicode-версии к обычному имени функции добавляет­ся символ W. Например, для функции DispatchMessage() имя Unicode-версии будет DispatchMessageWQ;

создается макрос, имя которого совпадает с обычным именем функции. При определении символа UNICODE макрос разворачивается в имя Unicode-версии функции. Если символ UNICODE не определен, то макрос разворачивается в имя ANSI-версии функции. Например, для уже известной нам функции DispatchMessageWQ эти определения выглядят следующим образом:

WINUSERAPI LONG WINAPI DispatchMessageA(CONST MSG *lpMsg); WINUSERAPI LONG WINAPI DispatdiMessageW(CONST MSG *IpMsg);



#ifdef UNICODE

#defmc DispatchMessagc DispatchMessagcW

#else

#defme DispatchMcssage DispatchMcssageA

#endif// IUNICODE

Все гениальное просто! Единственное, я прошу читателя обратить внимание на то, что при компиляции программ обычно приходится определять оба символа препроцессора - UNICODE и ^UNICODE.

НЕСКОЛЬКО ДОБРЫХ СОВЕТОВ

К этим советам читатель может не прислушиваться, если не захочет. Просто мне бы хотелось поделиться тем небольшим опытом, который появился у меня при изучении Unicode. Самое главное, что должен осознать программист, это то, что, включив Unicode в свои системы Windows NT и Windows'95, Microsoft на весь мир заявила о том, что у Unicode существует мощная поддержка в лице этой фирмы. А к словам Microsoft (за которыми в большинстве случаев следуют дела), прислу­шаться стоит. Так что хотят этого программисты или не хотят, рано или поздно им придется переходить на работу с Unicode. Поэтому неплохо было бы позаботиться о возможной работе приложения с обеими коди­ровками сейчас, чтобы избежать «большой крови» потом.

Что для этого нужно? Немногое:

для символов и строк использовать типы TCHAR и LPTSTR; при определении литералов использовать макрос   TEXT; не забывать о правилах строковой арифметики.

На этом заканчивается рассмотрение вопросов, связанных с Unicode. Мне думается, я рассказал достаточно, чтобы у читателя появился «направляющий косинус» в этом вопросе. На страницах этой книги мы еще не раз встретим имя этой кодировки.

ОСНОВЫ РИСОВАНИЯ И КОПИРОВАНИЯ ИЗОБРАЖЕНИЙ

НЕМНОГО ЛИРИКИ



Когда я' обдумывал план этой книги, мне казалось, что за главой, в ко-трой описана структура программы, должна идти глава о взаимодейст­вии программы с пользователем. Действительно, чего тянуть? Ведь по­давляющее большинство программ для Windows написано с расчетом, что пользователь будет работать с программой в интерактивном режиме. Я начал было писать главу, посвященную меню и диалоговым окнам, но тут же вынужден был остановиться. Мне пришлось чуть ли не на каждой строчке извиняться и говорить, что все это мы узнаем чуть позже. Hitmap'bi в меню и кнопках - это была последняя капля, переполнившая чашу. Поэтому было принято следующее решение: на время несколько отклониться от нашего курса и изучить основы работы с изображениями (убрать тот самый камень с нашей дороги), после чего идти дальше. Думаю, что время, затраченное на изучение работы с изображениями, окупится сторицей. Я понимаю, что "читателю не терпится написать какую-нибудь грандиозную программу. Постараюсь, чтобы наше откло­нение от курса было недолгим, и изложение буду вести почти в конспек­тивном стиле (не в ущерб содержанию, надеюсь).



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

Давайте, уважаемый читатель, обсудим вопрос: что должно происхо­дить в тех случаях, когда программе необходимо отобразить в рабочей области окна некоторое изображение? Для начала рассмотрим случаи, когда программе не нужно рисовать изображение, а необходимо скопи­ровать одно изображение на другое. Поговорим о том, что такое

КОНТЕКСТ УСТРОЙСТВА



Наверное, читатель уже знает о том, что с точки зрения программиста Windows является системой, не зависящей от устройств (device independent). Эту независимость со стороны Windows обеспечивает библиотека GDI32.dll, а со стороны устройства - драйвер этого устройст­ва. С точки зрения программы связующим звеном между программой и устройством является контекст устройства (Device Context - DC). Если

программе нужно осуществить обмен с внешним устройством, програм­ма должна оповестить GDI о необходимости подготовить устройство для операции ввода-вывода. После того, как устройство подготовлено, про­грамма получает хэндл контекста устройства, т. е. хэндл структуры, содержащей набор характеристик этого устройства. В этот набор входят:

bitmap (битовая карта, изображение), отображаемый в окне,

перо для прорисовки линий,

кисть,

палитра,

шрифт

и т. д. Программа никогда напрямую не обращается к контексту устрой­ства (кстати, эта структура не документирована Microsoft), она обращает­ся к нему опосредствованно, через определенные функции. После того, как все действия произведены, и необходимость в использовании устрой­ства отпала, программа должна освободить контекст устройства, чтобы не занимать память. Есть еще одна причина, из-за которой необходимо освобождать контекст устройства. В системе может существовать одно­временно только ограниченное число контекстов устройств. Если кон­текст устройства не будет освобождаться после операций вывода, то через несколько перерисовок окна система может зависнуть. Так что не забывайте освобождать контексты устройств!



Когда программа требует контекст устройства, она получает его уже заполненным значениями но умолчанию. Объект в составе контекста называется текущим объектом. Само слово - текущий - говорит о том, что контекст устройства можно изменить. Программа может создать новый объект, скажем, bitmap или шрифт, и сделать его текущим. Замещенный объект автоматически из памяти не удаляется, его необходимо позже удалить отдельно. Само собой разумеется, что программа может по­лучить характеристики текущего устройства. А вот изменить эти харак­теристики, увы, можно только через замену объекта (впрочем, это и так попятно).

ТИПЫ КОНТЕКСТА УСТРОЙСТВА

В Windows поддерживаются следующие типы контекстов устройств: контекст дисплея (обеспечивает работу с дисплеем); контекст принтера (обеспечивает работу с принтером); контекст в памяти (моделирует в памяти устройство вывода); информационный контекст (служит для получения данных от уст­ройства).

Контекст дисплея

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

Приложения, которые не очень интенсивно работают с экраном, ис­пользуют общин контекст. Контекст класса является устаревшим и поддерживается только для обеспечения совместимости с предыдущими версиями Windows. Microsoft не рекомендует использовать его при разработке новых приложении и рекомендует использовать только при-иатный контекст. Исходя из этого, я решил пожалеть свое и читателя время и не рассматривать контекст класса.

Контексты устройств хранятся в кэше, управляемом системой. Хэндл общею контекста программа получает с помощью функций GelDCQ, (JctDCEx() или BeginPaint(). После того, как программа отработает с дисплеем, она должна освободить контекст, вызвав функцию R.eleaseDC() или EndPaiutO (в случае, если контекст получался с помощью HcginPaintO). После того, как контекст дисплея освобожден, все нзмене-мня, внесенные в него программой, теряются и при повторном получении контекста все действия по изменению контекста необходимо повторять заново.



Приватный контекст отличается от общего тем, что сохраняет измене­ния даже после того, как прикладная программа освободила его. Приват­ный контекст Fie хранится в кэше, поэтому прикладная программа может не освобождать его. Естественно, что в этом случае за счет использова­ния большего объема памяти достигается более высокая скорость работы j дисплеем.

Для работы с приватным контекстом необходимо при регистрации класса окна указать стиль CS OWNDC. После этого программа может получать хэндл контекста устройства точно так же, как и в случае общего контекста. Система удаляет приватный контекст в том случае, когда удаляется окно.

При работе с контекстами необходимо запомнить, что хэндлы кон-гекста устройства с помощью функции BcginPaint() необходимо получать только в случае обработки сообщения WM PAINT. Во всех ^стальных случаях необходимо использовать функции GetDCQ или iietDCEx().

Контекст принтера

При необходимости вывода на принтер программа должна создать контекст устройства с помощью функции CrealeDC(). Аргументы этой функции определяют имя драйвера устройства, тип устройства и инициа-лизационные данные для устройства. Используя эти данные, система может подготовить принтер и распечатать требуемые данные. После распечатки прикладная программа должна удалить контекст принтера с помощью функции DeleteDCQ (а не ReleaseDC()). ,

Информационный контекст

Информационный контекст фактически не является контекстом уст­ройства и служит только для получения информации о действительном контексте устройства. К примеру, для того, чтобы получить характери­стики принтера, программа создает информационный контекст, исполь­зуя для этого функцию CreatelQ), а затем из него выбирает требующиеся характеристики. Естественный вопрос: а для чего нужно использовать информационный контекст? Почему нельзя те же самые данные получить из действительно контекста? Дело в том, что этот тип контекста создается и работает намного быстрее, а также занимает меньше памяти по сравне­нию с действительным контекстом. После того, как надобность в инфор­мационном контексте миновала, программа должна удалить его с помо­щью функции DeleteDCQ.



Чаще всего для вывода информации на устройство используется

Контекст в памяти

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

1. Получения хэндла контекста устройства (назовем его hDC - handle of Device Context) для окна, в которое будет осуществляться вывод изображения.

2. Получения хэндла bitmap'a, который будет отображаться в окне.

3. Получения совместимого с hDC контекста в памяти (для хранения изображения) с помощью функции CreateCompatibleDC() (обратите внимание на название функции - создать СОВМЕСТИМЫЙ контекст).

4. Выбора изображения (hBitmap) как текущего для контекста в па­мяти (hCompatibleDC).

5. Копирования изображения контекста в памяти (hCompatibleDC) на контекст устройства (hDC).

6. Удаления совместимого контекста (hCompatibleDC);

7. Принятия мер для того, чтобы замещенный bitmap из контекста в памяти не остался в памяти.

8. Освобождения контекста устройства (hDC).

При необходимости шаги 6 и 7 можно поменять местами. Когда и как удалять замещенный bitmap, зависит от программиста и поставленной перед ним задачи.

Именно этот способ и используется в большинстве программ для ко­пирования изображения.

Но, как известно, лучше один раз увидеть, чем сто раз услышать (по-английски это звучит еще более категорично - seeing is believing - уви­деть, значит поверить). Поэтому давайте напишем небольшую програм­му, в которой продемонстрируем возможности Windows по манипулиро­ванию изображениями.

SEEING IS BELIEVING

Перед тем, как начинать писать программу, мы должны прояснить для себя еще одну тему. В каких единицах измеряются размеры окна и, соответственно, все смещения в окне? Для того чтобы ответить на этот вопрос, мы должны рассмотреть



Режимы отображения

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

Таким образом, даже при перемещении окна координаты объектов внутри окна остаются неизменными. При этом единицы, в которых измеряются координаты, зависят от режима отображения (mapping mode), установленного для данного окна. Единицы измерения, зависящие от режима отображения, называют логическими единицами, а координаты в )том случае называют логическими координатами.

При выводе информации на конкретное устройство единицы ло­гических координат преобразуются в физические единицы, которыми являются пиксели.

Таблица 6. Илетнфикаторы, применяемые для обозначения режимов отображения

Идентификатор

Значение

 

Эффект

 

 

 

MMJTEXT

 

1

 

Логическая единица равна пикселю, начало

 

 

 

 

 

 

 

координат - левый верхний угол окна, положи-

 

 

 

 

 

 

 

тельное значение х - вправо, положительное

 

 

 

 

 

 

 

значение у - вниз (обычный отсчет)

 

 

 

MM_LOMETRIC

 

2

 

Логическая единица равна 0, 1 мм, отсчет

 

 

 

 

 

 

 

координат - обычный

 

 

 

MM_HIMETRIC

 

3

 

Логическая единица равна 0.01 мм, отсчет

 

 

 

 

 

 

 

координат - обычный

 

 

 

MM_LOENGLISH

 

4

 

Логическая единица равна 0,1 дюйма, отсчет

 

 

 

 

 

 

 

координат - обычный

 

 

 

MM_HIENGLISH

 

5

 

Логическая единица равна 0,001 дюйма, отсчет

 

 

 

 

 

 

 

координат - обычный

 

 

 

MM_TWIPS

 

6

 

Логическая единица равна 1/12 точки на

 

 

 

 

 

 

 

принтере (~ 1/1440 дюйма - «твип»), отсчет

 

 

 

 

 

 

 

координат - обычный

 

 

 

MMJSOTROPIC

 

7

 

Логические единицы и направление осей

 

 

 

 

 

 

 

определяются программистом с помощью

 

 

 

 

 

 

 

функций SctWindowExtExO и

 

 

 

 

 

 

 

SetVicwportExtExf), единицы по осям имеют

 

 

 

 

 

 

 

одинаковый размер

 

 

 

MM ANISOTROP1C

 

8

 

Логические единицы и направления осей

 

 

 

 

 

 

 

определяются так же, как и для

 

 

 

 

 

 

 

MM ISOTROP1C. но размеры единиц по осям

 

 

 

 

 

 

 

различны

 

 

 

MMJvflN

 

 

 

MM TEXT

 

 

 

MM MAX

 

 

 

MM_ANISOTROPIC

 

 

 

MM MAX FIXEDSCALE

 

 

 

MMJTW1PS

 

 

 

<


Для установки текущего режима отображения используется функция SetMappingModeQ, которая в файле wingdi.h описана следующим образом:

WiNGDIAPI int   WINAPI SetMapMode(HDC. int);

Первый аргумент этой функции - хэндл контекста устройства, для которо­го устанавливается данный режим. Второй аргумент определяет задаваемый режим отображения. В том же файле wingdi.h можно найти и идентификато­ры, использующиеся для обозначения режимов отображения (табл. 6). Наде­юсь, что после того, как была просмотрена таблица, вопросов у читателя не возникло. Теперь ясно, что иногда для решения конкретных задач (например, построения графиков) можно использовать различные режимы отображения. При создании окна по умолчанию устанавливается режим ММ_ТЕХТ, т. е. все координаты исчисляются в пикселах.

Пишем програмл i у

Наша программа будет отображать bitmap в окне и при необходимо-: in производить его масштабирование:

«include <wmdows.h>

LRESULT CALLBACK. DCDenioWndProc ( HWND. UINT, UINT, LONG );

ml WINAPI WinMaiiuHINSTANCL hlnstance. HINSTANCE hPrevInstance, LPSTR IpszCmdParam, in! nCmdShow )

HWNDhWnd;

WNDCLASS WndClass ;

MSU Msg;

char szClassNamcf] - "DCDcmo";

* Registering our window class */

* Fill WNDCLASS structure */

WndClass.stylc - CS_HRF.DRAW | CS_VREDRAW;

WndClass.lpfnWiulProc - DCDenioWndProc;

WndClass.cbClsExtra - 0;

WndClass.cbWndExtra = 0;

WndClass.hlnstance ~ lilnstance ;

WndClass.hlcon - Loadlcon (NULL,IDI_APPLICATION);

WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);

WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);

WndClass.IpszMcnuName = "MyMcnu";

WndClass.IpszClassNamc = szClassNamc;

if ( !RcgislcrClass(&WndClass))

i i

McssagcBox(NULL,"Cannot register class","F.rror",MB_OK); return 0;

hWnd ~ CreateWindow(szClassNamc, "Program No 1",

WS OVERLAPPEDWINDOW, CW USEDEFAULT, CWJJSEDEFAULT, CWJJSEDEFAULT. CWJJSEDEFAULT, NULL, NULL, hlnstance.NULL); if(!hWnd) {

MessagcBox(NULL,"Cannot create window","Error",MB_OK); retuni 0;



/* Show our window */

ShowWindow(hWnd,nCmdShow);

UpdateWindow(hWnd);

/* Beginning of messages cycle */

while(GetMessage(&Msg, NULL, 0, 0))

! TranslatcMessage(&Msg);

DispatchMessage(&Msg);

} return Msg.wParam;

LRESULT CALLBACK DCDemoWndProc (HWND hWnd, UINT Message,

UINT wParam. LONG IParam )

HDC hDC, hCompatibleDC; PAINTSTRUCT PaintStnict; HANDLE hBitmap, hOldBitmap; RECT Rect; BITMAP Bitmap; switch(Messagc)

{

case WM_PATNT: // Получаем контекст устройства.

hDC = BeginPaint(hWnd, &PaintStruct);

// Загружаем bitmap, который будет отображаться в окне , из файла. hBitmap = LoadImage(NULL, "MSDOGS.BMP", IMAGEJ3ITMAP, 0,0,

LR_LOADFROMFILH); // Получаем размерность загруженного bitmap'a.

GetObject(hBitmap, sizeof(BITMAP), &Bitmap); //' Создаем совместимый с контекстом окна контекст в памяти.

hCompatibleDC = CreateCompatibleDC(hDC); // Делаем зафуженный из файла bitmap текущим в совместимом контексте.

hOldBitmap - SelectObject(hCompatiblcDC, hBitmap); // Определяем размер рабочей области окна.

GctClientRect(h\Vnd,&Rect);

// Копируем bitmap с совместимого на основной контекст с масштабированием. SlrctchBlt(hDC, 0, 0, Rect.right, Rect.bottom,

hCompatibleDC, 0, 0, Bitmap.bmWidth, Bitmap.bmHcight, SRCCOPY); // Вновь делаем старый bitmap текущим.

SelcctObject(hCompatibleDC, hOldBitmap); i/ Удаляем загруженный с диска bitmap.

DeleteObject(hBitmap); // Удаляем совместимый контекст.

DeleteDC(hCompatibleDC);

// Освобождаем основной контекст, завершая перерисовку рабочей области окна. EndPaint(hWnd,&PaintStruct); return 0;

case WM_DESTROY: PostQuitMessage(O); return 0; } return DefWindowProc(hWnd,Mcssage,wParam, IParam);

Листинг № 2. Программа, осуществляющая отображение bitmap'a с масштабированием.

Результатом работы программы является окно, показанное на рис. 2.

Если читатель набрал программу буква в букву, его при запуске ожи­дает одна неприятность - отобразится точно такое же окно с белым фоном, как и при запуске «Hello, world!» (только без надписи в центре жрана). Дело в том, что в моей программе отображается тот bitmap, который нравится мне и, что более важно, находится на моем компьютере в доступной директории. Предлагаю читателю в тексте программы найти оператор, который начинается с «hBitmap = Loadlmage» и заменить в нем имя моего bitmap'a, «msdogs.bmp», на имя того bitmap'a, который будет отображать программа на читательском компьютере. Не забудьте при ном проверить, чтобы новый bitmap был доступен, т. е. находился бы в директории, доступной через переменную окружения PATH, или в теку­щей директории. Сделали? Теперь попробуйте еще раз запустить про­грамму. Если все выполнено правильно, то bitmap точно впишется в окно, причем можно будет заметить, что он несколько растянут или сжат в обоих направлениях. Попробуйте поизменять размеры окна. Bitmap



постоянно будет точно вписываться в окно, при этом растягиваясь или сжимаясь. А теперь давайте разберем эту программу и сопоставим опера-горы программы с шагами алгоритма работы с контекстом в памяти, о котором я говорил ранее.

Функция WinMainQ стандартна - ничего интересного в данном случае она не содержит. В оконной функции, которая называется DCDemoWndProc, интерес для нас представляет обработка сообщения \VM_PAINT, которое мы и рассмотрим. Первый шаг алгоритма -получить хэндл контекста устройства - мы выполняем посредством »мзова функции BeginPaint(hWnd, &PaintStruct). Аргумент hWnd очевиден - мы получаем контекст данного окна. Что же касается структуры PaintStruct...

В Program No 1



Рис. 2. Вид окна, отображаемого программой

Понятно, что окно далеко не всегда должно перерисовываться полно­стью. К примеру, только часть окна перекрывается другим окном. Есте­ственно, что и перерисоваться должна только часть окна. В этом случае полная перерисовка всего окна будет только лишней тратой времени и ресурсов компьютера. Windows «знает» о том, какая часть окна перекры­та. При необходимости перерисовки (при вызове BeginPaintQ) система заполняет структуру типа PAINTSTRUCT, адрес которой передается системе как второй аргумент BeginPaintQ. Среди всех полей структуры типа PAINTSTRUCT основным (на мой взгляд) является поле, содержа­щее координаты той области (clipping region), которая должна быть перерисована. В нашем примере информация, хранящаяся в этой структуре, не используется, но я прошу читателя обратить внимание на эту структуру и в дальнейшем использовать ее. Получив от функции BeginPaint() хэндл контекста устройства (hDC), будем считать первый шаг выполненным.

Второй шаг - получение хэндла bitmap'а, который будет отображаться в окне - мы делаем, вызывая функцию Loadlmage(). Я не случайно вос-

пользовался именно этой функцией. Во-первых, возможности этой функ­ции достаточно широки. Она позволяет загружать графические образы как из ресурсов, записанных в исполняемом файле, так и из файлов, содержащих только изображения. Графическим образом может быть bitmap, курсор и иконка. Кроме этого, функция позволяет управлять параметрами отображения и загрузки образа. Во-вторых, подавляющее большинство функций работают с ресурсами, сохраненными в исполняе­мом файле, и у программистов, начинающих осваивать Win32, попытки загрузить что-либо из файла сопровождаются некоторыми трудностями. (Помнится, что я сам в начале изучения программирования для Windows die Win32) несколько часов потратил на поиски в help'ax функции, по­зволяющей загрузить bitmap из файла). Но обо всем по порядку. В файле winuser.h эта функция описана следующим образом:



WINUSERAPI HANDLE WINAPI LoadImageA(HINSTANCE, LPCSTR, UINT,

int, int, UINT); WINUSERAPI HANDLE WINAPI LoadImageW(HINSTANCE, LPCWSTR,

UINT, int. int, UINT); tfifdef UNICODE (/define Loadlmage LoadlmageW T/eise

//define Loadlmage LoadlmageA #endif // IUNICODE

Первый, второй и последний аргументы этой функции работают в связке. Первый apryMeirr(hlnst) - хэндл программы. Как читатель должен помнить, если вместо хэндла программы указан нуль, то объект является предопределенным, т. е. хранится в «глубинах» системы. В противном случае, объект загружается откуда-то снаружи. Второй аргумент -IpszName - определяет загружаемый объект. Последний аргумент - fuLoad - содержит флаги, определяющие режим загрузки объекта. Среди этих флагов есть флаг LR LOADFROMFILE. Его название определяет его назначение - если этот флаг установлен, загрузка происходит из внешнего файла. От значения первого и последнего аргументов зависит, как будет интерпретирован второй аргумент. Взаимодействие этих трех аргументов объясняется в табл. 7.

Третий аргумент - тип образа, он может принимать значения IMAGE BITMAP, IMAGE_CURSOR, IMAGEJCON и IMAGE_ENHMETAFILE. Здесь комментарии излишни. Четвертый и :iv:b,i;i аргументы указывают ширину и высоту иконки или курсора и в нашем примере не используются.

Таблица 7. Взаимодействие аргументов функции LoadlmageQ

ШЬоас1(флаг LR_LOADFROMKILE)

 

hlnst

 

IpszName

 

Не установлен Не установлен Установлен Установлен

 

NULL не NULL NULL не NULL

 

Идентификатор предопределенного объекта Имя ресурса Имя файла, в котором содержится bitmap Имя файла, в котором содержится bitmap

 

Таблица 8. Флаги, определяющие параметры загртеки образа в память

 

Флаг

 

Значение

 

Эффект

 

LR_DEFAULTCOLOR

 

0x0000

 

Указывает, что загружаемый образ - не

 

 

 

 

 

монохромный

 

LRJVIONOCHROME

 

0x0001

 

Указывает, что загружаемый образ - черно-

 

 

 

 

 

белый

 

LR COLOR

 

0x0002

 

 

 

LR COPYRETURNORG

 

0x0004

 

 

 

LR COPYDELETEORG LR_LOADFROMFILE

 

0x0008 0x0010

 

Образ необходимо загружать из файла, а не

 

 

 

 

 

из ресурсов

 

LR_LOADTRANSPARENT

 

0x0020

 

Все пиксели, цвет которых совпадает с цветом пикселя, расположенного в левом

 

 

 

 

 

верхнем углу bitmap'a, отображаются как

 

 

 

 

 

прозрачные

 

LRJ3EFAULTSIZE

 

0x0040

 

Использовать ширину и высоту образа, определенные в системных метриках для

 

 

 

 

 

иконки и курсора, если cxDesircd или

 

 

 

 

 

cyDesired равны 0. Если этот флаг не

 

 

 

 

 

установлен, a cxDesired и/или cyDesired

 

 

 

 

 

равны 0, используются размеры образа,

 

 

 

 

 

указанные в ресурсе

 

LR_LOADMAP3DCOLORS

 

0x1000

 

Заменить следующие оттенки серого цвета: RGB(128, 128, 128)(DkGray)-na

 

 

 

 

 

COLOR 3DSHADOW,RGB(192, 192, 192)

 

 

 

 

 

(Gray) - на COLOR 3DFACE, RGB(223,

 

 

 

 

 

223, 223) (LtGray) - на COLOR JDLIGHT

 

LR_CREATEDIBSECTION

 

0x2000

 

При загрузке bitmap'a возвращает ориги­нальные значения цветов, не преобразуя

 

 

 

 

 

bitmap в совместимый с данным контек-

 

 

 

 

 

стом

 

LR COPYEROMRESOURCE LR__SHARED

 

0x8000

 

Разделять хэндл загруженного изображе­ния, в случае, если образ загружается

 

 

 

 

 

несколько раз. Нежелательно применять к

 

 

 

 

 

образам нестандартных размеров

 

<


Последний аргумент - флаги, определяющие параметры загрузки об­раза в память. Их достаточно много, только в файле winuser.h я насчитал 12 возможных идентификаторов. Все они начинаются с букв LR. Ничего сложного в них нет, и читатель сам сможет изучить их. Краткое описание угих флагов приведено в табл. 8.

Функция LoadlmageQ возвращает нам хэндл загруженного bitmap'a (hBitmap) (или NULL, если где-то что-то не так), после чегомы можем считать второй шаг нашего алгоритма пройденным.

Третий шаг - получение совместимого контекста в памяти - выполня­емся посредством вызова функции CreateCompatibleDCQ. Единственный аргумент этой функции - хэндл контекста (hDC), для которого создается совместимый контекст.

Четвертый шаг мы реализуем вызовом функции SelectObjectQ. Пер-иым аргументом указываем хэндл контекста, в котором замещается 1екущий элемент (в данном случае это хэндл только что созданного контекста в памяти hCompatibleDC), а вторым - хэндл элемента, которым замещается текущий элемент (хэндл загруженного bitmap'a hBitmap). Немаловажно, что эта функция возвращает хэндл ЗАМЕЩЕННОГО элемента (hOldBitmap), т. е. впоследствии с этим элементом могут также производиться манипуляции.

А вот на пятом шаге происходит то, ради чего мы заварили всю эту кашу с загрузкой bitmap'oB, совместимыми контекстами и прочим. Для копирования bitmap'a (с масштабированием) с одного контекста на другой, мы используем функцию StretchBlt(), одну из «могучих bit», по меткому выражению Чарльза Петцольда. К их числу, помимо StretchBltQ, относятся BitBlt() и PatBltQ.

Наверное, StretchBltQ является самой «могучей» из них. И наверное, се мощь и обусловила наличие у этой функции «всего-навсего» одинна­дцати аргументов. В файле wingdi.h эта функция описана следующим образом:

WINGDIAPI BOOL   W1NAP1 StretehBlt(HDC, int. int. int, int, HDC, hit, int. int. int,

DWORD);

Первые пять аргументов описывают тот прямоугольник на экране, в который будет вписан bitmap (на рис. 3 он обозначен светло-серым инешм). Ту часть bitmap'a, которая будет вписана в прямоугольник на чоране (на рисунке - пересекающаяся часть светло-серого и темно-серого прямоугольников), описывают следующие пять аргументов. И последний, одиннадцатый аргумент, так называемый код растровой операции, опи­сывает, каким образом пиксели одного bitmap'a будут взаимодействовать



с пикселами другого bitmap'a. Для того чтобы лучше понять аргументы функции, обратимся к рис. 3. Представим, что в окне, в регионе, обоз­наченном на рисунке светло-серым цветом, нужно отобразить bitmap (обозначен на рисунке темно-серым цветом) или часть bitmap'a, при необходимости сжав или растянув ее.



Рис. 3. Взаимодействие аргументов функции StrelchBIt

Итак, первый и шестой аргументы функции - хэндлы окна (hDC) и со­вместимого контекста (hCompatibleDC) соответственно. Второй (nXOriginDest) и третий (nYOriginDest) аргументы содержат смешение верхнего левого угла прямоугольника, в который будет вписываться bitmap, относительно левой и верхней сторон рабочей области окна (В каких единицах выражено смещение? Напомню, мы создали окно и не меняли режим отображения, т. е. текущий режим является уста-нов*ленньга по умолчанию). Четвертый (nWidthDest) и пятый (nlleightDest) аргументы - ширина и высота этого прямоугольника. Седь­мой (nXOriginSrc) и восьмой (nYOriginSrc) аргументы аналогичны вто­рому и третьему аргументам. Девятый (nWidthSrc) и десятый (ntleightSrc) аргументы содержат ширину и высоту отображаемой части bitmap'a. He нужно обладать развитым пространственным воображением, чтобы

понять,  что, изменяя положения прямоугольников друг относительно ipyra, а также относительно окна, меняя их размеры, можно отобразить на экране любую часть или целый bitmap.

В примере мы хотим фактически совместить наш bitmap с рабочей об-мсгыо окна, поэтому второй и третий аргументы вызываемой функции раины нулю. Четвертый и пятый аргументы равны ширине и высоте рабочей области (мы получили ширину и высоту рабочей области с помощью функции GctClientRectO). Седьмой и восьмой аргументы равны пулю (подумайте, почему?), а девятый и десятый содержат ширину и высоту bitmap'a, которые мы получили, обратившись к GetObject(). Вот, кажется, и все. Нет, не все.

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



Мы прошли уже пять шагов алгоритма. Остались еще три шага - уда­ление совместимого контекста, объекта и контекста устройства - пусть чшатель сам определит, какие операторы программы их реализуют.

I (еужели мы добрались до конца нашего алгоритма? Мне он показался бесконечным! Давайте прежде чем рассматривать одиннадцатый аргу­мент упомянем об оставшихся «могучих Bit».

Функция BitBltQ тоже копирует bitmap с одного контекста на другой, но без масштабирования. Следовательно, ей не нужны девятый и десятый аргументы - ширина и высота отображаемой области - отображается все ю, что поместится в отведенную для этого область (светло-серый прямо­угольник на рис. 3).

Последняя из «могучих» - функция PatBltQ - просто закрашивает пря­моугольник на экране, используя для этого выбранную в контексте устройства кисть. Раз нет отображаемого объекта, то зачем нам сто контекст и координаты? Отбрасываем аргументы с шестого по десятый включительно и получаем список аргументов PatBltQ.

Наконец мы подошли к тому, чтобы уяснить, что же представляет со­бой одиннадцатый аргумент функции StretchBltQ. Сейчас мы поговорим о том, что же такое