Глава 12.
В глубине меню
Чем хуже готовят повара,
тем вежливей должны быть официанты.
Михаил Генин
Что такое меню, как оно создается и как с ним работать? Начальные сведения уже были приведены в главе 4, здесь же мы постараемся осветить эти вопросы более подробно. Однако, прежде чем переходить к изучению соответствующих средств Windows и библиотеки MFC, необходимо рассмотреть стандартные типы меню, которые можно создавать в приложениях Windows, и их основные характеристики.
Меню обычно располагается под полосой заголовка в верхней части перекрывающегося или всплывающего окна и представляет собой словесные или символические опции, дающие возможность манипулировать особенностями программы. Такое меню называется Главным меню приложения (верхний уровень иерархии). Отдельные элементы меню располагаются в полосе меню (menu bar) — рис. 12.1 и 12.2.
Элементы меню могут иметь различные типы и содержать ряд модификаторов (рис. 12.3):
Рис. 12.1. Заголовок и Главное меню приложения WordPad
Рис. 12.2. Раскрытое меню пункта "Вид"
Рис. 12.3. Команда, имеющая подменю
Каждое окно, имеющее заголовок, может иметь системное меню, которое вызывается щелчком левой кнопки мыши на пиктограмме, расположенной в левой части заголовка окна (рис. 12.4), и представляет собой набор стандартных команд. В Windows 95 системное меню появляется, если щелкнуть правой кнопкой мыши в полосе заголовка или на пиктограмме свернутого окна.
Рис. 12.4. Системное меню
Система Windows предоставляет специальный тип меню, которое можно создавать в любом месте экрана. Такое меню называется контекстным или плавающим (floating popup menu) и не привязано к полосе меню (рис. 12.5)'. Оно ассоциируется с некоторой областью окна или объектом, например, пиктограммой. Контекстное меню в некоторых случаях удобнее других, т. к. его содержимое зависит от того, для какого объекта оно было создано. Обычно оно выводится на экран щелчком правой кнопки мыши.
Рис. 12.5. Контекстное меню рабочей области окна приложения WordPad
Теперь, перечислив основные типы используемых в Windows меню, рассмотрим те возможности, которые предоставляет библиотека MFC для работы с ними.
Точно так же, как в Win32 API есть функции, специально предназначенные для работы с меню, в библиотеке MFC есть для них адекватные альтернативы, реализованные в виде класса СМenu (рис. 12.6), полностью вобравшего в себя функции API.
Рис. 12.6. Место класса СМenu в иерархии библиотеки MFC
Класс СМenu инкапсулирует объект Windows, определяемый дескриптором HMENU, и предоставляет функции для работы с меню. С помощью этого класса приложение может создавать меню, даже не имея его шаблона, добавлять или удалять элементы или подменю, управлять их состоянием — активизировать или блокировать, отмечать команды и т. д.
Рассмотрим основные члены этого класса.
CMenu::m_hMenu
Определяет дескриптор HMENU меню Windows, присоединенного к объекту класса, для создания которого, как обычно, используется конструктор.
СМеnu::СМеnu ()
Конструктор меню. Следует совершенно четко представлять, что при создании объекта не происходит автоматического создания самого меню. Для того чтобы это реализовать, необходимо вызвать одну из функций, отвечающих за создание или загрузку меню.
Для создания "пустого" меню, т. е. меню, не содержащего ни одной строчки и ни одного подменю, достаточно воспользоваться функцией
BOOL CMenu::CreateMenu ()
Создает пустое меню Windows и присоединяет его к объекту класса. Добавлять в него элементы можно, используя функции AppendMenu и InsertMenu. При успешном создании меню функция возвращает TRUE и FALSE — в случае неудачи.
Для того чтобы добавить в существующее меню некоторое подменю, в классе реализована функция
BOOL CMenu::CreatePopupMenu ()
Создает пустое подменю. Добавить в него элементы можно с помощью тех же функций — AppendMenu и InsertMenu. При успешном завершении функция возвращает TRUE и FALSE — в случае неудачи.
Если создание меню производится на основе шаблона, то можно воспользоваться функциями
BOOL CMenu::LoadManu (LPCTSTR IpszResourceName)
и
BOOL CMenu::LoadMenu (UINT nIDResource)
Загружают ресурс меню из исполняемого файла приложения и присоединяют его к объекту. В качестве параметра используется либо указатель на текстовую строку IpszResourceName, содержащую имя ресурса загружаемого меню, либо его числовой идентификатор nID.
Если по каким-либо причинам понадобилось меню, которое не присоединяется ни к одному окну, то при завершении работы с ним следует воспользоваться функцией
BOOL CMenu::DestroyMenu ()
Удаляет меню перед завершением работы приложения и возвращает системе занимаемые им ресурсы.
Для изменения состава меню, созданного функцией CreateMenu, в классе предусмотрена специальная группа функций.
Добавление элементов в меню производится функциями AppendMenu и InsertMenu.
BOOL CMenu::AppendMenu (
UINT nFlags,
UINT nIDNewItem = 0, LPCTSTR IpszNewItem = NULL)
BOOL CMenu::AppendMenu (
UINT nFlags,
UINT nIDNewItem = 0,
const CBitmap *pBmp)
Функции добавляют новый элемент в конец меню. Состояние элемента задается параметром nFlags, который может содержать одно или несколько из следующих значений:
MF_CHECKED, MF_UNCHECKED
При изображении элемент меню отмечается (MF_CHECKED) или не отмечается (MF_UNCHECKED) галочкой
MF_DISABLED, MF_ENABLED
Элемент меню доступен (MF_ENABLED) или блокирован (MF_DISABLED), но изображается обычным (черным) цветом
MF_GRAYED
Элемент меню блокирован и изображается серым цветом
MF_MENUBREAK
Элемент меню верхнего уровня выводится с новой строки, а элемент подменю— в новом столбце без разделительной вертикальной линии
MF_MENUBARBREAK
Аналогично MF_MENUBREAK, но дополнительно новый столбец отделяется вертикальной линией
MF_OWNERDRAW
Меню само отвечает за изображение своего элемента
MF_POPUP
Элемент меню имеет ассоциированное с ним подменю, дескриптор которого задается в параметре nIDNewItem
MF_SEPARATOR
Элемент представляет собой горизонтальную разделительную линию; этот флаг не может комбинироваться с MF_POPUP и MF_DISABLED; при установленном данном флаге все другие игнорируются
MF_STRING
Элемент представляет собой текстовую строку
При использовании этих флагов следует иметь в виду, что нельзя комбинировать следующие флаги:
Параметр nIDNewltem определяет идентификатор команды нового элемента, а если nFlags установлен в MF_POPUP, то дескриптор (HMENU) подменю. Через этот параметр передается дополнительная информация о содержимом нового элемента, если nFlags принимает одно из следующих значений:
nFlags IpszNewltem
MF_OWNERDRAW
Содержит 32-битное значение, которое приложение может использовать в качестве дополнительных данных, ассоциированных с элементом меню, при обработке сообщений WM_MEASUREITEM и WM_DRAWITEM
MF_SEPARATOR
Значение игнорируется
MF_STRING
Содержит текстовую строку
Параметр рВтр определяет указатель на объект класса СВ'Лтар, который может использоваться в качестве содержимого элемента меню вместо текстовой строки. При этом параметр nFlags не может принимать значения MF_OWNERDRAW и MF_STRING.
BOOL CMenu.::InsertMenu (
UINT nPosition,
UINT nFlags,
UINT nIDNewltem =0,
LPCTSTR IpszNewltem = NULL)
и
BOOL CMenu::InsertMenu (
UINT nPosition,
UINT nFlags,
UINT nIDNewltem =0,
const CBitmap *pBmp)
Функции вставляют новый элемент в позицию, определяемую параметром nPosition, и сдвигают вниз существующие элементы. Значения всех параметров, кроме nFlags, такие же, как и для функции AppendMenu. Параметр nFlags дополнительно используется для интерпретации значения nPosition:
nFlags nPosition
MF_BYCOMMAND
Определяет идентификатор элемента меню, перед которым вставляется новый элемент
MF_BYPOSITION
Определяет порядковый номер элемента меню, перед которым вставляется новый элемент; если установить nPosition = —1, то элемент будет добавлен в конец меню
Если требуется не вставлять новый, а заменить существующий элемент меню, то можно воспользоваться функциями:
BOOL CMenu: :ModifyMenu (
UINT nPosition,
UINT nFlags,
UINT nIDNewItem,
LPCTSTR IpszNewItem)
и
BOOL CMenu: :ModifyMenu (
UINT nPosition,
UINT nFlags,
UINT nIDNewItem,
const CBitmap *pBmp)
Параметры этих функций идентичны параметрам функций AppendMenu и InsertMenu за исключением того, что новый элемент не вставляется в меню, а заменяет тот, на который указывает параметр nPosition.
Для удаления элемента из меню в классе предусмотрены две функции:
BOOL CMenu:rDeleteMenu (
UINT nPosition,
UINT nFlags)
и
BOOL CMenu::RemoveMsnu (
UINT nPosition,
UINT nFlags)
Функции удаляют элемент меню, задаваемый параметром nPosition, значение которого интерпретируется так же, как в функции InsertMenu. Отличие между функциями заключается в том, что при удалении подменю функцией DeleteMenu все связанные с ним ресурсы освобождаются, а в случае RemoveMenu— нет, и можно снова воспользоваться удаленным элементом (по его идентификатору).
Для изменения состояния элемента меню используется функция:
UINT CMenu::EnableMenuItem (
UINT nIDEnableltem,
UINT nEnable)
Позволяет установить элемент меню nlDEnableltem в одно из трех состояний (задается параметром nEnable): доступное (MF_ENABLED), запрещенное (MF_DISABLED) или заблокированное (MF_GRAYED). Эти значения должны комбинироваться с одним из флагов MF_BYCOMMAND или MF_BYPOSITION, которые определяют, каким образом задается элемент — идентификатором или порядковым номером. Функция возвращает предыдущее состояние элемента или —1, если элемент не существует.
У элемента можно проставить отметку с помощью следующих функций.
UINT CMenu::CheckMenuItem (
UINT nIDCheckltem,
UINT nCheck)
Устанавливает или сбрасывает галочку у элемента меню. Параметр nCheck может принимать значение MF_CHECKED или MFJJNCHECKED, как обычно, в комбинации с флагами MF_BYCOMMAND или MF_BYPOSITION, которые определяют интерпретацию параметра nIDChecked— идентификатор или порядковый номер. Функция возвращает предыдущее состояние элемента или —1, если элемент не существует.
BOOL CMenu::CheckMenuRadioItem (
UINT nIDFirst,
UINT nIDLast,
UINT nIDItem,
UINT nFlags)
Помечает определенный элемент меню в группе переключателей, одновременно сбрасывая метку у остальных элементов группы. Для отметки элемента вместо галочки используется кружок. Параметры nIDFirst и nIDLast определяют, соответственно, первый и последний элемент в группе переключателей. Сам отмечаемый элемент задается параметром nIDItem. Параметр nFlags определяет интерпретацию этих параметров и может принимать одно из значений: MF_BYCOMMAND или MF_BYPOSITION.
Вместо стандартных галочки или кружка, используемых для отметки элемента, в меню можно поместить произвольный битовый массив:
BOOL CMenu::SetManuItemBitmaps (
UINT nPosition,
UINT nFlags,
const CBitmap *pBmpUnchecked,
const CBitmap *pBmpChecked)
Функция ассоциирует с элементом меню битовые массивы, которые будут выводиться для помеченного (pBmpChecked) и непомеченного (pBmpUnchecked) состояний. Параметры nPosffion и nFlags определяют элемент меню по идентификатору или порядковому номеру. Если либо pBmpChecked, либо pBmpUnchecked равен NULL, то для соответствующего атрибута рядом с элементом ничего не отображается, а если оба параметра равны NULL, то используются стандартные галочка и кружок. При использовании функции следует иметь в виду, что при разрушении меню не происходит автоматического удаления битовых массивов — эта задача возлагается на приложение.
После внесения в меню всех изменений необходимо вызвать функцию
void CWnd::DrawMenuBar ()
Производит перерисовку полосы меню.
Для поддержки работы с контекстными меню в классе реализована функция
BOOL CMenu::TrackPopupManu (
UINT nFlags,
int x,
int y,
CWnd *pWnd,
LPCRECT IpRect = 0)
Выводит на экран контекстное меню и создает свой собственный цикл обработки сообщений. Функция не завершается до тех пор, пока работа с меню не будет закончена либо выбором элемента, либо отказом от выбора, после чего меню уничтожается. Параметр nFlags определяет расположение контекстного меню (относительно экрана) и кнопку мыши, с помощью которой должен выполняться выбор. Он может содержать одно из трех значений (для расположения на экране) в комбинации с одним из двух флагов для кнопки мыши.
Значения параметра nFlags для задания расположения меню на экране
TMP_CENTERALIGN
Центрирование относительно координаты, заданной параметром х
TMP_LEFTALIGN
Выравнивание по левой границе относительно координаты, заданной параметром х
TMP_RIGHTALIGN
Выравнивание по правой границе относительно координаты, заданной параметром х
Значения параметра nFlags для кнопки мыши
TMP_LEFTBUTTON
Использование левой кнопки мыши
TMP_RIGHTBUTTON
Использование правой кнопки мыши
Параметр pWnd задает окно, которое получит сообщение WM_COMMAND после того, как пользователь сделает выбор в контекстном меню, a IpRect является указателем на структуру типа ПЕСТ (или класса CRect), определяющую координаты прямоугольной области, в которой пользователь может выполнять выбор из меню. Если щелчок мышью будет сделан вне этой области, контекстное меню исчезнет с экрана, что эквивалентно отказу от выбора. Если для IpRect задать значение NULL, то размеры и расположение этой прямоугольной области будут совпадать с размерами контекстного меню.
В классе представлены две виртуальные функции, которые произвольным образом позволяют изменять внешний вид меню: Measureltem и Drawltem. Но они заслуживают отдельного разговора и мы к ним вернемся в разделе "Самоотображение элементов меню" данной главы.
Теперь, после теоретического знакомства с возможностями, заложенными в классе СМеnu, можно переходить к практическим аспектам и тонкостям программирования при построении и работе с меню.
В системе Windows реализованы три способа создания меню:
1. На основе шаблона, задаваемого в файле ресурса.
2. Динамическое создание при помощи специальных функций.
3. На основе шаблона в оперативной памяти. Рассмотрим каждый из этих способов более подробно.
Создание меню на основе шаблона
Этот подход мы уже рассматривали и поэтому не будем останавливаться на нем более подробно, а приведем только описание атрибутов, определяющих внешний вид и поведение строки меню (рис. 12.7, табл. 12.1).
Рис. 12.7. Атрибуты меню в окне свойств, открытом в режиме настройки параметров меню
Таблица 12.1. Описание атрибутов, определяющих внешний вид меню
Атрибут |
Описание |
|
Separator |
Элемент представляет собой горизонтальную разделительную линию, служащую для разбиения элементов меню на группы |
|
Checked |
При выводе на экран элемент меню отмечается слева галочкой |
|
Pop-up |
Элемент определяет подменю |
|
Grayed |
Элемент отображается серым цветом и находится в заблокированном состоянии; несовместим с атрибутом Inactive |
|
Help |
Элемент выравнивается по правому краю полосы меню |
|
Break |
Этот атрибут может принимать одно из трех значений: None — обычный элемент меню; Column — для меню верхнего уровня элемент выводится с новой строки, а для подменю — в новом столбце; Ваг— дополнительный столбец подменю отделяется вертикальной линией. |
|
ID |
Идентификатор элемента меню |
Атрибут |
Описание |
Caption |
Текст, определяющий имя элемента меню; текстовая строка может содержать символы: & — буква, перед которой стоит этот знак, будет подчеркнута; \t — включает в строку символ табуляции; \а — выравнивает текст по правой границе меню |
Prompt |
Текст, который будет отображаться в строке состояния и (после \п) в окне всплывающей подсказки |
Inactive |
Элемент находится в заблокированном состоянии, но отображается обычным цветом |
После того как создан шаблон, необходимо подключить меню к окну приложения. Этот вопрос также достаточно подробно рассматривался, поэтому только перечислим те места, где это можно сделать.
wc.lpszMenuName = "IDR_MAINFRAME";
if (!pMainFrame->LoadFrame(IDR_MAINFFLAME))
return FALSE;
pDocTemplate = new CMultiDocTemplate(
IDR_NOTETYPE, // идентификатор меню
RUNTIME_CLASS(CNoteDoc),
RUNTIME_CLASS(CNoteFrame),
RUNTIME_CLASS(CNoteView));
Примечание
Мы обязательно рассмотрим архитектуру "документ/представление" более подробно.
При использовании любого из перечисленных способов подключения меню к окну нет необходимости уничтожать меню перед завершением работы приложения, т. к. библиотека MFC делает это сама.
Меню, созданное в системе Windows, где взаимодействие между объектами осуществляется при помощи сообщений, также работает по этому принципу. Наиболее важным из сообщений, которое передает меню, является WM_COMMAND, означающее, что выбран тот или иной элемент меню. В табл. 12.2 приведены этапы работы с меню и возникающие при этом сообщения.
Таблица 12.2. Этапы работы с меню и возникающие при этом сообщения
Этап работы |
Сообщение |
Обработчик MFC |
|
Инициализация меню |
WM INITMENU |
OnlnitMenu |
|
Вывод всплывающего меню на экран |
WM INITMENUPOPUP |
OnlnitMenuPopup |
|
Инициализация и вывод на экран всплывающего меню |
WM INITMENU и WM INITMENUPOPUP |
OnlnitMenu и OnlnitMenuPopup |
|
Нахождение нужного элемента меню |
WM_MENUSELECT |
OnMenuSelect |
|
Выбор элемента меню |
WM_COMMAND |
OnCommand |
|
Выбор элемента системного меню |
WM_SYSCOMMAND |
OnSysCommand |
Рассмотрим кратко роль перечисленных сообщений.
Примечание
Действие приведенных обработчиков справедливы и для двух других способов создания меню.
Когда пользователь активизирует элемент системного меню, сначала вызывается обработчик сообщения WM_SYSCOMMAND:
void CWnd::OnSysCommand(
UINT nID, LPARAM IParam)
Первый параметр nID может содержать идентификатор строки системного меню, а также некоторые другие значения:
SC_CLOSE
Удаление окна
SC_HOTKEY
Активизация определенным приложением окна, связанного с командной клавишей
SC_HSCROLL
Прокрутка по горизонтали
SC_KEYMENU
Выбор элемента меню при помощи командной клавиши
SC_MAXIMIZE или SС_ICОМ
разворачивание окна
SC_MINIMIZE или SC_ICON
Сворачивание окна
SC_MOUSEMOVE
Выбор элемента меню при помощи мыши
SC_MOVE
Перемещение окна
SC_NEXTWINDOW
Переключение на следующее окно
SC_PREVWINDOW
Переключение на предыдущее окно
SC_RESTORE
Восстановление нормального положения и размеров окна
SC_SCREENSAVE
Запуск приложения-заставки (screen-saver application), определенного в разделе [boot] файла <system.ini>
SC_SIZE
Изменение размеров окна
SC_TASKLIST
Запуск или активизация приложения Task Manager
SC_VSCROLL
Прокрутка по вертикали
Примечание
Кроме перечисленных, можно использовать и свои идентификаторы, как будет показано в рассматриваемом ниже примере.
Второй параметр IParam используется только при следующих значениях первого параметра nID:
nID IParam
SC_HOTKEY
Содержит идентификатор активизируемого окна
SC_MOUSEMOVE
Младшее слово содержит х-координату, а старшее — у-координату
Перед отображением всплывающего меню (один раз, при активизации) система Windows посылает сообщение WM_INITMENU с тем, чтобы приложение могло изменить меню перед тем, как его увидит пользователь. При этом библиотека MFC вызывает обработчик:
void CWnd:lOnlnitMenu (CMenu *pMenu)
Параметр рМепи задает указатель на объект СМеnu, определяющий активизируемое меню. Обработка может заключаться в активизации или деактивизации строк меню, изменении их состояния и т. п.
Когда пользователь выбирает элемент, имеющий подменю, система Windows, прежде чем отобразить всплывающее меню, посылает владельцу сообщение WM_INITMENUPOPUP. Библиотека MFC вызывает обработчик, который дает приложению возможность изменить всплывающее меню перед его отображением:
void CWnd::OnlnitMenuPopup (
CMenu pPopupMenu,
DINT nlndex,
BOOL bSubMenu)
Параметр рРорuрМеnu— указатель на объект "всплывающее меню"; nID— индекс всплывающего меню в меню верхнего уровня; bSubMenu— определяет, является ли всплывающее меню системным (TRUE) или нет.
Каждый раз, когда пользователь перемещается от одного элемента меню к другому, Windows посылает сообщение WM_MENUSELECT, которое идентифицирует текущий элемент, на что библиотека MFC реагирует вызовом обработчика
void CWnd::OnMenuSelect (
UINT nltemlD,
UINT nFlags,
HMENU hSubMenu)
Параметр nltemlD— идентификатор выделенного элемента или индекс всплывающего меню. Параметр nFlags содержит комбинацию следующих флагов, определяющих атрибуты элемента меню:
MF_BITMAP
Элемент является битовым массивом
MF_CHECKED
Элемент отмечается галочкой
MF_DISABLED
Элемент является заблокированным, но отображается в нормальном виде
MF_GRAYED
Элемент отображается серым цветом и не может быть выбран
MF_MOUSESELECT
Элемент выделен мышью
MF_OWNERDRAW
Элемент рисуется окном, создавшим меню
MF_POPUP
С данным элементом связывается всплывающее меню
MF_SEPARATOR
Элемент является разделителем
MF_SYSMENU
Элемент принадлежит системному меню
Последний параметр hSubMenu используется в двух случаях, зависящих от состояния параметра nFlags:
nFlags hSubMenu
MF_SYSMENU
Идентифицирует меню, ассоциированное с сообщением
MF_POPUP
Идентифицирует дескриптор меню верхнего уровня
Многие приложения используют сообщение WM_MENUSELECT для отображения дополнительной информации о выбранном элементе меню в строке состояния.
Случай, когда пользователь выбирает элемент меню, a Windows посылает сообщение WM_COMMAND, уже не раз описывался в предыдущих примерах — необходимо либо воспользоваться одним из предопределенных обработчиков стандартных команд меню, либо создать свой, например, так, как было продемонстрировано в главе 11.
С учетом новых сведений покажем, как можно организовать "переключение" количества столбцов панели инструментов рисования в нашем приложении Graph:
1. Добавить в уже имеющуюся полосу меню элемент Tools, а в качестве его подпунктов — "2 column" и "3 column" (рис. 12.8).
Рис. 12.8. Добавляем новые элементы меню
2. Внести приведенный ниже код в карту сообщений класса CMainFmme:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{(AFX_MSG_MAP(CMainFrame)
//}}AFX_MSG_MAP
ON_COMMAND_RANGE(ID_TOOLS_2 COLUMN, ID_TOOLS_3 COLUMN,
OnToolsColumn)
ON_UPDATE_COMMAND_UI_RANGE(ID_TOOLS_2COLUMN, IDJTOOLS_3COLUMN,
OnUpdateToolsCo.lumn)
END_MESSAGE_MAP()
3. В описание класса CMainFrame добавить определение обработчиков:
//{{AFX_MSG(CMainFrame)
...
//}}AFX_MSG
afx_msg void OnToolsColuinnfUINT nID) ;
afx_msg void OnUpdateTooisColumn(CCmdUI* pCmdUI);
4. И, наконец, написать код самих обработчиков:
void CMainFrame::OnToolsColumn(UINT nID)
{
// Запоминаем выбранное число столбцов
m_nPaneCol = (nID = ID_TOOLS_2COLUMN) ? 2 : 3;
// Устанавливаем столбцы на панели инструментов рисования
SetColumns(mJnPaneCol);
}
void CMainFrame::OnUpdateToolsColumn(CCmdUI* pCmdUI)
{
// Определяем выбранный пункт меню
UINT nCol = (pCmdUI->m_nID == ID_TOOLS_2COLUMN) ? 2 : 3;
// Устанавливаем маркер состояния — галочку у выбранного
// пункта меню и сбрасываем у остальных
if(m_nPaneCol == nCol)
pCmdUI->SetCheck(TRUE);
else
pCmdUI->SetCheck(FALSE);
}
Как видите, все довольно просто и, на мой взгляд, нет надобности больше задерживаться на этом вопросе: ведь при использовании шаблонов подавляющую часть работы выполняет операционная система, а все действия, необходимые для работы с меню, представляют относительную сложность только при построении самого первого меню.
Поэтому мы переходим к практическим примерам. Как вы помните, мы еще не закончили наше приложение Graph. Здесь мы добавим в него возможность выбора цвета как для рисования линий, так и для заливки. Но сделаем это, как и договаривались, не общепринятым способом, а таким, который позволит проиллюстрировать максимум возможностей работы с меню.
Добавление элемента в системное меню
Итак, мы создаем контекстное меню, появляющееся при нажатии правой кнопки мыши в клиентской области окна и позволяющее выбрать цвет заливки (да и текущий цвет линий). Одновременно в системное меню добавим пункт, разрешающий работу с этим контекстным меню (рис. 12.9).
int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct)
{
// Получаем доступ к системному меню
CMenu* pSysMenu = GetSystemMenu(FALSE);
// Определяем текст, которым будет представлен
// созданный элемент системного меню
CString strColorMenu("Разрешить смену текущего цвета");
// Добавляем в него разделитель и новый элемент
// "Разблокировать меню Цвет"
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_BYCOMMAND I MF_STRING,
IDM_COLOR, StrColorMenu);
...
}
Рис. 12.9. Добавляем новый пункт в системное меню
Давайте коротко разберем наши действия. Прежде всего необходимо получить доступ к системному меню, что мы и делаем с помощью функции
CMenu* CWnd::GetSystemMenu(BOOL bRevert)
Позволяет получить идентификатор копии системного меню, так что с ним можно проводить любые допустимые манипуляции. При добавлении своего элемента в системное меню необходимо учитывать, что его идентификаторы имеют номера, начинающиеся с OxFOOO. Поэтому собственный идентификатор должен иметь номер, меньший этого значения. Кроме того, в качестве последней цифры шестнадцатеричного представления номера команды рекомендуется использовать 0. Параметр bRevert задает режим работы функции: если он равен FALSE, можно выполнять модификацию системного меню. Значение TRUE "возвращает" системное меню в состояние, приписанное ему по умолчанию.
Следующая строка кода
CString strColorMenu("Разрешить смену текущего цвета");
задает текст, который должен появиться в системном меню. Хотя его длина не оговаривается, все же не надо делать его слишком длинным. Но в любом случае система Windows сама "подгонит" размеры этого меню под максимальный текст.
Нам осталось только добавить те элементы меню, которые нам необходимы. Это, безусловно, разделитель и собственно текст нашего меню. Выполняется это очень просто с помощью функции CMenu::AppendMenu.
Здесь необходимы некоторые пояснения. Как вы помните, при создании меню с помощью редактора ресурсов мы обязательно должны были присвоить каждому его элементу идентификатор, который является ничем иным, как некоторым целым числом. Так вот, редактор ресурсов сам назначает числовой эквивалент введенному идентификатору. Совсем по-другому обстоит дело в данном случае. При динамическом добавлении элемента меню мы самостоятельно должны сопоставить идентификатор с некоторым цифровым эквивалентом. Сделать это можно, непосредственно редактируя файл <resource.h>, а можно с помощью IDE. Именно такой подход мы сейчас и рассмотрим:
1. Выберите пункт меню View | Resource symbols и на экране появится одноименный диалог (рис. 12.10), в котором вы найдете все имеющиеся идентификаторы.
Рис. 12.10. Здесь можно получить информацию по имеющимся идентификаторам
2. Нажмите кнопку New, чтобы создать новый идентификатор. На экране появится диалог New Symbol.
3. В поле Name введите обозначение идентификатора, а в поле Value — его цифровое значение (рис. 12.11).
4. Нажмите последовательно кнопки ОК и Close в соответствующих диалогах — новый идентификатор создан.
Рис. 12.11. Создаем новый идентификатор
Вот, собственно, и все. Теперь можно смело вставлять новый элемент в системное меню и создавать для него соответствующий обработчик.
Примечание
Некоторые из представленных в блоке диалога Resource Symbols идентификаторы не снабжены галочкой в столбце In Use. И в поле Used by для них выводится загадочная надпись "not used". Естественно, это не означает, что данный идентификатор не нужен. Просто ClassWizard о нем ничего не знает.
Обработка команд системного меню несколько отличается от стандартной, и об этом необходимо сказать несколько слов. Я уже отмечал, что когда пользователь выбирает элемент системного меню, вызывается обработчик сообщения WM_SYSCOMMAND. Следовательно, наша задача — реализовать этот обработчик. Очевидно, что делать это мы будем с помощью мастера ClassWizard, который создаст необходимые компоненты карты сообщений и заготовку обработчика:
class CMainFrame : public CFrameWnd
{
...
public:
BOOL m__bEnable; // индикатор включения контекстного меню
protected:
//{{AFX_MSG(CMainFrame)
afx_msg void OnSysCommand(UINT nID, LPARAM IParam);
//}}AFX_MSG
...
};
...
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
/ / {{AFX_MSG_MAP (CMainFrame)
ON_WM_SYSCOMMAND()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
void CMainFrame::OnSysCommand(UINT nID, LPARAM IParam)
{
// Прежде всего отсекаем команды несистемного меню
if((nID & OxFFFO) == IDM_COLOR)
{
// Получаем доступ к системному меню
CMenu *pSysMenu = GetSystemMenu(FALSE);
// Определяем текущее состояние элемента
UINT nState = pSysMenu->GetMenuState(IDM_COLOR, MF_BYCOMMAND);
// Если элемент отмечен, ...
if(nState == MF_CHECKED)
{
// сбрасываем маркер, ...
pSysMenu->CheckMenuItem(IDM_COLOR,
MF_BYCOMMAND | MF_UNCHECKED);
// разрешаем работу с контекстным меню mjDEnable = FALSE;
}
else //и наоборот
{
pSysMenu->CheckMenu!tem(IDM_COLOR, MF_BYCOMMAND | MF_CHECKED);
// запрещаем работу с контекстным меню m_bEnable = TRUE;
}
}
else
{
// Если команда "не наша", необходимо передать ее
// библиотеке для корректной обработки
CWnd::OnSysCommand(nID, IParam);
}
}
В приведенном фрагменте обратите внимание на строчку
BOOL m_bEnable; // индикатор включения контекстного меню
которая включена для того, чтобы как-то оправдать задачу создания новой команды системного меню. Значение TRUE этой переменной разрешит нам создать и работать с контекстным меню, a FALSE, соответственно, запретит.
Для решения поставленной задачи нам осталось выбрать команду, по которой будет создаваться контекстное меню, и реализовать ее обработчик. Обычно контекстные меню выводятся на экран при нажатии на правую кнопку мыши. Мы не будем отступать от этого правила.
Создайте обработчик сообщения WM_RBUTTONDOWN. He забудьте, что помимо самой функции необходимо определить компоненты карты сообщений.
void CChildWnd::OnRButtonDown(UINT nFlags, CPoint point)
{
// Создавать и отображать контекстное меню будем только
//в том случае, если "получили разрешение" от системного меню
if(((CMainFrame *)GetParentFrame{))->m_bEnable)
{
// Поскольку мы создаем меню, нам нужен соответствующий объект
CColorMenu menu;
// Создаем контекстное меню
menu.CreatePopupMenu();
for(int i = 0; i < 16; i++)
{
// Добавляем в него элементы
menu.AppendMenu(MF_OWNERDRAW, ID^CLRO + i, "");
}
// Для расположения контекстного меню относительно экрана
// необходимо преобразовать положение курсора мыши
// в экранные координаты
ClientToScreen(Spoint);
// Выводим контекстное меню на экран
menu.TrackPopupMenu(TPM__LEFTALIGN I TPM_RIGHTBUTTON,
point.x, point.у, this);
}
// Как всегда, не мешаем нормальной работе
CWnd::OnRButtonDown(nFlags, point);
}
Первое, что бросается в глаза — это появление нового класса CColorMenu, о котором мы ничего не говорили. Да и не могли ничего сказать, потому что только приступаем к его созданию. До сих пор мы пользовались объектами класса СМеnu, и этого было вполне достаточно. Зачем же нам понадобился еще один класс? Все очень просто — мы хотим взять на себя задачу отрисовки элементов меню. Этим мы займемся несколько позднее, а пока будем считать, что работаем с объектом класса СМеnu.
Итак, после того как объект соответствующего класса создан, можно создавать собственно контекстное меню:
menu.CreatePopupMenu();
Однако пока в нем нет ни одного элемента. Чтобы исправить такое положение, добавим их :
for(int i = 0; i < 16; i++)
menu.AppendMenu(MF_OWNERDRAW, ID_CLRQ + i, "");
Следующим шагом является преобразование положения курсора мыши в экранные координаты
ClientToScreen(Spoint);
чтобы место, где мы щелкнули правой кнопкой мыши, соответствовало левому верхнему углу нашего контекстного меню. Если мы этого не сделаем, все будет работать по-прежнему, но выглядеть не очень аккуратно.
Примечание
Эта точка может соответствовать и левому нижнему углу контекстного меню, но такое перестроение автоматически выполняет сама система Windows.
После того как мы все подготовили, осталось только вывести созданное меню на экран. Что и делает, как вы помните, функция
menu.TrackPopupMenu(TPM_LEFTALIGN I TPM_RIGHTBUTTON,
point.x, point.у, this);
Теперь выбор любого элемента нашего меню приведет к тому, что будет вызван соответствующий обработчик, который, правда, мы еще не создали. Проделайте это самостоятельно либо найдите его в полном тексте приложения Graph, который имеется на сопроводительной дискете. А мы будем двигаться дальше.
Самоотображение элементов меню
Для того чтобы создать меню, внешний вид которого отличается от стандартного (например, кроме текста мы хотим вывести там еще и пиктограмму), нам необходимо перехватить и обработать два сообщения: WM_MEASUREITEM и WM_DRAWITEM. Библиотека MFC уже подготовила все для того, чтобы этот процесс не занял много времени, и реализовала в классе СМеnu(и не только в нем) следующие две функции:
virtual void CMenu::MaasureItem (LPMEASUREITEMSTRUCT IpMeasureltemStruct)
Вызывается библиотекой MFC при создании меню, имеющего стиль самоотображения (owner-draw). В качестве параметра в функцию передается указатель на структуру MEASUREITEMSTRUCT, с помощью которой можно настроить размеры меню.
virtual void CMenu::DrawItem (LPDRAWITEMSTRUCT IpDrawItemStruct)
Вызывается библиотекой MFC при создании меню, имеющего стиль самоотображения. В качестве параметра в функцию передается указатель на структуру DRAWITEMSTRUCT, которая содержит информацию для рисования элемента меню.
При использовании стиля самоотображения обе функции должны быть обязательно переопределены, т. к. их реализация в классе СМеnu не содержит кода.
Таким образом, прежде всего нам необходимо создать свой класс, производный от СМеnu. Рассмотрим один из возможных вариантов:
1. Раскроем окно мастера ClassWizard и выполним команду New раскрывающейся кнопки Add Class.
2. В появившемся диалоге введем имя создаваемого класса в поле Name.
3. К сожалению, ClassWizard не позволяет создавать класс на основе СМеnu. Поэтому поступаем следующим образом: в раскрывающемся списке Base class выбираем какой-нибудь класс, например, CButton (рис. 12.12). Нажимаем кнопку ОК, затем еще раз ОК — новый класс создан. Теперь нам надо внести некоторые изменения.
Рис. 12.12. Воспользоваться ClassWizard можно даже в том случае, если в списке базовых классов нет того, который нужен
4. В окне Workspace на вкладке ClassView дважды щелкните на имени CColorMenu, что приведет к открытию файла, где этот класс определен. Замените имя CButton на СМеnu — сделать это надо в одном единственном месте.
5. Раскройте вкладку FileView в окне Workspace и дважды щелкните по имени файла <colormenu.cpp>. В строке
BEGIN_MESSAGE_MAP(CColorMenu, CButton)
замените CButton на СМеnu. До этой строки можно "добраться" и другим способом, я вам показываю только один из возможных вариантов.
6. Сохраните сделанные изменения. В результате мы получили собственный класс CColorMenu, производный от СМеnu. Кстати, в данном конкретном случае можете удалить из определения и реализации класса (обязательно в обоих местах) все компоненты, кроме конструктора, — они нам не понадобятся.
После того как класс, базирующийся на СМеnu, создан, нам необходимо переопределить две функции, которые отвечают за обработку сообщений WM_MEASUREITEM и WM_DRAWITEM. Воспользуйтесь для этого мастером ClassWizard. В результате вы должны получить следующий код:
#if !defined(
AFX_COLORMENU_H_OC5A82D5_B46D_11D3_BAB9_00600864785A_INCLUDED_)
#define FX_COLORMENU_H_OC5A82D5_B46DJL1D3_BAB9_00600864785A_INCLUDED_
#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
// ColorMenuJh : header file
//
///////////////////////////////////////////////////////////////////
CColorMenu window
class CColorMenu : public CMenu
{
// Construction
public:
CColorMenu();
// Implementation
public:
virtual void MeasureItem(LPMEASUREITEMSTRUCT IpMeasureltemStruct);
virtual void Drawltern(LPDRAWITEMSTRUCT IpDrawItemStruct); };
#endif // !defined(
AFX_COLORMENU_H_OC5A82D5_B46D_11D3_BAB9_00600864785A__INCLUDED_)
//////////////////////////////////////////////////////////////
ColorMenu.cpp : implementation file
//
#include "stdafx.h"
#include "Graph.h"
#include "ColorMenu.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef.THIS_FILE
static char THIS_FILE[] = _FILE_;
#endif
///////////////////////////////////////////////////////////////
CColorMenu
CColorMenu::CColorMenu()
{
}
/////////////////////////////////////////////////////////////////
CColorMenu message handlers
void CColorMenu::MeasureItem(
LPMEASUREITEMSTRUCT IpMeasureltemStruct)
{
}
void CColorMenu::DrawItem(LPDRAWITEMSTRUCT IpDrawItemStruct)
{
}
//////////////////////////////////////////////////////////
CColorMenu message handlers
Нам осталось только наполнить содержанием созданные для нас оболочки функций Drawltem и Measurement. Однако сначала рассмотрим использованные в них .структуры DRAWITEMSTRUCT и MEASUREITEMSTRUCT.
Структура DRAWITEMSTRUCT имеет следующее определение:
typedef struct tagDRAWITEMSTRUCT {
UINT CtlType;
UINT CtllD;
UINT iteralD;
UINT itemAction;
UINT itemState;
HWND hwndltem;
HOC hDC;
RECT rcltem;
DWORD itemData;
} DRAWITEMSTRUCT;
CtlType— тип самоотображаемого элемента управления, внешний вид которого определяется пользователем. Может принимать следующие значения:
ODT_BUTTON
Кнопка
ODT_COMBOBOX
Комбинированный список
ODT_LISTBOX
Список
ODT_MENU
Меню
CtllD— идентификатор кнопки, списка или комбинированного списка, для меню этот параметр не используется; itemID — идентификатор пункта меню или индекс записи в списке или комбинированном списке. Для списка или комбинированного списка, не содержащих записей, этот параметр должен иметь отрицательное значение, в результате чего будет выведен прямоугольник с координатами, заданными в rcltem, и не содержащий никаких элементов. itemAction — определяет, как должна осуществляться перерисовка. Он может быть комбинацией следующих битов:
ODA_DRAWENTIRE
Весь элемент управления требует перерисовки
ODA_FOCUS
Устанавливается, когда элемент управления получает или теряет фокус ввода; поле itemState должно быть проверено для определения, имеет ли элемент управления фокус ввода
ODA_SELECT
Устанавливается, если сделан выбор; поле itemState должно быть использовано для определения нового выбора itemState — задает состояние записи после перерисовки:
ODS_DISABLED
Запись недоступна
ODS_FOCUS
Запись имеет фокус ввода
ODS_SELECTED
Запись выбрана
hwndltem— дескриптор окна элемента управления; hDC— контекст устройства вывода; rcltem — прямоугольник, задающий границы перерисовки; itemData — для списка или комбинированного списка этот параметр содержит значение, установленное функциями CComboBox::AddString, CComboBox:;lnsertString, CListBox::AddString или CListBox::lnsertString, а для меню— функциями СМепи:: AppendMenu, CMenu::lnsertMenu или СМепи::ModifyMenu.
Структура MEASUREITEMSTRUCT имеет следующий вид:
typedef struct tagMEASUREITEMSTRUCT {
UINT CtlType;
UINT' CtllD;
UINT itemID;
UINT itemWidth;
UINT itemHeight;
DWORD itemData
} MEASUREITEMSTRUCT;
CtlType — содержит информацию о типе элемента управления (см. описание структуры DRAWITEMSTRUCT); CtllD— идентификатор элемента управления. itemID— идентификатор элемента в списке (для списка и комбинированного списка) — не используется для списков и комбинированных списков с фиксированной высотой элементов. itemWidth — используется для задания ширины элемента в меню (для списков и комбинированных списков не используется). itemHeight— задает высоту записи в списке (максимальное значение равно 255). itemData— назначение этого поля аналогично одноименному полю структуры DRAWITEMSTRUCT.
Теперь задаем размеры элементов меню. Для этой задачи предназначена функция MeasureItern:
void CCoiorMenu::MeasureItem(
LPMEASUREITEMSTRUCT IpMeasureltemStruct)
{
lpMeasureItemStruct->iteraWidth = 130;
lpMeasure!temStruct->itemHeight = 20;
}
Как вы понимаете, размеры вы можете задавать произвольные. Главное — сделать это именно здесь.
Нам осталось только нарисовать в элементах меню то, ради чего, собственно, все это и затевалось. Используем функцию:
void CCoiorMenu::DrawItem(LPDRAWITEMSTRUCT IpDrawItemStruct)
{
// Определяем прямоугольник,
// который будем заполнять соответствующим цветом
CRect rect(lpDrawItemStruct->rcItem.left,
lpDraw!temStruct->rcItem.top,
lpDrawItemStruct->rcItem.right — 110,
lpDrawItemStruct->rdtem. bottom) ;
// При обработке сообщения WM_DRAWITEM для нас уже создан
// контекст устройства, и нам остается им воспользоваться
CDC *pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
// Дальнейшие действия выполняем только в случае,
// если перерисовки требует весь элемент меню
if(lpDraw!temStruct->itemAction & ODA_DRAWENTIRE)
{
// По, идентификатору элемента меню находим его цвет
COLORREF cr = colors[lpDraw!temStruct->itemID — ID_CLRO];
// Заполняем прямоугольник выбранным цветом
pDC->FillSolidRect(Srect, cr) ;
//и отделяем его от последующего текста
pDC->MoveTo(rect.right + I, 0);
pDC->LineTo(rect.right + 1, rect.bottom);
// Выводимый текст не должен создавать "пятна"
pDC->SetBkMode(TRANSPARENT);
// Выводим текстовое обозначение цвета
pDC-XTextOut(rect.right + 10, rect.top + 4,
clrString[lpDrawItemStruct->itemID - ID_CLRO]);
}
}
Помимо этого не забудьте определить необходимые идентификаторы и тексты:
// MainFrm.cpp : implementation of the CMainFrame class
//
// Определяем массив цветов
COLORREF colors!] =
{
RGB{ О, О, О),
RGB(255, 255, 255),
RGB(128, 128, 128),
RGB(192, 192, 192),
RGB(128, 0, 0),
RGB(255, О, О),
RGB(128, 128, 0),
RGB(255, 255, 0),
RGB( 0, 128, 0),
RGB( 0, 255, 0),
RGB( 0, 128, 128),
RGB( 0, 255, 255),
RGB( 0, 0, 128),
RGB! 0, 0, 255),
RGB(128, 0, 128),
RGB (255, 0, 255) );
// ColorMenu.cpp : implementation file
//
// Мы должны иметь доступ к массиву цветов
extern COLORREF colors[];
// Определяем массив названий цветов
CString clrStringf] =
{
"Черный",
"Белый",
"Темно-серый",
"Серый",
"Темно-красный",
"Красный",
"Темно-желтый",
"Желтый",
"Темно-зеленый",
"Зеленый",
"Темно-голубой",
"Голубой",
"Темно-синий",
"Синий",
"Темно-малиновый",
"Малиновый"
};
// Resource.h
//
...
// Для каждого цвета необходим свой идентификатор
#define ID_CLRO 32788
ttdefine ID_CLR1 32789
ttdefine ID_CLR2 32790
#define ID_CLR3 32791
ttdefine ID_CLR4 32792
ttdefine ID_CLR5 32793-
#define ID_CLR6 32794
#define ID_CLR7 32795
#define ID_CLR8 32796
#define ID_CLR9 32797
#define ID_CLR10 32798
#define ID_CLR11 32799
#define ID_CLR12 32800
#define ID_CLR13 32801
#define ID_CLR14 32802
#define ID_CLR15 32803
...
Думаю, что нет никакой необходимости давать еще какие-либо пояснения в дополнение к имеющимся комментариям. То, что получилось в результате наших действий, представлено на рис. 12.13.
Создание собственных маркеров состояния
Последний пример, который мы рассмотрим в связи с меню, заключается в реализации собственных маркеров, выводимых вместо "галочки".
Первое, что нам надо — это создать рисунки, которые будут показывать отмеченное и неотмеченное состояния. Например, такие, как представленные на рис. 12.14. Я уже не буду объяснять, как это сделать.
Следующий необходимый шаг — написать код, позволяющий использовать созданные маркеры. Как обычно, сначала определим необходимые переменные, что мы и делаем в описании класса CMainFrame:
class CMainFrame : public CFrameWnd
{
...
// Attributes
public:
CBitmap m_bmpCheck; // для отмеченного состояния
CBitmap m_bmpUnCheck; // для неотмеченного состояния
...
}
Рис. 12.13. Результат наших действий по рисованию элементов меню
Рис. 12.14. Так будет выглядеть "галочка" у отмеченного элемента меню
Затем добавляем код в обработчик события WM_CREATE:
int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct)
{
// Загружаем битовые массивы с новыми маркерами
m_bmpCheck.LoadBitmap(IDB_MENU_DOWN);
m_bmpUnCheck.LoadBitmap(IDB_MENU_UP);
// Получаем системные размеры маркера
int ex = ::GetSystemMetrics(SM_CXMENUCHECK);
int cy = ::GetSystemMetrics(SM_CYMENUCHECK);
// Приводим текущие размеры битовых массивов к системным
m_bmpUnCheck.SetBitmapDimension(сх, су);
m_bmpCheck.SetBitmapDimension(ex, cy);
// Получаем доступ к главному меню
CMenu *pViewMenu = NULL;
CMenu *pTopMenu = AfxGetMainWnd()->GetMenu();
int iPos;
// Ищем элемент меню "2 column" по идентификатору IDjroOLS_2COLUMN
for (iPos = pTopMenu->GetMenu!temCount0-1; iPos >= 0; iPos—)
{
// Поиск выполняем по позиции элемента
CMenu* pMenu = pTopMenu->GetSubMenu(iPos);
if (pMenu && pMenu->GetMenu!temID(0) == ID_TOOLS_2COLUMN)
1
// Если соответствующий идентификатор найден,
// прекращаем поиск, запомнив указатель на этот элемент
pViewMenu = pMenu;
break;
}
}
// Мы обязательно должны найти элемент меню
ASSERT(pViewMenu != NULL);
// Устанавливаем новые маркеры для отмеченного и
// неотмеченного состояний,
// которые теперь будут появляться вместо "галочек"
pViewMenu->SetMenuItemBitmaps(IDJTOOLS_2COLUMN, MF_BYCOMMAND,
&m_bmpUnCheck, &m_bmpCheck);
pViewMenu->SetMenuItemBitmaps(ID_TOOLS_3COLUMN, MF_BYCOMMAND,
&m_bmpUnCheck, &m_bmpCheck);
...
}
И вот что у нас получилось в результате (рис. 12.15).
Конечно, рисунки маркеров оставляют желать лучшего, но вы можете сами нарисовать что-то более красивое. А как "подключить" их к меню, я вам рассказал.
Рис. 12.15. Так выглядят новые маркеры отмеченного и неотмеченного состояний