Руководство программиста
Глава 2. Создание моделей блоков
§2.12. Реакция блоков на действия пользователя
§2.12.7. Добавление пунктов в системное меню RDS
Описывается возможность добавления моделями блоков собственных пунктов в меню «». Рассматривается пример блока, открывающего окно своей подсистемы при выборе пункта системного меню или при нажатии связанной с этим пунктом «горячей клавиши».
Контекстное меню блока, дополнение которого описано в §2.12.6, может быть вызвано, только если окно подсистемы с этим блоком открыто и сам блок видим в этом окне. Если модели блока необходимо вынести какие-либо функции на уровень всей системы, чтобы пользователь мог вызвать их независимо от состояния окон подсистем, ей следует добавить пункт в системное меню. Добавляемые моделями блоков пункты размещаются в меню «» главного окна RDS. С каждым из этих пунктов может быть связано вызывающее его сочетание клавиш, что позволяет использовать дополнение системного меню RDS для реализации «глобальной» реакции блока на клавиатуру: обычная реакция блока на нажатие клавиш возможна только в режимах моделирования и расчета (при этом окно подсистемы с блоком обязательно должно находиться на переднем плане), а реакция на нажатие сочетания клавиш, связанного с пунктом системного меню, не зависит ни от режима RDS, ни от расположения окна подсистемы, в которой находится добавивший этот пункт блок.
Пункты системного меню, в отличие от пунктов контекстного, не могут быть временными. Их, как правило, немного (слишком большое число пунктов в меню неудобно для пользователя, кроме того, площадь экрана ограничена), поэтому о занимаемом ими объеме памяти можно не беспокоиться. Они создаются сервисной функцией rdsRegisterMenuItem и существуют либо до тех пор, пока модель блока не удалит их, либо пока модель не будет отключена от блока (например, при удалении самого блока из схемы). Как и в случае постоянных пунктов контекстного меню, модель может в любой момент изменить название пункта системного меню и его внешний вид, а также связанное с ним сочетание клавиш.
Рассмотрим для примера блок, который добавляет в системное меню RDS пункт, выбор которого открывает окно подсистемы, в которой этот блок находится. Причем сделаем название этого пункта меню и его сочетание клавиш настраиваемыми. Такие блоки, размещенные в важных подсистемах, позволят пользователю быстро открывать их через меню или с клавиатуры, не тратя время на их поиск в схеме, чтобы открыть их двойным щелчком, как обычно.
Начнем написание модели блока с класса личной области данных – он понадобится нам для хранения идентификатора созданного пункта меню, а также настроечных параметров: названия пункта и сочетания клавиш, связанного с ним.
// Класс личной области данных блока class TOpenSysWinData { public: RDS_MENUITEM MenuItem; // Идентификатор созданного пункта меню // Настроечные параметры char *Caption; // Название пункта int Key; // Клавиша (или 0,если ее нет) DWORD KeyShifts; // Состояние Ctrl, Alt и Shift // Создание пункта меню с заданными параметрами или // изменение параметров уже созданного пункта void RegisterMenuItem(void); int Setup(void); // Открыть окно настройки void SaveBin(void); // Сохранить параметры int LoadBin(void); // Загрузить параметры // Конструктор класса TOpenSysWinData(void) { Caption=NULL; Key=0; KeyShifts=0; MenuItem=NULL; }; // Деструктор класса ~TOpenSysWinData() { // Освободить память, занятую строкой Caption rdsFree(Caption); // Удалить пункт меню rdsUnregisterMenuItem(MenuItem); }; }; //=========================================
Для хранения идентификатора пункта меню, который мы будем создавать, предназначено поле MenuItem. Оно имеет уже знакомый нам по работе с контекстным меню тип RDS_MENUITEM – в RDS для работы с постоянными пунктами контекстного и системного меню используются одни и те же функции и типы данных. В остальных полях класса хранятся настроечные параметры блока: строка названия пункта меню Caption (память под нее мы будем отводить динамически при помощи сервисных функций RDS), код «горячей клавиши» пункта меню Key и флаги KeyShifts, описывающие состояние клавиш-модификаторов Ctrl, Alt и Shift для этой клавиши.
В классе также описано несколько функций-членов, которые мы рассмотрим позднее, конструктор и деструктор. В конструкторе всем полям класса присваиваются начальные значения, а в деструкторе освобождается динамическая память, занятая строкой Caption, и удаляется созданный пункт меню. При удалении мы не проверяем, была ли на самом деле отведена память под строку и был ли создан пункт меню: все сервисные функции RDS, удаляющие какие-либо объекты и освобождающие память, допускают передачу значения NULL вместо указателей или идентификаторов, в этом случае никаких действий не производится.
Из всех функций-членов класса для нас интереснее всего функция RegisterMenuItem, предназначенная для создания пункта системного меню с заданными параметрами или изменения параметров этого пункта, если он уже создан:
// Создание или модификация пункта меню // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" void TOpenSysWinData::RegisterMenuItem(void) { // Вспомогательная переменная для заголовка пункта меню: // если Caption==NULL, пункт получит заголовок "Открыть окно" const char *text=Caption?Caption:"Открыть окно"; // Флаги пункта меню: если клавиша определена (Key!=0), // пункт будет иметь "горячую клавишу" DWORD options=Key?RDS_MENU_SHORTCUT:0; if(MenuItem) // Пункт уже есть - изменяем rdsChangeMenuItem(MenuItem,text,options,Key,KeyShifts,0,0); else // Пункта еще нет - создаем MenuItem=rdsRegisterMenuItem(text,options,Key,KeyShifts,0,0); } //=========================================
Поскольку Caption у нас исходно имеет значение NULL, а показывать пользователю пункт меню без названия нехорошо, в функции вводится вспомогательная переменная text, которая будет равна Caption, если значение последней не NULL, или ссылаться на строку «Открыть окно» в противном случае. Это значение будет передано в сервисную функцию в качестве названия пункта, таким образом, пункт не останется без названия в любом случае.
Другая вспомогательная переменная, options, содержит битовые флаги пункта меню. Кроме уже знакомых нам по работе с контекстными меню флагов RDS_MENU_CHECKED, RDS_MENU_DISABLED и RDS_MENU_HIDDEN, для пунктов системного меню могут использоваться еще два дополнительных флага:
- RDS_MENU_SHORTCUT – если флаг установлен, пункт меню будет иметь «горячую клавишу»;
- RDS_MENU_UNIQUECAPTION – если флаг установлен, RDS не будет создавать этот пункт меню, если в меню уже есть пункт с точно таким же названием.
Нас интересует только флаг RDS_MENU_SHORTCUT: если значение поля Key не нулевое, значит, клавиша для пункта меню определена, и этот флаг должен быть указан. В противном случае никакие флаги для пункта меню нам не нужны, и переменная options получает значение 0.
Напишем теперь функции записи и загрузки параметров блока. Чтобы не усложнять модель блока, будем, как и в примере из §2.12.4, использовать двоичный формат и теговую запись. Функция записи параметров будет иметь следующий вид:
// Запись параметров блока void TOpenSysWinData::SaveBin(void) { BYTE tag; // Переменная для байта тега int len=Caption?strlen(Caption):0; // Длина строки Caption tag=1; // Тег 1 – название пункта меню rdsWriteBlockData(&tag,sizeof(tag)); rdsWriteBlockData(&len,sizeof(len)); // Длина строки if(len) // При ненулевой длине – запись самой строки rdsWriteBlockData(Caption,len); tag=2; // Тег 2 – "горячая клавиша" rdsWriteBlockData(&tag,sizeof(tag)); rdsWriteBlockData(&Key,sizeof(Key)); rdsWriteBlockData(&KeyShifts,sizeof(KeyShifts)); tag=0; // Тег 0 – конец данных rdsWriteBlockData(&tag,sizeof(tag)); } //=========================================
Как и в упомянутом выше примере, в этой функции используются однобайтовые теги, указывающие на формат и тип следующих за ними данных. За тегом 1 следует четырехбайтовая целая длина строки (она записывается во вспомогательную переменную len в начале функции) и, если длина не нулевая, len байтов самой строки. За тегом 2 следуют два числа, содержащих код «горячей клавиши» и ее флаги. Тег 0 указывает на конец данных блока.
Функция загрузки считывает байт тега, а затем, в зависимости от его значения, загружает данные в те или иные поля класса:
// Загрузка параметров блока int TOpenSysWinData::LoadBin(void) { BYTE tag; // Переменная для байта тега int len; // Прежде всего, освобождаем память, которую занимало старое // название пункта меню – сейчас загрузится новое rdsFree(Caption); Caption=NULL; for(;;) { if(!rdsReadBlockData(&tag,sizeof(tag))) break; // Тег не считан – данные неожиданно кончились switch(tag) { case 0: // Конец данных RegisterMenuItem(); // Создаем или изменяем пункт меню return RDS_BFR_DONE; case 1: // Название пункта rdsReadBlockData(&len,sizeof(len)); // Читаем длину if(len) // Длина ненулевая – читаем len байтов строки { // Отводим место с учетом нуля в конце строки Caption=(char*)rdsAllocate(len+1); rdsReadBlockData(Caption,len); // Читаем строку Caption[len]=0; // Дописываем нулевой байт в конец } break; case 2: // Данные "горячей клавиши" rdsReadBlockData(&Key,sizeof(Key)); rdsReadBlockData(&KeyShifts,sizeof(KeyShifts)); break; default: // Тег не опознан – ошибка return RDS_BFR_ERROR; } } // Вышли из цикла из-за неожиданного конца данных блока - ошибка return RDS_BFR_ERROR; } //=========================================
В этой функции следует обратить внимание на специфику работы со строкой Caption. В самом начале функции, перед тем, как начнут читаться какие-либо данные, текущее содержимое строки уничтожается, и полю класса Caption присваивается значение NULL. В процессе чтения данных блока должно загрузиться новое название пункта меню, поэтому старое нам больше не нужно. При считывании тега 1 сразу за ним в переменную len считывается длина строки названия, и, если она не нулевая, функцией rdsAllocate отводится память под len+1 символов (еще один байт нужен для завершающего строку нуля). После этого символы строки считываются в отведенный буфер и в его конец записывается нулевой байт, которым должна оканчиваться каждая строка.
Второй важный момент в функции – действия при считывании нулевого тега, отмечающего конец данных блока. При этом вызывается функция RegisterMenuItem, уже написанная нами раньше, которая, на основе считанного названия и кода «горячей клавиши», создает пункт системного меню или изменяет его параметры, если он уже был создан ранее. Если эту функцию не вызвать, то параметры будут загружены во внутренние поля класса, но никак не отразятся на внешнем виде пункта в системном меню.
Теперь напишем функцию настройки, которая позволит пользователю задать параметры блока, которые мы уже научились сохранять и загружать:
// Настройка параметров блока // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" int TOpenSysWinData::Setup(void) { RDS_HOBJECT window; // Идентификатор объекта-окна BOOL ok; // Пользователь нажал "OK" // Создание окна window=rdsFORMCreate(FALSE,-1,-1,"Открытие окна"); // Поле ввода для строки названия rdsFORMAddEdit(window,0,1,RDS_FORMCTRL_EDIT, "Текст пункта меню:",200); rdsSetObjectStr(window,1,RDS_FORMVAL_VALUE,Caption); // Поле ввода для "горячей клавиши" rdsFORMAddEdit(window,0,2,RDS_FORMCTRL_HOTKEY,"Клавиша:",150); rdsSetObjectInt(window,2,RDS_FORMVAL_VALUE,Key); rdsSetObjectInt(window,2,RDS_FORMVAL_HKSHIFTS,KeyShifts); // Открытие окна ok=rdsFORMShowModalEx(window,NULL); if(ok) { // Нажата кнопка OK – запись параметров в блок rdsFree(Caption); // Уничтожение старой строки // Создание динамической копии введенной строки Caption=rdsDynStrCopy(rdsGetObjectStr(window,1, RDS_FORMVAL_VALUE)); // Чтение параметров клавиши Key=rdsGetObjectInt(window,2,RDS_FORMVAL_VALUE); KeyShifts=rdsGetObjectInt(window,2,RDS_FORMVAL_HKSHIFTS); // Создание пункта меню на основе изменившихся параметров RegisterMenuItem(); } // Уничтожение окна rdsDeleteObject(window); // Возвращаемое значение return ok?RDS_BFR_MODIFIED:RDS_BFR_DONE; } //=========================================
В этой функции используется уже многократно описанный способ создания окна при помощи вспомогательного объекта RDS. Опять следует обратить внимание на присваивание нового значения заголовку меню: сначала старое содержимое Caption уничтожается, затем строка, введенная пользователем в окне, копируется из внутреннего буфера объекта (указатель на который возвращает функция Наконец, напишем функцию модели, которая будет создавать и удалять объект описанного нами класса и вызывать его функции:
// Блок, открывающий окно родительской подсистемы extern "C" __declspec(dllexport) int RDSCALL OpenSysWin(int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { // Указатель на личную область данных блока,приведенный к // правильному типу TOpenSysWinData *data=(TOpenSysWinData*)(BlockData->BlockData); switch(CallMode) { case RDS_BFM_INIT: // Инициализация BlockData->BlockData=data=new TOpenSysWinData(); break; case RDS_BFM_CLEANUP: // Очистка delete data; break; case RDS_BFM_SETUP: // Настройка return data->Setup(); case RDS_BFM_SAVEBIN: // Сохранение параметров data->SaveBin(); break; case RDS_BFM_LOADBIN: // Загрузка параметров return data->LoadBin(); case RDS_BFM_MENUFUNCTION:// Выбор пункта меню rdsOpenSystemWindow(BlockData->Parent); break; } return RDS_BFR_DONE; } //=========================================
Поскольку практически все действия мы, как обычно, вынесли в функции-члены класса, сама функция модели получилась довольно простой. Тем более, что блок не работает со статическими переменными, поэтому в тексте функции нет ни макроопределений для них, ни проверки допустимости их типа. Единственное действие, для которого мы не стали писать отдельную функцию – это реакция на выбор пункта меню RDS_BFM_MENUFUNCTION. У нашего блока есть единственный пункт меню, поэтому никакой проверки не производится: при выборе пункта сразу вызывается функция rdsOpenSystemWindow, открывающая окно подсистемы (в нашем случае – родительской подсистемы этого блока, поскольку в ее параметрах в качестве идентификатора подсистемы передан BlockData->Parent).
Нужно отметить, что при выборе пунктов и системного, и контекстного меню вызывается одна и та же реакция RDS_BFM_MENUFUNCTION. В связи с этим пары чисел, связанные с пунктами контекстного меню, не должны совпадать с парами чисел пунктов системного, иначе модель не сможет отличить их.
Для проверки работы написанной модели создадим в разных подсистемах блоки с этой моделью и зададим в их настройках разные названия пунктов меню и разные «горячие клавиши». Например, поместим один такой блок в корневую подсистему, зададим для него название пункта «Открыть главное» и сочетание клавиш Ctrl + F1. В подсистему «Sys3» (создадим ее , если такой подсистемы в схеме еще нет) поместим другой блок, и зададим в его настройках название пункта «Открыть Sys3» и сочетание клавиш Ctrl + F2. Теперь, если открыть меню «» главного окна RDS (рис. 83), в нем можно будет увидеть пункт «» с двумя подпунктами (их порядок зависит от названий: добавленные пункты системного меню всегда упорядочиваются по алфавиту).
Рис. 83. Дополнительные пункты системного меню
Теперь, если пользователь выберет пункт «» или нажмет Ctrl + F1, окно корневой подсистемы откроется (если оно было закрыто) и переместится на передний план. При выборе пункта «» или нажатии Ctrl + F2 на передний план переместится окно подсистемы «Sys3». При этом не важно, в каком режиме находится RDS в данный момент, и какое окно находится на переднем плане.