Руководство программиста
Глава 2. Создание моделей блоков
§2.12. Реакция блоков на действия пользователя
§2.12.6. Добавление пунктов в контекстное меню блока
Рассматривается добавление дополнительных (постоянных и временных) пунктов в контекстное меню блока, вызываемое по правой кнопке мыши. В созданный ранее блок, имитирующий двухкоординатную рукоятку, добавляется возможность фиксировать одну из координат рукоятки, включаемая и выключаемая через контекстное меню. В другой блок, управляющий полем ввода, также добавляется возможность переключения состояния блока пунктом контекстного меню.
Контекстное меню в RDS, как и в большинстве других программ в Windows, вызывается при щелчке правой кнопкой мыши на выделенном объекте или группе объектов. Нас, в данном случае, будет интересовать контекстное меню блока, вызываемое щелчком правой кнопкой мыши на его изображении в окне подсистемы. В этом меню содержатся как пункты общего назначения (переключение режимов RDS, управление масштабом подсистемы), так и пункты, относящиеся к выбранному блоку (вызов окон настройки и параметров блока, копирование блока в буфер обмена, смена слоя и т.п.) Состав пунктов контекстного меню зависит от текущего режима RDS: в режимах моделирования и расчета, например, в меню не будет пунктов, позволяющих изменить какие-либо параметры блока. На самом деле, в этих режимах в контекстном меню по умолчанию будут только пункты общего назначения, поскольку все стандартные пункты меню, относящиеся к конкретному блоку, предназначены для изменения тех или иных его параметров.
RDS позволяет моделям блоков добавлять собственные пункты в контекстное меню. Таким образом модель может предоставить пользователю быстрый доступ к различным функциям блока, причем возможность выбора этих пунктов меню пользователем может не зависеть от текущего режима RDS. В отличие от пункта контекстного меню, специально предназначенного для вызова окна настройки блока, дополнительные пункты, добавленные моделью блока, могут вызываться не только из режима редактирования. Модель, при необходимости, может самостоятельно управлять их видимостью, разрешать и запрещать их, оперативно менять их названия и т.п.
Пункты, добавляемые моделью блока в контекстное меню, могут быть как постоянными, так и временными. Постоянные пункты могут добавляться в меню в любой момент и существуют до тех пор, пока не будут удалены из меню вызовом специальной сервисной функции, либо до тех пор, пока существует блок, модель которого их создала. Временные пункты могут добавляться в контекстное меню только в момент его открытия (для этого предусмотрен специальный вызов модели) и автоматически уничтожаются после закрытия меню. У каждого из двух этих типов есть свои достоинства и недостатки. Использование постоянных пунктов меню несколько облегчает работу программиста: в модель не нужно включать реакцию на открытие контекстного меню и размещать создание пунктов именно в ней, кроме того, в любом месте модели можно управлять видимостью и внешним видом пунктов меню. Однако, каждый постоянный пункт меню занимает место в памяти в течение всего времени своей жизни, и, если в системе будет несколько тысяч блоков, каждый из которых создаст один или несколько пунктов меню, общие потери памяти могут стать ощутимыми. При этом тот факт, что тысяча блоков имеет одну и ту же модель, создающую для каждого из этих блоков один и тот же дополнительный пункт меню, не поможет сэкономить память: физически это будут разные пункты меню, иначе блоки не смогли бы управлять своими пунктами независимо.
Временные пункты меню не занимают много памяти, поскольку немедленно уничтожаются при закрытии меню, но создавать их можно только в специально предусмотренной для этого реакции модели на открытие контекстного меню. Управлять внешним видом временных пунктов нельзя – вместо этого их нужно создавать сразу в том виде, в котором их должен увидеть пользователь в данный момент (то есть в текущем режиме, в текущем состоянии блока и т.п.)
Рассмотрим сначала работу с постоянными пунктами меню. В §2.12.2 мы создали блок, имитирующий двухкоординатную рукоятку. Добавим в него возможность фиксировать одну из координат, разрешая пользователю перемещать рукоятку только по другой координате. Зафиксировав координату X, он сможет перемещать рукоятку только по вертикали, зафиксировав Y – только по горизонтали. Для управления этими функциями добавим в контекстное меню блока пункты «» и «», причем около пункта, соответствующего включенной в данный момент функции, должна ставиться галочка. Первый выбор пункта будет включать фиксацию соответствующей координаты (одновременно отключая фиксацию другой, если она была выбрана: если зафиксировать обе координаты, перемещать рукоятку будет вообще невозможно), второй – выключать. Для того, чтобы по внешнему виду блока пользователь сразу мог определить, что одна из координат зафиксирована, будем закрашивать серым цветом ту часть прямоугольника, в которую пользователь не сможет переместить рукоятку.
Прежде всего, необходимо внести изменения в описание класса блока: нам потребуется несколько новых полей и функция реакции на выбор пункта меню.
//====== Класс личной области данных ====== class TSimpleJoystick { private: // Центр круга (рукоятки) до начала перетаскивания int OldHandleX,OldHandleY; // Координаты курсора на момент начала перетаскивания int OldMouseX,OldMouseY;// Флаги фиксации одной из координат BOOL LockX,LockY; // Идентификаторы добавленных пунктов меню RDS_MENUITEM MenuLockX,MenuLockY;public: // Настроечные параметры блока COLORREF BorderColor; // Цвет рамки блока COLORREF FieldColor; // Цвет прямоугольника COLORREF HandleColor; // Цвет круга в покое COLORREF MovingHandleColor; // Цвет круга при тасканииCOLORREF GrayedColor; // Цвет недоступной областиint HandleSize; // Диаметр круга // Реакция на нажатие кнопки мыши int MouseDown(RDS_PMOUSEDATA mouse,double x,double y,DWORD *pFlags); // Реакция на перемещение курсора мыши void MouseMove(RDS_PMOUSEDATA mouse,double *px,double *py); // Рисование изображения блока void Draw(RDS_PDRAWDATA draw,double x,double y,BOOL moving); // Конструктор класса TSimpleJoystick(void) { BorderColor=0; // Черная рамка FieldColor=0xffffff; // Белое поле HandleColor=0xff0000; // Синий круг MovingHandleColor=0xff; // Красный при тасканииGrayedColor=0x7f7f7f; // Серый LockX=LockY=FALSE; // Фиксация выключенаHandleSize=20; // Диаметр круга// Создание пунктов меню // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" MenuLockX=rdsRegisterContextMenuItem("Фиксировать X",1,0); MenuLockY=rdsRegisterContextMenuItem("Фиксировать Y",2,0);};// Деструктор класса ~TSimpleJoystick() { // Уничтожение пунктов меню rdsUnregisterMenuItem(MenuLockX); rdsUnregisterMenuItem(MenuLockY); };}; //=========================================
В закрытую область класса добавлены два логических поля LockX и LockY, которые будут отвечать за фиксацию горизонтальной и вертикальной координаты соответственно. Там же находятся поля MenuLockX и MenuLockY, в которых будут храниться идентификаторы созданных моделью дополнительных пунктов меню. Эти идентификаторы имеют тип RDS_MENUITEM (он описан в «RdsDef.h»), мы будем использовать их при вызове сервисных функций RDS для установки и снятия галочек рядом с этими пунктами меню. В настроечные параметры блока добавлено новое поле GrayedColor для хранения цвета, которым мы будем закрашивать недоступную для рукоятки область прямоугольника блока. Также добавлена новая функция-член класса MenuFunction для реакции на выбор пользователем одного из добавленных пунктов меню (позднее мы рассмотрим ее подробно), внесены изменения в конструктор класса и добавлен деструктор.
Самое важное изменение в конструкторе класса – это создание в нем двух постоянных пунктов контекстного меню при помощи сервисной функции RDS rdsRegisterContextMenuItem. Эта функция принимает три параметра: строку названия пункта меню (именно эту строку пользователь увидит в меню) и два произвольных целых числа, которые связываются с создаваемым пунктом. Когда пользователь выберет в меню дополнительный пункт, эти два числа будут переданы в модель блока, и по ним она сможет определить, какой именно пункт выбран. Первое число обычно называется номером (или идентификатором) функции меню, второе – данными меню. В нашем случае пункт «» связывается с парой (1,0), «» – с парой (2,0). На самом деле, практически всегда для точной идентификации выбранного пункта достаточно одного целого числа, второе число связывается с пунктом меню для удобства программиста: например, группа сходных по смыслу пунктов меню может иметь одинаковый номер функции меню, но разные данные. В нашей модели при выборе пункта меню мы будем анализировать только первое число (номер функции), второе будет игнорироваться.
Функция rdsRegisterContextMenuItem возвращает идентификатор созданного пункта меню типа RDS_MENUITEM (на самом деле, этот идентификатор является указателем на внутренний объект RDS, в котором хранятся данные пункта меню, но модель блока не может работать с ним напрямую, не используя сервисные функции). Идентификаторы пунктов «» и «» присваиваются полям класса MenuLockX и MenuLockY соответственно.
В деструкторе класса созданные пункты меню уничтожаются сервисной функцией rdsUnregisterMenuItem. Можно было бы и не уничтожать их – при отключении модели от блока они уничтожатся сами – но хороший стиль программирования требует явно уничтожать все то, что было создано.
Теперь внесем изменения в функцию рисования блока Draw. Если одна из координат зафиксирована (то есть если одна из переменных LockX или LockY имеет значение TRUE), нужно закрасить серым цветом недоступную для рукоятки область. Если зафиксирована горизонтальная координата, следует нарисовать слева и справа от рукоятки два серых прямоугольника, так что белой останется только узкая вертикальная полоса шириной с рукоятку, по которой круг рукоятки сможет перемещаться. Если зафиксирована вертикальная координата, белая полоса должна быть горизонтальной, то есть нужно рисовать серые прямоугольники выше и ниже рукоятки. Изменения в функции выглядят следующим образом:
// Рисование изображения блока void TSimpleJoystick::Draw(RDS_PDRAWDATA draw, double x,double y,BOOL moving) { // … // …(начало функции без изменений)… // … // Установка области отсечения r.left=draw->Left+1; r.top=draw->Top+1; r.right=draw->Left+draw->Width-1; r.bottom=draw->Top+draw->Height-1; rdsXGSetClipRect(&r);// Рисование ограничений if(LockX||LockY) // Фиксируется одна из координат { // Установка серого цвета заливки rdsXGSetBrushStyle(0,RDS_GFS_SOLID,GrayedColor); if(LockX) // Фиксируется X { rdsXGFillRect(r.left,r.top,hx-hR,r.bottom); // Слева rdsXGFillRect(hx+hR,r.top,r.right,r.bottom); // Справа } else // Фиксируется Y { rdsXGFillRect(r.left,r.top,r.right,hy-hR); // Сверху rdsXGFillRect(r.left,hy+hR,r.right,r.bottom);// Снизу } }// Линии перекрестия rdsXGMoveTo(cx,draw->Top); rdsXGLineTo(cx,draw->Top+draw->Height); rdsXGMoveTo(draw->Left,cy); rdsXGLineTo(draw->Left+draw->Width,cy); // …(далее без изменений)…
Если LockX или LockY – TRUE, устанавливается серый (GrayedColor из настроек блока) цвет заливки и функцией rdsXGFillRect рисуются два вертикальных или горизонтальных, в зависимости от зафиксированной координаты, прямоугольника. Разрыв между прямоугольниками вычисляется по координатам центра рукоятки (hx,hy) и радиусу рукоятки hR.
В функцию реакции на перемещение курсора мыши тоже необходимо внести изменения: при зафиксированной координате перемещение рукоятки по соответствующей оси должно быть запрещено:
// Реакция на перемещение курсора мыши void TSimpleJoystick::MouseMove(RDS_PMOUSEDATA mouse, double *px,double *py) { // … // …(начало функции без изменений)… // … // По новым координатам центра рукоятки вычисляем соответствующие // им вещественные значения выходов, ограничивая их // диапазоном [-1...1] if(!LockX) { *px=2.0*(hx-cx)/mouse->Width; if(*px>1.0) *px=1.0; else if(*px<-1.0) *px=-1.0; } if(!LockY) { *py=-2.0*(hy-cy)/mouse->Height; if(*py>1.0) *py=1.0; else if(*py<-1.0) *py=-1.0; } } //=========================================
Наконец, добавим в модель реакцию на выбор пользователем пункта меню (RDS_BFM_MENUFUNCTION) и напишем функцию-член класса, которая будет вызываться в этой реакции. В оператор switch(CallMode) внутри функции модели нужно вставить вызов функции MenuFunction:
// … case RDS_BFM_DRAW: data->Draw((RDS_PDRAWDATA)ExtParam,x,y, BlockData->Flags & RDS_MOUSECAPTURE); break;// Выбор пользователем добавленного пункта меню case RDS_BFM_MENUFUNCTION: data->MenuFunction((RDS_PMENUFUNCDATA)ExtParam); break;} return RDS_BFR_DONE; // Отмена макроопределений // …
При вызове функции модели в режиме RDS_BFM_MENUFUNCTION в параметре ExtParam передается указатель на структуру RDS_MENUFUNCDATA, описанную в «RdsDef.h» следующим образом:
typedef struct { int Function; // Номер функции меню (первое целое число, // указанное при создании пункта меню) int MenuData; // Данные меню (второе целое число, указанное // при создании пункта меню) } RDS_MENUFUNCDATA; typedef RDS_MENUFUNCDATA *RDS_PMENUFUNCDATA;
Структура содержит всего два поля, в которые записываются целые числа, связанные с выбранным пользователем пунктом меню при его создании. Например, если пользователь выберет пункт «», в поле Function структуры будет записано значение 1, а в поле MenuData – 0. Указатель на эту структуру, приведенный к правильному типу, передается в функцию MenuFunction, которую мы сейчас напишем:
// Функция реакции на выбор одного из пунктов меню void TSimpleJoystick::MenuFunction(RDS_PMENUFUNCDATA MenuData) { switch(MenuData->Function) { case 1: // Выбран пункт "Фиксировать X" LockX=!LockX; // Переключаем флаг фиксации X LockY=FALSE; // Отключаем фиксацию Y break; case 2: // Выбран пункт "Фиксировать Y" LockY=!LockY; // Переключаем флаг фиксации Y LockX=FALSE; // Отключаем фиксацию X break; } // Установка галочек у пунктов меню в зависимости от // флагов фиксации координат rdsSetMenuItemOptions(MenuLockX,LockX?RDS_MENU_CHECKED:0); rdsSetMenuItemOptions(MenuLockY,LockY?RDS_MENU_CHECKED:0); } //=========================================
В этой функции мы опознаем выбранный пользователем пункт меню по полю Function переданной структуры: оно может иметь значения 1 («») или 2 («»). В зависимости от выбранного пункта мы инвертируем флаг фиксации соответствующей координаты и сбрасываем флаг фиксации другой, чтобы не допустить одновременной фиксации обеих координат. Затем, в зависимости от текущего состояния флагов, сервисной функцией rdsSetMenuItemOptions устанавливается галочка у пункта меню, соответствующего зафиксированной координате. В RDS есть и другие сервисные функции, позволяющие управлять параметрами пунктов меню (например, оперативно изменять название пункта или связанные с ним целые числа), но в данном примере нам нужно только ставить и убирать галочку.
Функция rdsSetMenuItemOptions позволяет изменить видимость пункта меню, разрешить или запретить его (запрещенные пункты меню отображаются серым цветом), а также установить или сбросить галочку слева от его названия. Она принимает два параметра: первый – идентификатор пункта меню, второй – целое число, представляющее собой набор битовых флагов, управляющих состоянием этого пункта. Для пунктов контекстного меню можно использовать любое сочетание следующих флагов:
- RDS_MENU_CHECKED – если флаг установлен, слева от названия пункта меню будет изображаться галочка;
- RDS_MENU_DISABLED – если флаг установлен, пункт меню будет запрещен (изображается серым, не может быть выбран пользователем);
- RDS_MENU_HIDDEN – если флаг установлен, пункт меню будет невидимым для пользователя;
- RDS_MENU_DIVIDER − если флаг установлен, вместо пункта меню будет создан горизонтальный разделитель, при этом переданное название пункта игнорируется, и выбор этого пункта пользователем будет невозможен.
В нашей функции мы используем единственный флаг для управления галочкой. Если координата зафиксирована, мы передаем в функцию значение RDS_MENU_CHECKED (флаг галочки взведен, остальные сброшены), это сделает пункт меню видимым, разрешенным и отмеченным галочкой). В противном случае мы передаем значение 0 (все три флага сброшены), что также сделает пункт меню видимым и разрешенным, но уже без галочки.
После внесения изменений в модель блока никакие дополнительные настройки не требуются: во всех режимах в контекстном меню блока должны появиться дополнительные пункты (рис. 82).

(а)

(б)
Рис. 82. Дополнительные пункты контекстного меню блока-рукоятки в режимах редактирования (а) и моделирования (б)
Приведенный пример демонстрирует работу с постоянными пунктами меню: мы создаем их при инициализации блока в конструкторе класса личной области данных, а затем меняем их параметры (устанавливаем и сбрасываем галочки) в процессе работы блока. Теперь рассмотрим работу с временными пунктами контекстного меню.
В §2.12.3 мы создали блок для управления полем ввода, который может находиться в двух состояниях: открытом (при этом сквозь прозрачное окно в прямоугольнике блока видно лежащее под ним поле ввода) и закрытом. Состояния блока переключались щелчком левой кнопкой мыши. Добавим в этот пример возможность открывать и закрывать блок пунктом контекстного меню – в этом случае переключать состояние блока можно будет и в режиме редактирования, что ранее было невозможно: в режиме редактирования нажатия кнопок мыши не передаются в модель. И для открытия, и для закрытия блока будем использовать один и тот же пункт меню, меняя его название в зависимости от текущего состояния: при закрытом блоке пункт будет называться «», при открытом – «». И, поскольку полей ввода, а, значит, и блоков для их управления, в схеме может быть достаточно много, не будем зря тратить память и сделаем этот пункт контекстного меню временным.
Для реализации задуманного в модель нужно добавить две реакции: во-первых, реакцию на открытие контекстного меню RDS_BFM_CONTEXTPOPUP, в которой мы будем создавать временный пункт, во-вторых, уже знакомую нам реакцию на выбор пункта меню пользователем RDS_BFM_MENUFUNCTION – реакция на временные пункты меню ничем не отличается от реакции на постоянные. Внутрь оператора switch(CallMode) в функции модели добавляются два новых оператора case:
// Открытие контекстного меню блока case RDS_BFM_CONTEXTPOPUP: // Добавление временного пункта меню // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" rdsAdditionalContextMenuItemEx(bypass?"Открыть":"Закрыть",0,1,2); break; // Выбор пункта меню пользователем case RDS_BFM_MENUFUNCTION: bypass=!bypass; // Переключить режим out=bypass?x_ext:x_int; // Подать на выход один из входов Ready=1; // Взвести флаг готовности break;
В реакции на открытие контекстного меню (RDS_BFM_CONTEXTPOPUP) вызывается функция rdsAdditionalContextMenuItemEx, которая и создает нужный нам временный пункт меню. Функция принимает четыре параметра: строку с названием пункта (в нашем случае «Открыть» или «Закрыть», в зависимости от значения переменной состояния bypass), битовые флаги, указывающие состояние пункта (у нас – 0, то есть видимый, разрешенный пункт меню без галочки) и, как и для постоянного пункта меню, два целых числа, которые будут переданы в модель при выборе этого пункта пользователем. В нашей модели всего один пункт контекстного меню, поэтому в реакции на выбор пункта пользователем мы не будем анализировать эти числа – если бы у нас было несколько пунктов меню, как в предыдущем примере, нам нужно было бы отличать один от другого, а с единственным пунктом такой необходимости нет. В вызове функции с пунктом меню связывается пара чисел (1,2), но можно было бы указать любые другие числа.
Следует отметить, что функция rdsAdditionalContextMenuItemEx, в отличие от использованной в предыдущем примере rdsRegisterContextMenuItem, не возвращает уникальный идентификатор пункта меню. У временных пунктов меню вообще нет идентификаторов, и после их создания модель никак не может изменить их параметры. Этого и не требуется, поскольку временные пункты контекстного меню существуют только до момента закрытия этого меню, и каждый раз создаются заново в момент его открытия. Именно в момент создания временного пункта модель указывает его название и битовые флаги, определяющие внешний вид пункта на данный момент. В данном примере мы, вместо того, чтобы изменять название постоянного пункта меню в зависимости от текущего состояния блока (значения переменной bypass), в момент открытия меню создаем временный пункт сразу с нужным нам названием.
Во втором параметре функции rdsAdditionalContextMenuItemEx можно использовать уже описанные выше битовые флаги RDS_MENU_CHECKED, RDS_MENU_DISABLED и RDS_MENU_HIDDEN. На самом деле, использование флага RDS_MENU_HIDDEN при создании временного пункта меню бессмысленно, поскольку этот пункт не будет виден пользователю на протяжении всего своего существования – проще вообще не создавать этот пункт. Может показаться, что создание временного пункта с флагом RDS_MENU_DISABLED тоже не имеет особого смысла – пункт также все время будет запрещенным до момента своего уничтожения, и модель не сможет разрешить его. Однако, этот пункт будет показан в меню и изображен серым цветом, что даст пользователю понять, что в других режимах этот пункт, вероятно, доступен для выбора. Это дает разработчику модели блока выбор: менять ли состав и названия пунктов меню в зависимости от состояния блока, или запрещать недоступные в данный момент пункты, оставляя их видимыми для пользователя. Какой вариант будет выглядеть лучше с точки зрения построения интерфейса – решать программисту.
В реакции на выбор пункта меню (RDS_BFM_MENUFUNCTION), как и было указано выше, не анализируются переданные в модель целые числа, связанные с выбранным пунктом. Наш единственный пункт меню должен инвертировать состояние блока, то есть, выполнять те же самые действия, что и при щелчке левой кнопкой мыши. Именно это и выполняется в реакции на событие RDS_BFM_MENUFUNCTION: значение переменной bypass инвертируется, на выход блока out подается один из входов (в зависимости от значения bypass) и взводится сигнал готовности Ready, чтобы в ближайшем такте расчета новое значение выхода было передано по связям. На самом деле, оператор case реакции на выбор пункта меню можно было бы вставить внутрь реакции на нажатие кнопки мыши перед оператором «bypass=!bypass;», поскольку выполняемые ими действия совпадают:
// … if(mouse->Button!=RDS_MLEFTBUTTON) return RDS_BFR_SHOWMENU; // Нажата левая кнопка мыши, причем курсор попал // в рамку или блок в закрытом состоянии case RDS_BFM_MENUFUNCTION: bypass=!bypass; // Переключаем состояние Ready=1; // Взводим сигнал готовности // Здесь намеренно не поставлен оператор break: необходимо // выполнить действия в следующем case (такт расчета) // …
Этого не было сделано только для большей понятности примера.
Посмотрев на тексты моделей двух рассмотренных примеров, можно заметить, что реакция на выбор пункта меню пользователем не зависит от того, временный это пункт или постоянный. В обоих случаях в модель передаются два целых числа, связанных с выбранным пунктом меню. Технически, создание временных пунктов меню предпочтительнее, поскольку они не занимают лишнего места в памяти, немного проигрывая постоянным только в необходимости включения в модель дополнительной реакции RDS_BFM_CONTEXTPOPUP. Использовать для расширения контекстного меню блока постоянные или временные пункты – решать программисту.