Автор рассчитывает на то, что
А теперь - ВНИМАНИЕ! Автор рассчитывает на то, что в момент чтения этой книги читатель будет сидеть за компьютером и вслед за автором пройдет по заголовочным файлам 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 оговорены особо.
4
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))
10
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 - определяет порядок передачи
13
параметров при вызове процедуры. Наименование характеристики говорит само за себя - 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 - определяет, в каком виде создаваемое окно будет появляться на экране. Окно может появляться в максимизированном виде либо в виде иконки (минимизированном),
14
может иметь произвольный размер, определяемый программой и другие характеристики. В 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. А пока давайте считать, что это просто разные описания одной и той же структуры.
16
Особое внимание следует обратить на первое, второе и последнее поля. Почему? - Сейчас станет ясно.
Стиль окна я определяю оператором
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 будут использоваться еще реже.
17
Таблица 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
19
Таблица 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()?). Второй аргумент - указатель на строку, содержащую тот текст, который появится в заголовке окна.
Третий аргумент определяет стиль окна (не общие характеристики всех окон класса, а индивидуальные характеристики конкретного окна). Стиль определяет, будет ли окно иметь заголовок, иконку системного меню, кнопки минимизации, максимизации, характер границы окна, определяет также взаимоотношения окон типа предок-потомок и т. д. Под
20
это поле отводится 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, все эти проверки называются параноидальными, но я пришел к выводу, что на этапе отладки программы лучше все эти проверки оставить. Когда программа заработает полностью - это дело другое.
21
Таблица 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, зарегистрировали класс окна и создали экземпляр окна этого класса. Мы выдали окно на отображение, после чего запустили цикл обработки сообщений и предусмотрели условие, при котором работа цикла закончится. Теперь все зависит от того, как сообщения будут обрабатываться оконной про-
24
цедурой. Функция WinMain (), если, конечно, не считать работающего цикла обработки сообщений, на этом свою задачу выполнила.
ФУНКЦИЯ ОКНА И ЕЕ АРГУМЕНТЫ
Мы закончили изучение функции WinMain. Теперь нашей ближайшей задачей будет рассмотрение оконной процедуры.
Как обработать множество различных сообщений?
Первый вопрос, возникающий при изучении функции окна, касается реакции этой функции на огромное число сообщений. Ведь в файле заголовков прямо записано, что Майкрософт резервирует для использования в Win32 все сообщения с номерами менее 1024! Неужели функция окна должна обрабатывать все эти сообщения и в программе должен быть код, обеспечивающий эту обработку? Ответ на этот вопрос достаточно парадоксален. Да, оконная функция должна обрабатывать все сообщения, приходящие в адрес окна. Нет, программист не должен писать кода для обработки всех сообщений! Дело в том, что в Windows предусмотрена обработка всех сообщений по умолчанию. В программе должен присутствовать код обработки только тех сообщений, обработка которых по умолчанию не устраивает программу. Все остальные «протекают» сквозь функцию и передаются на обработку по умолчанию. Делается это простым обращением к функции DefWindowProc (Default Window Procedure). Именно эта функция, спрятанная глубоко в «недрах» Windows, производит обработку подавляющего большинства сообщений, получаемых окном. Обратите внимание на предпоследнюю строчку текста программы. В этой строке все необработанные сообщения передаются процедуре обработки по умолчанию. На долю оконной процедуры остается «самая малость» - обработать сообщения, которые нуждаются в нестандартной обработке. В нашей программе их два - WM_PAINT и WM_DESTROY.
О сообщении WM PAINT стоит сказать особо. В большинстве оконных библиотек при создании окна в памяти формировался буфер, в который записывалось то, что отображалось на закрываемой окном части экрана. Содержимое менялось при изменении положения и размеров окна При этом, кстати, одно окно (перекрывающее) управляло отображением другого (перекрываемого). И, если этот подход был приемлемым для текстового режима, то можно представить, сколько памяти «пожирали» бы подобные буферы в графических режимах! В Windows пошли по другому пути. В Windows каждое окно «знает» о том, что оно
25
должно проделать в тех случаях, когда условия его отображения изменились. К этим случаям я отношу изменение размеров окна, его положения на экране, восстановления на экране после полного или частичного перекрытия другим окном. В этих случаях окну посылается сообщение 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
26
При необходимости закрыть окно, 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. Для этого программисту достаточно знать два-три макроса, несколько новых типов данных, и, естественно, новые наименования некоторых хорошо знакомых функций. Но обо всем по порядку.
28
РАБОТА С 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.
29
Таким образом, используя этот тип, мы можем объявлять данные, которые будут восприниматься в разных обстоятельствах то как 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
30
Пользуясь этим макросом, можено описать наши данные таким образом, что они будут компилироваться в обоих случаях так, как мы того хотим.
Попробуйте записать
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;
31
создается макрос, имя которого совпадает с обычным именем функции. При определении символа 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. Мне думается, я рассказал достаточно, чтобы у читателя появился «направляющий косинус» в этом вопросе. На страницах этой книги мы еще не раз встретим имя этой кодировки.
32
ОСНОВЫ РИСОВАНИЯ И КОПИРОВАНИЯ ИЗОБРАЖЕНИЙ
НЕМНОГО ЛИРИКИ
Когда я' обдумывал план этой книги, мне казалось, что за главой, в ко-трой описана структура программы, должна идти глава о взаимодействии программы с пользователем. Действительно, чего тянуть? Ведь подавляющее большинство программ для Windows написано с расчетом, что пользователь будет работать с программой в интерактивном режиме. Я начал было писать главу, посвященную меню и диалоговым окнам, но тут же вынужден был остановиться. Мне пришлось чуть ли не на каждой строчке извиняться и говорить, что все это мы узнаем чуть позже. Hitmap'bi в меню и кнопках - это была последняя капля, переполнившая чашу. Поэтому было принято следующее решение: на время несколько отклониться от нашего курса и изучить основы работы с изображениями (убрать тот самый камень с нашей дороги), после чего идти дальше. Думаю, что время, затраченное на изучение работы с изображениями, окупится сторицей. Я понимаю, что "читателю не терпится написать какую-нибудь грандиозную программу. Постараюсь, чтобы наше отклонение от курса было недолгим, и изложение буду вести почти в конспективном стиле (не в ущерб содержанию, надеюсь).
К этому моменту читатель уже представляет, как должна выглядеть программа для Windows, знает кое-что о сообщениях и о том, что программа сама отвечает за перерисовку окна в случае необходимости.
Давайте, уважаемый читатель, обсудим вопрос: что должно происходить в тех случаях, когда программе необходимо отобразить в рабочей области окна некоторое изображение? Для начала рассмотрим случаи, когда программе не нужно рисовать изображение, а необходимо скопировать одно изображение на другое. Поговорим о том, что такое
КОНТЕКСТ УСТРОЙСТВА
Наверное, читатель уже знает о том, что с точки зрения программиста Windows является системой, не зависящей от устройств (device independent). Эту независимость со стороны Windows обеспечивает библиотека GDI32.dll, а со стороны устройства - драйвер этого устройства. С точки зрения программы связующим звеном между программой и устройством является контекст устройства (Device Context - DC). Если
33
программе нужно осуществить обмен с внешним устройством, программа должна оповестить GDI о необходимости подготовить устройство для операции ввода-вывода. После того, как устройство подготовлено, программа получает хэндл контекста устройства, т. е. хэндл структуры, содержащей набор характеристик этого устройства. В этот набор входят:
bitmap (битовая карта, изображение), отображаемый в окне,
перо для прорисовки линий,
кисть,
палитра,
шрифт
и т. д. Программа никогда напрямую не обращается к контексту устройства (кстати, эта структура не документирована Microsoft), она обращается к нему опосредствованно, через определенные функции. После того, как все действия произведены, и необходимость в использовании устройства отпала, программа должна освободить контекст устройства, чтобы не занимать память. Есть еще одна причина, из-за которой необходимо освобождать контекст устройства. В системе может существовать одновременно только ограниченное число контекстов устройств. Если контекст устройства не будет освобождаться после операций вывода, то через несколько перерисовок окна система может зависнуть. Так что не забывайте освобождать контексты устройств!
Когда программа требует контекст устройства, она получает его уже заполненным значениями но умолчанию. Объект в составе контекста называется текущим объектом. Само слово - текущий - говорит о том, что контекст устройства можно изменить. Программа может создать новый объект, скажем, bitmap или шрифт, и сделать его текущим. Замещенный объект автоматически из памяти не удаляется, его необходимо позже удалить отдельно. Само собой разумеется, что программа может получить характеристики текущего устройства. А вот изменить эти характеристики, увы, можно только через замену объекта (впрочем, это и так попятно).
ТИПЫ КОНТЕКСТА УСТРОЙСТВА
В Windows поддерживаются следующие типы контекстов устройств: контекст дисплея (обеспечивает работу с дисплеем); контекст принтера (обеспечивает работу с принтером); контекст в памяти (моделирует в памяти устройство вывода); информационный контекст (служит для получения данных от устройства).
34
Контекст дисплея
Windows поддерживает три типа контекста дисплея - контекст класса, приватный контекст и общий контекст. Первые два типа используются в приложениях, которые выводят на экран большое количество информа-цни. Ярким примером такого рода приложений являются настольные издательские системы, графические пакеты и т.д.
Приложения, которые не очень интенсивно работают с экраном, используют общин контекст. Контекст класса является устаревшим и поддерживается только для обеспечения совместимости с предыдущими версиями Windows. Microsoft не рекомендует использовать его при разработке новых приложении и рекомендует использовать только при-иатный контекст. Исходя из этого, я решил пожалеть свое и читателя время и не рассматривать контекст класса.
Контексты устройств хранятся в кэше, управляемом системой. Хэндл общею контекста программа получает с помощью функций GelDCQ, (JctDCEx() или BeginPaint(). После того, как программа отработает с дисплеем, она должна освободить контекст, вызвав функцию R.eleaseDC() или EndPaiutO (в случае, если контекст получался с помощью HcginPaintO). После того, как контекст дисплея освобожден, все нзмене-мня, внесенные в него программой, теряются и при повторном получении контекста все действия по изменению контекста необходимо повторять заново.
Приватный контекст отличается от общего тем, что сохраняет изменения даже после того, как прикладная программа освободила его. Приватный контекст Fie хранится в кэше, поэтому прикладная программа может не освобождать его. Естественно, что в этом случае за счет использования большего объема памяти достигается более высокая скорость работы j дисплеем.
Для работы с приватным контекстом необходимо при регистрации класса окна указать стиль CS OWNDC. После этого программа может получать хэндл контекста устройства точно так же, как и в случае общего контекста. Система удаляет приватный контекст в том случае, когда удаляется окно.
При работе с контекстами необходимо запомнить, что хэндлы кон-гекста устройства с помощью функции BcginPaint() необходимо получать только в случае обработки сообщения WM PAINT. Во всех ^стальных случаях необходимо использовать функции GetDCQ или iietDCEx().
35
Контекст принтера
При необходимости вывода на принтер программа должна создать контекст устройства с помощью функции CrealeDC(). Аргументы этой функции определяют имя драйвера устройства, тип устройства и инициа-лизационные данные для устройства. Используя эти данные, система может подготовить принтер и распечатать требуемые данные. После распечатки прикладная программа должна удалить контекст принтера с помощью функции DeleteDCQ (а не ReleaseDC()). ,
Информационный контекст
Информационный контекст фактически не является контекстом устройства и служит только для получения информации о действительном контексте устройства. К примеру, для того, чтобы получить характеристики принтера, программа создает информационный контекст, используя для этого функцию CreatelQ), а затем из него выбирает требующиеся характеристики. Естественный вопрос: а для чего нужно использовать информационный контекст? Почему нельзя те же самые данные получить из действительно контекста? Дело в том, что этот тип контекста создается и работает намного быстрее, а также занимает меньше памяти по сравнению с действительным контекстом. После того, как надобность в информационном контексте миновала, программа должна удалить его с помощью функции DeleteDCQ.
Чаще всего для вывода информации на устройство используется
Контекст в памяти
Этот контекст используется для хранения изображений, которые затем будут скопированы на устройство вывода. Сам по себе контекст в памяти не создается. Он обязательно создается как совместимый с тем устройством или окном, на которое предполагается копировать информацию (вот он - совместимый контекст - переходник между программой и драйвером устройства!). Алгоритм работы с контекстом в памяти состоит из нескольких шагов:
1. Получения хэндла контекста устройства (назовем его hDC - handle of Device Context) для окна, в которое будет осуществляться вывод изображения.
2. Получения хэндла bitmap'a, который будет отображаться в окне.
3. Получения совместимого с hDC контекста в памяти (для хранения изображения) с помощью функции CreateCompatibleDC() (обратите внимание на название функции - создать СОВМЕСТИМЫЙ контекст).
36
4. Выбора изображения (hBitmap) как текущего для контекста в памяти (hCompatibleDC).
5. Копирования изображения контекста в памяти (hCompatibleDC) на контекст устройства (hDC).
6. Удаления совместимого контекста (hCompatibleDC);
7. Принятия мер для того, чтобы замещенный bitmap из контекста в памяти не остался в памяти.
8. Освобождения контекста устройства (hDC).
При необходимости шаги 6 и 7 можно поменять местами. Когда и как удалять замещенный bitmap, зависит от программиста и поставленной перед ним задачи.
Именно этот способ и используется в большинстве программ для копирования изображения.
Но, как известно, лучше один раз увидеть, чем сто раз услышать (по-английски это звучит еще более категорично - seeing is believing - увидеть, значит поверить). Поэтому давайте напишем небольшую программу, в которой продемонстрируем возможности Windows по манипулированию изображениями.
SEEING IS BELIEVING
Перед тем, как начинать писать программу, мы должны прояснить для себя еще одну тему. В каких единицах измеряются размеры окна и, соответственно, все смещения в окне? Для того чтобы ответить на этот вопрос, мы должны рассмотреть
Режимы отображения
Подавляющее большинство функций, работающих с оконными координатами, определяют координаты относительно начала рабочей области окна, т. е. от левого верхнего угла.
Таким образом, даже при перемещении окна координаты объектов внутри окна остаются неизменными. При этом единицы, в которых измеряются координаты, зависят от режима отображения (mapping mode), установленного для данного окна. Единицы измерения, зависящие от режима отображения, называют логическими единицами, а координаты в )том случае называют логическими координатами.
При выводе информации на конкретное устройство единицы логических координат преобразуются в физические единицы, которыми являются пиксели.
37
Таблица 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). Надеюсь, что после того, как была просмотрена таблица, вопросов у читателя не возникло. Теперь ясно, что иногда для решения конкретных задач (например, построения графиков) можно использовать различные режимы отображения. При создании окна по умолчанию устанавливается режим ММ_ТЕХТ, т. е. все координаты исчисляются в пикселах.
38
Пишем програмл 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;
39
/* 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); // Удаляем совместимый контекст.
40
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...
41
В Program No 1
Рис. 2. Вид окна, отображаемого программой
Понятно, что окно далеко не всегда должно перерисовываться полностью. К примеру, только часть окна перекрывается другим окном. Естественно, что и перерисоваться должна только часть окна. В этом случае полная перерисовка всего окна будет только лишней тратой времени и ресурсов компьютера. Windows «знает» о том, какая часть окна перекрыта. При необходимости перерисовки (при вызове BeginPaintQ) система заполняет структуру типа PAINTSTRUCT, адрес которой передается системе как второй аргумент BeginPaintQ. Среди всех полей структуры типа PAINTSTRUCT основным (на мой взгляд) является поле, содержащее координаты той области (clipping region), которая должна быть перерисована. В нашем примере информация, хранящаяся в этой структуре, не используется, но я прошу читателя обратить внимание на эту структуру и в дальнейшем использовать ее. Получив от функции BeginPaint() хэндл контекста устройства (hDC), будем считать первый шаг выполненным.
Второй шаг - получение хэндла bitmap'а, который будет отображаться в окне - мы делаем, вызывая функцию Loadlmage(). Я не случайно вос-
42
пользовался именно этой функцией. Во-первых, возможности этой функции достаточно широки. Она позволяет загружать графические образы как из ресурсов, записанных в исполняемом файле, так и из файлов, содержащих только изображения. Графическим образом может быть 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 аргументы указывают ширину и высоту иконки или курсора и в нашем примере не используются.
43
Таблица 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 эта функция описана следующим образом:
44
WINGDIAPI BOOL W1NAP1 StretehBlt(HDC, int. int. int, int, HDC, hit, int. int. int,
DWORD);
Первые пять аргументов описывают тот прямоугольник на экране, в который будет вписан bitmap (на рис. 3 он обозначен светло-серым инешм). Ту часть bitmap'a, которая будет вписана в прямоугольник на чоране (на рисунке - пересекающаяся часть светло-серого и темно-серого прямоугольников), описывают следующие пять аргументов. И последний, одиннадцатый аргумент, так называемый код растровой операции, описывает, каким образом пиксели одного bitmap'a будут взаимодействовать
45
с пикселами другого bitmap'a. Для того чтобы лучше понять аргументы функции, обратимся к рис. 3. Представим, что в окне, в регионе, обозначенном на рисунке светло-серым цветом, нужно отобразить bitmap (обозначен на рисунке темно-серым цветом) или часть bitmap'a, при необходимости сжав или растянув ее.
Рис. 3. Взаимодействие аргументов функции StrelchBIt
Итак, первый и шестой аргументы функции - хэндлы окна (hDC) и совместимого контекста (hCompatibleDC) соответственно. Второй (nXOriginDest) и третий (nYOriginDest) аргументы содержат смешение верхнего левого угла прямоугольника, в который будет вписываться bitmap, относительно левой и верхней сторон рабочей области окна (В каких единицах выражено смещение? Напомню, мы создали окно и не меняли режим отображения, т. е. текущий режим является уста-нов*ленньга по умолчанию). Четвертый (nWidthDest) и пятый (nlleightDest) аргументы - ширина и высота этого прямоугольника. Седьмой (nXOriginSrc) и восьмой (nYOriginSrc) аргументы аналогичны второму и третьему аргументам. Девятый (nWidthSrc) и десятый (ntleightSrc) аргументы содержат ширину и высоту отображаемой части bitmap'a. He нужно обладать развитым пространственным воображением, чтобы
46
понять, что, изменяя положения прямоугольников друг относительно ipyra, а также относительно окна, меняя их размеры, можно отобразить на экране любую часть или целый bitmap.
В примере мы хотим фактически совместить наш bitmap с рабочей об-мсгыо окна, поэтому второй и третий аргументы вызываемой функции раины нулю. Четвертый и пятый аргументы равны ширине и высоте рабочей области (мы получили ширину и высоту рабочей области с помощью функции GctClientRectO). Седьмой и восьмой аргументы равны пулю (подумайте, почему?), а девятый и десятый содержат ширину и высоту bitmap'a, которые мы получили, обратившись к GetObject(). Вот, кажется, и все. Нет, не все.
Как работает одиннадцатый аргумент, определяющий взаимодействие двух bitmap'oB? Давайте закончим обсуждение нашего алгоритма, а затем вернемся к этому вопросу.
Мы прошли уже пять шагов алгоритма. Остались еще три шага - удаление совместимого контекста, объекта и контекста устройства - пусть чшатель сам определит, какие операторы программы их реализуют.
I (еужели мы добрались до конца нашего алгоритма? Мне он показался бесконечным! Давайте прежде чем рассматривать одиннадцатый аргумент упомянем об оставшихся «могучих Bit».
Функция BitBltQ тоже копирует bitmap с одного контекста на другой, но без масштабирования. Следовательно, ей не нужны девятый и десятый аргументы - ширина и высота отображаемой области - отображается все ю, что поместится в отведенную для этого область (светло-серый прямоугольник на рис. 3).
Последняя из «могучих» - функция PatBltQ - просто закрашивает прямоугольник на экране, используя для этого выбранную в контексте устройства кисть. Раз нет отображаемого объекта, то зачем нам сто контекст и координаты? Отбрасываем аргументы с шестого по десятый включительно и получаем список аргументов PatBltQ.
Наконец мы подошли к тому, чтобы уяснить, что же представляет собой одиннадцатый аргумент функции StretchBltQ. Сейчас мы поговорим о том, что же такое
КОДЫ РАСТРОВЫХ ОПЕРАЦИЙ
Выше уже было сказано, что одиннадцатый аргумент функции SiretchBltO - это код растровой операции. Другими словами, это код, который определяет, как при операции будут взаимодействовать биты, определяющие заливку и изображение совместимого контекста с изобра-
47
жением на действительном контексте. Комбинируются биты на основе логических операций над ними. По укоренившейся в книгах по программированию для Windows традиции, эти операции записываются в обратной польской нотации (не путать с венгерской, автор польской нотации не имеет к Microsoft ни малейшего отношения).
По той же традиции, биты, определяющие bitmap совместимого контекста, обозначают буквой S (source - источник, исходный), биты заливки - буквой Р (pattern - образец), а биты, на которых будет прорисовываться изображение - буквой D (destination - назначение, место назначения). Операции обозначаются следующим образом: а - побитовое И (AND), n -побитовое НЕТ (NO), о - побитовое ИЛИ (OR), x - побитовое исключающее ИЛИ (XOR).
Несколько слов о польской нотации. В ней операции записываются слева направо. Знак операции следует за операндами. Появление знака операции означает, что нужно произвести следующие действия: взять два последних операнда; произвести с ними требующуюся операцию; записать результат на место последних двух операндов. Фактически польская нотация описывает действия таким образом, словно операнды и операции находятся в стеке, для чего, собственно, эта польская нотация и была изобретена.
Обозначив знак операции как Ор, в польской нотации действия с битами можно записать таким образом: PSOp
Это говорит о необходимости взять пиксель патерны и прорисовываемого bitmap'a и произвести над ними операцию. Если в операции участвуют три операнда, то получим: DPSOplOp2
Что мы должны сделать в этом случае? Правильно, сначала произвести действие, определяемое Opl, с битами патерны и прорисовываемым bitmap'oM, после этого произвести Ор2 с полученным результатом и битами действительного контекста. Ничего сложного здесь нет.
Каждый код растровой операции представляется 32-битным целым. Старшее слово кода представляет собой индекс битовой операции, младшее - код операции. Как определяется индекс операции?
Давайте представим, что нам необходимо определить индекс растровой операции, определяемой в польской нотации записью DPSxx. Попутно можно определить и индекс операции PSx. Запишем друг под другом ОПРЕДЕЛЕННЫЕ значения Р, S
и D, а под ними - результаты побитовых операций PSx и DPSxx:
48
1 |
1 |
1 |
1 |
0 |
0 |
0 |
0 |
1 |
1 |
0 |
0 |
1 |
1 |
0 |
0 |
i |
0 |
1 |
0 |
1 |
0 |
1 |
0 |
x 0 |
0 |
1 |
1 |
1 |
1 |
0 |
0 |
'Sxx 1 |
0 |
0 |
1 |
0 |
1 |
1 |
0 |
Т а б л и ц а 9. Краткое описание колон растровых операций
] ^именование |
Инлскс операции |
Польская запись |
Эффект |
IU.ACKNF.SS |
0x00 |
{) |
Заполнение действительного |
|
|
|
контекста черным цветом |
\OTSRCERASE |
Oxl 1 |
DSoii |
|
NOTSRCCOPY |
0x33 |
Sn |
Прорисовываемый bitmap отобража- |
|
|
|
ется в негативном виде |
OSTINVHRT |
0x55 |
Dn |
Изображение действительного контекста |
|
|
|
проявляется негативным |
I'ATINVERT |
Ox5A |
DPx |
|
4RCTNVFRT |
0x66 |
DSx |
|
SRCAND |
Ox8X |
DSa |
|
MERGEPAINT |
OxBB |
DSno |
|
MI-RGFiCOPY |
OxCO |
PSa |
|
SRCCOPY |
OxCC |
S |
Копирование прорисовываемого |
|
|
|
bitmap'a на действительный кон- |
|
|
|
текст |
SRCPAINT |
OxEE |
DSo |
|
I'ATCOPY |
OxFO |
P |
Копирование патерны на действи- |
|
|
|
тельный контекст |
''\TPAINT |
OxFB |
DPSnoo |
|
WHITENESS |
OxFF |
1 |
Заполнение действительного |
|
|
|
кон текста белым цветом |
Фугом записываются не произвольные, а строго определенные значения.
Mil значения позволяют перебрать все возможные комбинации патерны,
«сходного и целевого bitmap'oB. Теперь, когда все стало ясно, вы можете i 'опробовать попрактиковаться в определении индексов любых операций. ! !есмотря на то, что существуют 256 индексов растровых операций, на
рактике используются только некоторые из них. В файле wingdi.h для чапболее часто используемых растровых операций определены иденти-'•'пкаторы, которые приведены в табл. 9.
49
На основании данных табл. 9 я затрудняюсь объяснить, как изменяется изображение при использовании разных растровых операций. Рекомендую читателю запустить приведенную выше программу несколько раз, и каждый раз в функции StretchBltQ указывать новую растровую операцию. Seeing is believing!
Теперь и одиннадцатый аргумент PatBlt() стал ясным и понятным - я просто копирую bitmap в окно. Только и всего. Кстати, понимание логики работы с растровыми операциями можег позволить избежать трудоемких преобразований bitmap'oe перед копированием.
ПОЛОСЫ ПРОКРУТКИ
Мне кажется, что у читателя может возникнуть вопрос: что делать в тех случаях, когда нам не нужно масштабировать bitmap, но нужно иметь возможность просматривать все части изображения? Ответ заключен в названии данного раздела - использование полос прокрутки.
Давайте, уважаемый читатель, чуть-чуть изменим предыдущую программу для того, чтобы продемонстрировать использование полос прокрутки, и посмотрим, как она будет работать. Текст программы с внесенными изменениями:
^include <windows.li*
long WINAPI DCDcmoWndProc ( HWND, UfNT, UINT, LONG );
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrevInstaiicc, LPSTR IpszCmdParam, int nCmdShow )
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msg;
char szClassName[] = "DCDemo";
/* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style = CSJ-IREDRAW [ CSJVREDRAW;
WndClass.lpfnWndProc - DCDcmoWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hlnstance = hlnstance ;
WndClass.hlcon - Loadlcon (NULL,IDI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH) GctStockObjcct (WHITEJ3RUSH);
WndClass.IpszMenuName = "MyMenu";
WndClass. IpszClassName — szClassNamc;
50
il'( !RegisterClass(&WndClass) )
McssageBox(NULL,"Cannot register class","Error",MB_OK); return 0;
hWnd = CreateWindow(szClassNamc, "Program No 1", WS_OVERLAPPEDWINDOW | WS_VSCROLL| WS_HSCROLL, CWJJSEDEFAULT, CWJJSEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hlnstance.NULL);
il'(!hWnd)
MessageBox(NULL,"Cannot create window","Error",MB_OK); return 0;
/* Show our window */ ShowWindow(hWnd,nCmdShow); UpdatcWindow(hWnd);
/* Beginning of messages cycle */
whilc(GctMcssage(&Msg, NULL, 0, 0))
TranslatcMessage(&Msg); DispatchMessage(&Msg);
return Msg.wParam;
LRESULT CALLBACK DCDcmoWndProc (HWND hWnd. UINT Message,
UINT wParam, LONG IParam )
HOC hDC, hCompatibleDC;
PAINTSTRUCT PamtStruct;
static HANDLE hBitmap;
HANDLE hOldBitmap;
RECTRect;
BITMAP Bitmap;
static int nliorizDifference = 0, nVertDilTerence ~ 0;
static int nHorizPosition = 0, nVertPosition - 0;
switch(Messagc) i
case WM CREATL:
hBitmap =• LoadlmagcfNULL, "MSDOGS.BMP" IMAGE_BITMAP,
51
о, о,
LR_LOADFROMFILE); return 0;
case WM_PAINT:
hDC ^ BeginPaint(hWnd, &PaintStruct); GetObject(hBitmap, sizeof(BITMAP), &Bitmap); hCompatibleDC = CreateCompalibleDC(hDC); hOldBitmap = SelectObject(hCompatibleDC, liBitmap); GctClientRcct(hWnd,&Rcct); BitBltdiDC, 0, 0, Rect.right, Rect.bottom,
hCompatibleDC, nHorizPosition, nVertPosilion, SRCC'OPY); if( (nHorizDii'terence = (Bitmap.bmWidth - Rect.right)) > 0)
SetScrollRange(hWnd, SBJTORZ, 0, nHonzDifference, TRUE); else
SctScrollRange(liWnd, SBJIORZ, 0. 0, TRUE); if( (nVertDifference = (Bitmap.bmHeight - Rect.bottom)) > 0)
SetScrol]Range(h\Vnd, SBJVERT, 0. nVertDilTcrence, TRUE); else
SetScrollRangc(hWnd, SB_VERT, 0. 0, TRUE); SelectObject(hCompatibleDC, hOldBitmap); DeleteDC(hCompatibleDC); EndPaint(hWnd,&PaintStmct); return 0;
case WM_VSCROLL: svvitcli(LOWORD(wParam))
\
case SB LINEDOWN;
if(nVcrtPosition < nVertDiiTerencc) nVerlPosition—;
break; case SB_LINEUP:
if(nVcrtPosi(ion > 0) nVerlPosition—;
break; ease SB_THUMBTRACK:
nVcrtPosition - HIWORD(wParam);
break; I
SetScrollPosfhWnd, SB VERT, nVcrtPosition, TRUE); InvalidatcRect(hWnd. NULL, TRUE); return 0;
case \VM_HSCROLL: switch(LOWORD(wParam))
I
'caseSB_LINEDO\VN: ii'fnHorizPosition < nHorizDilTerence)
ullori/.Position—'-; break:
case SB LINEUP: if(nHorizPosition > 0)
52
nHori/.Position--;
break;
case SBjniUMBTRACK: nHori/Posilion -- HIWORD(wParam); break;
SelScrollPos(h\Vmi, SBJIORZ, nHori/Position, TRUE); InvalidateRect(h\Vnd, NULL, TRUE); return 0;
case WM_DF,STROY: DeicteObjea(liBitmap); PostQu!lMessage(0); relnrti 0;
return DefWindowProc(h\Vnd,Messagc,wParam, Il'aram);
Пил. окна, создаваемого программой, показан на рис. 4.
Ноли в окне не отображаются горизонтальная и вертикальная полосы прокрутки, необходимо уменьшить размеры окна по горизонтали и нертикали.
BProgiamNol
i\ i. Пример |VK:.'ibi пилос прокрутки
53
Появились полосы прокрутки? Давайте разберемся, благодаря чему это произошло.
Во-первых, если сравнить вызовы функций CreateWindow() в этой и предыдущей программах, то можно увидеть, что у окна появились два новых стиля - WS__HSCROLL и WSJVSCROLL. Эти стили определяют наличие у окна горизонтальной и вертикальной полос прокрутки соответственно. Первый шаг сделан. Этот шаг можно было бы сделать и по-другому, определив полосы прокрутки как дочерние окна, но о дочерних окнах мы будем говорить позже. Разница между полосами прокружи, являющимися частью окна, и полосами прокрутки - дочерними окнами состоит в том, что дочерние окна имеют встроенный клавиатурный интерфейс, позволяющий воздействовать на полосу прокрутки с помощью клавиатуры. Встроенным полосам прокрутки, к сожалению, досталось только управление с помощью курсора мыши.
Теперь необходимо определить диапазон прокрутки, который определяет число шагов между крайними позициями бегунка (слайдера). По умолчанию для полос прокрутки, являющихся частью окна, этот диапазон определен от 0 до 100. Для того чтобы изменить диапазон прокрутки, необходимо вызвать функцию SetScrollRange(), которая в файле winuser.h определена следующим образом:
WINUSERAPI BOOL WINAPI SetScrollRange(HWND hWnd, int nBar,
int nMinPos, int nMaxPos, BOOL bRedniw);
Первый аргумент функции - хэндл окна, которому принадлежат полосы прокрутки. Второй аргумент определяет, для какой полосы прокрутки (вертикальной или горизонтальной) устанавливается диапазон. В данном случае этот аргумент может принимать значение SB_VERT или SB_HORZ, что определяет работу с вертикальной или горизонтальной полосой прокрутки. Третий и четвертый аргументы непосредственно указывают нижнюю и верхнюю границу диапазона прокрутки. Пятый аргумент представляет собой флаг, определяющий, нужно ли перерисовывать полосу прокрутки после определения диапазона. TRUE - полоса прокрутки перерисовывается, FALSE - перерисовка не нужна. Заметьте, что если диапазон прокрутки определен от 0 до 0, то полоса прокрутки становится невидимой. Это свойство используется и в приведенной выше программе. В том случае, когда размеры окна превышают размеры отображаемого bitmap'a, у полос прокрутки устанавливается диапазон от О до 0, следовательно, полоса прокрутки скрывается.
54
В данном случае с помощью функции SetScrollRangeQ диапазон прокрутки определен как разность между размером bitmap'a и размером окна по вертикали и по горизонтали, т. с. шаг полосы прокрутки соответствует одному пикселю.
Воздействовать на полосы прокрутки можно по-разному: во-первых, можно щелкнуть клавишей мыши на стрелах, расположенных по краям полосы; во-вторых, можно щелкнуть на полосе выше или ниже слайдера. Наконец, можно перетащить слайдер на другое место. Все эти воздействия приводят к тому, что оконная функция окна, которому принадлежат полосы прокрутки, получает сообщение WM VSCROLL (если действия производились вертикальной полосой) или WM_HSCROLL (реакция на воздействие на горизонтальную полосу).
Характер воздействия оконная функция может определить по параметрам сообщения. Младшее слово wParam, которое и определяет харак-i ср воздействия на полосу прокрутки, может принимать значения, приведенные в табл. 10. В таблице показано, что прокрутка при нажатии клавиши мыши в некоторых случаях производится на одну строку и одну страницу. В данном случае необходимо осознать, что понятия «строка» и «страница» ничего общего с текстовой строкой и страницей не имеют. Этими понятиями я заменит! условные единицы, на которые прокручивается изображение в окне. К примеру, в приведенной программе строке соответствует один пиксель, а понятие страницы вовсе не определено (что есть страница для картинки?).
После ознакомления с таблицей становится ясно, какие •xl.OWORD(wParam)» должна обрабатывать прикладная программа.
Старшее слово wParam используется только в тех случаях, когда LOWORD(wParam) равен SBJTHUMBPOSITION или SB THUMBTRACK. В этих случаях оно хранит позицию слайдера. В остальных случаях это значение не используется.
В тех случаях, когда полосы прокрутки реализованы как дочерние окна, IParam содержит хэндл окна полосы прокрутки. Если полоса реализована как часть окна, этот параметр не используется.
После того, как мы зафиксировали факт произведенного с полосой прокрутки действия и характер действия, программа должна правильно отреагировать на него и при необходимости изменить позицию слайдера в соответствии с произведенным воздействием. Делается это с помощью обращения к функции SetScrollPosQ, которая следующим образом описана в файле winuser.h:
WINUSERAPI int WINAPI SetScrolIPos(HWND hWnd, int nBar. int nPos,
BOOL bRcdraw);
55
Таблица 10. Идентификаторы характером воздействия на полосы прокрутки
Окончание табл. 10
Парамеф |
Значение |
Описание |
|
||
SBJLINF.UP |
0 |
Используется только с WM_VSCROLL; |
|||
|
|
щелчок мытью на стрелке вверх; приво- |
|
||
SB_LUNELEFT |
0 |
дит к прокрутке на одну «строку» вверх Используется только с WM HSCROLL, |
|
||
|
|
щелчок мышью на стрелке влево; приво- |
|
||
SBJLINEDOWN |
1 |
дит к прокрутке на одну «колонку» влево Используется только с \VM VSCROLL, |
|
||
|
|
щелчок мытью на стрелке ьни i; приводит к |
|
||
SBJJNERIGHT |
1 |
прокрутке на одну «строку» вниз Используется только с WM HSCROLL., |
|
||
|
|
щелчок мышью на стрелке вправо; приводит к |
|
||
SB_PAGFUP |
2 |
прокрутке на олну «колонку» вправо Используется •! ,ль„о с WMJVSCROLL, |
|
||
|
|
щелчок мышью на полосе прокрутки выше |
|
||
|
|
слайдера; приводит к прокрутке на одну |
|
||
SB PAGELEFT |
2 |
«страницу» вверх Используется ю.Т1,кос\\'М HSCROLL, |
|
||
|
|
щелчок мышью на полосе прокрутки левее |
|
||
|
|
слайдера; приводит к прокрутке на одну |
|
||
SB PAGEDOWN |
3 |
«страницу» влево Используется только с WM VSCROLL, |
|
||
|
|
щелчок мышью на полосе прокрутки ниже |
|
||
|
|
слайдера; приводит к прокрутке на одну |
|
||
SB_PAGERIGHT |
j |
«страницу» вниз Используется только с WM HSCROLL, |
|
||
|
|
щелчок мышью на полоее прокрутки правее |
|
||
|
|
слайдера. прпво.ттп к прокрутке на одну |
|
||
SB_THUMBPOSITION |
4 |
«страницу» вправо Перетаскивание слайдера закопчено, пользова- |
|
||
SB_THUMBTRACK |
5 |
тель отжал клавишу мыши Слайдер перетаскивается с помощью мыши, |
|
||
|
|
приводит к перемещению содержимого |
|
||
SB TOP |
6 |
чкрана Используется только с вертикальными полоса- |
|
||
SBJ.EFT |
6 |
ми прокрутки, реализованными как дочерние окна, пользователь нажал клавишу «Ноте» Используется только с горизонтальными |
|
||
|
|
полосами прокрутки, реализованными как |
|
||
|
|
дочерние окна, пользователь нажал клавишу «Ноте» |
|
||
SB BOTTOM |
7 |
Используется только с вертикальными полоса- |
|
||
|
|
ми прокрутки, реализованными как дочерние окна, пользователь нажал клавишу «bnd» |
|
||
I lapasieip |
Значение |
Опиеание |
|||
I SB RIGHT SBJ-NDSCROLL |
7 8 |
Используется только с горизонтальными полосами прокрутки, реализованными как дочерние окна, пользователь нажал клавишу «End» Пользователь отпустил клавишу мыши после удержания се нажатой на стрелке или на полосе прокрутки |
|||
I lepiibrii аргумент - это хэндл окна, содержащего полосу прокрутки (в u)M случае, если полоса прокрутки реализована как часть окна), второй аргумент может принимать значение SBJVERT или SB HORZ (об этих значениях говорилось выше), третий аргумент определяет, в какую позицию должен быть установлен слайдер. И наконец, четвертый аргумент определяет, нужно ли перерисовывать полосу прокрутки после установки слайдера. Если последний аргумент равен TRUE, то полоса прокрутки будет перерисована.
Для того чтобы в соответствии с новой позицией слайдера изменилось изображение в рабочей области, окну необходимо послать сообщение \VM_PAINT, которое заставит окно перерисоваться. В программе, приведенной выше, сообщение WM_PAINT окну посылается с помощью вызова функции InvalidateRectQ. Из этого следует, что код обработки сообщения WM
PAINT
в оконной функции должен разрабатываться с учетом того, что содержимое окна может прокручиваться (скроллироваться).
II в заключение мне бы хотелось слегка посыпать голову пеплом. Лю-•fioii
хоть немного понимающий в программировании человек ужаснется, когда увидит, чго я загружаю изображение из файла при каждой перерисовке окна (в программе, использующей функцию StretchBltQ). Это резко замедляет работу программы и занимает слишком много ресурсов. Но в шпном случае целью было не написание программы, работающей оптимальным образом., а простая демонстрация того, что должна сделать программа для того, чтобы вывести на экран изображение.
'Кстати, когда я начинал писать этот раздел, я думал, что уложусь в две-три страницы. Зато теперь надеюсь, что у читателя не осталось никаких вопросов относительно манипулирования изображениями. Естественно, что манипулированием изображениями графические возможности Windows отнюдь не исчерпываются. По я достиг своей цели.
56
57
Теперь мне не придется постоянно просить читателя подождать, когда мы потом что-нибудь изучим. И теперь я готов к тому, чтобы продолжить изложение нашей «азбуки».
КОНТЕКСТ УСТРОЙСТВА И WM PAINT
Я уже говорил, что в Windows окно само отвечает за перерисовку себя. Для того чтобы окно осуществило перерисовку, оно должно получить сообщение WM_PAINT. Каким образом осуществляется перерисовка?
Обычно используют один из трех методов:
рабочая область может быть восстановлена, если ее содержимое формируется с помощью каких-либо вычислений;
последовательность событий, формирующих рабочую область, может быть сохранена, а затем "проиграна" сколь угодно раз (имеются в виду метафайлы, но их рассмотрение выходит за рамки этой книги);
можно создать виртуальное окно и направлять весь вывод в виртуальное окно, а при получении основным окном сообщения WM_PAINT копировать содержимое виртуального окна в основное.
Думаю, что читатель догадался, что в качестве виртуального окна используется контекст в памяти. Как его копировать, мы уже знаем. Но как рисовать на нем?
РИСОВАНИЕ ГРАФИЧЕСКИХ ПРИМИТИВОВ
Наверное, в подавляющем большинстве случаев читателю для работы с графикой будет достаточно того, о чем он только что прочитал. Теперь он сможет создавать изображение в графическом редакторе и копировать его в окно.
Но что делать читателю в том случае, когда необходимо это изображение создать в ходе самое программы? Как ему быть, если, к примеру, необходимо отобразить спектр радиосигнала, принятого его приемником? Или нарисовать график функции? Без краткого введения в основы рисования не обойтись. Если читателю не нужно в ближайшее время создавать изображение в программе, он может смело пропустить этот раздел и перейти к разделу «Взаимодействие программы с пользователем», а к этому разделу вернуться только в случае надобности.
Итак, Windows - операционная система, которая предоставляет пользователю графический интерфейс. Наша задача - научиться создавать в программе изображение, которое в дальнейшем мы будем использовать.
58
ТО, БЕЗ ЧЕГО РИСОВАНИЕ НЕВОЗМОЖНО
А невозможно рисование, во-первых, без инструментов.
Инструментами рисования в Windows являются перо (реп) и кисть (brash). Перо является инструментом для прорисовки линий, цвет и способ заполнения замкнутых графических объектов, таких, как круги, прямоугольники, эллипсы и так называемые регионы, определяются текущей кистью. Во-вторых, рисование невозможно без определения той точки, от которой мы начинаем прорисовку того или иного графического объекта. Обычно эта точка называется текущей графической позицией.
Установка текущей позиции
Для установки текущей позиции используется функция MoveToExQ. В файле заголовков wingdi.h эта функция описывается следующим образом:
WTNGDIAPI BOOL WINAPI MoveToEx(HDC, irit, int, LPPOINT);
Первый аргумент - это контекст устройства, на котором мы будем рисовать, второй и третий - координаты точки, в которую мы устанавливаем текущую графическую позицию. Последний аргумент - указатель на структуру типа POINT, в которую функция запишет координаты старой текущей позиции. Структура типа POINT описана в файле windef.h и ее описание выглядит следующим образом:
typedef struct tagPOINT
LONG x; LONG y; } POINT, *PP()INT, NEAR *NPPOINT, FAR *LPPOINT;
Если при вызове функции указатель на структуру типа POINT равен NULL, то координаты старой текущей позиции не возвращаются.
Прорисовка одного пикселя
Прорисовать один пиксель в определенной позиции мы можем с помощью вызова функции SetPixelQ, описанной в wingdi.h:
WINGDIAPF COLORREF WINAPI SetPixcl(HDC. int. int, COLORREF);
59
Первые три аргумента очевидны - контекст устройства вывода и координаты прорисовываемого пикселя. Но что такое COLORREF?
Здесь следует пояснить, что каждый пиксель на экране состоит из тех микроточек - красной, зеленой и синей. Каждая из этих микроточек может светиться с интенсивностью от 0 (микроточка не светится) до 255 (максимальная яркость). Например, если светится только красная составляющая, то получаются цвета от темно-бордового (почти черного) до ярко красного. Комбинируя микроточки и их интенсивность, мы можем определить почти 17 миллионов цветов (будут ли они все поддерживаться на компьютере читателя, определяется видеоподсистемой компьютера, но это уже другой разговор). Обычно в таких случаях говорят об RGB значениях цвета (red, green, blue - красный, зеленый, голубой).
Вернемся к COLORREF. Опять обратимся к заголовочному файлу, но на сей раз не к wingdi.h, а к windef.h:
typedcf DWORD COLORREF;
Понятно, что COLORREF - это двойное слово. Оно кодируется следующим образом:
OxOObbggrr
т. е. младший байт определяет интенсивность красного, второй - зеленого, третий - синего цвета. Старший байт должен быть нулевым. Для того чтобы облегчить жизнь пользователю, Microsoft в wingdi.h вставила макрос RGB:
«define RGBfr. g, b) ( (COLORREF) (((BYTF.)(r) | ((WORD) ((BYTEKg)) <-'X ))
(((d/aokd) (ija.ij-:) (p)) «\c)))
С первого взгляда в этом не разобраться. Поэтому приведу пример определения цвета с RGB - значениями 0, 100, 200
RGB(0, 100,200);
Стало понятнее, не правда ли?
При нормальном завершении функция возвращает предыдущее значение цвета пикселя. Если возвращаемое значение равно -1, то по говорит либо о возникновении ошибки, либо о том, что координаты пикселя вышли за пределы рабочей области окна.
60
Теперь мы готовы прорисовывать пиксели везде, где только можно. А если мы сумеем прорисовывать один пиксель, то сможем прорисовать и много. Но для того, чтобы рисовать, скажем, прямую, необходимо знать и реализовать в программе алгоритм прорисовки линий. То же можно сказать и о кругах, эллипсах и т. д. Думаю, что перспектива самостоятельной разработки этих алгоритмов читателю вовсе не улыбается. Но нет ничего страшного, команда разработчиков Win32 и здесь сняла проблему с программистов. Перейдем к достаточно обширной теме под названием
РИСОВАНИЕ ГРАФИЧЕСКИХ ПРИМИТИВОВ
Создание пера для рисования линий
Рисование графических примитивов производится с помощью перьев. В Windows'95 есть три предопределенных пера - черное (BLACK_PEN), белое (W1IITE_PEN) и прозрачное (NULL_PEN). При создании окна по умолчанию ему присваивается черное перо. Хэндл каждого из них может быть получен с помощью функции GetStockObjectQ. Естественно, что программиста ие может удовлетворить столь малое число перьев, поэтому для прорисовки линий можно воспользоваться пером, созданным в программе посредством вызова функции CreatePenQ. Как всегда, обращаемся к файлам заголовков, в данном случае - к файлу wingdi.h:
WINGDIAPI HPEN WINAPI CreatePen(int, int. COLORREF);
Первый аргумент определяет стиль кисти. В wingdi.h эти стили описаны достаточно образно. Для того чтобы сохранить стиль этого описания (не путать со стилем кисти) я включил его третьим столбцом в табл. 11.
Не правда ли, «seeing is believing»?
Второй аргумент функции CreatePen() - толщина пера в логических единицах. Если этот аргумент равен 0, то толщина пера делается равной одному пикселю.
Третий аргумент - цвет чернил. Теперь для того, чтобы мы могли использовать наше перо, необходимо сделать его текущим в контексте устройства. Делается это уже давно знакомой нам функцией SclectObjectO. После того, как мы отработаем с пером, необходимо удалить его, вызвав функцию DeleteObject().
Мы создали перо. А теперь нам необходимо научиться рисовать примитивы.
61
Таблица И. Возможные стили кисти
Стиль пера |
Значение |
Описание |
Эффект |
PS SOLID |
0 |
|
Сплошная линия |
PS DASH |
1 |
|
Пунктирная линия |
|
|
|
|
PS DOT |
2 |
|
Линия из точек |
PS_DASHDOT |
3 |
|
Штрих-пунктирная линия (тирс- |
|
|
|
точка) |
PS DASHDOTDOT |
4 |
|
Штрих-пунктирная линия (тире - |
|
|
|
точка - точка) |
PS NULL |
5 |
|
Прозрачное перо |
PSJNSIDEFRAME |
6 |
|
При рисовании замкнутой фигуры |
|
|
|
граница фигуры будет определяться |
|
|
|
по внешнему краю, а не по середине |
|
|
|
линии (если толщина пера более 1 |
|
|
|
пикселя) |
Рисование линии
Нарисовать линию можно с помощью функции LineTo(). Она описана в файле wingdi.h:
WINGDIAPI BOOL WINAPI LincTo(HDC, int, int);
Первый аргумент - контекст устройства. Второй и третий аргументы - координаты точки, ДО КОТОРОЙ ОТ ТЕКУЩЕЙ ПОЗИЦИИ будет проведена линия. При успешном завершении функция возвращает TRUE.
Но здесь же возникает вопрос: где будет находиться текущая позиция после успешного выполнения функции? А будет она находиться там, где закончилась линия. Это сделано для того, чтобы легко можно было рисовать ломаные линии. В таком случае не нужно многократно вызывать функцию MoveToExQ для установления новой текущей позиции.
Рисование прямоугольника
Прямоугольник можно нарисовать, обратившись к функции RectangleQ. Её описание содержится в файле wingdi.h:
WINGDIAPI BOOL WINAPI Rectangle(HDC, int, int, int, int);
Аргумент первый понятен без объяснений - хэндл контекста устройства. Остальные аргументы - координаты верхнего левого и нижнего
62
правого углов прямоугольника. TRUE возвращается при нормальном завершении операции. Прямоугольник автоматически заполняется цветом и способом, определяемым текущей кистью.
Рисование эллипса
Для рисования эллипса необходимо вызвать функцию EllipseQ, которая в wingdi.h описывается следующим образом:
WFNGDIAPI BOOL WINAPI Ellipse(HDC, int, int, int, int);
Первый аргумент - это, как всегда, контекст устройства. Для того чтобы понять, как определяется эллипс, предлагаю читателю обратиться к рис. 5.
Как видно из рисунка, эллипс ограничен прямоугольником. Именно через координаты этого прямоугольника и определяется прорисовываемый эллипс. Второй и третий аргументы - координаты левого верхнего угла прямоугольника (на рисунке обозначены как UpX, UpY), четвертый и пятый аргументы - координаты нижнего правого угла (на рисунке обозначены как LowX, LowY).
Окружность является частным случаем эллипса. И в данном случае, если мы определим прямоугольник, у которого ширина равна высоте, т. е. квадрат, вместо эллипса получим окружность.
Как эллипс, так и окружность после прорисовки заполняются цветом и атрибутами текущей кисти.
UpX, UpY
EndX, EndY
StartX, StartY
~~LowX,LowY
Рис. 5. Определение аргумента функции FJlipscQ
63
Узнав, как рисуется эллипс, мы можем узнать, как рисуется прямоугольник с закругленными углами.
Рисование прямоугольника с закругленными краями
Прямоугольник с закругленными краями рисуется с помощью функции RoundRectQ. Из файла wingdi.h добываем ее описание WINGDIAPI BOOL WINAPI RoundRectfHDC, int, int, int, int. int. int);
Первые пять аргументов полностью идентичны аргументам функции Rect(). Последние два аргумента содержат ширину и высоту эллипса, определяющего дуги. После прорисовки прямоугольник закрашивается текущей кистью. В случае успешного завершения функция возвращает TRUE.
Рисование дуги и сектора эллипса
Возьмем из файла wingdi.h описание функции Агс(), которая используется для рисования дуги:
WINGDIAPI BOOL WINAPI Arc(HDC, int, int, int, int, inl, int. int, int);
Первые пять аргументов полностью аналогичны аргументам функции EllipseQ. Непосредственно дуга определяется ещё двумя точками. Первая - начало дуги - находится на пересечении эллипса, частью которого является дуга, и прямой, проходящей через центр прямоугольника и точку начала дуги. На рис. 5 начало дуги обозначено StartX, StartY. Вторая - конец дуги - определяется аналогично. Конец дуги обозначен EndX, EndY. Таким образом, для прорисовки дуги необходимо сначала определить точки StartX, StartY и EndX, EndY, после чего прорисовывать дугу. Дуга прорисовывается против часовой стрелки.
У функции Pie(), которая применяется для рисования сектора эллипса, набор аргументов и их назначение абсолютно идентичны функции Агс().
Несколько слов о заполнении объектов
Как читатель уже знает, заполнение замкнутых графических объектов происходит с помощью текущей кисти. Программист может использовать предопределенную кисть, а может создать свою собственную, после чего сделать ее текущей с помощью функции SelectObject().
Простейшим видом кисти является так называемая сплошная кисть, которая создается с помощью функции CreateSolidBrush():
WINGDIAPI HBRUSH WINAPI CreatcSolidBrush(COI.ORRP.F):
64
Единственный аргумент этой функции - цвет кисти (может, лучше сказать не кисти, а краски?).
Штриховая кисть создается с помощью функции CreateHatchBrush():
WINUDIAPI HBRUSH WINAPI СгсаюНаюШшЩли. COLORREF);
Первый аргумент этой функции - стиль штриховки. Возможные стили приведены в табл. 12.
Второй аргумент указывает цвет штриховки.
И наконец, с помощью функции CreatePattemBrush() мы можем создать кисть, которая при заполнении будет использовать bitmap. В wingdi.h она описана следующим образом:
WtNGDlAPf HHRUSH WINAPI CrcalcPattcrnBrush(HHITMAP);
Уже по типу аргумента видно, что единственным аргументом этой функции является хэндл bitmap'a.
Эти три функции при успешном завершении возвращают хэндл созданной кисти. В том случае, если произошла какая-то ошибка, возвращаемое значение равно NULL.
Давайте закрепим те знания, которые мы получили, рассмотрев небольшую демонстрационную программу.
К большому моему сожалению, до изучения меню я не могу написать программу, в которой действия пользователя определялись бы его выбором. Мне сейчас придется написать программу, которая только демонстрирует вынод на экран различных графических примитивов.
Т а б л и ц а 12. Стили штриховки
Спин, штриховки |
Значение |
Описание |
Эффект |
HS HORIZONTAL HS VERTICAL HS_FDIAGONAL |
0 1 2 |
liill \v\\\ |
Горизонтальная штриховка Вертикальная штриховка Наклонная слева направо |
HSJBDIAGONAL |
3 |
Hill |
штриховка Наклонная справа налево |
HS_CROSS HS DIAGCROSS |
4 5 |
+ —— }-+ xxxxx |
штриховка Штриховка крестиком Штриховка косым крестиком |
65
ДЕМОНСТРАЦИОННАЯ ПРОГРАММА
Ниже приведен текст программы, которая использует основные функции для вывода на экран 10 000 пикселей, поверх них несколько линий разных стилей, после которых, в свою очередь, прорисовывает прямоугольники и эллипсы:
^include <windows.h>
LRESULT CALLBACK GraphDemoWndProc ( HWND, UINT, UINT, LONG );
hit WINAPI WinMain ( HINSTANCE hlnstancc, HINSTANCE hPrevInstance, LPSTR IpszCmdParam, int nCmdShow )
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msg;
char szClassName[] = "GraphDcmo";
/* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style - CSJHREDRAW | CS_VREDRAW;
WndClass.IpfnWndProc = GraphDemoWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hlnstancc = hlnstance ;
WndClass.hlcon = Loadlcon (NULL.IDIJVPPLTCATION);
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
WndClass.hbrBackground - (HBRUSH) GetStockObject (WHITE_BRUSH);
WndClass.lpszMenuName = NULL;
WndClass.IpszClassName — szClassName;
if ( !RegisterClass(&WndClass))
I (
MessageBox(NULL."Cannot register class","Error",MB_OK); return 0;
hWnd = CreateWindow(szClassName, "Graph Demo",
WS_OVERLAPPEDWINDOW, CW USEDEFAULT. CWJJSEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hlnstance.NULL);
if(!hWnd) >
MessageBoxfNULL,"Cannot create window","Error",MB_OK); return 0;
66
/* Show our window */
ShowWmdow(hWnd,nCmdSho\v);
UpdateWindovv(hWnd);
/* Beginning of messages cycle */
whilc(GetMcssage(&Msg, NULL, 0, 0)) i
TranslateMessage(&Msg); DispatchMessage(&Msg); i return Msg.vvParam;
LRESULT CALLBACK GraphDemoWndProc (HWND hWnd, UtNT Message,
UINT wParam, LONG IParam )
HDC hDC, hCompatibleDC;
PAINTSTRUCT PaintStruct;
RFXT Reel;
HBITMAP hCompatibleBitmap, hOldBitmap;
HPENhOldPen;
static HPENPens[5];
HBRUSH hOklBnish;
static HBRUSH Brushcs[6];
int i;
swilch(Message)
case WM_PAINT: randomizcf); for(i = 0; i <=• 4; j-r~) {
Pcnsfil = (CreatePen(i, 1, RGB(random(255),random(255), random(255)))); Bnishesfil - (CreateHa(cliBrush(i, RCiB(random(255), random(255),
random(255))));
GetClientRect(h\Vnd. &Recl);
hDC - BeginPaint(hWnd, &PamtSiruct);
hCompatiblcDC - CreatcCompatibleDC(hDC);
GctClientRect(liWnd, &Rect);
hCompatibleBilmap = CreateCompaliblc,Bitmap(liDC, Rcct.right, Reel.bottom);
hOldBitmap - SelectObject(hCompatibleDC, hCompatibleBitmap);
PatBlt(hCompatibleDC, 0. 0, Red.right, Reel.bottom, PATCOPY); // Drawing of pixels
for(i = 0; i <- 9999; i---) SetPixel(hCompatibleDC,random(Rect.right), random(Rect.bottom),
RGB(random(255), random(255), random(255))); // Drawing of lines
67
for(i - 0; i <=- 9; i+-<-)
hOldPen = SelectObject(hCompatibleDC, Pcns[random(4)]); MoveToEx(hCompatibleDC.random(Rect.right), random(Rcct.bottom),
NULL);
LineTo(hCompatibleDC. nmdom(Rcct.right), random(Rect.bottom)); ScleclObjcct(hCompatibleDC, hOldPen); } // Drawing of rectangles
for(i - 0; i <=- 5; i++) i
hOldBrush - SelcctObject(hConipatibleDC, Bmshes[random(4)l); Rcctanglc(hCompaliblcDC, random(Rect.right).
random( Reel, bottom), random(Rcct.right), random(Rcet.bottom)); Ellipse(hCompatibicDC,random(Rcct.right),
random(Rect.bottom). random(Rect.right), random(Rect. bottom)); SelectObject(hCompatiblcDC, hOldBrush);
I BitBit(hDC, PaintStruct.rcPaint.left, PamtStruct.rcPaint.lop,
PaintStruct.rcPaint.right,
PaintStruct.rcPaint. bottom,
hCompatibleDC,
PaintStruct.rcPaint. left,
PaintStruct.rcPaint.top,
SRCCOPY); for(i = 0; i<=4; i++)
!
DeleteObject(Pens[i]); DeleteObjcct(Brushcs[il);
} SelectObject(hCompatibleDC, hOldBitmap);
DeleteObjcct(hCompatibleBitmap); DelelcDC(hCompatibleDC); EndPaint(h\Vnd, &PaintStruct); return 0;
case WM_DESTROY: PoslQuitMcssagc(O); return 0;
} return DefWindo\vProc(hWnd,Messagc,wParam, IParam);
На рис. 6 показан вид окна, создаваемого программой. Следует учесть, что положение линий, прямоугольников и эллипсов - случайное. При перерисовке их положение, размер и стиль штриховки изменяется, по-
68
этому при повторном запуске программы в окне может быть другое изображение.
Думаю, что после всего того, что мы обсудили в этом разделе, при разборе программы не встретится трудностей. Предоставляю вам возможность разобрать эту программу самостоятельно. У читателя может возникнуть вопрос, для чего все эти сложности с созданием контекста в памяти, копированием его на действительный контекст и прочее. Цель единственная - показать технику работы с виртуальным окном. Весь вывод осуществляется в виртуальное окно (контекст в памяти), после чего одна из «могучих Bit», BitBltQ выполняет копирование содержимого виртуального окна на действительное окно. Как я уже говорил, обычно вывод в действительное окно (т. е. копирование контекста в памяти на действительный контекст) происходит при обработке WM_PAINT.
В Giaph Demo
Рис. 6. Прорисовка геометрических объектов
69
ВЗАИМОДЕЙСТВИЕ ПРОГРАММЫ С ПОЛЬЗОВАТЕЛЕМ
НЕМНОГО О РЕСУРСАХ (ПРЕДИСЛОВИЕ К РАЗГОВОРУ)
ЧТО ТАКОЕ РЕСУРСЫ?
Выше упоминалось, что составной частью проекта, работа которого планируется в Windows, является файл определения ресурсов. Возникает вопрос: что же такое ресурсы, когда и в каких целях они используются?
У Windows, как уже говорилось, есть некоторые предопределенные данные (вспомним предопределенные курсоры, иконки и кисти). Точно так же, почти в каждой программе для Windows есть некоторые данные, которые определяются еще до начала работы программы, особым образом добавляются в выполняемый файл и используются при работе программы. Яркими примерами таких данных являются иконки и курсоры мыши. Кроме них, к числу ресурсов относятся: используемые в программе изображения; строки символов;
меню;
ускорители клавиатуры;
диалоговые окна;
шрифты;
ресурсы, определяемые пользователем.
Следует отметить, что выполняемым файлом может быть файл программы .ехе, файл динамической библиотеки .dll и другие бинарные файлы. Для удобства буду их называть bin-файлами.
Помимо того, что ресурсы определяются до начала работы программы и добавляются в bin-файл, у них есть еще одна характерная черта. При загрузке bin-файла в память, РЕСУРСЫ В ПАМЯТЬ НЕ ЗАГРУЖАЮТСЯ. Только в случае, если тот или иной ресурс требуется для работы программы, программа сама загружает ресурс в память.
Возможность использования того или иного атрибута в качестве ресурса не означает, что программист не может создавать эти атрибуты в программе. Яркий пример тому можно найти в работе старого доброго Program Manager'a. При перетаскивании иконки с места на место курсор меняет свою форму и принимает форму, подобную перетаскиваемой иконке. Естественно, что в этом случае курсоры определяются программой. Помимо .этого, вспомним drag-and-drop в Explorer'e и изменение формы курсора при этом.
70
Еще одним примером являются динамические меню, т. е. меню, которые изменяют свои вид и предоставляемые возможности в зависимости от обстоятельств. Пример динамического меню будет приведен при изучении меню.
РЕСУРСЫ СТАНДАРТНЫЕ И НЕСТАНДАРТНЫЕ
Все ресурсы, заранее определенные в Win32 API, называются стандартными. Для работы с ними существуют специальные функции. Но именно эта стандартность и ограничивает возможности программиста. Стандарт, он и есть стандарт.
Для того чтобы можно было преодолеть эти ограничения, был создан особый тип ресурсов - определяемые пользователем ресурсы. Используя именно этот тип, мы можем предоставить в распоряжение программы практически любые данные. Но, как известно, бесплатным бывает только сыр в мышеловке. В данном случае платой за универсальность является усложнение программы, так как забота о манипулировании данными из ресурсов лежит уже не на системе, а на программе, использующей эти ресурсы. Программа может только получить указатель на данные ресурсов, загруженные в память средствами Windows. Дальнейшая работа с ними ложится ИСКЛЮЧИТЕЛЬНО на плечи программы!
ПОДКЛЮЧЕНИЕ РЕСУРСОВ К ИСПОЛНЯЕМОМУ ФАЙЛУ
Ресурсы создаются отдельно от файлов программы и добавляются в bin-файл при линковании программы. Подавляющее большинство ресурсов содержится в файлах ресурсов, имеющих расширение .RC. Имя файла ресурсов обычно совпадает с именем bin-файла программы. Так, если имя программы MYPROG.EXE, то имя файла ресурсов - MYPROG.RC.
Некоторые типы ресурсов (меню, например) можно описать на специальном языке и воспользоваться при этом обычным текстовым редактором, поддерживающим текст в формате ASCII. Другие ресурсы (иконки, курсоры, изображения) тоже описываются в текстовом виде, но частью их описания является последовательность шестнадцатиричных цифр, описывающих изображения. Можно, конечно, попробовать написать эту последовательность и в текстовом редакторе, но, наверное, в этом случае сложность создания ресурса приблизится к сложности написания программы, а возможно, и превысит ее. Обычно для создания ресурсов пользуются специальными средствами - редакторами ресурсов. Они позволяют создавать ресурсы, визуально контролировать правильность их создания, после чего сохранять их в файлах ресурсов.
71
Я часто использую «смешанный» способ редактирования ресурсов. Например, при визуальном редактировании диалоговых окон достаточно трудно точно установить элементы диалогового окна именно так, как хочется. Устанавливаю все элементы ПРИБЛИЗИТЕЛЬНО на те места, где они должны находиться, после чего сохраняю ресурсы в виде файла с расширением RC. Затем редактирую RC-файл как обычный текстовый файл, точно указывая при этом все размеры и позиции.
При создании RC-файлов программист может столкнуться с одной тонкостью. Некоторые ресурсы, такие, как иконки, курсоры, диалоговые окна, изображения (bitmap) могут быть сохранены в отдельных файлах с расширениями .ico, .cur, .dig, .bmp соответственно. В этом случае в RC-файлах делаются ссылки на упомянутые файлы.
Файл ресурсов создан - теперь его нужно откомпилировать. Компилируется он специальным компилятором ресурсов. Обычно имя компилятора ресурсов заканчивается на RC.EXE. В частности, в Borland 5.0 он называется BRC.EXE.
После компиляции файла ресурсов компилятором ресурсов создается новый файл, имеющий расширение .RES. Именно этот RES-файл используется линкером для добавления ресурсов в bin-файл. Следует отметить, что при необходимости RES-файлы могут создаваться и редакторами ресурсов. В каком формате создавать ресурсы и как присоединять их к исполняемому файлу, зависит от потребностей и привычек создающего ресурсы программиста.
Итак, в очередной раз постараемся подвести итог сказанному. Ресурсы создаются и включаются в bin-файл посредством выполнения следующих шагов (некоторые шаги могут быть опущены в зависимости от обстоятельств) (табл. 13).
Те программисты, которые при работе пользуются интегрированной средой, получают в некотором смысле преимущество. Во-первых, все эти шаги можно осуществить без выхода из интегрированной среды. Во-вторых, компиляция RC-файла и линкование полученного RES-файла можно выполнить автоматически.
После выполнения этих шагов в нашем bin-файле содержатся все необходимые данные нам данные, которые можно использовать (добавлять меню к окну, загружать курсоры, иконки, работать с диалоговыми окнами). Но все это - только описание порядка работы. В следующих разделах мы попробуем создать некоторые ресурсы. Не буду описывать работу с редакторами ресурсов. Во-первых, работа с ними достаточно проста, а во-вторых, описана в технических руководствах. Постараюсь описать синтаксис языка, который используется для создания сценариев (скриптов) ресурсов, после чего продемонстрировать, как работу с ресурсами можно
72
заменить и/ или дополнить вызовами функций Win32 API. Рассмотрение мы построим следующим образом. Для каждого типа ресурсов сначала рассмотрим способ создания этого ресурса, подключения его к окну, а затем рассмотрим функции Win32, которые предназначены для работы с ресурсами.
В этом разделе мы будем рассматривать только те ресурсы, которые обеспечивают непосредственный диалог пользователя с программой. К их числу, прежде всего, относятся меню. Во-первых, именно с меню начинается знакомство с программой. Во-вторых, оценить функциональные возможности программы, можно просто взглянув на меню. То есть именно меню в большинстве случаев является визитной карточкой программы.
Кроме меню, наиболее часто для взаимодействия с пользователем используются диалоговые окна. Они, как правило, применяются для ввода данных и информирования программы о принятых решениях. При их рассмотрении, нам придется изучить элементы управления (controls) и общие элементы управления (common controls), научиться взаимодействовать с ними, устанавливать и считывать их состояние. Обращаю внимание читателя на следующее. Понимание работы меню и диалоговых окон очень важно. Зная принципы их работы, становится возможным написание программ для Windows, несущих какую-то полезную нагрузку.
Таблица 13. Последовательность сомания файла ресурсов
Действие
Используемое средство
Создание RC-файла (при необходимости включающего ссылки па файлы с расширением .ico, .cur, .bmp, .dig, .innu и т. д.) Редактирование RC-файла в текстовом виде Компиляция RC-файла - получение RES-файла
Добавление ресурсов, содержащихся в RIZS-файле, в bin-файл
Редактор ресурсов (при необходимости может быть использован текстовый редактор и графические редакторы) Текстовый редактор Компилятор ресурсов
Линкер
73
МЕНЮ И АКСЕЛЕРАТОРЫ
ПОДКЛЮЧЕНИЕ МЕНЮ К ОКНУ
В предыдущей главе мы написали и исследовали программу, создающую окно. В этой главе мы должны научиться подключать меню к нашему окну и, естественно, научиться реагировать на сообщения, исходящие от меню. Мы узнаем о том, какие типы меню и элементов меню бывают, изучим различные способы построения меню.
Любой, кто хоть немного работал в Windows, знает, что меню распо лагаются сразу под заголовком окна и позволяют пользователю осуществить выбор возможностей, предоставляемых программой. Помимо этого, существуют также всплывающие меню, которые могут появляться в любой точке экрана. Обычно их содержание зависит от того, на каком окне щелкнули клавишей мыши. Их изучение начнем с характеристик меню.
Давайте представим себе главное меню программы как древовидную структуру. Основная часть - корень нашего дерева - это непосредственно главное меню. Само по себе оно представляет только структуру в памяти, не отображается на экране, не содержит ни одного элемента, но хранит указатель на список структур, описывающих подключаемые к нему элементы и всплывающие (popup) меню. Если читатель вспомнит свой опыт работы с Windows, он согласитсяс тем, что в окне в качестве основного меню отображается именно набор рорир-меню. В свою очередь, рорир-меню должно указать на список структур очередного, более низкого уровня и т. д. Конечные элементы меню никаких указателей на списки не имеют, но хранят некий идентификатор действия (назовем его идентификатором элемента меню), которое должна произвести программа при выборе данного элемента меню. Используя эти структуры, мы можем построить меню практически любой глубины и сложности.
Эта многоуровневая древовидная структура описывается в файле ресурсов. Описание меню имеет вид:
MenuName MENU [параметры] // это - главное меню
Описание всех рорир-меню и элементов меню второго уровня
В данном случае MenuName - это имя создаваемого нами меню. Слово MENU обозначает начало определения меню. Параметры мы пока, до изучения работы с памятью, расматривать не будем.
74
В Win32 API для описания меню существуют два ключевых слова. Первое - POPUP - специфицирует всплывающее меню. Второе -MENUITEM - описывает обычный элемент меню.
Всплывающие меню описывается следующим образом:
POPUP «Имя» [,параметры] // <- описание рорир-меню
I i
Описание всех рорир-меню и элементов очередного уровня
У конечного элемента меню в его описании есть еще одна характеристика - тот самый идентификатор действия:
MENUITEM «Имя», MenuID [.параметры]
В обоих случаях «Имя» - это тот текст, который будет выведен на экран при отображении меню (обратите внимание - при описании главного меню выводимого на экран текста нет!). В том случае, когда вместо имени окна записано слово SEPARATOR (без кавычек!), на ме сте элемента меню появляется горизонтальная линия. Обычно эти горизонтальные линии (сепараторы или разделители) используются для разделения элементов подменю на логические группы (логика определяется только программистом и никаких особенностей не содержит).
Если в имени меню встречается символ «&», то следующий за ампер-сандом символ на экране будет подчеркнут одинарной чертой. Этот элемент меню можно будет вызывать с клавиатуры посредством одновременного нажатия клавиши Alt и подчеркнутого символа.
Таблица 14. Параметры, описывающие элемент меню в файле ресурсов
CHECKED
ENABLED DISABLED GRAYED
MENURREAK
MENUBARBREAK
Рядом с именем элемента может отображаться небольшой значок, говорящий о том, что соответствующий флаг установлен
Элемент меню доступен
Элемент меню недоступен, но отображается как обычный Элемент меню недоступен и отображается серым цветом Горизонтальные меню размещают следующие элементы в новой строке, а вертикальные - в новом столбце То же, что и предыдущее, но в случае вертикального меню столбцы разделяются вертикальной линией
75
MenuID - идентификатор действия. Он может быть передан функции окна, содержащего меню. Значение идентификатора определяется пользователем. Функция окна в зависимости от полученного MenuID производит определенные действия.
Параметры же описывают способ появления элемента на экране. Возможные значения параметров приведены в табл. 14.
Попробуем создать описание небольшого меню. Горизонтальное меню (menubar) позволит выбирать подменю «File», «Examples» и конечный элемент «Help». Подменю «File» будет содержать элементы «Open » и «Exit», разделенные горизонтальной линией, а подменю «Examples» -несколько конечных элементов.
Ниже приведен текст скрипта для этого меню:
MyMenu MENU
{
POPUP "&File"
{
MENUITEM "&Open", 101
MENUITEM SEPARATOR MENUITEM "E&xit", 102 }
POPUP "&Examples"
{ POPUP "Example 1"
{
MENUITEM "1&Г, 103 MENUITEM "1&2", 104
} POPUP "Example &2"
{
MENUITEM "2&1", 105 MENUITEM "2&2", 106
MENUITEM "&Help", 111 }
Следует обратить внимание на то, что идентификаторы действия есть только у MENUITEM'ов. Popup-меню идентификаторов не содержат.
Теперь необходимо сделать так, чтобы меню стало доступным программе. В интегрированной среде это делается следующим образом:
к проекту добавляется файл ресурсов (желательно, чтобы имя файла ресурсов совпадало с именем программы);
76
в текст программы вносится изменение - при определении класса окна полю IpszMenuNaine структуры типа WNDCLASS присваивается указатель на строку, содержащую имя меню. В данном случае WndClass. IpszMenuNaine = «MyMenu»;
производится перекомпиляция проекта.
Если читатель работает не в интегрированной среде, то ему необходимо до момента линкования откомпилировать ресурсы, а затем с помощью линкера присоединить их к исполняемому файлу. Попробуйте произвести эти действия с тем проектом, в котором вы создавали нашу первую программу. Если вы все сделали правильно, то у окна должно появиться меню, с которым можно немного поиграть. Попробуйте поэкспериментировать с описанием меню в файле ресурсов и видоизменить и непосредственно меню, и внешний вид popup-меню и элементов меню.
Таким образом, с помощью добавления к программе меню мы определили функциональность нашей программы. Конечно, тот пример, который здесь приведен, предназначен только для того, чтобы продемонстрировать возможности по управлению меню с помощью ресурсов. Более того, из сказанного можно сделать вывод, что возможности Win32 по управлению меню с помощью ресурсов достаточно скудны. Да, это так. Существует еще масса функций, позволяющих манипулировать меню. Мы приступим к их рассмотрению после того, как научимся реагировать на манипуляции, производимые с меню.
Реакция окна на сообщения от меню
Как уже было сказано выше, элементы в меню могут быть обычными, запрещенными и «серыми». Для пользователя обычные и запрещенные элементы выглядят одинаково, а текст в «серых» элементах напечатан серым шрифтом. Но только обычные элементы позволяют пользователю произвести выбор. Запрещенные и «серые» элементы меню могут быть только подсвечены, но с их помощью произвести выбор нельзя. Кроме этого, существуют отмечаемые элементы меню. В них слева от текста может находиться какой-либо значок. Если значок есть, то считают, что флажок, определяемый этим элементом, установлен. Если флажок сброшен, то значок отсутствует.
С другой стороны, в элементе меню может находиться как текст, так и картинка (bitmap). С точки зрения пользователя никакой разницы в применении меню с текстом или с картинкой нет.
Перед тем, как начать серьезный разговор о меню, напомню, что основному меню и всем всплывающим меню Windows присваивает хэндлы (другими словами, все они являются структурами в памяти). Этот факт в дальнейшем будет играть важную роль.
Давайте попробуем поговорить о меню с точки зрения сообщений. При смене подсвеченного элемента меню (если, к примеру, пользователь «пробегает» по элементам меню с помощью клавиш со стрелками вверх и вниз) в оконную процедуру посылается сообщение WM_MENUSELECT. Это сообщение посылают все элементы меню. Когда же пользователь производит выбор (нажимает клавишу «Enter», к примеру), сообщение WM_COMMAND оконной процедуре посылают только обычные элементы меню. Запрещенные и «серые» элементы меню в этом случае никаких сообщений не посылают. В элементах wParam и lParam посылаемых сообщений хранится информация, достаточная для того, чтобы программа смогла определить, какие действия ей необходимо выполнить случае выбора пользователем того или иного элемента меню.
Вспомним, что помимо обычного меню у окна в большинстве случаев есть еще и системное меню. Сказанное относится и к системному меню. Отличие между обычным меню и системным состоит в том, что оконной процедуре посылаются сообщения WM SYSMENUSELECT и WM_SYSCOMMAND. Кроме этого, сообщения WM_SYSCOMMAND оконная процедура получает и в случае нажатия кнопок минимизации, максимизации и закрытия окна, которые находятся не в системном меню, а в правом углу заголовка окна.
Рассмотрим параметры сообщения WMSYSMENUSELECT более подробно. В младшем слове wParam оконная процедура получает сведения о том, какой элемент стал подсвеченным. Если учесть, что макросы LOWORD() и HIWORDO выделяют соответственно младшее и старшее слово 32-битного аргумента, и назвать источник сообщения ultem, то можно записать:
ultem = (UINT) LOWORD(wParam);
В зависимости от обстоятельств смысл ultem различается:
если подсвеченный элемент является конечным и не влечет за собой вызов popup-меню, то ultem содержит идентификатор элемента меню;
если подсвеченный элемент при выборе влечет за собой вызов popup-меню, то ultem содержит номер (индекс) этого элемента в том меню, в котором оно находится;
В старшем слове wParam содержатся характеристики подсвеченного элемента. Аналогично предыдущему,
fuFlags - (UINT) HIWORD(wParam);
Возможные значения fuFlags приведены в табл. 15.
78
Таблица 15. Характеристики подсвеченного элемента меню
Флаг |
Знамение |
Описание |
MFJ3ITMAP |
Ox00000004L |
Вместо строки в качестве элемента меню |
|
|
применяется bitmap |
MF CHECKED |
OxOOOOOOOSL |
Элемент отмечаемый (со «значком») |
MF DISABLED |
Ox00000002L |
Элемент запрещен |
MF_GRAYED |
0x0000000 IE |
Элемент запрещен и отображается серым |
|
|
цветом |
MF HILITE |
OxOOOOOOSOL |
Элемент подсвечен |
MF MOUSESELECT |
OxOOOOSOOOL |
Элемент выбран мышью |
MF_OWNERDRAW |
0x00000 100L |
За прорисовку элемента отвечает не система, |
|
|
а программа |
MF_POPUP |
0x000000 10E |
Элемент вызывает появление рорчр-меню |
|
|
более низкого уровня |
MF SYSMENU |
Ox00002000L |
Элемент из системного меню |
IParam содержит в себе хэндл того меню, которому принадлежит подсвеченный элемент. Обозначив хэндл меню как hMenu, получим:
hMenu = (HMENU) IParam;
Теперь пришла очередь рассмотрения сообщения WM_COMMAND. Как и в случае с \VM_SELECTMENU, младшее слово wParam содержит сведения об источнике сообщения. Так как сообщение WM_COMMAND посылается только конечными элементами меню, то в младшем слове wParam
содержится идентификатор выбранного элемента меню. На языке С
wID = LOWORD(wParam);
Старшее слово wParam указывает, от какого управляющего элемента пришло сообщение. Если сообщение пришло от меню, то это слово равно нулю, т. е.
wNotifyCodc = HIWORD(wParam) = 0;
IParam в случае сообщения от меню ВСЕГДА равно NULL!
Теперь мы знаем вполне достаточно, чтобы написать программу, реагирующую на манипуляции с меню. Но я хотел бы, чтобы читатель набрался терпения и изучил еще одну тему. Ранее мы пришли к выводу о том, что возможности языка управления ресурсами достаточно скудны. Тем не менее, в Win32 существует множество функций, позволяющих манипулировать меню. Остановимся на этих функциях и выясним, каким образом можно реализовать меню без обращения к ресурсам.
Меню без использования ресурсов
Перед тем, как начать рассмотрение функций, предназначенных для работы с меню, я хотел бы сказать, что являюсь сторонником комбинированного использования меню, определенного в виде ресурса, и функций Win32. Решение об использовании того или иного способа должен принимать программист в соответствии с задачами, которые должна решать разрабатываемая программа. Если в примере я придерживаюсь какого-то одного способа, то читатель должен понимать, что пример - это всего-навсего демонстрация возможностей Win32, а не призыв всегда и везде делать именно так, как сделано в предлагаемом примере. Еще раз повторю - программист свободен в выборе применяемых технологий.
Как было сказано выше, меню имеет строгую древовидную структуру, которая начинается с меню первого уровня (оно обычно называется главным меню программы или тепиЬаг'ом и располагается сразу под заголовком окна). К этому меню первого уровня могут быть присоединены как конечные элементы меню, так и элементы, выбор которых приводит к появлению так называемых всплывающих (popup) меню, к которым, в свою очередь, присоединяются элементы очередного уровня и т. д. Перед началом создания меню вся его структура должна быть тщательно продумана. Неплохо, если бы программист имел перед глазами графическое представление этого меню. Если все предварительные вопросы решены, то мы готовы приступить к созданию меню.
Итак, для создания меню необходимо выполнить следующие действия:
выбрать подменю самого низкого уровня, которые содержат только конечные элементы меню, и создать их с помощью функций CreateMenuQ или CreatePopupMenuQ в зависимости от потребностей. Эти функции возвращают хэндл созданного меню. Меню создается пустым;
посредством функции AppendMenuQ добавляем в них требуемые элементы;
создаем меню следующего, более высокого уровня, и добавляем в них требуемые элементы и меню, созданные нами на предыдущем шаге;
повторяем эти шаги до тех пор, пока создание всех подменю не будет закончено;
создаем главное меню программы посредством использования функции CreateMenuQ;
присоединяем созданные подменю самого высокого уровня к главному меню программы с помощью функции AppendMenuQ;
присоединяем меню к окну посредством использования функции SetMenuQ;
прорисовываем меню с помощью функции DrawMenuBarQ. Если в ходе программы сложилась такая ситуация, что меню оказалось не присоединенным к окну, перед выходом из программы обязательно уничтожаем его, вызывая функцию DestroyMenuQ (присоединенное к окну меню уничтожается автоматически при уничтожении окна).
Для того чтобы проиллюстрировать сказанное, давайте разберем небольшую программу. Я постарался написать эту программу так, чтобы в ней имелись основные типы элементов меню, и была бы возможность обработать выдаваемые меню сообщения. Чтобы придать программе функциональность, информация о получении сообщения будет выдаваться в строку состояния - небольшую область внизу окна (возможности строки состояния мы будем изучать позже). При запуске у окна возникает меню. Внизу экрана появляется строка состояния, в которой будет отображаться информация о выбранном элементе меню. Основное меню окна состоит из двух всплывающих меню, «File» и «Help», и элемента меню, в котором вместо строки отображается bitmap. В первом всплывающем меню находятся два элемента, «Enable exit» и «Exit», во втором -один элемент, «About», который остается запрещенным в течение всего периода существования окна. Кроме этого, элемент «Exit» при запуске объявляется «серым», т. е. из программы можно выйти только через системное меню. Однако в случае выбора элемента «Enable exit» «Exit» становится обычным, а вместо «Enable exit» возникает «Disable exit». При выборе элемента с bitmap'ом отображается окно сообщений с текстом о том, что выбран именно этот элемент. На этом возможности программы исчерпываются.
#include <windows.h>
#include <commctrl.h>
const IDM_Enable_Disable = 0;
const IDM_Exit = i;
const IDM_About = 2;
const lDP_File = 3;
const IDP_Help = 4;
char* pMcssages[] {
"Enable or disable exit", "Exit from the program", "About this program", "File operations", "Help operations", "Menu example", "System menu"
LRESULT CALLBACK MenuDemoWndProc ( HWND, UINT, UINT, LONG );
HWND hStatusWindow;
UINT wld;
HMENU hMenu,hFileMenu,hHe!pMenu;
HINSTANCE hlnst;
int APIENTRY WinMain ( HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR IpszCmdParam, int nCmdShow )
HWND hWnd ; WNDCLASS WndClass ; MSG Msg;
hlnst = hinstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style = CS_HREDRAW | CS_VREDRAW;
WndClass.lpfiiWndProc = (WNDPROC) MenuDemoWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass. hinstance = hinstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE J3RUSH);
WndClass. IpszMenuName = NULL;
WndClass. IpszClassName = "MenuExample";
if ( !RegisterCiass(&WndClass) )
{
MessageBox(NULL,"Cannot register class","Error",MB_OK); return 0;
hWnd = CreateWindow("MenuExample", "Program No 2",
WS_O VERLAPPEDWIN DOW,
CWJJSEDEFAULT,
CWJJSEDEFAULT,
CWJJSEDEFAULT,
CWJJSEDEFAULT,
NULL, NULL,
hlnstance.NULL); if(!hWnd)
{ MessageBox(NULL, "Cannot create window", "Error", MB_OK);
return 0;
InitCommonConlrols();
hStatusWindow = CreateStatusWindow(WS_CHILD | WSJVISIBLE,
if(!hStatusWindow)
"Menu sample", hWnd,w!d);
MessageBox(NULL, "Cannot create status window", "Error", MB_OK); return 0;
/* Try to create menu */
AppendMenu( (hFileMenu=CreatePopupMenu()), MF_ENABLED, MF_STRING,
IDM_Enable_Disable, "&Enable exit");
AppendMenu(hFileMenu, MF_GRAYED MF_STRING, IDM_Exit,"E&xil"); AppendMenu((hHelpMenu=CreatePopupMenu()),MF_DISABLEDMF_STRING,
IDM_About, "&About"); hMenu = CreateMenu(); AppendMenufliMenu, MFJZNABLED | MFJ>OPUP, (UINT) hFileMenu,
AppendMcnu(hMenu, MF_ENABLED | MFJPOPUP, (UINT) hHelpMenu,
"&Help");
SetMenufhWnd, hMcnu); /* Show our window */
ShowWindow(hWnd, nCmdShow);
UpdatcWindow(hWnd);
DrawMenuBar(hWnd);
/* Beginning of messages cycle */
while(GelMessage(&Msg, NULL, 0, 0)) { TranslateMessage(&Msg);
DispatchMessage(&Msg); i t
return Msg.wParam;
LRESULT CALLBACK MenuDemoWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam ) {
RECT Reel; static UINT nFlag - MFJENABLED;
char* pContentf]
i i
"Enable exit",
"Disable exit" };
static UINT nlndex = 0; static НВ1ТМЛР hBitmap; int nDimcnsion;
switch(Message) { caseWM CREATE:
nDimension = GetSystemMetrics(SM_CYMENU);
hBitmap = Loadlmage(hlnst, "msdogs.bmp", IMAGE_BITMAP,
nDimension * 2, nDimension, LR_LOADFROMFILE); AppendMenu(GetMenu(hWnd), MF^BITMAP, IDM_Bitmap, hBitmap); break;
case WM_COMMAND: switch (wParam)
{ case IDM_Enable_Disable:
EnableMenuItem(hFileMenu, IDM_Exit, MF_BYCOMMAND | nFIag);
nFlag = ( nFIag = MF_ENABLED ) ? MF_GRAYED : MFJ-NABLED;
nlndex = ( nlndex == 0) ? I : 0;
ModifyMenu(hFileMenu, IDM_Enable_Disable, MF_BYCOMMAND | MF_STRING, IDM_EnabIe_Disable, pContent[ntndex]);
break; case IDM_Exit:
SendMessage(hWnd, WM_CLOSE, NULL, NULL);
break;
} case WM_SIZE:
SendMessage(hStatusWindow, WM_SIZE, wParam, IParam);
GetClientRect(hWnd, &Rect);
return 0;
case WM_MENUSELECT: // Selection is losted
iff ((UINT) HIWORD(wParam) == Oxffff) & ((HMENU) IParam = 0))
{ SendMessage(hStatusWindow ,SB_SETTEXT, (WPARAM) 0,
(LPARAM) PMessages[5]); return 0;
I if ((UINT) HIWORD (wParam) & MF_SYSMENU)
{ SendMessage(hStatusWindow, SB_SETTEXT, (WPARAM) 0,
(LPARAM) pMessages[6]); return 0;
} if ((UINT) HIWORD(wParam) & MF_POPUP)
{ SendMessage(hStatusWindow, SB_SETTEXT, (WPARAM) 0,
(LPARAM) pMessages[3 + LOWORD(wParam)]); return 0;
} SendMessage(hStatusWindow, SB_SETTEXT, (WPARAM) 0, (LPARAM)
pMessages[LOWORD(wParam)]); return 0;
case WM_DESTROY: DeleteObject(hBitmap); PostQuitMessage(O); return 0;
return DefWindowProc(hWnd,Message,wParam, IParam);
Листинг № З. Программа, демонстрирующая возможности по манипулированию меню.
Вид окна, создаваемого программой, показан на рис. 7.
Как и в случае нашей первой программы для Windows, давайте рассмотрим эту программу. Естественно, включаем файлы заголовков Win32. Включение файла «commctrl.h» обусловлено вызовом функций для работы со строкой состояния, заголовки которых находятся в этом файле. Далее идут многочисленные описания и определения, объяснять которые я не буду, они сразу же становятся понятными при разборе программы. В функции WinMain() все ясно до момента вызова функции InitCommonControls(). Но и здесь нет ничего страшного. Эта функция вызывается всегда перед использованием библиотеки общих элементов управления, к которым относится и строка состояния. К меню и нашей задаче эта функция имеет весьма далекое отношение. Интерес вызывает фрагмент, который начинается после строки комментария /* Try to create menu */.
[Menu example
ppic. 7. Окно с меню, содержащим bitmap
84
85
Таблица 16. Битовые флаги, определяющие поведение и вид элемента меню
Флаг
Назначение
MF_BITMAP
MF_CHECKED
MFJDISABLED
MF^ENABLED
MF_GRAYEU
MF_MENUBARBREAK
MF_MENUBREAK
MF_OWNERDRAW MF_POPUP
MF^SEPARATOR
MF_STRING
MF UNCHECKED
Вместо строки символов в качестве элемента меню используется изображение (bitmap) Отмечаемый элемент меню Запрещенный, но не «серый» элемент меню Разрешенный элемент меню Запрещенный «серый» элемент меню То же, что и следующее, но вертикальные столбцы отделяются вертикальной чертой
Очередной элемент меню размещают в новой строке (menubar) или в новом столбце За прорисовку элеме!гга отвечает программа, а не Windows Выбор элемента влечет за собой появление меню более низкого уровня Горизонтальная черта
В качестве элемента меню используется строка символов Неотмечаемый элемент меню
Как уже было сказано, сначала мы создаем меню с помощью вызова функции CreatePopupMenuQ, которая возвращает нам хэндл созданного меню hFileMenu. В это меню мы с помощью функции AppendMenuQ добавляем два элемента, «Enable exit» и «Exit». Функция AppendMenuQ заслуживает того, чтобы поговорить о ней подробнее.
При вызове функции AppendMenuQ она получает четыре аргумента. Аргумент первый - хэндл того меню, в которое добавляется элемент. Ничего сложного или интересного в этом вызове нет. Второй аргумент -комбинация битовых флагов, определяющих внешний вид и поведение добавляемого элемента меню. Перечень и назначение флагов приведены в табл. 16.
Заметим, что некоторые флаги не могут быть установлены одновременно. Например, MF_BITMAP, MF_STRING и MFJPOPUP, MF_ENABLED, MF_DISABLED и MF GRAYED.
Интерпретация третьего параметра зависит от того, установлен ли во втором параметре флаг MF POPUP, т. е. является ли добавляемый элемент меню конечным элементом или влечет за собой вызов очередного меню. В первом случае - конечный элемент - параметр содержит идентификатор этого элемента. Если же добавляется меню, то параметр содержит хэндл добавляемого меню.
И последний параметр тоже интерпретируется в зависимости от установленных флагов. Установлен флаг MF_BITMAP - параметр содержит хэндл bitmap'а. Установлен флаг MF_STRING - параметр содержит указатель на строку символов. Установлен MF_OWNERDRAW - параметр содержит информацию, используемую программой при прорисовке элемента.
Разобравшись с функцией AppendMenuQ, мы можем не останавливаться на последующих ее вызовах и остановиться на вызове функции SetMenuQ.
Что означает «создать меню»? Это, как и в случае создания окна, означает всего лишь создание и последующее заполнение некоторых структур в памяти. После создания меню не принадлежит никакому окну и до поры до времени бесполезно блуждает в глубинах памяти. Для того чтобы меню могло выполнять свои функции, оно должно быть «закреплено» за одним из окон. Функция SetMenuQ и привязывает меню к конкретному окну. Аргументы этой функции очевидны - хэндл закрепляемого меню и хэндл окна, к которому меню прикрепляется. После вызова этой функции указатель на меню включается в структуру окна и может нормально использоваться.
После отображения и прорисовки окна вызывается функция DrawMenuBarQ для прорисовки меню. И у этой функции очевидно наличие одного аргумента - хэндла окна, которому принадлежит прорисовываемое меню.
К этому моменту мы произвели все действия, требующиеся для создания меню. Далее программа запускает цикл обработки сообщений и начинает посылать сообщения функции окна. Перейдем к рассмотрению оконной функции. Перед этим я прошу читателя не пугаться, увидев обращение к функции SendMessageQ. Она только посылает окну (хэндл окна-адресата - первый аргумент) сообщение (второй аргумент) с заданными wParam (третий аргумент) и IParam (четвертый аргумент). В данном случае посылаются сообщения строке состояния для того, чтобы в ней отобразился текст, на который указывает IParam.
При этом оконная функция самостоятельно обрабатывает только четыре сообщения - WM_COMMAND, WMJVlENUSELECT, WM_SIZE и WMJ3ESTROY. WMJ5ESTROY мы уже рассматривали. WM_SIZE в этом примере используется для того, чтобы при изменении размеров окна строка состояния могла бы нормально перерисоваться, т. е. сейчас это сообщение нас не интересует. В настоящий момент интерес представляют только сообщения WM_MENUSELECT и WM COMMAND.
Начнем с рассмотрения WM_MENUSELECT. У этого сообщения есть одна особенность. Если поле fuFlags, т. е. старшее слово wParam, равно Oxffff и при этом поле hMenu (т. е. IParam) равно NULE, то это означает, что меню закрылось (не стало выделенных элементов), так как пользователь нажал Escape или щелкнул мышкой где-нибудь вне меню. В этом случае в строке состояния появляется текст, описывающий назначение программы в целом («Menu example»). При получении сообщения от системного меню в строке состояния возникает «System menu». Во всех остальных случаях текст в строке состояния описывает назначение подсвеченного элемента меню. Непосредственно ход обработки этого сообщения ясен из листинга и особых пояснений не требует.
Но обработка WM_MENUSELECT - это всего лишь прелюдия к настоящей работе, которая происходит тогда, когда пользователь выбирает конечный элемент меню посредством нажатия клавиши «Enter» или щелкнет левой клавишей мышки на элементе. В этом случае оконная процедура получает сообщение WM_COMMAND. Для того чтобы определить, как реагировать на сообщение, мы должны проанализировать младшее слово wParam, которое хранит идентификатор элемента меню, и в зависимости от его значения предпринимать те или иные действия. В данном случае оконная функция может получать \VM_COMMAND только с двумя идентификаторами - IDMJEnable^Disable и IDMJ3xit. При получении последнего мы осуществляем выход из программы. При обработке первого я демонстрирую использование двух функций -EnableMenuItemQ и ModifyMenuQ.
При получении WM_COMMAND, младшее слово wParam которого равно IDM_Enable_Disable, производятся следующие действия:
с помощью функции EnableMenuItemQ запрещается или делается доступным элемент «Exit»;
с помощью функции ModifyMenuQ изменяется текст элемента, выбор которого приводит к состоянию элемента «Exit».
Эти функции достаточно показательны и их разбор поможет читателю еще глубже понять функции, работающие с меню.
Функция EnableMenuItemQ позволяет программисту изменять состояние элемента (разрешенный, запрещенный, «серый») меню по своему усмотрению. При вызове функции, ей передаются три аргумента. Первый аргумент - хэндл того меню, которому принадлежит элемент. В нашем случае меняется состояние элемента, находящегося в меню «File», хэндл которого hFileMenu. Второй аргумент определяет тот элемент, состояние которого изменяется, но каким способом происходит определение, а также в какое состояние переходит элемент, зависит от третьего аргумента, который в очередной раз представляет комбинацию битовых флагов.
Возможные флаги приведены в табл. 17.
88
Таблица 17. Флаги, используемые при вызове функции EnableMenuItem()
Флаг
Значение
MF_BYCOMMAND MF_BYPOSITION
MF_ENABLF.D MF_DISABLED MF GRAYED
Изменяемый элемент меню определяется по его идентификатору
Изменяемый элемент меню определяется по его номеру (индексу) в меню
После вызова функции элемент становится разрешенным После вызова функции элемент становится запрещенным После вызова функции элемент становится «серым»
После изменения состояния элемента «Exit» с разрешенного на серое и наоборот, необходимо изменить текст в элементе, от которого зависит это состояние. Это изменение производится посредством вызова функции ModifyMenu(), которой передаются пять аргументов. Первые два аргумента функционально подобны аргументам EnableMenuItemQ, т. е. первый аргумент - хэндл меню, которому принадлежит изменяемый элемент, а второй аргумент определяет непосредственно изменяемый элемент. Можно было бы сказать, что и третий аргумент функционально подобен, но он представляет собой комбинацию флагов, во-первых, определяющих элемент, подлежащий изменению (MF_BYCOMMAND или MF_BYPOSITION), а во-вторых, определяющих состояние элемента после изменения (перечень этих флагов в точности соответствует приведенному в табл. 16). Четвертый аргумент указывает или идентификатор измененного элемента, или хэндл нового меню (если, конечно, в третьем аргументе установлен флаг MF_POPUP). И наконец, последний аргумент - новое содержание измененного элемента. В зависимости от того, какой флаг установлен в третьем аргументе (MF_BITMAP, MF^STRING или MF^OWNERDRAW), последний аргумент содержит хэндл bitmap'а, указатель на строку или информацию, используемую при прорисовке элемента.
Таким образом, с помощью только функций Win32 мы создали меню и изменили его состояние.
Надеюсь, что при чтении этого раздела и разборе приведенного примера читатель понял технические приемы работы с меню, и теперь может применять полученные знания при разработке собственных программ. Описанными функциями отнюдь не исчерпываются возможности Win32 по управлению меню. Например, в меню можно не добавлять, а вставлять элементы посредством функции InsertMenuQ, функция DeleteMenuQ удаляет элемент из меню, информацию о меню можно получить с помощью функций GetMenuQ, GetMenuStringQ, GetMenuIternCountQ и других.
89
В рамках этой книги нет возможности описать все функции работы с меню. Надеюсь, что читатель, получив начальные знания, проявит любознательность и сам найдет в системе помощи Win32 сведения о функциях, работающих с меню. Тем не менее, в примере, который приводится в разделе, посвященном созданию диалоговых окон, можно будет найти еще две функции, работающие с меню.
Изучение работы с меню на этом не заканчивается. Нераскрытым остался еще один вопрос - подключение акселераторов меню, который будет рассмотрен ниже.
АКСЕЛЕРАТОРЫ
Итак, мы научились создавать и манипулировать элементами меню. Но, к большому разочарованию тех, кто привык работать без мышки, у наших меню есть один серьезный недостаток. Выбор элементов мы можем производить только последовательно, входя в главное меню, подменю, подменю... и так до тех пор, пока не дойдем до нужного элемента. А у многих программ есть возможность обращаться к элементам меню напрямую посредством использования некоторых комбинаций клавиш. Возникает закономерный вопрос - как сделать так, чтобы и в наших программах была такая возможность?
Комбинации клавиш, которые при нажатии автоматически выбирают соответствующий им элемент меню (даже в тех случаях, когда оно не активно и не отображается), называются акселераторами. Это название (в переводе с английского акселератор означает ускоритель) выбрано достаточно удачно, ибо в тех случаях, когда пользователь запомнил их и привык к их использованию, ввод команд осуществляется намного быстрее, чем активизация меню и выбор этих команд.
Акселераторы являются одним из типов ресурсов, т. е. для того, чтобы использовать акселераторы, нам необходимо в файле ресурсов создать таблицу акселераторов. Она имеет следующий формат:
TableName ACCELERATORS
{ Keyl, MenuIDl [,тип] [,параметр]
Keyn, MenuIDn [,тип] [.параметр]
TableName - это определяемое пользователем имя таблицы акселераторов. Key определяет клавишу или комбинацию клавиш, при нажатии
90
которой происходит ввод команды. Тип определяет, является ли клавиша стандартной (это значение применяется по умолчанию) или виртуальной. Параметр может принимать одно из следующих значений: NOINVERT, ALT, CONTROL и SHIFT. Обычно при использовании акселераторных комбинаций меню отображается так, словно мы выбрали команду обычным способом. NOINVERT означает, что при использовании акселератора внешне меню на ввод команды никак не отреагирует, даже если будет активно и отображено. Присутствие ALT указывает, что для получения акселераторной комбинации одновременно с указанной клавишей необходимо нажать клавишу Alt. CONTROL говорит о том, что одновременно с клавишей должна нажиматься клавиша Control, a SHIFT требует одновременного с клавишей нажатия Shift.
В качестве клавиши можно указать либо ее символ в кавычках, либо код ASCII-символа, либо код виртуальной клавиши, определенной в файлах заголовков. При использовании ASCII-кода в качестве типа должно быть указано ASCII, а в случае применения виртуальной клавиши тип должен быть VIRTKEY. Виртуальная клавиша - это системно-независимый код, определенный для основного набора служебных клавиш. Этот набор включает клавиши F1-F12, стрелки и т. д. Коды виртуальных клавиш определены в заголовочных файлах. Все их идентификаторы начинаются с букв VK (Virtual Key). Разница между виртуальной клавишей и ASCII-символом с точки зрения пользователя состоит в том, что виртуальные клавиши не различают прописных и строчных букв, в отличие от ASCII-символов.
При определении акселераторов можно пойти на небольшую хитрость. Представим себе, что в качестве акселератора мы указали заглавную букву и, скажем, ALT. В этом случае нам придется одновременно нажимать три клавиши - букву, клавишу SHIFT (необходимо сделать символ заглавным!) и клавишу Alt. Таким образом, при указании в качестве основной клавиши заглавной буквы, можно определять трехкла-вишные акселераторы. Кстати, если мы хотим, чтобы для вызова команды использовалась клавиша Control, то можно символ в кавычках предварить знаком л.
Примерами акселераторов в файле ресурсов могут служить следующие записи:
«a», IDM_TheJFirst_Item, ALT // определяется комбинация Alt-a
«A», IDM_The_Second_Item, ALT // определяется комбинация
Shift-Alt-a
Таблица акселераторов должна быть загружена в память после создания окна до начала работы с меню. Поэтому желательно вызов функции LoadAccelerator(), осуществляющей загрузку таблицы акселераторов, вставить в текст программы сразу же после создания окна.
Функция LoadAcceleratorQ при вызове должна получить два аргумента. Первый аргумент - это хэндл экземпляра программы, а второй - имя таблицы акселераторов. В результате вызова мы получаем хэндл нового объекта - таблицы акселераторов в памяти.
Но и это еще не все. Если рассуждать логически, то каждое нажатие акселераторной комбинации должно генерировать сообщение WM_COMMAND. Для этого акселераторы и создавались. Поэтому, даже после загрузки таблицы в память программа не сможет на них правильно реагировать, если мы не будем использовать функцию TranstateAccelerator(), которая преобразует сообщения от клавиатуры в сообщения WM_COMMAND. Описание этой функции можно найти в заголовочном файле winuser.h:
WINUSERAPI int WIN API TranslateAcceleratorA(HWND hWnd,
HACCEL hAccTable, LPMSG IpMsg); WINUSERAPI int WINAPI TranslateAcceleratorW(HWND hWnd,
HACCEL hAccTable, LPMSG IpMsg); «ifdefUNICODE
#define TranslateAccelerator TranslateAcceleralorW «else
#dcfine TranslateAccelcrator TranslateAccelcratorA «endif// (UNICODE
Аргументы этой функции в достаточной степени очевидны. Первый аргумент -хэндл окна, которому принадлежит меню с акселераторами, второй - хэндл таблицы акселераторов, с помощью которой производится генерация сообщения WM_COMMAND, третий - указатель на сообщение. TranslateAcceleratorQ возвращает Heir/левое значение, если нажата акселераторная комбинация и нуль в противном случае. Поэтому с учетом вызова этой функции цикл обработки сообщений должен выглядеть следующим образом:
while(GetMessagc(&Msg, NULL, О, О))
( i
if( !TranslaleAccclcrator(hWnd, hAccel, &Msg))
!
TranslateMessage(&Msg); DispatchMcssage(&Msg);
return Msg.wParam;
Итак, с созданием таблиц акселераторов мы разобрались. Дело за малым - рассмотреть небольшой пример. В данном случае я не стал изобретать велосипед, и сделал следующее:
в программе, взятой из предыдущего примера, создал меню не в программе, а в файле ресурсов;
определил в файле ресурсов акселераторные комбинации;
добавил в цикл сообщений обработку акселераторных комбинаций.
В результате получились файлы, которые приведены ниже:
«define IDM_Enable_Disable О «define IDMJExit 1 «define IDM_About 2 «define IDP_File 3 «define IDPJHelp 4
Листинг № 4. Файл определений:
#include «menu.h»
MyMenu MENU
POPUP "&File" {
MENUITEM "Enable exit\te", IDM_Enable_Disable, GRAYED
MENUITEM "E&xit", IDM_Exit
POPUP "&Help"
MENUITEM "About\ta", IDM About, DISABLED
MyMenu ACCELERATORS {
«x», IDM_Exit, ASCII «a», IDM_About, ASCII «e», IDM_Enable_Disable, ASCII «d», IDM_Enable_Disable, ASCII
Листинг № 5. Файл ресурсов:
«include <windows.h>
«include <commctrl.h>
«include "menu.h"
char* pMessages[] = {"Enable or disable exit", "Exit from the program", "About this program", "File operations", "Help operations", "Menu example", "System menu"};
long WINAPI HelloWorldWndProc ( FTWND, UINT, UINT. LONG );
HWND hStatusWindow;
UINT wld;
HMENU hMenu,hFileMenu,hHelpMenu;
HrNSTANCE hlnst;
int APIENTRY WinMain (HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR IpszCmdParam, int nCmdShow )
!
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msg;
HACCEL hAccel;
hlnst = hinstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style = CSJHREDRAW | CSJVREDRAW;
WndClass.lpfnWndProc = (WNDPROC) HelloWorldWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra - 0;
WndClass.hinstance = hinstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, IDC ARROW);
WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
WndClass.IpszMenuName = "MyMenu";
WndClass.IpszClassName = "MenuExample";
if ( !RegisterClass(&WndClass)) !
MessageBox(NULL,"Cannot register class","Error",MB_OK); return 0;
} hWnd = CreateWindowC'MenuExample", "Program No 2",
WS_OVERLAPPEDWINDOW,
CWJJSEDEFAULT,
CW_USEDEFAULT,
CWJJSEDEFAULT,
CWJJSEDEFAULT,
NULL, NULL,
hlnstance,NULL); if(!hWnd)
{
MessageBox(NULL,"Cannot create window","Error",MBJ3K); return 0;
InitCoinmonControlsQ;
hStatusWindow = CreateStatusWindow(WSJTHILD | WS_VISIBLE,
"Menu sample",
hWnd,w!d); if(!hStatusWindow)
{
MessageBox(NULL,"Cannot create status window","Error",MB JDK); return 0;
/* Load the accelerators table */
hAccel = LoadAccelerators(hInst,"MyAccelerators"); /* Show our window */
ShowWindow(hWnd,nCmdShow);
UpdateWindow(hWnd);
hFilcMcnu = GetSubMenu(GetMenu(hWnd),0);
/* Beginning of messages cycle */
while(GetMessage(&Msg, NULL, 0, 0)) {
if( !TranslateAccelerator(hWnd,hAccel,&Msg)) {
TranslateMessage(&Msg); DispatchMessage(&Msg);
return Msg.wParam;
long WINAPI HelloWorldWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
!
RECT Rect;
static UINT nFIag = MFJENABLED; char* pContent[]
>
t
"&EnabIe exit\te", "&Disable exit\td"
};
static UINT nlndcx = 0;
switch(Message) {
case WMJCOMMAND: switch (wParam)
)
case IDMJinable J3isable:
EnableMenuItem(hFileMenu, IDMJЈxit, MFJBYCOMMAND | nFIag); nFIag - ( nFIag -= MFJZNABLED )'.' MFJ3RAYED : MFJENABLED; nlndex = ( nlndex = 0) ? 1 : 0;
ModifyMcnu(hFileMenu, IDM_Enable_Disable, MF_BYCOMMAND | MF_STRING, IDM_Enable_Disable, pContentfnlndex]); break;
case IDM_Exit:
SendMessage(hWnd, WM_CLOSE, NULL, NULL); break;
} case WM_SIZE:
SendMessage(hSlatusWindow,WM SIZE.wParam,IParam);
GetClicnlRcct(hWnd,&Rect);
return 0;
case WM_MENUSELECT: // Selection is lostcd
if ( ((UINT) HIWORD(wParam) == Oxfffl) & ((HMENU) IParam == 0))
{ SendMessage(hStatusWindow, SB_SETTEXT, (WPARAM) 0,
(LPARAM) pMessages[5]); return 0;
} if ((UINT) HIWORD(wParam) & MF_SYSMENU)
{ SendMessage(hStatusWindow, SB_SETTEXT, (WPARAM) 0,
(LPARAM) pMessages[6]); return 0;
if ((UINT) HIWORD(wParam) & MF_POPUP)
(
ScndMcssage(hStatusWindow, SB^SETTEXT, (WPARAM) 0.
(LPARAM) pMessages[3 + LOWORD(wParam)]); return 0;
! SendMessage(hStatusWindow,SB_SETTEXT. (WPARAM) 0, (LPARAM)
pMcssages[LOWORD(wParam)]); return 0;
case WM_DESTROY: PostQuitMessage(O); return 0; } return DefWindowProc(hWnd,Message,wParam, IParam);
Листинг № 5. Программа, демонстрирующая возможности акселераторов меню.
Мне бы хотелось, чтобы читатель поэкспериментировал с этой программой, попробовал переопределить акселераторы, и вновь провел несколько экспериментов.
Завершим рассмотрение темы о меню ответом на вопрос о том, как можно создать акселераторы без использования ресурсов.
96
Таблица 18. Возможные значения флагов поля fVirt структуры типа ACCEL
Флаг
Значение
FALT FCONTROL
FNOFNVERT FSHIFT
FVIRTKEY
При нажатии акселераторной комбинации должна быть нажата
клавиша Alt
При нажатии акселераторной комбинации должна быть нажата
клавиша Control
Внешне меню не реагирует на нажатие акселераторной комбинации
При нажатии акселераторной комбинации должна быть нажата
клавиша Shift
Поле key определяет виртуальную клавишу. Если это поле не
установлено, то считается, что поле key содержим символ ASCII
Для создания таблицы акселераторов применяется функция CreateAcceleratorTableQ, которой в качестве аргументов передаются адрес массива структур типа ACCEL и число структур в этом массиве.
Назначение полей структуры ACCEL должно быть понятно читателю, внимательно прочитавшему текущий раздел. В файле winuser.h эта структура описана следующим образом:
typedef struct tagACCEL {
BYTE fVirt; /* Also called the flags field */
WORD key;
WORD cmd; ( ACCEL, «LPACCEL;
Если мы вспомним формат описания акселератора в файле ресурсов, то сразу можно догадаться о том, что поле cmd - это аналог поля Menuld, key соответствует Key, а значения поля fVirt являются комбинациями флагов (табл. 18), которые однозначно соответствуют полям Тип и Параметр.
И наконец, чтобы завершить тему об акселераторах, замечу, что при уничтожении окна автоматически из памяти удаляются только акселераторы, созданные с помощью функции LoadAccelerator(). В случае, если использовалась функция CreateAcceleratorTable(), программа должна сама заботиться об удалении таблицы из памяти. Для этого применяется функция DestroyAcceleratorTableQ, в качестве аргумента которой передается хэндл таблицы акселераторов.
Мы завершили рассмотрение темы, связанной с меню и акселераторами. К этому моменту читатель должен быть готов к тому, чтобы самостоятельно использовать ПОЧТИ все возможности по управлению меню,
97
предоставляемые Win32. За пределами нашего внимания остался один пункт. Он связан с созданием в памяти структуры типа MENUITEMTEMPLATE и использования ее для создания меню посредством вызова функции EoadMenuIndirect(). В книге Чарльза Петцольда есть одна фраза, которая меня не только развеселила, но и подвигла на изучение этого совершенно бесполезного (это моя личная точка зрения, но тогда-то я еще не знал этого!) вопроса. Вот эта фраза: «If you're brave, you can try using it yourself (Если ты смелый, ты можешь самостоятельно попробовать использовать ее (функцию LoadMenuIndirectQ).» Больше времени на разбор этой функции я терять не стану. Уважаемый читатель! Если вы не желаете прислушаться к моему совету, изучите, пожалуйста, третий способ создания меню самостоятельно.
Очередное ура! Мы прошли еще одну тему! Теперь мы умеем полностью определять порядок взаимодействия программы с пользователем через меню. Для того чтобы завершить раздел, посвященный непосредственному взаимодействию пользователя с программой, осталось всего ничего - начать и завершить тему о диалоговых окнах и обо всем, что с ними связано.
ДИАЛОГОВЫЕ ОКНА И ИХ ЭЛЕМЕНТЫ
В предыдущей главе мы разобрались с порядком создания меню и ак-селераторных комбинаций. Но любой работавший с Windows знает, что возможности программы, обеспечивающие взаимодействие с пользователем, отнюдь не ограничиваются рамками меню. Основным средством «общения» пользователя с программой являются диалоговые окна (их также называют диалогами). В этом разделе мы рассмотрим работу диалоговых окон и их взаимодействие не только с пользователем и программой, но и окнами более низкого уровня, называемыми элементами управления, которые выполняют большую часть черновой работы, незаметной не только пользователям, но и программистам.
Диалоговые окна можно классифицировать по двум критериям. Первый критерий - это модальность диалогового окна. Второй критерий, не всегда заметный, заслуживает особого внимания. Второй критерий фактически определяет, с одной стороны, возможности диалогового окна, а с другой стороны, ответственного за обработку сообщений посылаемых диалоговому окну. В подавляющем большинстве случаев обработку сообщений, адресованных диалоговому окну, производит функция диалогового окна (о ней речь впереди), но иногда диалоговое окно своей функции не имеет (она запрятана в «глубинах» системы) и всю обработку производит система. Возможности таких окон очень ограничены, в
98
основном они предназначены для выдачи сообщений пользователю. Об этом говорит даже их название - окна сообщений. Сейчас мы определим, что такое модальное и немодальное окно, рассмотрим процесс создания диалоговых модальных и немодальных диалоговых окон, остановимся на некоторых элементах управления диалоговыми окнами, а потом поговорим об окнах сообщений. Надеюсь, что даже после такого краткого экскурса в область диалоговых окон читатель сможет спокойно манипулировать окнами и элементами управления.
Модальные и немодальные диалоги
Диалоговые окна бывают модальными и немодальными.
Наиболее часто используются модальные окна. Эти окна не дают пользователю возможности работать с другими окнами, созданными приложением, породившим диалоговое окно, но разрешают переключаться на работу с другими приложениями. Для того чтобы пользователь мог продолжить работу с другими окнами своего приложения, необходимо завершить работу с диалоговым окном.
В особых случаях, представляющих угрозу системе и требующих немедленной реакции оператора, могут использоваться системные модальные окна. Эти окна не позволяют переключаться ни на какое другое окно. Естественно, что и применять системное модальное окно нужно с умом.
Немодальные диалоговые окна не требуют своего завершения для продолжения работы, и пользователь может во время работы с ними свободно переключаться на любое приложение.
РАБОТА С ДИАЛОГОВЫМИ ОКНАМИ
Диалоговое окно позволяет вводить и получать информацию, которую сложно или вовсе невозможно ввести через меню. Я уже говорил о том, что диалоговое окно имеет в своем составе некие элементы, окна более низкого уровня. Их называют элементами управления. Примером одного из таких элементов могу служить кнопки, которые, наверное, видел любой, хоть чуть-чуть поработавший с Windows. Так как без элементов управления диалоговые окна теряют всякий смысл, то рассмотрим, что такое
КНОПКИ, СПИСКИ И ПРОЧЕЕ...
Как уже было сказано, элементы управления - это ОКНА более низкого по отношению к диалоговому окну уровня. Предлагаю отметить то, что элементы управления никогда не могут использоваться как самостоятельные окна. Они всегда используются на фоне какого-то окна, которое
99
является для них родительским окном. Элементы управления, таким образом, всегда являются дочерними окнами, другими словами, у них всегда присутствует стиль WM_CHILD.
Как и любые другие окна, элементы управления могут получать и выдавать сообщения. Правда, это относится не ко всем элементам управления, но... Стоп! Давайте прервемся на секунду.
Мне бы хотелось обратить внимание читателя на один интересный момент. Для посылки сообщения обычно используют функции SendMessageQ и SendDlglteinMessageQ. Дело в том, что значение, которое возвращают эти функции, зависит только от того сообщения, которое они отправили. Таким образом, если вам необходимо узнать по возвращенному значению, что произошло в результате обработки того или иного сообщения, ищите описание возвращаемых значений не в описаниях функций, а в описаниях сообщений.
По умолчанию подавляющее большинство сообщений от элементов управления получает диалоговое окно, которому они принадлежат. Диалоговое окно должно каким-то образом их обрабатывать. Отсюда очередной вывод - у диалогового окна должна быть собственная оконная функция.
Каждому элементу управления присваивается идентификатор. При каком-либо воздействии на этот орган управления со стороны пользователя диалоговое окно получает сообщение, содержащее идентификаторы элемента и типа производимого пользователем действия. Диалоговая функция обрабатывает эти сообщения и выполняет соответствующие действия. Этот процесс происходит параллельно с обработкой сообщений в оконной функции. При этом нужно заметить, что в отличие от обычного окна, «нормальное» диалоговое окно Eie имеет своего цикла обработки сообщений. Цикл обработки сообщений запускается один раз при запуске программы.
Элементами управления могут быть кнопки (buttons), которые мы уже использовали в окнах сообщений, переключатели (check boxes), селекторы (radio buttons), списки (list boxes), комбинированные списки (combo boxes), линейки прокрутки (scroll bars) и статические элементы (statics). Все элементы в этом перечне относятся к категории базовых, и все они присутствовали и в Windows 3.x. На их основе Microsoft разработала серию новых элементов управления (common controls), которые позволили расширить возможности интерфейса с пользователем и улучшить внешний вид приложений. Мы рассмотрим как базовые, так и новые общие (как еще можно перевести на русский язык название «common controls»?) элементы управления.
100
Я уже упоминал, что основную часть работы диалогового окна выполняют элементы управления. Поэтому рассмотрим сначала вопрос о том, как может быть создано диалоговое окно, а потом на примерах -работу каждого типа элементов управления.
СОЗДАНИЕ ДИАЛОГОВОГО ОКНА
Диалоговое окно, как и меню, может быть создано несколькими способами: во-первых, с помощью описания его в файле ресурсов и, во-вторых, во время выполнения программы. Наиболее часто используется описание диалога в файле ресурсов. Лучше всего при создании диалога воспользоваться редактором ресурсов, с помощью которого может быть создан текстовый файл, содержащий описание диалогового окна. Ресурсы диалога в этом текстовом файле задаются оператором DIALOG, который имеет следующий формат:
DialogName DIALOG [DISCARDABLE] CAPTION «Заголовок окна» STYLE <Стили диалогового окна> FONT n, <имя шрифта>
X, Y, Width, Height
Описание элементов диалога
В данном случае DialogName - это имя диалогового окна. Опция DISCARDABLE станет совершенно ясной при рассмотрении вопроса об организации памяти в Windows. Параметры X и Y - это координаты верхнего левого угла диалогового окна, Width и Height - ширина и высота диалога. STYLE описывает стили окна. Здесь могут использоваться как стили, применяемые для описания обычных окон (об этих стилях мы говорили при создании первой программы для Windows), так и стили, применяемые только в диалоговых окнах. Эти новые стили приведены в табл. 19.
Приведенных выше сведений вполне достаточно, чтобы написать заготовку диалогового окна в файле ресурсов. Но какой смысл описывать диалоговое окно, если в нем нет ни одного из элементов управления? Ведь даже закрыть такое диалоговое окно (если в нем, конечно, нет системного меню) невозможно! Значит, нам необходимо срочно научиться описывать эти элементы !
101
Я уже упоминал о том, что в «недрах» Win32 есть масса предопределенных объектов. В частности, там находятся и некоторые предопределенные классы окон. К таким классам относятся кнопки (класс «button»), списки (класс «listbox»), комбинированные списки (класс «combobox»), окна редактирования (класс «edit»), полосы прокрутки класс «scrollbar»), статические элементы (класс «static»). У каждого класса есть свой определенный набор стилей, которые определяют внешний вид и поведение элементов управления, относящихся к данному классу.
Управление окном каждого класса, а также получение информации от него производится с помощью обмена управляющими сообщениями. О действиях пользователей с ними элементы управления оповещают свои родительские окна через нотификационные сообщения. Предлагаю читателю запомнить это, так как мы еще неоднократно вспомним о предопределенных сообщениях.
Приступим к изучению элементов управления. Вспомним, что мы уже неоднократно встречались с кнопками. Давайте и начнем с описания обычных кнопок (buttons).
Таблица 19. Стили диалоговых окон
Стиль |
Значение |
Эффект |
DS^ABSALIGN |
0x000 1L |
Положение диалогового окна исчисляется в |
|
|
экранных координатах |
DS_SYSMODAL |
Ox0002L |
Создается системное модальное диалоговое |
|
|
окно |
DSJDLOOK |
Ox0004L |
Создается диалоговое окно, имеющее |
|
|
зрительную иллюзию трехмерности |
DS_FIXEDSYS |
OxOOOSL |
Вместо SYSTEM FONT используется |
|
|
SYSTEM_FIXED^FONT |
DS_NOFAILCREATE |
0x00 10L |
Диалоговое окно создается, несмотря на то, |
|
|
что при его создании произошли ошибки |
DS LOCALEDIT |
Ox0020E |
В 32-битных приложениях не используется |
DS_SETFONT |
Ox0040L |
Определяется шрифт, который будет |
|
|
применяться в диалоговом окне |
DS MODALFRAME |
OxOOSOL |
Создается модальное диалоговое окно |
DS NOIDLEMSG |
OxOlOOL |
|
DS^SETFOREGROUND |
Ox0200L . |
Поместить диалоговое окно на передний |
|
|
план |
DS CONTROL |
Ox0400L |
|
DS_CENTER |
OxOSOOL |
Диалоговое окно помещается в центр |
|
|
рабочей области |
DS CENTERMOUSE |
OxlOOOL |
|
DS_CONTEXTHELP |
Ox2000L |
|
Кнопки
Перед тем, как начать рассказ о кнопках, хочу предостеречь читателя. Дело в том, что можно использовать кнопки и в обычных окнах. Но они, как и большинство элементов управления, проектировались для использования именно в диалоговых окнах. Использование кнопок в обычных окнах не рекомендуется, ибо это увеличивает риск того, что программа будет работать неправильно.
Кнопка - это имитация на экране обычной кнопки или переключателя. В этом разделе под кнопками я также подразумеваю не только PushButtons (обычные нажимаемые кнопки), но и Check Boxes (обычно это небольшие квадратики, в которых можно установить или не установить пометку) и Radio Buttons (небольшие кружочки, В ОДНОМ из которых стоит точка). Пользователь может установить курсор мыши на кнопку, щелкнуть клавишей мыши - и кнопка пошлет диалоговому окну сообщение WM_COMMAND. To же произойдет и в случае, если пользователь сначала выберет кнопку клавишей Tab, а потом нажмет Enter.
В параметрах сообщения WM_COMMAND содержится информация, которой достаточно, чтобы диалоговое окно узнало, от какой кнопки пришло сообщение, какое действие требуется выполнить, и каким образом пользователь инициировал выдачу сообщения.
При этом необходимо отметить, что обычная кнопка (её называют PushButton) не помнит того, что с ней делали, т. е. она на короткое время становится нажатой, а затем возвращается в исходное состояние. Можно нажать кнопку десять раз подряд, и все десять раз она пошлет диалоговому окну одно и то же сообщение, если, конечно, кнопка не сделана запрещенной. CheckBox помнит о том, что он находится в одном из двух состояний - установленном или не установленном, некоторые CheckBox'bi могут находиться еще и в неопределенном состоянии.
Говорить о состоянии одной RadioButton бессмысленно. Дело в том, что RadioButton'bi предназначены для осуществления выбора одного из нескольких взаимоисключающих вариантов, поэтому можно говорить о группе (иногда говорят кластере) RadioButton'ов. В частности, для объединения RadioButton'ов в кластеры служит такой элемент управления, как группа (GroupBox). Обычно группы используются для группирования органов управления только для улучшения дизайна диалоговых окон. Что же касается RadioButton'oB, то без обрамляющей группы существование кластера не имеет смысла.
Формат описания кнопок в окне диалога достаточно прост:
CONTROL «Заголовок», ButlonlD, class, styles, X, Y, Width, Height
103
X, Y, Width, Height - это все ясно. Все то же самое, что и при описании непосредственно диалогового окна. «Заголовок» - надпись на кнопке или рядом с кнопкой. ButtonID - идентификатор кнопки, т. е. значение, которое посылается диалоговому окну при нажатии кнопки в качестве LOWORD (wParam). Через HIWORD(wParam) диалоговое окно получает код нотификации, т. е. код того действия, которое произвел пользователь. Примерами действия пользователя могут служить нажатие клавиши Enter, двойной щелчок правой или левой клавишей мыши и так далее. А источник, т. е. хэндл инициировавшего сообщение окна, сообщения содержится в IParam (я напомню, что если сообщение приходит от меню, то IParam всегда равен 0, а если от акселератора - 1). Все легко и просто. Сложности начинаются при рассмотрении класса, определяющегося полем class, типа кнопки и стиля кнопки, которые определяется параметром style.
Для кнопок, вне зависимости от того, PushButton ли это, RadioButton или CheckBox, класс всегда определяется как «button».
Читатель, наверное, уже привык к тому, что ответы на большинство вопросов можно найти в файлах заголовков и файлах помощи Win32. Поэтому и сейчас, как всегда, смотрим в заголовочный файл winuser.h, выбираем оттуда стили кнопок, которые начинаются с букв BS_, и сводим их в табл. 20. Надоело изучать эти таблицы? Ничего, тяжело в учении - легко в бою! А.В. Суворов, «Наука побеждать».
А теперь, когда мй изучили весь вопрос теоретически, попробуем разобраться со всем этим разнообразием кнопок и стилей. Напишем небольшую программу, в которой будут присутствовать все виды кнопок (но отнюдь не все виды стилей). От кнопок будем получать сообщения и обрабатывать их. Мы также научимся устанавливать кнопки в те или иные состояния. В предлагаемой программе ничего не делается, за исключением того, что демонстрируется, как устанавливать кнопки в то или иное состояние и считывать состояние, в котором кнопки находятся.
В программе создается диалоговое окно, имеющее кластер RadioButtons, состоящий из трех кнопок, три CheckBox'a, а также две PushButton. Состояния RadioButtons и CheckBoxes до отображения диалогового окна могут быть определены через меню. Затем проявляется обратная связь - состояния RadioButtons и CheckButtons определяют состояния элементов меню после закрытия диалога. PushButton с надписью «Cancel» приводит к закрытию диалогового окна.
104
Т а б л и ц а 20. Стили кнопок
Флаг-
Значение
Эффект
BS_ PUSHBUTTON BS DEFPUSHBUTTON
BS_CHECKBOX
BS^AUTOCHECKBOX
BS^RADIOBUTTON BSJSTATE
BS^AUTO3STATE
BS_GROUPBOX BSJJSERBUTTON
BS_AUTORADIOBUTTON
BS_OWNERDRAW BS_LEFTTEXT
BSJTEXT BSJCON BS^BfTMAP BS_LEFT
BS_RtGHT BS CENTER
OxOOOOOOOOL 0x00000001L
Ox00000002L
Ox00000003L
Ox00000004L OxOOOOOOOSL
Ox00000006L
Ox00000007L OxOOOOOOOSL
Ox00000009L
OxOOOOOOOBL Ox00000020L
OxOOOOOOOOL Ox00000040L OxOOOOOOSOL 0x000001OOL
Ox00000200L Ox00000300L
Создается обычная кнопка Создается обычная кнопка, которая срабатывает при нажатии «Enter» даже тогда, когда не выбрана Создастся CheckBox, при нажатии состояние автоматически не изменяется, забота об этом ложится на программу
Создается CheckBox, который автоматически меняет свое состояние при нажатии
Создается Radio Button, автоматически состояние не меняется То же, что и BS_CHECKBOX, но имеет три состояния - включенное, отключенное и неопределенное, автоматически состояние не меняет То же, что и предыдущее, но состояние меняется автоматически Группа
Устаревший стиль, необходимо использовать BS_OWNERDRAW То же, что и RadioButton, но при нажатии состояние меняется автоматически
За прорисовку кнопки отвечает программа, а не система Текст помещается слева от RadioButton'a или CheckBox'a, то же, что и BS_RIGHTBUTTON Внутри или рядом с кнопкой отображается текст
Внутри кнопки или рядом с кнопкой отображается иконка Внутри кнопки или рядом с кнопкой отображается bitmap Размещает текст у левого края прямоугольника, выделенного для размещения кнопки Размещает текст у правого края прямоугольника, выделенного для размещения текста Размещает текст по горизонтали в центре прямоугольника, выделенного для размещения кнопки
105
Окончание табл. 20
Флаг
BSJTOP
BS_BOTTOM
BSJVCENTER
BS_PUSHL1KE
BSJvfULTILINE
BS_NOTIFY
BS_FLAT
BS RIGHTBUTTON
Значение
Ox00000400L OxOOOOOSOOL OxOOOOOCOOL
0x0000IOOOL Ox00002000L Ox00003000L
OxOOOOSOOOL Ox00000020L
Эффект
Размещает текст у верхнего края прямоугольника, выделенного для размещения кнопки Размещает текст у нижнего края прямоугольника, выделенного для размещения кнопки Размещает текст по вертикали в центре прямоугольника, выделенного для размещения кнопки Делает Checkbox или RadioBulton внешне похожими на PushBulton При необходимости текст разбивается на несколько строк Разрешает посылку родительскому окну нотификационных сообщении BN_DBLCLK, BN KILLFOCUSи BN^SETFOCUS Не добавляется имитация трёхмерности изображения элемента управления
RadioButton или CheckBox размещаются справа от надписи (то же, что и BS LEFTTEXT)
При работе программы любое изменение состояния кнопок приводит к выдаче сообщения в строке состояния диалогового окна.
Ниже приволен файл описаний, использующийся при работе демонстрационной программы:
#dcfineIDM EXIT |
101 |
#defmeIDM RadioButtonl |
102 |
#defmc IDM_RadioButton2 |
103 |
tfdcfine IDM_RadioButton3 |
104 |
#dcfine IDM CheckButton 1 |
105 |
#define IDM_CheckButton2 |
106 |
#define IDM_CheckButton3 |
107 |
Adeline IDM DisplayDialog |
108 |
#dcimclDC BUTTON! |
201 |
«define IDC BUTTON2 |
202 |
#ddine IDC GROUPBOX 1 |
203 |
#defineIDC RADIOBUTTON 1 |
204 |
#define IDC RADIOBUTTON2 |
205 |
#defme IDC
#defme IDC
#defme IDC~
#define IDC
#defme IDC~
#define IDC
RADIOBUTTON3
GROUPBOX2
^CHECKBOX!
CHECKBOX2
CHECKBOX3
STATUSBAR
206
207
208
209
210
301
А теперь - основной файл программы:
#includc <windows.h> ^include <commctrl.h>
#iiiclude "buttons.h"
HINSTANCE hlnst; HWND hWnd ; int nRadioButtonld;
UINT uCheckBoxesState[3] - {MFJJNCHECKED, MF_UNCHECKED, MFJJNCHECKED};
long WINAPI ButtonsExampleWndProc ( HWND, UINT, UINT, LONG ); BOOL CALLBACK ButtonsExampleDialogProc(HWND, UINT, WPARAM,
LPARAM);
int WINAPI WinMain ( HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPSTR IpszCmdParam, int nCmdShow )
WNDCLASS WndClass ;
MSG Msg;
char szClassName[] = "ButtonsExample";
hlnst — hlnstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style = CSJHREDRAW | CS_VREDRAW;
WndClass.IpfnWndProc = ButtonsExampleWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hlnstance = hlnstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);'
WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
WndClass.IpszMenuName = "ButtonsExampleMenu";
WndClass.IpszClassName = szCIassName;
if ( !RegisterClass(&WndClass))
'i
MessageBoxfNULL,"Cannot register class","Error",MB_OK); return 0; ~~ } liWnd ^ CreatcWindow(szClassName, "Button Use Example",
107
WS_OVERLAPPEDWINDOW, CWJJSEDEFAULT, CW_USEDEFAULT, CWJJSEDEFAULT, CW_USEDEFAULT, NULL, NULL, Mnstance,NULL);
ifl[!hWnd)
MessageBox(NULL, "Cannot create window", "Error", MB_OK); return 0;
}
InitCommonControls(); /* Show our window */
ShowWindow(hWnd,nCmdShow);
UpdateWindow(hWnd); /* Beginning of messages cycle */
while(GetMessage(&Msg, NULL, 0, 0))
{
TranslateMessage(&Msg); DispatchMessage(&Msg);
} return Msg.wParam;
LRESULT CALLBACK ButtonsExampleWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
{
static BOOL bFlag = FALSE; static HMENU hMenu I , hMenu2; int i;
switch(Message)
{
case WM_CREATE:
hMenu I =GetSubMenu(GetSubMenu(GetMenu(hWnd), 1), 0); hMenu2 = GetSubMenu(GetSubMenu(GetMenu(hWnd), 1), I); CheckMenuRadioItem(hMenu 1 , IDM_RadioButton 1 , IDM_RadioButton3 ,
IDM_RadioButtonl, MF_BYCOMMAND); nRadioButtonld = IDM_RadioButtonl + 102; for(i = 0; i < 3; i++)
ClieckMenuItem(hMenu2, IDM_CheckButton 1 + i, MF_UNCHECKED); break;
case WM_COMMAND: switch(LOWORD(wParam)) {
case IDM^EXIT:
SendMessage(hWnd, WM_CLOSE, 0,0); break;
case IDM_RadioButton 1 : case IDM_RadioButton2: case IDM_RadioButton3: nRadioButtonld = LOWORD(wParam) + 102;
108
Cht-ckMenuRadiohem(hMenuI, lpM_RadioButtonI, IDM_RadioButton3,
LOWOR6(wParam), MF_BYCOMMAND); break;
case IDM_CheckButtonl: case IDM_CheckButton2: case IDM_CheckButton3: i - LOWORD(wParam) - 105; uCheckBoxesState[i] = uCheckBoxesState[i] == MF_CHECKED ?
MF_UNCHECKED : MF_CHECKED; ChcckMenuItcm(liMenu2, LOWORD(wParam), MF_BYCOMMAND
uCheckBoxesState[i]); break;
case IDM_DisplayDialog:
DialogBox(hInst, "ButtonsExample", hWnd, ButtonsExampleDialogProc); break; }
break;
case WMJ5ESTROY: PostQu i tMcssage(O); return 0; i return DefWindowProc(hWnd,Message,wParam, IParam);
BOOL CALLBACK. ButtonsExampleDialogProc(HWND hDIg, UINT Message,
WPARAM wParam, LPARAM IParam)
f \
int i;
char cMyMcssage[SO];
switch(Messagc)
f
case WMJNITDIALOG: // Set slates of controls
ScndDlgltemMcssageOiDlg. nRadioButtonld, BM SETCHECK,
BST_CHECKED, 0);
lbr(! -- [DC CHECKBOX!; i <- IDC_CHECKBOX3; i++) if(uCheckBoxesState[i - 208])
SeiidDlgItemMessagc(hDlg, i, BM_SETCHECK, BST_CHECKED, 0); return TRUE: case WM_COMMAND: swilch(LOWORD(wParanv)
case IDC_RADIOBUTTON1: case IDC_RADIOBUTTON2: case IDC_RADIOBUTTON3:
sprintf(cMyMcssage,"Message from RadioButton%d", LOWORD(wParam) - 203);
109
SendDlgItemMessage(hDlg,IDC_STATUSBAR,SB_SETTEXT,
(WPARAM) 0, (LPARAM) cMyMessagc);
CheckMenuRadioItem(GetSubMenu(GetSubMenu(GetMenu(hWnd), 1), 0), IDM_RadioButton 1, IDM_RadioButton3, LOWORD(wParam)- 102, MF_BYCOMMAND); return FALSE; caseHXJCHECKBOXl: case IDC_CHECKBOX2: caseIDC_CHECKBOX3: sprintf(cMyMessage,"Message from CheckBox%d",
LOWORD(wParam) - 207); SendDlgItemMessage(hDlg,IDC_STATUSBAR,SB_SETTEXT,
(WPARAM) 0, (LPARAM) cMyMessage); i = LOWORD(wParam) - 208; uCheckBoxesState[i] = uCheckBoxesState[i] == MF_CHECKED ?
MF_UNCHECKED : MF_CHECKED;
CheckMenuItem(GetSubMenu(GetSubMenu(GetMenu(hWnd), 1), 1), LOWORD(wParam) - 103. uCheckBoxesState[i]); return FALSE; caseIDC_BUTTONl:
SendDlgItemMessage(hDlg,roC_STATUSBAR,SB_SETTEXT, (WPARAM) 0,
(LPARAM) "Message from PushButton"); return TRUE; case IDCJ3UTTON2: // Save the state of RadioButtons
i = IDC_RADIOBUTTON 1; while(!SendDlgItemMessage(hDlg, i, BM_GETCHECK, 0, ()))
nRadioButtonld = i; // Save the state of CheckButtons
for(i = RXJCHECKBOX1; i <= IDC_CHECKBOX3; i++) uCheckBoxesState[i - 208] = SendDlgltemMessagefhDlg, i, BM_GETCHECK, 0, 0) = 0 ? MF_UNCHECKED : MF_CHECKED; EndDialog(hDlg,0); return TRUE;
i /
break; return FALSE;
При линковании программы необходимо использовать фага ресурсов:
#include "buttons.h"
ButtonsExample DIALOG 50, 50, 154, 108
110
STYLE DS_MODALFRAME | DSJDLOOK | DS_CONTEXTHELP |
WS_POPUP | WS_VISIBLE | CAPTION "Buttons Example" FONT 8, "MS Sans Serif
WS CAPTION WS SYSMENU
CONTROL "PushButton", IDC_BUTTONl. "button", BS_PUSHBUTTON |
BS_CENTER | BS_NOTIFY | WS_CHILD | WSJVISIBLE |
WS_TABSTOP, 8, 72, 64, 16 CONTROL "RadioButtonl", IDC_RADIOBUTTON 1, "button",
BS_AUTORADIOBUTTON | WS „CHILD | WS_VISIBLE |
WSJTABSTOP, 8, 12,64, 16 CONTROL "RadioButton2", IDCJRADIOBUTTON2, "button",
BS_AUTORADIOBUTTON | BS_FLAT | WS_CHILD
WSJVISIBLE | WSJTABSTOP, 8, 28, 64, 16 CONTROL "RadioButton3", IDC__RADIOBUTTON3, "button",
BS_AUTORADIOBUTTON | BS_LEFTTEXT | WS_CHILD |
WSJVISIBLE I WSJTABSTOP, 8, 44, 64, 16 CONTROL "Groupl", IDCJ3ROUPBOX1, "button", BS_GROUPBOX |
WS^CHILD | WSJVISIBLE | WS_GROUP, 4, 4, 72, 60 CONTROL "CheckBoxl", IDC_CHECKBOX1, "button", BS_AUTOCHECKBOX |
WS^CHILD | WS_VISIBLE | WS_TABSTOP, 82, 12, 64, 16 CONTROL "CheckBox2", IDC_CHECKBOX2, "button", BS_AUTOCHECKBOX |
WS_CHILD | WSJVISIBLE | WSJTABSTOP, 82, 28, 64, 16 CONTROL "CheckBox3", IDC_CHECKBOX3, "button", BS^AUTOCHECKBOX |
WS_CHILD | WS_VISIBLE | WS_TABSTOP, 82, 43, 64, 16 CONTROL "Group2", IDC_GROUPBOX2, "button", BS_GROUPBOX |
WS_CHILD | WS_VISIBLE WS_GROUP, 78, 4, 72, 60 CONTROL "Cancel", IDC_BUTTON2, "button", BS_PUSHBUTTON |
BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 82, 72,
64, 16 CONTROL "StatusWindow 1", IDC^STATUSBAR, "msct!s_statusbar32", 3 |
WS_CHILD | WS_VISIBLE, 0, 116, 154, 12 }
ButtonsExampleMenu MENU f
POPUP "&File" ;
MENUITEM "E&xit", IDM_EXIT \
POPUP "&Dialog"
f t
POPUP "Initialize &RadioButtons"
f (
MENUITEM "Set RadioButton&l", IDM_RadioButton I MENUITEM "Set RadioButton&2", IDM_RadioButton2 MENUITEM "Set RadioButton&3", IDM_RadioButton3
i
POPUP "Initialize &CheckButtons"
111
\
MENUITEM "Set CheckButton&l", IDM_CheckButtonl MENUITEM "Set CheckButton&2", IDM_CheckButton2 MENUITEM "SetCheckButton&3", IDM_CheckButton3
MENUITEM SEPARATOR
MENUITEM "Displa&y Dialog", IDM_DisplayDialog
Buttons Example
Рис. 8. Диалоговое окно с кнопками различных стилей
На рис. 8 показано диалоговое окна, которое создается данной программой. Функция WinMainQ полностью стандартна и ничего нового не содержат.
При обработке сообщения WM^CREATE мы узнаем о новой возможности, связанной с меню. С помощью функции CheckRadioMenuItem() можно заставить группу элементов меню работать как кластер RadioButtons. В этом случае установка отметки у одного элемента меню приводит к сбросу отметки у всех других элементов меню, входящих в состав группы. Характерно, что при определении группы мы должны указать минимальный и максимальный идентификаторы элементов меню, включаемых в группу. Элементы, включаемые в меню, должны иметь идентификаторы, попадающие в определенный интервал, а не произвольно определенные. Этим мы будем пользоваться при определении и отображении в меню состояния кластера RadioButtons.
При обработке того же сообщения мы встречаем еще одну функцию, позволяющую установить отметку у элемента меню, - CheckMenultemQ. Эта функция позволяет изменять состояние только одного элемента меню. С помощью этой функции мы будем устанавливать и отображать состояние CheckButtons.
112
Наверное, читателю небезынтересно узнать, что эти две функции для отметки элементов по умолчанию используют различные значки. Рекомендую читателю обратить внимание на то, какие значки используются для отметки в обеих функциях.
Но эта программа написана отнюдь не для того, чтобы рассказывать о новых возможностях меню. При обработке сообщения от элемента меню с надписью «Display Dialog» создается диалоговое окно, в котором и содержатся те кнопки, о которых мы говорили. В зависимости от того, какое диалоговое окно должно быть создано, могут быть использованы функции DialogBox() и CreateDialogQ. Функция DialogBoxQ создает модальное диалоговое окно. Немодальное диалоговое окно создается с помощью функции CreateDialogQ. Мы используем функцию DialogBoxQ.
В файле winuser.h эта функция описана следующим образом:
WINUSERAPI int WINAPI DialogBoxParamA(HINSTANCE hlnstance,
LPCSTR IpTcmplateName, HWND hWndParent, DLGPROC IpDialogFunc, LPARAM dwInitParam); WINUSERAPI int WINAPI DialogBoxParamW(HINSTANCE hlnstance,
LPCWSTR IpTemplatcName, HWND hWndParent, DLGPROC IpDialogFunc, LPARAM dwInitParam);
#ifdef UNICODE
#define DialogBoxParam DialogBoxParamW
#elsc
#dcfine DialogBoxParam DialogBoxParamA
#endif// IUNICODE
#dcfine DialogBoxA(hInstance, IpTemplatc, hWndParent, IpDialogFunc) \
DialogBoxParamA(hInslance, IpTemplate, hWndParent, IpDialogFunc, OL)
//define DialogBoxW(hInstance, IpTemplate, hWndParent, IpDialogFunc) \
DialogBoxParamW(hInstance, IpTemplate, hWndParent, IpDialogFunc, OL)
//ifdef UNICODE
//define DialogBox DialogBoxW
//else
//define DialogBox DialogBoxA
#endif // IUNICODE
Видно, что функция DialogBoxQ фактически является частным случаем функции DialogBoxParamQ. На их различии мы остановимся чуть позже, а сейчас рассмотрим аргументы DialogBoxQ.
Первый аргумент понятен, мы его используем в каждой программе. Второй аргумент - указатель на имя шаблона, использующегося при
113
построении диалога. В нашем случае диалог сохранен в виде ресурса, поэтому мы указываем имя ресурса.
Третий аргумент - хэндл родительского окна. О том, что такое родительское окно, нужно сказать особо.
Во-первых, если мы представим окна, начиная с Desktop'a, как располагающиеся друг над другом (в так называемом Z-порядке), то дочернее окно обязательно будет над родительским, от этого зависит порядок обработки сообщений. Во-вторых, сообщения о действиях с диалоговым окном (нотификационые сообщения) система будет посылать именно родительскому окну. В-третьих, при закрытии родительского окна дочернее окно закрывается автоматически. Возможно, конечно, создание диалоговых окон без родителя, но тогда придется писать огромное количество кода, обеспечивающее нормальное функционирование окна.
Обычно родительским окном является то, оконная функция которого создает диалоговое окно. В программе основное окно программы является родительским по отношению к диалоговому окну.
И наконец, последний аргумент - указатель на функцию диалогового окна, т. е. на функцию, которая будет обрабатывать получаемые от элементов управления сообщения.
В дополнение к этому заметим, что последний аргумент функции DialogBoxParamQ - это какой-то параметр, определяемый программистом, который может быть передан функции диалогового окна.
Итак, мы рассмотрели функцию основного окна и остановились на функции окна диалога. На очереди - ее рассмотрение.
Диалоговая функция очень напоминает функцию окна, но имеет ряд отличий. Во-первых, обычная оконная функция возвращает значение типа LRESULT. Диалоговая функция возвращает значение типа BOOL. Во-вторых, обычная функция окна передает сообщения, обрабатывать которые не нужно, процедуре обработки по умолчанию (DefWindowProc()). Диалоговая функция в том случае, если она обработала сообщение, возвращает TRUE, а в противном случае FALSE. Другими словами, диалоговая функция должна вернуть FALSE в том случае, если ей необходимо передать сообщение для дальнейшей обработки в «недрах» Windows.
Для функции диалогового окна аналогом сообщения WM_CREATE является сообщение WM_INITDIALOG. Диалоговая функция получает его после создания диалогового окна в памяти, но до его отображения. Обычно именно при обработке этого сообщения производится инициализация диалога. Программа, которую мы сейчас разбираем, не является исключением. При обработке WM_INITDIALOG мы встречаемся с одним исключением из стройной системы правил Win32. Если программе необ-
114
ходимо, чтобы система продолжила обработку сообщения WIVMNITDIALOG, то после обработки этого сообщения программа должна вернуть TRUE. Рекомендую читателю немного поэкспериментировать с программой и попробовать после обработки WMJNITDIALOG вернуть вместо TRUE значение FALSE. Думаю, разница будет заметна сразу (по моему мнению, она бросается в глаза).
При обработке сообщения WM_DIALOG в программе производится установка состояний RadioButtons и CheckButtons. Здесь мы встречаемся еще с одной интересной функцией. Давайте вспомним, что любой элемент управления является окном. Для управления окнами используются сообщения, а для того, чтобы послать сообщение окну, нужно знать хэндл окна - адресата. При создании диалога мы определяли идентификатор элемента управления. Win32 позволяет определить хэндл окна -элемента управления. Для этого предназначена функция GetDlgltemQ, возвращающая искомый хэндл. Теперь мы можем послать сообщение окну с помощью функции SendMessageQ. Таким образом, нам нужно написать что-то типа SendMessage(GetDlgItem(...));
Для того чтобы облегчить жизнь программистам, в Win32 включена функция SendDlgltemMessageQ, объединяющая две упомянутые выше функции в одну. Прототип этой функции можно найти в файле winuser.h:
WINUSERAPI LONG WINAPI SendDlgItemMessageA(HWND hDIg,
int nlDDlgltem, UINT Msg, WPARAM wParam, LPARAM IParam);
WINUSERAPI LONG WINAPI SendD!gItemMessageW(HWND hDIg,
int nlDDlgltem, UINT Msg, WPARAM wParam, LPARAM IParam);
tfifdefUNICODE
#define SendDlgltemMessage SendDlgltemMessageW
#else
#defme SendDlgltemMessage SendDlgltemMessageA
#endif //! UNICODE
Первый аргумент - хэндл диалогового окна, функция диалога получает его от системы при вызове. Второй аргумент - идентификатор элемента управления. Третий аргумент - посылаемое элементу сообщение. Для управления кнопками служат сообщения, идентификаторы которых начинаются с ВМ_. Все определенные в winuser.h идентификаторы приведены в табл 21.
115
Т а б л и ц а 21. Сообщения, посылаемые кнопкам
Идснтифнкачор |
Значение |
Описание |
ВМ_ОЕТСНЕСК. |
OxOOFO |
Получить состояние отметки CheckBox'a или |
|
|
RaJioButton'a |
ВМ SETCHECK |
OxOOFl |
Установить отметку в CheckBox'a или RadioButton'a |
ВМ GETSTATE |
OxOOF2 |
Получить состояние кнопки |
BM_SETSTATE |
ОхООРЗ |
Установить состояние подсветки кнопки (имитация |
|
|
удержания нажатой клавиши мыши на кнопке) |
ВМ SETSTYLE |
OxOOF4 |
Изменить стиль кнопки |
BPv-fcLICK |
OxOOF5 |
Симуляция нажатия кнопки мыши |
BM_GETIMAGE |
OxOOF6 |
Получить хэндл изображения (иконки или bitmap'a), |
|
|
связанною с кнопкой |
BM_SETIMAGE |
OxOOF? |
Связать изображение с кнопкой |
Идентификаюр |
Значение |
Описание |
BST_ UNCHECKED BST_CHECKED BSTJNDETERMINATE BST PUSHED BST_BST_FOCUS |
0x0000 0x000 1 0x0002 0x0004 0x0008 |
Checkbox или RadioButton делается неотмеченной (может устанавливаться) ChcckBox или RadioButton делается отмеченной (может устанавливаться) Состояние CheckBox не определено (может устанавливаться) Кнопка нажата (только в ответ на запрос о состоянии кнопки) Кнопка имеет клавиатурный фокус (только в ответ на запрос о состоянии кнопки) |
Четвертый и пятый аргументы - это wParam и IParam посылаемого сообщения. Для каждого сообщения они определяются отдельно. Надеюсь, что читатель разобереться с параметрами этих сообщений самостоятельно по файлам помощи. Состояния кнопок имеют идентификаторы, начинающиеся с BST_ (табл. 22).
После того, как мы разобрали функцию SendDigIteinMessage(), все остальное в программе вызывать каких-либо трудностей не должно.
Окно сообщений
Но возникает вопрос: неужели же даже для простейших действий, например, для запроса подтверждения о необходимости удаления файла, необходимо писать функцию диалогового окна? Ответ очевиден: нет, не нужно. Для решения этих вопросов вполне достаточно гак называемого окна сообщений.
116
Окно сообщений, которое мы неоднократно использовали, является простейшим типом диалогового окна. Его назначение определяется его названием. Ни для чего другого, кроме вывода сообщения пользователю и предложения нажать одну из имеющихся кнопок, эти окна не предназначены. Тем не менее, работу с этим типом диалогов я хотел бы рассмотреть очень подробно, так как она очень часто используется не только для выдачи сообщений пользователю, но и для отладки. Функция, с помощью которой создастся окно сообщений, называется MessageBoxQ.
В файле winuser.h эта функция описана следующим образом:
WINUSERAPI int WINAPI McssagcBoxA(HWND hWiid. LPCSTR IpTcxt,
LPCSTR IpCaption, UINT uTypc);
WINUSERAPI int WINAPI MessageBoxW(HWND liWnd . LPCWSTR IpText,
LPCWSTR IpCaption, UINT uTypeV
#ifdcfUNICODE
#define MessageBox MessageBoxW
#clse
#defmc MessageBox McssaucBoxA
#cndi(7/ IUNICODF.
Первый аргумент этой функции - liWnd - хэндл родительского окна, т. е. того окна, которому будут посылаться сообщения от окна сообщений (извините меня, уважаемый читатель, за тавтологию. В данном случае я прошу не путать окно сообщений ДЛЯ ПОЛЬЗОВАТЕЛЯ с сообщениями ДЛЯ ОКОН). Второй аргумент - IpText - указатель на строку, содержащую отображаемый внутри окна текст. Перед отображением этот текст может быть отформатирован с помощью функции sprintfQ. Третий аргумент - IpCaption -заголовок окна сообщений. (Мне, например, использующему окна сообщений в основном для вывода отладочных сообщений, нравится давать окну заголовки типа «Hurray!» или «At last...».) Четвертый аргумент - иТуре - определяет тип окна сообщений, т. е.: перечень кнопок, отображаемых в окне сообщений; иконку, отображаемую в окне сообщений; кнопку, считающуюся кнопкой по умолчанию; модальность окна сообщений.
Наверное, вы неоднократно видели на экране окно сообщений с различным набором кнопок. Этот набор состоит из кнопок «OK», «Retry», «Abort» и др. Наличие каждой из таких кнопок определяется флагами, установленными в четвертом аргументе. Возможные значения флагов иТуре можно найти в файле winuser.h. Все они начинаются с букв MB (табл. 23).
117
Таблица 23. Типы окон сообщений
Флаг |
Значение |
Эффект |
MB OK |
OxOOOOOOOOL |
Кнопка «OK» |
MB OKCANCEL |
0x0000000 1 L |
Кнопки «OK» и «Cancel» |
MB ABQRTRETRYIGNORE |
Ox00000002L |
Кнопки «Abort», «Retry», «Ignore» |
MB YESNOCANCEL |
Ox00000003L |
Кнопки «Yes», «No», «Cancel» |
MB YESNO |
0x000000041. |
Кнопки «Yes», «No» |
MB RETRYCANCF.L |
OxOOOOOOOSL |
Кнопки «Retry», «Cancel» |
Флаг |
Значение |
Эффект |
MB ICONHAND |
0x000000 10L |
Иконка с изображением знака «Stop» |
MBJCONQUESTION |
Ox00000020L |
Иконка с изображением вопроситель- |
|
|
ного знака |
MBJCONEXCLAMATION |
0x0000003 OL |
Иконка с изображением восклицатель- |
|
|
ного знака |
MB ICONASTERISK |
Ox00000040L |
Иконка с изображением буквы i |
MB ICONINFORMATION |
Ox00000040L |
Иконка с изображением буквы i |
MB ICONSTOP |
0x000000 10L |
Иконка с изображением знака «Stop» |
Флаг |
Значение |
Эффект |
MB DF.FBUTTON1 MB DEFBUTTON2 MB DEFBUTTON3 |
OxOOOOOOOOL 0x00000 100L Ox00000200L |
Первая кнопка работает но умолчанию Вторая кнопка работает по умолчанию Третья кнопка работает по умолчанию |
Т а б л и ц а 26. Идентификаторы, определяющие модальность окна сообщений
Флаг |
Значение |
Эффект |
|
MB |
_APPLMODAL |
OxOOOOOOOOL |
Разрешаются переключения на другие |
MB |
_SYSTEMMODAL |
0x00001000 |
приложения Не разрешаются переключения на другие |
MB |
TASKMODAL |
0x00002000 |
приложения Применяется в случае отсутствия роди- |
|
|
|
тельского окна для запрещения ввода в |
|
|
|
другие окна |
118
Т а б л и ц а 27. Значения, возвращаемые фунунией iVIessageBoxf)
Нажатая клавиши |
Числовое -jiia'K'Miic |
Возвращаемое функцией значение |
ок |
1 |
IDOK |
Cancel |
2 |
IDCANCEL |
Abort |
3 |
IDABORT |
Retry |
4 |
IDRETRY |
Ignore |
5 |
IDIGNORF, |
Yes |
6 |
IDYES |
No |
7 |
IDNO |
|
8 |
IDCLOSE |
|
9 |
IDHELP |
Следующие флаги определяют, какая из кнопок будет считаться кнопкой по умолчанию (табл. 25). Модальность окна сообщений определяют флаги, приведенные в табл. 26.
Ну, вот, кажется и все. Мне бы хотелось обратить внимание читателя на то, что комбинировать с помощью логических операций можно только величины из разных таблиц. Это заметно даже при просмотре численных значений. Что произойдет при комбинировании этом, известно только фирме Microsoft. Например, указав в порядке эксперимента одновременно флаги MB_RETRYCANCEL и MB_ABORTRETRYIGNORE, я вообще не получил никакой кнопки в окне сообщений. Пришлось завершать процесс аварийно.
Итак, выдавать сообщения мы научились. Не хватает самой малости, выяснить, как приложение узнает о том, какую кнопку нажал пользователь. Но здесь дело обстоит просто. Возвращаемое функцией MessageBoxQ значение напрямую определяется тем, какую кнопку нажал пользователь (табл. 27).
К сожалению, два последних значения, которые я нашел в заголовочном файле winuser.h, в файлах помощи фирмы Microsoft не описаны Об их назначении можно только догадываться по их названиям.
Мы закончим рассмотрение темы об окнах сообщений очередной демонстрационной программой. Для того чтобы увидеть, что возвращает функция MessageBoxQ, я воспользовался обычным окном сообщений из интегрированной среды Borland C++ 5,0. В демонстрационной программе я не стал мудрствовать лукаво и выдал на отображение олно-
119
единственное окно сообщений с одной иконкой и тремя кнопками, «Abort», «Retry» и «Ignore». При нажатии клавиш «Retry» и «Ignore» в окне Message появляются сообщения о том, какая клавиша нажата. При нажатии клавиши «Abort» работа программы прекращается:
#include <windows.h>
int WINAPI WinMain(HINSTANCE hlnstance.HINSTANCE hPrevInstance, LPSTR IpszCmdLine, int nCmdShow)
{
int nRcsult = IDRETRY; int nlndex; char* pcMessage[] {
"Retry key pressed", "Ignore key pressed"
while( (nRcsult = MessagcBox(NULL, "And what do you want to see?", "See in Message window, please", MB ABORTRETRYIGNORE | MBJCONASTERISK)) != IDABORT7)
{ switch(nResult)
{ case IDRETRY:
nlndex — 0;
break; case IDIGNORE:
nlndex = 1 ;
break;
} OutputDebugString(pcMessage[nIndex]);
} return 1;
На рис. 9 показано окно сообщений, создаваемое программой.
See in Message window, please
Рис. 9. Окно сообщений с тремя кнопками
120
Единственное, что осталось неясным в этой программе - это функция OutputDebugString(). Ее единственным аргументом является указатель на строку, которая должна быть передана отладчику. В случае отсутствия отладчика, содержимое строки появляется в окне сообщений интегрированной среды.
Мы разобрали вопросы о создании окна сообщений и о работе кнопок. Теперь я расскажу о таком мощном элементе, как окно списка. Наверное, это наиболее часто используемый элемент управления (не считая, конечно, кнопок), а возможности, которые список предоставляет программисту, иногда просто поражают. И это без учета комбинированных списков, всевозможных производных типа окон просмотра деревьев и т.д.
СПИСКИ
Окно списка - это совокупность строк и/или картинок, которые отображаются в скроллируемом окне. Список позволяет добавить элемент в список, удалить элемент из списка, найти элемент, получить общее число элементов и т. д. Образно говоря, список - это некое уменьшенное подобие базы данных, позволяющее выполнять большинство стандартных функций по управлению базой данных.
Все списки делятся на две большие группы. В первую группу входят списки, которые позволяют выбрать только один элемент из всех имеющихся. Вторую группу составляют списки, позволяющие выбрать одновременно несколько элементов, удовлетворяющих определенному критерию.
У списков есть еще одно очень важное свойство. С каждым элементом списка мы можем связать некоторый объект в памяти. Другими словами, то, что мы видим в списке на экране, может быть только вершиной айсберга. Сам айсберг, сиречь информация, связанная с элементом, может храниться глубоко в недрах Win32. Список не может быть очень большим (список, как и любая динамическая структура данных, хранится в памяти), но он может быть весьма удобным инструментом для создания и хранения небольших объемов данных. Кстати, для хранения данных можно создать окно списка, но на отображение его не выводить. В памяти будет создана динамическая структура, с которой удобно работать.
Список может быть создан вместе с диалоговым окном в качестве ресурса, а также посредством использования функции CreateWindow(). В последнем случае в качестве имени класса необходимо указывать «LISTBOX». В подавляющем большинстве случаев списки создаются в ресурсах, поэтому мы остановимся именно на этом способе. При работе в редакторе ресурсов ничего сложного в создании списка нет. Некоторые
121
сложности возникают при создании ресурса в текстовом редакторе, jio все эти сложности преодолимы. Формат описания окна списка в файле ресурсов ничем не отличается от описания, которое мы использовали для описания кнопок. Дабы читателю не пришлось разыскивать это описание, я приведу его еще раз:
CONTROL «Заголовок», ListboxID, «listbox», styles, X, Y, Width,
Height
Здесь я заменил класс «button» на класс «listbox», ибо именно к этому классу относятся списки. Возможные стили списков мы получим, обратившись к файлу winuser.h (табл. 28).
Все сообщения, с которыми работают списки, можно разделить на несколько логических групп. Постараюсь описать три группы, в которые я включил сообщения, представляющиеся мне наиболее важными.
Сообщения, обеспечивающие добавление и удаление элемента
Для того чтобы добавить элемент в список, необходимо просто послать списку сообщение LB ADDSTRING. При этом wParara должен быть равным нулю, a IParam должен указывать на добавляемый к списку объект. Этот элемент совсем необязательно должен быть строкой. Если у списка не установлен стиль LBS_HASSTRING, то IParam, указывает на объект, связанный с элементом. Для того чтобы получить или изменить эти данные, можно воспользоваться сообщениями LB_GETITbMDATA и LB_SETITEMDATA.
Если у списка установлены стили LBS^SORT и LBS HASSTRING, то строка добавляется в список, после чего происходит сортировка. Если стиль LBS SORT не указан, строка добавляется в конец списка. Если, наоборот, указан стиль LBS SORT, но не указан LBS HASSTRING, то список посылает родительскому окну одно или несколько сообщений WM_COMPAREITEM, которые позволяют определить, где должен быть расположен включаемый элемент. Возвращает это сообщение либо номер, под которым элемент включен в список, либо одно из двух значений, говорящих об ошибке: LB_ERR - встретилась ошибка; LB_ERRSPACE - не хватило памяти для размещения элемента.
Элемент может быть добавлен в список и другим способом. Отличие сообщения LBJNSERTSTRING от предыдущего состоит в том, что wParam этого сообщения содержит номер (считается от нуля) элемента, ПОСЛЕ которого нужно включить данный элемент. Кроме этого, сортировка элементов в этом случае не производится. Возвращаемые значения точно такие же, как и в предыдущем случае.
122
Особо нужно рассмотреть случай, когда необходимо создать список файлов в текущей директории, Для того чтобы облегчить жизнь программисту, в систему было включено сообщение LB_DIR. В качестве wParam этого сообщения записываются атрибуты файлов, имена которых необходимо добавить в список. Возможные значения этого параметра приведены в табл. 29.
Т а б л и ц а 28. Стили окон списков
Флаг'
Значение
Описание
LBS NOTIFY
LBS SORT
LBS NOREDP.AW
LBS MULTIPLESEL
LBS OWNERDRAWFIXED
LBS OWNERDRAWVARIABLE
LBS HASSTRING LBSJJSETABSTOPS
LBS NOINTEGRALHEIGHT
LBS_MULTICOLUMN LBSJVVANTKEYBOARDINPUT
LBS_EXTENDEDSEL LBS DISABLENOSCROLL
LBSJMODATA LBS_NOSEL
LBS STANDARD
0x0001L
Ox0002L Ox0004L
Ox0008L 0x001OL
Ox0040L Ox0080L
OxOIOOL
Ox0200L Ox0400L
OxOSOOL OxlOOOL
0x20001 Ox4000L
Посылает сообщение родительскому окну о щелчке или двойном щелчке клавишей мыши
Строки сортируются по алфавиту Внешний вид списка не изменяется даже тогда, когда производятся изменения Список позволяет множественный выбор Родительское окно ответственно за прорисовку элементов, все -элементы списка одинаковой высоты То же, что и предыдущее, но элементы списка могут быть разной высоты Элементы списка - строки Разрешает расширять символы табуляции, встречающиеся в строках Список создается точно такого же размера, который указан в программе, выравнивание не производится
В списке создается несколько колонок, он скроллируется по горизонтали Позволяет приложению обрабатывать ввод с клавиатуры тогда, когда список удерживает фокус ввода
Позволяет списку с множественным выбором использовать для выделения клавишу Shift совместно с мышью или другие клавиатурные комбинации Показывать запрещенную линейку прокрутки тогда, когда в списке недостаточно элементов для прокрутки Устаревший стиль
Элементы списка видны, но выделение запрещено
LBSJvlOTIFY | LBS_SORT | WSJVSCROLL | WS_BORDER
123
Таблица 29. Атрибуты файлов, добавляемых в окно списка
Параметр |
Значение |
Описание |
DDL_READWRITE |
0x0000 |
Включить только файлы, доступные для чтения и |
|
|
•записи, без дополнительных атрибутов |
DDLJIEADONLY |
0x000 1 |
Включить в список только файлы, доступные для |
|
|
чтения |
DDL HIDDEN |
0x0002 |
Включить в список скрытые файлы |
DDL SYSTEM |
0x0004 |
Включить в список системные файлы |
DDL DIRECTORY |
0x00 1 0 |
Включить в список поддиректории |
DDL ARCHIVE |
0x0020 |
Включить в список архивные файлы |
DDL POSTMSG |
0x2000 |
|
DDL DRIVES |
0x4000 |
Включить в список имена дисководов |
DDL EXCLUSIVE |
0x8000 |
Включать в список файлы только с указанными |
|
|
атрибутами |
IParam сообщения LB_DIR указывает на строку, которая определяет, какие файлы необходимо добавить в список. Строка формируется по правилам, принятым ещё в MS DOS, то есть, к примеру, для того, чтобы отобразить все файлы в директории MyDir на диске С: необходимо записать «c:\VMyDir\\*.*»
Удаление элемента из списка производится посредством посылки списку сообщения LB DELETESTRING. В wParam этого сообщения необходимо указать номер удаляемого элемента. При анализе возвращаемого значения необходимо учесть, что при нормальном удалении возвращается число оставшихся элементов списка. Значение LB ERR должно указать программисту на то, что он неверно указал номер удаляемого элемента.
Вторая большая группа сообщений - это
Сообщения, обеспечивающие навигацию в списке
Под навигацией в списке я понимаю возможность программы определить, где ешходится указатель (выделенный элемент) списка и/или установить указатель на тот элемент, который в данный момент требуется программе. Возможности здесь достаточно обширные.
Наверное, наиболее часто для определения места выделенного элемента в списке будет использоваться сообщение LB_GETCURSEL. Никаких параметров это сообщение не использует, и wParam, и IParam должны быть равны 0. Если возвращаемое значение равно LB_ERR, в списке нет выделенных элементов.
Сделать элемент выделенным позволяет сообщение LB_SETCURSEL, wParam которого должен содержать номер элемента, который должен
124
стать текущим. Это сообщение имеет дело только со списками, позволяющими одиночный выбор.
Узнать, какая строка или какие данные хранятся в элементе списка, можно с помощью сообщения EB_GETTEXT. wParam должно хранить индекс интересующего нас элемента, a IParam должно указывать на буфер, в который будут записаны строка или указатель на ассоциированные данные.
Число элементов в списке может быть определено посредством сообщения LB GETCOUNT. Параметры этого сообщения не используются и должны быть установлены в 0, а возвращает оно число элементов в списке. Одна тонкость - число элементов всегда на 1 больше индекса последнего элемента списка. Например, в списке один элемент. Его помер будет равным нулю, но EB_GETCOUNT вернет 1.
И последней группой сообщений, на которых мы остановимся, являются
Нотификациопные сообщения
Если у списка установлен стиль EBS_NOTIFY, то список будет оповещать родительское окно о том, какие события с ним произошли посредством нотификационных сообщений. Нотификационные сообщения в случае списка - это сообщения WM_COMMAND, у которых младшее слово wParam содержит идентификатор окна списка, старшее слово wParam - код нотификации, a IParam - хэндл окна списка.
Кодов нотификации всего шесть (табл. 30).
Таблица 30. Коды нотификационных сообщений, посылаемых окнами списков
Код нотификации |
Описание |
LBN ERRSPACE LBN SELCHANGE LBN DBLCLK LBN SELCANCEL LBN SETFOCUS LBN KILLFOCUS |
Не хватает памяти Выделенным стал другой элемент Пользователь сделал двойной щелчок клавишей мыши Пользователь снял выделение Список получил клавиатурный фокус Список потерял клавиатурный фокус |
125
Для нормальной компиляции программы требуется файл ресурсов:
#include "list.h"
ListBox DIALOG 50, 50, 150, 140
STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP |
WS_POPUP | WS_VTSIBLE WS_CAPTION WS_SYSMENU CAPTION "ListBox Example" FONT 8, "MS Sans Serif
{
PUSHBUTTON "Display", ID_OK, 15, 100, 50, 14
PUSHBUTTON "Cancel", ID_Cancel, 85, 100, 50, 14
CONTROL "Families", ID_MyListBox, "listbox", LBS_STANDARD |
WS_CHILD | WS_VISIBLE | WS_TABSTOP, 15, 16, 120, 65 CONTROL "Families", -1, "static", SSJLEFT | WS_CHILD | WS_VISIBLE, 16, 6,
120, 10 CONTROL "StatusBar", ID_StatusBar, "msctls_statusbar32", 3 | WS_CHILD
WS_VISIBLE, 0,129, 150,12
ListBoxMenu MENU
{ POPUP "&File"
{ MENUITEM "E&xit", IDM_Exit
} MENUITEM "&Display Dialog", IDM_DisplayDialog
}
Далее следует файл заголовков, также используемый в программе:
#defme IDM_Exit 101
#defme IDM_CanceI 102
#define IDM_DisplayDialog 103
#defmeID_OK 201
#defme ID_Cancel 202
«define ID_MyListBox 203
#defme ID_StatusBar 204
И, наконец, основной файл программы:
#include <windows.h>
#include "list.h" ^include <commctrl.h>
HINSTANCE hlnst;
LRESULT CALLBACK ListBoxExampleWndProc(HWND, U1TMT, UINT, LONG);
126
BOOL CALLBACK ListBoxExampleDialogProc(HWND, UINT, WPARAM,
LPARAM);
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrevInstance,
LPSTR IpszCindParam, int nCmdShow ) {
HWND hWnd ; WNDCLASS WndClass ; MSG Msg; char szClassName[] = "ListExample";
hlnst= hlnstance;
InitCommonControls(); /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style - CS_HREDRAW | CS_VREDRAW;
WndClass.IpfnWndProc = ListBoxExampleWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hlnstance = hlnstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION),
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
WndClass.IpszMenuName = "ListBoxMenu";
WndClass.IpszClassName = szClassName;
if ( !RegisterClass(&WndClass)) !
MessageBox(NULL,"Cannot register class","Error",MB_OK); return 0; } hWnd = CreateWindow(szClassName, "ListBox Example Program",
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CWJJSEDEFAULT, CWJJSEDEFAULT, NULL, NULL, hlnstance.NULL); if(!hWnd) {
MessageBox(NULL,"Cannot create window","Error",MBJ3K); return 0;
/* Show our window */
ShowWindow(hWnd,nCmdShow);
UpdateWindow(hWnd);
/* Beginning of messages cycle */
while(GetMcssage(&Msg, NULL, 0, 0))
TranslatcMessage(&Msg);
127
DispatchMessage(&Msg);
} return Msg.wParam;
LRESULT CALLBACK ListBoxExampleWndProc(HWND liWnd, UINT Message,
UINT wParam, LONG IParam )
{
switch(Message) !
case WM_COMMAND: switch(LOWORD(wParam))
{ case IDM_DisplayDialog:
DialogBox(hInst, "ListBox", hWnd, ListBoxExampleDialogProc);
break; case IDM_Exit:
SendMessage(hWnd, WM_CLOSE, 0, 0);
break;
}
break;
case WM JDESTROY: PostQuitMessage(O); return 0;
v i
return DefWindowProc(hWnd, Message, wParam, IParam);
BOOL CALLBACK ListBoxExampleDiaIogProc(HWND hDlg, UINT Message,
WPARAM wParam, LPARAM IParam)
{
int i; // 1 like «Ivanov, Petrov, Sidorov ...» TV-program. ©
LPSTR pszltems[12] = {"Berdiev", "Vasilkov", "Ivanov", "Petrov", "Sidorov", "Johnson", "Jackson", "Tompson", "Pererepenko", "Khabibullin", "Novozhenov", "Mamedov"};
char cMessage[36] = "Message about ";
char cltem[12];
static HWND hListBox;
switch(Message)
{
case WMJNITDIALOG: hListBox = GetD!gItem(hDlg, ID_MyListBox); for(i = 0;i< 12;i++) SendMessage(hListBox, LB^ADDSTRING, (WPARAM) 0, (LPARAM)
pszltems[i]); return TRUE;
128
case WM_COMMAND: switch(LOWORD(wParam)) {
case ID_MyListBox:
if(HIWORD(wParam) == LBN_SELCHANGE) { SendMessagc(hListBox, LB^GETTEXT, SendMessage(hListBox,
LB_GETCURSEL, 0, 0), (LPARAM) cltcm); strcpy(cMessage + 14, cllcm); SendDlgItemMessage(liDlg, ID_StatusBar, SB_SETTEXT,
(WPARAM) 0, (LPARAM) cMessage); }
break;
case ID_Cancel: EndDialogfliDlg, 0); break; } break;
) return FALSE;
В этой программе основное меню окна предлагает отобразить диалог. При запуске диалога (при обработке сообщения WMJNITDIALOG) производится заполнение списка фамилиями. При переносе выделения с выбранного элемента окна списка на другой элемент в строке состояния отображается выбранная фамилия. Таким образом, я продемонстрировал заполнение списка и выборку информации из него, а также работу по обработке нотификашюнного сообщения. Вид создаваемого программой окна со списком показан на рис. 10.
ListBox Example
_%«! • families1
Berdiev Ivanov Jackson
Johnson
Mamedov Novozhenov
[Message-about КЬаЪ1Ь|Ш1Ш№Ш;Ь:-
Рис. IO. Диалоговое окно, содержащее окно списка
129
Следующим шагом в изучении органов управления, применяющихся в диалоговых окнах, является изучение достаточно интересного элемента, который называется
ОКНО РЕДАКТИРОВАНИЯ
Окно редактирования - это один из наиболее сложных (с точки зрения реализации, а не использования) и наиболее интересных элементов управления. Фактически этот элемент представляет собой небольшой текстовый редактор, который позволяет вводить текст, редактировать его, копировать в буфер, вставлять из буфера и т. д. Окна редактирования могу быть однострочными и многострочными. Однострочные окна редактирования обычно используются для ввода небольших элементов текста. Например, в Program Manager'e для запуска программы пользователю необходимо ввести имя и командную строку этой программы. Примером редактора, большую часть функциональности которого обеспечивается за счет многострочного окна редактирования, является Notepad, который поставляется со всеми версиями Windows.
Окно редактирования можно создать как в файле ресурсов, так и как отдельное дочернее окно, указав при этом предопределенный класс «edit». В этом разделе мы разберем оба случая использования окна редактирования .
Перед тем как начать изучение, давайте вспомним, что все поведение элемента управления зависит от того, какие стили мы укажем при его создании. Все стили окна упомянуты в winuser.h. Все они начинаются с букв ES_. Эти стили приведены в табл. 31.
Таблица 31. Стили окна редактирования
Стиль |
Значение |
Описание |
esj.eft |
OxOOOOL |
Текст в окне редактирования выравнивается по |
|
|
левому краю |
F.SJTENTFR |
0x000 IL |
Текст в окис редактирования выравнивается по |
|
|
правому краю |
HS RIGHT |
0x00021. |
Текст в окне редактирования выравнивается по центру |
ES MULTILINE |
Ox()004L |
Создается многострочнос окно редактирования |
es uppercase |
0x00081. |
Вводимый текст преобразуется в прописные буквы |
F.S LOWERCASF. |
0x00 101. |
Вводимы:'] текст преобразуется в строчные буквы |
FS PASSWORD |
0x0020!. |
Все вводимые символы отображаются в виде звез- |
|
|
дочек |
130
Окончание табл. 31
(л и .'1 ь |
Значение |
Описание |
ES_AUTOVSCROLL |
Ox0040L |
При необходимости текст в многострочном окне |
ES_AUTOHSCROLL |
OxOOSOL |
редактирования скроллируется по вертикали При необходимости текст в окне редактирования |
ES_NOHIDFSF.L |
0x0 100 1. |
скроллируется по горизонтали При потере окном редактирования фокуса ввода |
|
|
выделение с текста не снимается |
ES_OEMCONVERT |
0x04001. |
Вводимые символы из одного набора преобразуются |
ES_READONEY |
OxOSOOL |
в символы из друг ого наоора Текст в окне редактирования можно только просмат- |
ESJWANTRETURN |
Ox 1 OOOL |
ривать, но не редактировать При нажатии клавиши Enter в многострочном окне |
ESJNUMBER |
0x20001, |
система вставляет в текст символ возврата каретки Разрешается осуществлять ввод только цифр |
#incl''dc <windo\vs.h>
#definc ID Edit 101
HINSTANCE Must;
ERESUET CALLBACK EditDemoWndProc ( HWND, UINT, U1NT, EONG );
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrevInstance,
LPSTR IpszCmdParam, int nCmdShow )
i i
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msi-
char szClassName[] - "EditDemo";
hlnst — hlnstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.slyle - CSJIREDRAW | CS VREDRAW;
WndClass.lpfnWndProc = EditDcmoWndProc;
WndClass.cbClsExtra - 0;
WndClass.cbWndExtru ---- 0;
WndClass.hlnstance = hlnstance ;
WndClass.hlcon ----- Loacllcon (NULEJDI APPLICATION),
131
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW); WndClass.librBackground - (HBRUSH) GctStockObject (WHITE_BRliSH); WndClass.lpszMcnuNamc - NULL; WndClass.lpszClassNamc ~ sxClassName;
if ( !RegisterClass(&WndClass) )
f
MessageBox(NULL,"Cannot register class", "Error", MB_OK); return 0;
} liWud = C'reateWmdow(szClassName, "EditDemo",
WS_OVERLAPPEDWINDOW, CWJJSEDEFAULT.
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL,
hlnstance.NULL); if(!hWnc!)
t MessageBo,x(NULL,"Cannot create window ', "Error", MBJDK);
return 0;
/* Show our window */
ShowWindow(hWnd,nCmdSliow);
UpdateWindow(hWnd); /* Beginning of messages cycle */
while(GetMessage(&Msg, NULL, 0, 0))
!
TranslateMessage(&Msg);
DispatchMessagef&Msg); } return Msg.wParam;
LRESULT CALLBACK EditDemoWndProc (HWND hWnd, UINT Message,
UFNT wParam, LONG IParam )
'static HWND hEditWnd; RECT Rcct;
switch(Message)
!
case WMJTREATE:
GetClientRect(hWnd, &Rect);
hEditWnd = CreatcWindow("edit", NULL,
WS_CHILD | WSJVISIBLE | WS HSCROLL! WS VSCROLL| WS BORDER | ES_LEFT | ES MULTILLNE [ ES_AUTOHSCROLL | ES^AUTOVSCROLL, 0, 0, 0, 0,
132
hWnd,
(HMENU) ID_Edit, hlnst, NULL); return 0; case WM_SIZE: MoveWindow(liEditWnd, 0, 0, LOWORD(lParam), HIWORD(IParam),
TRUE); return 0;
case WM_SETFOCUS: SetFocus(hEditWnd); return 0;
case WM_DESTROY: PostQuitMessage(O); return 0; } return DefWindowProc(hWnd,Message,wParam, IParam);
Вид окна, создаваемого программой, показан на рис. 11.
Эта программа создает окно редактирования, которое располагается поверх основного окна программы. Фактически в программе создается текстовый редактор, позволяющий осуществлять набор и редактирование текста, выделять части текста. Выделенные части текста могут быть перемещены в Clipboard посредством нажатия клавиш Shift-Delete, а после нажатия клавиш Shift-Insert текст из Clipboard'a может быть вставлен в окно.
В EditDemo
Это окно, представляющее собой простейший текстовый редактор, создано на основе многострочного окна редактирс ния.
.Этот текст выдепен и подготовлен для перемещения
ib 6i
Рис. 11. Окно редактирования с невыделенным и выделенным текстом
133
Управление окном редактирования, как и всех остальных элемен тов управления, осуществляется посредством посылки окну сообщений.
Перед тем, как начать рассмотрение сообщений необходимо отметить, что для нормальной работы часть текста в окне редактирования должна быть выделена. Операции производятся именно с выделенной частью текста. Копирование текста в окно редактирования осуществляется от места размещения текстового курсора.
Следующие пять сообщений не имеют параметров, а их действия очевидны из их названия:
WM_COPY - выделенная часть текста копируется в Clipboard;
WM_PASTE - содержимое Clipboard'a копируется в окно редактирования (данные вставляются только в том случае, если в Clipboard'e находится текст);
WM_CUT - выделенная часть текста удаляется из окна редактирования и помещается в Clipboard;
WM_CLEAR - выделенная часть текста удаляется из окна редактирования и не помещается в Clipboard;
WM_UNDO - отменяется последняя операция.
Для того чтобы получить границы выделения текста, необходимо использовать сообщение EM_GETSEL. Младшее слово возвращаемого значения содержит начальную, а старшее слово - конечную позицию выделения плюс 1. Другими словами,
DWORD dwPosition = SendMessage(hEditWnd, EMJ3ETSEL,
(WPARAM) 0, (LPARAM) 0); WORD wBcginPosition = LOWORD(dwPosition); WORD wEndPosition = HIWORD(dwPosition) - 1;
Если программе необходимо выделить часть текста, то она может использовать сообщение EM_SETCEL, в IParam которого необходимо указать начальную и конечную позиции выделения:
SendMessagefhEditWnd, EM^SETSEL, 0, MAKELONG(wBeginPosition, wEndPosition));
С помощью сообщения EM_REPLACESEL программа может заменить выделенный текст на другой:
SendMcssage(hEditWnd, EM_REPLACESEL, О, (LPARAM) pszNcwTcxt);
134
И наконец, посылка сообщения, с помощью которого приложение может скопировать в собственный буфер набранный пользователем текст, выглядит следующим образом:
SendMessagefhEditWnd, EM_GETLINE, (WPARAM) nLine, (LPARAM) (LPCSTR) pBufler);
В этом сообщении в качестве wParam указывается номер строки ( в случае однострочного окна номер строки игнорируется), а в качестве IParam - указатель на буфер, в который будет записана строка из окна редактирования.
На этом завершается рассмотрение стандартных элементов управления.
Должен заметить, что в Win32 включены новые, так называемые общие элементы управления (common controls). Многие из них уже были реализованы в приложениях, работающих в среде Windows 3.x, но до их документирования в Windows 3.x дело не дошло. Встречались попытки описать их реализацию в частности, в MSDN были описаны строка состояния, панель инструментов (toolbar), окно просмотра деревьев. Поэтому, наверное, можно сказать, что появление общих элементов управления ожидалось. И теперь мы приступаем к их изучению.
ОБЩИЕ ЭЛЕМЕНТЫ УПРАВЛЕНИЯ
Перед тем, как мы начнем изучение работы непосредственно элементов управления, мы должны научиться подключать библиотеку, реализующую эти элементы. Для нормальной работы программы с общими элементами управления необходимо выполнить два шага. Первым шагом является подключение к программе файла заголовков commctrl.h. Вторым шагом является подключение при линковашш непосредственно библиотеки. Она называется comctl32.dll.
Перед использованием любого из общих элементов управления необходимо загрузить эту библиотеку. Это делается с помощью функции InitCommonControls(), которая описана в файле commctrl.h как
void InitCommonContorls(void);
Эта функция не только загружает библиотеку общих элементов управления, но и инициализирует их подсистему. Обычно эта функция вызывается перед первым использованием одного из элементов.
135
Характерно, что все общие элементы управления являются дочерними окнами, т. е. они не могут создаваться и использоваться в качестве главного окна программы. Управление ими, как и обычными элементами управления, осуществляется посредством посылки им сообщений. Элементы управления информируют родительское окно о событиях, произошедших с ними, посредством передачи нотификационных сообщений.
Первым элементом управления, работу с которым мы рассмотрим, будет строка состояния (status bar). Ранее мы использовали ее в наших демонстрационных программах, но работу с ней не изучали.
РАБОТА СО СТРОКОЙ СОСТОЯНИЯ
Для того чтобы отобразить информацию о текущем состоянии программы, выполняемых операциях и режимах, в программе может использоваться элемент управления, который называют окном или линейкой состояния (status bar). Мне кажется более удачным термин «строка состояния». Включение окна состояния в программу в значительной степени изменяет внешний вид окна и позволяет создать более понятный и удобный для пользователя интерфейс.
Строка состояния может быть включена в описание диалогового окна в файле ресурсов. Но так как окно состояния является стандартным окном, то, естественно, что для его создания могут быть использованы функции и стандартные функции CreateWindowQ и CreateWindowExQ. При этом в качестве имени класса окна необходимо задать макро «STATUSCLASSNAME». В зависимости от того, какую систему кодировки использует прснрамма, можно воспользоваться также и «истинным» именем класса (msctls_statusbar32). В commctrl.h эти макро и имена описаны следующим образом:
#ifdcf_WIN32
«define STATUSCLASSNAMEW
«define STATUSCLASSNAMEA
«ifdefUNICODE
«define STATUSCLASSNAME
«else
«define STATUSCLASSNAME
#cndif
#else
«define STATUSCLASSNAME
«endif
L"msctls_statusbar32" "msctls statusbar32"
STATUSCLASSNAMEW STATUSCLASSNAMEA
"msctls statusbar"
136
Тем не менее, для создания окна состояния предусмотрена и отдельная функция CreateStatusWindowQ. В файле commctrl.h эта функция определяется так:
WINCOMMCTRLAPI HWND WINAPI CreateStatusWindowA(LONG style,
LPCSTR IpszText, HWND hwndParent, UINT wID);
WINCOMMCTRLAPI HWND WINAPI CreatcStatusWindowW(LONG style,
LPCWSTR IpszText, HWND hwndParent, UINT wID);
#ifdcf UNICODE
«define CreateStatusWindow
«else
«define CreateStatusWindow
«cndif
CrcateStatusWindowW
CreateStatusWindowA
Как можно узнать из описания функции, она требует передачи ей четырех аргументов. Первый аргумент, style, должен определять стиль создаваемого окна. У строки состояния есть единственный собственный стиль, SBARS_SIZEGRIP, который позволяет в правый угол строки состояния добавить «ручку» (внешне, честно говоря, на ручку это совсем не похоже) для изменения ее размеров. Но наличие у строки состояния единственного стиля не мешает комбинировать его со стандартными стилями окон, например с WS^CHILD и WSJVISIBLE.
Второй аргумент - IpszText - является указателем на строку, которая будет отображена в строке состояния сразу после ее создания. Ничего особенного здесь нет.
Третий аргумент - hwndParent - тоже не требует особых объяснений. Он является хэндлом родительского окна строки состояния.
Наконец, четвертый аргумент - uID - идентификатор окна состояния.
Попробуйте вызвать эту функцию (не забудьте про InitCommonControls()!) - и вы увидите, что в окне появилась строка состояния с текстом, определенным во втором аргументе этой функции.
После того, как строка состояния прорисована, иногда бывает необходимо разделить ее на несколько панелей для того, чтобы в каждой панели отображать информацию, логически не связанную с отображаемой в других панелях. Для того чтобы сделать это, необходимо послать строке состояния сообщение SB_SETPARTS. При этом wParam этого сообщения должен определять число панелей, a IParam должен содержать указатель
137
на массив целых чисел (число элементов массива должно быть равно wParam). Каждый элемент в этом массиве должен определять позицию (в координатах родительского окна) правой границы соответствующей части. Если элемент равен -I, то границей панели считается правая граница строки состояния. В случае успешного завершения операции функция, с помощью которой послано сообщение, возвращает TRUE. Значение FALSE должно заставить программиста поискать ошибку в собственной программе.
Приложение может определить число панелей, на которые разделена строка состояния, и их координаты с помощью сообщения SB_GETPARTS. Если IParam этого сообщения равен нулю, то функция, пославшая сообщение, возвращает число панелей строки состояния. При этом значение wParam роли не играет. Для того чтобы получить координаты панелей, wParam должен определять число панелей, для которых нужно получить координаты, a IParam должен указывать на массив целых чисел, в который будут записаны эти координаты. В этом случае функция также возвращает число панелей. Если при обработке сообщения произошла какая-то ошибка, то функция возвращает нулевое значение.
Итак, строка состояния сформирована. Но зачем она нужна без отображенной информации? Для того чтобы отобразить определенный текст в строке состояния, нужно послать ей сообщение SB SETTEXT. В качестве wParam этого сообщения используется результат логического сложения двух величин. Первая (iPart) - номер (считая от нуля) панели, в которой необходимо отобразить текст. Вторая (иТуре) определяет, как будет выглядеть текст. В качестве иТуре могут быть использованы значения, приведенные в табл. 32.
Таблица 32. Возможные типы строки состояния
Тип |
Значение |
Описание |
|
|
0x0000 |
Текст кажется вдавленным в панель |
|
SBT_NOBORDERS |
0x0 I 00 |
Панель прорисовывается без ограничительны |
\ |
|
|
линии |
|
SBT POPOUT SBTJITLREADING |
0x0200 0x0400 |
Панель прорисовывается выпуклой Используется для языков, в которых чтение и |
дет |
SnT_OWNERDRAW |
Ох 1 000 |
справа налево, как, например, в арабском За прорисовку панели отвечает родительское |
окно |
138
IParam сообщения должен содержать указатель на строку, которую необходимо отобразить в панели строки состояния.
А теперь, для того, чтобы прочитать текст в панели, необходимо строке состояния послать сообщение WM_GETTEXT. wParam этого сообщения должен содержать номер панели, a IParam - указатель на строку, в которую будет записан текст, содержащийся в панели.
Это основные сообщения, используемые при работе со строкой состояния.
Для иллюстрации некоторых возможностей строки состояния, о которых я только что рассказал, рассмотрим несколько измененную программу из раздела о кнопках. Из-за мелких отличий не буду приводить текст программы целиком, а приведу только текст диалоговой функции:
BOOL CALLBACK ButtonsExampleDialogProc(HWND liDIg,
UiNT Message, WPARAM wParam, LPARAM IParam)
int i;
char cMyMessage[80];
RECT Rect;
int nBorders[3];
switch(Message)
case WMJNITDIALOG: // Set states of controls
SendDIgItemMessage(hDlg, nRadioButtonld, BM^SETCHECK,
BST_CHECKED, 0);
for(i - IDC_CHECKBOXI; i <= IDC_CHECKBOX3; i++) if(uCheckBoxesState[i - 208])
SendDlgItemMessage(hDlg, i, BM_SETCHECK, BST_CHECKED, 0); GetClientRect(hDIg, &Rect); nBorders[0] = Rect.right / 3; nBorders[l]-Reel.right/3 * 2; nBorders[2] = -1; SendDlgItemMessage(hDlg, IDC_STATUSBAR, SB_SETPARTS, 3,
(LPARAM) nBorders); return TRUE; case WM__COMMAND: switch(LOWORD(wParam))
case IDCJIADIOBUTTONI: case IDC_RADIOBUTTON2: case IDC_RADIOBUTTON3:
sprintf(cMyMessage,"RadioButton%d", LOWORD(wParam) - 203);
SendDlgItemMessage(hDlg,IDC_STATUSBAR,SB_SETTEXT, (WPARAM) 0, (LPARAM) cMyMessage);
139
CheckMenuRadioItem(GetSubMenu(GetSubMenu(GetMenu(hWnd), 1), 0), IDM_RadioButtonl,IDM_RadioButton3, LOWORD(wParam) - 102, MF_BYCOMMAND); return FALSE; caseIDC_CHECKBOXl: case IDC_CHECKBOX2: case IDC_CHECKBOX3:
sprintf(cMyMessage,"CheckBox%d", LOWORD(wParam) - 207); SendDlgItemMessage(hDlg,IDC_STATUSBAR,SB_SETTEXT,
(WPARAM) 1, (LPARAM) cMyMcssage); i = LOWORD(wParam) - 208; uCheckBoxesStatc[i] = uChcckBoxesStale[i] == MF_CHECKED ?
MFJJNCHECKED : MF_CHECKED;
CheckMenuttem(GetSubMenu(GetSubMenu(GetMenu(hWnd), I), 1), LOWORD(wParam) - 103, uCheckBoxesStatc[i]); return FALSE; caseIDC_BUTTONl:
SendDlgItemMessage(hDlg, IDC_STATUSBAR, SB_SETTEXT, (WPARAM) 2, (LPARAM) "PushButton"); return TRUE; case IDC_BUTTON2; // Save the state of RadioButtons
i = IDC_RADIOBUTTON 1; while(!SendDlgItemMessage(hDlg, i, BM_GETCHECK, 0, 0))
i++;
nRadioButtonld = i; // Save the state of CheckButtons
for(i = IDC_CHECKBOXl; i <= IDC_CHECKBOX3; i++) uCheckBoxesState[i - 208] = SendDlgItemMessage(hDlg, i, BM_GETCHECK, 0, 0) == 0 ? MFJJNCHECKED : MF_CHECKED; EndDialog(hDlg,0); return TRUE; } break;
} return FALSE;
Отличия, внесенные в эту функцию, весьма невелики. Во-первых, строка состояния делится на три панели, и, во-вторых, каждый кластер объектов управления отображает сообщения в своей панели. Как видите, уважаемый читатель, ничего сложного здесь нет. Вид диалогового окна, создаваемого программой, показан на рис. 12.
Сравните внешний вид строки состояния, приведенной на рисунке, и строки состояния из раздела о кнопках.
140
Buttons Example
-^^fffpel-r
.. ...„. ...
Q-"R"a'df6eutton2®,
'
IBadioButtpnS.
Рис. 12. Диалоговое окно со строкой состояния
РАБОТА СО СПИНОМ
Иногда в приложениях встречаются ситуации, в которых полосы прокрутки (скроллирования) не нужны, достаточно только кнопок «вверх» и «вниз». Ярким примером такой ситуации может служить окно, открываемое в WinWord'e для Windows'95 при необходимости определить параметры страницы. Естественно, полоса прокрутки для того, чтобы чуть увеличить значение размера бумаги или сделать поля поменьше, не нужна. Для подобных случаев в Win32 предусмотрен новый элемент управления, называемый спином. Спин является особым видом линейки прокрутки и состоит только из кнопок со стрелками, которые находятся на концах линейки, и не включает линейки прокрутки. Обычно спин используется в одном из двух вариантов. Во-первых, он может применяться а 1а маленькая линейка прокрутки. В этом случае его называют up-down control'oM. Во-вторых, часто он используется в сочетании с другим элементом управления, называемым в этом случае buddy window (приятельским окном). Как правило, этим приятельским окном оказывается окно редактирования. Кстати, в приведенном ниже примере именно окна редактирования и оказываются приятельскими окнами. В этом случае элемент управления называется spin'oM. В данном разделе под словом «спин» будем понимать как up-down control, так и собственно спин.
Спин может создаваться и в составе диалогового окна в файле ресурсов, и как обычное окно посредством использования функций или _..„.. .....__.,_..,ч. При этом в качестве имени класса
. Соответствующее описание
шдом()
[ЧеобходймЬ указывать можно встретить в
сошшсгц-и:
141
#ifdef JWIN32
#defme UPDOWN_CLASSA
#define UPDOWN_CLASSW
#ifdefUNICODE
#dcfine UPDOWN_CLASS
#elsc
«define UPDOWN_CLASS
#endif
#else
#define UPDOWN_CLASS
#endif
"msctls_updown32" L"msctls_updown32"
UPDOWN_CLASSW UPDOWN_CLASSA
"msctls_updown"
Тем не менее, для создания спина создана специальная функция CreateUpDownControlQ, описание которого мы можем найти в файле commctrl.h:
WINCOMMCTRLAPI HWND WINAPI CreateUpDownControl(
DWORD dwSlylc, int x, int у, int ex, int cy, HWND hParent. int nID, HINSTANCE hlnsl, HWND hBuddy, int nUpper, int nLower, int nPos);
Таблица 33. Стили спина
Стиль
Значение
Описание
UDS WRAP
UDS_SETBUDDYINT UUS_ALIGNRIGHT
UDS_ALIGNLEFT UDS_AUTOBUDDY
UDS_ARROWKEYS
UDDS_MORZ
UDS NOTHOUSANDS
0x0001
0x0002 0x0004
0x0008 0x0010
0x0020
0x0040 0x0080
При достижении максимальной позиции отсчет начинается вновь с минимальной позиции и наоборот.
У спина есть приятельское окно Спин размещается справа от приятельского окна
Спин размещается слева от приятельского окна При изменении позиции спина текст в приятельском окне меняется автоматически Разрешается использование клавиатур1»1 для изменения текущего состояния спина Спин располагается горизонтально Не отображать запятую для разделения классов в числах, отображаемых в приятельском окне
142
Таблица 34. Сообщения, посылаемые спину
Сообщение
Значение
Описание
UDM_SETRANGE UDM_GETRANGE
UDM_SETPOS UDM_GETPOS
UDM_SETRUDDY UDM GETBUDDY
WMJJSER-HOI WMJJSER+102
WMJJSER+103 WMJJSER+I04
WM_USER+l05 WM USER+I06
Установка диапазона прокрутки, wParam = О, min значение - в старшем слове IParam, max значение - в младшем слове IParam Получение диапазона прокрутки, wParam и IParam должны быть равны 0, возвращает max значение в младшем, a min значение - в старшем слове возвращаемого значения Установка текущей позиции спина, wParam~0, IParam - значение новой позиции Получение текущей позиции спина, wParam и IParam должны быть равны 0, возвращает текущую позицию в младшем слове во (вращаемого значения
Определение приятельского окна, wParam хэндлу приятельского окна, IParam - О, возвращает хэндл бывшего приятельского окна
Получение хэндла приятельского окна, wParam и IParam должны быть равны О, возвращает хзндл приятельского окна в младшем слове возвращаемого значения
Параметр dwStyle определяет стиль окна. Все стили, разработанные специально для спина, начинаются с UDS_. Они приведены в табл. 33.
Как и всегда, для управления спином используются сообщения. Обычно при нажатии одной из стрелок спина родительскому окну посылается сообщение WM_VSCROLL. При этом IParam этого сообщения содержит хэндл спина. При обработке этого сообщения необходимо быть аккуратным и ire забывать, что сообщения WM VSCROLL могут поступать и от других элементов управления.
Остальные сообщения, используемые спином, приведены в табл. 34.
Для того чтобы проиллюстрировать возможности спина, рассмотрим небольшую демонстрационную программу. Для работы этой программы вам потребуется файл описаний, приведенный ниже:
#defmeIDM_F.xit l()l
#deime IDM_Pialog 102
#defineIDM About 103
^define ID OK 104
#define IDJIdit 105
fdefine ID Spin 106
143
Кроме этого, ниже приведен файл ресурсов, используемый
#include "spin.h"
SpinDemoMenu MENU
! POPUP "&File"
I
MENUITEM "E&xit", IDM Exit }
MENUITEM "&Dialog", IDM_Dialog
POPUP "&HcIp"
{ MENUITEM "&About", IDM_About
SpinDcmoDialog DIALOG 0, 0. 100, 100
STYLE DS_MODALFRAME | DS_3DLOOK | \VS_POPUP | WS_VISIBLE |
WS_CAPTION | WS_SYSMENU CAPTION "Spin Demo Dialog" FONT 8, "MS Sans Serif f
DEFPUSHBUTTON "OK". 1D_OK. 25, 60, 50, 14
CONTROL "", ID_Edit, "edit", ES LEFT | WSJTHILD j WS_VISIBLE |
WSJJORDER ! \VS_TABSTOP | ES_NUMBER, 25, 20, 50, 12
И наконец, непосредственно текст программы:
^include <windows.li> #includc <commctrl.h> ^include "spin.h"
HINSTANCE hlnst;
LRESULT CALLBACK SpinDemoWndProcfHWND, UINT, UINT, LONG ); BOOL CALLBACK SpinDcmoDialogProc(HWND. UINT, WPARAM, LPARAM);
int WINAPI WinMain (HINSTANCE hinstancc, HINSTANCE hPrevInstance, LPSTR IpszCmdParam, int nCmdShow )
>
HWND liWnd;
WNDCLASS WndClass ;
MSG Msg;
char szClassNamef] = "SpinDcmo";
hlnst = hlnstance;
144
/* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.stylc = CS_HREDRAW j CS_VREDRAW;
WndClass. IpfnWndProc = SpinDemoWndProc;
WndCiass.cbClsExtra = 0;
WndClass. cbWndExtra = 0;
WndCIass.hlnstance = hlnstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, IDC^ARROW);
WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
WndClass. IpszMenuName = "SpinDemoMenu";
WndClass. IpszClassName = szCIassName;
if ( !RegisterClass(&WndClass) ) I
MessageBox(NULL,"Cannot register class", "Error", MB_OK); return 0;
hWnd = CreateWindow(szClassName, "Spin Demo",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CWJJSEDEFAULT, CWJJSEDEFAULT, NULL, NULL, h!nstance,NULL); if(!hWnd) {
MessageBox(NULL,"Cannot create window", "Error",MB_OK); return 0;
InitCommonControlsO; /* Show our window */ ShowWindow(hWnd,nCmaShow); UpdateWindow(hWnd);
/* Beginning of messages cycle */
whilc(GetMessage(&Msg, NULL, 0, 0)) {
TranslateMcssage(&Msg); DispatchMcssage(&Msg); } return Msg.wParam;
LRESULT CALLBACK SpinDemoWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam ) I switch(Messagc)
145
case \VM_COMMAND: switch(wParam)
{ case IDM_Exil:
SendMessage(hWnd, WM_CLOSE, 0, 0);
break; case IDM_Dialog:
DialogBox(hInst, "SpinDemoDialog", hWnd, SpinDcmoDialogProc);
break;
i /
return 0;
case WM_DESTROY; PostQuitMessage(O); return 0;
I return DefWindowProc(hWnd,Message,wParam, IParam);
BOOL CALLBACK. SpinDemoDialogProc(HWND hDlg, UINT Message,
WPARAM wPararn, LPARAM IParam)
!
static HWND hEditWnd; static HWND hSpinWnd; switch(Message)
{
case WMJNITDIALOG: hEditWnd = GetDlgItem(hDlg,ID_Edit);
hSpinWnd = CreatcUpDownControl(WS_CHILD | WS_BORDER |
WS_VISIBLE ] UDS_SETBUDDYINT | UDS_ALIGNRIGHT, 0, 12,50,50, hDlg, ID_Spin, hlnst,
hEditWnd, 100,0,50);
return TRUE; case WM_COMMAND: switch(LOWORD(wParam))
{
case ID_OK: EndDialog(hDlg,0); return TRUE;
} break;
!
return FALSE;
146
Spin Demo Dialog
;.]50
Рис. 13. Диалоговое окно со спином
При выборе элемента «Dialog» из меню протраммы производится ото бражение диалогового окна, вид которого показан на рис. 13.
Действия, приводящие к отображению спина, локализованы в той части программы, которая обрабатывает сообщение WM_INITDIALOG. Спин создается посредством вызова функции CreateUpDownControl(), при этом в качестве приятельского окна указывается окно редактирования, созданное как часть ресурса с описанием диалогового окна. Все остальные действия производятся автоматически. Читатель может попробовать изменить стили спина и посмотреть, к чему это приведет.
Т а б ,i и ц а 35. Стили трекбара
Слип, |
Значение |
Описание |
TBSJTORZ |
0x0000 |
Определяет горизонтальную ориентацию |
|
|
т| скбара |
TBSJiOTTOM |
0x0000 |
Шкала расположена пол ползунком (для |
|
|
горизонтального трекбара) |
TBS_RIGHT |
0x0000 |
Шкала расположена справа от трекбара (для |
|
|
вертикального трекбара) |
TBS AUTOTICKS |
0x000 1 |
Шкала трекбара создается с делениями |
TBS VERT |
0x0002 |
Определяет вертикальную ориентацию |
|
|
трекбара |
TBS_TOP |
0x0004 |
Шкала расположена над ползунком (для |
|
|
горизонтального трекбара) |
TBS_LEFT |
0x0004 |
Шкала расположена слева от трекбара (для |
|
|
вертикального трекбара) |
TBS BOTH |
0x0008 |
Шкала расположена с двух сторон трекбара |
TBS NOTICKS |
0x0010 |
Шкала трекбара создается без делений |
TBS ENABLESELRANGE |
0x0020 |
Разрешается отображение диапазона |
TBS_FIXEDLENGTH |
0x0040 |
При изменении диапазона длина трекбара не |
|
|
изменяется |
TBSJMOTHUMB |
0x0080 |
У трекбара нет слайдера |
147
РАБОТА С ТРЕКБАРОМ
Очередным клоном линейки прокрутки является ползунок (trackbar или slider). Его внешний вид достаточно эффектен и интересен. Он напоминает регулятор, используемый в аппаратуре, скажем, в качестве регулятора громкости. Небольшим отличием трекбара от линейки прокрутки является то, что у ползунка есть шкала, вдоль которой он движется. Честно говоря, мне очень не нравится переводить на русский слова, к которым я уже привык и которые обычно используются в качестве программистского сленга. Поэтому давайте будем в данном случае под словом «трекбар» понимать весь элемент управления, а под словом «слайдер» - указатель, движущийся вдоль шкалы.
К сожалению, для создания трекбара не предусмотрено специальной функции, поэтому создавать его необходимо посредством вызова функции CreateWindowQ или CreateWindowExQ. При этом в качестве имени класса следует указать макрос TRACKBARjCLASS, который описан в commctrl.h:
#ifdef JATN32
«define TRACKBAR_CLASSA
#deime TRACKBAR_CLASSW
#ifdcf UNICODE
#define TRACKBAR_CLASS
tfdefine TRACKBAR_CLASS
#endif
#else
#defme TRACKBAR_CLASS
#cndif
"msctls_trackbar32" L"msclls trackbar32"
TRACKBAR_CLASSW TRACKBAR_CLASSA
"msctls trackbar"
При создании трекбара могут использоваться стили окна, идентификаторы которых начинаются с TBS (табл. 35).
Что еще можно сказать об этих стилях? По-моему, здесь все ясно. Даже понятно, что стили TBS_HORZ, IBS BOTTOM и TBS RIGHT являются стилями, принимаемыми по умолчанию.
А теперь настало время рассмотреть сообщения, посредством которых осуществляется управление трекбаром. Все эти сообщения приведены в табл. 36.
148
Таблица 36. Сообщения, посылаемые трекбару
Сообщение
Значение
Описание
TBM_GETPOS TBM^GETRANGEMIN TBM_GETRANGEMAX TBMJ3ETTIC
TBM_SETTIC TBM_SETPOS
TBM_SETRANGE
TBM_SETRANGEMIN TBM_SETRANGEMAX
TBM^CLEARTICS TBM_SETSEL
'BM_SETSELSTART "BM_SETSELEND BMJ3ETPTICS BM_GETTICPOS
WMJJSER WM_USER + I WMJJSER + 2 WM_USER + 3
WMJJSER + 4 WMJJSER + 5
WMJJSER + 6
WMJJSER + 7 WMJJSER + 8
WMJJSER + 9 WMJJSER + 10
WMJJSER + 11 WMJJSER+12 WMJJSER+14 WM USER+ 15
wParam и IParam = 0, возвращает текущую позицию слайдера
wParam и IParam = 0, возвращает нижнюю границу диапазона прокрутки wParam и IParam = 0, возвращает верхнюю границу диапазона прокрутки wParam = номер (от нуля) пометки, IParam = 0, возвращает позицию в диапазоне, соответствующую указанной пометке
wParam = 0, IParam = позиции, устанавливает пометку в указанной позиции wParam = TRUE, IParam = новой позиции слайдера, слайдер устанавливается в новую позицию
wParam = флаг перерисовки (BOOL), IParam = MAKELONG(wMinimum, wMaximum), устанавливает диапазон для слайдера трекбара
wParam — флаг перерисовки (BOOL), IParam = wMinimum, установка нижней
•раницы диапазона для слайдера wParam = флаг перерисовки (BOOL), IParam = wMaximum, установка верхней границы диапазона для слайдера wParam = флаг перерисовки (BOOL), IParam = 0, удаляет текущую пометку wParam = флаг перерисовки (BOOL), IParam = MAKELONG(wMinimum, «Maximum, установка диапазона выделения в трекбаре wParam = флаг перерисовки (BOOL), IParam = wStart, установка нижней границы выделения wParam = флаг перерисовки (BOOL), IParam = wEnd, установка верхней границы выделения wParam и IParam = 0, возвращает указа-
-ель на массив, содержащий позиции юметок
wParam = номер (от нуля) пометки, ^aram = 0, возвращает позицию пометки оконных координатах
149
Окончание табл. 36
Сообщение
Значение
Описание
TBM_GETNUMTICS TBM_GETSELSTART TBM_GETSELEND TBM_CLEARSEL
TBM_SETTICFREQ TBM SETPAGESIZE
TBM GETPAGESIZE
TBM SETLINESIZE
TBM GETLINESIZE
tbm getthumbrect tbm"getchannelrect tbm_setthumbrect tbm getthumbrect
WMJJSER + 16 WM_USER+ 17 WMJJSER + 18 WMJJSER + 19
WM_USER + 20 WM USER+ 21
WM USER+ 22
WM USER+ 23
WM USER + 24
WMJJSER + 25 WMJJSER + 26 WMJJSER + 27 WM USER + 28
wParam = 0, IParam - 0,возвращает
число пометок
wParam = 0, IParam = 0, возвращает
нижнюю границу выделения
wParam = 0, IParam - 0, возвращает
верхнюю границу выделения
wParam = флаг перерисовки (BOOL),
IParam ~~ 0, сбрасывает текущее
выделение
wParam = частоте следования пометок,
IParam = номеру позиции, начиная с
которой расставляются пометки,
устанавливает шаг расстановки
пометок
wParam ~ 0, IParam — wPageSize,
определяет, на сколько позиций
необходимо передвинуть слайдср при
получении сообщений
ТВ_PAGEDOWN и TBJ>AGEUP,
возвращает предыдущее значение
этого параметра
wParam = 0, IParam = 0, возвращает
значение параметра,о котором
говорится в предыдущей строке
таблицы
wParam — 0. IParam — wLineSize,
определяет, на сколько позиций
необходимо передвинуть слайдер при
получении сообщений
TB_LINEDOWN и TB_LINEUP,
возвращает предыдущее значение
этого параметра
wParam = 0, IParam •- 0, возвращает
значение параметра,о котором
говорится в предыдущей строке
таблицы
150
Т а б л ч ц а 37. Коды нотификации, посылаемые трекбаром
Сообщение
Описание
TB_LINEUP
TBJLINEDOWN
TB_PAGEUP
TBJ'AGEDOWN
TB_THUMBPOSITION
TB_THUMBTRACK TB_TOP
TB_BOTTOM ТВ ENDTRACK
Нажата VKJLEFT (стрелка влево) или VK_UP (стрелка вверх)
Нажала VK_RIGHT (стрелка вправо) или VK_DOWN (стрелка вниз) Нажата VKJNEXT (PageUp) или щелчок мытью перед елайдером Нажата VK_PRIOR (PageDown) или щелчок мышью после слайдера Слайдер зафиксирован после протяжки с помощью мыши
Слайдер протягивается с помощью мыши Нажата VK_HOME (Home), слайдер устанавливается в положение, соответствующее верхней границе диапазона Нажата VK_END (End), слайдер устанавливается в положение, соответствующее нижней границе диапазона
Слайдер зафиксирован после перемещения с помощью клавиатуры
При манипуляциях с трекбаром последний посылает родительскому окну сообщения WM_HSCROLL. В младшем слове wParam содержится код нотификации, который определяет характер действия, произведенного с трекбаром. Старшее слово wParam содержит позицию слайдера в момент возникновения сообщения. Дескриптор окна трекбара записывается в IParam. В табл. 37 приведены нотификационные сообщения, используемые при работе с трекбаром.
Сообщениия WM TOP, WM BOTTOM, WM_LINEDOWN и WM LINEUP посылаются только в том случае, если пользователь воздействует на трекбар с помощью клавиатуры, TB_THUMBPOSITION и TBJTHUMBTRACK посылаются в случае работы с мышью, остальные сообщения могут посылаться в обоих случаях.
Теперь, изучив теоретически работу трекбара, мы можем рассмотреть программу, в которой демонстрируются возможности этого элемента управления. Для нормальной работы программы помимо основного файла с текстом программы необходимы еще два файла: описаний и ресурсов. Файл описаний приведен ниже:
#с!еЛпе IDM_Exit 101
#define IDM_Dialog 102
ftk-fine IDM About 103
151
«define ID_OK 104 «define ID_Edit 105 «define ID_Spin 106 «define IDJTrackbar 107 А теперь - файл ресурсов. «include "trackbar.h" TrackbarMenu MENU { POPUP "&File"
{ MENUITEM "E&xit", IDM_Exit
}
MENUITEM "&Dialog", IDM_Dialog POPUP "&Help"
{ MENUITEM "&About", IDM_About
TrackbarDialog DIALOG 0, 0, 100, 100
STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE |
WS_CAPTION | WS_SYSMENU CAPTION "Trackbar Demo Dialog" FONT 8, "MS Sans Serif
{
DEFPUSHBUTTON "OK", ID_OK, 25, 73, 50, 14
CONTROL "", ID_Edit, "edit", ES_LEFT | WS_CHILD | WSJV1SIBLE |
WS_BORDER WSJTABSTOP | ES_NUMBER, 25, 14, 50, 12
А теперь - очередь основного файла программы. «include <windows.h> «include <commctrl.h> «include "trackbar.h"
HINSTANCE hlnst;
LRESULT CALLBACK TrackbarWndProc(HWND, UINT, UINT, LONG ); BOOL CALLBACK TrackbarDialogProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain (HINSTANCE hinstance, HINSTANCE hPrevInstance,
LPSTR IpszCmdParam, int nCmdShow ) {
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msg;
char szClassName[] = "TrackbarDemo";
hlnst = hinstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.stylc = CSJHREDRAW | CSJVREDRAW;
152
WndClass.lpfnWndProc = TrackbarWndProc; WndClass.cbClsExtra = 0; WndClass.cbWndExtra = 0; WndClass. hinstance = hinstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION); WndClass. hCursor = LoadCursor (NULL, IDC_ARROW); WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); WndClass. IpszMenuName = "TrackbarMenu"; WndClass. IpszClassName = szClassName; if ( !RegisterClass(&WndClass) ) {
MessageBox(NULL, "Cannot register class", "Error", MB_OK);
return 0;
hWnd = CreateWindow(szClassName, "Trackbar Demo Program",
WS_OVERLAPPEDWINDOW, CWJJSEDEFAULT, CW_USEDEFAULT, CWJJSEDEFAULT, CWJJSEDEFAULT, NULL, NULL, h!nstance,NULL); if(!hWnd) {
MessageBox(NULL,"Cannot create window", "Error",MB_OK); return 0;
InitCommonControls(); /* Show our window */ ShowWindow(hWnd.nCmdShow); UpdateWindow(hWnd); /* Beginning of messages cycle */
while(GetMessage(&Msg, NULL, 0, 0)) {
TranslateMessage(&Msg); DispatchMessage(&Msg); } return Msg.wParam;
LRESULT CALLBACK TrackbarWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam ) {
switch(Message) {
case WM_COMMAND: switch(wParam)
i \
case IDM_Exit: SendMessage(hWnd, WM_CLOSE, 0, 0);
153
break;
case IDM_Dialog;
DialogBox(hInst, "TrackbarDialog", hWnd, TrackbarDialogPruc); break;
}
return 0;
case WMJDESTROY: PostQuitMessage(O); return 0;
return
DefWindowProc(h\Vnd,Mcssage,\vParam, IParam);
BOOL CALLBACK TrackbarDialogProc(HWND hDlg, UINT Message,
WPARAM wParam, LPARAM IParam)
{
static HWND hEditWnd; static HWND hSpinWnd; static HWND hTrackbarWnd; switch(Message) j
case WMJNITDIALOG: hEditWnd = GctDlgltemfliDlg, ID_Edit); hTrackbarWnd = GctDlgltem(hDlg, ID J'rackbar); hSpinWnd = CreateUpDownControl(WS_CHILD | WSJ3ORDER |
WS_VISIBLE j UDS_SETBUDDYINT | UDS_ALIGNRIGHT, 0, 12,50,50,
hDlg, !D_Spin, hlnst, hEditWnd, 10,0,5); hTrackbarWnd = CreatcWindow(TRACKBAR_CLASS,"Trackbar Demo",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | TBS_AUTOTICKS, 4,75, 142.40, hDlg, NULL, hlnst, NULL); SendMessagediTrackbarWnd. TBM_SETRANGE, TRUE,
MAKELONG(0,10));
SendMessagc(hTrackbarWnd. TBM^SETPOS, TRUE, 5); return TRUE; case WMJVSCROLL: SendMessagediTrackbarWnd, TBM^SETPOS, TRUE,
GetDlgIiemInt(hDlg, ID_Edit, NULL,!)); return TRUE; case WMJISCROLL: SetDlgltemlntOiDlg, ID_Edit, SendMcssage(hTrackbarWnd,
TBMJ3ETPOS, 0,0), TRUE); case WM_COMMAND: switch(LOWORD(wParam))
case ID OK:
154
EndDialog(hDlg,0); return TRUE;
break;
i i
return FALSE;
После того, как эта программа будет запущена и в основном меню программы будет выбран элемент «Dialog», на экране появится диалоговое окно, вид которого показан на рис. 14.
Tiackbar Demo DialogQ!
,__...
Рис. 14. Диалоговое окно со спином и трскбаром
Попробуйте изменить положение спина. При этом изменится положение слайдера на трекбаре. Аналогично, если изменить положение слайде-ра с помощью мыши, то изменится значение в окне редактирования, которое является приятельским окном спина.
При написании этой программы использовались две новые функции. Первая, GetDlgItemInt(), описана в файле winuser.h следующим образом:
WINUSERAPI UINT WINAPI GetDlgItemInt(HWND hDlg, int nIDDlgllem,
BOOL «IpTraiislated. BOOL bSigned);
Эта функция берет число, записанное в окне редактирования (оно представлено в виде строки), преобразует его в числовой вид и возвращает числовое значение. В программе мы используем эту функцию для того чтобы считать значение спина и соответствующим образом изменить положение слайдера трекбара.
Вторая функция, описанная в том же файле, имеет следующий прототип:
WINUSERAPI BOOL WINAPI SetDIg[temTextA(HWND hDlg. int ninDlgltem,
LPCSTR IpString); WINUSERAPI BOOL WINAPI SetDlgItcmTcxtW(HWND hDlg. int nlDDlgltcm,
155
LPCWSTR IpString);
#ifdefUNICODE
tfdefine SetDlgltemText SetDlgltemTextW
#e!se
#defme SetDlgltemText SetDlgltemTextA
#endif//! UNICODE
Она производит действие, обратное GetDlgItemInt(), т. е. получает в качестве аргумента целое число и возвращает его представление в виде строки. В программе она используется для того, чтобы в окне редактирования отобразить номер позиции слайдера. Все просто, не так ли?
РАБОТА С ИНДИКАТОРОМ (PROGRESS BAR'OM)
Надеюсь, что читатель уже имеет опыт инсталляции программных продуктов для Windows. Там степень завершенности задачи отражается синей полосой, которая постоянно растет. По достижении ею отметки, соответствующей 100%, процесс инсталляции оказывается завершенным. Вот эта синяя полоса и является индикатором, который индицирует степень завершенности достаточно длительной задачи.
По своему внешнему виду это, наверное, самый простой из общих элементов управления. То же самое можно сказать и о работе с ним. У него только один стиль, и всего пять сообщений применяются для управления им.
Как и в случае трекбара, специальной функции для создания индикатора нет. Для создания индикатора необходимо использовать функцию CreateWindowQ или CreateWindowExQ. Для указания имени класса необходимо использовать макрос PROGRESS_CLASS, который в файле commctrl.h описан следующим образом:
#ifdef_WIN32
tfdefme PROGRESS_CLASSA
#define PROGRESS_CLASSW
#ifdef UNICODE
#defme PROGRESS_CLASS
#else
tfdefme PROGRESSJXASS
#endif
#else
#define PROGRESS_CLASS
#endif
"msctls_progress32" L"msctls_progress32"
PROGRESS_CLASSW PROGRESS_CLASSA
"msctls_progress"
156
Для управления индикатором используются сообщения. Поговорим о них.
Наверное, для того чтобы использовать индикатор, необходимо определить для него минимальное и максимальное значения (в приведенном примере, когда я говорил об инсталляционных программах, минимальное и максимальное значения равны 0 и 100 соответственно). С этой целью используется сообщение PBM_SETRANGE (WM_USER+1). wParam его должен быть равно 0, a IParam должно определяться как MAKELONG(wMinRange, wMaxRange), где wMinRange и wMaxRange -минимальное и максимальное значения. Функция, пославшая это сообщение, возвращает значения старого диапазона. Если обозначить возвращаемое значение как IReturn, то LOWORD(lReturn) будет определять нижнюю границу диапазона, a HIWORD(lReturn) будет содержать верхнюю границу диапазона.
Для установки индикатора в определенную позицию (другими словами, для определения необходимой длины индикатора) используется сообщение PBM_SETPOS (WMJJSER + 2). При этом wParam должен содержать позицию, в которую необходимо установить индикатор. IParam должен быть равным 0. Предыдущая позиция индикатора возвращается функцией, пославшей сообщение.
Сообщение РВМ DELTAPOS применяется для определения значения, на которое будет увеличена длина индикатора. wParam этого сообщения определяет приращение, a IParam должен быть равным 0. Функция, пославшая сообщение, возвращает значение предыдущей позиции.
Для определения шага, с которым будет увеличиваться длина индикатора, используется сообщение PBM^SETSTEP, wParam которого определяет шаг, a IParam должно быть равно нулю. По умолчанию, шаг приращения равен 10.
Сообщение РВМ STEPIT указывает индикатору, что необходимо осуществить увеличение длины, используя при этом все текущие установки, т. е. текущую позицию и текущий шаг. Оба параметра сообщения должны быть равны 0.
И, как всегда, демонстрационная программа, с помощью которой читатель сможет увидеть управление индикатором в действии.
Вся программа состоит из трех файлов. Первый - файл заголовков:
fldefinelDC MSCTLS TRACKBARI 101
Adeline IDFvf Exit 101
#dcfme IDM_Dialog 102
#definc IDM_About 103
#defincID_OK. 104
#dcilne ID_Cdit 105
#deiine ID_Spin 106
157
#define ID^ProgressBar 107
Второй файл - файл ресурсов, он также приводится ниже.
#include "ProgressBar.h"
ProgrcssBarMenu MENU
{
POPUP "&File"
!
MENUITEM "E&xit", IDM Exit
MENUITEM "&Dialog", IDM_Dia!og
POPUP "&Help"
{
MENUITEM "&About", IDM_About
ProgressBarDialog DIALOG 0, 0, 100, 100
STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WSJVISIBLE |
WS_CAPTION i WS_SYSMENU CAPTION "Progrcssbar Demo Dialog" FONT 8, "MS Sans Serif
{
DEFPUSHBUTTON "OK", ID_OK, 25, 73, 50. 14
CONTROL "", ID_Edit, "edit", ES_LEFT | WS_CH1LD | WS_VISIBLE [
WS_BORDER | WS_TABSTOP | HS_NUMBER, 25, 14, 50. 12 >
И, естественно, основной файл программы.
#include <windows.h>
#include <commctrl.h>
#include "ProgressBar.h"
HINSTANCE hlnst;
LRESULT CALLBACK ProgressBarWndProc ( HWND, UINT, UINT, LONG ); BOOL CALLBACK ProgressBarDialogProc(I!WND, UINT, WPARAM,
LPARAM);
int WINAPI WinMain ( HINSTANCE hinstancc, HINSTANCE hPrcvInstancc, LPSTR IpszCmdParam, int nCmdShow )
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msg;
char szClassNamef] = "ProgressBarDemo"
hlnst = hinstancc;
158
/* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.stylc = CS_HREDRAW j CS_VREDRAW;
WndClass.IpfhWndProc = ProgressBarWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hlnstance = hlnstance ;
WndClass.hlcon = Loadlcon (NULL,!DI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, 1DC_ARROW);
Wndclass.hbrBackground - (HBRUSH) GetStockObject (WHITE_BRUSH);
WndClass.IpszMenuNamc = "ProgressBarMenu";
WndClass. IpszClassName = szClassName;
if ( !RegistcrClass(&WndClass)) {
MessageBox(NULL,"Cannot register cIass","Error",MB_OK); return 0;
} hWnd = CrealeWindow(szClassName, "Progressbar Demonstration Program",
WS_OVERLAPPED\VINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, Mnstance,NULL); if(!hWnd) {
MessageBox(NULL,"Cannot create window","Error",MB_OK); return 0;
/* Show our window */ ShowWindow(hWnd,nCmdShow); Update Window(hWnd);
/* Beginning of messages cycle */
while(GetMessage(&Msg, NULL, 0, 0)) {
TranslateMessagc(&Msg); DispatchMessage(&Msg); ( return Msg.wParam;
LRESULT CALLBACK ProgressBarWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
j t
swilch(Mcssage) {
case WM_COMMAND; swilch(LOWORD(wParain))
159
case IDM_Dialog:
DialogBoxfhlnst, "ProgressBarDialog", liWnd, ProgressBarDialogProc);
break; case IDMJExit:
SendMessage(hWnd, WM_CLOSE, 0,0);
break;
}
return 0;
case WM_DESTROY: PostQuitMessage(O); retuni 0;
! return DefWindowProc(hWnd,Message,wParam, IParam);
BOOL CALLBACK ProgressBarDialogProc(HWND hDlg, UINT Message,
WPARAM wParam, LPARAM IParam)
{
static HWND hEditWnd; static HWND hSpinWnd; static HWND hProgressBarWnd; int i; switch(Message)
{
case WMJNITDIALOG: hEditWnd = GetDlgItem(hDlg, ID_Edit);
hSpinWnd = CreatcUpDownControl(WS_CHILD | WS_BORDER |
WS_VISIBLE | UDS_SETBUDDYINT | UDS_ALIGNRIGHT, 0, 12,50,50, hDlg, ID_Spin, hlnst, hEditWnd, 10,0,5);
hProgressBarWnd = CreateWindow(PROGRESS CLASS,
"ProgressBar Demo", WS_CHILD j WSJVISIBLE, 10,75, 130,20, hDlg, NULL, hlnst, NULL); SendMessage(hProgressBarWnd, PBM^SETRANGE, 0,
MAKELPARAM(O.IO));
SendMcssage(hProgressBarWnd, PBM_SETSTEP, 1, 0); SendMessage(hProgressBarWnd, PBM_SETPOS, 5, 0); return TRUE; case WM_VSCROLL: SendMessage(hProgressBarWnd, PBM_SETPOS, GetDlgItemInt(hDlg,
ID_Edit,NULL,l),0); return TRUE; caseWM COMMAND:
160
switch(LOWORD(wParam))
{
case ID_OK: EndDialog(hDlg,0); return TRUE; } break;
} return FALSE;
Вид диалогового окна, возникающего после выбора пользователем элемента «Dialog» в главном меню программы, показан на рис. 15.
Piogiessbai Demo . |
4XI |
|
, : - •' •' |
"-- ...:.,:.'4i.;-:;;v,W>»i';..-';.v" ---:: \: !^"i';,V;"«Tv::,«™. • ,,.- ,', • -.;•:•*?- •-•>-/.: |
cm --..-' |
•••••• |
:}'-- |
;. r.......0.____,:|| |
•i'ij"-. |
|
V^-V""V. |
Эта программа по своему действию очень похожа на приведенную в предыдущем разделе. Разница состоит в том, что спин управляет не трекбаром, а индикатором. Рекомендую читателю нажать несколько раз кнопки спина для того чтобы позиция спина изменилась, и посмотреть, что произойдет с индикатором. При разборе программы особое внимание следует уделить обработке сообщений WMJNITDIALOG и WM_VSCROLL.
Далее рассмотрим окна подсказок (Tooltip Controls) и списки изображений (ImageLists). При изучении Win32 я не увидел тех моментов, когда два этих элемента управления использовались бы самостоятельно. Они являются только вспомогательными элементами.
РАБОТА С ОКНАМИ ПОДСКАЗОК
Окна подсказок - это небольшие всплывающие окна, которые содержат одну строку текста, объясняющую назначение какого-либо инструмента (tool) родительского окна. Под инструментом в данном случае понимается либо элемент управления, присутствующий в родительском
161
окне (пример - полоса инструментов в WinWord'e), либо прямоугольная область внутри рабочей области окна.
Окна подсказок почти всегда скрыты. Отображаются они только в том случае, если пользователь задерживает курсор мыши на инструменте. Подсказка появляется рядом с инструментом и исчезает тогда, когда пользователь либо нажимает клавишу (мыши или клавиатуры), либо убирает курсор с инструмента. Одна подсказка может появляться при задержке курсора на разных инструментах.
К сожалению, не все общие элементы управления имеют специальную функцию для своего создания ( © ). Окна подсказок тоже создаются только посредством применения CreateWindow() или CreateWindowExQ. В этом случае для их создания необходимо использовать макрос TOOLTIPS^CLASS, который в файле commctrl.h описан следующим образом:
#ifdef_WIN32
^define TOOLTIPS_CLASSW
#defmc TOOLTIPS_CLASSA
#ifdef UNICODE
ftleflne TOOLTIPS_CLASS
#else
#define TOOLTI PS CLASS
#cndif
#clse
#define TOOLTIPS (CLASS
#endif
L"tooltips_class32" "tooltips_class32"
TOOLTIPS CLASSW TOOI.TIPS_CLASSA
"tooltips_class"
При создании подсказки могут использоваться два стиля, специально разработанные для окон этого типа - TTS_ALWAYSTIP и TTS_NOPREFIX. Подсказка, имеющая стиль ITS ALWAYSTIP, появляется при помещении курсора на инструмент вне зависимости от того, активно или не активно родительское окно.
Необходимо отметить еще одну возможность создания окон подсказок. Дело в том, что в Win32 некоторые элементы управления имеют специальный стиль, обычно оканчивающийся на TOOLTIPS. Он позволяет программе не создавать собственные окна подсказок, а использовать встроенные возможности системы. Разумеется, этот способ использования подсказок намного проще.
Достаточно часто подсказки используются в панели инструментов, при этом каждая кнопка в панели инструментов соответствует элементу
162
меню. При этом подсказки, как правило, совпадают с текстом, отображаемым в соответствующем элементе меню. Если окно подсказки создается со стилем TTS_NOPREFIX, то система автоматически удаляет знак амперсанта, если он присутствует в строке меню.
Помимо этого, подсказка сама по себе может быть активной и неактивной. Акпганая подсказка появляется при нахождении курсора на инструменте, неактивная подсказка ire отображается ни в каких случаях.
Т а б л и ц а 38. Сообщения, посылаемые "подсказкам"
С сюощсшк'
Описание
ТТМ_ ACTIVATE ТТМ SETDI-LAYTIMF.
ТТМ ADDTOOL TTM_DELTOOL
TTM_NEWTOOLRECT TTM_RELAYEVENT
ТТМ GF.TTOOLINFO TTM_SETTOOLINFO ТТМ НГГП-ST
ТТМ GF.TTEXT TTMJJPDATETIPTEXT TTMJiETTOOLCOUNT ТТМ ENUMTOOLS
TTM_GETCURRENTTOOL ТТМ WINDOWFROMPOINT
WM USER
WM USER + 3
WM USER • 7
WM USER ~ 13
WM USER + i 6
Подсказка делается активной или неактивной. wParam = TRUE - под-ска зка активна, IParam всегда = О Определяются интервал инициализации, интервал отображения и интервал повторного отображения Регистрирует инструмент, с которым будет работать подсказка Удаляет ранее добавленный инструмент
Определяет границы окна подсказки Передаст сообщение от мыши окну подсказки для обработки Запрос информации об инструменте, с которым работает подсказка Установка информации для инструмента
Определение, попадает ли точка в указанный для инструмента прямоугольник, и, если попадает, получение информации об инструменте
Получение текста подсказки, отображаемой с инструментом Установка текста подсказки для инструмент
Получение числа инструментов, с которыми работает подсказка Позволяет программе последовательно перебрать все инструменты, с которыми работает окно подсказки Получение информации о текущем инструменте
Выдача окна подсказки в месте, определяемом не положением курсора, а параметрами сообщения
163
Так как подсказка является окном, то для управления ею используются сообщения. Их список сообщений с кратким описанием приведен в табл. 38. Рассмотрим наиболее часто используемые сообщения.
Для того чтобы подсказка работала с тем или иным инструментом, необходимо этот инструмент включить в список инструментов, с которыми работает окно подсказки. Для этого окну подсказки надо направить сообщение TTM_ADDTOOL. WParam этого сообщения должен быть равен 0, a IParam содержать указатель на структуру типа TOOLINFO. Эта структура описана в файле commctrl.h :
typedef struct tagTOOLINFOA {
UINT cbSize;
UINT uFlags;
HWND hwnd;
UINT uld;
RECT reel;
HINSTANCE hinst;
LPSTR IpszText; } TOOLINFOA, NEAR «PTOOLINFOA, FAR «LPTOOLINFOA;
typedef struct tagTOOLINFOW {
UINT cbSize;
UINT uFlags;
HWND hwnd;
UINT uld;
RECT rcct;
HINSTANCE hinst;
LPWSTR IpszText; } TOOLINFOW, NEAR *PTOOLINFOW, FAR *LPTOOLINFOW;
#ifdef UNICODE tfdcfine TOOLINFO
#define PTOOLINFO
#defme LPTOOLINFO
#else
tfdefine TOOLINFO tfdefine PTOOLINFO tfdefine LPTOOLINFO
#endif
TOOLINFOW PTOOLINFO W LPTOOLINFOW
TOOLINFOA PTOOLINFOA LPTOOLINFOA
Первое поле этой структуры - cbSize - должно содержать размер в байтах структуры типа TOOLINFO. Сам факт присутствия поля, содержащего такую информацию, говорит о том, что фирма Microsoft не исключает возможности изменения и/или дополнения этой структуры.
164
Таблица 39. Битовый файлы, определяющие вил и поведение подсказки
Фл;и |
Значение |
Описание |
TTFJDISHWND TTF CENTERTIP TTF_RTLREADING |
0x0 1 0x02 0x04 |
Флаг установлен - поле uld содержит хэндл инструмент;!, иначе - идентификатор инструмента Центрирует подсказку под инструментом Отображает текст справа налево, как в арабском |
|
|
Я'|ЫКС |
TTF_SUBCLASS |
Ох 10 |
Подсказка должна перехватывать сообщения WM MOUSEMOVE, адресованные инструменту |
Поле hwnd определяет родительское окно инструмента.
Поле uld обычно содержит идентификатор инструмента. Если поле uFlags включает TTF_IDISHWND, то поле uld содержит хэндл окна, внутри которого находится область, используемая в качестве инструмента.
Следующее поле - reel - определяет координаты окна инструмента относительно левого верхнего угла клиентской области окна, определяемого полем hwnd. Если поле uFlags включает флаг TTF_IDISHWND, поле rect игнорируется.
В поле hinst хранится хэндл экземпляра программы, которая содержит строковый ресурс, определяющий текст подсказки. Если это поле не равно нулю, то поле IpszText содержит идентификатор строкового ресурса.
Как, надеюсь, читатель уже понял, поле IpszText может интерпретироваться по-разному. Итак, вариант первый. Если значение поля IpszText равно LPSTR TEXTCALEBACK, то именно окно, хэндл которого указан в поле hwnd, получает нотификационное сообщение TTN__NEEDTEXT, уведомляющее о том, что родительское окно инструмента должно определить, какой текст подсказки должен быть отображен. Второй вариант -поле содержит идентификатор строкового ресурса, в котором определен текст сообщения. Этот вариант используется тогда, когда поле hinst не равно 0. Кроме этого, признаком того, что поле содержит идентификатор строкового ресурса, является старшее нулевое слово. И, наконец, третье, наиболее часто использующееся поле, содержит указатель на строку, содержащую текст подсказки.
165
Таким образом, мы видим, что структура типа TOOLINFO полностью определяет, что используется в качестве инструмента - элемент управления или область экрана, а также где находится текст подсказки - в ресурсах, в строке программы или он определяется родительским окном инструмента.
В любой момент программа может изменить текст подсказки. Для этого окну подсказки посылается сообщение ТТМ UPDATETIPTEXT, wParam которого должен быть равным 0, a IParam - указывать на структуру типа TOOLINFO.
Программа может получить текст, который используется для выдачи подсказки об инструменте с помощью посылки окну подсказки сообщения ТТМ GETTEXT. В этом сообщении wParam должен быть равным 0. IParam, как и в предыдущем случае, должен содержать указатель на структуру типа TOOLINFO, в которой определяется инструмент, подсказка о котором запрашивается. Ноле IpszText указывает на буфер, в который будет записан текст подсказки.
Для того чтобы отобразиться, окно подсказки должно получить сообщение от мыши. Так как Windows посылает сообщения только тому окну, поверх которого находится курсор, программа должна использовать сообщение TTM_R.ELAYEVENT для того чтобы транслировать сообщение окну подсказки. wParam этого сообщения должен быть равным нулю, a IParam должен содержать указатель на структуру типа MSU, в которой хранится информация о транслируемом сообщении. При этом необходимо учесть, что окно подсказки обрабатывает информацию только о сообщениях, приведенных ниже: WM IJiUTTONDOWN;
" WM LBUTTONUP;
* WM]MUUTTONI)OWN: ]WM MBUTTONUP;
* WM^MOUSEMOVF;
* WM RBUTTONDOWN;
* WM RBUTTONUP.
Если инструмент представляет собой прямоугольную часть окна, то тогда никаких сложностей не появляется. Если же инструмент является системным окном (таким, например, как кнопка), то в этом случае возникают определенные трудности. Программа должна будет или перехватывать сообщения посредством использования hook'oB, или подменить оконную функцию системного окна (осуществить subclassing). К сожалению, рассмотрение вопросов, связанных с поок'амн и subclassing'oM выходит за рамки этой книги, поэтому я вынужден буду остановиться только на подсказках, связанных с областью окна. Уважаемый читатель!
166
Предлагаю вам изучить вопросы о перехвате сообщений и подмене оконных функций самостоятельно.
Когда окно подсказки получает транслированное сообщение WM MOUSEMOVE, оно определяет, находится ли курсор в области, к которой привязана подсказка. При положительном ответе окно подсказки устанавливает таймер. О конце интервала окно снова проверяет, находится ли курсор в нужной области. При втором положительном ответе формируется текст подсказки, копируется в окно подсказки и окно выдается на отображение. Подсказка отображается до тех пор, пока окно подсказки не получит сообщения о нажатии или отжатнн клавиши мыши или о перемещении курсора за пределы интересующей области.
При работе окно подсказки использует три определенных временных интервала. Первый из них, называемый интервалом инициализации, определяет период, в течение которого курсор должен находиться в пределах интересующей области для того, чтобы отобразилась закладка. Второй - интервал повторного отображения - определяет задержку между последовательными отображениями окна подсказки в тех случаях, когда курсор скользит по инструментам, например, по панели инструментов. Третий интервал - интервал отображения - определяет время, в течение которого подсказка находится на отображении в тех случаях, когда курсор находится внутри интересующей области или в пределах границы инструмента. Все чти периоды могут быть определены с помощью сообщения ТТМ SETDELAYT1ME. wParam этого сообщения определяет, какой интервал устанавливается. IParam определяет длительность интервала в миллисекундах. Допустимые значения wParam приведены в табл. 40.
В тол) случае, когда инструментом является область окна, размеры и/или положение которой изменились, окну подсказки необходимо послать сообщение ТТМ NEWTOOLRECT для того, чтобы подсказка появлялась в нужном месте. wParam этого сообщения всегда равен 0.
Тай .1 и и а 40 Идентификаторы временных интервалов
П„р |
Значение |
Описание |
TTDT AUTOMATIC TTDT RP.SHOW ТТГУГ"ЛЬ'ТОРОР TTDT INITIAL |
0 1 |
Все интервалы вычисляются автоматически на основе IParam Определяется интервал повторного отображения Определяется интервал отображения Определяется интервал инициализации |
IParam этого сообщения должен указывать на структуру типа TOOLINFO, поля hwnd и uld которой должны определять инструмент, а поле rect - новые границы инструмента. В том случае, когда инструмент реализован как окно, информировать подсказку о его изменении не нужно, так как подсказка определит факт нахождения курсора в границах инструмента по хэндлу окна.
Перед своим отображением окно подсказки посылает родительскому окну нотификационное сообщение TTN_SHOW, а перед скрытием -TTN_POP. В данном случае нотификационные сообщения посылаются с помощью сообщения WM NOTIFY.
Для получения информации об инструменте программа может использовать сообщения TTM_GETCURRENTTOOL и TTMJ3ETTOOLINFO. Изменить информацию об инструменте можно с помощью сообщения TTM_SETTOOLINFO. Если программе требуется, чтобы подсказка с данным инструментом больше не работала, окну подсказки нужно направить сообщение ТТМ DELTOOL. Параметры этих сообщений однотипны и ничего сложного в них нет. Рекомендую читателю изучить работу этих сообщений самостоятельно.
Как уже было сказано, сами по себе окна подсказок не используются, поэтому демонстрационной программы я не приведу. Тем не менее, в разделе, посвященном закладкам, будут даны примеры другого способа использования подсказок, применимого, к сожалению, только к общим элементам управления, которые появились в Windows NT и Windows'95. В чем состоит этот способ? Дело в том, что при создании некоторых элементов управления можно указать стиль, позволяющий этим окнам реагировать на сообщения WMJNOTIFY с нотпфикационным кодом TTNJNEEDTEXT. Скажем, для закладок этот стиль называется TCSJTOOLTIPS, для панели инструментов - TBSJTOOLTIPS и т. д. В этих случаях родительскому окну элемента управления в качестве IParam сообщения WM_ NOTIFY передается указатель на структуру типа TOOLTIPTEXT, описание которой, находящееся в файле commctrl.h имеет следующий вид:
typcdef struct tagTOOLTIPTEXTA {
NMHDR heir;
LPSTR ipszTcxt;
char s/Tcxt[8()];
HINSTANCE hinst;
UINT u Flags; ! TOOLTIPTEXTA, FAR «LPTOOLTIPTEXTA;
168
typcdef struct tagTOOLTIPTEXTW {
NMHDR hdr;
LPWSTR IpszText;
WCHAR szText[80];
HINSTANCE hinst;
UINT uFIags; ) TOOLTIPTEXTW, FAR *LPTOOLTIPTEXTW;
#ifdcf UNICODE
#define TOOLTIPTEXT Wcllne LPTOOLTIPTEXT
#else
tfdefmc TOOLTIPTEXT
#definc LPTOOLTIPTEXT
#endif
TOOLTIPTEXTW LPTOOLTIPTEXTW
TOOLTIPTEXTA LPTOOLTIPTEXTA
Первое поле этой структуры, тоже структура, но типа NMHDR, описана в файле winuser.h так:
typcdef struct tagNMHDR
i
HWND hwndFrom;
UINT idFrom;
UINT code; //NM_code } NMHDR; typedef NMHDR FAR * LPNMHDR;
hwndFrom - хэндл элемента, пославшего нотификационное сообщение, idFrom - идентификатор этого элемента, code - код нотификационного сообщения. Вроде бы все ясно.
Второе поле стуктуры типа TOOLTIPTEXT (как бы не запутаться с этими структурами!) - IpszText - может содержать указатель на строку, выдаваемую в качестве подсказки, или, если поле hinst не равно 0, содержит идентификатор строкового ресурса, определяющего текст выдаваемой подсказки.
Вместо того чтобы определять указатель на строку, можно скопировать эту строку в буфер szText, который является третьим полем структуры типа TOOLTIPTEXT.
Поле hinst является хэндлом экземпляра, содержащего строковый ресурс с текстом подсказки. Если IpszText является указателем на строку подсказки, это поле должно быть равным NULL.
И наконец, последнее поле - uFIags - содержит комбинацию флагов TTFJDISHWND и TTF RTLREADING, которые были рассмотрены ранее.
169
Анализируя поля этих двух структур, можно определить элемент, для которого определяется подсказка. Для того чтобы эта подсказка появи-лась на экране, достаточно определить либо указатель на строку подсказки, либо ее идентификатор в таблице строк (не забыть при этом о поле hinst!), либо скопировать эту строку в предлагаемый буфер. И все! Пример подобного использования подсказок приведен в разделе о работе с закладками.
РАБОТА СО СПИСКОМ ИЗОБРАЖЕНИЙ
И Windows предусмотрен интересный элемент, который лично я могу назвать элементом управления с большой натяжкой. Тем не менее, этот элемент активно используется при работе с другими элементами управления, например, с закладками, речь о которых еще впереди. Я имею в виду список изображений (Image List). Он представляет собой коллекцию изображении одинакового размера, к каждому из которых можно осуществить доступ по его индексу. Список изображении используется для эффективного управления и манипулирования большими наборами изображении.
Наверное, этот элемент управления самостоятельно не используется еще и потому, что он не является окном. Список изображений - это всего-навсего структура в памяти, обеспечивающая простой доступ к изображениям.
Как и в случае обычных списков, при работе со списками изображений можно создавать и удалять списки, добавлять, удалять и изменять элементы списков. Специфическим именно для списков изображений являются операции по прорисовке и перетаскиванию изображений.
Списки изображений могут быть немаскированными и маскированными. Немаскированный список представляет собой один большой цветной bitmap, который, в свою очередь, состоит из одного или нескольких изображений. Маскируемый список состоит из двух больших bitmap'oB, первый из которых, цветной, содержит непосредственно список изображений, а второй, монохромный, содержит список масок -по одной для каждого элемента.
При прорисовке немаскируемого изображения оно просто копируется на соответствующее устройство. R случае прорисовки маскируемого изображения биты изображения комбинируются с битами маски, создавая обычно прозрачные области, сквозь которые видно то изображение, которое было на устройстве до прорисовки.
Для создания списка изображений приложению необходимо вызвать функцию ImageList Create(), которая описана в файле commctrl.h следующим образом:
WINCOMMCTRLAPI HIMAUEL1ST WINAM
lst Crcatc(int ex. int су,
UfNT Hags. int cliiitial, inl cGrow):
G этой функции первые два аргумента, сх и су, определяют размер в пикселах каждого изображения. Третий аргумент, flags, указывает тип списка изображений. Для каждого типа в файле commctrl.h предусмотрен. макрос, начинающийся с ILC . Список этих типов приведен в табл. 41.
Размер создаваемого bitmap 'а вычисляется, исходя из значения четвертого аргумента. Этот аргумент определяет, сколько изображений должен включать bitmap. Если на каком-то этапе число изображений, включенных в bitmap, достигнет предельного значения, то система автоматически расширит bitmap, добавив место для хранения еще определенного числа изображений, которое определяется последним параметром функции - cGrow.
Т а Я л и ц а 41. Флаги, используемые при создании списка изображении
|
Флаг |
Значение |
Описании |
|
ILC COLOR |
0x1)000 |
Используется флаг ни умолчанию, обычно |
|
|
|
ILC COLOR4. для старых драйверов - |
|
|
|
ILC COLORDDH |
|
ILC MASK |
0x0001 |
Создается маскированный bitmap |
|
ILC_COLORDDO |
OxOOFE |
Используется bitmap, зависящий от устройства |
|
|
|
(устаревший формат) |
|
ILC_COI.OR4 |
O.\0004 |
U качестве bitmap 'а. содержащею изображения, |
|
|
|
используется 16-цвстнын bitmap |
|
ILC COLOR» |
0x0008 |
И качестве bitmap'a, содержащего изображения. |
|
|
|
используется 256-цветный bitmap |
|
II. С COLOR 1П |
0x0010 |
I) качестве bitmap'a. содержащего изображения. |
|
|
|
используется bitmap, допускающий одновременное |
|
|
|
использование до 65536 цветок |
|
If С COLOR24 |
0x0018 |
В качестве bitmap'a, содержащего изображения, |
|
|
|
используется bitmap, допускающий до 2**24 цветов |
|
ILC COLOR32 |
ОхП('.20 |
Н кичестнс hitmap'a. содержащего изображения. |
|
|
|
нсполмусгси bitmap, допускающий до 2**J2 цвет он |
|
ILC I'ALETIl- |
ОхОЙОО |
Со списком изображений используется цнстовая |
|
|
|
палитра |
170
171
При успешном выполнении функция возвращает хэндл созданного списка. NULL должен заставить программиста вздохнуть и найти ошибку.
Но что же происходит при создании списка изображений? Помимо создания непосредственно bitrnap'a с указанными характеристиками, функция создает контекст, совместимый с экраном, и выбирает созданный bitmap в качестве текущего для этого контекста. В случае маскированного bitmap'a функция создает два экранно-совместимых контекста, при этом для одного в качестве текущего она выбирает bitmap с изображениями, а для другого - bitmap с масками.
Естественно, что при использовании такой техники резко уменьшается время, необходимое для копирования изображения на экран. Как результат - программы, использующие список изображений, могут работать значительно быстрее тех, которые перед использованием вынуждены загружать изображения из ресурсов.
Для того чтобы удалить список изображений из памяти (отдельные изображения, из которых состоит список, остаются на своих местах, уничтожаются только указатели, что, собственно, и делает набор изображений списком), необходимо вызвать функцию ImageList_Destroy(), которая описана следующим образом:
WINCOMMCTRLAPI BOOL WINAPI ImageList_Dcstroy(HIMAGELIST himl);
Единственным аргументом этой функции является хэндл удаляемого списка изображений. Возвращенное этой функцией значение FALSE говорит о том, что при удалении списка произошла какая-то ошибка, и список из памяти не удален.
Итак, надеюсь, читатель понял, что такое список изображений и для чего он служит. Теперь возникают очередные вопросы: как добавлять изображения в список, удалять их и производить манипуляции с ними?
Для того чтобы добавить изображение в список, необходимо воспользоваться функцией ImageList_Add(), описание которой имеет следующий вид:
WINCOMMCTRLAPI int WINAPI ImageList_Add(HIMAGELlST him!,
HBITMAP hbmlmage, HBITMAP hbmMask);
Первый аргумент этой функции - himl - очевиден: хэндл списка изображений. Второй аргумент - hbmlmage - представляет собо хэндл добавляемого в список изображения. Третий аргумент - hbmMask - хэндл
172
монохромного bitmap'a, который содержит маски. В случае немаскированного списка третий аргумент игнорируется.
Описание функции ImageList_AddMasked() приведено ниже:
WINCOMMCTRLAPI int WINAPI ImageList_AddMasked(HIMAGELIST himl,
HBITMAP hbmlmage, COLORREF crMask);
Эта функция действует почти так же, как и предыдущая, но маска генерируется автоматически. Для генерации маски необходимо задать цвет. Если в изображении встречается пиксель указанного цвета, то цвет этого пикселя заменяется на черный, а соответствующий бит маски - на 0. В результате, при прорисовке изображения пиксели указанного цвета становятся прозрачными. Аргументы этой функции также очевидны. Первый - хэндл списка изображений, второй - хэндл включаемого в список изображения, третий - цвет пикселей, которые необходимо сделать прозрачными, при прорисовке.
Для добавления в список иконки или курсора используется макрос ImageList_AddIcon(), первым аргументом которого необходимо указать хэндл списка изображений, а вторым - хэндл добавляемой иконки или добавляемого курсора. Макрос возвращает индекс добавленного изображения.
При необходимости программа может создать новую иконку или курсор, используя изображение и маску из списка изображений. Для этой цели необходимо использовать функцию ImageList_GetIcon(). Её описание приведено ниже:
WINCOMMCTRLAPI HICON WINAPI ImageList_GetIcon(HIMAGELIST himl,
int i, UINT nags);
Первый аргумент этой функции - хэндл списка изображений. Второй -индекс изображения, на основе которого будет создана иконка или курсор. Третий аргумент - флаги прорисовки, которые можно найти в таблице, приведенной при описании функции ImageList_Draw().
Функция возвращает хэндл созданной иконки или курсора.
К этому моменту мы научились добавлять изображения в список. А для удаления изображения нужно вызвать функцию ImageList_Remove(), описанную так:
WINCOMMCTRLAPI BOOL WINAPI ImageList_Removc(HIMAGELIST himl,
int i);
173
Автор уверен, что даже не заглядывая дальше, читатель догадался, что первым аргументом является хэндл списка изображений, а вторым -индекс удаляемого изображения. Если вместо индекса изображения подставить -1, то функция удалит все изображения из списка, но не удалит сам список. Для удаления всех изображений из списка можно воспользоваться макросом ImageList_RemoveAH(), единственным аргументом которого является хэндл списка изображений.
Для замены изображения в списке служит функция ImageList_Replace(). Её описание находим в файле commctrl.h:
WINCOMMCTRLAPI BOOL WINAPI ImagcList_Replace(HIMAGFLLIST him],
int i,
HBITMAP hbmlmage, HBITMAP hbmMask);
Аргументы этой функции вполне понятны: первый - хэндл списка изображений; второй - индекс замещаемого изображения; третий и четвертый - хэндлы нового изображения и его маски. Если список немаскированный, четвертый аргумент игнорируется.
Очередная функция - ImageList_ReplaceIcon() - описана так:
WINCOMMCTRLAPI int WINAPI ImageList_RcplaceIcon(HIMAGELIST himl,
int i. HICON hicon);
Нужно ли описывать аргументы этой функции?
Если в функции ImageEist_ReplaceIcon() второй аргумент заменить на -1, то иконка или курсор будут не замещать старое изображение, а добавляться в список. Этот нюанс используется в макросе ImageList_ AddlconQ, аргументами которого являются хэндл списка изображений и хэндл добавляемой иконки или курсора.
Вполне вероятно, что если второй аргумент функции ImageList_Replace() равен -1, то изображение не замещает старое, а добавляется. Проверку этой гипотезы я оставляю читателю.
Для того чтобы прорисовать изображение, хранящееся в списке, необходимо использовать функцию ImageListJDraw(). Вполне понятно, что для прорисовки изображения вызывается что-то типа функции BitBlt(), которая требует два контекста устройства, координаты и прочее. Вспомним, что контекст устройства, в котором хранится список изображений, у нас ' есть. Координаты отдельного изображения моментально вычисляются по его индексу. И что остается? Хэндл контекста, на кото-
174
рый будет копироваться изображение, координаты в этом контексте и флаги прорисовки.
Функция ImageList_Draw() имеет следующий прототип:
WINCOMMCTRLAPI BOOL WINAPI ImageList_Draw(HIMAGELIST himl,
int i,
HOC hdcDst, intx, int y, UINT (Style);
Сравните аргументы этой функции с теми, наличие которых мы вычислили чуть выше. На всякий случай поясню. Первый аргумент -хэндл списка изображений, второй - индекс изображения, третий - хэндл контекста, на который будет копироваться изображение, четвертый и пятый - координаты в этом контексте, начиная с которых будет скопировано изображение, и, наконец, шестой - именно флаги прорисовки. Они приведены в табл. 42.
Привлекательность использования списков изображений повышает еще одно обстоятельство. При использовании списков возможно использование специальных функций, позволяющих пользователю перемещать изображения (drag-and-drop) на экране: во-первых, с минимальными затратами на написание нового кода; во-вторых, без заметного мерцания.
Таблица 42. Флаги прорисовки отдельного изображения в списке изображений
Значение
ILD_NORMAL
ILD TRANSPARENT
ILD_BLEND25 ILD_BLEND50
ILD_MASK
ILD IMAGK
ILD OVERLAYMASK
ILD SELECTED
ILD FOCUS
ILD BLEND
0x0002 0x0004
0x0010
0x0020
OxOFOO
ILD BLEND50 ILD BLEND25 ILD~BLEND50
Обычное копирование изображения Каждый белый бит маски заставляет соответствующим бит изображения прорисовываться как прозрачный Снижение интенсивности цветов изображения на 25 %
Снижение интенсивности цветов изображения на 50 % Прорисовка маски Прорисовка изображения
175
Операция перемещения изображения начинается вызовом функции ImageList_BeginDrag(), прототип которой выглядит следующим образом:
WINCOMMCTRLAPI BOOL WINAPI ImageList_BcginDrag(IMAGELIST himlTrack,
int iTrack, int dxHotspot, int dyHotspot);
В число аргументов этой функции входят хэндл списка изображений, индекс перемещаемого изображения и координаты «горячего пятна» (hot spot'a - о проблемы перевода!) внутри изображения. Горячее пятно - это пиксель, по которому определяется точное положение изображения на экране. Обычно горячее пятно определяется таким образом, чтобы оно совпадало с горячим пятном курсора мыши. С этим горячим пятном нам еще предстоит помучиться.
Эта функция начинает операцию по перетаскиванию изображения, накладывая курсор мыши на изображение, определяя горячее пятно и прорисовывая изображение в начальной позиции. Помимо этого, функция также сообщает системе о том, что обновление каких-либо частей экрана запрещено.
Следом за функцией ImageList_BeginDrag() обычно используется функция ImageList_DragEnter(). Описание этой функции можно найти в файле commctrl.h:
WINCOMMCTRLAPI BOOL WINAPI ImagcList_DragEnter(IIWND hwndLock,
int x, int y);
Эта функция запрещает обновление указанного окна во время выполнения операции «drag-and-drop» и прорисовывает перемещаемое изображение в промежуточных позициях (до того, как будет отпущена клавиша мыши). В некотором смысле можно сказать, что эта функция делает изображение курсором мыши (конечно, нельзя понимать это буквально, сходство чисто внешнее). Первый аргумент этой функции понятен -хэндл окна, обновление которого запрещается. Этим окном является то окно, в котором производится перемещение изображения. Второй и третий аргументы определяют координаты той точки, в которой необходимо прорисовать изображение. ВНИМАНИЕ! В данном случае необходимо указывать координаты оконные, а не координаты в рабочей области окна. Таким образом, до обращения к этой функции необходимо определить ширину границы окна, высоту заголовка и, при необходимости, ширину полосы меню. В демонстрационной программе это сделано при
176
функции GetSystemMetrics(). Рекомендую читателю изучить эту функцию самостоятельно.
Функция ImageListJDragMove() описана следующим образом:
WINCOMMCTRLAPI BOOL WINAPI I mage I ist_DragMove(int x, int y);
Эта функция перемещает изображение, но не прорисовывает его. Попробуйте в демонстрационной программе убрать функцию lmageList_DragEnter() и посмотреть, что получится.
Последней функцией, обеспечивающей «drag-and-drop», является lmageList_Endnrag(). Эта функция завершает перемещение, но не разрешает обновление окна и не производит прорисовку перемещенного изображения. Для разрешения обновления окна необходимо вызвать функцию ImageList_DragLeave(), передав ей в качестве аргумента хэндл окна, а затем прорисовать изображение, например, с помощью функции ImageList Draw().
Л теперь, как всегда, демонстрационная программа. В этой программе при создании окна производится прорисовка двух икон в левой верхней части рабочей области. Иконки могут быть скопированы в другое место при использовании операции «drag-and-drop». Думаю, читателю не составит труда при необходимости изменить эту программу так, чтобы иконки не копировались, а перемещались.
При разработке программы я допустил определенного рода плагиат. Одну из иконок я «выдрал» из программы pview95, другую - из примера mixtree, поставляемых с Borland Си- 5.0. Если читатель захочет, то он легко может заменить иконки на свои.
//include <windo\vs.h> //include <commctrl.h>
//define CX_ICON 32 //define CYJCON 32
UINSTANCE hlnst;
LRLSULT CALLBACK ImageListWndProc ( HWND, UINT, UINT, LONG );
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hl'revlnstancc, LPSTR IpszCmdParam, int nCmdShow )
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msg;
char szClassNamcf] ^ "ImageList";
177
hlnst = hlnstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style = CS_HREDRAW | CS_VREDRAW;
WndClass.lpfnWndProc = ImageListWndProc;
WndClass.cbClsExtra = 0;
WndCIass.cbWndExtra = 0;
WndClass.hlnstance = hlnstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH) GetStockObjcct (WHITE_BRUSH);
WndClass.lpszMenuName = "";
WndClass.lpszClassName = szClassName;
if ( !RegisterClass(&WndClass)) {
MessageBox(NULL,"Cannot register class","Error",MBJ3K); return 0;
}
hWnd = CreateWindow(szClassName, "Image List Demo Program",
WS_OVERLAPPEDWINDOW, CWJJSEDEFAULT, CWJJSEDEFAULT, CW_USEDEFAULT, CWJJSEDEFAULT, NULL, NULL, hlnstance.NULL);
if(!hWnd)
{
MessageBox(NULL,"Cannot create window», «Error», MB_OK); return 0; }
InitCommonControlsO; /* Show our window */ ShowWindow(hWnd,nCmdSliow); UpdateWindow(hWnd);
/* Beginning of messages cycle */
whi!e(GetMessage(&Msg, NULL, 0, 0))
t i
TranslateMessage(&Msg); DispatchMessage(&Msg);
} return Msg.wParam;
LRESULT CALLBACK ImageListWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
<
static HIMAGELIST hlmageList;
static int i;
static HOC hDC, hPaintDC;
178
PAINTSTRUCT PaintStruct;
RECT rBigRect = {0, 0, CXJCON * 2, CYJCON};
RECT rLittleRecll = {0, 0. CX_ICON, CYJCON};
static POINT Point, pHotSpot;
static BOOL bCapture = FALSE;
static int nXBordcr, nYBordcr. nYCaption;
switch(Message) {
case WM_CREATE: hDC = GctDC(hWnd);
hlmageList - IniageListJJreate(CXJCON, CYJCON, ILCJvIASK, 3, 3); ImageList_AddIcon(hImageList, Loadlmage(hlnst, "Mixtree.ico",
IMAGEJCON, 0, 0, LRJJ3ADFROMFILE)); ImageList_AddIcon(hImageList, Loadlmage(hlnst, "Pview95.ico",
IMAGEJCON, 0, 0, LRJ.OADFROMFILE)); nXBordcr = GetSystemMclrics(SM_CXBORDER); nYBorder = GetSystemMctrics(SMJCYBORDER); nYCaption = GetSystemMetrics(SMJCYCAPTION); return 0;
case WMJ'AINT:
hPaintDC = BcginPaint(hWnd, &PaintStruc(); for(i = 0; i < 2; i++) Image-ListJ)raw(hImageList, i, hPaintDC, i * CXJCON, 0,
ILD_NORMAL); EndPaint(hWnd. &PaintStruct); return 0;
case WMJ.BUTTONDOWN: Point.x = LOWORD(lParam); Point.y = HIWORD(lParam); if(PtInRect(&rBigRect, Point)) >
SetCapture(hWnd); bCapture = TRUE; if(Pt!nRect(&rLittlcRectI, Point))
i = 0; else
i= I;
pHotSpot.x = Point.x - i * CXJCON; pHotSpot.y = Point.y;
ImageListJ3eginDrag(hImageList, i, pHotSpot.x, pHotSpot.y); ImageListJJragEnterfhWnd, Point.x + nXBordcr, Point.y + nYBorder +
nYCaption); }
return 0;
case WMJVIOUSEMOVE: iffbCapture) ImageListJ>agMove(LOWORD(!Param), HIWORD(lParam) + nYBorder
-i- nYCaption); return 0;
179
case WM_LBUTTONUP: if(bCapture)
ImageListJSndDragO;
ImageList_DragLeave(hWnd);
ImageListJ>aw(hImageList, i, hDC, LOWORD(lParam)- pHotSpot.x,
HIWORD(lParam) - pHolSpot.y, ILDJMORMAL); ReleaseCapturc(); bCapture = FALSE;
}
return 0;
case WM_DESTROY: ReleascDC(hWnd, hDC); ImageList_Destroy(hImageList); PostQuitMessage(O); return 0;
\ j
return DefWindowProc(hWnd,Message,wParam, IParam); >
Вид окна до произведения операций «drag-and-drop» показан на рис. 16. На рис. 17 приведен вид этого же окна после выполнения нескольких операций копирования иконок.
Мне бы хотелось, чтобы читатель обратил внимание на возможность легкого перемещения и копирования изображений. Что бы пришлось делать в том случае, если бы здесь не использовался список изображений?
В Image List Demo Progiam
Рис. 16. Окно с двумя изображениями in списка изображений до операции "drag-and-drop"
180
Ш Image List Demo Program
mmm\
Рис. 17. Предыдущее окно после нескольких операций "drag-and-drop"
РАБОТА С ЗАКЛАДКАМИ
Достаточно интересным элементом управления, появившимся только в Win32, являются закладки. Их появление, как и появление большинства общих элементов управления, давно ожидалось. Этот элемент действует подобно «алфавиту» в записной книжке, при выборе определенной буквы (в данном случае - определенной закладки) всплывает нужная страница (в данном случае - диалоговое окно). Читатель вспомнит, надеюсь, интерфейс электронной таблицы Excel 5.0 и рабочие листы в ней, которые можно было перебрать с помощью переключателей в нижней части таблицы. Эти переключатели и являлись закладками (tab control'ами). Связав каждую из закладок со страницей информации, возможно разместить несколько С'фаниц информации на одном и том же месте. Специальный тип закладок действует как кнопки - при выборе закладки вместо отображения очередной страницы просто производится посылка команды.
К сожалению, и в этом случае специальной функции для создания окна не предусмотрено. Программист должен использовать одну из функций - CreateWindowQ или CreateWindowExQ. При этом в качестве имени класса необходимо указать макрос WC_TABCONTROL, который описан в файле commctrl.h следующим образом:
#ifdef_WIN32
#define WC_TABCONTROLA
"SysTabControl32"
181
#detme WC_TABCONTROLW
#ifdef UNICODE
#define WC_TABCONTROL
#elsc
#define WC_TABCONTROL
#endif
#elsc
#dcfine WC_TABCONTROL
#cndif
L"SysTabControl32" WC TABCONTROLW WC'_TABCONTROL.A
"SysTabControl"
При создании окна с закладками могут использоваться как общие стили, применяющиеся для всех окон, так и стили, специфические для закладок (табл. 43).
Таблица 43. Стили закладок
Стиль
TCS_TABS
tcs_singleline
tcs rightjustify tcs"forceiconleft
tcs_forcelabelleft tcs_buttons tcs multiline tcs_fixedwidtm
tcs raggfdright
tcs focusonbuttondown
tcsjavnfrdrawfixed
tcs_. tooltips tcs focusnever
0x0020 0x0100 0x0200 0x0400
0x0800 0x1000 0x2000
0x4000 0x8000
Закладки являются закладками, а не
кнопками
Закладки располагаются в одну линию и
при необходимости скроллируются с
помощью up-down control'a
Иконка сдвинута к левому краю заклад-ки, текст центрирован И текст, и иконка сдвинуты к левому краю закладки
Закладки выглядят и действуют как кнопки
Закладки при необходимости располагаются в несколько строк Все закладки имеют одинаковую ширину
За прорисовку закладок отвечает прикладная npoipaMMa. а не система Задержка курсора мыши на одной из закладок вызывает появление подсказки Закладка никогда не получает фокуса ввода
После создания окна с закладками, необходимо определить каждую закладку, в которой могут быть иконка, заголовок (текст) и дополнитель-
182
ные данные, определяемые приложением. Для этого в программе нужно заполнить столько структур типа ТСМТЕМ, сколько закладок планируется создать. Структура ТСМТЕМ определена в файле commctrl.h:
typedcf struct JTCJTEMA
I
UINTmask; UINTlpRcservedl; UINT lpRcservcd2; LPSTR pszTcxt: inl cchTexlMax, int ilmagc:
LPARAM IParam; | TCJTEMA;
lypedef struct _TC_ITEMW {
UINT mask;
UINTlpRcservedl;
UINT lpRescrved2;
LPWSTR ps/Text;
int ccliTcxtMax;
int ilmage;
LPARAM IParam; } TCJTEMW;
#ifdefUNICODE
#define TCJTEM
#elsc
Adeline TCJTEM
#endif
TCJTEMW TC ITF.MA
Как следует из названий, поля IpReservedl и lpReserved2 не используются, они зарезервированы Microsoft для применения в будущем.
В поле mask этой структуры указывается, какие данные определяют внешний вид закладки. Это поле может принимать значения, приведенные в табл. 44.
После того, как читатель ознакомился с этой таблицей, назначения полей pszText, ilmage и IParam, надеюсь, стали понятны. Единственное поле, оставшееся нерассмотренным, - это cehTextMax. В случае, если структура типа ТСМТЕМ используется для получения информации о закладке, в поле cehTextMax определяется размер буфера, на который указывает pszText.
Т а б л it ц а 44. Битовые флаги, определяющие внешний вид и поведение закладок
183
Флаг |
Значение |
Описание |
TCIFJTEXT TCIFJMAGE TCIFJITLREADING TCIF_PARAM |
0x0001 0x0002 0x0004 0x0008 |
Поле pszText заполнено, в нем хранитея указатель на строку - заголовок закладки или па буфер, в который будет записана информация Полк ilmage заполнено, в нем хранится индекс отображаемого на закладке изображения в списке изображений или -1, если список изображений не используется Текст отображается справа налево, как, например, в арабском языке Поле IParam заполнено и содержит данные, определяемые приложением |
typedef struct JTCJTEMHEADERA
{
UINT mask;
UINTlpRescrvedl;
UINT ]pReserved2;
LPSTR pszText;
int cchTextMax;
int ilmage: } TCJTEMHEADERA;
typedef struct JTCJTEMHEADERW
{
UINT mask;
UINTipReservedl;
UTNT lpRescrved2;
LPWSTR pszText;
int cchTextMax;
int ilmage; } TCJTEMHEADERW;
tfifdcf UNICODE
«define TCJTEMHEADER
Seise
#definc TCJTEMHEADER
#endit'
TCJTEMHEADERW TC ITEMHEADERA
Как всегда, для управления окном с закладками используются сообщения. Их список приведен в табл. 45.
184
Таблица 45. Сообщения, посылаемые закладкам
Сообщение
Значение
Описание
TCMJ4RST
ТСМ GETIMAGELIST
ТСМ SETIMAGELIST
ТСМ GETITEMCOUNT
ТСМ GETITEM
ТСМ SETITEM
ТСМ INSERTITEM
ТСМ DELETEITEM
ТСМ DELETEALLITEMS
TCM_GETITEMRECT ТСМ GETCURSEL
ТСМ SETCURSEL
ГСМ HITTEST
0x1300 ТСМ FIRST+ 2
ТСМ FIRST+ 3
ТСМ FIRST+ 4
ТСМ FIRST+ 8
ТСМ FIRST+ 9
TCMJ4RST+ 10 TCMJ4RST + 11
ТСМ FIRST + 12
ТСМ FIRST + 13
Получить хэндл используемого совместно с закладками списка изображений, wParam и IParam = О, возвращается хэндл списка изображений
Связать список изображений с закладками, wParam = 0, IParam хэндлу списка изображений, возвращается хэндл предыдущего списка изображений
Получить число закладок, wParam = О, IParam — 0, возвращается число закладок
Получить информацию о закладке, wParam = индексу закладки, IParam указателю на структуру типа ТС JTEM, в которую будет записана информация
Установить атрибуты закладки, wParam = индексу закладки, IParam указателю на структуру типа TCJTEM, которая определяет атрибуты
Вставить закладку, wParam - индексу новой закладки, IParam = указателю на структуру тина TCJTEM, возвращается индекс новой закладки или -I Удалить закладку, wParam = индексу закладки, IParam = 0, возвращается TRUE при успешном выполнении Удалить все закладки, wParam — О, IParam = 0, возвращается TRUE при успешном выполнении
Получение индекса текущей закладки, wParam = 0, IParam = О Установка заданной закладки текущей, wParam = индексу закладки, возвращается индекс ранее выбранной закладки
185
Окончание таил, 45
Сообщение |
Значение |
Описание |
TCM_SETITEMEXTRA |
ТСМ FIRST + 14 |
Установка размера дополнительных |
|
|
данных для закладки, wParam — числу |
|
|
байт, выделяемых для дополнитель- |
|
|
ных данных |
ТСМ ADJUSTRECT |
ТСМ FIRST ^40 |
|
ТСМ SETTTEMSIZE |
ТСМ FIRST + 41 |
|
ТСМ REMOVEIMAGE |
ТСМ FIRST -.-42 |
|
ТСМ SETPADDING |
ТСМ FIRST + 43 |
|
ТСМ GETROWCOUNT |
ТСМ FIRST + 44 |
|
ТСМ GETTOOLTIPS |
ТСМ FIRST + 45 |
|
ТСМ SETTOOLTIPS |
ТСМ FIRST + 46 |
|
ТСМ GETCURFOCUS |
ТСМ FIRST + 47 |
|
ТСМ SETCURFOCUS |
ТСМ FIRST + 48 |
|
Сообщение TCM_SETITEMEXTRA может в работе окна с закладками использоваться только один раз и только до момента добавления первой закладки. Некоторые сообщения в этом списке, которые могут использовать параметры как в Unicode, так и в ANSI-кодировках, сами являются макросами. В таких случаях в графе «Значение» оставлен пропуск. Для примера ниже приведено описание макроса TCM_GETITEM:
#defme TCM_GETITEM/\
#deime TCM_GETITEMW
ffifdef UNICODE
#defme TCM_GETITEM
#eise
«define TCM_GETITEM
#endif
(TCM_FIRST + 5)
(TCM_FIRST + 60)
TCM GETITEMW TCM GETITEMA
В отличие от других элементов управления, для окна с закладками разработаны специальные макросы, которые облегчают работу с сообщениями. Вместо привычного SendMessage(...) можно использовать соответствующие макросы, о которых будет сказано дальше.
Имя каждого макроса образуется из имени сообщения:
1. От имени сообщения отбрасывается префикс ТСМ_.
2. Все слова оставшейся части изменяются таким образом, что прописной остается только первая буква слова, а все остальные делаются строчными, например, Getltem, Insertltem и т. д.
3. К полученному добавляется префикс TabCtrl_, например, TabCtrl Getltem, TabCtrl Insertltem.
186
Сообщение, которое посылается тем или иным макросом, определяется именем этого макроса. Каждый макрос может содержать один, два или три параметра. Число аргументов определяется очень просто - (число параметров сообщения, не равных нулю) + I. Если у сообщения wParam и IParam равны 0, то у макроса определен только первый аргумент, если определен только wParam - макрос требует наличия двух аргументов. Первым аргументом макроса всегда является хэндл окна, которому посылается сообщение, т. е. хэндл окна с закладками. Второй и третий аргументы (при необходимости) - это wParam и IParam сообщения соответственно. К примеру, макрос TabCtrl_DeleteAHItems() имеет один аргумент, макрос TabCtrl Deleteltem() - два аргумента, TabCtrl Insertltem() - все три аргумента.
Итак, с управляющими сообщениями и макросами все ясно. А как обстоит дело с получением информации о том, что выбрана одна из закладок'.' Если пользователь что-то сделал с закладкой, то закладка посылает родительскому окну сообщение WMJNOTIFY, при этом wParam этого сообщения содержит идентификатор элемента управления, a
IParam
- указатель на структуру типа NMHDR. Эта структура описана в файле winuser.h и имеет вид, приведенный ниже:
typedef struct lagNMHDR i
HWND hwmlFrom;
UINT idFrom;
HINT code: //NM_code } NMHDR; typedcfNMHDR FAR * LPNMHLJR:
Первое поле этой структуры - hwndFrom - содержит хэндл элемента управления, который послал сообщение WMJNOTIFY. Второе поле -idFrom - идентификатор элемента управления. Третье поле - code - содержит код нотификации, т. е. код того действия, которое было произведено с элементом управления. В случае закладки это может быть один из двух кодов - TCN SF.LCHANGING или TCN SELCHANGE.
Сообщение с кодом TCN SELCHANGING посылается после того, как пользователь произвел действие, но до изменения состояния закладки. Это сообщение может быть использовано, скажем, для того, чтобы сохранить информацию, введенную пользователем в диалоговом окне, связанном с закладкой. После того, как состояние закладки изменилось, посылается сообщение TC'N_SELCHANGE. При получении этого сообщения программа может произвести какие-либо действия по формированию вновь отображае-,'ой страницы.
187
Но использование закладок именно тем и осложнено, что каждая из закладок обычно связана с диалогом. Другими словами, при выборе новой закладки старое диалоговое окно должно исчезнуть, а на его месте должно появиться новое. Так как каждая из закладок может быть выбрана в любой момент, то с закладками должны быть связаны немодальные диалоги. Для того чтобы отобразить впоследствии диалог, связанный с невыбранной закладкой, программа должна сохранять состояние диалога. В качестве примера приведена небольшая программа, иллюстрирующая работу с закладками. Эта программа создает три закладки. При размещении курсора мыши над любой из них возникает подсказка (помните, при разборе окон подсказок я обещал, что продемонстрирую их использование при описании закладок?). При выборе любой из закладок отображается окно диалога, связанное с этой закладкой. В программе используется файл ресурсов:
Dialog I DIALOG 2, 40, 250, 108
STYLE DSJDLOOK DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE
FONT 8, "MS Sans Serif'
{
DEFPUSHBUTTON "OK". IDOK, 24, 65, 50, 14
CONTROL "This is a text in the first dialog", -1, "static". SSJ.EFT | WS_CHILD | WS VISIBLE, 20, 13,96,9
Dialog2 DIALOG 2, 40, 250, 108
STYLE DS_3DLOOK DS_CONTEXTHELP
FONT 8. "MS Sans Serif"
WS POPUP I WS VISIBLE
DEFPUSHBUTTON "OK", IDOK, 24, 65, 50, 14
CONTROL "This is a text in the second dialog", -1, "static", SS_LEFT | WSJTHILD | WS VISIBLE, 20, 13, 120.9
Dialog3 DIALOG 2, 40, 250, 108
STYLE DS_3DLOOK | DSJTONTEXTHELP
FONT 8, "MS Sans Serif
WS POPUP I WS VISIBLE
DEFPUSHBUTTON "OK", IDOK, 24, 65, 50, 14
CONTROL "This is a text in the third dialog", -1, "static", SS_LEFT | WS_CHILB WS VISIBLE, 20, 13,96,9
Ниже приводится текст программы:
#includc <windows.h> ^include <commctrl.h> ^include <stdio.h>
188
HINSTANCE hlnst; HWND hWnd;
I.RESULT CALLBACK TabControlWndProc ( HWND. UINT, UINT, LONG ); BOOL CALLBACK DialogProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrevInstance,
LPSTR IpszCmdParam, int nCmdShow ) }
WNDCLASS WndClass ;
MSG Msg;
char szClassName[] = "TabControl";
hlnst = hlnstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.stylc = CSJTREDRAW | CS_VREDRAW;
WndClass.lpfnWndProc = TabControlWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass. hlnstance ~ hlnstance ;
WndCIass.hlcon = Loadlcon (NULL.IDI_APPLICATION);
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH) GetStockObjcct (LTGRAY_BRUSH);
WndClass. Ips/McmiNamc = "";
WndClass. IpszClassName = szClassName;
if ( !RegisterClass(&WndClass) ) {
McssageBox(NULL,"Cannot register class". "Error", MB__OK); return 0;
liWnd = CrcateWindow(szClassName, "Tab Control Demo Program",
WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
CW USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, hfnstance,NULL); ifllhWnd) {
Message I.iox(NULL, "Cannot create window", "Error", MB_OK); return 0;
fnitC'oinmonControls(); /* Show our window */
ShowWmdow(hWnd,nCmdShow); UpdateWindow(hWnd);
189
/* Beginning of messages cycle */
while(GetMessage(&Msg, NULL, 0, 0))
{
TranslateMessage(&Msg); DispatchMessage(&Msg);
} return Msg.wParam;
LRESULT CALLBACK TabControIWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
{
HWND hTabControlWnd; RECT Rect; LPNMHDR IpNMHdr; LPTOOLTIPTEXT IpTooITipText; static HWND hDlg = 0; int nTab; TC_ITEM TCJtem;
switch(Message)
{
case WM_CREATE: GetClientRect(hWnd, &Rect);
hTabControlWnd = CreateWindow(WC_TABCONTROL, "", WS_ VISIBLE |
WS_TABSTOP | WS_CHILD | TCS_TOOLTIPS, 0, 0, Rect.right, Rect.bottom, hWnd, NULL, hlnst, NULL); TCJtem.mask = TCIFJTEXT; TC_Item.iImage = -1 ; TC_Item.pszText = "The first dialog"; TabCtrl_InsertItem(hTabControlWnd, 0, &TC_Item); TC_Item.pszText = "The second dialog"; TabCtrl_InsertItem(hTabControlWnd, 1, &TCJtem); TC_Item.pszText = "The third dialog"; TabCtrl_InsertItem(hTabControlWnd, 2, &TC_Item); hDlg = CreateDialog(hInst, "Dialog 1", hTabControlWnd, DialogProc); return 0;
case WMJMOTIFY: IpNMHdr = (LPNMHDR) IParam; switch(lpNMHdr->code) {
case TTN_NEEDTEXT: IpToolTipTcxt = (LPTOOLTIPTEXT) IParam; sprintf(lpToolTipText->lpszText, "Tip about tab No %d",
lpToolTipText->hdr.idFrom); break; caseTCN SELCHANGE:
190
if(hDlg)
Destroy Window(hDlg);
nTab = TabCtrl_GetCurSel( (HWND) lpNMHdr->hwndFrom); switch(nTab)
{
case 0:
hDlg = CreateDialog(hInst, "Dialog 1", hTabControlWnd, DialogProc); break; case 1:
hDlg = CreateDialog(hInst, "Dialog2", hTabControlWnd, DialogProc); break; case 2:
hDlg = CreateDialog(hInst, "Dialog3", hTabControlWnd, DialogProc); break; } break;
x t
return 0;
case WM_DESTROY: PostQuitMessage(O); return 0;
} return DefWmdo\vProc(hWnd,Message,wParam, IParam);
BOOL CALLBACK DialogProc(HWND hDlg, UINT Message,
WPARAM wParam, LPARAM IParam) {
switch(Message) {
case WM_COMMAND: PostQuitMessage(O); return I;
} return 0;
Вид окна, создаваемого программой, приведен на рис. 18.
В этом примере не была использована одна из возможностей, которая резко улучшает внешний вид программы. В закладках можно использовать изображения из созданного ранее списка изображений. Я этого не сделал. В качестве упражнения рекомендую читателю самостоятельно добавить изображения к закладкам.
Ещё один элемент управления рассмотрен. А сколько их осталось? Рассмотрим еще один интересный и достаточно сложный элемент управления, который называется окно просмотра деревьев (Tree View control).
191
В Tab Control Demo Program
The first dialog The second dialog The third dialog j
.'--•""• -This is: a text in the fiist dialog
OK
Рис. 18. Окно с закладками
РАБОТА С ОКНОМ ПРОСМОТРА ДЕРЕВЬЕВ
Окно просмотра деревьев используется для просмотра списка объектов, имеющего иерархическую структуру, как, например, файлы на диске. Ярким примером использования окна просмотра деревьев является Explorer, который использует окна этого типа для отображения структуры информации на диске.
И этот элемент управления не имеет специальной функции для своего создания, т. е. для того, чтобы создать окно просмотра деревьев, программа должна использовать функции CreateWindowQ или CreateWindowEx(). При этом в качестве имени класса создаваемого окна необходимо использовать макрос WC_TREEVIEW, который описан в файле commctrl.h следующим образом:
#ifdef_WIN32
#define WCJTREEVIEWA
#define WC_TREEVTEWW
#ifdef UNICODE
#define WC_TREEVir-W
#else
"SysTrecVicw32" E"SysTrecVicw32"
WC TREEVIEWW
192
tfdetinc WC_TREEVIEW
#endif
#clse
«define WC_TREEVIEW
#endif
WC TREEVIEWA
"SysTreeView"
При создании окна просмотра деревьев можно использовать несколько стилей, разработанных специально для окон данного типа. Все эти стили приведены в табл. 46.
На некоторых из этих стилей остановимся более подробно. Начнем со стиля Т VS_H AS BUTTONS. Если читатель запустит Explorer, то увидит, что у директорий, содержащих внутри себя еще что-то, справа есть небольшой квадратик. Это и есть те кнопки, наличие которых и предполагает стиль. Если элементы следующего для директории уровня отображаются, то внутри квадратика содержится знак «+», в противном случае -знак «-». Для сворачивания и разворачивания ветви дерева необходимо щелкнуть кнопкой мыши на этой кнопке. К сожалению, этот стиль не добавляет кнопки к элементам наивысшего уровня. Для того чтобы появились кнопки и у этих элементов, необходимо комбинировать стили TVS_HASLINES, TVS_LINESATROOT и TVS_HASBUTTONS.
При использовании стиля TVSJHASLINES есть одна особенность. Линиями соединяются только родительские и дочерние элементы.
Таблица 46. Стили окна просмотра деревьев
Стиль
Значение
Описание
TVS_HASBUTTONS
TVSJ-IASLINES
TVSJJNESATROOT TVS_EDITLABELS TVSJ3ISABLEDRAGDROP TVS SHOWSELALWAYS
0x0001
0x0002
0x0004 0x0008 0x0010 0x0020
К элементам, имеющим дочерние элементы, слева добавляютея небольшие кнопки, позволяющие раскрывать и закрывать список подчиненных элементов Дочерние элементы списка соединяются с родительским элементом линиями, элементы высшего уровня не соединяются Элементы высшего уровня соединяются друг с другом
Названия элементов списка могут быть изменены
Запрещает операции drag-and-drop с элементами списка
Выбранные элементы остаются таковыми даже тогда, когда окно теряет фокус
193
Таблица 47. Сообщения, посылаемые окнам просмотра деревьев
Сообщение
Опиеание
TV_FIRST
TMVJNSERTTTEM
TVM_DELETEITEM
TVM_EXPAND
TVM_GETITEMRECT
TVM_GETCOUNT
TVM_GETINDENT TVM_SETINDENT TVM GETIMAGEEIST
TVM_SETIMAGELIST TVM GETNEXTITEM
TVM_SELECTITEM
TVM_GETITEM
TVM_SETITI-M
TVMJZDITLABEL
TVM_GETEDITCONTROL
TVMJ3ETVISIBLECOUNT
TVMJ1ITTEST
TVM_CREATEDRAGIMAGE
TVM_SORTCHILDREN
TVM_ENSUREVISIBLE TVM SORTCHILDRENCB
tvm endeditlabelnow tvm' "getisearchstring
TV_F1RST -i- 1 TV_FIRST + 2 TV_FIRST + 4
TVJTRST + 5
TV_FIRST + 6 TV_FIRST + 7 TV FIRST 1-8
TVJTRST + 9 TV_FIRST+ 10
TV FIRST + 11
TV FIRST- 15 TVJ-TRST + 16
TV_FIRST+ 17 TVJFIRST- 18 TVJ4RST+ 19
TV_FIRST + 20 TV FIRST+ 21
TV FIRST + 22
Вставка элемента в список Удаление элемента из списка «Распахнуть» или «свернуть» элемент Получить ограничивающий прямоугольник для элемента списка Вернуть количество элементов в списке
Получить значение отступа Установить значение отступа Получить хэндл списка изображений, связанного с окном просмотра деревьев
Связать список изображений с окном просмотра деревьев Получить дополнительную информацию об элементе, находящемся с текущим в определенных отношениях (следующий, родительский, первый дочерний и так далее) Сделать элемент' текущим Получить информацию об элементе Установить параметры элемента Изменитьтекст элемента
Получить число видимых элементов списка
Отсортировать дочерние элементы в алфавитом порядке
Отсортировать дочерние элементы в соответствии с критерием, определенным программой
Элементы наивысшего уровня друг с другом не соединяются, т. е. визуально отображаются несколько отдельных деревьев. Если пользователь хочет, чтобы отобразилось единое дерево, необходимо указать комбинацию стилей TVS J1ASLINES и TVS LINESATROOT.
Раз окно просмотра деревьев является окном (прошу извинить меня за тавтологию), то обмен информацией с этим окном и управление им
194
осуществляется е помощью сообщений. Список всех возможных сообщений, используемых при работе с окном просмотра деревьев, приведен в табл. 47.
Как и в случае с закладками, посылка сообщений дереву просмотра деревьев может быть осуществлена с помощью макросов. Имена макросов для сообщений формируются точно так же, как и в случае закладок. Единственное отличие состоит в том, что в качестве префикса используется не TabCtrl, a Tree View.
А теперь, после краткого знакомства с сообщениями, применяемыми в работе с окнами просмотра деревьев, рассмотрим некоторые из этих сообщений, наиболее части применяемые в прикладных программах.
Для того чтобы вставить элемент в синеок, необходимо послать окну просмотра деревьев сообщение TVMJNSERTITEM или, что то же самое, использовать макрос TreeView Insertltem. При этом параметр wParam должен быть равным 0, a IParam должен содержать указатель на структуру типа TVJNSERTSTRUCT. Эта структура описана в файле commctrl.h так:
typedef struct JTVJNSERTSTRUCTA (
HTREEITEM hParent:
HTREEITEM hlnsertAt'tcr;
TVJTEMA item; } TVJNSERTSTRUCTA, FAR *LPTV_INSERTSTRUCTA;
typedef struct _TV INSF.RTSTRUCTW {
HTREEITEM hParent;
HTREEITEM hlnsertArter;
TVJTEMW item; } TVJNSERTSTRUCTW, FAR *LPTVJNSERTSTRUCTW;
#ifdef UNICODE
«define TVJNSERTSTRUCT
Adeline LPTVJNSERTSTRUCT
#clse
#deime TVJNSERTSTRUCT Adeline LPfvjNSERTSTRUCT
#endif
TVJNSERTSTRUCTW LPTVJNSERTSTRUCTW
TV INSERTSTRUCTA LPTV INSERTSTRUCTA
Поле первое - hParent - хэндл родительского элемента. Если этот элемент равен TVI_ROOT или NUEE, то элемент не имеет родителей и добавляется в список наивысшего уровня.
Второе поле - hlnsertAfter - определяет хэндл элемента, после которого вставляется новый элемент. Помимо этого, поле может принимать следующие значения:
195
TVI_FIRST - элемент вставляется в начало списка;
TVI_LAST - элемент вставляется в конец списка;
TVI~SORT - элемент вставляется в список в алфавитном порядке.
Третье поле - item - описывает непосредственно вставляемый элемент. Он представляет собой очередную структуру (структура в структуре!). Тип этой структуры - TVJTEM - описан в файле commctrl.h:
typedef struct JTVJTEMA {
UINT mask;
HTREEITEM hltem;
UINT state;
UINT stateMask;
LPSTR pszText;
int cchTextMax;
int ilmage;
int iSelectedlmage;
int cChildren;
LPARAM IParam; } TVJTEMA, FAR "LPTVJTEMA;
typedef struct _TV_ITEMW {
UINT mask;
HTREEITEM hltem;
UINT state;
UINT stateMask;
LPWSTR pszText;
int cchTextMax;
int ilmage;
int iSelectedlmage;
int cChildren; LPARAM IParam; } TVJTEMW, FAR *LPTV_ITEMW;
Таблица 48. Флаги, определяющие в каком поле структуры типа TV_ITEM содержится (или куда должны записываться) информация
#ifdef UNICODE
#defme TVJTEM
#definc LPTVJTEM
#else
#definc TVJTEM tfdefine LPTVJTEM
#endif
TVJTEMW LPTVJTEMW
TVJTEMA LPTV ITEMA
Теперь наберемся сил и рассмотрим структуру типа TVJTEM. Это поможет нам понять, что представляет собой элемент списка. Кроме этого, при её рассмотрении мы выясним, какого рода информацию об элементе списка можно получить, так как структура именно этого типа используется и для получения информации об элементе.
196
Поле |
Значение |
Описание |
TVIF TEXT |
0x000 1 |
Информация содержится в полях pszText и |
|
|
cchTextMax |
TVIF IMAGE |
0x0002 |
Информация содержится в поле ilmage |
TVIF PARAM |
0x0004 |
Информация содержится в поле IParam |
TVIF_STATE |
0x0008 |
Информация содержится в полях state и |
|
|
staleMask |
TVIF HANDLE |
0x00 1 0 |
Информация содержится в поле hltem |
TVIF SELECTEDIMAGE |
0x0020 |
Информация содержится в поле iSelectedltem |
TVIF CHILDREN |
0x0040 |
Информация содержится в поле cChildren |
Дело за малым - выяснить, что может храниться в каждом из этих полей.
hltem - хэндл элемента, информация о котором содержится в структуре.
Поле state определяет флаги состояние элемента, а поле stateMask -какое состояние элемента должно быть установлено или получено. Поле state может принимать значения, приведенные в табл. 49.
Т а б л и ц а 49. Флаги, определяющие внешний вид и состояние окна просмотра деревьев
|
Состояние |
Значение |
Описание |
|
TVIS_FOCUSED |
0x000 1 |
Элемент получил фокус ввода, т. е. он обрам- |
|
|
|
лен стандартным прямоугольником |
|
TVIS SELECTED |
0x0002 |
Элемент выбран |
|
TVIS CUT |
0x0004 |
Элемент выбран для операции копирования |
|
TVISJ3ROPHILITED |
0x0008 |
Элемент выбран как место назначения для |
|
|
|
операции drag-and-drop |
I |
TVIS BOLD |
0x00 1 0 |
Текст элемента написан жирным шрифтом |
|
TVISJZXPANDED |
0x0020 |
Дочерние элементы списка видны, т. е. |
|
|
|
элемент «распахнут» |
|
TVIS EXPANDEDONCE |
0x0040 |
Элемент «распахивался» минимум один раз |
|
TVIS OVERLAYMASK |
OxOFOO |
|
|
TVIS STATEIMAGEMASK |
OxFOOO |
|
|
TVIS USERMASK |
OxFOOO |
То же, что и предыдущее |
197
Очередное поле - IpszText - содержит указатель на строку, появляющуюся в элементе списка. Помимо этого, поле может иметь значение LPSTR_CALLBACK, в этом случае родительское окно отвечает за формирование текста элемента.
Если структура используется для получения информации об элементе, поле IpszText содержит указатель на буфер, в который будет записан текст элемента. В этом случае поле cchTextMax определяет размер выделенного буфера.
Поля ilmage и iSelectedlmage определяют индексы изображений, использующихся с элементом, для прорисовки невыбранного и выбранного элементов. Если значение поля равно IJMAGECALLBACK, то за формирование изображения отвечает родительское окно.
Поле cChildren имеет значение, равное единице, в том случае, когда у элемента есть дочерние элементы. В противном случае значение этого поля равно нулю.
И наконец, последнее поле - IParam - хранит данные, связанные с элементом. Об этих данных мы уже говорили при обсуждении окон списков.
На этом заканчивается рассмотрение параметров сообщения TVMJNSERTITEM. Много ли еще подобных структур ждет нас? Конечно^ работает все это эффективно и эффектно (самоё Wmdows'95 и Windows NT тому подтверждение!), но, по-моему, иногда фирме Microsoft неплохо было бы подумать и о тех, кто будет изучать ее творения! ( © )
Мы изучили только параметры сообщения. Еще нужно узнать, что возвращает функция, с помощью которой было послано данное сообщение. А возвращает она при успешном завершении хэндл элемента, а в случае неудачи - NULE.
Те же действия, как читатель уже догадался, могут быть произведены с помощью макроса TreeViewJmsertItem().
После изучения структуры типа TVJTEM, рекомендую читателю самостоятельно изучить работу сообщений TVM GETITEM и TVM SETITEM. В них тоже используется структура этого типа, поэтому никаких сложностей встретиться не должно.
В каждый момент элемент, у которого есть дочерние элементы, может быть «свернут» или «распахнут». Элемент автоматически меняет свое состояние либо при двойном щелчке мышью на нем, либо при щелчке мышью на кнопке элемента, если, конечно, у элемента установлен стиль TVSJ-IASBUTTONS. Программа может изменять состояние элемента с помощью посылки окну просмотра деревьев сообщений TVM_EXPAND или, что то же самое, обращением к макросу TreeView_Expand(). IParam этого сообщения определяет хэндл элемента, с которым производится
198
действие, a wParam определяет, что нужно произвести с элементом. В данном случае wParam
может принимать значения, приведенные в табл. 50.
В случае изменения состояния элемента окно просмотра деревьев посылает родительскому окну сообщение WMJNOTIFY, посредством которого передает информацию о том, что состояние элемента каким-то образом изменилось. В случае «распахивания» или «сворачивания» элемента родительскому окну посылаются сообщения TVN_ITEMEXPANDING до «распахивания» или «сворачивания» и TVNJTEMEXPANDED - после.
В том случае, когда элемент «распахнут», дочерние элементы списка отображаются смещенными вправо относительно родительского элемента. Получить значение смещения или установить это значение можно с помощью сообщений TVM GETINDENT и TVM_SETINDENT.
Программа может дать пользователю возможность изменить текст элемента, послав этому элементу сообщение TVM_EDITLABEL. В этом случае родительское окно получает нотификационные сообщения TVN BEGINLABELEDIT перед началом редактирования и TVM_ENDLABELEDIT после его окончания.
При необходимости программа может отсортировать элементы списка в алфавитном порядке, послав окну сообщение TVM_SORTCHILDREN. Если необходима сортировка списка по какому-то другому критерию, то тогда окну просмотра деревьев необходимо послать сообщение TVM_SORTCHILDRENCB, указав в качестве IParam адрес процедуры сортировки.
Т а б л и ц а 50. Действия, производимые с элементом окна просмотра деревьев при посылке окну срообшепия TVM_EXPAND
wParam |
Значение |
Описание |
TVR COLLAPSE |
0x000 1 |
Элемент «сворачивается» |
TVE EXPAND |
0x0002 |
Элемент «распахивается» |
TVE TOGGLE |
0x0003 |
Если элемент «свернут», то он |
|
|
«распахивается», и наоборот |
TVE COLLAPSHRESET |
0x8000 |
Элемент «сворачивается», при этом |
|
|
дочерние элементы удаляются, действует |
|
|
только в паре с TVE COLLAPSE |
199
выбранного элемента. Для того чтобы сменить выбор, программа должна послать окну сообщение TVM_SELECTITEM.
Для того чтобы получить информацию об элементе, необходимо послать окну сообщение TVM_GETITEM. Сообщение TVM_GETNEXTITEM, вопреки своему названию, позволяет получить информацию не только о следующем за текущим элементе, но и о других элементах, находящихся в определенных отношениях с текущим.
Сообщение TVM_GETCOUNT позволяет получить число элементов списка, а сообщение TVM GETVISIBLECOUNT - число элементов списка, видимых в данный момент.
Для того чтобы связать с окном просмотра деревьев список изображений, нужно воспользоваться сообщением TVM_SETIMAGEEIST. Сообщение TVM_GETIMAGELIST позволяет получить хэндл списка изображений, связанного с окном просмотра деревьев.
Для того чтобы пояснить то, о чем шла речь в этом разделе, ниже приведена демонстрационная программа. В ней элементами наивысшего уровня являются десятки от 0 до 100 (0, 10, 20... 100), а элементами второго уровня - числа, располагающиеся на числовой оси между целыми десятками. Вот текст этой программы:
#include <windows.h>
#include <commctrl.h> ^include <stdio>
HINSTANCE hlnst;
LRESULT CALLBACK TrceVicwWndProc ( HWND, UINT, UINT, LONG );
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrcvInstancc, LPSTR IpszCmdParam, int nCmdShow )
{
HWND hWnd ; WNDCLASS WndClass ; MSG Msg; char szClassName[] = "TreeView";
hlnst = hlnstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style = CS_HREDRAW | CSJVREDRAW;
WndClass.IpfnWndProc = TrecViewWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hlnstance = hlnstance ;
WndClass.Mcon = Loadlcon (NULL,IDI_APPLICATION);
200
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW); WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); WndClass.IpszMenuName = ""; WndClass.IpszClassNamc = szClassName;
if ( IRegisterClassf&WndClass))
>
\
MessageBox(NULL,"Cannot register class","Error",MB_OK); return 0;
hWnd = CreateWindow(szClassName, "TreeView Demo Program",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hlnstance.NULL); if(!hWnd) i
MessageBox(NULL,"Cannot create window","Error",MB_OK); return 0;
}
InitCommonControlsO; /* Show our window */
ShowWmdo\v(hWnd,nCmdSliow); UpdatcWindow(hWnd);
/* Beginning of messages cycle */
while(GctMcssage(&Msg, NULL, 0, 0)) {
TranslateMessage(&Msg); DispatchMessage(&Msg);
} return Msg.wParam;
LRESULT CALLBACK TreeViewWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
static HWND hTreeView;
RECT Rcct;
TV_INSERTSTRUCT TV InsertStruct;
TVJTEM TVJtem;
int i. j;
charcBufferfl2]:
swileh(Message) { caseWM CREATE:
201
GetCiientRect(hWnd, &Rect);
hTreeView = CreatcWindow(WC_TREEVIKW,"",
WSJVISIBLE j WS JTABSTOP | WS_CHILD | TVSJIASLINES j TVSJiASBUTTONS | TVS_LINESATROOT, 0, 0. Rcct.right, Rcct.bottom, hWnd, NULL, hlnst, NULL);
TVJnsertStruct.MnsertAHcr = TVI_ LAST;
TVJtem.mask = TVIF_TEXT;
lor(i"0; i< 100; i+= 10)
!
TVJnsertStruct.hParent - TVIJIOOT; TVJtcm.pszText = itoa(i, cBuffer, 10); TVJnsertStruct.itcm = TVJtcm; TV_InscrtStruct.hParcnt = TreeViewJnsertItem(hTrceView,
&TV_InscrtStrucl); for(j = l;j < 10;j++)
{
TVJtcm.pszText = iloa(i + j, cBufTer, 10); TVJnsertStruct.itcm = TVJtcm; TrceViewJnsertItcm(hTreeView, &TVJnscrtStruct);
return 0;
case WMJ5ESTROY: PostQuitMessage(O); return 0;
return DefWindowProc(hWnd,Message,wParam. IParam);
На рис. 19 показан вид создаваемого программой окна. Как всегда, самое трудное (©) - добавить изображения в список - я оставляю читателю в качестве упражнения.
РЕЕСТР
Реестр - это база данных, определенная в Windows, которая используется приложениями для того, чтобы хранить в ней конфигурационные данные.
Надеюсь, что читатель помнит массу файлов с расширением .ini в подавляющем большинстве случаев, которые приложения, разработанные для Windows более старых версий, использовали для хранения данных. Там хранились данные, нужные только данному приложению для работы. Для работы с ними использовались функции, имена которых содержали строку PrivateProfile. В Win32 для хранения подобных данных разработан совершенно новый механизм, получивший название реестра (registry -
202
реестр, регистратура). Этот механизм, во-первых, облегчил работу с данными приложений, и, во-вторых, упростил работу с ними. При этом следует заметить, что хотя никаких особых ограничений для хранимой в реестре информации нет, хранить в нем следует только инициализацион-ные и конфигурационные данные. В help'e no Win'32 API записано, что если данные превышают один килобайт, их целесообразно хранить в отдельном файле, а не в регистре. Мне кажется, что в большинстве случаев следует поступать именно так. Какой же должна быть программа, чтобы данные инициализации и конфигурационные данные занимали бы 1 кбайт!
В TreeView Demo Program
в о
•: Ш- 10
: и 20 , и зо
:: Ш 40 ; Й-50
В 60 . Ш- 70
:ш 80 I а эо
Рис. 19. Окно просмотра деревьев с одним "распахнутым" и девятью "нераспахнутыми" элементами
СТРУКТУРА РЕЕСТРА
Данные в реестре хранятся в виде иерархического дерева. Каждый элемент этого дерева называется ключом. Каждый ключ может содержать как ключи более низкого уровня, гак и конечные элементы данных. При необходимости приложение открывает ключ и использует данные, сохра-
203
ненные в нем. Каждый ключ может содержать любое число данных (конечно, все ограничивается объемом памяти), при этом данные могут быть в произвольном формате. Учитывая то, о чем я говорил выше, если данных очень много и они хранятся в отдельном файле, то в реестре может быть создан ключ, который ссылался бы на этот файл. Имена ключей не могут содержать обратные слеши (\), пробелы, звездочки ( * ) и вопросительные знаки. Имя ключа не должно совпадать с именами ключей, располагающихся выше него по иерархии.
РАБОТА С РЕЕСТРОМ
СОЗДАНИЕ И ОТКРЫТИЕ КЛЮчЕЙ
Для того чтобы работать с данными реестра, приложение должно сначала создать собственный ключ или открыть ключ, созданный ранее. Для создания ключа приложению необходимо вызвать функцию RegCreateKeyExQ, которая описана в файле winreg.h так:
WINADVAPI LONG APIENTRY RegCrcatcKcyExA (HKEY hKey,
LPCSTR IpSubKcy, DWORD Reserved, LPSTR IpClass, DWORD dwOptions, REGSAM samDesircd,
LPSECURITY^ATTRIBUTESIpSecurityAttnbutes, PHKEY phkResult, LPDWORD IpdwDisposition); WINADVAPI EONG APIENTRY RegCreateKeyExW (HKEY hKey,
LPCWSTR IpSubKey, DWORD Reserved, EPWSTR IpClass, DWORD dwOptions, REGSAM samDesircd,
LPSECURITY_ATTRIBUTESlpSecurityAttributcs, PHKEY phkResult, LPDWORD IpdwDisposition);
#ifdef UNICODE
#defme RegCreateKeyEx RegCreateKeyExW
#elsc
#define RegCreateKeyEx RegCreateKeyExA
#cndif// iUNICODE
Опять функция с массой аргументов! Первый аргумент - hKey -хэндл ранее открытого ключа или одно из следующих значений: HKEY_CLASSES_ROOT; HKEY_CURRENT_USER; HKEY_LOCALJVIACHINE; HKEY USERS.
204
Здесь нужно остановиться и рассмотреть, что за значения были приведены выше.
При инсталляции Windows создаются четыре ключа. Их имена совпадают со значениями, приведенными выше. Другими словами, эти ключи являются основой для создания иерархии ключей.
Ключи, находящиеся по иерархии ниже первого из предопределенных ключей, HKEY_LOCAL_MACHINE, определяют физическое состояние компьютера, включая данные о типе шины, системной памяти, инсталлированном аппаратном и программном обеспечении.
Ключи, находящиеся по иерархии ниже HKEY_CLASSES_ROOT, определяют типы (или классы) файлов и свойства, ассоциированные с этими классами. Свойства классов определяются только программистом. Обычно эти свойства применяются при работе приложений, использующих внедрение и связывание объектов, а также приложений, использующих среду Windows (shell applications). К примеру, при открытии файлов в Explorer'e используются свойства файлов, записанные в реестре.
Ключи, подчиненные HKEY USERS, определяют конфигурацию по умолчанию при подключении нового пользователя на локальной машине и конфигурацию текущего пользователя.
И наконец, ключи, подчиненные HKEY_CURRENT_USER, определяют установки, сделанные текущим пользователем, касающиеся переменных окружения, данных о принтерах, сетевых подключениях и т. д. Кроме этого, в этой ветви дерева хранятся установки, сделанные конкретными приложениями.
Возвращаясь к аргументам функции RegCreateKeyExQ, я теперь могу сказать, что перед созданием нового ключа необходимо продумать, в какую ветвь дерева необходимо включить новый ключ. Если новый ключ необходимо создать подчиненным ключу более низкого уровня, то определенным образом можно пройти по дереву и найти тот ключ, который необходим. Кроме этого, ключ, хэндл которого указан в первом аргументе, должен быть открыт с атрибутом доступа KEY_CREATE_SUB_KEY. Об атрибутах доступа мы поговорим при рассмотрении шестого аргумента функции.
Вторым аргументом - IpSubKey - является указатель на строку, содержащую имя создаваемого ключа. Создаваемый ключ будет подчиненным ключа, хэндл которого указан в первом аргументе.
Третий аргумент - Reserved -зарезервирован и должен быть равным нулю.
Четвертый аргумент - IpClass - указатель на строку, определяющую класс создаваемого ключа.
205
Очередной, пятый аргумент - dwOptions, определяет опции создаваемого ключа. Этот аргумент может принимать одно из значений -REGJ3PTION VOLATILE или REG_OPTIONNON_VOLATILE. ' В Windows'95 первое значение не используется. Второе значение указывает, что при перезагрузке системы значение этого ключа сохраняется, т. е. информация сохраняется в файле, а не в памяти.
Следующий, шестой аргумент - samDesired, определяет маску доступа к ключу. Этот параметр представляет собой битовую шкалу и может быть комбинацией флагов, приведенных в табл. 51.
Седьмой аргумент - IpSecurityAttributes - указатель на структуру типа SECURITY_ATTRIBUTES, которая определяет атрибуты безопасности создаваемого ключа. К сожалению, Windows'95 не поддерживает безопасность, поэтому этот параметр игнорируется.
Туда, куда указывает восьмой аргумент - phkResult - записывается хэндл созданного ключа.
Т а б .ч и ц а 51. Флаги, составляющие маску доступа к ключу
Флаг
Значение
Описание
KF.Y_QUERY_ VALUE
KEY_SET_VALUE
KEY_CREATE_SUB_KEY
KEY_ENUMERATE_SUB_KEY
KEY_NOTIFY
KEY_CREATE_LINK
KEY READ
KEY WRITE
KEY_EXECUTE KEY ALL ACCESS
0x0001 0x0002 0x0004 0x0008 0x0010 0x0020
Права 'запрашивать данные подключен
Права устанавливать данные подключен
Права создавать подключи
Права перебирать подключи
Права изменять нотификацию
Права создавать символическую связь
(STANDARD_RIGHTS_READ |
KEY_QUERY_VALUE
KEYJiNUMERATE SUBJCEYS |
KEY_NOTIFY) & (-SYNCRONIZE)
(STANDARD_RIGHTS_WRITE
KEY_SET_VALUE |
KEY_CREATE_SUB_KEY) &
(-SYNCRONIZE)
KEY READ & (-SYNCRONIZE)
(STANDARD_RIGHTS_ALL j
KEY_QUERY_VALUE|
KEY_SET_VALUE [
KEY_CREATE_SUB_KEY |
KEY_ENUMERATE_SUB_KEYS |
KEY_NOTIFY j KEY_CREATE_LINK) &
(-SYNCRONIZE))
206
И наконец, последний, девятый аргумент - IpdwDisposition - указывает место, куда будет записана информация о том, что произошло с ключом. Дело в том, что если с помощью этой функции производится попытка создать ключ, который уже существует, то ключ не создается, а просто открывается. Поэтому приложению необходимо знать, что произошло при создании ключа. Если ключ был создан, то в поле, определяемое IpdwDisposition, записывается значение REG_CREATEDJNEWJCEY. В том случае, если ключ существовал и был открыт, записываемое значение равно REG OPENED EXISTING KEY. Это поле может быть использовано и для того, чтобы узнать, не открыт ли ключ другим приложением. Открытый ключ доступен только тому приложению, которое создало его. Таким образом, если приложение открывает заведомо существующий ключ и получает в ответ значение REG_CREATED_NEW KEY, то можно сделать вывод о том, что ключ занят другим приложением.
Функция возвращает значение ERROR SUCCESS в том случае, если ключ создан или открыт удачно. Любое другое значение является свидетельством того, что при создании или открытии ключа встретилась ошибка.
Итак, считаем, что ключ мы создали. А что необходимо сделать для того, чтобы не создать, а открыть существующий ключ? Для этого нужно вызвать функцию RegOpenKeyEx(), описание которой приведено ниже:
WINADVAPI LONG APIENTRY RcgOpenKeyExA (HKEYhKey,
LPCSTR IpSubKey, DWORD ulOptions, REGSAM samDcsired, PHKEY phkResult);
WINADVAPI LONG APIENTRY RegOpcnKeyExW (HKEY hKey,
LPCWSTR IpSubKey, DWORD ulOptions, REGSAM samDesired, PHKEY phkResult);
tfifdcfUNICODE
ffdeime RcgOpenKeyEx RegOpenKcyExW
#elsc
/'define- RcgOpcnKeyEx KcgOpenKeyExA
#cndil"// 'UNICODE
.Надеюсь, что читатель, сравнив описания обеих функций, разберется с аргументами открывающей ключ функции самостоятельно (небольшая подсказка - полю Reserved функции RegCreateKeyExQ соответствует поле ulOptions).
Так, создавать и открывать ключи мы научились. Теперь необходимо научиться ключи закрывать.
207
ЗАКРЫТИЕ КЛЮЧЕЙ И СОХРАНЕНИЕ ПРОИЗВЕДЕННЫХ
В НИХ ИЗМЕНЕНИЙ
Закрывается ключ с помощью функции RegCloseKeyO, описание которой, приведенное ниже, можно встретить в файле winreg.h:
WINADVAPI LONG APIENTRY RegCloseKey (HKEY hKey);
Единственным аргументом этой функции является хэндл закрываемого ключа. Но при выполнении этой функции читатель может встретиться с одной проблемой, незаметной с первого взгляда Дело в том, что данные из реестра на время работы с ними переписываются в кэш и записываются обратно на диск при выполнении функции RegFlushKeyO, описание которой имеет следующий вид:
WINADVAPI LONG APIENTRY RegFlushKey (HKEY hKey);
Другими словами, если вы не хотите, чтобы данные, которые вы изменили во время работы программы, были потеряны, перед закрытием ключа сбрасывайте на диск. С другой стороны, у программиста может появиться соблазн сбрасывать данные на диск достаточно часто. Так как RegFlushKeyO использует огромное количество системных ресурсов, то эту функцию нужно вызывать только в том случае, когда действительно в этом есть необходимость.
ДОБАВЛЕНИЕ ДАННЫХ К КЛЮЧАМ И УДАЛЕНИЕ
ДАННЫХ ИЗ КЛЮЧЕЙ
После того, как ключ создан, возникает необходимость добавить к ключу некоторые данные, которые будут использоваться программой. Для этого нужно вызвать функцию RegSetValueExQ. Описание этой функции, которое приведено ниже, взято из файла winreg.h:
WFNADVAPI LONG APIENTRY RegSetValueExA (HKEY hKcy,
LPCSTR IpValueName, DWORD Reserved, DWORD dwType, CONST BYTE* IpData, DWORD cbData); WINADVAPI LONG APIENTRY RegSetValueExW (HKEY hKey,
LPCWSTR IpValueName, DWORD Reserved, DWORD dwType, CONST BYTE* IpData,
DWORD cbData);
#ifdef UNICODE
#define RegSetValueEx RegSetValueExW
#else
#defme RegSetValueEx RegSetValueExA
#endif// IUNICODE
208
Первый аргумент - хэндл ключа, к которому добавляются данные. Второй аргумент - указатель на строку, содержащую имя добавляемых данных. Третий аргумент зарезервирован. Четвертый аргумент определяет тип информации, который будет сохранен в качестве данных. Этот параметр может принимать одно из значений, приведенных в табл. 52.
Таблица 52. Типы сохраняемой в реестре информации
I lapaMCip
Значение
Описание
REG_NONE
REG_SZ
REG_EXPAND_SZ
REG_BINARY REGJDWORD REG_LINK REG MULTI SZ
REG_RESOURCE_LIST
REG FULL RESOURCE_DESCRIPTOR
REG_RESOURCE_REQUIREMENTS LIST
REG_DWORD_LITTLE_ENDIAN
REG DWORD BIG ENDIAN
10
4
Тип данных не устанавливается Строка, оканчивающаяся нулем Строка со ссылками на переменные окружения (типа %РАТН%) Бинарные данные в любой форме Двойное слово Символическая связь Массив из нескольких строк, заканчивающихся нулями, который, в свою очередь, заканчивается двумя нулями Список драйверов устройств Список ресурсов в виде частей аппаратуры
То же, что и REG_DWORD То же, что и REGJ5WORD, но наиболее значащим в слове является младший байт
Пятый аргумент является указателем непосредственно на данные, которые будут сохранены. И наконец, шестой аргумент определяет размер данных, на которые указывает пятый аргумент. Все легко и просто, не так ли?
А удалить данные можно с помощью обращения к функции RegDeleteValueQ. Её описание приведено ниже:
WINADVAPI LONG APIENTRY RegDeleteValueA (HKEY hKey,
LPCSTR IpValueName);
WINADVAPI LONG APIENTRY RegDeleteValueW (HKEY hKey,
LPCWSTR IpValueNamc);
#ifdcfUNICODE
#define RegDeletcValue RegDeleteValueW «else
209
#defme RegDek-teValue RegDeleteValucA
#cndif// IUNICODE
Аргументы этой функции очевидны - хэндл ключа и указатель на строку с именем данных.
Но если данные записываются в реестр, то, наверное, их можно и нужно считывать из реестра. Поэтому сейчас мы рассмотрим вопрос о том, как происходит
ВЫБОРКА ДАННЫХ ИЗ РЕЕСТРА
Если прикладной программе нужно осуществить выборку данных из реестра, то для начала программа должна определить, из какой ветви дерева регистрации ей нужно выбрать данные. Естественно, что никаких функций для этого нет. При написании программы программист должен сам позаботиться об этом. После того как решение принято, начинается второй этап. Программа должна перебирать все ключи в этой ветви до тех пор, пока не найдет нужный ключ. Для этого приложение может воспользоваться функцией RegEnumKeyEx(). Как и всегда, обратимся к заголовочному файлу winreg.h для того, чтобы найти описание этой функции. Оно приведено ниже:
WINADVAPI LONG APIENTRY RegEnumKeyExA (HKEY hKey,
DWORD dwlndex, LPSTR IpName, LPDWORD IpcbName, LPDWORD IpReserved, LPSTR IpClass, LPDWORD IpcbClass, PFLETIME IpftLastWriteTime);
WINADVAPI LONG APIENTRY RegEnumKeyExW (HKEY hKey,
DWORD dwlndex, LPWSTR IpName, LPDWORD IpcbName, LPDWORD IpReserved, LPWSTR IpClass, LPDWORD IpcbClass, PFLETIME IpftLastWriteTime);
#ifdef UNICODE
#defme RegEnumKeyEx RegEnumKeyExW
#else
«define RegEnumKeyEx RegEnumKeyExA
#endif// 'UNICODE
Функция перебора объектов нам встречается впервые. Давайте сначала рассмотрим аргументы этой функции, а потом поговорим о том,
210
что происходит при переборе ключей. Многие аргументы этой функции уже должны быть знакомы читателю. Первый аргумент - это хэндл ключа, подчиненные ключи которого будут перебираться в поисках нужного ключа. Второй аргумент - dwlndex - является индексом требуемого подключа. Третий аргумент - IpName - указывает на буфер, в который будет записано имя ключа. Четвертый аргумент - IpcbName - определяет размер этого буфера в байтах. Пятый аргумент, как следует из его названия - IpReserved - зарезервирован для использования в будущем и должен быть равным NULL. Шестой аргумент - IpClass - должен указывать на буфер, в котором после завершения работы функции будет содержать имя класса подключа. Если это имя программе не требуется, то этот аргумент должен быть равным NULL. Размер этого буфера определяется седьмым аргументом - IpcbClass. И последний, восьмой аргумент -IpftLastWriteTime - после завершения работы функции содержит время последнего обновления данного подключа.
Знать функцию и ее аргументы - это хорошо. Но какой от функции и аргументов прок, если мы не умеем пользоваться функцией? Для того чтобы перебрать подключи, приложение должно сначала вызвать функцию RegEnumKeyExQ со вторым аргументом (dwlndex), равным нулю (поиск начинается с начала дерева). Если искомый ключ найден с первой попытки, то приложению повезло. В противном случае необходимо dwlndex увеличить на единицу и снова обратиться к функции. Так необходимо делать до тех пор, пока не будет найден искомый ключ или функция не вернет значение ERROR_NO_MORE ITEMS. Естественно, что поиск можно производить и в обратном порядке. Для того чтобы поиск мог быть нормально осуществлен, ключ, хэндл которого указан первым аргументом, должен быть открыт с правом доступа KEY__ENUMERATE_SUB_KEYS. Если функция выполнена успешно, то она возвращает значение ERROR SUCCESS. Любое другое возвращенное значение является кодом ошибки. Кстати, получить полную информацию о подключе можно с помощью функции RcgQueryInfoKey().
Давайте считать, что с помощью способа, описанного выше, мы перебрали подключи и нашли нужный нам подключ. Теперь в этом подключе нам необходимо найти нужные данные. Способ поиска точно такой же, как и в предыдущей функции. Для поиска необходимо перебрать все данные, связанные с подключом. Чтобы произвести этот перебор, обычно используется функция RegEnumVa!ue(). описание которой, приведенное ниже, можно найти в файле winreg.h:
WINADVAPI LONG APIENTRY KegEnumValueA (HKEY hKey,
DWORD dwlndex.
211
LPSTR IpValueName, LPDWORD IpcbValueName, LPDWORD IpReserved, LPDWORD IpType, LPBYTE IpData. LPDWORD IpcbData); WINADVAPI LONG APIENTRY RegEnumValueW (HKEY hKey,
DWORD dwlndex, LPWSTR IpValueName, LPDWORD IpcbValueName, LPDWORD IpReserved, LPDWORD IpType, LPBYTE IpDala, LPDWORD IpcbData); flifdefUNICODE
#defme RegEnumValue RegEnumValueW
#else
#defme RegEnum Value RegEnum ValueA
#endif// IUNICODE
Порядок использования этой функции полностью совпадает с порядком использования функции RegEnumKeyEx(), поэтому я не стану на нем останавливаться. Опишу только аргументы этой функции. Понятно, что hKey - это хэндл ключа, которому принадлежит подключ, индекс которого представлен вторым аргументом - dwlndex. Следующий аргумент -указатель на буфер, в который будет записано имя подключа. IpcbValueName определяет размер этого буфера. Аргумент IpReserved зарезервирован и должен быть равным NULL. Последние три аргумента определяют класс подключа, указатель на буфер, в который будут записаны эти данные и размер буфера. После возврата функции предпоследний аргумент содержит число записанных данных.
Теперь я, наконец, могу сказать, что у читателя есть полное представление о том, как использовать реестр. Как всегда, рассмотрение темы заканчивается демонстрационной программой:
#includc <windows.h>
#include <commctrl.h>
#defme hKeyMin 0x80000000
#dcfi:ie hKeyMax 0x80000006 HINSTANCE hlnst; HWND hTrccChild; TVJNSERTSTRUCT InscrtStruct;
LRESULT CALLBACK RegistryWndProc ( HWND, UINT, UINT, LONG ); void FillTree(H\VND, HTREEITEM);
212
void FillBranch(ULONG, DWORD, HWND, HTREEITEM); void FillSubBranch(HKEY, char*, HWND, HTREEITEM);
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrevhistance. LPSTR IpszCmdParam, int nCmdShow )
HWND hWnd;
WNDCLASS WndClass ;
MSG Msg;
char s/ClassNamef] = "Registry";
hlnst = hlnstance; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.stylc = CS_HREDRAW | CSJVREDRAW;
WndClass.lpfnWndProc - RegistryWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hlnstance = hlnstance ;
WndClass.hlcon = Loadlcon (NULL.IDI_APPLICATION1;
WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH) GetStockObjcct (WHITE_BRUSH);
WndClass.IpszMenuName = "";
WndClass.IpszClassName = szClassName;
if ( !RcgisterClass(&WndClass))
!
MessageBox(NULL,"Cannot register class","Error",MB_OK); return 0;
hWnd - CreateWindow(szClassName, "Registry Demo Program",
WS_POPUPWINDOW | WS_VISIBLE | WSJTAPTION,
100, 100,300,400,
NULL, NULL,
hlnstancc.NULL); if(!hWnd)
i
>
MessageBox(NULL,"Cannot create window" ."Error", MB_OK); return 0;
InitCommonControls(); /* Show our window */
ShowWindow(hWnd, nCmdShow); UpdatcWindow(hWnd);
/* Beginning of messages cycle */
while(GctMessage(&Msg, NULL, 0, ()))
213
TranslatcMessage(&Msg); DispalchMcssage(&Msg);
} return Msg.wParam;
LRESULT CALLBACK RegistryWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
»
RECT Rcct;
static HWND hTreeChild;
static HTREEITEM hParentltem;
switch(Message)
{ case WM_CREATE:
GetC'licntRcct(hWnd, &Rect);
hTreeChild = CreateWindow(WC_TREEVIEW,"",
WS_VISIBLE | WS_TABSTOP j WS_CHILD |
TVS_HASLINES | TVS J.INESATROOT |
TVS_HASBUTTONS | WS_DLGFRAME,
0, 0, Rect.right, Rcct.bottom,
hWnd,
NULL,
hlnst,
NULL);
InsertStruct.item.mask = TVIF_TEXT;
InscrtStruct.item.hltem = NULL;
InsertStruct.item.pszText = "Registry Keys";
InsertStruct. item. cchTextMax = 14;
InscrtStmct.hParcnt = TVI_ROOT;
InsertStruct.hlnsertAfter = TVI_LAST;
hParentltem = TreeView_InscrtItem(hTreeChild, &InsertStruct);
FillTrce(hTreeChild, hParentltem);
TreeView_Expand(hTreeChild, hParentltem. TVE_EXPAND);
TreeView_SclcclItem(hTreeChild, hParentltem);
return 0; case WM_SIZE:
Move\Vindow(hTreeChild, 0. 0, LOWORD(lParam), HIWORD(lParam), TRUE);
return 0; case WM_DESTROY:
PostQuitMcssage(O);
return 0;
!
return DeAVindovvProc(hWnd, Message, wParam, ll'aram);
214
void FillTrce( HWND hTrceWnd, HTREEITEM hParentltem) {
ULONG i; TVJTEM Item;
TVJNSERTSTRUCT IiisertSmict; LPSTR IpszKeysf?] •--= {"HKEY CLASSES ROOT",
"HKEY~CURRENf USER",
"HKEY" LOCAL_MACHINE".
"HKEYJJSERS",
"HKEY PERFORMANCE_DATA",
"HKEY CURRENT_CONFIG",
"HKEY DYN_DATA"j; char cClass[80] - ""; DWORD dwSizc ~ 80, dwSubKeys, dwMaxLength, dwMaxClass, dwValucs,
(IwMaxValuc, dwMaxData, dwSec; FILETIME ItFilcTime; HTREEITEM hNewParentltem;
for(i - hKcyMin, i <-•' hKeyMax; i-i-~)
// Add the highest items
illERROR^SUCCESS -=-- RegQiierylnl'oKeyffHKEY) i, cClass, &dwSizc,
NULL.
&dwSubKeys, &dwMaxLength, &dwMaxClass, &dwValues, &dwMaxValue, &dwMaxData, &dwSec, &ftF)leTime))
Uem.mask = TVIFJTEXT;
Itcin.pszTcxt - lps/Keys[i - hKcyMin];
InsertStruct.item ~ Item;
InsertStruct.hParcnl = hParentltem;
hNewParentltem = TreeView_Insertltem(hTrecWnd, &InsertStruct);
FillBrauclUi. dwSubKeys, hTrceWnd, liNewParcntltcm);
void FillBranch( ULONG i, DWORD dwSubKeys, IIWND hTrceWnd, HTREEITEM hNewParentltem)
int j;
DWORD dwClassNameSizc = 80;
char cChissName[80] - "";
if (dwSubKeys ^= 0)
return; else
for(j = 0, j < dwSubKeys; j—)
215
RegEnumKey((HKEY) i, (DWORD) j, cClassName. dwClassNameSize);
dwClassNameSize = 80;
FillSubBranch((HKEY) i, cClassName, hTreeWnd, hNewParentltem);
I
void FillSubBranch(HKEY hKcy, char* cClassName, HWND hTreeWnd, HTREEITEM hParentltem)
)
HKEY hNewKey; char cClass[80], cNewCIass[80]; DWORD dwClassSize = 80, dwSK, j; TVJTEM Item;
TVJNSERTSTRUCT InscrtStruct; HTREEITEM hNewParentltem;
Item.mask = TVIFJTEXT;
Item.pszText = cClassName;
InsertStruct.hParent = hParentltem;
InsertStruct.hlnsertAfter = TVI_SORT;
InsertStruct.item = Item;
hNewParentitem = TreeView_InsertItem(hTreeWnd, &InsertStruct);
RegOpenKey(hKey, cClassName, &hNewKey);
RegQueryIntbKey(hNewKey, cClass, &dwClassSize, NULL, &dwSK, NULL,
NULL, NULL, NULL, NULL, NULL, NULL); dwClassSize = 80; if(dwSK != 0)
for(j = 0; j < dwSK; j+
RegEnumKey(hNewKey, j, cNewClass, dwClassSize); FillSubBranch(hNewKey, cNewClass, hTreeWnd, hNewParentltem);
} RegCloseKey(hNewKey);
Вид, создаваемого программой окна, показан на рис. 20.
Я не большой специалист в рисовании ( © ), поэтому подключение изображений, как всегда, оставляю на долю читателя.
Эта программа просто перебирает ключи и позволяет просмотреть все «дерево» реестра. Обращаю внимание читателя на тот факт, что предопределенные ключи (их имена начинаются с HKEY_) всегда открыты. Открывать следует только ключи, находящиеся ниже предопределенных в иерархии. Если читатель будет разрабатывать программу, храпящую конфигурационные данные на диске, я настоятельно рекомендую использовать реестр, а не пользоваться произвольными файлами.
216
Ш Registry Demo Program
Й- HKEY_CLU.SSES_ROOT Ш HKEY_CURRENT_USER Ш HKEY_LOCAL_MACHINE Й- HKEY_USERS
В- .Default i Ep AppE vents
И Console
: Ш Control Panel : i-- InstallLocationsMRU : Ш keyboard layout i Ш Network : :•••• RemoteAccess ; И Software В HKEY_CURRENT_CONFIG i В Display
i !•• • Fonts '• i '-•• Settings : 0 System
; В CurrentControlSet H HKEY_DYN_DATA Ш Config Manager В PerfStats
Рис. 20. Окно с деревом реестра, созданное программой
КОЕ-ЧТО О МНОГОЗАДАЧНОСТИ В WINDOWS
Одним из основных отличий Win32 от его предшественников явилась многозадачность. "Как это - разве в Windows 3.x не была реализована истинная многозадачность?" - может спросить кто-нибудь из неискушенных пользователей. "НЕ БЫЛА!" - отвечу я. И вот почему.
Мне Windows 3.x представляется чем-то вроде однорукого натуралиста. У этого натуралиста есть зверинец, в каждой клетке которого сидит хищник - программа. Каждому хищнику (программе) натуралист подает корм (сообщение) рукой. Как только хищник (программа) съест корм (обработает сообщение), корм (сообщение) получает очередной хищник. И так далее по кругу. Но иногда один из хищников (программ) мертвой хваткой вцепляется в руку (зависает и не возвращает управление) и натуралист умирает (Windows зависает), после чего на смену умершему смотрителю зверинца приходит новый однорукий натуралист (производится перезагрузка системы). И все начинается сначала. Разве
217
вам не знакома эта ситуация, уважаемый читатель? В Windows 3.x была реализована ПСЕВДОМНОГОЗАДАЧНОСТЬ, т. е. управление передавалось программе, возвращалось системе и передавалось следующей программе, т. е. фактически программы работали последовательно, друг за другом. В случае зависания одной из задач средств завершить ее, не перезагружая систему, практически не было. Кроме этого, фактически все задачи разделяли одни и те же системные ресурсы, например, память. Не было никаких проблем для одной задачи затереть содержимое памяти, выделенной другой.
В Windows'95 и Windows NT дело обстоит не так. В этой системе реализована истинная многозадачность, т. е. каждой программе системой выделяется квант времени, в течение которого программа обрабатывает поступившие в ее адрес сообщения. Вне зависимости от состояния программы СИСТЕМА забирает управление у программы и передает его другой программе. Если программа зависла, то система от этого не пострадает. Управление в любом случае будет передано другой программе. Кроме этого, в Windows'95 и Windows NT введено понятие процесса. Грубо говоря, процесс - это совокупность выполняющейся программы и выделенных ей системных ресурсов. Случай, при котором программа может вырваться из рамок своего процесса и повредить еще чьи-то ресурсы, практически не возможен.
Рассуждаем дальше. Раз программа получает управление на время, то почему бы этой программе не распараллелить свою работу и не запустить несколько одновременно выполняющихся программ под своим управлением? В WTin32 эти программы называются потоками.
Таким образом, в системе Windows реализованы два типа многозадачности - процессная и потоковая. Рассмотрим оба типа многозадачности, после чего отдельно остановимся на вопросе синхронизации работы в многозадачной среде.
Остановимся на одной детали. Оттого, что мы назвали Windows многозадачной системой, физический смысл этой многозадачности не изменился. На однопроцессорном компьютере в каждый конкретный момент выполняется одна задача. Если при запуске двух-трех маленьких программ временная задержка субъективно не заметна, то при запуске нескольких программ, требующих колоссальных ресурсов (к примеру, WinWord или Borland C++ 5.0), задержка при выполнении программ становится достаточно заметной. На многопроцессорных системах за каждым процессором может быть закреплен свой ноток, поэтому на таких системах выполнение программ осуществляется действительно в многозадачном режиме.
218
ЗАПУСК ПРОЦЕССА
Давайте, уважаемый читатель, все же более точно определим, что есть процесс. В Windows 3.x, да иногда и в Win32 процесс определяют как копию (экземпляр) выполняющейся программы. Так оно и есть, но при этом забывают, что копия - понятие статическое. Другими словами, процесс в Win32 - это объект, который не выполняется, а просто «владеет» выделенным ей адресным пространством, другими словами, процесс является структурой в памяти. А вот в адресном пространстве процесса находятся не только код и данные, но и потоки - выполняющиеся объекты. При запуске процесса автоматически запускается поток (он называется главным). При остановке главного потока автоматически останавливается и процесс. А так как процесс без потока просто бесцельно занимает ресурсы, то система автоматически уничтожает ставший ненужным процесс. Первичный процесс создается системой при запуске, точно так же при создании первичного процесса в нем создается и поток.
Приложение тоже может создать процесс с главным потоком, используя для этой цели функцию CreateProcessQ. Её прототип, находящийся в файле winbase.h, при первой встрече с ним внушает легкий ужас:
WINBASEAPI BOOL WINAPI CreateProcessA(LPCSTR IpApplicationName,
LPSTR IpCommandLinc,
LPSECURITY_ATTRroUTESlpProcessAttributes,
LPSECUR ITY_ATTRIBUTES IpThreadAttributes,
BOOL blnheritHandles,
DWORD dwCreationFlags,
LPVOID IpEnvironment,
LPCSTR IpCurrentDirectory,
LPSTARTUPINFOA IpStartupInfo,
LPPROCESSJNFORMATIONlpProcessInformation); WINBASEAPI BOOL WINAPI CreateProcessW(LPCWSTR IpApplicationName,
LPWSTR IpCommandLinc,
LPSECURITY_ATTRIBUTESlpProcessAttributes,
LPSECURITY_ATTR]BUTESlpTlireadAttributcs,
BOOL blnheritHandles,
DWORD dwCreationFlags,
LPVOID IpEnvironment,
LPCWSTR IpCurrentDirectory,
LPSTARTUPINFOW IpStartupInfo,
LPPROCESSJNFORMATIONlpProccssInformation);
#ifdcf UNICODE
#defme CrcateProccss CrcateProcessW
#else
#defme CreateProcess CreateProcessA tfendif/'IUNICODE
219
Так как понимание сущности процессов и потбков крайне важно для программирующих в Win32, в этом месте я чуть отступлю от принятого стиля изложения и более подробно расскажу о том, что происходит при вызове этой функции.
Я уже говорил, что процесс - это структура в памяти. Таким образом, в начале работы функция выделяет память для этой структуры, а потом выделяет память (виртуальную, естественно) для адресного пространства процесса. Если выделение памяти прошло без ошибок, в адресное пространство процесса загружается код исполняемой программы и используемых программой динамических библиотек. Только после этого создается главный поток процесса. Если функции удастся произвести все эти действия без ошибок, то возвращаемое значение будет равно TRUE. FALSE явится индикатором того, что по каким-то причинам процесс не создан.
Перейдем к рассмотрению аргументов функции CreateProcessQ.
АРГУМЕНТЫ ФУНКЦИИ CREATEPROCESSQ
С моей точки зрения, взаимодействие первых двух аргументов не совсем продумано.
Первый аргумент - IpApplicationName - определяет имя исполняемого файла (обязательно указывать имя и расширение файла, автоматически расширение .ехе не подставляется), для которого создается процесс.
Второй аргумент - IpCommandLine определяет передаваемую этому файлу командную строку. Если IpApplicationName равен NULL, то первый (до первого пробела) элемент IpCommandLine считается именем исполняемого файла.
Таким образом, имя исполняемого файла можно передавать как в первом, так и во втором аргументе. Но здесь нужно быть внимательным и не допустить, скажем, такой ситуации, когда IpApplicationName равен «Wordpad.exe», a IpCommandLine - «Wordpad.exe MyFile.doc». Нетрудно догадаться к чему это приведет.
Третий и четвертый аргументы определяют атрибуты доступа к процессу и потоку соответственно. Я намеренно употребил слово «должны», ибо Windows'95 системы разграничения доступа не имеет. В Windows'95 эти значения, как правило, равны NULL.
Очередное поле - dwCreationFlag - является комбинацией битовых флагов. Одна группа битовых флагов определяет способ создания процесса.
220
Флаги способа создания процесса
Флаг DEBUG_PROCESS (0x00000001) устанавливается в тех случаях, когда родительский процесс должен осуществлять отладку порождаемого процесса и всех его потомков. Система будет оповещать родительский процесс о возникновении определенных событий в порождаемом процессе и его потомках.
Флаг DEBUG_ONLY_THIS_PROCESS (0x00000002) почти эквивалентен предыдущему, разница состоит в том, что система будет оповещать о событиях только в порождаемом процессе, но не в его потомках.
Флаг CREATE_SUSPENDED (0x00000004) указывает, что главный поток порождаемого процесса создается, но не выполняется до вызова функции ResumeThreadQ. Этот флаг обычно используется в отладчиках.
Флаг DETACHED PROCESS (0x00000008) запрещает создаваемому консольному процессу использовать консоль родительского процесса. Порождаемый процесс вынужден будет вызвать функцию AllocConsoleQ для получения собственной консоли.
Флаг CREATE NEW CONSOLE (0x00000010) указывает на необхо димость создания новой консоли для порождаемого процесса. Этот флаг не может использоваться вместе с предыдущим.
Флаг CREATE_NEW_PROCESS_GROUP (0x00000200) создает группу консольных процессов, которые будут одновременно реагировать на нажатие клавиш Ctrl-C и Ctrl-Break.
Флаг CREATE_UNICODE_ENVIRONMENT (0x00000400) означает, что данные, на которые указывает IpEnvironment, используют символы Unicode. По умолчанию считается, что используется ANSI-кодировка.
Флаг CREATE_SEPARATE_WOW_VDM (0x00000800) используется только при запуске 16-битовых Windows-приложений и указывает, что приложению необходимо выделить отдельную виртуальную машину (Virtual DOS Machine, VDM) (по умолчанию, все 16-битовые Windows-приложения используют одну разделяемую виртуальную машину). Преимуществом выделения отдельной машины является то, что приложение почти не влияет на остальные. Даже зависнув, оно приведет к краху только своей VDM. Недостаток - каждая виртуальная машина требует большого объема памяти.
Флаг CREATE_SHARED_WOW_VDM (0x00001000) используется при запуске 16-битовых Windows-приложений и указывает на необходимость создания для процесса разделяемой VDM.
221
Таблица 53. Флаги класса приоритета процесса
Флаг |
Значение |
Эффект |
NORMAL PRIORITY CLASS IDLE_PRIORITY_CLASS |
0x00000020 0x00000040 |
Нормальный приоритет Потоки этого процесса выполня- |
|
|
ются только тогда, когда система |
HIGH_PRIORITY_CLASS |
0x00000080 |
простаивает Приоритет выше нормального, но |
|
|
ниже приоритета реального |
REALTIME PRIORITY CLASS |
0x00000100 |
времени Самый высокий возможный |
|
|
приоритет |
На этом флаги, определяющие способ создания процесса, исчерпаны. Вторая группа битовых флагов определяет класс приоритета создаваемого процесса.
Флаги класса приоритета процесса
При создании процесса можно указать и класс его приоритета (табл. 53). Если при создании процесса не указан ни один из флагов, приведенных ниже, класс приоритета порождаемого процесса по умолчанию устанавливается равным IDLE PRIORITY CLASS, если этот класс установлен у процесса родителя, и NORMAL_PRIORITY_CLASS во всех остальных случаях.
Тем не менее, присвоение класса приоритета вновь создаваемому потоку не рекомендуется - Windows сама присвоит потоку класс приоритета по умолчанию.
Следующий аргумент функции CreateProcessQ - IpEnvironment -обычно равен NULL. Это означает, что порождаемый процесс наследует переменные окружения родительского процесса. Если этот аргумент не равен NULL, то он должен содержать указатель на блок памяти, содержащий те переменные окружения, которыми будет пользоваться порождаемый процесс.
Наименование следующего аргумента функции - IpCurrentDirectory -говорит само за себя. Этот аргумент позволяет установить текущие диск и директорию для порождаемого процесса. Если этот аргумент равен NULL, порождаемый процесс наследует текущие диск и директорию
222
родительского процесса. В противном случае этот аргумент должен указывать на строку, в которой указан полный путь к устанавливаемой текущей директории, включающий и букву дисковода.
Очередной аргумент - указатель на структуру типа STARTUPINFO. Эта структура, служащая для описания свойств окна, создаваемого в новом процессе, описана в winbase.h следующим образом:
typedef struct _STARTUPINFOA {DWORD cb; LPSTR ipReserved;
LPSTR IpDesktop; LPSTR IpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdlnput; HANDLE hStdOutput; HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA; lypedef struct _STARTUPINFOW {DWORD cb; LPWSTR IpReserved;
LPWSTR IpDesktop; LPWSTR IpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReservedl; LPBYTE lpReserved2; HANDLE hStdlnput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFOW, «LPSTARTUPINFOW;
#ifdef UNICODE
typedef STARTUPINFOW STARTUPINFO;
typedef LPSTARTUPINFOW LPSTARTUPINFO;
#else
typedef STARTUPINFOA STARTUPINFO;
typedef LPSTARTUPINFOA LPSTARTUPINFO;
#endif//UNICODE
Рассмотрим поля этой структуры.
223
Поля структуры типа STARTUPINFO
Первое поле - cb - размер этой структуры. Оно должно быть равно sizeof(STARTUPrNFO).
Второе поле - IpReserved - зарезервировано и должно быть равно NULL.
Третье поле - IpDesktop - в Windows'95 просто игнорируется.
Четвертое поле - IpTitle - определяет заголовок консольного приложения. Для GUI или приложений, не создающих новой консоли, должен быть равным NULL.
Поля с пятого по восьмое включительно определяют положение окна и его размеры (dwX, dwY - координаты верхнего левого угла окна в пикселах, dwXSize, dwYSize - ширина и высота окна в пикселах).
Таблица 54. Флаги, определяющие, в каких полях структуры типра STARTUPINFO содержится информация
Флаг
Значение
Эффект
STARTFJJSESHOWWINDOW STARTF JJSESIZE STARTFJJSEPOSITION STARTFJJSECOUNTCHARS
STARTF_USEFILLATTRIBUTE
STARTF_RUNFULLSCREEN STARTF FORCEONFEEDBACK
0x00000001 0x00000002 0x00000004 0x00000008
0x00000010
0x00000020 0x00000040
STARTF_FORCEOFFFEEDBACK STARTFJJSESTDHANDLES
STARTF USEHOTKEY
0x00000080 0x00000100
0x00000200
Если флаг не установлен, поле
wShowWindow игнорируется
Если флаг не установлен, поля
dwXSize и dwYSize игнорируются
Если флаг не установлен, поля dwX
и dwY игнорируются
Если флаг иг установлен, поля
dwXCountChars и dwYCounChars
игнорируются
Если флаг не установлен, поле
dwFillAttribute игнорируется
Курсор становится « песочными часами» на две секунды, за которые должно произойти обращение к GUI, после чего за 5 секунд должно быть создано окно и еще за 5 секунд оно должно перерисоваться При создании процесса форма курсора не меняется Если флаг установлен, то используются потоки, хэндлы которых определены полями hStdlnput, hStdOutput, hStdError
224
Девятое и десятое поля (dwXCountChars, dwYCountChars) определяют ширину и высоту окна консоли в символах (не пикселах!). Одиннадцатое поле - dwFillAttribute - определяет атрибуты консольного окна.
Двенадцатое поле - dwFIags - используется для того, чтобы определить, какие поля структуры типа STARTUPINFO будут использоваться при создании окна порождаемым процессом. Это поле представляет собой комбинацию битовых флагов (табл. 54).
Тринадцатое поле - wShowWindow - определяет, каким образом окно будет отображено (помните функцию ShowWindow())? Значение этого поля игнорируется, если только в dwFIags не установлен флаг STARTF USESHOWWINDOW. Возможные значения этого поля - те же константы, начинающиеся с SW__, которые используются в функции ShowWindowQ.
Четырнадцатое и пятнадцатое поля, - cbReserved2 и lpReserved2 зарезервированы. Должны инициализироваться нулем и NULL соответственно.
Шестнадцатое, семнадцатое и восемнадцатое поля - hStdlnput, hStdOutput и hStdError - определяют хэндлы стандартных потоков ввода-вывода.
Нерассмотренным остался только один аргумент функции CreateProcess() - IpProcessInformation, указывающий на структуру типа PROCESS INFORMATION, в которую записывается информация о порожденном процессе после его создания. Структура описана в файле winbase.h следующим образом:
typedcf struct J>ROCESS_rNFORMATION {
HANDLE hProcess; HANDLE hThrcad; DWORD dwProcessId; DWORD dwThreadld;
PROCESSJNFORMATION, *PPROCESS INFORMATION, *LPPROCESS_INFORMATION;
В первое поле - h Process - система записывает хэндл созданного процесса, во второе - hThread - хэндл потока. Поля dwProcessId и dwThreadld являются уникальными идентификаторами процесса и потока соответственно. Рекомендую обратить особое внимание на последние два поля. Дело в том, что Win32, если идентификатор освобожден, может повторно использовать его. К примеру, пусть процессу присвоен идентификатор 0x00001111. После завершения процесса идентификатор освобождается и какому-нибудь новому процессу может опять быть присвоен тот же
225
идентификатор 0x00001111. Это необходимо учитывать при написании программ.
Итак, аргументы функции рассмотрены. Основные результаты - хэндлы и идентификаторы процесса и потока - получены. А какое значение возвращает функция? Возвращаемое функцией значение TRUE говорит о том, что процесс создан и функция завершилась нормально. А при получении значения FALSE программисту придется искать ошибку.
Итак, мы подробно рассмотрели вопрос о запуске процесса. Теперь, очевидно, процесс необходимо завершить.
ЗАВЕРШЕНИЕ ПРОЦЕССА
Процесс может быть завершен вызовом одной из двух функций -ExitProcess() пли TerminateProcess(). Рассмотрим более подробно каждую из этих функций.
ФУНКЦИЯ EXITPROCESSQ
В обычных условиях процесс завершается тогда, когда один из принадлежащих ему потоков вызывает функцию ExitProcessQ, которая описана в файле winbase.h следующим образом:
WINBASEAPI VOID WINAPI ExitProcess(UINT uExitCode);
Читателю следует обратить внимание на тот факт, что завершение процесса начинается изнутри процесса. Почему так сделано? Во-первых, только поток процесса знает, когда он выполнил свою работу и когда ему необходимо завершиться. Во-вторых, только процесс в тот момент, когда он узнает о необходимости завершения, может оповестить об этом все принадлежащие ему потоки и произвести нормальное завершение. Извне процесса эти действия произвести почти невозможно.
Если говорить более конкретно, то при завершении процесса производятся следующие действия:
вызываются функции деинициализации всех подключенных библиотек динамической компоновки, т. е. происходит нормальное завершение всех подключенных DLL;
закрываются и/или уничтожаются все объекты, открытые и/или созданные процессом;
состояние процесса изменяется на «освобожденный» (signaled), что является сигналом для всех потоков, ожидающих завершения процесса;
состояние всех потоков изменяется на «освобожденный» (signaled), что является сигналом для всех потоков других процессов, которые ожидают завершения потоков текущего процесса;
226
код завершения меняется со STILL ACTIVE на код, записываемы!! в uExitCode;
счетчик числа пользователей процесса уменьшается на единицу (заметим, что данные процесса удаляются из памяти, но сам объект остается в памяти до того момента, пока счетчик пользователей не достигнет нулевого -.шачения, или, другими словами, пока не будут закрыты все хэндлы процесса. Определить, завершен ли процесс можно с помощью функции GetExitProcessCode(), которая в случае незавершенности процесса возвращает STILL ACTIVE).
Необходимо отметить, что завершение процесса не приводит к завершению порожденных им процессов.
Сразу после деинициализации и выгрузки библиотек из памяти, по до своего завершения, функция заносит в параметр uExitCode код завершения. После этого процесс можно считать полностью завершенным.
ФУНКЦИЯ TERMINATEPROCESSO
Эта функция является аварийным средством завершения процесса и её рекомендуется использовать только в крайнем случае. Она описана в том же winbase.li:
WINBASHAPI BOOL WINAPI Termm;UeProcess(HANDLE hProccss,
UlNTuExilCodc);
Функция используется только тогда, когда иными средствами завершить процесс не удается. С этой целью извне (!), а не изнутри процесса вызывается функция TerminateProcess(). которая и завершает процесс. Но в данном случае не освобождаются используемые процессом DLL, хотя все используемые объекты освобождаются. Освобождается также и память, занимаемая процессом. Число пользователей процесса также уменьшается.
Отметим один интересный факт. Обычно один процесс запускает другой как обособленный и после запуска забывает о нем. Для того чтобы порожденный процесс мог быть завершен, сразу после создания процесса порождающий процесс должен закрыть хэндл порожденного процесса и его потока. Делается это примерно следующим образом:
PROCESS INFORMATION Processlnformation;
BOOL hMyProcess;
if ((hMyProcess = CreateProcess(...... &ProcessInformation))
{
CloseHandle(ProcessInfonnation.hThread);
CloseHandle(ProcessInformation.hProcess);
227
О процессах можно рассказать намного больше, но надеюсь, что написанного хватит для того, чтобы приступить к программированию. Для того чтобы проиллюстрировать все эти длинные рассуждения, приведу в качестве примера небольшую программу.
ДЕМОНСТРАЦИОННАЯ ПРОГРАММА
В этой программе не происходит ничего интересного. Просто при выборе элемента меню «Создать процесс» создается процесс, в котором запускается обычный Notepad (надеюсь, он у всех в доступной директории? Если нет, то вы можете заменить Notepad любой другой программой). Максимум может быть запущено 10 процессов. По команде «Kill process» процессы уничтожаются в порядке, обратном их созданию. Предлагаю читателю обратить внимание на то, что я завершаю процесс посредством вызова TerminateProcess(), а не ExitProcessQ. Для того чтобы завершить процесс обычным способом, пришлось бы писать программу, которая вызывала бы функцию ExitProcessQ изнутри процесса, а мне бы не хотелось рассеивать внимание читателя. Результаты создания процесса, взятые из структуры типа PROCESS_ INFORMATION, отображаются в окне сообщений. Если кого-то раздражает необходимость постоянно убирать окно сообщений с отображения, рекомендую воспользоваться программой pview95.exe, которая поставляется с SDK.
Текст демонстрационной программы приведен ниже:
#include <windows.h>
#include <stdio.h>
#include "proc.h"
LRESULT CALLBACK ProcessesWndProc ( HWND, UINT, UINT, LONG );
int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrevInstancc, LPSTR IpszCmdParam, int nCmdShow )
HWND hWnd ;
WNDCLASS WndClass ;
MSG Msg;
char szClassName[] = "Processes"; /* Registering our window class */ /* Fill WNDCLASS structure */
WndClass.style = CS_HREDRAW CS_VREDRAW;
WndClass. IpfnWndProc = ProcessesWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra - 0;
WndClass. hlnstance = hlnstance ;
WndClass.hlcon = Loadlcon (NULL,IDI_APPLICATION);
228
WndClass.hCursor= LoadCursor (NULL, IDC_ARROW); WndClass.hbrBackground - (HBRUSH) GetStockObject (WHITE_BRUSH); WndClass.IpszMcnuName = "ProcessesMeivu"; WndClass.IpszClassName = szClassName;
if( !RegisterClass(&WndClass)) {
MessageBox(NULL,"Cannot register class","Error",MB_OK); return 0;
hWnd = CrcateWindow(szClassName, "Processes Demo",
WS^OVERLAPPEDWINDOW, CWJJSEDEFAULT, CWJJSEDEFAULT, CWJJSEDEFAULT, CWJJSEDEFAULT, NULL, NULL, hlnstance.NULL);
if(!hWnd)
t t
MessageBox(NULL,"Cannot create window", "Error",MB_OK); relurn 0;
/* Show our window */
ShowWindow(hWnd,nCmdShow);
UpdateWindow(hWnd);
/* Beginning of messages cycle */
while(GetMessage(&Msg, NULL, 0, 0))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg); i return Msg.wParam;
LRESULT CALLBACK ProcessesWndProc (HWND hWnd, UINT Message,
UINT wParam, LONG IParam )
const Max ~ 10;
STARTUP1NFO Startuplnfo;
static int ProccssNumber = 0;
static PROCESS INFORMATION ProccssInformationfMax];
static char cMyMessage[80]; static HMENU hSubMcnu;
switch(Message)
i
case WM CREATE: hSubMemi = GetSubMenu(GetMcnu(hWnd).0);
229
return 0;
case WM_COMMAND: switch( LOWORD(wParam))
case IDM_New_Process: if(ProcessNumber < Max)
t
Startuplnfo.cb = sizeof(STARTUPINFO);
StartupInfo.lpReservcd = NULL;
StartupInfo.lpDesktop = NULL;
StartupInfo.lpTitlc - NULL;
StartupInfo.dwFlags = STARTFJJSESHOWWINDOW;
StartupInfo.wShowWindow = SW_SHOWNORMAL;
StartiipInfo.cbReserved2 = 0;
StartupInfo.lpReserved2 = NULL;
if(CreateProcess(NULL."Notepad.exe",
NULL, NULL, FALSE, 0,
NULL, NULL, &StartupInfo,
&(ProcessInformation[ProcessNumber])))
ProcessNumber—;
wsprintffcMyMessage,"hProcess is %x.\nhThread is
%x.\ndwProcess!d is %x.\ndwThrcad!d is %x.", ProcessInformation[ProcessNumber - Ij.hProcess, ProcessInrormation[ProcessNumber - IJ.hThread, ProcessInformation[ProcessNumber - IJ.dwProcessId, ProcessInformation[ProccssNumber - IJ.dwThrcadld);
MessageBox(hWnd, cMyMessage, "Process is created", MB_OK);
EnableMenuItem(hSubMenu, IDMJCill_Process,
MF_BYCOMMAND | MF_ENABLED);
else
MessageBox(hWnd, "Cannot create process", "Process creation", MB_OK);
else
!
MessageBoxfhWnd, "Too many created processes...", "Process creation", MB_OK);
break;
case IDM_Kill_Process: if(ProcessNumber > 0)
if(TenninatcProcess(ProcessIntbrmation[ProcessNumber-l].hProcess, 0))
ProcessNumber-; if(! ProcessNumber) Enab!eMenuItem(hSubMenu, IDM Kill_Process,
MF BYCOMMAND | MF_GRAYED);
230
else McssageBox(hWnd. "Cannot terminate process",
"Process termination", MB OK)' }
else MessageBox(hWnd, "No more processes", "Process termination"
MB_OK); break;
case IDM_Exit:
SendMessage(hWnd, WM_CLOSE, 0, 0); break; !
return 0;
case WM_ DESTROY: PostQuitMessage(O); return 0; » return DefWindowProc(hWnd,Mcssage,wParam, IParam),
В этой программе используется файл описаний:
#define IDM_ About 104
#define IDM_Exit 103
#define IDM_KiIl_Process 102
#define IDM_New_Proeess 101
Кроме этого, в программе используется файл ресурсов:
^include "proc.h"
ProcessesMcnu MENU i
POPUP "&Processes"
{
MENU1TEM "&New process", IDM_New Process
MENUITEM "&Kill process", IDM_Kill_Proccss GRAYED
MENU1TEM SEPARATOR
MENUITEM "E&xit", IDM Exit
POPUP "AHelp"
{
MENUITEM "&About", IDM About
231
СОЗДАНИЕ ПОТОКА
Создание потока в большей степени (внешне, конечно) напоминает программу для Windows, чем создание процесса. Дело в том, что для создания потока используется функция CreateThread() (аналог WinMain()), одним из аргументов которой является указатель на функцию потока (аналог оконной функции). Но давайте обо всем по порядку.
Итак, начнем по уже сложившейся традиции, с прототипа функции. Она описана в файле winbase.h:
WINBASEAPI HANDLE WINAPI CreateThrcadf
LPSECURITY_ATTRIBUTES IpThrcadAttributes, DWORD dwStackSizc,
LPTHREAD_STARTJlOUTINEIpStartAddress, LPVOID IpParameler. DWORD dwCreationFlags, LPDWORD IpThreadld);
При вызове этой функции происходит следующее:
в памяти создаются все необходимые для управления потоком структуры (назовем их объектом «поток»);
код завершения потока инициализируется значением STILL_ACTIVE;
создается структура типа CONTEXT для потока (к сожалению, я не могу описать структуру в рамках этой книги - она слишком велика, но рекомендую читателю самостоятельно разобраться с ней по заголовочным файлам и файлам системы помощи);
создается стек потока;
инициализируется регистр - указатель стека в структуре типа CONTEXT так, чтобы он указывал на верхнюю границу стека, а регистр -указатель команд - на точку входа функции потока.
Рассмотрим аргументы этой функции.
АРГУМЕНТЫ ФУНКЦИИ CREATETHREADQ
Первый аргумент - IpThreadAttnbutes - является указателем на структуру типа SECURITY_ATTRIBUTES. Так как в Windows'95 атрибуты безопасности не используются, то обычно этот аргумент равен NULL.
Второй аргумент - dwStackSize - определяет размер выделяемого потоку стека. Если в качестве этого параметра указан 0, то поток будет иметь стек такого же размера, как и у породившего его потока.
Третий аргумент этой функции - IpStartAddress - собственно и определяет поток, так как является адресом точки входа функции потока. Функ-
232
ция потока может иметь имя, определяемое программистом, но должна иметь следующий прототип:
DWORD WINAPI ThreadFunctionfLPVOID IpParameter);
Я не случайно дал аргументу этой функции и четвертому аргументу функции CreateThreadQ одинаковые имена. Четвертый аргумент функции CreateThreadQ - это параметр, передаваемый функции потока. Что и каким образом передается в этом параметре, совершенно неважно. Это могут быть всевозможные данные, которые функция потока может использовать для своей работы.
Если следующий аргумент - dwCreationFlags - равен нулю, то выполнение потока начнется немедленно. Если этот аргумент будет равен CREATE_SUSPENDED, то начало выполнение потока будет задержано до определенных событий, например, до вызова функции ResumeThreadQ.
И наконец, в значение, определяемое последним аргументом, IpThreadld, записывается идентификатор созданного потока. А значение, возвращаемое функцией, является хэндлом этого потока.
Раз у потока есть начало, то должно быть и
ЗАВЕРШЕНИЕ ПОТОКА
Как и процесс, поток может быть завершен двумя способами - вызовом функции ExitThreadQ и обращением к функции TenninateThread(). Отличаются они друг от друга примерно тем же, что и функции ExitProcessQ и TerminateProcessQ. Первая функция, ExitThreadQ, используется для нормального завершения потока. Естественно, что она вызывается изнутри потока. Она описана в файле winbase.h:
WINBASEAPI VOID WINAPI ExitThread(DWORD dwExitCode);
Единственным ее аргументом является двойное слово, в которое будет помещен код возврата этой функции.
Функцию TerminateProcess(), описанную в том же файле winbase.h следующим образом,
WINBASEAPI BOOL WINAPI TerminateThreadfHANDLE hThread,
DWORD dwExitCode);
следует вызывать только в крайних случаях, когда поток завис, и ни на какие действия пользователя не реагирует. Функция вызывается из какого-либо внешнего (по отношению к завершаемому) потока, а ее
233
аргументами являются хэндл завершаемого потока и двойное слово, в которое будет записан код завершения потока.
Осталось только узнать, что происходит при завершении потока. Во-первых, освобождаются или удаляются все занятые или созданные объекты. Это действие является стандартным и ничего особенного собой не представляет. Во-вторых, поток получает статус незанятого (signaled). В-третьих, код завершения процесса меняется со STILL ACTIVE на указанный при вызове завершающей поток функции. В-четвертых, уменьшается счетчик пользователей потока. Если пользователей потока больше не осталось, и поток является единственным потоком процесса, то завершается и процесс. Все легко, просто и логично.
СИНХРОНИЗАЦИЯ
К этому моменту читатель уже знает, что в программе один поток, главный, запускается автоматически при запуске. Следовательно, создавая новые потоки, мы тем самым делаем программу многопотоковой. Хорошо, конечно, если эти потоки работают независимо. А как быть тем потокам, которые зависят друг от друга? Например, осуществляют доступ к одному и тому же файлу или продолжение работы одного зависит от выполнения какого-то условия в другом? Для решения этих проблем в Win32 предусмотрен механизм синхронизации, который позволяет, что следует из его названия, синхронизировать работу потоков.
Обычно поток, работа которого зависит каким-то образом от другого потока, сообщает системе о том, какое событие он ожидает. После этого выполнение этого потока приостанавливается до наступления ожидаемого события. Обычно для синхронизации используются четыре типа объектов - семафоры, исключающие семафоры (объекты типа mutex), события и критические секции. Далее мы поговорим об этих объектах.
СЕМАФОРЫ
Семафор действует как обычный флажок, и используется для того, чтобы определить, свободен или нет в настоящее время требующийся потоку ресурс. Тем не менее, фактически семафор является не просто флагом. Для того чтобы создать семафор, приложение должно вызвать функцию CreateSemaphore(), описание которой находится в файле winbase.h и выглядит следующим образом:
WINBASEAPI HANDLE WINAPI CreateSemaphoreA(
LPSECURITY_ATTRIBUTESlpSemaphoreAtlributes, LONG ITnitialCount. LONG IMaximumCount, LPCSTR IpName);
234
WINBASEAPI HANDLE WINAPI CreateSemaphoreWf
LPSECURITY_ATTRIBUTESlpSemaphoreAttributes, LONG UnitialCount, LONG IMaximumCount. LPCWSTR IpNamc);
#ifdef UNICODE
#define CreateSemaphore CreateSemaphoreW
#else
#dcfine CreateSemaphore CreateSemaphoreA
#endif// IUNICODE
Первый аргумент, что и следует из его типа, является указателем на структуру, содержащую атрибуты доступа к семафору. Он может также принимать значение NULL в том случае, если эти атрибуты не используются, как, например, в Windows'95.
Второй аргумент - начальное значение счетчика учета ресурсов. Этот аргумент определяет, сколько потоков может получить доступ к ресурсам в момент вызова функции. К примеру, компьютер имеет три порта, к которым обращается программа. В этом случае значение счетчика учета ресурсов может быть в пределах от 0 (нет свободных портов) до трех (все порты свободны). При обращении потока к ресурсу система проверяет, свободен ли ресурс, т. е. не установлено ли максимальное значение счетчика учета ресурсов (третий аргумент функции), после чего разрешает или запрещает доступ к ресурсу. Если для потока ресурсы недоступны, то он будет ждать освобождения ресурсов.
Последний, четвертый аргумент, - это указатель на строку, содержащую имя семафора.
При успешном завершении функция возвращает хэндл созданного семафора. Возвращение NULL сигнализирует о том, что произошла ошибка.
Если два процесса используют семафор с одним и тем же именем, то в этом случае используется один и тот же семафор. Использование этого семафора и является способом синхронизации потоков.
Перед завершением потока, использующего семафор, последний должен быть освобожден. Это делается с помощью функции ReleaseSemaphoreQ, описание которой, взятое из файла winbase.h, приведено ниже:
WINBASEAPI BOOL WINAPI ReleaseSemaphore(HANDLE hSemaphore,
LONG IReleaseCount, LPLONG IpPreviousCount);
Первый аргумент - это хэндл семафора, полученный с помощью функции CreateSemaphore(). Второй аргумент определяет, какое значение
235
должно быть установлено в счетчике ресурсов семафора при его освобождении. В двойное слово, адрес которого определяется третьим аргументом, записывается предыдущее значение счетчика.
Таблица 55. Флаги доступа к семафору
Параметр
Описание
SEMAPHORE_ALL_ACCESS
SEMAPHORE_MODIFY_STATE
SYNCHRONIZE
Устанавливает все возможные флаги доступа к семафору
Разрешается изменение счетчика ресурсов в функции ReleaseSemaphoreQ Разрешается использование в любой из ожидающих функций сигнала об изменении состояния семафора
Поток, которому заведомо известно, что семафор уже создан, может не создавать семафор, а открыть его с помощью функции OpenSemaphoreQ. Ниже приведено описание этой функции, взятое из файла winbase.h:
WINBASEAPI HANDLE WINAPI OpenSemaphoreA(DWORD dwDesiredAccess,
BOOL blnheritHandle, LPCSTR IpName);
WINBASEAPI HANDLE WINAPI OpenSemaphoreW(DWORD dwDesiredAccess,
BOOL blnheritHandle, LPCWSTR IpName);
#ifdefUNICODE
#defme OpenSemaphore OpenSemaphoreW
#else
#defme OpenSemaphore OpenSemaphoreA
#endif// ! UNICODE
Первый аргумент определяет уровень доступа к семафору и может принимать значения, приведенные в табл. 55.
Второй аргумент определяет, наследуют ли этот семафор другие процессы, создаваемые функцией CreateProcessQ. Значение TRUE говорит о том, что семафор является наследуемым.
Главным аргументом в этой функции является третий аргумент, определяющий имя открываемого семафора. Если функция выполняется успешно, то она возвращает хэндл открытого семафора.
Созданный или открытый семафор можно использовать с помощью функции WaitForSingleObject(), описание которой, приведенное ниже, можно найти в файле winbase.h:
236
WINBASEAPI DWORD WINAPI WaitForSingleObject(HANDLE hHandle,
DWORD dwMilliseconds);
Первый аргумент функции очевиден - хэндл семафора. Второй аргумент определяет время ожидания наступления события в миллисекундах. Если это значение равно 0, то функция сразу же прекращает ожидание и возвращает управление. Если время ожидания определено как INFINITE, то ожидание наступления события не прекращается. Функция может вернуть значения, приведенные в табл. 56.
Алгоритм работы с семафорами выглядит следующим образом:
поток создает или открывает семафор с помощью функций CreateSemaphore() или OpenSemaphore() соответственно;
поток вызывает функцию WaitForSingleObjectQ (или WaitForMultipleObjects()) Для того, чтобы определить, свободен ли требующийся потоку ресурс. В зависимости от результата, возвращаемого этой функцией, определяются дальнейшие действия;
при завершении поток вызывает функцию ReleaseSemaphoreQ, освобождающую семафор.
СОБЫТИЯ
События являются самой примитивной разновидностью объектов синхронизации. Они используются для того, чтобы оповестить поток о том, что наступило ожидаемое событие. Эти объекты обычно используются для того, чтобы синхронизировать потоки, которые работают по принципу конвейера. К примеру, один поток опрашивает датчики и загружает считанные значения в буфер. Другой поток считывает эти данные из буфера и производит их обработку. Первый поток может сигнализировать второму о том, что событие - заполнение буфера -наступило. Второй поток может сигнализировать первому о том, что наступило другое событие - данные из буфера считаны, ожидается новая порция данных. Событие может иметь два состояния - занятое (nonsignaled) и свободное (signaled).
Таблица 56. Значения, возвращаемые функцией WaitForSingleObjectQ
Параметр |
Значение |
Описание |
WAIT OBJECT 0 WAITJTIMEOUT WAIT_ABANDONED WAIT_FAILED |
0x00000000 0x00000102 0x00000080 OxFFFFFFFF |
Объект перешел в состояние свободного Объект за указанное время не перешел в состояние свободного Объект mutex стал свободным из-за отказа от него Произошла ошибка |
237
Для того чтобы использовать событие, его нужно создать. Делается это посредством функции CreateEvent(), описание которой, приведенное ниже, взято из файла winbase.h:
WINBASEAPI HANDLE W1NAPI CreateEventA(
LPSECURITY_ATTRIBUTESlpEvc[itAttributes,
BOOL bManualResel,
BOOL blnitialState,
1.PCS1 R IpName); WINBASEAPI HANDLE WINAPI CreatcEvcntW(
LPSnCURITY_ATTRIBUTES IpEveiuAtlribules,
BOOL bManualReset,
BOOL blnitialState,
LPCWSTR IpName);
#ifdef UNICODE
#definc CreateEvcnt CreateEvcntW
«else
Adeline CreateEvent CreateEventA
#endif// IUNICODE
С первым аргументом этой функции - указателем на структуру, содержащую атрибуты доступа, мы уже знакомы.
Второй аргумент определяет тип создаваемого события. Если значение этого параметра равно TRUE, то создается объект, для сброса которого в свободное состояние необходимо использовать функцию ResetEventQ. При значении FALSE создается событие, автоматически сбрасывающееся в свободное состояние.
Третий аргумент определяет начальное состояние создаваемого события. Значение TRUE определяет, что создается событие в СВОБОДНОМ состоянии. Если поток намерен создать событие в занятом состоянии, то он должен установить этот параметр в FALSE.
И наконец, последний, четвертый аргумент, определяет имя создаваемого объекта - события.
При успешном выполнении функция возвращает хэндл созданного объекта-события. Если при выполнении функции встретилась ошибка, то возвращается значение NULL.
Для того чтобы сигнализировать о наступлении события, в потоке должна присутствовать функция SetEventQ, переводящая событие в свободное состояние, описанная в winbase.h следующим образом:
WINBASEAPI BOOL WINAPI Se(Event(HANDLE hEvent);
Единственный аргумент этой функции очевиден - хэндл события, созданного посредством CreateEventQ.
238
Каким образом прикладная программа может узнать о наступлении события? Да с помощью уже знакомой нам функции WaitForSingleObjectf).
Для того чтобы сбросить событие в занятое состояние, необходимо вызвать функцию ResetEvent(), описанную в том же winbase.h следующим образом:
WINBASEAPI BOOL WINAPI ResetEvent(HANDLE hEvent);
В качестве аргумента функции передается хэндл события.
Достаточно часто встречаются случаи, когда после установки события с помощью SetEventQ тут же следует вызов ResetEventQ. Для этих целей предусмотрена функция PulseEvent(), описанная так:
WINBASHAPI BOOL WINAPI PulscEvcnt(HANDLE hEvent);
Аргументом является хэндл события. После выполнения функции объект-событие остается в занятом состоянии.
Алгоритм использования объекта-события полностью аналогичен алгоритму использования семафора.
Надеюсь, что после того, что сейчас узнал читатель, разобрать критические секции и объекты типа mutex ему не составит труда.
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
Думаю, что, посмотрев на размер исполняемого файла, полученного после компиляции нашей первой программы «Helloworld», многие были поражены. Как! Столько всего умеет делать программа при таком малом размере! Даже на ассемблере невозможно написать библиотеку такого размера и обладающую такими возможностями. Как же это сделано'? Ответ прост - большая часть кода, обеспечивающего возможности программы, находится вне самое программы, в библиотеках. Это естественно и понятно. Но с исполняемым файлом эти библиотеки соединяются не на стадии липковаиия, как обычные библиотеки, а НА СТАДИИ ВЫПОЛ-НР.НИц! Это одно из принципиальных положений, отличающих все версии Windows от ее главного в прошлом конкурента MS DOS.
Библиотеки динамической компоновки представляют собою одного из тех китов, на которых базировались Windows всех версий, в том числе и Windows'95. Все функции API, с которыми мы работаем, находятся в библиотеках динамической компоновки - DLL (dynamic iink libraries). Основу Windows составляют три библиотеки: kerne!32dll, user32.dll и gdi32.dll. Первая отвечает за управление памятью, процессами и потоками, вторая - за систему окон с подсистемой сообщений, третья - за графику и вывод текста (само название - GDI - является аббревиатурой выражения Graphical User Interface - графический интерфейс пользователя).
239
Это, так сказать, самое Windows. Многочисленные DLL, которые можно найти в директории Windows, являются ее расширениями. Но, естественно, пользователи об этом и не догадываются.
Как правило, написать DLL значительно легче, чем разработать программу, ибо DLL - это всего-навсего набор автономных функций, предназначенных для вызова из других приложений или DLL. Естественно, что при этом условии в DLL совершенно не нужен цикл обработки сообщений или функция создания окна, что, как правило, присутствует в вызывающей программе. DLL компилируются и линкуются точно так же, как и обычные программы, единственное, необходимо указать линкеру, что необходимо создать DLL, а не программу. В этом случае цинкование проходит несколько другим способом, в результате чего в конечный файл записывается другая информация, и загрузчик Windows с легкостью отличает DLL от обычной программы.
СПОСОБЫ ПРИСОЕДИНЕНИЯ DLL К ПРОГРАММЕ
Для того чтобы программа смогла выполнить код, находящийся в библиотеке, необходимо этот код сначала загрузить в память, выделенную вызывающему процессу, после чего оповестить вызывающий процесс о том, где находится загруженный код.
Решить эту задачу можно двумя способами. Первый способ - это неявное линкование с DLL. Второй способ - явная загрузка DLL.
НЕЯВНОЕ ЛИНКОВАНИЕ С DLL
При неявном линковании программа не знает о том, какую библиотеку ей необходимо присоединить. Для того чтобы неявно прилинковать библиотеку, необходимо на этапе подготовки проекта произвести некоторые действия: создать файл с расширением .lib, содержащий ссылку на DLL и перечень находящихся в ней функций. Делается это с помощью утилиты implib. Вызывается она следующим образом:
implib FileNamel.lib FileName2.dll
где FileNamel.lib - это имя создаваемого файла; а FileName2.dll - DLL. Полученный lib-файл можно прилинковать к вашей программе точно так же, как и любую другую библиотеку.
Для того чтобы проиллюстрировать процесс вызова DLL
при неявном линковании, я напишу библиотеку, в которой будет находиться всего одна функция. При обращении к этой функции будет выдаваться окно с сообщением (MessageBox) «Сейчас мы в DLL!». Наверное, трудно при-
думать что-то более простое, а? Приложение, которое будет обращаться к этой библиотеке, всего-навсего будет вызывать эту функцию, не создавая никаких окон, циклов обработки сообщений и т. д. Листинги программы и библиотеки (я назвал ее dll.dll) приведены ниже:
#ifdef_MYDLL_
#define MY API _decispec(dllexport) #e!sc
#define MYAPI _declspec(dllimport)
#endif
MYAPI void CALLBACK MyMessagc();
Листинг № 7. Файл заголовков библиотеки dll.h: ^include <windows.h> #define _MYDLL_
MYAPI void CALLBACK MyMcssageQ {
McssageBox(NULL. "Now we are in DLL!", "Hurray!" , MB_OK); i
Листинг № 8. Основной файл библиотеки dll.cpp:
LIBRARY MyMessage
DESCRIPTION 'Program'
EXETYPE WINDOWS
CODE PRELOAD MOVEABLH DISCARDABLE
DATA PRELOAD MOVEABLE SINGLE
Листинг № 9. Файл определения модуля dll.def:
//include <\vindows.h> //include "dll.h"
hit WiNAPI WmMaiufHlNSTANCE hInsiance,HINSTANCE hPrevInstance,
LPSTR Ips/CmdLinc, int nCmdShow)
i
MyMessageQ; return !;
240
241
Листинг №10. Основной файл программы, осуществляющей вызов библиотечной функции, арр.срр посредством неявной компоновки:
NAME МуЛрр
DESCRIPTION 'Program'
EXETYPE WINDOWS
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE MULTIPLE
Листинг № 11. Файл определения модуля app.def.
Для того чтобы эта связка программа-DLL заработала, проделайте следующие действия:
создайте DLL;
с помощью утилиты IMPLIB создайте lib-файл;
при создании ехе-файла прилинкуйте файл, полученный с помощью implib, к вашей программе;
запустите ехе-файл.
Надеюсь, что после всех этих действий вы увидите на экране сообщение о том, что произошло обращение к DLL.
В данном случае при загрузке выполняемого файла система просматривает его для того, чтобы определить, какие DLL используются при его работе, после чего пытается загрузить требующиеся DLL. Поиск DLL осуществляется в следующих каталогах:
каталоге, содержащем ехе-файл;
текущем каталоге процесса;
системном каталоге Windows;
каталоге Windows;
каталогах, указанных в PATH.
Попробуйте изменить имя DLL-файла. Вы заметите, что если файл DLL не найден, то система выдает сообщение об этом и немедленно завершает процесс
У неявной компоновки есть свои преимущества и недостатки. По моему мнению, к преимуществам нужно отнести следующее:
программа может ничего не знать о том, что она использует DLL;
проверка доступности DLL производится еще до загрузки программы, т. е. в случае отсутствия DLL программа просто не запустится.
Недостатком такого способа можно считать то, что DLL загружается в память до программы и выгружается после окончания программы, т. е. программист не может управлять загрузкой и выгрузкой библиотек. Если программа использует несколько библиотек, то придется все библиотеки хранить в памяти от запуска до завершения программы. Наверное, иногда
242
неплохо было бы обратиться и к другому способу подключения библиотек к программе, который называется
ЯВНАЯ ЗАГРУЗКА DLL
В этом случае все манипуляции с DLL производит вызывающая программа. Для того чтобы библиотека загрузилась в память, должна быть вызвана одна из функций - LoadLibraryQ или LoadLibraryExQ.
В winbase.h эти функции описаны следующим образом:
WINBASEAPI HMODULE WINAPI LoadLibraryA(LPCSTR IpLibFileNamc);
WINBASEAPI HMODULE WINAPI LoadLibraryW(LPCWSTR IpLibFileName);
#ifdcf UNICODE
#define LoadLibrary LoadLibraryW
#else
#defme LoadLibrary LoadLibraryA tfendilV/ !UNICODE
WINBASEAPI HMODULE WINAPI LoadLibraryExA(LPCSTR IpLibFileNamc,
HANDLE hFile, DWORD dwFlags);
WINBASEAPI HMODULE WINAPI LoadLibraryExW(LPCWSTR IpLibFileName,
HANDLE hFile, DWORD dwFlags);
#ifdcf UNICODE
#dcflnc LoadLibraryEx LoadLibraryExW
#elsc
#define LoadLibraryEx LoadLibraryExA
#endif // (UNICODE
Аргументом первой функции является имя загружаемой DLL. Другая же при вызове должна получить три аргумента. Первый - то же имя загружаемой DLL. Второй аргумент зарезервирован и должен быть равен NULL. Третий аргумент должен представлять собой либо нуль, либо комбинацию ИЗ трех флагов: DONT_RESOLVE_DLL_REFERENCES, LOADJJBRARY_AS_DATAFILE, LOAD_WITH_ALTI:RED_SEARCH_PATH.
DONT_RESOLVE_DLL_REFERENCES
Чуть позже мы рассмотрим функцию, которая производит инициализацию и деинициализацию DLL автоматически при загрузке. Данный флаг заставляет систему не вызывать функцию инициализации. Кроме этого, при загрузке библиотеки система проверяет, не используются ли данной DLL функции из других DLL. Если используются, то загружаются и они. Если данный флаг установлен, то дополнительные библиотеки не загружаются.
243
LOAD LIBRARY_AS_DATAFILE
Этот флаг может использоваться в нескольких случаях. Во-первых, можно загружать библиотеку, не содержащую никакого кода и содержащую только ресурсы. Полученное значение HINSTANCE можно использовать при вызове функций, использующих ресурсы. Во-вторых, если мы загружаем ехе-файл обычным способом, то это приводит к запуску нового процесса. А как быть в том случае, если мы хотим получить доступ к ресурсам ехе-файла, не запуская его? При загрузке ехе-файла с помощью функции LoadLibraryExQ с установленным флагом LOAD_LIBRARY_AS_DATAFILE, возможно получить доступ к ресурсам ехе-файла.
LOAD_WITH__ALTERED_SEARCH_PATH
Ранее мы рассмотрели, какие каталоги и в какой последовательности просматриваются системой при загрузке DLL. Если установлен флаг LOAD_WITH_ALTERED_SEARCH_PATH, то просмотр каталогов начинается с каталога, указанного в первом аргументе функции LoadLibraryExQ. Далее просмотр продолжается в обычном порядке.
После загрузки DLL программа не может вызывать требующиеся ей функции. Дня того чтобы вызвать какую-либо функцию, ей необходимо сначала определить адрес этой функции с помощью GetProcAddressQ, a затем вызывать функцию через полученный адрес. После того, как надобность в присутствии DLL в памяти отпала, программа должна выгрузить ее с помощью функций FreeLibrary() или FreeLibraryAndExitThread(). Но сейчас разговор не об этом. Давайте попробуем рассмотреть предыдущий пример, измененный таким образом, чтобы DLL загружалась явно.
Само собой разумеется, что все, что касается DLL, никаким изменениям не подвергалось. Иначе какой смысл писать DLL, если в зависимости от потребностей программиста ее нужно было бы каждый раз переписывать? Изменился только основной файл программы, которая вызывает функцию из DLL. Итак...
^include <windows.h> #include "dll.h"
int WINAPI WinMain(HINSTANCE hlnstance. HINSTANCE hPrevInstance, LPSTR IpszCmdLine, int nCindShow)
i
HINSTANCE hDII; FARPROC MyProcAddr;
if( (hDII = LoadLibrary("dll.dll")) != NULL)
244
MyProcAddr - GetProcAddress(hDH, "MyMcssage"); else {
MessageBox(NULL, "Sorry, cannot find requested DLL", "Sorry", MB_OK); return 0; i
(MyProcAddr)O; FreeLibrary(liDII); return 1 ;
Листинг № 12. Основной файл программы, осуществляющей вызов библиотечной функции, арр.срр посредством явной за!рузки DLL.
С точки зрения пользователя заметить какие-либо отличия в работе программ, использующих для вызова функций из DLL неявную компоновку и явную загрузку, невозможно. С точки зрения программиста два отличия прямо-таки бросаются в глаза. Первое - если при неявной компоновке в случае отсутствия DLL программа просто не запускается, то в случае явной загрузки, возможно перехватить такую ситуацию и предпринять какие-либо действия. И второе - у программиста прибавилось головной боли. Вместо обычного обращения к функции он должен вызвать еще три вспомогательные функции, да и требующаяся функция из DLL вызывается не напрямую, а косвенно, посредством использования ее адреса. Еще раз повторю - программист должен сам решить, стоит ли овчинка выделки.
ВЫВЕРНЕМ ПРОГРАММЫ НАИЗНАНКУ
Давайте попробуем разобраться в том, что все это означает и к чему может привести.
Начнем с файла, который использует ся как библиотекой, так и приложением - файла заголовков. Чтобы ехе-файл мог вызывать функции из DLL, то, с одной стороны, библиотека должна объявить их как доступные, или, как говорят, экспортируемые. С другой стороны, сам ехе-файл должен определить эти функции как находящиеся в DLL, т. е. как импортируемые. Если мы объявим эти функции по-разному в заголовочном файле и непосредственно в тексте, мы не оберемся ошибок при компилч-ции. Следовательно, выход один - условная компиляция. Если мы посмотрим на распечатку заголовочного файла dll.li, то увидим, что я определяю макро MYAPI, которое принимает одно из двух значений (_declspec(dllexport)) или (_declspec(dllimport) ) в зависимости от факта определения другого макро, _MYDLL_ . Теперь понятно, что и в заголовочном, и в исходных файлах можно описать функцию, находящуюся в
245
DLL, как MYAPI, но при этом в исходном файле библиотеки мы должны определить макро _MYDLL , а в исходном файле приложения ни в коем случае это макро не определять. Что и сделано. Проверьте - работает! Посмотрите в заголовочные файлы Win32. По-моему, в них используется эта техника.
Описание функций как экспортируемых требуется линкеру для того, чтобы правильно построить таблицу экспортируемых функций в dll-файле. Каждый элемент в этой таблице содержит имя экспортируемой функции, а также ее адрес. Немаловажно, что список функций сортируется по алфавиту. Таким образом, если функция должна работать бмстро, то какое-то преимущество можно получить в том случае, если имя функции будет начинаться с первых букв алфавита. По это преимущество сработает только в момент выполнения GetProcAddressQ, а не (увы!) при обращении к функции. Для того чтобы разобраться во внутренностях DLL, воспользуемся утилитой TDUMP, поставляемой с Borland C++ 5.0 (аналогичные утилиты есть и в других системах программирования, например, в Microsoft Visual C++ подобная утилита называется DUMPBIN). Часть распечатки таблицы экспорта для kernel32.dll приведена ниже:
Turbo Dump Version 4.2.15.2 Copyright (с) 1988, 1996 Borland International Display of File KERNEL32.DLL
Exports from KERNEL32.dll
680 exported name(s), 780 export addresse(s). Ordinal base is I. Ordinal RVA Name
0049 |
0002d900 AddAtomA |
0101 |
00034c99 AddAtomW |
0102 |
0002f44b AllocConsole |
0103 |
00021b22 AllocLSCallback |
0104 |
0002 Ib55 AllocSLCallback |
0105 0106 |
0002e75b AreFileApisANSl 00034d20 BackupRead |
|
|
0774 0775 0776 0777 |
00007 Ida Istrcpyn 00007 Ida IstrcpynA 00034ccf IstrcpynW 00007251 Istrlen |
0778 |
00007251 IstrlenA |
0779 |
0002b83c IstrlenW |
246
Как можно убедиться, все функции рассортированы в алфавитном порядке? В первой колонке указаны ordinals - порядковые номера функций. В более ранних версиях Windows функции могли экспортироваться не только по именам, но и но порядковым номерам. Сейчас Microsoft рекомендует пользоваться только именами функций (именно поэтому я не описываю способ экспорта и импорта функций посредством указания порядковых номеров). Для справки - средняя колонка содержит Real Virtual Addresses - адреса функций внутри DLL.
Аналогично решается и вопрос при импорте функций. Функция объявляется как импортируемая, после чего линкер строит специальный код и таблицу импортируемых функций.
Кстати, помимо TDUMP можно воспользоваться утилитой IMPDEF, которая выдает список присутствующих в DLL функций. Что же касается моего личного мнения, то я рекомендую читателю изучить форматы файлов Windows и написать самостоятельно несколько утилит, которые будут использоваться для «выворачивания программ наизнанку» и показывать все те данные и в таком виде, который удобен программисту.
ИНИЦИАЛИЗАЦИЯ И ДЕИШЩИАЛЮАЦИЯ DLL
Мы разобрались, каким образом можно написать DLL и вызвать функцию из DLL. По, как правило, ьо всех нормальных (не демонстрационных) примерах существуют блоки, отвечающие за инициализацию и деннициализацию программы. Возникает вопрос: как это сделать в DLL? LcTb ли такая возможность? Да, есть! В каждой библиотеке может быть функция, которая вызывается строго в определенных обстоятельствах и обычно используется библиотекой для инициализации и деиницналнза-ции. В нашей микро-DLL эта функция не использовалась, однако, я счел бы тему рассмотренной не полностью, если бы обошел этот вопрос стороной
В Borland C++ v. 5.0 эта функция по умолчанию называется DllEntryPointO и в некотором смысле является аналогом связки LibMi/:n() + WEP() в Windows 3.x. Вызывается эта функция всего в четырех случаях и имеет следующий вид:
BOOL WINAPI DHEntryPoint(HINSTANCE hinstDll, DWORD fdwReason, LPVOID IpvReserved)
{ switch(fdwReason)
{ case DLL_PROCESS ATTACH:
247
/* Операторы */ case DLL_THREAD_ATTACH:
/* Операторы */ case DLL_THREAD_DETACH:
/* Операторы */ case DLL_PROCESS_DETACH:
/* Операторы */
} return(TRUE);
Лично мне именно такой способ инициализации и деинициализа-ции библиотек динамической компоновки очень импонирует. Дело в том, что он использует оператор switch - case, который применяется при обработке сообщений. Именно из-за этого вся конструкция зрительно воспринимается так же, как и оконная процедура. Мне кажется, что с такой функцией намного приятнее иметь дело, чем со связкой LibMainQ - WEP в Windows 3.x.
Первый аргумент этой функции - это хэндл библиотеки, присваиваемый системой.
Второй аргумент указывает причину вызова этой библиотеки системой.
Третий аргумент, как понятно из его названия, пока зарезервирован и обычно должен быть равным NULL.
Теперь нам необходимо до конца разобраться с причинами вызова библиотеки.
DLL_PROCESS_ATTACH
Система вызывает функцию инициализации с этим значением параметра fdwReason единственный раз при загрузке библиотеки. Другими словами, если один из потоков процесса, вызвавшего DLL, пытается вновь загрузить ее с помощью LoadLibraryQ, то обращение к DLL с параметром DLL_PROCESS_ATTACH не произойдет. Система увеличит счетчик пользователей этой DLL.
Значение, возвращаемое функцией инициализации, после обработки DLL_PROCESS_ATTACH, уведомляет пользователя, была ли инициализация успешной. В случае неуспешной инициализации функция должна возвратить FALSE, при успехе - TRUE. Это значение используется как при неявной, так и при явной загрузке DLL в память.
248
DLL PROCESS JDETACH
Вызов функции инициализации для обработки DLL_PROCESS_DETACH означает, что библиотека из памяти выгружается и должна произвести действия по деинициализации самое себя. Помимо того, что необходимо освободить память и другие ресурсы, хорошим тоном считается оставить систему точно в том же состоянии, в каком ее приняла библиотека (если, конечно, изменение параметров системы не является задачей одной из функций DLL). При выгрузке библиотеки есть одна тонкость, связанная с причиной завершениия процесса, обратившегося к ней. Если DLL выгружается в связи с вызовом функции ExitProcessQ (или FreeLibraryQ, хотя это и не связано с процессом), вызов функции инициализации проходит нормально. Но если процесс завершается благодаря функции TerminateProcess(), функция инициализации НЕ ВЫЗЫВАЕТСЯ! Таким образом, попутно можно сделать еще один вывод - функцией TerminateProcessQ можно и нужно пользоваться только в самых крайних случаях!
Ниже приведен чуть измененный листинг библиотеки dll.c; попробуйте загрузить ее и понаблюдать за тем, как она работает:
^include <windows.h> tfdettne _MYDLL_ #include "dll.h"
BOOL WINAPI D!IEntryPoint(HINSTANCE hinstDII, DWORD fdwReason,
LPVOID IpvReserved) {
switch(fdwReason) {
case DLL PROCESS_ATTACH: MessageBox(NULL, "We are in DLL_PROCESS_ATTACH!", "Hurray!",
MB_OK); break;
case DLL_THREAD_ATTACH: MessageBox(NULL, "We are in DLL_THREAD_ATTACH!", "Hurray!",
MB_OK); break;
case DLL_THREAD_DETACH: MessageBox(NULL, "We are in DLL_THREAD_DETACH!", "Hurray!",
MB_OK); break;
case DLL PROCESS DETACH:
MessageBox(NULL, "We are in DLL_PROCESSJ)ETACH!", "Hurray!", MBJDK);
249
break;
} retum(TRUE);
/
MYAPI void CALLBACK MyMessageQ
"I
MessageBox(Gc4DcsktopWindo\v().''nLI, is called!", "Hurray!" . MB_OK},
i j
Листинг № 13. Библиотека dll.c, включающая код функции инициализации.
Очередной раз - ура! Мы научились писать библиотеки динамической компоновки, добавлять в них функции инициализации и деишшиализа-ции, подключать DLL к нашим программам, используя как неявную, так и явную загрузку.
КОНСОЛИ
«Неужели для того, чтобы написать простейшую программу, которая выводит на экран несколько строк, мне необходимо городить этот огород с WinMainQ и функцией окна'.' Неужели в каждой моей программе, предназначенной для вывода текста на экран, должно присутствовать «стандартное заклинание»? Или же я вынужден вечно быть ограниченным рамками DOS?» Я предвижу такие вопросы со стороны тех, которым при разработке их программ не только ns нужен, но и мешает графический интерфейс.
Что ж, вопросы вполне понятны и закономерны. Наверное, именно эта закономерность и обусловила появление в Win32 новых функций, обеспечивающих эмуляцию текстового терминала. Эти функции называются функциями консоли,
ЧТО ТАКОЕ КОНСОЛЬ
Консоль - это интерфейс, обеспечивающий поддержку программ, работающих в текстовом режиме, т. е. программ, написанных в стиле MS DOS. Консоль состоит из буфера ввода и одного или нескольких экранных буферов. Буфер ввода включает в себя очередь, каждая запись в которой содержит информацию о вводных событиях. Под вводными событиями в данном случае подразумеваются нажатия и отжатая клавиш на клавиатуре, на мыши, движения мыши, а также действия пользователя, производимые с окном. Экранный буфер - это двумерный массив, кото-
250
рый содержит коды символов и цвета символов текстового экрана (аналог видеобуфера в текстовом режиме при работе в MS DOS).
Каждая программа, работающая в текстовом режиме, взаимодействует с Windows через консоль. Если одна программа запускается из консоли, принадлежащей другой программе (скажем, aidstest запускается из консоли Norton Commander'a), то запускаемая программа работает в той же консоли. Если же программа запускается самостоятельно из Windows, то ей выделяется собственная консоль (фактически ей выделяется целая виртуальная машина). Другими словами, каждая программа может получить для себя эмулятор DOS-машины и считать, что весь компьютер принадлежит только ей.
Даже из этого краткого описания видно, что консоли могут оказать программисту довольно существенную помощь, состоящую, во-первых, в том, что обработка действий пользователя с мышью и клавиатурой производится средствами Windows, а, во-вторых, разрешают доступ к некоторым функциям API. В-третьих, каждая программа может работать в своей сессии. В-четвертых, программе доступны стандартные потоки ввода-вывода DOS. Наверное, даже этого краткого перечисления достаточно для того, чтобы убедить читателя в том, что разработку программ, не имеющих графического интерфейса, имеет смысл производить с учетом новых возможностей, предоставляемых Win32.
Мы долго говорили о том, что с консолями работают программы, написанные в стиле MS DOS. Но в языке С точкой входа для ООЗ'овских программ является функция mainQ, а не WinMainQ, следовательно, и консольные программы должны точкой входа тоже иметь функцию mainQ, а не WinMainQ. Таким образом, основными отличиями консольных программ от обычных программ для Windows являются: отсутствие графического интерфейса;
использование в качестве точки входа функции mainQ, а не WinMainQ.
ТЕХНИКА РАЗРАБОТКИ КОНСОЛЬНОЙ ПРОГРАММЫ
СОЗДАНИЕ КОНСОЛИ
Как уже было сказано, консольная программа должна иметь точкой входа не WinMainQ, a mainQ. При запуске программы она должна запросить для себя консоль, используя для этого функцию AllocConsoleQ. Ее прототип находится в файле wincon.h, к которому мы будем обращаться в этом разделе:
WINBASEAPI BOOL WINAPI AllocConsole(VOID);
251
Эта функция, возвращающая TRUE при успешном завершении, пре доставляет вызвавшей ее программе консоль. Внешне консоль выглядит так же, как и обычное окно. У него есть заголовок, системное меню, кнопки максимизации и минимизации. Если программе необходима собственная консоль, а она работает в унаследованной, то программа может перед вызовом AlIocConsole() произвести освобождение консоли, вызвав для этого функцию FreeConsole(), описание которой практически не отличается от описания предыдущей функции:
WINBASEAPI BOOL WINAPI FreeConsole( VOID );
Если программа запускается в самостоятельной консоли, то вызов FreeConsoleQ не повлечет за собой никаких неприятных последствий.
При завершении программы самостоятельная консоль автоматически уничтожается, унаследованная же продолжает свое существование до момента завершения породившей ее программы.
ПРИСВОЕНИЕ КОНСОЛИ ИМЕНИ
Очередным шагом после создания консоли будет присвоение консоли имени. Это имя будет отображено в заголовке консоли. Делается это с помощью функции SetConsoleTitle(). Из wincon.h извлекаем прототип этой функции:
WINBASEAPI BOOL WINAPI SetConsoleTitleA(LPCSTR IpConsoleTitle ); WINBASEAPI BOOL WINAPI SetConsoleTitleW(LPCWSTR IpConsoleTitlc);
#ifdefUNICODE
#define SetConsoleTitle SetConsoIeTitleW
#else
«define SetConsoleTitle SetConsoleTitleA
tfcndif// IUNICODE
Аргумент этой функции - указатель на строку символов, содержащую текст, который будет отображен в заголовке окна консоли.
ВВОД И ВЫВОД В КОНСОЛИ
Основные функции вывода в окно консоли
Когда я изучал этот вопрос, функции ввода и вывода в консоли напомнили мне вызов прерываний DOS (наверное, так и должно быть, ведь консоль эмулирует DOS-машину). Поэтому знакомые с прерываниями DOS программисты увидят в функциях ввода - вывода много «знакомого».
252
В DOS для операций ввода - вывода считалось, что стандартные потоки, такие, как поток ввода, поток вывода и поток ошибок, имеют стандартные, заранее определенные хэндлы. При работе в режиме консоли стандартные потоки предопределенных хэндлов не имеют, поэтому эти хэндлы необходимо получить, обратившись к функции GetStdHandleQ. По ее описанию -
WINBASEAPI HANDLE WINAPI GetStdHandle(DWORD nStdHandle);
извлеченному в данном случае из файла winbase.h, мы видим, что для получения хэндла стандартного потока в консольной сессии мы в качестве аргумента функции должны указать номер того потока, хэндл которого нам нужен. Запоминать номера потоков не нужно, они определены в том же файле winbase.h как STD_INPUT_ HANDLE, STD_OUTPUT_HANDLE и STD_ERROR_HANDLE. При успешном завершении функция возвращает хэндл требующегося потока, в противном случае возвращаемое значение равно INVALID_HANDLE_VALUE.
Определив хэндл стандартного потока, можно попытаться вывести текст в окно консоли с помощью функции WriteConsoleQ:
WINBASEAPI BOOL WINAPI WriteConso!eA(HANDLE hConsoleOutput,
CONST VOID «IpBuffer,
DWORD nNumberOfCharsToWrite,
LPDWORD IpNumberOfCharsWritten,
LPVOID IpReserved); WINBASEAPI BOOL WINAPI WriteConsolcW(HANDLE hConsoleOutput,
CONST VOID "IpBufTer,
DWORD nNumberOfCharsToWrite,
LPDWORD IpNumberOfCharsWritten,
LPVOID IpReserved);
#ifdefUNICODE
#defme WritcConsole WriteConsoleW
#else
#defme WriteConsole WriteConsoleA
#endif// IUNICODE
Аргументами этой функции являются:
хэндл стандартного потока вывода;
указатель на выводимую строку;
длина выводимой строки в символах;
в указатель на двойное слово, в которое записывается действительное число выведенных символов;
253
указатель на двойное слово, зарезервированное для использования в дальнейшем, который должен быть равным NULL.
Строка выводится, начиная с текущей позиции курсора, при этом используются текущие цветовые атрибуты текста и фона. Курсор устанавливается в позицию, следующую за последним символом строки.
В случае успешного завершения функция возвращает TRUE.
Для установки позиции курсора в консоли необходимо вызвать функцию SetConsoleCursorPositionQ, прототип которой можно найти в wincon.h:
WINBASEAPI BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOulput, COORD dwCursorPosition);
hConsoleOutput - это хэндл стандартного вывода консоли, а структура типа COORD, содержащая координаты новой позиции курсора, определяется в wincon.h следующим образом:
typedef struct _COORD {
SHORT X;
SHORT Y; } COORD, *PCOORD;
X и Y - координаты новой позиции курсора.
Если функция завершена успешно, она возвращает ненулевое значение.
Последнее, что нам осталось сделать для того, чтобы мы могли полностью управлять выводом, - это научиться устанавливать цветовые атрибуты выводимого текста. Учиться недолго - это делается с помощью функции SetConsoleTextAttribute(). Извлечем из wincon.h ее прототип:
WINBASEAPI BOOL WINAPI SetConsolcTexlAttribute(HANDLE hConsoleOutput,
WORD wAttribute.s);
hConsoleOutput - хэндл стандартного потока вывода консоли, a wAttribues определяет цвета тона и фона текста. wAttributes должен быть комбинацией нескольких флагов. Перечень флагов приведен в табл. 57.
254
Таблица 57. Атрибуты цветов фона и тона окна консоли
Флаг |
Значение |
Эффект |
FOREGROUND BLUE |
0x0001 |
Тон содержит синюю составляющую |
FOREGROUND_GREEN |
0x0002 |
Тон содержит зеленую составляющую |
FOREGROUND RED |
0x0004 |
Тон содержит красную составляющую |
FOREGROUND INTENSITY |
0x0008 |
Тон имеет повышенную интенсивность |
BACKGROUNDJ3LUE |
0x0010 |
Фон имеет синюю составляющую |
BACKGROUND GREEN |
0x0020 |
Фон имеет зеленую составляющую |
BACKGROUND RED |
0x0040 |
Фон имеет красную составляющую |
BACKGROUND INTENSITY |
0x0080 |
Фон имеет повышенную интенсивность |
|
|
или текст мигает (только в полноэкранном |
|
|
режиме) |
Точно так же обстоит дело и с фоном символов. Однако в зависимости от некоторых условий (обсуждение возможностей и регистров видеоадаптера явно выходит за рамки этой книги), установленный фла! интенсивности тона обеспечивает либо повышенную интенсивность фона, либо мигание символа. Но все это касается только программ, работающих в полноэкранном режиме (не путать максимизированную консоль с работой в полноэкранном режиме!). Рекомендую читателю проверить эту установку на своем компьютере. Программы в консольном режиме, к сожалению, не могут использовать мигание фона, поэтому те символы, для которых в полноэкранном режиме установлен флаг мигания, в консольном режиме не мигают, а отображаются с повышенной интенсивностью цвета фона символов.
Мы закончили краткое обсуждение функций, обеспечивающих вывод на консоль. Давайте теперь рассмотрим функцию, обеспечивающую
Ввод из окна консоли
Для ввода из окна консоли текста с клавиатуры используется функция
ReadConsoleO, описанная следующим образом: WINRASEAPI BOOL WINAPI ReadConso!c.A(
255
HANDLE hConsolelnput, LPVOID IpBuffer, DWORD nNumberOfCharsToRead, I.PDWORD IpNumberOfCharsRead, LPVOID IpRcserved); WINBASEAPI BOOL WINAPI ReadConsoleW(
HANDLE hConsolelnput, LPVOID IpBuffer, DWORD nNumberOPCharsToRead, LPDWORD IpNumberOfCharsRead, LPVOID IpReserved);
#ifdefUNICODE
#defme ReadConsole ReadConsoleW
#else
#defme ReadConsole ReadConsoleA
#endif// IUNICODE
Аргументами этого файла являются: hConsolelnput - хэндл потока ввода;
IpBuffer - указатель на символьный массив, в который будет записана строка символов;
nNumberOfCharsToRead - максимальное число вводимых символов; IpNumberOfCharsRead - число фактически считанных символов; IpReserved - зарезервировано для дальнейшего использования и должно быть равно NULL.
На этом мы завершаем рассмотрение основных функций, обеспечивающих ввод-вывод в окно консоли. После демонстрационной программы рассмотрим еще несколько функций, обеспечивающих работу с клавиатурой и мышью.
Демонстрационная программа
Перед тем, как привести текст демонстрационной программы, мне бы хотелось обратить внимание читателя на одну тонкость. Программы, которые мы рассматривали до сих пор, являлись оконными программами. Я не давал никаких пояснений по поводу их компиляции. Но та программа, которую мы будем разбирать сейчас, не имеет собственного графического интерфейса, т. е. оконной программой не является, а следовательно, и компилировать ее нужно несколько иным способом. Предлагаю читателю обратиться к руководству по той системе, с которой он работает, и выяснить, каким образом можно скомпилировать консольную программу. Если читатель паче чаяния работает с Borland C++ 5.0 в IDE, то ему при создании нового проекта в TargetExpert необходимо изменить TargelMode с GU! на Console.
256
Сделано? Тогда листинг демонстрационной программы перед вами:
#include <windows.h>
#includc <stdio.h> mainQ {
HANDLE hStdlnputHandle, hStdOutputHandle;
COORD Coord;
char cMyString[255] = "This is our first console program! It's working !";
DWORD dwResult;
FreeConsoleQ;
AllocConsoleQ;
SetConsoleTitle("ConsoIe Demonstration program");
hStdlnputHandle = GetStdHandle(STD_lNPUT HANDLE);
hStdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
Coord.X = (80 - strlen(cMyString)) / 2;
Coord. Y= 12;
SetConsoleCursorPosition(hStdOutputHandle, Coord);
SetConsoleTextAttribute(hStdOutputHandle, FOREG ROUNDJIED |
BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_GREEN FOREGROUNDJNTENSITY | BACKGROUNDJNTENSITY);
WriteConsole( hStdOutputHandle, cMyString, strlen(cMyString), &dwResult, NULL); SetConsoleTextAttribute(hStdOutputHandlc, 0); getchar();
SetConsoleCursorPosition(hStdOutputHandle, Coord); WriteConso!e(hStdOutputHandle, cMyString, strlen(cMyString),
&dwResult, NULL); Coord.X = 0; Coord.Y= 12;
SetConsolcCursorPosition(hStdOutputHandle, Coord); SetConsoleTextAttribute(hStdOutputHandle, FOREGROUND_RED [
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY); WriteConsolefhStdOutputHandle, "Type some letters and press Enter, please: ",
strlen(cMyString), &dwResult, NULL); SetConsoleTextAttributefhStdOutputHandle,
FOREGROUND_RED | BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUNDJ3REEN | FOREGROUNDJNTENSITY);
ReadConsolefhStdlnputHandle, cMyString, strlen(cMyString), &dwResult, NULL); return 0;
257
Окно, отображаемое при запуске программы, показало на рис
5 Сонм» yemomuation pcogi
Рис. 21. Окно-консоль, в которое иронзвелен выно.; счроки
Ничего особенного в этой программе не происходи1! Программа запрашивает собственную консоль и устанавливает заголовок этой консоли, после чего получаст хэндл' потоков ввода и вывода. Курсор устанавливается с таким расчетом, чтобы выводимая фраза размещалась в центре экрана. Цвет выводимых символов устанавливается как ярко-красный на ярко-белом (на рисунке показана консоль программы именно после этого момента). После вывода фразы программа ждет нажатия-Enter, после чего затирает фразу и предлагает пользователю что-нибудь набрать на экране, завершив набор нажатием клавиши Enter. Завершение ввода пользователя означает и завершение программы. Ничего сложного, но используются все функции управления консолью, которые мы уже изучили. На долю читателя я оставляю возможность самостоятельно исследовать, какие функции ввода - вывода из стандартных библиотек С и C++ могут быть использованы в консольных программах вместо функций API. Попробуйте, например, начать с исследования функции printf(). С другой стороны, приятной особенностью консольных программ является возможность вызова таких функций API, как, скажем, MessageBoxQ. Другими словами, при очень небольшой доработке большинство программ MS DOS могут быть перекомпилированы как консольные программы и могут использовать многие из тех возможностей, которые предоставляет
258
ОБРАБОТКА НАЖАТИЙ КЛАВИШ НА КЛАВИАТУРЕ И СОБЫТИЙ, ПРОИСШЕДШИХ С МЫШЬЮ
Мы уже знаем, что Windows - система, управляемая событиями. Консольные программы не являются исключением. По есть одна деталь, которую необходимо ел метить. События с клавиатурой и мышью записываются во входной буфе]) только в тех случаях, когда программа, во-первых, имеет клавиатурный фокус, и, во-вторых, указатель мыши находится в рабочей области консольного окна. Каждому событию, произошедшему с консолью, соответствует одна запись во входном буфере. Каждая запись о событии представляет собой заполненную структуру типа INPUT_RECORD, описание которой можно найти в файле vvincon.h:
typedcf struct _1NPUT_REC'ORD { WORD EventType; union )
KEY EVENT_RECORD KeyEvent;
MOUSE_EVENTRECORD MouseEvent;
WINDOWJ3UFFER SIZE RECORD WindowBuflcrSizcEvent;
MENU EVENT_RECORD MenuEvcnt;
FOCUS_EVENT_RECORD FocusEvent; j Event; } INPUT RECORD, *PINPUT RECORD;
Даже не особо вникая в смысл полей, видно, что консоль обрабатывает пять типов событий. Их перечень, взятый из файла wincon.h, приведен в табл. 58.
Т а б л и ц а 58. События, обрабатываемые консолью
Флаг |
Значение |
Эффект |
KEY EVENT |
0x000 1 |
Событие с клавиатурой |
MOUSE EVENT |
0x0002 |
Событие с мышью |
WINDOW_BUFFER_SIZE_EVENT |
0x0004 |
Событие по изменению размерен |
|
|
экрана |
MENU EVI-NT |
0x0008 |
Событие с меню |
FOCUS_ EVENT |
0x00 1 0 |
Изменение фокуса |
259
WINBASEAPI BOOL WfNAPI ReadConsoleInputA(
HANDLE hConsolelnput,
PfNPUT_RECORD IpBuffer,
DWORD nLength,
LPDWORD IpNumberOfEventsRead); WINBASEAPI BOOL WINAPI ReadConsoleInputW(
HANDLE hConsolelnput,
PINPUT_RECORD IpBuffer,
DWORD nLength,
LPDWORD IpNumberOfEventsRead);
#ifdef UNICODE
#define ReadConsoIelnput ReadConsolelnputW
#else
#define ReadConsoIelnput ReadConsolelnputA
#endif// IUNICODE
Здесь:
hConsolelnput - хэндл входного потока консоли;
IpBuffer - указатель на структуру типа INPUT_RECORD, в которую будут записаны данные о событии (или массив структур, если считываются данные более чем об одном событии);
nEength - число считываемых записей о событии;
IpNumberOfEventsRead - указатель на двойное слово, в которое записывается число реально считанных данных.
До нормальной работы осталось немного - узнать, какая информация записывается в структуру типа LNPUT_RECORD и как мы можем ее использовать. Давайте остановимся на каждом типе событий отдельно.
События с клавиатурой
События клавиатуры генерируются каждый раз при нажатии клавиши. При этом поле EventType структуры типа _INPUT_RECORD содержит значение KEY_EVENT, а в объединение Event записывается поле KeyEvent типа KEY_EVENT_RECORD. Этот тип определен в wincon.h: typedef struct _KEY_EVENT_RECORD { BOOL bKeyDown; WORD wRepeatCount; WORD wVirtuaiKeyCode; WORD wVirtualScanCode; union {
WCHAR UnicodeChar; CHAR AsciiChar; } uChar;
DWORD dwControlKeyState; } KEY_EVENT_RECORD, *PKEY_EVENT_RECORD;
260
Для того чтобы нормально обрабатывать события с клавиатурой, нам необходимо подробно разобрать назначение полей этой структуры. Программисты, знакомые с обработкой клавиатурных прерываний в DOS, увидят здесь множество знакомых характеристик. К сожалению, рамки этой книги не позволяют мне описать основы работы с клавиатурой. Если читатель чувствует, что у него в этой области есть пробел, рекомендую изучить этот вопрос по другим изданиям.
Если событие с клавиатурой состояло в нажатии клавиши, то поле bKeyDown принимает значение TRUE. Значение FALSE говорит о том, что произошло отжатие клавиши.
Если клавиша нажата и код клавиши начал генерироваться повторно, поле wRepeatCount является счетчиком повторов, что, кстати, следует и из его названия.
Виртуальный код нажатой клавиши записывается в поле wVirtualKeyCode, а виртуальный скан-код - в поле wVirtualScanCode.
Объединение uChar содержит ASCII или Unicode код нажатой клавиши в зависимости от того, какая версия функции ReadConsolelnputQ, ASCII или Unicode, используется.
И наконец, поле dwControlKeyState указывает на состояние управляющих клавиш. Их возможные значения приведены в табл. 59.
Т а б л и ц а 59. Флаги состояния управляющих клавиш
Флаг |
Значение |
Эффект |
RIGHT ALT PRESSED |
0x0001 |
Нажат правый Alt |
LEFT ALT PRESSED |
0x0002 |
Нажат левый Alt |
RIGHT CTRL PRESSED |
0x0004 |
Нажат правый Ctrl |
LEFT CTRL PRESSED |
0x0008 |
Нажат левый Ctrl |
SHIFT PRESSED |
0x0010 |
Нажат Shift |
NUMLOCK ON |
0x0020 |
NumLock горит |
SCROLLLOCK ON |
0x0040 |
ScrollLock горит |
CAPSLOCK ON |
0x0080 |
CapsLock горит |
ENHANCED KEY |
0x0100 |
Клавиша с двойным скан-кодом |
Демонстрационная программа
Я думаю, что даже конспективного изложения достаточно для того, чтобы можно было начать работу с клавиатурой в консольной сессии. Разве не так, уважаемый читатель? Тем не менее, я привожу демонстра-
261
ционную программу для того, чтобы показать, как можно обрабатывать клавиатурные события.
#include <windows.li>
#include <stdio.h>
main() {
HANDLE hStdlnputHandie, hStdOutputHandle; char cMyMessage[80] - "Do something with mouse to exit"; COORDCoord= {0,24}; DWORD dwResult; BOOL bMyFlag = TRUE; _INPUT^RECORD InputRecord; char cMyString[16]; char* cMyKeys[9] •= {" RAlt", " LAlt"," RCtrl", " LCtrl", " Shift", " NumLock",
" ScrollLock", " CapsLock"," EnhKey"); DWORD dwMyFlag;
FreeConsole();
AllocConsoleO;
SetConsoleTitle("Keyboard in console session demo program");
hStdlnputHandie = GetStdHandle(STDJNPUTJlANDLE);
hStdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
SctConsoleCursorPosition(hStdOutputHand!e, Coord);
SetConsoleTextAttribute(hStdOutputHandIe, FOREGROUND_RF.D |
FOREGROUND_GREEN | FOREGROUND_BLUE); WriteConsole(hStdOutputHandle, cMyMcssage, strlen(cMyMessage), &dwResult,
NULL);
while(bMyFlag) t
ReadConsoleInput(hStdInputHandlc, &InputRecord, 1, &dwResult); if(dwResult>= 1) f
ir(InputRecord.EvcntType == KEY EVENT)
t »
SetConsoleCursorPosition(hStdOutputHandlc. Coord); SctConso!eTextAttribute(hStdOutputHandle,0); WritcConsole(hStdOutputHandle, cMyMcssage, strlen(cMyMessage),
&dwResull, NULL); for( int i = 0, i < 80; i Ч
cMyMessagcfi] ~ 0; C'oord.X - 0; Coord. Y= 1;
SetConsoleCursorPositioii(hStdOutput!iandle. Co»/rd); if(lnputRecord. Event. Key Event, b Key Down)
strcat(cMyMessagc, "Pressed "); else
262
strcat(cMyMcssage, "Released "); strcat(strcat(cMyMessage,
itoa(InputRccord.Event KcyEvent.wVirtualKeyCode,
cMyString, 16)),""); strcat(cMyMessage,
itoa( InputRecord.Event.KeyEvent.wVirtualScanCode,
cMyString, 16));
if(InputRccord.Event.KeyEvent.dwControlKeyState != 0) ior(int i - 0; i <= 8; i+-r) {
dwMyFlag = 1;
if(InputRecord.Event.KeyEvent.dwControlKeyState & (dwMyFlag « i)) strcat(cMyMessage, cMyKeys[i]);
(
SetConsoleTcxtAttribute(hStdOutpulHandle, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE): WriteConsolc(hStdOutpu (Handle, cMyMessage. strlen(cMyMessage),
&dwResult, NULL); }
else
ifTInputRecord.EvcntType == MOUSE_EVENT) bMyFlag = FALSE;
return 0;
;Jnc. '12. (/KHo-KOiicoiib, отогбражающес лоложслие курсора мыши и сосюяимс клавиатур!-;
263
На рис. 22 показан вид окна, созданного этой программой.
Эта программа запрашивает для себя отдельную консоль, после чего в нижней части экрана выдает сообщение о том, для выхода необходимо сделать что-либо с мышкой. Но суть ее не в этом. При нажатии любой клавиши в верхней части экрана появляется строка, в которой указывается, какой тип действия (нажатие или отжатие клавиши) был произведен с клавиатурой, а также перечисляются некоторые характеристики нажатой клавиши, как-то ее виртуальный и скан-коды и состояние управляющих клавиш. Я думаю, что каких-либо трудностей при разборе программы не встретится.
События с мышью
События с мышью происходят в тех случаях, когда мышь двигается (при этом курсор должен находиться поверх окна консоли), либо на ней нажимается одна или более кнопок. При возникновении события с мышью поле EventType структуры типа INPUT RECORD содержит значение MOUSE_EVENT, В объединении Event этой структуры в этом случае будет содержаться поле MouseEvent типа MOUSE_EVENT RECORD. Для того чтобы понять, какую информацию мы можем извлечь из события с мышью, рассмотрим описание типа MOUSE_EVENT_RECORD. Его мы извлечем из заголовочного файла wincon.ii:
typedef smict _MOUSn_EVENT_RECORD {
COORD dwMousePosition;
DWORD dwButtonState;
DWORD dwControlKeyState;
DWORD dwEventFlags; ! MOUSEJEVENT_RECORD, *PMOUSE_EVENT_RECORD;
В этой структуре некоторые поля нам уже знакомы. Первое поле dwMousePosition типа COORD - координаты курсора мыши во время наступления события. Если обычно координаты курсора указываются в пикселах, то в данном случае они указываются в символах, причем начало отсчета - левый верхний угол рабочей области окна консоли. Не забудьте, экран-то текстовый!
Поле dwButtonState описывает состояние кнопок мыши на момент возникновения события. Кодируется это поле достаточно замысловато. Считается, что максимум у мыши может быть четыре кнопки (лично я таких мышей не видел и не слышал о них. Может, Microsoft боится повторить ситуацию с 640 кбайтами в DOS?). При этом младший бит определяет состояние самой левой клавиши ( 1 - клавиша нажата), сле-
264
дующий по старшинству бит определяет состояние самой правой клавиши. Очередной бит связан со второй слева кнопкой, следующий - с третьей слева и, наконец, последний - с четвертой слева кнопкой. Для каждого из этих битов в файле wincon.h определены макросы, которые приведены в табл. 60.
Т а б л и ц а 60. Флаги, определяющие нажатую клавишу мыши
Макрос |
Значение |
FROM LEFT 1ST BUTTON PRESSED RIGHTMOST BUTTON PRESSED FROM LEFT 2ND BUTTON PRESSED FROM LEFT 3RD BUTTON PRESSED from "left Чтн "button "pressed |
0x0001 0x0002 0x0004 0x0008 0x0010 |
Флаг |
Значение |
Эффект |
MOUSE MOVED DOUBLE J7LICK |
0x0001 0x0002 |
Перемещение мыши Второй щелчок кнопки (при двойном щелчке) |
С полем dwControlKeyState мы познакомились при изучении работы с клавиатурой. Никаких изменений это поле по сравнению с аналогичным в структуре KEY_EVENT_RECORD не претерпело.
Значение последнего поля, dwEventFlags, определяет действие, которое привело к возникновению события. Если его значение равно нулю, то это означает, что была нажата или отпущена одна из кнопок мыши. Еще два возможных значения этого поля приведены в табл. 61.
Не напоминает ли это все нотификационные события при разработке оконных программ?
Демонстрационная программа
Думаю, что все дальнейшие объяснения излишни. Вспомним о принципе «Seeing is believing». Давайте разберем небольшую демонстрационную программу. При написании этой программы я, чтобы не утомлять читателя, сделал одно допущение: у мыши всего две кнопки. Надеюсь, это допущение не повлияет на восприятие программы читателем:
^include <windows,h>
265
mainQ
HANDLE hSldlnputHandle. liSldOtitputHandle;
COORD Coord = {0,24};
char cMyMessage[80] = "Press any key lo exit";
DWORD dwResult;
BOOL bMyFlag = TRUE;
_INPUTJ?ECORD InputRecord;
char cMyString[l6];
char* cMyButtons[4] - {" LcftButton", " RightButton".
" Mouse moved". " Double Click"}; DWORD dwMyFlag;
FrceConsoleQ;
AllocConsole();
SctConsoleTitlc("Mouse in console session demo program");
hStdlnputHandle = GctStdHandle(STD_INPUT HANDLE);
hStdOutputHandle - GetStdHandle(STD _OUTPUT_HANDLE);
SetConsoleCursorPosition(hStdOutputHandle. Coord);
SetConsoleTextAttribute(liStdOutputHandle, FOREGROUND_RED |
FOREGROUND_GREEN | FOREGROUND^BLUE);
WriteConsole(hStdOutputHand!e, cMyMessagc. strlen(cMyMessagc). &dwRcsult,NULL);
while(bMyFlag)
( i
ReadConsolclnput(hStd!nputHandle, &lnpulRecord, 1, &dwRcsult);
irflnputRccord.EventType -= MOUSE EVENT)
SelConsoleCursorPosition(hStdOtitputHandlc, Coord); SelConsolcTcxtAttribute(liS(dOulputHandle,0); WritcConsole(hSldOutput Handle. cMyMcssage, slrlen(cMyMessage),
&d\vResult, NULL); for( int i = 0; i < 80; i — )
cMyMessagcfi] = 0; Coord. X - 0; Coord. Y - I ;
SclConsolcC'ursorl'ositioiKhStdOiitpul Handle, Coord); slrcaUcMyMcssage. "P(!situ;n - "): strcatfcMy Message.
itoa( input Record. I'.vciit.MouseEvent.dwMuuscPosition.X,
cM>'String. 10)): sircat(cMyMessai:e. ". "); strcaKcMyMessagc.
itoat InputRecord. Event. Mouse Event. dwMouscPosition.Y, cMySlrin». 10));
266
strcat(cMyMessage, " "); l'or(int i = 0; i <= I; i-r+) {
dwMyFlag = 1; if(InputRecord.Event.MouseEvcnt.dwButtonStatc & (dwMyFlag « i))
strcaI(cMyMcssage, cMyButtons[il);
if(InputRecord.Event.MouseEvent.dwEvcntFlags & (dwMyFlag « i)) strcat(cMyMessage, cMyButtons[i+2]);
i t
SetConsoleTextAttributc(hStdOutputHamIle, FOREGROUND_RED |
FOREGROUND^GREEN FOREGROUND_BLUE); WriteConsolc(hStdOulpulHandle, cMyMessagc, strlen(cMyMcssagc),
&dwRcsull,NULL); }
else
if(lnputRecord.EvenlType =-=- KEY_EVENT) bMyFlag = FALSE;
return 0;
И, как всегда, вид окна, созданного программой (рис. 23).
1НШН1 т on»li ibcbob diB» рмшю
Рис 2~<. Омю-конс'оль, см поражающее по южсние и состояние мыши
267
Как и в предыдущей программе, здесь нет ничего особенного. При запуске программа сообщает пользователю, что для выхода необходимо нажать любую клавишу. При возникновении же любою события с мышью, в первой строке экрана появляется информация о событии. Все данные берутся из структуры типа _INPUT RECORD. Попробуйте подвигать мышь внутри окна и понажимать кнопки мыши, понаблюдайте за результатами.
ЗАКЛЮЧЕНИЕ
На этом книга завершена. Не знаю, для кого она оказалась более трудной - для читателя или для меня. Не знаю, какой получилась - хорошей или плохой, принесла она пользу или читатель пожалел о потерянном времени. Писал я ее с душой. Конечно, можно было бы рассказать о Windows намного больше, но, как мне кажется, цель достигнута -читатель получил первоначальные знания и знает, где искать дополнительную информацию.
Пусть читатель не судит меня строго. Я сделал все, что мог.
Теперь я должен расстаться со своим читателем и мне немного грустно. Я не знаю, что я должен сказать: «До свидания» или «Прощайте». Надеюсь, что до свидания, мой читатель.
268
ПРИЛОЖЕНИЕ
Список макросов, используемых для создания программ, способных работать как в кодировке ANSI, так и в кодировке Unicode
Макрос in tchar.h |
Функция |
Макрос из tchar.h |
Функция |
tWinMain |
WinMain |
puttchar |
pu(char |
targv |
_argv |
tputenv |
pu(env |
_tenviron |
_environ |
pulls |
puts |
tfullpath |
fullpath |
tremove |
remove |
tmakepath |
makepath |
(rename |
rename |
_tpopen |
popen |
trmdir |
rmdir |
tsplitpath |
splitpath |
tscanf |
scanf |
tstrdate |
_strdate |
tsetlocale |
setlocale |
tstrtimc |
strtime |
tsopen |
sopen |
_taccess |
access |
tspawnl |
spawnl |
tasctime |
asctime |
tspawnle |
spawnle |
Jtoi |
atoi |
tspawnlp |
spawnlp |
Jtol |
atol |
tspawnlpe |
spawnlpe |
tchdir |
chdir |
tspawnv |
spawnv |
tchmod |
chmod |
tspawnve |
spawnve |
_tcreat |
creat |
(spawn vp |
spawnvp |
tctime |
ctime |
(spawnvpe |
spawnvpe |
_texecl |
execl |
stprintf |
sprin(f |
texecle |
execle |
s(scanf |
sscanf |
texeclp |
execlp |
_tsla( |
stat |
_texeclpe |
execlpe |
_tcsca( |
strcat |
texecv |
execv |
tcschr |
strchr |
texecve |
execve |
icscmp |
strcmp |
_texecvp |
execvp |
_tcscoll |
strcoll |
texecvpe |
execvpe |
tcscpy |
strcpy |
_tfdopen |
fdopen |
(cscspn |
strcspn |
_fgettc |
fgets |
Jcsdec |
s(rdec |
fgettchar |
fgetchar |
tcsdup |
strdup |
_fgettc |
fgetc |
tcsftime |
s(rf(ime |
tfmdfirst |
findfirst |
_tcsicmp |
stricmp |
tfmdnext |
findnext |
tcsinc |
strinc |
_tfopen |
fopen |
_tcslen |
strlen |
ftprintf |
fprintf |
tcslwr |
strlwr |
_fputtc |
fputc |
_tcsncmp |
s(rncmp |
fputtchat |
fputchar |
_(csnbcnt |
stmcnt |
fputts |
fputs |
tcsncoll |
strncoll |
tfreopen |
freopen |
tcsncpy |
stnicpy |
_ftscanf |
fscanf |
(csnextc |
strnextc |
tfsopcn |
fsopen |
tcsnicmp |
strnicmp |
_gcttc |
getc |
tcsninc |
strninc |
269
СОДЕРЖАНИИ
|
|
|
|
II IC lcm... |
....................... 3 |
Макрос и ilchar.h |
Функция |
Макрос in tdiar.h |
|
|
|
gctlchar Igelcwd |
gctchar gctcwd |
_tcspbrk tcsrchi |
strrchr |
(;.••' riM.STARTh;) • ДЛВЛ1ГП-: If Ч'ПГ.-М' ..НИ U< WOKUr !; ; -.- -.• '4 v -. <>] |
.. . ^ |
igelcnv |
getcnv |
_tcsrcv |
strrcv |
|
|
„getts • ( | istaltiunt Jstalpha istascii • , -.,.,.,.1 istcnirl istdigit Jsigraph _istlowcr , t • ( _istpnni |
isalnum isalpha isascii iscntrl isdigit :,..„„] isgrapn islower isprint |
_tcsset Jcsspn _tcsspnp "tcstod tcstok Jcstol Jcstoul tcsxtrm (system |
slrset strspn strspnp strslr strtod strtok strtol strtoul stnipr strxfrm system |
'.'.-•,•-•. ••• h- .'!>! 110 iviL-i.-лч i'm,.: •. л-..-'i .-.I i: :-; ..... '1 n;,i- i ,-• i .1* I'pr-KMHCMi-ik. H ^.V'.n':.! • ... •i,.!': :;l ' • ' ' . v:i > IIPOI p.IMMI.. ' .'. "• '• . • ' !..-[ или 'ч • • i'. • ' .•-; Vy 1,1, jo'. . ' N'l ! .... . . . 'hi- 1,.- ... : |
••> |
istupper istxdigit ilot |
isupper isxdigit itoa |
, ti _tot lower |
tempnam tolower |
•;.•• .'--I '••' ' .ii.--,f!!iM ii л>.;;ч!'' ••. • .•:•. и с .ч 'л<н i!;r.i .... |
.... Г, |
Hot |
Itoa |
Jottipper |
toupper |
|
. . . .... ..... ^ |
tmain |
|
_ultot |
ultoa |
|
. . .... ..... . -17 |
tmkdir |
mkdir |
_ungettc |
|
|
|
|
mlctemp |
|
unlmK |
Кодекс; -и: inoi.rina и WM PAINT .. . . . ... .................... |
.............. ..... ... 5Х |
Jopen |
open |
Jutime |
|
''ПСОН'М IIi;' Г; !Ui')J[4ecKII\ JlpIIN'II 1 МНОВ |
..... ......... .... ..58 |
tporror tcrintf |
pcrror printf |
vftprintf vtprintf |
viprmtr vprinlf |
IflAliMOJlLfiCTIiill- ПРОГРАММЫ С :U):i!,4i)!!\rr:irM . ....... |
70 |
|
putc |
. • .1-vstpnntl |
vsprintf |
|
.... ... .... .. .. ... 70 |
|
|
|
|
|
......... . . .......... , 74 |
|
|
|
|
|
......................... 98 |
|
|
|
|
ОБ1ЦП1- ОЛГМЕНТЫ УПРАВЛЕНИЯ ... ...... ........... ... ,. ................. |
......... .... ...... 135 ......................... 136 |
|
|
|
|
1>абога со скипом . . . ... ... . |
.... . ......... .... 141 |
|
|
|
|
Работа с тпскоаром |
........................ 14Х |
|
|
|
|
|
........... ...... 156 |
|
|
|
|
|
.............. ...... 161 |
|
|
|
|
|
... ........... 170 |
|
|
|
|
P'iOTI"! С Т1КЛЧ ЧКЧМН |
............ .... ... , IX! |
|
|
|
|
Работа с окном ппосмо; па депсш.св ............................ . ........ |
.................... 192 |
РПЕГТР ...... .............. ....... .... .......................................................... 202
Структура реестра.. .........
Работа '.' pl-ос гром . .. . ...
.................................. 203
................... ............... 204
271
КОЕ - ЧТО О МНОГОЗАДАЧНОСТИ В WINDOWS
Запуск процесса .........
Завершение процесса. Создание потока .........
Завершение потока ....
Синхронизация ...........
217
219
226
232
233
234
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ ................................................. 239
Способы присоединения DLL к программе .......
Вывернем программы наизнанку .........................
Инициализация и деинициализация DLL ............
240
245
247
КОНСОЛИ ........................................................................................................................... 250
Что такое консоль .............................................
Техника разработки консольной программы
Заключение .. Приложение .
250
251
268
269