Текущий раздел:

Описания RDS

Руководство программиста

Глава 2. Создание моделей блоков

§2.12. Реакция блоков на действия пользователя

§2.12.8. Реакция на действия пользователя при редактировании схемы

# Руководство программиста

## Глава 2. Создание моделей блоков

### §2.12. Реакция блоков на действия пользователя

#### §2.12.8. Реакция на действия пользователя при редактировании схемы

Рассматриваются различные реакции модели на действия пользователя, связанные с редактированием схемы: добавлением и удалением блоков, изменением их размера и т.п. В рассмотренные ранее примеры добавляются новые функции, влияющие на редактирование схемы пользователем.

Реакция модели блока на действия пользователя в режиме редактирования существенно отличается от реакции в режимах моделирования и расчета. В двух последних режимах пользователь управляет работой схемы – нажимает кнопки, перемещает рукоятки и т.п., и реакции моделей, предусмотренные в RDS, ориентированы именно на эти действия. В режиме редактирования же пользователь собирает схему: добавляет и удаляет блоки, перемещает их, изменяет их параметры и названия. Для того, чтобы облегчить пользователю редактирование схемы и, при необходимости, частично автоматизировать его работу, модель блока может реагировать на эти действия.

Мы уже рассматривали некоторые реакции на действия пользователя в режиме редактирования, в том числе и используемый практически в каждом более-менее сложном блоке вызов функции настройки RDS_BFM_SETUP. Реакции на вывод всплывающей подсказки RDS_BFM_POPUPHINT, открытие контекстного меню блока RDS_BFM_CONTEXTPOPUP и выбор пункта меню RDS_BFM_MENUFUNCTION вызываются в любом режиме RDS, поэтому их тоже можно отнести к реакциям в режиме редактирования, тем более, что они чаще всего используются именно для облегчения работы пользователя при сборке и настройке схемы. Также нами уже рассмотрена реакция на выбор блока мышью RDS_BFM_MOUSESELECT (см. §2.12.3), позволяющая определить, относится ли конкретная точка внутри описывающего прямоугольника блока к изображению этого блока и может ли он быть выбран щелчком по этой точке. В RDS предусмотрено еще несколько реакций, которые мы здесь и рассмотрим.

В §2.12.7 мы создали блок, добавляющий в системное меню RDS пункт для открытия окна родительской подсистемы, причем название пункта и его «горячая клавиша» задаются в настройках блока. Если записать этот блок в библиотеку, пользователь сможет легко «вытаскивать» его оттуда и добавлять в важные подсистемы, которые он хотел бы быстро открывать нажатием какого-либо сочетания клавиш. Однако, в библиотеке блок будет записан с какими-то конкретными настройками, поэтому пользователю придется после каждой вставки блока из библиотеки открывать окно его настроек и менять название пункта меню и связанное с ним сочетание клавиш. Было бы логично автоматически открывать окно настроек блока при добавлении его в схему. Воспользуемся для этого специальной реакцией RDS RDS_BFM_MANUALINSERT, которая вызывается сразу после того, как пользователь загрузил в схему новый блок из файла, из библиотеки или вставил его из буфера обмена (заметим, что эта реакция не вызывается, если блок появляется в схеме при добавлении в нее подсистемы, внутри которой он содержится).

Добавим в оператор switch внутри функции модели OpenSysWin новый оператор case:

          // Добавление блока пользователем
          case RDS_BFM_MANUALINSERT:
            if(data->Setup()) // Параметры изменены
              rdsSetModifiedFlag(TRUE); // Взвести флаг изменений
            break;

Теперь, если блок будет добавлен в схему, модель вызовет функцию Setup класса личной области данных блока, которая открывает окно настройки и возвращает ненулевое значение, если параметры в нем были изменены. При этом вызовется сервисная функция RDS rdsSetModifiedFlag с параметром TRUE, которая взведет флаг наличия изменений в схеме. В реакции RDS_BFM_SETUP нам не приходилось взводить этот флаг вручную: при возврате функцией модели, вызванной для этой реакции, ненулевого значения RDS автоматически взводил этот флаг и предупреждал пользователя, если он хотел завершить работу со схемой, не сохранив изменения. В реакции RDS_BFM_MANUALINSERT это не предусмотрено, поэтому флаг приходится взводить при помощи сервисной функции.

Проверяя работу измененной модели блока, можно заметить в ней один недостаток. Если пользователь выделит несколько таких блоков, скопирует их в буфер обмена, а затем вставит его содержимое в какую-либо подсистему, окно настройки откроется столько раз, сколько блоков было в буфере обмена. Реакция RDS_BFM_MANUALINSERT будет вызвана для каждого вставляемого блока, и каждый раз функция Setup будет открывать окно настройки. Можно не обращать внимания на этот недостаток, поскольку в подсистеме не должно быть более одного такого блока – помещать в подсистему несколько блоков, открывающих ее окно, бессмысленно. Однако, можно и исправить его, проанализировав поля структуры RDS_MANUALINSERTDATA, указатель на которую передается в ExtParam при реакции на событие RDS_BFM_MANUALINSERT. Эта структура описана в «RdsDef.h» следующим образом:

      typedef struct
      { int Reason;  // Причина добавления: загрузка из файла
                     // (RDS_LS_LOADFROMFILE) или из буфера
                     // обмена (RDS_LS_LOADCLIPBRD)
        BOOL Single; // Добавляется единственный блок
      } RDS_MANUALINSERTDATA;
      typedef RDS_MANUALINSERTDATA *RDS_PMANUALINSERTDATA;

Нам нужно проверить значение поля Single и открывать окно настроек только в том случае, если оно истинно (TRUE). Если оно ложно, значит, в схему добавляется сразу несколько блоков, и окно открывать не нужно. В приведенный выше оператор case необходимо внести следующие изменения:

          // Добавление блока пользователем
          case RDS_BFM_MANUALINSERT:
            if( ((RDS_PMANUALINSERTDATA)ExtParam)->Single )
              { if(data->Setup()) // Параметры изменены
                  rdsSetModifiedFlag(TRUE);// Взвести флаг изменений
              }
            break;

Теперь вызов функции настройки заключен внутрь оператора if, который проверяет значение поля Single переданной структуры, предварительно приведя ExtParam к правильному типу.

Кроме добавления блоков в схему иногда имеет смысл реагировать на их удаление, например, чтобы предупредить пользователя об изменениях работы схемы, которые удаление этого блока может повлечь. Для этого используется реакция RDS_BFM_MANUALDELETE – RDS вызывает модель блока в этом режиме непосредственно перед удалением блока из схемы по команде пользователя (при удалении подсистемы в этом режиме также вызываются модели всех ее внутренних блоков). Следует помнить, что модель, реагируя на этот вызов, не может отменить удаление блока – оно будет выполнено, независимо от того, что будет делать функция модели в этой реакции и какое значение она вернет.

Не следует путать реакции RDS_BFM_MANUALDELETE и RDS_BFM_CLEANUP. Первая вызывается только при удалении блока пользователем вручную, вторая же – при любом отключении модели от блока. В реакции RDS_BFM_CLEANUP невозможно определить, по какой причине отключается модель: из-за удаления блока, из-за подключения к нему другой функции модели, при закрытии RDS и т.д.

Рассмотрим пример, в котором будем реагировать на удаление блока. В §2.6.4 мы создали пару блоков, один из которых перемещается в окне подсистемы, а второй задает скорость и направление его движения через пару динамических переменных. Если удалить из подсистемы блок-задатчик, подвижный блок прекратит работу, поскольку не сможет получать значения скорости и направления. Будем предупреждать об этом пользователя, выводя ему текстовое сообщение.

Прежде, чем предупреждать пользователя о последствиях удаления этого блока, необходимо проверить, подписан ли какой-либо другой блок на созданные им динамические переменные. Если переменными никто не пользуется, блок можно удалять без предупреждения – на другие блоки это не повлияет. Мы будем подсчитывать число подписчиков на каждую динамическую переменных блока. Один подписчик всегда будет обнаружен – это сам создавший переменные блок. Если число подписчиков окажется большим или равным двум, значит, на переменные подписан кто-то еще, и об этом необходимо предупредить пользователя.

В RDS нет сервисной функции, определяющей число подписчиков на динамическую переменную, однако, есть функция rdsEnumDynVarSubscribers, перечисляющая всех подписчиков (то есть вызывающая для каждого блока-подписчика указанную функцию обратного вызова). Ей мы и воспользуемся. Сначала напишем функцию обратного вызова – она должна иметь следующий формат:

      BOOL RDSCALL имя_функции(
        RDS_BHANDLE block,    // Блок-подписчик
        RDS_PDYNVARLINK link, // Связь с переменной
        LPVOID ptr);          // Дополнительный параметр

В первом параметре функции передается идентификатор обнаруженного блока-подписчика, во втором – указатель на структуру подписки на динамическую переменную, подписчики которой перечисляются, в третьем – дополнительный указатель произвольного типа, который передается в функцию rdsEnumDynVarSubscribers при ее вызове и переносится в функцию обратного вызова без изменений. Мы в этом третьем параметре будем передавать указатель на целое число, которое функция обратного вызова будет увеличивать на единицу для каждого подписчика – таким образом, мы сможем их сосчитать. Возвращаемое функцией обратного вызова значение указывает на необходимость остановить перебор подписчиков (FALSE) или продолжить его (TRUE). Нам не нужно останавливать перебор, поэтому мы всегда будем возвращать TRUE.

Функция обратного вызова будет иметь следующий вид:

      // Функция обратного вызова для подсчета подписчиков динамической переменной
      BOOL RDSCALL DynVarUsersCountEnum(RDS_BHANDLE /*block*/,
          RDS_PDYNVARLINK /*link*/,LPVOID ptr)
      { // Приводим ptr к типу "указатель на целое"
        int *pCount=(int*)ptr;
        (*pCount)++; // Увеличиваем счетчик
        return TRUE;
      }
      //=========================================

Теперь напишем функцию, которая, пользуясь DynVarUsersCountEnum, вернет число подписчиков на заданную переменную. Она будет совсем простой:

      // Подсчет числа подписчиков на динамическую переменную
      int CountDynVarUsers(RDS_PDYNVARLINK link)
      { int count=0;
        // Перебрать всех подписчиков переменной link
        rdsEnumDynVarSubscribers(link,DynVarUsersCountEnum,&count);
        return count;
      }
      //=========================================

Используемая нами сервисная функция rdsEnumDynVarSubscribers принимает три параметра. Первый – указатель на структуру подписки на переменную, подписчиков которой нужно перечислить. Второй – функция обратного вызова (точнее, указатель на нее), которую нужно вызвать для каждого подписчика. Третий – указатель произвольного типа, который без изменений передается в третий параметр функции обратного вызова. В нашем случае это указатель на целую переменную – счетчик подписчиков. После перебора всех подписчиков в переменной count будет находиться их число, которое функция и возвращает.

Теперь внесем изменения в модель. В оператор switch функции модели задатчика TestBlkMoveSetter необходимо добавить новый оператор case:

            // Удаление блока пользователем
            case RDS_BFM_MANUALDELETE:
              // Приведение указателя на личную область данных к
              // правильному типу
              privdata=(TestBlkMoveSetterData*)(BlockData->BlockData);
              // Если блок удаляется в составе подсистемы,
              // предупреждать не нужно
              if( ((RDS_PMANUALDELETEDATA)ExtParam)->WithSys)
                break;
              // Считаем число подписчиков на переменные этого блока
              if(CountDynVarUsers(privdata->VLink)>1 ||
                 CountDynVarUsers(privdata->ALink)>1)
                // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
                //   в противном случае необходимо использовать версии функций
                //   с суффиксом "W" и символьные константы с префиксом "L"
                rdsMessageBox("Внимание! Этот блок предоставляет "
                              "данные другим блокам. Его удаление "
                              "приведет к их отключению.",
                              BlockData->BlockName,
                              MB_OK | MB_ICONWARNING);
              break;

Прежде всего, мы проверяем, что удаляется: сам блок-задатчик или вся подсистема, в которой он содержится. В последнем случае вместе с ним будут удалены и все блоки, которые могли пользоваться его динамическими переменными (они создаются задатчиком в родительской подсистеме), поэтому никаких предупреждений выводить не нужно. Для этой проверки анализируется структура RDS_MANUALDELETEDATA, указатель на которую передается в ExtParam в режиме RDS_BFM_MANUALDELETE. Эта структура описана в «RdsDef.h» следующим образом:

      typedef struct
      { BOOL Single;  // Удаляется один блок (TRUE)
                      // или несколько (FALSE)
        BOOL WithSys; // Удаляется внутри подсистемы (TRUE)
                      // или самостоятельно, то есть один или в группе
                      // выделенных блоков (FALSE)
      } RDS_MANUALDELETEDATA;
      typedef RDS_MANUALDELETEDATA *RDS_PMANUALDELETEDATA;

Нас в этой структуре интересует поле WithSys, значение TRUE которого указывает на удаление блока в составе подсистемы. Если это так, реакция завершается оператором break. В противном случае подсчитывается число подписчиков на две созданные блоком динамические переменные, и, если хотя бы одно из этих чисел больше единицы (то есть на переменную подписан кто-то кроме создавшего ее блока), выводится сообщение пользователю.

Еще одно событие, реакцию на которое достаточно часто включают в модели блоков, это изменение размеров блока. На самом деле, это не одна, а две реакции. Одна из них – RDS_BFM_RESIZE – вызывается после изменения размеров блока, другая – RDS_BFM_RESIZING – при изменении размеров блока мышью в процессе перетаскивания пользователем одного из восьми прямоугольных маркеров выделения. Реакцию RDS_BFM_RESIZE мы уже использовали при создании блока-графика для того, чтобы заново вычислить координаты рабочего поля графика при изменении размера всего блока. Эта реакция вызывается один раз после изменения размеров блока, независимо от того, каким способом это изменение размеров было выполнено: перетаскиванием маркеров выделения, вводом точных значений ширины и высоты в окне параметров блока или значения масштабного коэффициента для блоков с векторной картинкой. Реакция RDS_BFM_RESIZING вызывается только при перетаскивании маркеров выделения блока, то есть при изменении размеров блока мышью, причем вызывается она каждый раз при перемещении курсора. В обеих реакциях модель может скорректировать задаваемый пользователем размер, при этом в RDS_BFM_RESIZING пользователь будет сразу же видеть это изменение, поскольку оно отразится на размере перетаскиваемого мышью прямоугольника.

Изменим модель блока, имитирующего двухкоординатную рукоятку так, чтобы блок всегда оставался квадратным. При этом масштаб блока по горизонтали и вертикали будет одинаковым, то есть горизонтальное и вертикальное перемещение рукоятки на одну и ту же величину будет приводить к одинаковому изменению выходных переменных.

Прежде всего добавим в класс личной области данных блока новую функцию для реакции на изменение размеров блока:

      void Resizing(RDS_PRESIZEDATA ResData);

Эта функция будет принимать один параметр – указатель на структуру RDS_RESIZEDATA, который передается в функцию модели в параметре ExtParam и в режиме RDS_BFM_RESIZE, и в режиме RDS_BFM_RESIZING. Структура описана в «RdsDef.h» следующим образом:

      typedef struct
      { BOOL HorzResize;        // Изменение размера по горизонтали
        BOOL VertResize;        // Изменение размера по вертикали
        int newWidth,newHeight; // Новые значения ширины и высоты
        // Данные о сетке редактора
        int GridDx,GridDy;      // Шаг сетки
        BOOL SnapToGrid;        // Включена привязка к сетке
      } RDS_RESIZEDATA;
      typedef RDS_RESIZEDATA *RDS_PRESIZEDATA;

Логические поля структуры HorzResize и VertResize указывают на то, каким именно образом производится изменение размеров блока:

HorzResize | VertResize | Способ изменения
--- | --- | ---
FALSE | FALSE | Новые размеры блока заданы числами в окне параметров блока (кнопка «размер для функции DLL»).
FALSE | TRUE | Пользователь перетащил верхнюю или нижнюю метку масштабирования вверх или вниз (изменена только высота).
TRUE | FALSE | Пользователь перетащил левую или правую метку масштабирования влево или вправо (изменена только ширина).
TRUE | TRUE | Пользователь перетащил одну из угловых меток масштабирования (изменены и ширина, и высота).

В полях newWidth и newHeight содержатся новые значения ширины и высоты блока. Если необходимо скорректировать размер блока, функция модели может перед возвратом изменить эти значения. В нашем случае, чтобы блок всегда оставался квадратным, его ширина и высота должны быть равны. Нужно только определить, как правильно изменить значения ширины и высоты. Если пользователь перетаскивает левый или правый маркер выделения, он, тем самым, задает ширину блока – в этом случае нужно сделать высоту блока, равной ширине. Если он перетаскивает верхний или нижний маркер, он задает высоту, при этом нужно сделать ширину равной высоте. Если же он перетаскивает один из угловых маркеров, задавая одновременно оба размера блока, или размеры введены в окне параметров, проще всего присвоить ширине и высоте среднее арифметическое заданных пользователем размеров, при этом поведение блока будет выглядеть более-менее естественным.

Реализуем описанный выше алгоритм в функции:

      // Реакция на изменение размеров блока
      void TSimpleJoystick::Resizing(RDS_PRESIZEDATA ResData)
      {
        if(ResData->HorzResize && (!ResData->VertResize))
          // Перетаскивается левый или правый маркер
          ResData->newHeight=ResData->newWidth;
        else if((!ResData->HorzResize) && ResData->VertResize)
          // Перетаскивается верхний или нижний маркер
          ResData->newWidth=ResData->newHeight;
        else // Перетаскивается угловой маркер или размер задан точно
          { // Вычисляем среднее арифметическое
            int avg=(ResData->newWidth+ResData->newHeight)/2;
            // Присваиваем его ширине и высоте
            ResData->newWidth=ResData->newHeight=avg;
          }
      }
      //=========================================

Теперь осталось добавить вызов этой функции в функцию модели блока SimpleJoystick. Внутрь оператора switch нужно вставить следующий case:

            // Изменение размеров блока
            case RDS_BFM_RESIZE:
            case RDS_BFM_RESIZING:
              data->Resizing((RDS_PRESIZEDATA)ExtParam);
              break;

Здесь мы вызываем функцию Resizing в реакциях на оба события, связанных с изменением размера блока. Дело в том, что если вызывать ее только в реакции на RDS_BFM_RESIZE, блок будет всегда оставаться квадратным, но у пользователя при перетаскивании маркеров выделения не будет визуальной обратной связи. Если же вызывать ее только в RDS_BFM_RESIZING, визуальная обратная связь будет, но при вводе размеров блока в окне параметров они не будут скорректированы моделью, поскольку RDS_BFM_RESIZING при этом не вызывается. Таким образом, корректировать заданные пользователем размеры необходимо в обеих реакциях.

Теперь легальными способами пользователь никак не сможет сделать блок-рукоятку не квадратным. На самом деле, эту функцию следовало бы сделать отключаемой, но мы не стали делать этого, чтобы не усложнять пример.

В RDS предусмотрены и другие реакции моделей блоков на действия пользователя, связанные с редактированием схемы. Они применяются значительно реже, и мы не будем приводить примеры их использования. Просто перечислим их с кратким указанием возможного применения:

- Реакция на перемещение блока RDS_BFM_MOVED. Может применяться, например, для синхронного перемещения группы блоков при перемещении одного из них, или для сообщения новых координат блока управляющей программе, если такая есть.
- Реакция на переименование блока RDS_BFM_RENAME. Может применяться для синхронного с блоком переименования каких-либо связанных с блоком объектов (например, динамических переменных), имена которых формируются из имени блока.
- Реакция на загрузку схемы RDS_BFM_AFTERLOAD, вызываемая у всех блоков схемы сразу после ее загрузки в память. Может применяться для инициализации общесистемных переменных и объектов, которую, по каким-либо причинам, неудобно проводить при подключении модели к блоку.
- Реакция на завершение работы со схемой RDS_BFM_UNLOADSYSTEM, то есть на ее выгрузку из памяти. Может применяться, например, для удаления каких-либо временных файлов, созданных для работы с этой схемой.
- Пара реакций на сохранение всей схемы: одна из них (RDS_BFM_BEFORESAVE) вызывается до сохранения, другая (RDS_BFM_AFTERSAVE) – после. Могут применяться, например, для преобразования файла схемы в какой-либо другой формат.