Сейчас нам надо рассмотреть один интересный прием - порождение подкласса окон. Часто бывает так, что возможностей, предоставляемых окном того или иного стандартного класса Вам не хватает, а создавать эквивалентный стандартному класс с небольшими отличиями слишком сложно. В этом случае было бы удобно научиться создавать дополнительные классы окон, основанные на уже известных классах.
Именно это и называется порождением подкласса окон. Основная идея заключается в использовании собственной функции обработки сообщений, которая выполняла бы требуемую обработку, отличную от стандартной. При этом в качестве процедуры обработки сообщений по умолчанию должна выступать процедура, определенная в уже существующем классе.
Для реализации этого метода нам надо сделать три вещи:
узнать адрес процедуры обработки сообщений заданного окна (или заданного класса).
научиться вызывать нужную процедуру вместо процедуры обработки сообщений по умолчанию.
сделать так, что бы сообщения обрабатывала написанная нами процедура, а не определенная в классе.
Первую и третью задачи удобно решать с помощью функции
LONG SetWindowLong( hWnd, GWL_WNDPROC, lpfnNewProc );
эта функция одновременно устанавливает новый адрес процедуры обработки сообщений и возвращает адрес прежней функции. Конечно, когда мы передаем адрес новой процедуры обработки сообщений он должен быть адресом связанной с нашим приложением функции, то есть он должен быть возвращен процедурой MakeProcInstance.
Теперь нам надо только организовать обращение к старой процедуре обработки сообщений вместо процедуры по умолчанию (DefWindowProc). Сделать это непосредственно мы не можем, так как при вызове оконной процедуры мы должны связать ее с приложением, зарегистрировавшем этот класс. Вместо этого нам надо воспользоваться функцией:
LONG CallWindowProc( lpfnProc, hWnd, wMsg, wPar, lPar );
Итак, приведем небольшой пример:
static HANDLE hInstance;
static FARPROC lpfnNewProc;
static FARPROC lpfnOldProc;
LONG WINAPI ChildProc( HWND, UINT, UINT, LONG );
// функция обработки сообщений главного окна
LONG WINAPI _export WinProc(
HWND hWnd, UINT wMsg, UINT wPar, LONG lPar
) {
static HWND hChild;
switch ( wMsg ) {
case WM_CREATE:
lpfnNewProc= MakeProcInstance( (FARPROC)ChildProc, hInstance );
hChild= CreateWindow(
“BUTTON”, “Btn A”,
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE,
10,10, 50,50,
hWnd, 0, hInstance, NULL
);
// заменяем процедуру обработки сообщений дочернего окна
lpfnOldProc= (FARPROC)SetWindowLong(
hChild,GWL_WNDPROC,(LONG)lpfnNewProc
);
break;
case WM_DESTROY:
DestroyWindow( hChild );
FreeProcInstance( lpfnNewProc );
break;
...
}
return DefWindowProc( hWnd, wMsg, wPar, lPar );
}
LONG WINAPI _export ChildProc(
HWND hWnd, UINT wMsg, UINT wPar, LONG lPar
) {
// специфичная обработка сообщений
// и вызов прежней функции, а не функции DefWindowProc
return CallWindowProc( lpfnOldProc, hWnd, wMsg, wPar, lPar );
}
Конечно, рассмотренный нами вариант не единственный. Так, например, мы можем заменять функцию обработки сообщений не окна, а класса. Тогда все вновь создаваемые окна этого класса будут применять нашу процедуру. Для этого мы должны использовать функцию
LONG SetClassLong( hWnd, GCW_WNDPROC, lpfnNewProc );
Что неудобно, так это то, что мы должны сначала создать окно, а только затем заменять процедуру обработки сообщений. Мы можем поступить и иначе - сначала узнать адрес процедуры обработки сообщений, используя функцию
GetClassInfo( hInstance, lpszClassName, lpWndClass );
которая заполняет структуру WNDCLASS информацией о данном классе, а затем создать свой класс, который будет применять вместо процедуры обработки сообщений по умолчанию процедуру этого класса.
Связывание данных с окном
При работе с окнами очень часто возникает необходимость хранения данных, связанных с окном. Часто это приходится делать при работе с дочерними окнами, особенно при порождении подклассов окон, когда в таких связанных данных удобно сохранять адреса процедур обработки сообщений и пр. При этом не должно возникать вопросов с разделением данных между двумя окнами одного класса.
Для такого связывания в Windows предусмотрено два разных метода. Первый метод основан на выделении дополнительного пространства для данных пользователя в структуре, описывающей окно или класс окон. Этот способ достаточно эффективен, но ограниченно применим, так как выделенное пространство статично и не может изменить свой размер.
Для использования данных окна (или класса) мы должны, при регистрации класса окон указать размеры дополнительного пространства, выделяемого в струткуре окна (поле .cbWndExtra структуры WNDCLASS) и в структуре класса (поле .cbClsExra). При выделении пространства оно автоматически обнуляется. Подробнее об этом смотри лекцию 2.
Для доступа к элементам описаний класса и окна можно применять функции:
UINT GetWindowWord( hWnd, nOffset );
LONG GetWindowLong( hWnd, nOffset );
UINT SetWindowWord( hWnd, nOffset, wNewValue );
LONG SetWindowLong( hWnd, nOffset, dwNewValue );
UINT GetClassWord( hWnd, nOffset );
LONG GetClassLong( hWnd, nOffset );
UINT SetClassWord( hWnd, nOffset, wNewValue );
LONG SetClassLong( hWnd, nOffset, dwNewValue );
Параметр nOffset задает смещение требуемого слова (двойного слова) относительно начала дополнительных данных в байтах. Эти же функции могут применяться для доступа к некоторым полям самих структур, а не дополнительных данных, но при этом они имеют отрицательные значения.
Второй метод основан на применении специального списка свойств (property) окна. Этот список может динамически изменяться, но работа с ним медленее, чем с данными окна. Кроме того он размещается в локальной памяти модуля USER, поэтому ограничен размерами свободной памяти модуля USER.
Так как списки свойств размещаются не в нашем приложении, то перед уничтожением окна, мы должны удалить все внесенные нами свойства. Свойства состоят из имени (строка символов) и слова данных. Часто это слово рассматривается как хендл блока данных. Мы можем записывать читать, удалять и перебирать свойства, связанные с окном. Для этого предназначены следующие функции:
BOOL SetProp( hWnd, lpszName, hData );
HANDLE GetProp( hWnd, lpszName );
HANDLE RemoveProp( hWnd, lpszName );
int EnumProp( hWnd, lpfnEnumProc );
Хотя в документации данные, связанные с конктретным свойством, рассматриваются всегда как хендлы, но это не обязательно так. Конечно применение хендлов может несколько сократить размеры списка свойств за счет того, что одному элементу списка может соответствововать больше данных.
Так как Windows не знает, хендл какого блока данных (глобального или локального), объекта GDI или просто данные связан с конкретным элементом списка свойств, то при удалении записи эти данные не удаляются, а передаются Вам для их удаления.
Ресурсы приложения
На предыдущих лекциях мы довольно часто ссылались на такие ресурсы приложения, как курсоры и иконки, ранее мы познакомились более подробно с еще одной разновидностью ресурсов - битмапом. На примере битмапа мы рассмотрели основные правила применения ресурсов.
Сейчас несколько обобщим эти правила и рассмотрим несколько видов ресурсов более подробно. Для описания ресурсов мы должны сформировать файл описания ресурсов, имеющий расширение .RC. В этом файле перечисляются ресурсы, которые могут быть использованы приложением.
Описание ресурсов имеет следующий вид:
ResNameId TypeNameId [load-opt] [mem-opt] ResSource
Каждый ресурс должен иметь собственное уникальное имя или номер ResNameId и имя или номер типа ресурса TypeNameId.
Эти имена задаются либо текстом (имя), либо числом (номер), либо символическим именем (номер).
Примеры:
MYBITMAP BITMAP my_bmp.bmp
100 ICON my_ico.ico
MYDATA 500 my_data.dat
#define IconID 101
IconID ICON second.ico
(компилятор ресурсов может использовать директивы препроцессора C и включать заголовочные файлы .H, для задания символических имен).
Далее, при работе с ресурсами они будут загружаться из файла приложения в память. При этом для ресурса обычно выделяется блок глобальной памяти мы можем задать некоторые характеристики ресурса как блока памяти mem-opt и определять некоторые правила его загрузки load-opt.
load-opt, описывая правила загрузки, может быть:
PRELOAD ресурс должен загружаться в памяти при запуске приложения
LOADONCALL ресурс загружается только по требованию (используется по умолчанию)
mem-opt задает характеристики выделяемого блока и может быть:
FIXED ресурс должен размещаться в фиксированном блоке памяти
MOVEABLE ресурс размещается в перемещаемом блоке памяти (используется по умолчанию)
DISCARDABLE перемещаемый ресурс может быть удален из памяти (практически любой перемещаемый ресурс может быть удален, так как его содержимое не меняется)
Наконец, нам требуются данные этого ресурса ResSource. Некоторые виды ресурсов должны быть размещены в отдельном файле, в этом случае в качестве ResSource используется имя файла, другие виды ресурсов требуют непосредственного описания непосредственно в файле описания ресурсов, в этом случае ResSource может быть:
BEGIN
данные ресурса
END
или
{
данные ресурса
}
Иногда допускается либо использования файлов, либо непосредственное описание ресурсов. Некоторый “разнобой” может быть связан с применением компиляторов (и редакторов) ресурсов разных фирм - так как многие из них используют расширенные возможности.
Так, например, редактор ресурсов Borland WorkShop может описывать практически все ресуры непосредственно в файле описания ресурсов, включая их в виде дампа ресурса, а стандартные компилятор ресурсов Microsoft RC не допускает этого, например, для курсоров или битмапов. Компилятор ресурсов Symantec позволяет применять кавычки при задании имен ресурса или типа, что позволяет составлять имена из нескольких слов, что невозможно для Borland и Microsoft и т.д.
Windows предусматривает несколько стандартных типов ресурсов, а Вы можете легко описать ресурсы собственного типа, указав собственный тип (или номер, больший 255 - номера типов от 0 до 255 зарезервированы Windows). При задании данных собственного ресурса Вы можете указать имя файла, содержащего этот ресурс - тогда этот файл включается в ресурс как он есть, либо описав в виде текста непосредственно в файле описания ресурсов:
MyResource MyType
BEGIN
“This is a 0-terminated string\0”, 1, 2, 3, 4, 5,
100, 0x1000
END
В качестве типа ресурса можно указать стандартный тип RCDATA, который соответствует включаемым в файл описания ресурсов данным пользователя в этом-же формате. Если Вы хотите получить доступ к Вашим ресурсам, то надо воспользоваться парой функций:
HRSRC FindResource( hInstance, lpszName, lpszType );
HGLOBAL LoadResource( hInstance, hrSrc );
Первая функция позволяет получить промежуточный хендл описания ресурса, а вторая - загрузить ресурс память и получить хендл глобального блока памяти, содержащего ресурс. Если Вы декларировали ресурс как LOADONCALL, то физическое размещение ресурса в памяти произойдет не при вызове функции LoadResource, а при непосредственном обращении к ресурсу.
Если для задания имен ресурса или типа вы использовали текст, то параметры lpszName и lpszType являются указателями на соответствующие строки; если же используются номера, то вы можете их указывать двумя способами - передав строку, начинающуюся на #, например, “#123”, либо разместив в младшем слове адреса нужный номер, а старшем 0. Последний механизм реализуется с помощью макроса:
LPSTR MAKEINTRESOURCE( nId );
Этот способ считается самым эффективным, тем более, что Вы можете применять символические имена для задания номеров. При необходимости чтения данных ресурса Вы должны его зафиксировать в памяти с помощью процедуры
LPVOID LockResource( hGlobResource );
и после доступа к данным разрешить его перемещение:
BOOL UnlockResource( hGlobResource );
(Это не отдельная процедура, а обычный GlobalUnlock). После использования ресурса его можно удалить с помощью процедуры:
BOOL FreeResource( hGlobResource );
Когда Вы применяете ресурсы какого-либо типа, предусмотренного Windows, то приходится применять несколько другие способы доступа к данным, связанные с необходимостью специальной обработки таких ресурсов. Можно выделить следующие основные типы ресурсов:
ACCELERATORS – таблица акселераторов клавиатуры; для загрузки применяется функция
HACCEL LoadAccelerators( hInstance, lpszAccName );
BITMAP – битмап, включенный в приложение для загрузки применяется функция
HBITMAP LoadBitmap( hInstance, lpszBitmapName );
CURSOR – ресурс, представляющий курсор мыши для загрузки применяется функция
HCURSOR LoadCursor( hInstance, lpszCursorName );
DIALOG – диалог с ресурсами типа DIALOG и с самими диалогами мы разберемся позже.
FONT – включение шрифтового ресурса о применении шрифтов мы говорили ранее. Включать ресурс этого типа в Ваше приложение следует специфическим способом, существенно отличающимся от остальных ресурсов.
ICON – иконка для загрузки применяется функция
HICON LoadIcon( hInstance, lpszIconName );
MENU – меню, которое может быть назначено к окну для загрузки применяется функция
HMENU LoadMenu( hInstance, lpszMenuName );
STRINGTABLE – таблица строк ресурс этого типа вообще не загружается целиком. Он представляет собой таблицу строк, имеющих идентификаторы, и обеспечивает доступ к конкретной строке по ее идентификатору. Реально ресурсу этого типа соответствует не блок данных в памяти, а, может быть, несколько - для каждых 16 строк (по порядку номеров) выделяется отдельный блок. Приложение может содержать только один ресурс этого типа, поэтому при описании таблицы строк в файле описания ресурсов ее имя не указывается - указывается только тип этого ресурса. Для доступа к строкам применяется функция
int LoadString( hInstance, idString, lpszBuff, nmaxCount );
Мы достаточно близко познакомились с ресурсами типа BITMAP и FONT; практически можно считать что мы знакомы и с ресурсами типа CURSOR и ICON, так как они описываются так же, как и BITMAP. Сейчас нам надо лучше разобраться с тремя новыми типами – ACCELERARTORS, MENU и DIALOG.
Акселераторы
Акселераторы представляют собой простейшее средство для связывания определенных комбинаций клавиш с конкретными действиями. Можно считать, что акселераторы представляют собой таблицу, в которой записываются нажимаемые клавиши и генерируемые ими сообщения.
Точнее, акселераторы генерируют только сообщение WM_COMMAND, указывая более подробную информацию в параметрах этого сообщения. Параметр wPar сообщения содержит идентификатор, назначенный клавише, а параметр lPar всегда равен 0x00010000L.
Для создания таблицы акселераторов Вы должны поместить в файле описания ресурсов соответствующий ресурс:
AccName ACCELERATORS [load-opt] [mem-opt]
BEGIN
key, id [, type] [, options]
...
END
параметры могут быть следующими:
key определяет назначаемую клавишу
id посылаемый код извещения
type тип клавиши ASCII, VIRTKEY или опущен
options указывает состояние специальных клавиш и некоторые действия: NOINVERT, ALT, SHIFT, CONTROL или опущен.
подробнее рассмотрим назначение акселераторов на примере:
“A”, 100 // послать извещение 100 при нажатии А
65, 100, ASCII // то же самое, ASCII код 65 соответствует А
“^A”, 101 // послать 101 при нажатии Ctrl-A
“A”, 101, CONTROL // то же самое
VK_SPACE, 102, VIRTKEY // послать 102 при нажатии Space
VK_SPACE, 103, VIRTKEY, SHIFT // послать 103 при нажатии Shift-Space
Несколько слов следует сказать о применении акселераторов в приложении. Сам факт загрузки акселератора в память еще не обозначает его применения. Для того, что бы сообщения от клавиатуры начали обрабатываться акселератором надо в главном цикле обработки сообщений включить специальлные средства для их трансляции.
int TranslateAccelerator( hWnd, hAccel, lpMSG );
Параметр hWnd указывает окно, которое будет получать извещения, hAccel задает хендл таблицы акселераторов, lpMSG - адрес структуры MSG, содержащей сообщение.
При обычном применении акселератора извлеченное из очереди сообщение передается в эту функцию. Если это сообщение клавиатуры и данное нажатие на клавишу транслируется акселератором, то указанное окно получает сообщение WM_COMMAND и процедура возвращает TRUE; во всех остальных случаях возвращается FALSE, говоря о том, что сообщение не было трансировано в другое. Считается, что если сообщение было обработано акселератором, то дальнейшая его обработка не требуется - то есть обычный процесс трансляции и диспетчеризации этого сообщения исключается. При этом главный цикл обработки сообщений приобретает следующий вид:
MSG msg;
HACCEL hAccel;
HWND hWnd;
...
hAccel= LoadAccelerators( hInstance, “AccName” );
...
while ( GetMessage( &msg, NULL, NULL, NULL ) ) {
if ( !hAccel || !TranslateAccelerator( hWnd, hAccel, &msg ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}
...
Загруженные акселераторы автоматически удаляются при завершении приложения.
Меню
Еще одна разновидность ресурсов, которую мы должны сейчас рассмотреть – меню. Меню, предоставляемое Windows имеет иерархическую организацию. Основное меню всегда представлено строкой в верхней части окна, из него могут “выпадать” вертикальные меню, связанные с конкретным пунктом, и так далее - причем все уровни меню, кроме верхнего, представлены вертикальными меню.
Меню может быть описано как с помощью ресурсов, так и программно, причем существующее меню всегда может быть изменено. При работе с меню наше приложение будет получать следующие сообщения:
WM_ENTERMENUIDLE когда меню было активизировано и находится в состоянии ожидания.
WM_MENUSELECT посылается окну, использующему меню, как извещение о выборе пункта меню.
WM_MENUCHAR информирует окно о том, что при работе с меню была нажата кнопка не соответствующая никакому пункту меню. Обрабатывая это сообщение Вы можете вернуть номер пункта меню который надо выбрать, указать Windows о том, что работа с меню закончена или потребовать короткий звуковой сигнал.
WM_COMMAND посылается окну, извещая его о выборе требуемого пункта меню. Параметр wPar содержит идентификатор пункта меню, а lPar равен 0L. Обычно при работе с меню обрабатывается только сообщение WM_COMMAND, остальные применяются в специальных случаях.
Сейчас мы можем подвести некоторый итог под применением сообщения WM_COMMAND, которое может быть получено от дочернего окна, акселератора или меню:
wPar |
LOWORD(lPar) |
HIWORD(lPar) |
|
окно |
Id |
hWndChild |
wCode |
акселератор |
Id |
0 |
1 |
меню |
Id |
0 |
0 |
При работе с системным меню вместо сообщения WM_COMMAND мы будем получать сообщения WM_SYSCOMMAND. При описании меню в ресурсе текст описания меню должен размещаться в файле описания ресурсов в следующей форме:
MenuName MENU [load-opt] [mem-opt]
BEGIN
определения пунктов меню 0-го уровня:
MENUITEM для определения пункта
POPUP для определения пункта, связанного с меню следующего уровня
BEGIN
определение пунктов меню 1-го уровня
...
END
END
Для определения пункта меню, связанного с меню следующего уровня мы должны использовать следующую форму записи:
POPUP text [, options]
BEGIN
...
END
Для задания обычного пункта меню, то есть посылающего WM_COMMAND при его выборе, мы должны использовать следующую форму записи:
MENUITEM text, id [, options]
В этих случаях приняты следующие обозначения:
text задает текст пункта меню в виде строки взятой в двойные кавычки, один из символов строки может предваряться символом &, который приводит к подчеркиванию этого символа и его автоматической интерпретации как акселератора.
id определяет идентификатор пункта меню
options указывает на некоторые возможные характеристики данного пункта меню:
CHECKED пункт меню отмечен галочкой. (невозможно для меню 0-го уровня)
INACTIVE пункт меню неактивен (его нельзя выбрать), но рисуется обычным способом
GRAYED пункт меню неактивен и нарисован серым цветом
MENUBREAK пункт меню размещен либо в новой строке (для уровня 0), либо в новом столбце.
MENUBARBREAK то же, что и MENUBREAK, но столбец (строка) отделяется сплошной чертой
HELP обозначает пункт меню, связанный с подсказкой.
Для использования меню совместно с окном мы можем воспользоваться любым удобным способом:
при регистрации класса окна мы можем указать имя требуемого ресурса. Тогда все окна этого класса при создании получат указанное меню.
мы можем указать хендл меню при создании окна, тогда это окно будет создано с указанным меню.
в любой момент мы можем вызвать функцию
BOOL SetMenu( hWnd, hMenu );
с помощью которой мы можем установить новое меню, заменить одно на другое или удалить имеющееся (указав hMenu=NULL).
Во время работы мы можем легко получить хендл меню, используемого данным окном:
HMENU GetMenu( hWnd );
и, зная хендл меню, столь же легко можем узнать хендл меню следующего уровня:
HMENU GetSubMenu( hMenu, nPos );
здесь параметр ‘nPos’ указывает номер пункта меню, связанного с меню следующего уровня.
Вы можете создать новое пустое меню, которое можно использовать как в качестве меню верхнего уровня, так и в качестве меню других уровней. Это определяется только его применением. Если Вы его добавите к другому меню, то оно будет POPUP меню, а если Вы его назначите окну, то оно будет меню верхнего уровня. Для создания нового меню предназначена функция
HMENU CreateMenu( void );
Кроме того мы можем узнать хендл системного меню данного окна:
HMENU GetSystemMenu( hWnd, FALSE );
с помощью которого мы можем добавить новые пункты к системному меню, изменить или удалить прежние. При коррекции меню можно использовать два разных способа задания пункта меню:
по его идентификатору (пункт, связанный с меню следующего уровня не имеет идентификатора, поэтому не может быть указан этим способом).
При выборе этого способа Вы должны указать флаг MF_BYCOMMAND при указании пункта меню.
по его номеру в меню. В этом случае Вы должны указать флаг MF_BYPOSITION при задании пункта.
При необходимости корректировать меню мы можем воспользоваться следующими функциями:
BOOL AppendMenu( hMenu, nFlags, idNew, lpszNewName );
BOOL InsertMenu( hMenu, idItem, nFlags, idNew, lpszNewName );
BOOL ModifyMenu( hMenu, idItem, nFlags, idNew, lpszNewName );
BOOL DeleteMenu( hMenu, idItem, nFlags );
Эти четыре функции позволяют добавлять в конец меню, вставлять, изменять или удалять пункты меню. С их же помощью можно манипулировать с меню следующего уровня - добавлять, изменять или удалять.
BOOL CheckMenuItem( hMenu, idItem, nFlags );
BOOL EnableMenuItem( hMenu, idItem, nFlags );
BOOL HiliteMenuItem( hWnd, hMenu, idItem, nFlags );
С помощью этих функций можно отметить отдельный пункт меню галочкой, запретить его или пометить выделенным. Меню может не перерисовывается после изменений, поэтому Вы должны, окончив все изменения, вызвать процедуру
void DrawMenuBar( hWnd );
которая перерисует меню. Конечно, если изменения делались в невидимом пункте меню, то его можно не перерисовывать.
Диалоги
Последний, и самый сложный вид ресурсов, рассматриваемый нами - диалог. Диалог представляет собой отдельное окно, называемое панелью диалога, с размещенными на его поверхностями управляющими элементами - кнопками, списками, статическими элементами и пр. Каждый управляющий элемент диалога является дочерним окном.
При работе с диалогами надо придерживаться основных правил их описания. Все управляющие элементы составлены в список, по порядку их описания. При работе с диалогом перемещение по списку элементов осуществляется с помощью кнопок стрелка вверх и стрелка влево для перехода к ранее описанным, стрелка вниз и стрелка вправо - к позже описанным.
Обычно в диалоге выделяют несколько групп управляющих элементов и в каждой группе описывают хотя бы один элемент со стилем WS_TABSTOP (стиль дочернего окна). С помощью клавиш Tab и Shift-Tab осуществляется перемещение от одного элемента с этим стилем к другому. То есть с помощью Tab можно осуществить быстрый переход от одной группе к другой.
Для переключения состояния элементов используется клавиша Space (не Enter!). Обычно в диалоге выделяют одну из кнопок, которая описывается как DEFPUSHBUTTON и именно эта кнопка “нажимается” клавишей Enter.
Помимо этого, если диалог использует AUTORADIOBUTTON, надо выделять отдельные группы кнопок, из которых только одна может быть отмечена. Это делается с помощью стиля WS_GROUP. Управляющий элемент со стилем WS_GROUP начинает новую группу элементов, которая заканчивается на следующем элементе с этим стилем.
Для описания диалогов используется специальный ресурс - DIALOG, описывающий набор управляющих элементов, их стилей, размеров, положение на панели диалога и т.д. При описании диалога применяется совершенно специфичная система координат, которая больше нигде не используется - она основана не на физических единицах величина, а на долях величиные символа системного шрифта. Считается, что средний символ системного шрифта содержит 4 единицы диалога по оси X и 8 единиц по оси Y. Если Вам надо самим определять реальные значений координат, то Вы можете воспользоваться функциями
DWORD GetDialogBaseUnits( void );
void MapDialogRect( hWndDlg, lpRect );
Первая функция возвращает двойное слово, младшее слово которого содержит размер символа системного шрифта по оси X, а старшее - по оси Y. Разделив эти числа на 4 и 8 Вы можете узнать цену единиц диалога.
Вторая функция преобразует координаты точек прямоугольника из системы координат диалога в систему координат экрана.
Функции для создания диалогов
Для создания диалога существует 8 функций:
int DialogBox( hInstance, lpszDlgTemplate, hWndOwner, lpfnDlgProc );
int DialogBoxParam(
hInstance, lpszDlgTemplate, hWndOwner, lpfnDlgProc, lParamInit
);
int DialogBoxIndirect( hInstance, hglbDlgTemplate, hWndOwner, lpfnDlgProc );
int DialogBoxIndirectParam(
hInstance, hglbDlgTemplate, hWndOwner, lpfnDlgProc, lParamInit
);
HWND CreateDialog( hInstance, lpszDlgTemplate, hWndOwner, lpfnDlgProc );
HWND CreateDialogParam(
hInstance, lpszDlgTemplate, hWndOwner, lpfnDlgProc, lParamInit
);
HWND CreateDialogIndirect( hInstance, hglbDlgTemplate, hWndOwner, lpfnDlgProc );
HWND CreateDialogIndirectParam(
hInstance, hglbDlgTemplate, hWndOwner, lpfnDlgProc, lParamInit
);
Для создания диалога необходимо передать соответствующей функции структуру данных, описывающую этот диалог (то есть указывающую стили, размеры, положение и идентификаторы управляющих элементов и самого диалога).
Половина из перечисленных функций, содержащих слово ...Indirect... в названии, использует хендл глобального блока памяти hglbDlgTemplate в котором должна размещаться такая структура. Вы должны сами позаботиться о создании и заполнении этого блока данными.
Другая половина функций, не содержащих слова ...Indirect... в названии, использует имя (номер) ресурса, описывающего эту структуру данных. При этом приложение должно содержать ресурс типа DIALOG, для которого компилатор ресурсов создает нужный блок данных. Вы можете сами загружать ресурс в блок глобальной памяти, что-либо корректировать в нем, если это необходимо, и использовать функцию ...Indirect... для создания диалога.
Кроме информации о самом диалоге Вы должны указать хендл копии приложения, с которой будет связано окно диалога, и которое содержит требуемые ресурсы; хендл окна - пользователя диалога (о различии Owner и Parent мы уже говорили). Помимо этого Вы должны указать адрес процедуры, обрабатывающей сообщения диалога lpfnDlgProc (об этой функции чуть позже). Это должен быть адрес функции, связанной с копией приложения с помощью функции MakeProcInstance.
Когда окно диалога создается, оно дополнительно получит сообщение WM_INITDAILOG, которое используется для инициализации управляющих элементов. Вы можете передать вместе с этим сообщением параметр lParam, содержащий нужные Вам данные. Для этого предназначены функции, содержащие слово ...Param в названии.
Модальные и немодальные диалоги
Диалоги разделяются на два общих класса - модальные (modal) и немодальные (modeless) диалоги. Модальные диалоги требуют обязательного завершения для продолжения работы всего приложения. Пример – диалог для выбора файла в редакторе. До тех пор, пока файл не выбран продолжение работы редактора бессмыслено.
Немодальные диалоги работают паралелльно с остальным приложением. Пример – диалог поиск/замена в большинстве редакторов. Вы можете перейти в окно редактора и поработать там, не завершая работу с диалогом. При этом Вы можете оперировать как с диалогом, так и с остальными окнами Вашего приложения.
Разница между модальными и немодальными диалогами на программном уровне заключается в правилах обработки сообщений, поступающих к этим диалогам.
Модальный диалог, работающий монопольно, должен исключить передачу сообщений к остальным окнам приложения. Для этого организуется новый цикл обработки сообщений, обрабатывающий все сообщения, нужные диалогу, и исключающие обработку сообщений, направленных другим окнам приложения (исключаются, в основном, сообщения от клавиатуры и мыши).
Для создания модальных диалогов предназначены функции DialogBox..., которые создают требуемый диалог и организуют цикл обработки сообщений для этого диалога.
При этом надо быть достаточно аккуратным - Ваш главный цикл обработки сообщений будет бездействовать, и какая-либо дополнительная обработка сообщений в нем будет игнорирована. Так, например, акселераторы, транслируемые в главном цикле обработки сообщений, не окажут никакого эффекта в модальном диалоге.
Немодальный диалог, работающий параллельно с остальным приложением, получает сообщения через главный цикл обработки сообщений. То есть окно диалога, вместе со всеми управляющими элементами, выступает в качестве самого обычного окна приложения.
Для создания немодального диалога Вам надо создать окно диалога с помощью функции CreateDialog..., и предусмотреть специальную обработку сообщений для диалога в главном цикле обработки сообщений. Модификация главного цикла обработки сообщений производится следующим образом:
MSG msg;
HWND hWndModeless= NULL;
...
while ( GetMessage( &msg, NULL, NULL, NULL ) ) {
if ( !hWndModeless || !IsDialogMessage( hWndModeless, &msg ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}
Порядок применения функций IsDialogMessage и TranslateAccelerator определяется желаемым эффектом: если Вам надо, что бы акселераторы использовались диалогом, то трансляцию акселератора надо производить до вызова функции IsDialogMessage. Обычно это не требуется и трансляция производится позже.
В некоторых случаях Вы можете с помощью CreateDialog иммитировать модальный диалог, организовав после создания окна дилога дополнительный цикл обработки сообщений. При этом Вы можете предусмотреть специальную трансляцию некоторых сообщений.
Вы можете применять окна диалога в качестве главных окон приложения.
Базовые классы окон
Для того, что бы упростить работу с диалогами Windows содержит специальную процедуру, определяющую новый базовый класс окон – класс диалогов.
Основное отличие базового класса от обычного класса заключается в том, что для базового класса не существует никаких структур данных, описывающих этот класс и не существует окон, принадлежащих этому классу. Базовый класс полностью определяется процедурой обработки сообщений, которой должны пользоваться все окна, построенные на этом базовом классе.
До сих пор мы сталкивались только с одним базовым классом - обычное перекрывающееся окно. Процедура обработки сообщений этого базового класса - DefWindowProc.
Для диалогов существует специальная функция DefDlgProc, определяющая базовый класс диалогов. При желании Вы можете создавать собственные классы диалогов, основанные на этом базовом классе так-же, как и обычные окна, применяя функцию RegisterClass. При этом Вы должны в структуре данных окна зарезервировать дополнительное пространство, размером .cbWndExtra= DLGWINDOWEXTRA (30 байт). В этом случае Вы сможете создавать окна немодального диалога с помощью функции CreateWindow, либо указав имя зарегистрированного класса в шаблоне диалога.
Функция DefDlgProc выполняет обработку нескольких дополнительных сообщений, которые не обрабатываются (или редко используются) обычным окном:
WM_INITDIALOG инициализация диалога (это сообщение не посылается оконной функции диалога, оно передается только в процедуру диалога)
WM_GETDLGCODE посылается управляющему элементу для выяснения ожидаемых управляющих сообщений
WM_NEXTDLGCTL установка фокуса на требуемый управляющий элемент, сообщение можно только посылать.
WM_PARENTNOTIFY извещение о создании/удалении/”щелчке” мышкой
WM_ENTERIDLE модальный диалог или меню ожидает ввода данных. посылается диалогом или меню главному окну приложения.
DM_GETDEFID узнать идентификатор DEFPUSHBUTTON
DM_SETDEFID выбрать новую DEFPUSHBUTTON
Внимание! сообщения DM_GETDEFID и DM_SETDEFID имеют номера WM_USER и WM_USER+1, поэтому не используйте собственных сообщений WM_USER и WM_USER+1 для посылки окну диалога!
Функция диалога
Для диалогов принят необычный способ, предусматривающий нестандартную обработку сообщений. Если для обычных окон мы обрабатываем сообщения сами, обращаясь к функции, выполняющей обработку по умолчанию, только при необходимости, то для диалогов предусмотрена специальная функция диалога, которая вызывается стандартной процедурой DefDlgProc.
Эта функция диалога, не являясь обычной оконной процедурой, возвращает результат не в виде двойного слова, а в виде логической величины:
BOOL CALLBACK _export DlgProc( hWnd, wMsg, wPar, lPar ) {
return FALSE;
}
Функция DlgProc возвращает FALSE, если сообщение надо обрабатывать стандартным образом и TRUE, если сообщение обработано. Единственное исключение - сообщение WM_INITDIALOG, где значение TRUE указывает на необходимость установить фокус на требуемый управляющий элемент, а FALSE говорит о том, что Вы уже установили фокус.
Обычно Вы пишете только DlgProc. Однако эта функция возвращает логическую величину, используемую процедурой DefDlgProc. В некоторых случаях требуется возвращать конкретный конечный результат обработки сообщения (например сообщение WM_QUERYOPEN).
Вы можете сделать это с помощью функций GetWindowLong и SetWindowLong, указывая смещение DWL_MSGRESULT для чтения/изменения возвращаемого по умолчанию значения.
Кроме того Вы можете изменить при желании адрес функции диалога на новый, используя смещение DWL_DLGPROC для чтения/записи адреса процедуры обработки сообщений.
Эти данные размещены в пространстве, добавляемом к структуре, описывающей окно, при его создании (DLGWINDOWEXTRA). Соответственно DWL_MSGRESULT и DWL_DLGPROC имеют положительные значения.
При разработки необходимых функций диалога надо учитывать некоторую разницу между модальным и немодальным диалогами. Все отличия можно свести к нескольким пунктам:
модальный диалог завершается с помощью процедуры
void EndDialog( hWnd, wPar );
которая возвращает указанный результат и прерывает цикл обработки сообщений, организованный процедурой DialogBox.
немодальный диалог заканчивается при уничтожении окна диалога с помощью обычной функции DestroyWindow. При этом Вы должны принять меры, что бы в главном цикле обработки сообщений больше не вызывалась процедура IsDialogMessage для этого диалога.
так как функция создания немодального диалога возвращает хендл окна диалога, то мы можем обойтись без функции диалога, поступая обычным способом - написав оконную процедуру, использующую в качестве функции обработки сообщений по умолчанию процедуру DefDlgProc. При этом мы указываем NULL в качестве адреса функции диалога и, после создания окна, заменяем адрес процедуры обработки сообщений на собственный (по–сути применяя прием порождения подкласса окон).
При этом обработка сообщений диалога может быть изображена следующей схемой:
Если мы указали адрес функции диалога NULL, то DlgProc, изображенная на этой схеме, вызываться не будет. Рассмотрим небольшой пример:
FARPROC lpfnOwnProc;
// новая оконная процедура
LONG CALLBACK _export OwnDlgProc(
HWND hWnd, UINT wMsg, UINT wPar, LONG lPar
) {
switch ( wMsg ) // нестандартная обработка сообщений
case WM_CTLCOLOR:
return ...;
default:
break;
}
return DefDlgProc( hWnd, wMsg, wPar, lPar );
}
// в какой–либо иной процедуре:
// создание немодального диалога
HWND hModeless;
lpfnOwnProc= MakeProcInstance( (FARPROC)OwnDlgProc, hInstance );
hModeless= CreateDialog( hInstance, “my_res”, hWndOwner, NULL );
SetWindowLong( hModeless, GWL_WNDPROC, (LONG)lpfnOwnProc );
// Внимание! Так как подстановка процедуры осуществляется после
// создания окна, то первые сообщения, включая WM_INITDIALOG
// уже обработаны стандартной функцией
...
// после закрытия окна диалога
FreeProcInstance( lpfnOwnDlgProc );
Вообще нам может понадобиться порождать подкласс и от модального диалога. В этом случае подмену процедуры обработки сообщений лучше производить в функции диалога при обработке сообщения WM_INITDIALOG:
FARPROC lpfnOwnProc;
LONG PASCAL FAR _export OwnDlgProc(
HWND hWnd, UINT wMsg, UINT wPar, LONG lPar
) {
// см. выше
}
FARPROC lpfnDlgProc;
BOOL PASCAL FAR _export DlgProc(
HWND hWnd, UINT wMsg, UINT wPar, LONG lPar
) {
switch ( wMsg ) {
case WM_INITDIALOG:
/*
установить новую оконную процедуру и запретить вызов
данной функции диалога:
*/
SetWindowLong( hWnd, GWL_WNDPROC, (LONG)lpfnOwnProc );
SetWindowLong( hWnd, DWL_DLGPROC, (LONG)NULL );
/*
если мы устанавливаем новую функцию при обработке сообщения
WM_INITDIALOG, то наша новая функция его уже не получит. Поэтому нам
надо послать какое-либо специальное сообщение или вызвать отдельную
функцию для первоначальной инициализации диалога.
*/
return TRUE;
default:
break;
}
return FALSE;
}
// в какой–либо иной процедуре:
// вызов модального диалога
...
lpfnDlgProc= MakeProcInstance( (FARPROC)DlgProc, hInstance );
lpfnOwnProc= MakeProcInstance( (FARPROC)OwnDlgProc, hInstance );
int answer;
answer= DialogBox( hInstance, “my_res”, hWndOwner, lpfnDlgProc );
FreeProcInstance( lpfnDlgProc );
FreeProcInstance( lpfnOwnDlgProc );
Функции для управления диалогом
Windows содержит достаточно большое количество функций, применяемых при работе с диалогами, что бы их здесь не рассматривать подробно. Мы попробуем только лишь выделить некоторые из них и дать короткую характеристику.
int GetDlgCtrlID( hwndControl );
HWND GetDlgItem( hwndDlg, nCtrlId );
Эти функции позволяют определить идентификатор управояющего элемента диалога по его хендлу или хендл управляющего элемента по его идентификатору и хендлу диалога.
LONG SendDlgItemMessage( hwndDlg, nCtrlId, wMsg, wPar, lPar );
Эта функция используется для посылки сообщения конкретному управляющему элементу диалога.
Следующая группа функций может задавать текст управляющего элемента в виде числа или строки.
void SetDlgItemInt( hwndDlg, nCtrlId, nValue, bSigned );
UINT GetDlgItemInt( hwndDlg, nCtrlId, lpbOk, bSigned );
void SetDlgItemText( hwndDlg, nCtrlId, lpszString );
int GetDlgItemText( hwndDlg, nCtrlId, lpsBuffer, nMaxCount );
Еще несколько функций предназначены для работы с кнопками разных видов:
void CheckDlgButton( hwndDlg, nCtrlId, nCheck );
void CheckRadioButton( hwndDlg, nCtrlFirst, nCtrlLast, nCheck );
UINT IsDlgButtonChecked( hwndDlg, nCtrlId );
Для выбора файлов с помощью списков разного вида. Эти функции одновременно обслуживают список с именами файлов и статический текст, в котором представлен текущий путь:
int DlgDirList( hwndDlg, lpszPath, idListBox, idText, nFileType );
int DlgDirListComboBox( hwndDlg, lpszPath, idComboBox, idText, nFileType );
BOOL DlgDirSelect( hwndDlg, lpszPath, idListBox );
BOOL DlgDirSelectEx( hwndDlg, lpszPath, nMaxCount, idListBox );
BOOL DlgDirSelectComboBox( hwndDlg, lpszPath, idComboBox );
BOOL DlgDirSelectComboBoxEx( hwndDlg, lpszPath, nMaxCount, idComboBox );
При необходимости программной передачи управления могут пригодиться следующие функции:
HWND GetNextDlgGroupItem( hwndDlg, hwndCtrl, bPreviouse );
HWND GetNextDlgTabItem( hwndDlg, hwndCtrl, bPreviouse );
Напоследок несколько особенностей диалога.
Во первых, надо очень аккуратно применять элемент типа DEFPUSHBUTTON, так как он может “перехватывать” клавишу Enter у других элементов диалога, даже если эта клавиша необходима для их нормальной работы.
Так, если при работе в COMBOBOX вы нажмете Enter для выбора текущего элемента из списка, DEFPUSHBUTTON может перехватить это сообщение, соответственно возьмет на себя фокус ввода, а COMBOBOX отреагирует на это как на отмену выбора элемента.
Во вторых, могут возникнуть сложности с раскраской управляющих элементов диалога и самой панели далога. Если вы будете обрабатывать сообщение WM_CTLCOLOR функцией диалога, то для возвращения хендла кисти Вам надо устанавливать поле DWL_MSGRESULT структуры окна диалога.
Однако, диалог выполняет некоторую обработку этого сообщения после того, как Вы вернули ответ. При этом он руководствуется своими правилами для назначения кисти и может вовсе заменить назначенную Вами на желаемую им.
При необходимости управлять цветом элементов диалога эффективно может использоваться прием порождения подкласса от диалога – когда Вы можете обрабатывать WM_CTLCOLOR самостоятельно и не использовать стандартной обработки этого сообщения.