Азбука программирования в 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 |
|
|
Не забывайте, что в языке С, в отличие, скажем, от 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. Сейчас мы поговорим о том, что же такое