Описание пользователя
Глава 3. Использование стандартных модулей автокомпиляции
§3.6. Принципы создания автокомпилируемых моделей блоков
§3.6.12. Добавление пунктов в контекстное и системное меню
Рассматривается программное добавление моделью блока новых пунктов в контекстное (вызываемое по правой кнопке) и главное меню RDS. При помощи этих пунктов блок может принимать от пользователя команды в режиме редактирования, в котором большинство остальных способов взаимодействия моделей блоков с пользователем отключено.
Модели блоков могут добавлять свои собственные пункты как в контекстное меню (меню, которое вызывается при нажатии правой кнопки мыши на изображении блока), так и в главное меню RDS. Добавление пунктов в контекстное меню обычно используется для предоставления пользователю быстрого доступа к каким-либо функциям, выполняемым блоком, или для переключения режимов работы этого блока. Добавление пунктов в главное меню используется гораздо реже: в отличие от контекстного, индивидуального для каждого блока, главное меню – общее для всей схемы, поэтому туда имеет смысл добавлять пункты для действий, уникальных для данной схемы. Например, в главное меню можно добавить пункт для открытия какого-либо очень важного окна подсистемы, чтобы пользователь не искал эту подсистему по всей схеме.
Пункты контекстного меню всегда добавляются в его конец, а пункты главного – в специальный подпункт «». Доступность этих пунктов пользователю определяется не режимом RDS, а разработчиком модели: он может разрешать или запрещать различные пункты в зависимости от текущего состояния блока по своему желанию. Здесь будут приведены только самые простые примеры добавления пунктов в меню, более подробно эти вопросы рассмотрены в §2.12.6 и §2.12.7 руководства программиста.
В §3.6.11 рассматривался блок-рукоятка, с помощью которого пользователь мог задавать значения двух координат, перемещая мышью круг внутри прямоугольника. Добавим в контекстное меню этого блока пункты, позволяющие обнулять одну из задаваемых координат или обе координаты сразу: это позволит не только точно приводить к нулю выходы блока (точно переместить мышью круг может оказаться не такой простой задачей), но и даст пользователю возможность сбрасывать значения блока не только в режимах моделирования и расчета, но и в режиме редактирования, в котором реакции блока на мышь не вызываются.
Чтобы добавить пункты в контекстное меню блока, в модель этого блока необходимо ввести сразу две реакции: реакцию на вызов контекстного меню, в которой и будут добавлены новые пункты, и реакцию на выбор пункта меню, в которой модель блока будет реагировать на выбор пользователем одного из этих добавленных пунктов. В списке событий редактора модели обе эти реакции находятся в разделе «» (рис. 460).
Рис. 460. Реакции на вызов меню и выбор пункта в списке событий
Введем в ранее созданный блок-рукоятку реакцию на открытие контекстного меню, в которой мы добавим в меню три новых пункта. Для этого откроем редактор модели этого блока, на вкладке «» в разделе «» дважды щелкнем на подразделе «» и введем на открывшейся вкладке «» следующий текст:
// Разделитель rdsAdditionalContextMenuItemEx(NULL,RDS_MENU_DIVIDER,0,0); // Исполняемые пункты rdsAdditionalContextMenuItemEx("Обнулить X", x==0.0?RDS_MENU_DISABLED:0,1,0); rdsAdditionalContextMenuItemEx("Обнулить Y", y==0.0?RDS_MENU_DISABLED:0,2,0); rdsAdditionalContextMenuItemEx("Обнулить все", x==0.0&&y==0.0?RDS_MENU_DISABLED:0,3,0);
Для добавления пунктов в контекстное меню из реакции на вызов этого меню мы используем функцию rdsAdditionalContextMenuItemEx. В первом параметре функции передается текст добавляемого пункта, во втором – набор битовых флагов, определяющих его внешний вид, в третьем и четвертом – два целых числа, которые без изменения будут переданы в реакцию модели на выбор этого пункта (можно считать эти числа идентификатором пункта, по которому модель сможет понять, какой именно из добавленных пунктов был выбран пользователем).
В самом первом вызове функции вместо текста пункта передается NULL, а в битовых флагах – RDS_MENU_DIVIDER. Это приведет к тому, что вместо нормального пункта в контекстное меню будет добавлена горизонтальная линия, которая визуально отделит добавляемые нами пункты от остальной части меню, за которую отвечает RDS. На самом деле, можно было бы и не передавать флаг RDS_MENU_DIVIDER, поскольку NULL вместо текста пункта уже дает RDS понять, что нужно добавить не пункт меню, а горизонтальный разделитель. Два последних параметра rdsAdditionalContextMenuItemEx при таком вызове игнорируются (разделитель не может быть выбран пользователем), поэтому в них можно передать что угодно – мы передаем нули.
Следующие три вызова rdsAdditionalContextMenuItemEx добавляют в меню пункты «», «» и «», с которыми мы связываем пары целых чисел (1,0), (2,0) и (3,0) соответственно. В реакции на выбор пункта меню нам достаточно будет проверять только первое из этих чисел: значение 1 будет означать, что выбран пункт «», значение 2 – «», значение 3 – «». Во втором параметре каждого вызова стоит условное выражение, имеющее значение RDS_MENU_DISABLED, если соответствующие этому пункту меню переменные блока уже равны нулю, и ноль в противном случае. Битовый флаг RDS_MENU_DISABLED указывает RDS на то, что пункт меню должен быть запрещенным и что пользователь не может его выбрать. Таким образом, пункты «» будут разрешенными, только если соответствующие им координаты еще не обнулены – это даст пользователю некоторую визуальную обратную связь.
Добавление пунктов мы реализовали, теперь нужно ввести в модель реакцию на их выбор. На вкладке «» в разделе «» дважды щелкнем на подразделе «» и введем на открывшейся вкладке «» следующий текст:
// Какой пункт выбран switch(MenuData->Function) { case 1: x=0; break; // Обнулить X case 2: y=0; break; // Обнулить Y case 3: x=y=0; break; // Обнулить все } // Взводим сигнал готовности для передачи выходов по связям Ready=1;
В эту реакцию в параметре MenuData передается указатель на структуру RDS_MENUFUNCDATA, состоящую всего из двух целых полей: Function и MenuData. В поле Function записано первое из двух целых чисел, которые мы связали с выбранным пользователем пунктом меню в вызове rdsAdditionalContextMenuItemEx, в поле MenuData – второе из них. Таким образом, для того, чтобы выяснить, какой их трех добавленных нами пунктов выбрал пользователь, нам нужно сравнить MenuData->Function с числами 1, 2 и 3, что мы и делаем в операторе switch.
После того, как, в зависимости от выбранного пользователем пункта, мы обнулили нужный выход блока, мы, как всегда, взводим сигнал готовности блока Ready, чтобы новое значение выхода передалось по связям.
Теперь, если скомпилировать модель и щелкнуть на блоке правой кнопкой мыши, в конце его контекстного меню появятся разделитель и три новых пункта (рис. 461). Меню будет выглядеть по-разному, в зависимости от режима RDS, но добавленные нами пункты будут в нем присутствовать всегда. При этом пункт обнуления выхода будет разрешенным только в том случае, если этот выход не равен нулю в точности (на рисунке выход «x» в точности равен нулю, и пункт «» заблокирован).
Рис. 461. Контекстное меню блока в режиме редактирования (слева)
и моделирования (справа)
Теперь рассмотрим добавление пунктов меню в главное меню RDS. Это несколько сложнее, чем добавлять пункты в контекстное. В рассмотренном выше примере мы добавляли в контекстное меню временные пункты, которые автоматически уничтожались RDS при закрытии этого меню (подробнее об этом – в §2.12.6 руководства программиста). Главное меню, в отличие от контекстного, не связано с каким-либо конкретным блоком, поэтому у блока не может быть реакции на открытие главного меню. Если бы такая реакция была предусмотрена в RDS, ее пришлось бы вызывать для всех блоков загруженной схемы, спрашивая у модели каждого из них, не желает ли она добавить что-либо в главное меню. Такое усложнение не было бы оправданным, поэтому добавление пунктов в главное меню устроено иначе. Модель блока в любой момент своего существования может создать в главном меню свой собственный пункт при помощи вызова rdsRegisterMenuItem, RDS запомнит этот факт и свяжет этот пункт с создавшим его блоком, пока модель не уничтожит этот пункт или пока блок не будет удален.
Чтобы проиллюстрировать возможность добавления пунктов в главное меню, создадим блок, который будет вычислять скорость расчета RDS в тактах в секунду. Значение этой скорости будет показываться пользователю при выборе пункта меню «» или при нажатии сочетания клавиш Ctrl+Alt+S. Наш блок будет работать следующим образом: при запуске расчета он запомнит время с момента загрузки Windows, возвращаемое стандартной функцией API GetTickCount. В каждом такте расчета он будет увеличивать на единицу внутреннюю переменную – счетчик тактов. При запросе пользователем статистики модель разделит число тактов на прошедший интервал времени и покажет результат пользователю. Чтобы статистику можно было вызвать и при остановленном расчете, в момент остановки блок тоже будет запоминать результат GetTickCount в специальной переменной, и, при запросе статистики, показывать скорость последнего выполненного расчета. Внутренний счетчик тактов блока мы назовем «Num», переменную для хранения времени начала расчета – «StartTick», переменную, для хранения времени его конца – «EndTick». Таким образом, наш блок будет иметь следующую структуру статических переменных:
| Имя | Тип | Вход/выход | Пуск | Начальное значение |
|---|---|---|---|---|
| Start | Сигнал | Вход | ✓ | 0 |
| Ready | Сигнал | Выход | 0 | |
| Num | int | Внутренняя | 0 | |
| StartTick | double | Внутренняя | 0 | |
| EndTick | double | Внутренняя | 0 |
Несмотря на то, что функция GetTickCount возвращает целое число миллисекунд с момента загрузки Windows, переменные для хранения результата ее возврата мы сделали вещественными – для вычисления скорости нам потребуются вещественные числа, поскольку нам придется делить число миллисекунд на тысячу, чтобы получить скорость в тактах в секунду.
Создадим в схеме новый блок и зададим ему указанную выше структуру статических переменных. Нам нужно, чтобы блок запускался каждый такт (он должен считать эти такты) – можно просто при его создании установить флаг запуска каждый такт (см. рис. 367), а можно написать модель таким образом, чтобы блок запускался каждый такт независимо от состояния этого флага. Так мы и поступим.
Прежде всего, добавим в нашу модель стандартный файл заголовков «stdio.h» – сообщение пользователю мы будем формировать функцией sprintf, описанной в этом файле. Раскроем на вкладке «» левой панели редактора раздел «» и дважды щелкнем на открывшемся подразделе «». На появившейся в правой части окна пустой вкладке введем команду включения нужного файла:
#include <stdio.h>
Теперь добавим в модель реакцию на запуск расчета: при запуске мы должны сбросить счетчик тактов и запомнить время начала расчета. На вкладке «» раскроем раздел «» и дважды щелкнем на подразделе «» (рис. 462). Откроется вкладка «», на которой нужно ввести следующие команды:
// Сброс счетчика тактов Num=0; // Запоминение времени старта StartTick=GetTickCount(); // Принудительный запуск модели Start=1;
Рис. 462. Запуск и остановка расчета в списке событий
В счетчик тактов Num мы записываем ноль, в переменную времени начала расчета – результат возврата GetTickCount (то есть текущее время с момента загрузки Windows в миллисекундах), а в сигнал запуска блока Start – единицу, что приведет к принудительному запуску модели в ближайшем такте расчета. Если для блока установлен запуск каждый такт, то модель запустится независимо от значения Start. Если же установлен запуск по сигналу, то модель запустится потому, что мы только что взвели этот сигнал.
При остановке расчета нам нужно запомнить время этой остановки в переменной EndTick. Добавим в модель реакцию на остановку: в том же самом разделе «» вкладки «» дважды щелкнем на подразделе «» (см. рис. 462) и на открывшейся вкладке «» введем оператор присваивания:
// Запоминение времени остановки EndTick=GetTickCount();
В такте расчета мы должны, во-первых, увеличить на единицу счетчик тактов Num, и, во-вторых, взвести сигнал Start для принудительного запуска модели в следующем такте независимо от настроек блока. В разделе «» дважды щелкнем на подразделе «» и введем на открывшейся вкладке следующий текст:
// Увеличиваем счетчик тактов Num++; // Принудительный запуск модели Start=1;
Теперь перейдем собственно к добавлению пункта в главное меню. Будем делать это при инициализации блока, то есть в момент подключения к нему модели. На вкладке «» раскроем раздел «» и дважды щелкнем в нем на подразделе «» (рис. 463). Откроется вкладка «», на которой мы введем вызов для добавления пункта в главное меню:
rdsRegisterMenuItem("Статистика", RDS_MENU_SHORTCUT|RDS_MENU_UNIQUECAPTION, 'S', RDS_KALT|RDS_KCTRL, 0,0);
Рис. 463. Инициализация блока в списке событий
В первом параметре функции rdsRegisterMenuItem передается текст добавляемого пункта меню – в нашем случае, это «статистика». Второй параметр содержит битовые флаги, управляющие пунктом. У нас это объединение флага RDS_MENU_SHORTCUT, указывающего на то, что к пункту будет привязано сочетание клавиш, и флага RDS_MENU_UNIQUECAPTION, блокирующего добавление пункта, если другой пункт с таким названием в меню уже есть. В третьем и четвертом параметрах передаются код и флаги сочетания клавиш соответственно – у нас это клавиша «S» и объединение флагов RDS_KALT и RDS_KCTRL, то есть сочетанием клавиш для нашего пункта будет Ctrl+Alt+S. Наконец, в двух последних параметрах передается пара целых чисел, по которым модель в реакции на вызов пользовательского пункта меню сможет опознать этот пункт. Точно так же модель в предыдущем примере опознавала пункты контекстного меню – реакция на выбор пункта контекстного и главного меню в модели общая. В нашем случае мы передаем два нуля: наш блок имеет единственной пользовательский пункт главного меню и не добавляет ничего в контекстное, поэтому в реакции на выбор пункта меню можно вообще ничего не проверять: если модель реагирует на вызов какого-то созданного ей пункта, значит, это пункт «».
Теперь можно добавить в модель реакцию на выбор пункта, точно так же, как мы делали это для контекстного меню ранее. На вкладке «» в разделе «» дважды щелкнем на подразделе «» (см. рис. 460) и введем на открывшейся вкладке «» следующий текст:
// У нас - единственный пункт меню double speed,msec; // Вспомогательные переменные char buffer[100]; // Буфер для сообщения пользователю if(rdsCalcProcessIsRunning()) // Расчет работает EndTick=GetTickCount(); // Текущее время else if(rdsCalcProcessNeverStarted()) { // Расчет остановлен и ни разу не запускался rdsMessageBox("Расчет еще не запускался","Статистика", MB_OK|MB_ICONWARNING); return; } // Расчет работает или остановлен, в EndTick – конечное время msec=EndTick-StartTick; // Прошло миллисекунд if(msec==0) // Невозможно вычислить скорость return; speed=Num/(msec/1000.0); // Скорость в тактах/сек // Формирование и вывод сообщения sprintf(buffer,"Скорость расчета: %d тактов/сек",(int)speed); rdsMessageBox(buffer,"Статистика",MB_OK|MB_ICONWARNING);
Сначала мы вызываем функцию rdsCalcProcessIsRunning, чтобы узнать, работает ли сейчас расчет. Если он работает, мы записываем текущее время в переменную EndTick. В противном случае, то есть если расчет не работает, мы вызываем функцию rdsCalcProcessNeverStarted, чтобы узнать, запускался ли расчет хотя бы один раз. Если расчет ни разу не запускался (функция вернула TRUE), мы не можем вычислить статистику, о чем функцией rdsMessageBox выводится сообщение пользователю, и реакция завершается. Если же расчет запускался, значит, он был остановлен, и в реакции на остановку в переменную EndTick уже было записано время конца расчета. Таким образом, в переменной EndTick будет записано либо текущее время, если расчет сейчас работает, либо время остановки, если он был остановлен ранее. В обоих случаях мы можем вычислить скорость расчета.
Чтобы вычислить скорость, сначала мы определяем общее время расчета в миллисекундах – это разность между EndTick и StartTick. Если эта разность равна нулю, мы не можем вычислить скорость, и реакция завершается (впрочем, такая ситуация маловероятна: для этого пользователь должен успеть вызвать пункт меню в той же миллисекунде, в которой запущен расчет). Далее мы вычисляем скорость, разделив число выполненных тактов на время расчета, при помощи функции sprintf формируем текст сообщения и показываем его пользователю вызовом rdsMessageBox.
Теперь можно скомпилировать модель, и в главном меню появится новый пункт «», рядом с которым будет указано вызывающее его сочетание клавиш (рис. 464). Если запустить расчет и выбрать этот пункт или нажать Ctrl+Alt+S, на экране должно появиться сообщение с вычисленной скоростью расчета.
Рис. 464. Добавленный пункт главного меню
В этом примере мы добавляем пункт в главное меню, но не удаляем его: он будет удален автоматически при отключении модели от блока (например, при удалении блока). Кроме того, при создании пункта мы нигде не запомнили уникальный идентификатор, который возвращает функция rdsRegisterMenuItem. Если бы мы хотели как-то изменять пункт меню (например, запрещать его, или включать возле него галочку), нужно было бы запомнить этот идентификатор и передавать его в соответствующие функции RDS, описанные в А.5.18 приложений. Этот идентификатор потребовался бы и в том случае, если бы мы захотели удалить созданный пункт вручную.