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

Описания RDS

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

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

§2.13. Вызов функций блоков

§2.13.2. Прямой вызов функции одного блока

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

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

### §2.13. Вызов функций блоков

#### §2.13.2. Прямой вызов функции одного блока

Рассматривается вызов функции блока, идентификатор которого известен вызывающей модели. Приводятся примеры использования функции «Common.ControlValueChanged», которая используется блоками пользовательского интерфейса из библиотеки «Common.dll» для общения между собой. В двухкоординатную рукоятку и блок увеличения/уменьшения, созданные ранее, добавляется поддержка этой функции, что позволяет соединять их со стандартными полями ввода и синхронно изменять значения блоков в режиме моделирования.

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

Рис. 84. Кольцевое соединение
блока с полем ввода

В §2.12.1 мы создали блок, уменьшающий и увеличивающий значение своего целого выхода при щелчках мыши и нажатиях клавиш. Этот блок вполне справляется со своей работой, однако, пользователь обычно хочет не только увеличивать и уменьшать какое-то значение, но и иметь возможность ввести его с клавиатуры. Самое очевидное решение этой проблемы – добавить в переменные блока целый вход, значение которого без изменений передается на выход, и соединить его со стандартным полем ввода. Выход нашего блока также необходимо соединить со входом поля ввода, замкнув их в кольцо (рис. 84). При таком соединении ввод числа в поле ввода будет приводить к попаданию нового значения на вход нашего блока и, следовательно, к изменению его выхода, а изменения значения выхода нашего блока при щелчках мыши или нажатии клавиш будет приводить к изменению значения в поле ввода. Таким образом, значение в поле ввода и выход нашего блока будут изменяться синхронно.

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

Рис. 85. Кольцо из трех полей ввода

Модели всех стандартных органов управления (рукояток, полей ввода и т.п.) из библиотеки «Common.dll» поддерживают функцию «Common.ControlValueChanged», специально предназначенную для взаимодействия элементов интерфейса пользователя друг с другом. Если пользователь меняет значение в одном из таких интерфейсных блоков, модель этого блока принудительно передает данные выходов по связям, даже если RDS находится на в режиме расчета (для этого используется специальная сервисная функция rdsActivateOutputConnections), после чего вызывает во всех блоках, с которыми соединены выходы данного, функцию «Common.ControlValueChanged». У этой функции нет параметров, блоки должны считать измененное пользователем значение со своих входов. Таким образом, если несколько полей ввода соединены связями в кольцо, когда выход каждого из них соединен со входом другого, как на рис. 85, изменение пользователем значения в поле ввода A вызовет принудительную передачу этого значения по связи на вход соединенного с ним поля B, и вызов в нем функции «Common.ControlValueChanged». Реагируя на вызов функции, поле ввода B должно изменить свое значение согласно своему входу, снова принудительно передать значение своего выхода по связям, и снова вызвать у соединенных блоков, то есть у поля ввода C, функцию «Common.ControlValueChanged». Таким образом, введенное пользователем значение будет распространяться по цепочке соединенных полей ввода. Если эта цепочка замкнута в кольцо, как на рисунке, в конце концов значение вернется к полю ввода A, изменение значения в котором и привело к цепочке вызовов функции у соединенных с ним блоков. У этого поля тоже будет вызвана функция «Common.ControlValueChanged», и здесь очень важно не допустить повторного вызова всей цепочки – если, реагируя на вызов функции, поле ввода A снова передаст значение полю ввода B, вызовы функции по кольцу будут продолжаться бесконечно, точнее, до тех пор, пока не переполнится стек и не произойдет аварийное завершение RDS. Для предотвращения этого проще всего ввести в каждый блок специальный флаг, который взводится перед вызовом «Common.ControlValueChanged» у соседних блоков и сбрасывается после этого вызова. Если, реагируя на вызов функции, модель блока обнаружит этот флаг взведенным, значит, этот блок уже участвовал в цепочке вызовов, и передавать вызов дальше не нужно. В этом случае ввод пользователем числа в поле ввода A приведет к следующей последовательности вызовов:

Действие | Флаг поля A | Флаг поля B | Флаг поля C
--- | --- | --- | ---
Исходное состояние блоков. | – | – | –
Пользователь ввел число в поле A, модель поля A, реагируя на его действия, взвела флаг и вызывает модель поля B. | ✓ | – | –
Модель поля B приняла новое значение, взвела флаг и вызывает модель поля C. | ✓ | ✓ | –
Модель поля C приняла новое значение, взвела флаг и вызывает модель поля A. | ✓ | ✓ | ✓
Модель поля A обнаружила, что флаг у ее блока взведен, и не стала реагировать на функцию. Управление возвращается вызвавшей модели, то есть модели поля C. | ✓ | ✓ | ✓
Модель поля C сбрасывает свой флаг и завершает реакцию на функцию. Управление возвращается модели поля B. | ✓ | ✓ | –
Модель поля B сбрасывает свой флаг и завершает реакцию на функцию. Управление возвращается модели поля A. | ✓ | – | –
Модель поля A сбрасывает свой флаг и завершает реакцию на действия пользователя. | – | – | –

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

Изменим модель и параметры нашего блока таким образом, чтобы его можно было подключать к полю ввода (или другому интерфейсному блоку) как на рис. 84. Прежде всего в переменные блока необходимо добавить целый вход «input», на который будет подаваться значение с поля ввода. Для этого входа необходимо установить флаг запуска – при срабатывании связи, подключенной к нему, модель блока должна будет передать значение на выход. Новая структура переменных блока будет иметь следующий вид:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | v | int | 4 | Выход |  | 0
6 | input | int | 4 | Вход | ✓ | 0

Для работы с функцией «Common.ControlValueChanged» введем целую глобальную переменную ControlValueChangedId, в нее при регистрации функции будет записываться присвоенный ей уникальный идентификатор. Функцию мы будем регистрировать при инициализации блока, причем только в том случае, если она еще не зарегистрирована, поэтому дадим этой глобальной переменной нулевое значение:

      // Глобальная переменная для уникального идентификатора функции
      // "Common.ControlValueChanged"
      int ControlValueChangedId=0;

Для того, чтобы реализовать задуманное, нам потребуется функция, принудительно передающая значение выхода блока по связям, после чего вызывающая «Common.ControlValueChanged» у всех соединенных блоков. Мы будем вызывать ее при изменении выхода блока из-за действий пользователя (щелчков мыши или нажатия клавиш), а также при реакции нашего блока на «Common.ControlValueChanged», чтобы новое значение передавалось по цепочке от блока к блоку. Эту функцию мы не будем делать членом класса личной области данных нашего блока. Вместо этого сделаем ее глобальной – она может потребоваться нам и в других блоках.

Для принудительной передачи выхода блока по связям мы будем использовать сервисную функцию RDS rdsActivateOutputConnections. Она принимает два параметра, первый из которых – идентификатор блока (RDS_BHANDLE), данные которого необходимо передать. Если вместо идентификатора блока указать NULL, будут переданы данные того блока, модель которого в данный момент выполняется. Второй параметр функции – логическое значение, указывающее на способ передачи данных. Если во втором параметре передается TRUE, при передаче, как и в режиме расчета, учитываются логические и сигнальные переменные, привязанные ко входам и выходам блока:

- данные выходов не будут передаваться, если выход готовности блока (вторая сигнальная переменная, «Ready») будет иметь нулевое значение;
- если выход имеет связанную логическую переменную, его значение будет передано по связи только при ненулевом значении этой переменной;
- если для входа, к которому подсоединена связь, в структуре переменных установлен флаг «пуск», первая сигнальная переменная («Start») принявшего данные блока автоматически получит значение 1;
- если у входа, к которому присоединена сработавшая связь, есть связанная сигнальная переменная, она тоже автоматически получит значение 1.

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

Для того, чтобы после передачи данных по связям вызвать во всех получивших данные блоках «Common.ControlValueChanged», мы будем использовать сервисную функцию RDS rdsEnumConnectedBlocks. Она позволяет вызвать для каждого блока, соединенного связями с заданным, специально написанную функцию обратного вызова, указатель на которую передается в rdsEnumConnectedBlocks в одном из параметров. Наша задача – написать эту функцию обратного вызова таким образом, чтобы она вызывала в каждом блоке «Common.ControlValueChanged».

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

      BOOL RDSCALL имя_функции(
        RDS_PPOINTDESCRIPTION nearpoint, // Точка связи данного блока
        RDS_PPOINTDESCRIPTION farpoint,  // Точка связи "соседа"
        LPVOID ptr);                     // Дополнительный параметр

В первых двух параметрах функции передаются указатели на структуры описания точки связи RDS_POINTDESCRIPTION, соединенной с заданным блоком (первый параметр) и с блоком на другом конце связи (второй параметр). Эти структуры содержат идентификатор блока, имя переменной, к которой подходит связь, идентификатор связи, которой принадлежит точка, и т.п. Фактически, функция обратного вызова вызывается не для каждого блока, а для каждой точки связи, которая соединяет эту связь с каким-либо входом или выходом блока. Например, если связь разветвляется и соединяется с двумя входами одного и того же блока, функция обратного вызова будет вызвана дважды: один раз для первого входа, другой раз – для второго. Нас будет интересовать только идентификатор блока из структуры, переданной во втором параметре функции – именно у этого блока, находящегося на другом конце связи от нашего, мы должны будем вызвать «Common.ControlValueChanged». Имя переменной, к которой подходит связь, нас, в данном случае, не волнует.

Третий параметр функции обратного вызова – указатель произвольного типа (void*, или, в определениях Windows API, LPVOID), передаваемый в одном из параметров rdsEnumConnectedBlocks. Он никак не обрабатывается RDS и обычно используется для передачи в функцию обратного вызова каких-либо дополнительных параметров. Возвращаемое функцией обратного вызова логическое значение позволяет остановить перебор блоков-соседей, если это необходимо: возврат FALSE прекращает перебор, возврат TRUE продолжает его. Нем необходимо перебрать все соединенные блоки, поэтому в нашей функции обратного вызова мы будем всегда возвращать TRUE.

Функция rdsEnumConnectedBlocks позволяет перебрать все блоки, соединенные с выходами заданного, с его входами, или и те, и другие:

      RDS_BHANDLE RDSCALL rdsEnumConnectedBlocks(
        RDS_BHANDLE Block,     // Заданный блок или NULL
        DWORD Flags,           // Флаги, управляющие перебором
        RDS_BhPdPdpV CallBack, // Функция обратного вызова
        LPVOID Data);          // Дополнительный параметр

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

- RDS_BEN_INPUTS – если флаг взведен, будут перебираться блоки, связи от которых идут ко входам заданного блока;
- RDS_BEN_OUTPUTS – если флаг взведен, будут перебираться блоки, связи к которым идут от выходов заданного блока;
- RDS_BEN_TRACELINKS – если флаг взведен, функция обратного вызова не будет вызываться для обнаруженных подсистем, блоков-входов и блоков-выходов. Вместо этого связи будут прослеживаться внутрь и наружу подсистем до простых блоков.

В третьем параметре передается указатель на функцию обратного вызова уже описанного вида, а в четвертом – дополнительный параметр, который без изменений передается в третьем параметре функции обратного вызова. Возвращает rdsEnumConnectedBlocks идентификатор блока, для которого функция обратного вызова вернула FALSE, или NULL, если перебраны все блоки и функция обратного вызова каждый раз возвращала TRUE.

Теперь мы можем написать функцию обратного вызова, которая будет уведомлять блоки, соединенные с нашим, о поступлении на их входы нового значения:

      // Функция обратного вызова для "Common.ControlValueChanged"
      BOOL RDSCALL ControlValChanged_Callback(
        RDS_PPOINTDESCRIPTION /*src*/,
        RDS_PPOINTDESCRIPTION dest,
        LPVOID /*data*/)
      { // Вызов функции "Common.ControlValueChanged" у блока на другом конце связи
        rdsCallBlockFunction(dest->Block,ControlValueChangedId,NULL);
        // Возвращаем TRUE – не останавливаем перебор блоков
        return TRUE;
      }
      //=========================================

Из трех параметров функции обратного вызова нам нужен только второй (dest) – указатель на структуру, описывающую точку связи, соединенную с соседним блоком. В параметре Block этой структуры содержится идентификатор соединенного блока, который мы и используем в вызове rdsCallBlockFunction. Во втором параметре rdsCallBlockFunction мы передаем идентификатор вызываемой функции, который должен находиться в глобальной переменной ControlValueChangedId (предполагается, что функция уже зарегистрирована). В третьем параметре должен был бы быть указатель на параметры вызываемой функции, но «Common.ControlValueChanged» не имеет параметров, поэтому мы передаем вместо него NULL.

Теперь напишем функцию, которая будет принудительно передавать по связям данные всех выходов блока, модель которого выполняется в данный момент, и уведомлять об этом все соединенные блоки вызовом «Common.ControlValueChanged». Мы сможем вызывать эту функцию из любого места модели нашего блока – не важно, из самой ли функции модели, или из какой-либо функции-члена класса его личной области данных (функции-члены вызываются из самой функции модели, и параллельно с ней не сможет выполняться никакая другая из-за блокировки данных). Дополнительно мы включим в эту функцию работу с флагом блокировки, принцип действия которого был описан выше. Функция будет иметь следующий вид:

      // Функция уведомления соседей об изменении значения
      void ControlValueChangedCall(BOOL *pCancelCall)
      { // В параметре pCancelCall передается указатель на флаг
        // блокировки вызова функции
        if(*pCancelCall) // Вызов заблокирован
          return;

        // Принудительно передаем данные выходов блока, модель которого
        // сейчас выполняется, по связям
        rdsActivateOutputConnections(NULL,TRUE);

        // Взводим флаг блокировки перед вызовом функций
        *pCancelCall=TRUE;
        // Перебираем все простые блоки, соединенные с выходами текущего
        // (для каждого будет вызвана ControlValChanged_Callback)
        rdsEnumConnectedBlocks(NULL,
                               RDS_BEN_OUTPUTS | RDS_BEN_TRACELINKS,
                               ControlValChanged_Callback,
                               NULL);
        // Сбрасываем флаг блокировки после вызова функций
        *pCancelCall=FALSE;
      }
      //=========================================

В параметре pCancelCall в функцию передается указатель на флаг блокировки вызова, который должен быть выполнен в виде логического (BOOL) поля в классе личной области данных блока. Прежде всего функция проверяет, взведен ли этот флаг, то есть истинно ли значение логической переменной, на которую ссылается переданный указатель. Если это так, функция немедленно завершается – блок, для которого она вызвана, уже участвует в цепочке вызовов «Common.ControlValueChanged», и продолжение работы функции приведет к бесконечной рекурсии. Если флаг сброшен, вызывается функция rdsActivateOutputConnections, которая передает данные выходов блока, из модели которого вызвана функция ControlValueChangedCall (вместо идентификатора блока передано значение NULL), по связям, с учетом логики их работы (второй параметр – TRUE). Теперь нужно уведомить все соединенные блоки о поступлении на их входы новых значений, вызвав в них функцию «Common.ControlValueChanged». Для этого флаг блокировки вызова взводится, чтобы данный блок, для которого вызвана ControlValueChangedCall, не отреагировал на ее повторный вызов, и вызывается функция перебора соединенных блоков rdsEnumConnectedBlocks. В первом параметре функции перебора передается NULL, чтобы она работала с текущим блоком (то есть с тем блоком, для которого вызвана ControlValueChangedCall). Нам нужно перебрать только блоки, соединенные с выходами данного, причем нам нужно прослеживать связи до простых блоков внутрь и наружу подсистем (соединенные поля ввода не обязательно находятся в одной и той же подсистеме), поэтому мы используем флаги RDS_BEN_OUTPUTS и RDS_BEN_TRACELINKS. Для каждого блока будет вызываться уже написанная нами функция ControlValChanged_Callback, в которую мы не передаем никаких дополнительных параметров, поэтому последний параметр rdsEnumConnectedBlocks – NULL. После перебора всех соединенных блоков флаг блокировки вызовов снова сбрасывается, чтобы блок был готов к приему новых значений.

В личную область данных блока необходимо добавить логический флаг для блокировки повторных вызовов, и в конструкторе класса присвоить ему значение FALSE. Кроме того, включим в конструктор блока регистрацию функции «Common.ControlValueChanged» (внесенные изменения выделены цветом):

      //====== Класс личной области данных ======
      class TPlusMinusData
      { public:
          int KeyPlus;       // Клавиша увеличения
          DWORD ShiftsPlus;  // и ее флаги
          int KeyMinus;      // Клавиша уменьшения
          DWORD ShiftsMinus; // и ее флаги

          // Флаг блокировки повторных вызовов
          // функции "Common.ControlValueChanged"
          BOOL NoCall;
          int Setup(void);    // Функция настройки клавиш
          void SaveBin(void); // Сохранение параметров
          int LoadBin(void);  // Загрузка параметров

          // Конструктор класса
          TPlusMinusData(void)
            { KeyPlus=ShiftsPlus=KeyMinus=ShiftsMinus=0;

              NoCall=FALSE; // Исходно флаг блокировки сброшен
              if(ControlValueChangedId==0) // Регистрация функции
                ControlValueChangedId=
                  rdsRegisterFunction("Common.ControlValueChanged");
            };
      };
      //=========================================

Теперь все готово для внесения изменений в функцию модели блока. С учетом того, что структура переменных блока изменилась (мы добавили новый вход), теперь функция будет выглядеть так:

      // Увеличение/уменьшение значения по щелчку и клавишам
      extern "C" __declspec(dllexport)
        int RDSCALL PlusMinus(int CallMode,
                          RDS_PBLOCKDATA BlockData,
                          LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define v (*((RDSINT32 *)(pStart+2*RDS_VSZ_S)))
      #define input (*((RDSINT32 *)(pStart+2*RDS_VSZ_S+RDS_VSZ_I)))
        // Вспомогательная — указатель на структуру события мыши
        RDS_PMOUSEDATA mouse;
        // Вспомогательная — указатель на структуру события клавиатуры
        RDS_PKEYDATA key;
        // Указатель на личную область данных блока,приведенный к
        // правильному типу
        TPlusMinusData *data=(TPlusMinusData*)(BlockData->BlockData);
        switch(CallMode)
          { // Проверка типа статических переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSII}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // Реакция на нажатие кнопки мыши
            case RDS_BFM_MOUSEDOWN:
              // Приведение ExtParam к нужному типу
              mouse=(RDS_PMOUSEDATA)ExtParam;
              if(mouse->Button==RDS_MLEFTBUTTON)
                { // Нажата левая кнопка
                  // Проверяем, есть ли у блока картинка (получаем описание блока)
                  RDS_BLOCKDESCRIPTION descr;
                  descr.servSize=sizeof(descr);
                  rdsGetBlockDescription(BlockData->Block,&descr);
                  if(descr.Flags & RDS_BDF_HASPICTURE)
                    { // Картинка есть – определяем идентификатор
                      // элемента под курсором
                      int id=rdsGetMouseObjectId(mouse);
                      v+=id;
                    }
                  else if(mouse->y<mouse->Top+mouse->Height/2)
                    v++; // В верхней половине блока - увеличиваем
                  else
                    v--; // В нижней половине блока — уменьшаем
                  // Взводим сигнал готовности
                  Ready=1;
                  // Принудительно передаем в соседние блоки
                  ControlValueChangedCall(&(data->NoCall));
                }
              break;

            case RDS_BFM_INIT: // Инициализация
              BlockData->BlockData=new TPlusMinusData();
              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_KEYDOWN:
              // Приведение ExtParam к нужному типу
              key=(RDS_PKEYDATA)ExtParam;
              // Сравнение нажатой клавиши с клавишами уменьшения
              // и увеличения
              if(key->KeyCode==data->KeyPlus &&
                 key->Shift==data->ShiftsPlus)
                { v++; Ready=1; }
              else if(key->KeyCode==data->KeyMinus &&
                      key->Shift==data->ShiftsMinus)
                { v--; Ready=1; }

              // Принудительно передаем в соседние блоки
              if(Ready)
                ControlValueChangedCall(&(data->NoCall));
              break;

            // Такт расчета
            case RDS_BFM_MODEL:
              if(v==input) // Новое значение равно старому
                { Ready=0; // Не передаем по связям
                  break;
                }
              v=input; // Передаем значение входа на выход
              // Принудительно передаем в соседние блоки
              ControlValueChangedCall(&(data->NoCall));
              break;
            // Реакция на вызов функции блока
            case RDS_BFM_FUNCTIONCALL:
              if(((RDS_PFUNCTIONCALLDATA)ExtParam)->Function==
                                         ControlValueChangedId)
                { // Вызвана "Common.ControlValueChanged"
                  if(data->NoCall) // Взведен флаг блокировки
                    break;
                  if(v==input) // Новое значение равно старому -
                    break; // передавать не нужно
                  v=input; // Передаем значение входа на выход
                  Ready=1; // Взводим сигнал готовности
                  // Принудительно передаем в соседние блоки
                  ControlValueChangedCall(&(data->NoCall));
                }
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef input
      #undef v
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Кроме макроопределения для дополнительной переменной и дополнительного символа «I» для нее в строке типа статических переменных, в функции модели изменены реакции на мышь и клавиатуру, а также добавлены две новые реакции: выполнение такта расчета RDS_BFM_MODEL и реакция на вызов функции блока RDS_BFM_FUNCTIONCALL.

После изменения значения выхода блока при нажатии кнопки мыши или одной из заданных в настройках клавиш, мы теперь вызываем функцию ControlValueChangedCall, передавая ей указатель на логический флаг блокировки вызова NoCall из класса личной области данных. Следует обратить внимание на то, что этот вызов производится после присваивания переменной блока Ready значения 1: внутри ControlValueChangedCall мы принудительно передаем значения выхода блока на входы соединенных с ним с учетом логики работы связей, поэтому сигнал готовности блока на момент этой передачи должен быть взведен, иначе логика связей не даст им сработать.

В реакции на такт расчета мы просто копируем значение, поступившее на вход блока input, в его выход v, если оно отличается от текущего значения выхода, и вызываем ControlValueChangedCall, чтобы передать данные соседним блокам и вызвать у них Common.ControlValueChanged. Может возникнуть вопрос: в режиме расчета данные с выхода блока передаются по связям автоматически, так зачем нам предпринимать действия по принудительной передаче этих данных? Дело в том, что, в данном случае, нам лучше передать данные как можно быстрее, а не в конце текущего такта расчета. Представим себе, что, по какой-то причине, на выходе нашего блока и на выходе соединенного с ним поля ввода оказались разные значения, и у обоих блоков взведен сигнал готовности. В этом случае в схеме возникнут колебания: в каждом такте расчета блоки будут обмениваться значениями и взводить сигнал готовности. Принудительная передача данных по связям ликвидирует эти колебания: на момент конца такта, когда происходит обычная передача, значения в блоках уже будут одинаковыми, а сигналы готовности – сброшенными (их сбрасывает rdsActivateOutputConnections после передачи).

В реакции на вызов функции блока мы прежде всего проверяем, какая именно функция вызвана. Для этого мы сравниваем поле Function структуры RDS_FUNCTIONCALLDATA, указатель на которую передан в ExtParam, с глобальной переменной ControlValueChangedId, в которой хранится целый идентификатор, присвоенный функции «Common.ControlValueChanged» при регистрации (мы регистрируем ее в конструкторе класса). Если они совпали, значит, вызвана именно «Common.ControlValueChanged», и нам необходимо принять новое значение со входа и передать его дальше по цепочке соединенных блоков. Сначала проверяется, взведен ли флаг блокировки вызовов data->NoCall: если это так, на вызов функции реагировать нельзя. Если флаг сброшен, мы сравниваем поступившее на вход значение с текущим значением выхода блока: если они совпадают, передавать значение дальше по цепочке не имеет смысла – оно не изменилось. В противном случае мы копируем значение входа в выход, взводим сигнал готовности и вызываем ControlValueChangedCall для принудительной передачи данных дальше по цепочке.

Теперь к нашему блоку можно подключить поле ввода, как на рис. 84, или любой другой блок пользовательского интерфейса. Значения в обоих блоках теперь будут синхронно изменяться и в режиме расчета, и в режиме моделирования.

Если у блока, позволяющего пользователю изменять какие-либо значения, есть несколько независимых выходов, и мы хотим добавить в него поддержку функции «Common.ControlValueChanged», нужно уметь различать, какое именно из значений изменилось. В этом нам могут помочь связанные со входами блока сигналы. Хотя это и не имеет прямого отношения к вызову функций блоков, для большей ясности рассмотрим такую ситуацию на примере.

Рис. 86. Рукоятка с полями ввода

В §2.12.2 мы создали блок, имитирующий рукоятку, позволяющую задавать две независимые координаты, и уже добавили в него несколько дополнительных возможностей. Тем не менее, у блока остался один, достаточно серьезный, недостаток: он не отображает точных значений координат, соответствующих текущему положению рукоятки. Можно подключить к блоку числовые индикаторы, как на рис. 75, но они будут показывать значения выходов блока только в режиме расчета. Если пользователь захочет остановить расчет, изменить значения координат, а затем запустить расчет заново, ему придется двигать рукоятку наугад. Кроме того, блок не позволяет вводить точные значения координат с клавиатуры. Напрашивается очевидное решение: добавить в блок рукоятки поддержку функции «Common.ControlValueChanged» и подключить к нему пару полей ввода – по одному полю на координату, как на рис. 86). На рисунке в блок добавлены два дополнительных входа: «x_in» для связи с полем ввода «x» и «y_in» для связи с полем ввода «y».

Когда мы будем добавлять в блок реакцию на вызов функции, очень важно отличать ее вызов из-за изменения «x_in» от вызова из-за изменения «y_in». Если, реагируя на вызов, считывать обе переменные одновременно, поведение блока будет неправильным. Допустим, пользователь передвинул рукоятку из положения (0.1,0.3) в положение (0.5,0.7). Рассмотрим, что будет происходить в схеме, если мы не различаем изменения переменных в реакции на «Common.ControlValueChanged», при подключении к блоку полей ввода, как на рисунке:

1. Значения выходов блока-рукоятки изменились: x=0.5, y=0.7.
2. Изменившиеся значения x и y передались на входы одноименных полей ввода при помощи вызова rdsActivateOutputConnections.
3. Как и в предыдущем примере, мы начали перебирать соединенные с выходами рукоятки блоки при помощи функции rdsEnumConnectedBlocks, вызывая у каждого из них «Common.ControlValueChanged». Допустим, первым обнаруженным блоком оказалось поле ввода «x».
4. Поле ввода «x», реагируя на вызов функции, считало значение 0.5 со своего входа, скопировало его на свой выход, передало его по связи на вход рукоятки x_in, и вызвало у нее функцию «Common.ControlValueChanged».
5. Рукоятка, реагируя на вызов функции, считала значения своих входов x_in и y_in, и переписала их в выходы x и y. Вход x_in имеет правильное значение 0.5, полученное с поля ввода «x», но вход y_in до сих пор имеет старое значение 0.3, поскольку поле ввода «y» еще не вызывалось, и поэтому не успело передать поступившее на его вход значение 0.7 по связи в рукоятку.

Дальнейшие действия можно уже не рассматривать: значение выхода y=0.7, заданное пользователем, может быть потеряно из-за того, что блок-рукоятка, реагируя на вызов функции, поступивший от поля ввода «x», считал не только значение x_in, но и y_in. Избежать возникновения подобных проблем можно связав входы x_in и y_in с внутренними переменными-сигналами (назовем их «x_in_ok» и «y_in_ok» соответственно). Поскольку при вызове rdsActivateOutputConnections мы не отключаем логику работы связей (второй параметр функции – TRUE), эти сигналы автоматически взведутся при срабатывании связи, подключенной к соответствующему входу. Так мы сможем узнать, какая из переменных изменилась.

Таким образом, блок-рукоятка теперь будет иметь следующую структуру переменных:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | x | double | 8 | Выход |  | 0
10 | y | double | 8 | Выход |  | 0
18 | x_in | double | 8 | Вход/сигнал x_in_ok | ✓ | 0
26 | y_in | double | 8 | Вход/сигнал y_in_ok | ✓ | 0
34 | x_in_ok | Сигнал | 1 | Внутренняя |  | 0
35 | y_in_ok | Сигнал | 1 | Внутренняя |  | 0

Как и в предыдущем примере, добавим в класс личной области данных блока флаг блокировки вызова, а в конструктор этого класса – регистрацию функции «Common.ControlValueChanged» (внесенные изменения выделены цветом):

      //====== Класс личной области данных ======
      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;            // Диаметр круга

          // Флаг блокировки повторных вызовов
          // функции "Common.ControlValueChanged"
          BOOL NoCall;
          // Реакция на нажатие кнопки мыши
          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);
          // Реакция на выбор добавленного пункта меню
          void MenuFunction(RDS_PMENUFUNCDATA MenuData);
          // Ограничение входных значений
          void LimitInputValues(double *px_in,double *py_in,double x,double y);
          // Конструктор класса
          TSimpleJoystick(void)
            { BorderColor=0;          // Черная рамка
              FieldColor=0xffffff;    // Белое поле
              HandleColor=0xff0000;   // Синий круг
              MovingHandleColor=0xff; // Красный при таскании
              GrayedColor=0x7f7f7f;   // Серый
              LockX=LockY=FALSE;      // Фиксация выключена
              HandleSize=20;          // Диаметр круга
              // Создание пунктов меню
              MenuLockX=rdsRegisterContextMenuItem("Фиксировать X",1,0);
              MenuLockY=rdsRegisterContextMenuItem("Фиксировать Y",2,0);

              NoCall=FALSE; // Исходно флаг блокировки сброшен
              if(ControlValueChangedId==0)	// Регистрация функции
                ControlValueChangedId=
                     rdsRegisterFunction("Common.ControlValueChanged");
            };

            // Деструктор класса
            ~TSimpleJoystick()
            { // Уничтожение пунктов меню
              rdsUnregisterMenuItem(MenuLockX);
              rdsUnregisterMenuItem(MenuLockY);
            };
      };
      //=========================================

Глобальную переменную ControlValueChangedId для хранения идентификатора функции «Common.ControlValueChanged», полученного при ее регистрации, мы уже ввели в предыдущем примере. Там же мы написали функцию ControlValueChangedCall, которая вызывает «Common.ControlValueChanged» у всех блоков, соединенных с выходами данного. В этом примере она нам тоже понадобится.

Мы также добавили в класс новую функцию LimitInputValues, которую будем использовать для ограничения значений, поступивших на вход блока. Значение каждой из координат рукоятки должно лежать в диапазоне [−1…1]. Кроме того, если одна из координат рукоятки зафиксирована, блок должен игнорировать значение, поступившее на соответствующий вход. Поскольку на входы блока могут поступить любые значения, функция LimitInputValues будет корректировать значения входов (указатели на них передаются в первом и втором ее параметрах) согласно указанным требованиям. В третьем и четвертом параметрах передаются текущие значения выходов блока: они понадобятся, если координата зафиксирована (в этом случае в соответствующий вход будет записано зафиксированное значение выхода). Функция будет иметь следующий вид:

      // Ограничение значений входов блока
      void TSimpleJoystick::LimitInputValues(double *px_in,double *py_in,
                                             double x,double y)
      { // px_in, py_in - указатели на входы блока
        // x, y - текущие значения выходов
        if(LockX) // Координата x зафиксирована
          *px_in=x; // Присваиваем входу x_in зафиксированное значение
        else // Ограничиваем x_in диапазоном [-1...1]
          { if(*px_in<-1.0) *px_in=-1.0;
            else if(*px_in>1.0) *px_in=1.0;
          }
        if(LockY) // Координата y зафиксирована
          *py_in=y; // Присваиваем входу y_in зафиксированное значение
        else // Ограничиваем y_in диапазоном [-1...1]
          { if(*py_in<-1.0) *py_in=-1.0;
            else if(*py_in>1.0) *py_in=1.0;
          }
      }
      //=========================================

Теперь внесем изменения в функцию модели блока:

      // Двухкоординатная рукоятка
      extern "C" __declspec(dllexport)
        int RDSCALL SimpleJoystick(int CallMode,
                RDS_PBLOCKDATA BlockData,
                LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define pStart  ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define x (*((double *)(pStart+2*RDS_VSZ_S)))
      #define y (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D)))
      #define x_in (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D)))
      #define y_in (*((double *)(pStart+2*RDS_VSZ_S+3*RDS_VSZ_D)))
      #define x_in_ok (*((char *)(pStart+2*RDS_VSZ_S+4*RDS_VSZ_D)))
      #define y_in_ok (*((char *)(pStart+3*RDS_VSZ_S+4*RDS_VSZ_D)))
        // Вспомогательная переменная - указатель на личную область,
        // приведенный к правильному типу
        TSimpleJoystick *data=(TSimpleJoystick*)(BlockData->BlockData);
        switch(CallMode)
          { // …
            // Реакции RDS_BFM_INIT и RDS_BFM_CLEANUP - без изменений
            // …

            // Проверка допустимости типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSDDDDSS}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // … Реакции RDS_BFM_MOUSEDOWN и RDS_BFM_MOUSEUP -
            // без изменений …

            // Перемещение курсора мыши
            case RDS_BFM_MOUSEMOVE:
              // Проверка: включен ли захват мыши
              if(BlockData->Flags & RDS_MOUSECAPTURE) // Включен
                { // Запоминаем старые значения
                   double oldX=x,oldY=y;
                  // Вызываем функцию реакции
                  data->MouseMove((RDS_PMOUSEDATA)ExtParam,&x,&y);
                  Ready=1; // Взводим сигнал готовности
                  // Если значения изменились, передаем соседям
                  if(oldX!=x || oldY!=y)
                    ControlValueChangedCall(&(data->NoCall));
                }
              break;

            // … Реакции RDS_BFM_DRAW, RDS_BFM_MENUFUNCTION,
            // RDS_BFM_RESIZE, RDS_BFM_RESIZING - без изменений …

            // Один такт расчета
            case RDS_BFM_MODEL:
              if(x_in_ok==0 && y_in_ok==0)
                { // Связанные сигналы не взведены
                  Ready=0; // Сбрасываем сигнал готовности
                  break;
                }
              if(x==x_in && y==y_in)
                { // На входах те же значения, что и на выходах
                  Ready=x_in_ok=y_in_ok=0; // Сбрасываем все сигналы
                  break;
                }
              // Ограничиваем входные значения
              data->LimitInputValues(&x_in,&y_in,x,y);
              // В зависимости от того, какой связанный сигнал взведен,
              // копируем соответствующие входы в выходы
              if(x_in_ok) x=x_in;
              if(y_in_ok) y=y_in;
              x_in_ok=y_in_ok=0; // Сбрасываем связанные сигналы
              // Передаем данные соединенным блокам
              ControlValueChangedCall(&(data->NoCall));
              break;
            // Реакция на вызов функции
            case RDS_BFM_FUNCTIONCALL:
              if(((RDS_PFUNCTIONCALLDATA)ExtParam)->Function==
                ControlValueChangedId)
                { // Вызвана "Common.ControlValueChanged"
                  if(data->NoCall) // Вызов заблокирован
                    break;
                  // Ограничиваем входные значения
                  data->LimitInputValues(&x_in,&y_in,x,y);
                  // Если связанные сигналы не взведены - не реагируем
                  if(x_in_ok==0 && y_in_ok==0)
                    break;
                  // Если значения входов те же, что и у выходов -
                  // не реагируем
                  if(x_in==x && y_in==y)
                    break;
                  // Копируем входы в выходы согласно связанным сигналам
                  if(x_in_ok) x=x_in;
                  if(y_in_ok) y=y_in;
                  x_in_ok=y_in_ok=0; // Сбрасываем связанные сигналы
                  Ready=1;           // Взводим сигнал готовности
                  // Передаем данные соединенным блокам
                  ControlValueChangedCall(&(data->NoCall));
                }
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef y_in_ok
      #undef x_in_ok
      #undef y_in
      #undef x_in
      #undef y
      #undef x
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Поскольку в блоке появилось четыре новых переменных, соответствующим образом изменена строка типов в реакции RDS_BFM_VARCHECK. Изменения внесены также в реакцию на перемещение курсора мыши RDS_BFM_MOUSEMOVE: теперь, если в результате действий пользователя выходы блока изменились (то есть пользователь перетащил круг рукоятки на новое место), вызывается функция ControlValueChangedCall, которую мы написали в предыдущем примере. Она передаст новые значения по связям и вызовет у соединенных блоков функцию «Common.ControlValueChanged». Это изменит значения в полях ввода, соединенных с рукояткой.

В функцию модели добавлены две новых реакции: выполнение такта расчета RDS_BFM_MODEL и реакция на вызов функции RDS_BFM_FUNCTIONCALL. В такте расчета прежде всего проверяется, взведен ли хотя бы один из флагов x_in_ok и y_in_ok, связанных с входами блока x_in и y_in соответственно. Если оба флага сброшены, блоку не нужно ничего делать, и реакция завершается, предварительно сбросив значение флага готовности Ready. В противном случае значения, поступившие на входы, сравниваются с текущими значениями выходов блока x и y. Если они совпадают, блок тоже может ничего не делать: нет смысла передавать по связям то же самое значение еще раз. В этом случае сбрасывается флаг готовности Ready и оба связанных со входами сигнала, и реакция завершается. Если же значения на входах отличаются от значений на выходах, значит, нужно их обработать и передать на выходы. Поступившие на вход значения обрабатываются функцией LimitInputValues, чтобы не допустить их выход за пределы диапазона [−1…1] и нарушения фиксации одной из координат, если она включена. Затем значения входов, соответствующие взведенным связанным сигналам, копируются в выходы, и связанные сигналы сбрасываются (в RDS сигналы всегда нужно сбрасывать вручную, см. §2.5.2). После этого, как и в предыдущем примере, вызывается функция ControlValueChangedCall для того, чтобы принудительно передать данные соседним блокам, не дожидаясь окончания такта расчета. Это необходимо для устранения возможных колебаний в схеме, если вдруг у нашего блока и соединенного с ним поля ввода окажутся разные значения на выходах при одновременно взведенных сигналах готовности.

В реакции на вызов функции блока RDS_BFM_FUNCTIONCALL мы, прежде всего, проверяем, совпадает ли переданный при вызове идентификатор функции со значением глобальной переменной ControlValueChangedId. Если это так, значит, вызвана функция «Common.ControlValueChanged». Если при этом взведен флаг блокировки вызовов NoCall, реакция завершается – этот блок уже участвует в цепочке вызовов функции, и реагировать на повторный вызов нельзя. Если же флаг сброшен, выполняются те же действия, что и в такте расчета, с единственным исключением: если в такте расчета флаг готовности блока взводится автоматически, то при вызове функции блока этого не происходит. Его необходимо взвести вручную (выходу Ready присваивается значение 1), причем сделать это необходимо до вызова функции ControlValueChangedCall, поскольку сервисная функция rdsActivateOutputConnections, вызываемая внутри нее, не будет работать при сброшенном сигнале готовности блока.

Теперь, когда пользователь будет перемещать рукоятку, ее координаты будут отображаться в соединенных с ней полях ввода (см. рис. 86) как в режиме расчета, так и в режиме моделирования. В свою очередь, если пользователь будет менять значения в полях ввода, рукоятка будет перемещаться согласно введенным значениям.

В обоих рассмотренных примерах мы использовали одну и ту же стандартную функцию «Common.ControlValueChanged» без параметров. В §2.13.3 мы создадим собственную функцию с параметрами, и будем вызывать ее у всех блоков подсистемы.