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

Описания RDS

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

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

§2.16. Программное изменение схемы

§2.16.1. Изменение структуры переменных блока

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

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

### §2.16. Программное изменение схемы

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

#### §2.16.1. Изменение структуры переменных блока

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

Структура статических переменных простого блока обычно задается один раз при создании этого блока, а модель только проверяет допустимость этой структуры в вызове RDS_BFM_VARCHECK и запрещает работу блока, если структура его переменных не соответствует ожиданиям модели (см. §2.5.1). Тем не менее, RDS позволяет модели, при необходимости, изменять структуру переменных блока или параметры этих переменных.

Одним из самых простых примеров из рассмотренных ранее был блок с моделью TestSub, выдававший на выход разность двух своих вещественных входов. В режиме RDS_BFM_VARCHECK его модель проверяла наличие двух обязательных сигналов «Start» и «Ready» и трех вещественных переменных, возвращая константу RDS_BFR_BADVARSMSG, если переданная ей строка типа отличалась от «{SSDDD}». Таким образом, для того, чтобы этот блок мог работать, пользователь после подключения к новому блоку модели TestSub обязательно должен вручную задать ему правильную структуру переменных. Изменим модель так, чтобы при подключении к блоку с неподходящими переменными она автоматически меняла их на нужные ей. На самом деле, большой практической ценности этот пример иметь не будет: как правило, после создания блока его сразу записывают в библиотеку, откуда он добавляется в новые схемы уже с правильной структурой переменных. Однако, модель к блоку может быть подключена не только пользователем, но и, например, другим блоком при вызове сервисной функции rdsSetBlockModel – в этом случае автоматическое создание правильной структуры переменных может оказаться полезным.

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

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 1
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | x1 | double | 8 | Вход | ✓ | 0
10 | x2 | double | 8 | Вход | ✓ | 0
18 | y | double | 8 | Выход |  | 0

Мы будем вызывать эту функцию из модели в режиме RDS_BFM_VARCHECK , чтобы установить правильную структуру переменных, если это необходимо.

      BOOL CreateVarStruct_x1x2y(RDS_BHANDLE Block)
      { // Строка описания структуры переменных
        static char str[]=
          "struct\nbegin\n"
          "signal name \"Start\" in run default 1\n"
          "signal name \"Ready\" out default 0\n"
          "double name \"x1\" in menu run default 0\n"
          "double name \"x2\" in menu run default 0\n"
          "double name \"y\" out menu default 0\n"
          "end";
        RDS_HOBJECT dv; // Вспомогательный объект
        BOOL ok;
        // Создаем объект для работы со структурой переменных
        dv=rdsVSCreateEditor();
        // Создаем в объекте набор переменных по строке описания
        ok=rdsVSCreateByDescr(dv,str);
        if(ok) // Переписываем структуру переменных из объекта в блок
          ok=rdsVSApplyToBlock(dv,Block,NULL);
        // Удаляем вспомогательный объект
        rdsDeleteObject(dv);
        // Возвращаем успешность операции
        return ok;
      }
      //=========================================

Большая часть операций над структурой переменных блока в RDS осуществляется через вспомогательный объект, который создается сервисной функцией rdsVSCreateEditor. Сначала в таком объекте подготавливается нужная структура, а затем вызовом функции rdsVSApplyToBlock эта структура записывается в конкретный блок. В данном случае идентификатор блока, которому нужно установить новую структуру переменных, передается в параметре нашей функции CreateVarStruct_x1x2y. Эта функция будет возвращать TRUE, если изменение структуры удалось, и FALSE в противном случае – мы не сможем установить такую структуру переменных, например, во внешнем входе или выходе, у которого может быть только одна переменная, или в блоке, модель которого требует другой структуры.

В самом начале функции вводится статическая переменная str, которой присваивается строка описания нашей структуры переменных в том же виде, в каком описываются переменные при сохранении блока или схемы в текстовом формате. В этой строке перечислены все имена и типы переменных, включая обязательные для простого блока сигналы «Start» и «Ready». Такую строку описания для любой структуры переменных получить достаточно просто: нужно создать новую схему, добавить в нее единственный блок с нужной структурой переменных, сохранить этот блок в файл, а затем открыть этот файл в любом текстовом редакторе, найти в нем описание переменных блока, начинающееся со слова «vars», и скопировать это описание вплоть до ближайшего слова «end», заменив слово «vars» на слово «struct» (пример текста, получающегося при сохранении блока, приведен в §2.8.3).

Для того, чтобы создать структуру переменных по этой строке описания, мы сначала вызовом rdsVSCreateEditor создаем объект для манипуляций с переменными, и присваиваем его идентификатор переменной dv типа RDS_HOBJECT (идентификаторы всех вспомогательных объектов RDS имеют этот тип). Затем при помощи функции rdsVSCreateByDescr мы создаем в этом объекте структуру переменных, соответствующую строке str. Логическое значение, возвращенное функцией, записывается в переменную ok: если по какой-либо причине создать структуру не удастся (например, в строке описания будет синтаксическая ошибка), функция вернет FALSE. В случае успешного создания структуры мы копируем переменные из объекта в блок, идентификатор которого передан в параметре нашей функции, при помощи сервисной функции rdsVSApplyToBlock. Эта функция тоже возвращает логическое значение, свидетельствующее об успешности копирования – не всякому блоку можно назначить любую структуру переменных. В последнем параметре rdsVSApplyToBlock можно передать указатель на целую переменную, в которую функция запишет код ошибки, но нам это не нужно, поэтому мы передаем в нем NULL.

После того, как структура переменных установлена (или, в случае ошибок, ее установка не удалась), мы удаляем объект dv и возвращаем значение ok: если какая-то из вызванных сервисных функций вернула FALSE, ok тоже будет содержать FALSE.

Теперь нужно вставить вызов этой функции в модель блока TestSub в реакцию на вызов в режиме RDS_BFM_VARCHECK. Может возникнуть соблазн написать такую реакцию:

            // Проверка типа переменных
            case RDS_BFM_VARCHECK: // ОШИБКА!
              return CreateVarStruct_x1x2y(BlockData->Block)?
                RDS_BFR_DONE:RDS_BFR_BADVARSMSG;

Однако, ни к чему хорошему такая запись не приведет. Рассмотрим работу такой модели подробнее. Допустим, модель TestSub подключается к только что созданному блоку. После инициализации она будет вызвана в режиме RDS_BFM_VARCHECK, что приведет к вызову функции CreateVarStruct_x1x2y, которая изменит структуру переменных блока функцией rdsVSApplyToBlock. Функция rdsVSApplyToBlock опять вызовет модель в режиме RDS_BFM_VARCHECK для проверки правильности новой структуры, что опять приведет к новому вызову rdsVSApplyToBlock и новому вызову модели в режиме RDS_BFM_VARCHECK, и т.д. – эта последовательность вызовов будет продолжаться, пока не переполнится стек, после чего RDS завершится с сообщением об ошибке.

Чтобы не допустить возникновения этого бесконечного цикла, изменять структуру переменных нужно только тогда, когда текущая структура не подходит модели. В этом случае второй вызов модели в режиме RDS_BFM_VARCHECK не приведет к новому вызову rdsVSApplyToBlock, поскольку структура переменных в этот момент уже правильная. Таким образом, цикл не возникнет, и реакция модели благополучно завершится.

Изменим реакцию модели, вернув в нее удаленную ранее проверку строки типа переменных:

            // Проверка типа переменных
            case RDS_BFM_VARCHECK:
              if(strcmp((char*)ExtParam,"{SSDDD}")==0)
                return RDS_BFR_DONE; // Тип правильный
              // Тип неверный – меняем структуру (ОШИБКА!)
              return CreateVarStruct_x1x2y(BlockData->Block)?
                RDS_BFR_DONE:RDS_BFR_BADVARSMSG;

Однако, тут нас подстерегает еще одна опасность: что произойдет, если, по каким-либо причинам, функции rdsVSApplyToBlock не удастся установить нужную нам структуру переменных, или если строка описания переменных, используемая в функции CreateVarStruct_x1x2y, не будет соответствовать строке типа «{SSDDD}» из-за ошибки в исходном тексте программы? В этом случае проверка строки типа функцией strcmp, которую мы вернули в модель, укажет на несовпадение, и функция CreateVarStruct_x1x2y будет вызвана снова, что, как и в предыдущем варианте, приведет к бесконечной рекурсии. Чтобы избежать этого, нам нужно каким-то образом отличать вызовы модели в режиме RDS_BFM_VARCHECK из-за ее собственной попытки изменить структуру переменных блока от всех прочих ее вызовов в том же режиме. Если модель вызвана из-за того, что она сама меняет структуру переменных блока, и при этом структура переменных оказывается неверной, модель не должна снова пытаться изменить структуру – она только что уже попробовала, и это не дало результата. В этом случае модель должна просто завершиться, вернув константу RDS_BFR_ERROR, сигнализирующую об ошибке. В данном случае логично использовать RDS_BFR_ERROR вместо RDS_BFR_BADVARSMSG, поскольку нам не нужно выводить сообщение о неверной структуре переменных: нам нужно просто сообщить вызвавшей модель функции rdsVSApplyToBlock о том, что структура неправильная.

Чтобы отличить вызов модели при программном изменении структуры переменных от других вызовов, нужно использовать какой-либо флаг: перед вызовом функции CreateVarStruct_x1x2y мы будем взводить его, после возвращения из нее – сбрасывать. Таким образом, если при вызове модели в режиме RDS_BFM_VARCHECK флаг взведен, значит, мы попали сюда из-за программного изменения переменных, если сброшен – по какой-то другой причине. В качестве флага можно использовать целое поле Tag структуры данных блока RDS_BLOCKDATA. Это поле никак не используется RDS и предназначено для хранения пользовательских данных. Конечно, нам придется добавить в модель инициализацию этого поля нулем при вызове модели в режиме RDS_BFM_INIT, поскольку RDS его инициализировать не будет, а в исходном состоянии наш флаг должен быть сброшен. Таким образом, новая модель блока будет выглядеть так (изменения выделены цветом):

      extern "C" __declspec(dllexport)
        int RDSCALL TestSub(int CallMode,
                            RDS_PBLOCKDATA BlockData,
                            LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define x1 (*((double *)(pStart+2*RDS_VSZ_S)))
      #define x2 (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D)))
      #define y (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D)))
        BOOL ok;
        switch(CallMode)
          {
            // Инициализация
            case RDS_BFM_INIT:
              BlockData->Tag=0; // Сброс флага
              break;
            // Проверка типа переменных
            case RDS_BFM_VARCHECK:
              if(strcmp((char*)ExtParam,"{SSDDD}")==0)
                return RDS_BFR_DONE; // Тип переменных правильный
              // Тип переменных неправильный
              if(BlockData->Tag) // Флаг взведен – программное изменение
                return RDS_BFR_ERROR; // Ошибка
              // Модель вызвана не из-за программного изменения
              // структуры переменных
              // Взводим флаг на время программного изменения структуры
              BlockData->Tag=1;
              // Пытаемся изменить структуру переменных блока
              ok=CreateVarStruct_x1x2y(BlockData->Block);
              // Сбрасываем флаг обратно
              BlockData->Tag=0;
              // Возвращаем результат попытки изменения
              return ok?RDS_BFR_DONE:RDS_BFR_BADVARSMSG;
            // Выполнение такта расчета
            case RDS_BFM_MODEL:
              // Вычисление значения выхода
              if(x1==DoubleErrorValue || x2==DoubleErrorValue)
                y=DoubleErrorValue;
              else
                y=x1-x2;
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef y
      #undef x2
      #undef x1
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Эта модель защищена от бесконечной рекурсии, она будет менять структуру переменных блока только в том случае, если эта структура не соответствует строке типа «{SSDDD}». Кроме решения технической проблемы с рекурсией, эта проверка позволяет пользователю переименовывать переменные блока, как ему вздумается – строка типа при этом не изменится, и модель не будет пытаться вернуть блоку жестко прописанную в функции CreateVarStruct_x1x2y структуру переменных с именами «x1», «x2» и «y».

В рассмотренном примере модель устанавливала в блоке жесткую, заранее заданную структуру переменных. Иногда блоку необходимо менять свою структуру переменных в зависимости от каких-либо других параметров, или даже из-за действий пользователя. В §2.15.2 мы создали блоки, способные передавать по сети вещественные числа и массивы вещественных чисел. Передача массивов позволяет снизить нагрузку на сеть, однако для пользователя это может оказаться не слишком удобным: ему нужно помнить, что передается в каждом элементе массива. Например, если в схеме на одной машине он подаст на пятый элемент «X[4]» входного массива передающего блока значение скорости какого-либо объекта, на другой машине он будет получать его на выходе «Y[4]» принимающего блока. Было бы гораздо удобнее, если бы пользователь мог назвать этот вход и соответствующий ему выход «Speed», тогда у него не возникло бы вопросов, с какого выхода принимающего блока снимать сигнал.

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

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

      // Личная область данных блоков приема и передачи по сети
      class TNetSendRcvData
      { public:
          int Mode;	// Режим данного блока: прием или передача
            #define NETSRMODE_SENDER    0 // Передатчик
            #define NETSRMODE_RECEIVER  1 // Приемник
          char *ChannelName; // Имя канала
          BOOL LimitSpeed;   // Задан минимальный интервал передачи
          DWORD Delay;       // Минимальный интервал в мс
          BOOL StructInOut;  // Этот блок передает или принимает структуру
          int ConnId; // Идентификатор соединения

          // Переменные состояния блока-передатчика
          BOOL Connected;      // Соединение установлено
          BOOL DataWaiting;    // Передача данных отложена
          RDS_TIMERID Timer;   // Таймер для отсчета интервала
          BOOL WaitingForTimer;// Таймер запущен - ждем
          DWORD LastSendTime;  // Время последней отправки

          // Функции класса
          void Connect(void);   // Установить соединение
          void Disconnect(void);// Разорвать соединение

          void SendValue(double value); // Передать число в канал
          BOOL ReceiveValue(RDS_NETRECEIVEDDATA *rcv,// Реакция на
                            double *pOut);           // пришедшие данные
          void SendArray(void *input); // Передать массив в канал
          BOOL ReceiveArray(RDS_NETRECEIVEDDATA *rcv, // Реакция на
                            void *output);            // пришедшие данные
          void SendStruct(RDS_BHANDLE Block); // Передать структуру
          BOOL ReceiveStruct(RDS_NETRECEIVEDDATA *rcv,// Реакция на
                             RDS_BHANDLE Block);      // пришедшую структуру
          void CreateTimer(void); // Создать таймер
          void DeleteTimer(void); // Удалить таймер
          BOOL CheckSendTimer(void);// Проверить, можно ли передавать,
                                    // и запустить таймер, если нельзя

          int Setup(char *title); // Функция настройки блока
          void SaveText(void);    // Сохранить параметры
          void LoadText(char *text);// Загрузить параметры

          // Конструктор класса
          TNetSendRcvData(int mode)
            { ConnId=-1; // Нет соединения
              Connected=DataWaiting=FALSE;
              LimitSpeed=WaitingForTimer=FALSE;Timer=NULL;Delay=100;
              ChannelName=NULL; StructInOut=FALSE;
              Mode=mode;// Режим передается в параметре конструктора
            };
          // Деструктор класса
          ~TNetSendRcvData()
            { Disconnect(); // Разорвать соединение
              DeleteTimer();// Удалить таймер
              rdsFree(ChannelName);// Освободить строку имени канала
            };
      };
      //=========================================

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

Для передачи и приема структуры мы включили в класс две новых функции: SendStruct и ReceiveStruct. При наличии в блоке сложных переменных (матриц, строк и т.п.) в структуре будут содержаться указатели на другие области памяти, поэтому, в общем случае, структура переменных блока не является набором последовательных байтов. Для того, чтобы можно было передать ее по сети единым блоком, воспользуемся сервисной функцией rdsBlockVarToMem, которая записывает все дерево переменных блока в последовательный динамически отведенный буфер и возвращает указатель на него и его длину. Кроме того, эта функция может добавить к этому буферу строку типа переменных блока, что пригодится нам при приеме: успешно принять структуру переменных мы сможем только в том случае, если их типы в точности соответствуют типам переменных блока-приемника, поэтому, приняв из канала двоичные данные, блок-приемник должен будет проверить, соответствуют ли они его структуре переменных.

Начнем с функции SendStruct – в ее параметре мы будем передавать идентификатор блока, переменные которого нужно передать по сети:

      // Передать структуру переменных блока
      void TNetSendRcvData::SendStruct(RDS_BHANDLE Block)
      { void *buf;
        int len;

        // Является ли данный блок передатчиком?
        if(Mode!=NETSRMODE_SENDER)
          return; // Не является – это ошибка

        if(!Connected) // Нет связи с сервером
          { // Взводим флаг наличия данных, ожидающих передачи
            DataWaiting=TRUE;
            return;
          }
        // Связь с сервером есть

        // Формируем буфер со всеми переменными блока
        buf=rdsBlockVarToMem(Block,-1,TRUE,&len);
        if(buf==NULL) return; // Не удалось сформировать буфер

        // Передаем по сети сформированный буфер
        rdsNetBroadcastData(ConnId,
                            RDS_NETSEND_UPDATE|RDS_NETSEND_UDP,
                            0,NULL,
                            buf,len);
        // Освобождаем буфер – он уже передан
        rdsFree(buf);
        // Сбрасываем флаг ожидания – мы только что передали данные
        DataWaiting=FALSE;
        // Запоминаем время последней передачи
        LastSendTime=GetTickCount();
      }
      //=========================================

Эта функция очень похожа на функции SendValue и SendArray, которые уже есть в этом классе. У нее только два отличия: во-первых, в самом ее начале значение поля Mode сравнивается с константой NETSRMODE_SENDER, чтобы проверить, является ли блок, для которого вызвана функция, передатчиком. Раньше у нас были отдельные модели блоков для приемника и передатчика, теперь же мы делаем универсальный блок, который будет принимать и передавать данные в зависимости от настроек. Если блок используется как приемник, важно не дать ему передать данные, если, например, пользователь зачем-то подаст единицу на его сигнальный вход Start, из-за чего модель блока вызовется в такте расчета (до сих пор мы передавали данные именно в такте расчета, при срабатывании какой-либо подключенной ко входу связи). Проще всего заблокировать передачу непосредственно внутри функции SendStruct, прервав ее выполнение, если, согласно настройкам, блок не является передатчиком.

Во-вторых, данные для передачи теперь подготавливаются во вспомогательном буфере при помощи функции rdsBlockVarToMem. В первом ее параметре передается идентификатор блока, переменные которого нужно записать в буфер, во втором – номер переменной в блоке (значение −1 указывает на запись всех переменных блока как единой структуры). Значение TRUE в третьем параметре заставит функцию записать в буфер не только значения переменных, но и строку типа, чтобы блок-приемник смог проверить совместимость принятых данных со своей структурой переменных. Наконец, в последнем параметре передается указатель на целую переменную, в которую функция запишет длину получившегося буфера. После того, как содержимое буфера будет передано в канал функцией rdsNetBroadcastData, он будет освобожден функцией rdsFree.

Функция приема структуры ReceiveStruct будет выглядеть так:

      // Принять структуру переменных
      BOOL TNetSendRcvData::ReceiveStruct(RDS_NETRECEIVEDDATA *rcv,
                  RDS_BHANDLE Block)
      {
        // Проверяем первый параметр и является ли блок приемником
        if(rcv==NULL || Mode!=NETSRMODE_RECEIVER)
          return FALSE;
        // Есть ли среди принятых данных двоичные?
        if(rcv->Buffer==NULL)
          return FALSE;
        // Пытаемся записать принятые данные в структуру переменных блока
        return rdsBlockVarFromMem(Block,-1,rcv->Buffer,rcv->BufferSize);
      }
      //=========================================

В этой функции тоже сначала проверяется, может ли этот блок принимать данные (поле Mode должно содержать константу NETSRMODE_RECEIVER). Если это не так, значит, объект класса принадлежит блоку-передатчику, и принятые данные нужно игнорировать. Затем функция обрабатывает принятые двоичные данные, если они есть, функцией rdsBlockVarFromMem. Эта функция – обратная к rdsBlockVarToMem, она переписывает данные из буфера в памяти в структуру переменных блока согласно ее формату. Если в этом буфере, кроме значений переменных, записана и строка типа, функция сама проверит соответствие этой строки типам переменных блока и вернет FALSE, если они не совпадут. В первом параметре функции передается идентификатор блока, в переменные которого будут записаны значения, во втором – номер переменной в блоке (мы передаем −1, чтобы считалась вся структура переменных), в третьем и четвертом – указатель на буфер и его размер соответственно.

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

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

      // Функция обратного вызова окна настройки блока (прототип)
      void RDSCALL TNetSendRcvData_Setup_Check(
        RDS_HOBJECT window,          // Объект-окно
        RDS_PFORMSERVFUNCDATA data); // Описание события

В самой функции настройки мы не имеем права менять структуру переменных блока до тех пор, пока пользователь не нажмет кнопку «OK». Значит, нам нужно куда-нибудь скопировать текущую структуру переменных, и дать пользователю редактировать эту копию. Эта копия будет переписана в блок только при нажатии «OK». Чтобы не создавать никаких дополнительных объектов, указатели на которые достаточно затруднительно передавать в функцию обратного вызова, вызываемую при нажатии на кнопку редактирования переменных, воспользуемся специальным невидимым полем ввода объекта-окна: RDS_FORMCTRL_NONVISUAL. Это поле ввода предназначено для временного хранения параметров, для которых не предусмотрен пользовательский интерфейс, к его содержимому легко получить доступ как из функции настройки, так и из функции обратного вызова. Перед открытием окна мы запишем в это поле текст, описывающий все переменные блока. При нажатии кнопки редактирования переменных в функции обратного вызова мы будем восстанавливать переменные по этому тексту и предоставлять их пользователю для редактирования. Когда он закроет редактор, мы снова будем записывать в это поле текстовое описание отредактированных переменных. Наконец, когда пользователь нажмет «OK», мы снова восстановим переменные по тексту и запишем их в блок.

Таким образом, в функцию Setup нужно внести следующие дополнения:

      // Функция настройки блока
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      int TNetSendRcvData::Setup(char *title)
      { RDS_HOBJECT win; // Вспомогательный объект-окно
        BOOL ok;         // Пользователь нажал "OK"
        char *str;
        // Создаем окно
        win=rdsFORMCreate(FALSE,-1,-1,title);

        if(StructInOut) // Блок работает со структурами
          { // Выпадающий список "прием/передача"
            rdsFORMAddEdit(win,0,10,RDS_FORMCTRL_COMBOLIST,
                "Действие:",150);
            rdsSetObjectStr(win,10,RDS_FORMVAL_LIST,
                "Передача данных\nПрием данных");
            rdsSetObjectInt(win,10,RDS_FORMVAL_VALUE,
                (Mode==NETSRMODE_RECEIVER)?1:0);

            // Получение строки описания переменных блока
            str=rdsCreateVarDescriptionString(
                    rdsGetBlockVar(NULL,-1,NULL),TRUE,0,NULL);
            // Установка этой строки как невидимого поля ввода 1000
            rdsFORMAddEdit(win,0,1000,RDS_FORMCTRL_NONVISUAL,NULL,0);
            rdsSetObjectStr(win,1000,RDS_FORMVAL_VALUE,str);
            rdsFree(str); // Строка больше не нужна

            // Кнопка редактирования переменных
            rdsFORMAddEdit(win,0,11,
                RDS_FORMCTRL_BUTTON | RDS_FORMFLAG_LINE,
                "Переменные:",150);
            rdsSetObjectStr(win,11,RDS_FORMVAL_VALUE,"Изменить...");
          }
        // Поле ввода имени канала
        rdsFORMAddEdit(win,0,1,RDS_FORMCTRL_EDIT,
                       "Имя канала:",200);
        rdsSetObjectStr(win,1,RDS_FORMVAL_VALUE,ChannelName);

        if(Mode==NETSRMODE_SENDER || StructInOut)
          { // Для передатчика – ввод интервала
            rdsFORMAddEdit(win,0,2,
                RDS_FORMCTRL_EDIT | RDS_FORMFLAG_CHECK,
                "Интервал передачи, мс:",80);
            rdsSetObjectInt(win,2,RDS_FORMVAL_VALUE,Delay);
            rdsSetObjectInt(win,2,RDS_FORMVAL_CHECK,LimitSpeed);
          }

        // Открытие окна
        ok=rdsFORMShowModalServ(win,TNetSendRcvData_Setup_Check);
        if(ok)
          { // Пользователь нажал OK – запись параметров в блок
            char *NewName=rdsGetObjectStr(win,1,RDS_FORMVAL_VALUE);
            if(ChannelName==NULL || strcmp(NewName,ChannelName)!=0)
              { // Имя канала изменилось – запоминаем новое
                rdsFree(ChannelName);
                ChannelName=rdsDynStrCopy(NewName);
              }
            // Флаг ограничения интервала и сам интервал
            LimitSpeed=rdsGetObjectInt(win,2,RDS_FORMVAL_CHECK)!=0;
            Delay=rdsGetObjectInt(win,2,RDS_FORMVAL_VALUE);

            if(StructInOut) // Блок работает со структурами
              { RDS_HOBJECT dv; // Объект для работы с переменными
                DWORD flags,mask;
                RDS_VARDESCRIPTION vdescr;
                // Прием или передача
                Mode=rdsGetObjectInt(win,10,RDS_FORMVAL_VALUE)?
                         NETSRMODE_RECEIVER:NETSRMODE_SENDER;
                // Чтение строки описания переменных из невидимого поля
                str=rdsGetObjectStr(win,1000,RDS_FORMVAL_VALUE);
                // Создаем объект для работы с переменными
                dv=rdsVSCreateEditor();
                // Создаем структуру переменных по строке описания
                if(rdsVSCreateByDescr(dv,str))
                  { // Установка флагов переменных – входы или выходы
                    if(Mode==NETSRMODE_SENDER) // Передатчик (входы)
                      flags=RDS_VARFLAG_INPUT|RDS_VARFLAG_RUN|
                            RDS_VARFLAG_MENU|RDS_VARFLAG_SHOWNAME;
                    else // Приемник (выходы)
                      flags=RDS_VARFLAG_OUTPUT|RDS_VARFLAG_MENU|
                            RDS_VARFLAG_SHOWNAME;
                    // Маска изменяемых флагов
                    mask=RDS_VARFLAG_INPUT|RDS_VARFLAG_OUTPUT|
                         RDS_VARFLAG_RUN|RDS_VARFLAG_MENU|
                         RDS_VARFLAG_SHOWNAME;
                    // Получение числа переменных в объекте dv
                    vdescr.servSize=sizeof(vdescr);
                    rdsVSGetVarDescription(dv,-1,&vdescr);
                    // Установка флагов начиная со второй переменной
                    for(int i=2;i<vdescr.StructFields;i++)
                      rdsVSSetVarFlags(dv,i,flags,mask);
                    // Запись переменных из объекта в блок
                    if(!rdsVSApplyToBlock(dv,NULL,NULL))
                      rdsMessageBox("Невозможно установить переменные "
                        "блока","Ошибка",MB_OK | MB_ICONWARNING);
                  }
                // Удаление вспомогательного объекта
                rdsDeleteObject(dv);
              }
            Disconnect();
            Connect();
          }
        // Уничтожение окна
        rdsDeleteObject(win);
        // Возвращаемое значение
        return ok?RDS_BFR_MODIFIED:RDS_BFR_DONE;
      }
      //=========================================

Дополнительные поля вводятся в окно настроек, только если StructInOut имеет значение TRUE, то есть только для новых блоков. Прежде всего, вводится выпадающий список (RDS_FORMCTRL_COMBOLIST) с идентификатором 10. В нем доступно для выбора всего два варианта: «Передача данных» и «Прием данных». Текущий вариант устанавливается по значению поля класса Mode. Затем вызывается сервисная функция rdsCreateVarDescriptionString, которая формирует в динамически отведенной области памяти текст с описанием переменных блока, аналогичный тексту в функции CreateVarStruct_x1x2y, рассмотренной в предыдущем примере. Эта функция принимает четыре параметра:

      RDSSTR RDSCALL rdsCreateVarDescriptionStringA( // Возвращает UTF8
        RDS_VHANDLE Var,   // Идентификатор переменной
        BOOL StructFields, // Раскрывать поля структуры
        int Indent,        // Число пробелов перед каждой строкой
        int *pLength);     // Возвращаемая длина строки
      RDSWSTR RDSCALL rdsCreateVarDescriptionStringW( // Возвращает UTF16
        RDS_VHANDLE Var,   // Идентификатор переменной
        BOOL StructFields, // Раскрывать поля структуры
        int Indent,        // Число пробелов перед каждой строкой
        int *pLength);     // Возвращаемая длина строки
      // Функция-псевдоним
      RDSXSTR RDSCALL rdsCreateVarDescriptionString( // Возвращает в кодировке по умолчанию
        RDS_VHANDLE Var,   // Идентификатор переменной
        BOOL StructFields, // Раскрывать поля структуры
        int Indent,        // Число пробелов перед каждой строкой
        int *pLength);     // Возвращаемая длина строки

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

Для получения идентификатора структуры переменных блока мы использовали сервисную функцию rdsGetBlockVar. Она принимает три параметра:

      RDS_VHANDLE RDSCALL rdsGetBlockVar(
        RDS_BHANDLE Block,         // Идентификатор блока или NULL
        int num,                   // Номер переменной или -1
        RDS_PVARDESCRIPTION descr);// Указатель на структуру описания

В первом параметре функции передается идентификатор блока, переменные которого нас интересуют. Мы передаем NULL, что означает, что нам нужна переменная блока, модель которого выполняется в данный момент. Во втором параметре передается номер переменной в блоке – мы передаем −1, поскольку нас интересует не какая-то конкретная переменная, а вся структура переменных блока как единое целое. И, наконец, в третьем параметре можно передать указатель на структуру описания переменной, которую функция должна заполнить. Нам это описание не нужно – мы передаем в последнем параметре NULL.

Теперь, когда у нас есть текст, описывающий все переменные блока, мы создаем в объекте-окне win для его хранения невидимое поле с идентификатором 1000 и записываем в него значение переменной str. Теперь функция обратного вызова TNetSendRcvData_Setup_Check, прототип которой мы описали ранее, сможет считать этот текст, создать по нему структуру переменных и открыть отдельное окно для их редактирования, а потом записать измененный текст обратно в невидимое поле. Как она будет это делать – пока не важно, мы разберемся с этим, когда приступим к ее написанию. После того, как значение str записано в поле, эта динамически отведенная строка больше не нужна, и она освобождается вызовом rdsFree.

Рис. 114. Окно настроек блока
приема/передачи структуры

Кроме невидимого поля, в котором хранится текстовое описание переменных, нам нужна кнопка, которая будет вызывать их редактор. Кнопка – это поле ввода типа RDS_FORMCTRL_BUTTON. У этого поля нет значения как такового: функции установки значения задают надпись на кнопке. Нажатие кнопки передается в функцию обратного вызова, а она уже выполняет соответствующие действия. Созданная нами кнопка будет иметь идентификатор 11 и, поскольку она создана с флагом RDS_FORMFLAG_LINE, от остальной части окна она будет отделяться горизонтальной линией (рис. 114).

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

Поскольку для реакции на нажатие кнопки нам нужна функция обратного вызова с расширенными возможностями, окно теперь открывается не функцией rdsFORMShowModalEx, а функцией rdsFORMShowModalServ, в которую передается указатель на функцию TNetSendRcvData_Setup_Check (нам еще предстоит ее написать, пока мы описали только ее прототип). После закрытия окна кнопкой «OK» мы сначала, как и раньше, считываем из полей ввода новое имя канала ChannelName, флаг ограничения интервала передачи LimitSpeed и значение этого интервала Delay. Затем, если значение StructInOut истинно (то есть если этот объект принадлежит блоку, принимающему и передающему структуры), мы должны считать из окна настроек режим работы блока Mode и измененную пользователем структуру переменных. Режим работы считывается из выпадающего списка: если в нем выбран вариант с индексом 0 («прием данных»), в Mode записывается константа NETSRMODE_RECEIVER, если вариант с индексом 1 («передача данных») – константа NETSRMODE_SENDER. Структура переменных (возможно, измененная пользователем) хранится в виде текстового описания в невидимом поле с идентификатором 1000. Прежде всего, мы, как обычно, получаем указатель на строку с этим текстовым описанием в памяти объекта-окна при помощи вызова rdsGetObjectStr и записываем его в переменную str (это не динамическая строка, ее не нужно будет потом освобождать). Функцией rdsVSCreateEditor мы создаем вспомогательный объект для работы с переменными и записываем его идентификатор в переменную dv. Затем мы, точно так же, как и в предыдущем примере с программным изменением структуры переменных, создаем в этом объекте структуру переменных по текстовому описанию при помощи функции rdsVSCreateByDescr. Теперь в объекте есть структура, однако ее еще рано записывать в блок: нужно сделать переменные входами, если блок будет передавать структуру по сети, и выходами, если он будет ее принимать. При этом мы не имеем права трогать первые две переменных: это обязательные сигналы «Start» и «Ready», первый из них обязательно должен быть входом, а второй – выходом.

В зависимости от значения поля Mode (оно уже получило свое значение из выпадающего списка в окне) в переменную flags записываются флаги переменных, которые будут установлены для каждой переменной в объекте dv, за исключением первых двух. Если блок передает структуру (Mode равно NETSRMODE_SENDER), переменная получит сочетание флагов RDS_VARFLAG_INPUT (вход), RDS_VARFLAG_RUN (запускать модель при срабатывании связи), RDS_VARFLAG_MENU (имя переменной присутствует в меню соединений) и RDS_VARFLAG_SHOWNAME (показывать имя переменной рядом с точкой подключения связи). Если же блок будет принимать структуру, у переменной будут установлены флаги RDS_VARFLAG_OUTPUT (выход), RDS_VARFLAG_MENU и RDS_VARFLAG_SHOWNAME. В переменную mask записывается маска флагов, которые мы будем изменять у переменной, то есть объединение битовым ИЛИ всех перечисленных выше флагов (в маске должны быть взведены биты, соответствующие флагам, которые мы меняем, и обнулены биты флагов, которые мы не трогаем). Чтобы определить общее число переменных в объекте, мы вызываем функцию rdsVSGetVarDescription, которая заполняет структуру vdescr описанием структуры переменных объекта (вместо номера переменной во втором параметре функции передается −1, чтобы получить описание структуры переменных как единого целого). Предварительно, как обычно, в поле servSize структуры vdescr мы записываем размер этой структуры, чтобы функция смогла проверить правильность переданного параметра. После этого вызова в поле StructFields будет содержаться общее число переменных в объекте dv. Теперь мы можем в цикле установить всем переменным объекта, начиная с третьей (то есть с индекса 2, поскольку переменные нумеруются начиная с нуля), флаги из переменной flags при помощи функции rdsVSSetVarFlags. В эту функцию передается идентификатор объекта dv, индекс переменной i, битовые флаги переменной flags и битовая маска устанавливаемых флагов mask. После того, как флаги установлены, мы копируем структуру переменных из объекта в блок при помощи уже рассматривавшейся ранее функции rdsVSApplyToBlock. Если копирование не удалось, выводится сообщение об ошибке. В самом конце объект dv уничтожается вызовом rdsDeleteObject.

В конце функции настройки у нас записаны последовательные вызовы функций Disconnect и Connect, поэтому какую-либо дополнительную реакцию на изменение режима работы блока Mode, который раньше не менялся на протяжении всего существования блока, писать не нужно. Если пользователь изменит режим работы, это будет учтено функцией Connect, которая проверяет Mode при соединении с сервером.

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

      // Функция обратного вызова окна настройки
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void RDSCALL TNetSendRcvData_Setup_Check(RDS_HOBJECT window,
                RDS_PFORMSERVFUNCDATA data)
      { RDS_HOBJECT dv=NULL;
        char *descr;
        RDS_VARDESCRIPTION vdescr;
        BOOL sender;

        // Реагируем на событие, из-за которого вызвана функция
        switch(data->Event)
          { case RDS_FORMSERVEVENT_CLICK: // Нажатие кнопки
              if(data->CtrlId!=11) // Это не кнопка "Изменить"
                break;
              // Создаем объект для манипуляций с переменными
              dv=rdsVSCreateEditor();
              // Считываем текст описания переменных из поля 1000
              descr=rdsGetObjectStr(window,1000,RDS_FORMVAL_VALUE);
              // Заполняем объект dv описанием переменных descr
              if(!rdsVSCreateByDescr(dv,descr))
                break;
              // Удаляем из объекта две первых переменных
              rdsVSDeleteVar(dv,0); // Start
              rdsVSDeleteVar(dv,0); // Ready
              // Открываем окно редактора переменных
              if(!rdsVSExecuteEditor(dv,FALSE,RDS_HVAR_FALLNS|
                    RDS_HVAR_FNOSTRUCTNAME|RDS_HVAR_FNOOFFSET,-1,
                    "Переменные"))
                break; // Пользователь нажал кнопку "Отмена"
              // Добавляем сигналы Start и Ready
              // Ready
              rdsVSAddVar(dv,0,"Ready",RDS_VARTYPE_SIGNAL,NULL,
                  RDS_VARFLAG_OUTPUT|RDS_VARFLAG_SHOWNAME|
                  RDS_VARFLAG_EXT_CHGNAME,0,"0");
              // Start
              rdsVSAddVar(dv,0,"Start",RDS_VARTYPE_SIGNAL,NULL,
                  RDS_VARFLAG_INPUT|RDS_VARFLAG_RUN|
                  RDS_VARFLAG_SHOWNAME|RDS_VARFLAG_EXT_CHGNAME,0,"0");
              // Формируем текст описания получившихся переменных
              vdescr.servSize=sizeof(vdescr);
              if(!rdsVSGetVarDescription(dv,-1,&vdescr))
                break;
              descr=rdsCreateVarDescriptionString(vdescr.Var,TRUE,0,NULL);
              if(descr)
                { // Записываем новый текст в невидимое поле
                  rdsSetObjectStr(win,1000,RDS_FORMVAL_VALUE,descr);
                  rdsFree(descr);
                }
              break;

            case RDS_FORMSERVEVENT_CHANGE: // Изменилось поле
              // Считываем режим работы блока из выпадающего списка
              sender=rdsGetObjectInt(window,10,RDS_FORMVAL_VALUE)==0;
              // sender истинно, если блок - передатчик
              // Ограничение интервала разрешено только для передатчика
              rdsSetObjectInt(window,2,RDS_FORMVAL_ENABLED,sender);
              break;
          }

        // Если в процессе работы функции был создан объект, удаляем его
        if(dv)
          rdsDeleteObject(dv);
      }
      //=========================================

В параметре window в эту функцию передается идентификатор объекта-окна, для которого она вызвана, в параметре data – указатель на структуру, описывающую произошедшее событие. Нас интересуют два события: нажатие кнопки и изменение поля ввода, которые анализируются в операторе switch.

При нажатии какой-либо кнопки поле Event структуры описания события принимает значение RDS_FORMSERVEVENT_CLICK. При этом мы проверяем идентификатор поля ввода, выдавшего сообщение: он должен быть равен 11, поскольку такой идентификатор мы дали кнопке «Изменить». Если нажата наша кнопка, вызовом rdsVSCreateEditor создается уже знакомый нам объект для работы с переменными, в который записывается текстовое описание переменных блока из невидимого поля с идентификатором 1000. Теперь нужно удалить из объекта две первых переменных: это сигналы «Start» и «Ready», мы не будем показывать их пользователю в редакторе переменных. Для этого мы два раза вызываем функцию rdsVSDeleteVar с нулевым номером переменной – после этого в объекте останутся только те переменные, которые можно изменять пользователю. Теперь можно открыть окно редактора переменных функцией rdsVSExecuteEditor. Она принимает следующие параметры:

      BOOL RDSCALL rdsVSExecuteEditorA(
        RDS_HOBJECT Object, // Объект с переменными
        BOOL Extended,      // Редактор в обычном (FALSE) или
                            // расширенном (TRUE) режиме
        DWORD Flags,        // Флаги, управляющие редактором
        int MaxDepth,       // Максимальная вложенность матриц
        RDSCSTR Caption);   // Заголовок окна (UTF8)
      BOOL RDSCALL rdsVSExecuteEditorW(
        RDS_HOBJECT Object, // Объект с переменными
        BOOL Extended,      // Редактор в обычном (FALSE) или
                            // расширенном (TRUE) режиме
        DWORD Flags,        // Флаги, управляющие редактором
        int MaxDepth,       // Максимальная вложенность матриц
        RDSWCSTR Caption);  // Заголовок окна (UTF16)
      // Функция-псевдоним
      BOOL RDSCALL rdsVSExecuteEditor(
        RDS_HOBJECT Object, // Объект с переменными
        BOOL Extended,      // Редактор в обычном (FALSE) или
                            // расширенном (TRUE) режиме
        DWORD Flags,        // Флаги, управляющие редактором
        int MaxDepth,       // Максимальная вложенность матриц
        RDSXCSTR Caption);  // Заголовок окна (кодировка по умолчанию)

Окно редактора может быть открыто в обычном или расширенном режиме, за это отвечает параметр Extended. В обычном режиме (в RDS он используется при редактировании полей структур) пользователь может задавать для каждой переменной только имя, тип и значение по умолчанию. В расширенном он также может сделать ее входом, выходом или внутренней, включить запуск модели при срабатывании связи, соединенной с входом и т.п. (в RDS этот режим используется при задании переменных простых блоков, см. рис. 9). Нам не нужен расширенный режим, поэтому мы передаем в этом параметре FALSE.

В параметре Flags передается набор битовых флагов, управляющих поведением и внешним видом редактора. Полный список этих флагов приведен в описании функции, нам же нужны три из них: RDS_HVAR_FALLNS (пользователь может давать переменным все типы, кроме сигналов), RDS_HVAR_FNOSTRUCTNAME (имя редактируемой структуры не отображается – в нашем случае его просто нет) и RDS_HVAR_FNOOFFSET (колонка смещения переменной от начала дерева не выводится – пользователю она, в данном случае, не нужна). Параметр MaxDepth определяет максимально возможную вложенность матриц: при значении 1 пользователь может задать матрицу, при значении 2 – матрицу матриц и т.д. Значение −1, передаваемое нами, указывает на максимально возможную вложенность (в RDS она равна пяти). Наконец, в параметре Caption передается заголовок открываемого окна редактора. При указанных параметрах функции rdsVSExecuteEditor окно редактора будет выглядеть как на рис. 115.

Рис. 115. Окно редактора переменных блока приема/передачи структуры

Если пользователь закроет окно редактора кнопкой «Отмена», функция rdsVSExecuteEditor вернет значение FALSE, и выполнение оператора switch прервется оператором break. В противном случае мы добавляем в объект, переменные которого только что изменены пользователем, сигналы «Start» и «Ready», которые мы стерли из него перед вызовом редактора. Для добавления переменных в объект мы используем функцию rdsVSAddVar:

      int RDSCALL rdsVSAddVarA(
        RDS_HOBJECT Object, // Объект с переменными
        int Index,          // Номер добавляемой переменной
        RDSCSTR VarName,    // Имя переменной (UTF8)
        char BaseVarType,   // Базовый тип переменной
        RDSCSTR StructType, // Имя структуры, если это структура (UTF8)
        DWORD Flags,        // Флаги переменной
        int ArrayDepth,     // Вложенность матриц (если есть)
        RDSCSTR DefVal);    // Значение по умолчанию (UTF8)
      int RDSCALL rdsVSAddVarW(
        RDS_HOBJECT Object, // Объект с переменными
        int Index,          // Номер добавляемой переменной
        RDSWCSTR VarName,   // Имя переменной (UTF16)
        char BaseVarType,   // Базовый тип переменной
        RDSWCSTR StructType,// Имя структуры, если это структура (UTF16)
        DWORD Flags,        // Флаги переменной
        int ArrayDepth,     // Вложенность матриц (если есть)
        RDSWCSTR DefVal);   // Значение по умолчанию (UTF16)
      // Функция-псевдоним
      int RDSCALL rdsVSAddVar(
        RDS_HOBJECT Object, // Объект с переменными
        int Index,          // Номер добавляемой переменной
        RDSXCSTR VarName,   // Имя переменной (кодировка по умолчанию)
        char BaseVarType,   // Базовый тип переменной
        RDSXCSTR StructType,// Имя структуры, если это структура (кодировка по умолчанию)
        DWORD Flags,        // Флаги переменной
        int ArrayDepth,     // Вложенность матриц (если есть)
        RDSXCSTR DefVal);   // Значение по умолчанию (кодировка по умолчанию)

В качестве номера переменной мы оба раза передаем 0, поэтому обе добавляемые переменные вставляются в начало списка. Сначала мы добавляем сигнал «Ready», и он становится самой первой переменной в структуре. Затем мы добавляем «Start», из-за чего «Ready» сдвигается на одну позицию вниз и становится вторым сигналом, а «Start» – первым, то есть оба сигнала теперь занимают правильное положение в списке. Параметр VarName указывает имя добавляемой переменной, а сочетание параметров BaseVarType, StructType и ArrayDepth – ее тип. В параметре BaseVarType передается символ из строки типа, соответствующий типу этой переменной или элемента матрицы, если это матрица или массив (все эти символы описаны в «RdsDef.h» как константы RDS_VARTYPE_*). В параметре StructType передается имя структуры из общего списка структур, если добавляемая переменная – структура, а в параметре ArrayDepth – глубина вложенности матриц, или 0, если добавляемая переменная – не матрица. Использование этих параметров может показаться несколько запутанным, поэтому приведем несколько примеров:

Тип переменной | BaseVarType | StructType | ArrayDepth
--- | --- | --- | ---
double | «D» (RDS_VARTYPE_DOUBLE) | NULL | 0
Матрица double | «D» (RDS_VARTYPE_DOUBLE) | NULL | 1
Логический | «L» (RDS_VARTYPE_LOGICAL) | NULL | 0
Матрица матриц double | «D» (RDS_VARTYPE_DOUBLE) | NULL | 2
Структура «Complex» | «{» (RDS_VARTYPE_STRUCT) | "Complex" | 0
Матрица структур «Complex» | «{» (RDS_VARTYPE_STRUCT) | "Complex" | 1

Мы добавляем одиночные сигналы, поэтому в параметре BaseVarType мы в обоих случаях передаем константу RDS_VARTYPE_SIGNAL (она имеет привычное нам значение «S»), в параметре StructType – NULL, а в параметре ArrayDepth – 0.

В параметре Flags в функцию rdsVSAddVar передаются уже знакомые нам по приведенной выше функции настройки флаги переменной: «Start» должен быть входом (RDS_VARFLAG_INPUT), «Ready» – выходом (RDS_VARFLAG_OUTPUT). Эти флаги объединены с флагом RDS_VARFLAG_SHOWNAME (показывать имя переменной) и новым флагом RDS_VARFLAG_EXT_CHGNAME, который указывает на то, что добавляемую переменную нужно переименовать, если переменная с таким именем уже есть в объекте. Если среди добавленных пользователем переменных будет, например, переменная с именем «Start», добавляемый нами сигнал будет автоматически переименован в «Start1» – модели блока все равно, как называется ее сигнал запуска, главное, чтобы это была первая переменная в структуре.

После того, как в объект добавлены два обязательных для простого блока сигнала, мы заполняем описанием его переменных структуру vdescr – нам нужно ее поле Var, в котором содержится уникальный идентификатор переменной, в данном случае – структуры переменных объекта dv как единого целого. Этот идентификатор мы передаем в уже знакомую нам функцию rdsCreateVarDescriptionString, чтобы получить новый текст описания структуры переменных и записать его обратно в невидимое поле 1000. На этом реакция на нажатие кнопки «Изменить…» завершается: мы преобразовали текст из невидимого поля в набор переменных в объекте, дали пользователю отредактировать эти переменные, наложив на редактор некоторые ограничения, и записали текст описания новых переменных обратно в невидимое поле.

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

При открытии окна, а также при изменении пользователем любого поля ввода, в поле Event структуры описания события data будет находиться константа RDS_FORMSERVEVENT_CHANGE. В данном случае мы не проверяем идентификатор изменившегося поля ввода – при открытии окна он не передается, а нам в этот момент тоже нужно установить разрешенность поля интервала. Мы будем управлять этим полем при любом изменении в окне – так функция получится проще, а задержки из-за лишних вызовов здесь не важны. Сначала мы считываем значение поля ввода с идентификатором 10 (это выпадающий список режимов) и сравниваем его с нулем – если они равны, значит, блок настроен на передачу данных. Результат сравнения записывается в логическую переменную sender и используется для установки разрешенности поля с идентификатором 2 (это поле ввода интервала передачи) при вызове rdsSetObjectInt с константой RDS_FORMVAL_ENABLED. Если sender истинно, поле интервала будет разрешено, если ложно – запрещено.

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

      // Сохранение параметров блока
      void TNetSendRcvData::SaveText(void)
      { RDS_HOBJECT ini; // Вспомогательный объект
        // Создаем вспомогательный объект
        ini=rdsINICreateTextHolder(TRUE);
        // Создаем в объекте секцию "[General]"
        rdsSetObjectStr(ini,RDS_HINI_CREATESECTION,0,"General");
        // Записываем в эту секцию имя канала
        rdsINIWriteString(ini,"Channel",ChannelName);

        // Записываем режим для приемопередатчика структур
        if(StructInOut)
          rdsINIWriteInt(ini,"Mode",Mode);
        if(Mode==NETSRMODE_SENDER) // Передатчик
          { // Создаем новую секцию
            rdsSetObjectStr(ini,RDS_HINI_CREATESECTION,0,"Timer");
            // Записываем параметры
            rdsINIWriteInt(ini,"On",LimitSpeed);
            rdsINIWriteInt(ini,"Delay",Delay);
          }
        // Сохраняем текст, сформированный объектом, как параметры блока
        rdsCommandObject(ini,RDS_HINI_SAVEBLOCKTEXT);
        // Удаляем вспомогательный объект
        rdsDeleteObject(ini);
      }
      //=========================================

      // Загрузка параметров блока
      void TNetSendRcvData::LoadText(char *text)
      { RDS_HOBJECT ini; // Вспомогательный объект
        char *str;
        // Создаем вспомогательный объект
        ini=rdsINICreateTextHolder(TRUE);
        // Записываем в объект полученный текст с параметрами блока
        rdsSetObjectStr(ini,RDS_HINI_SETTEXT,0,text);
        // Начинаем чтение секции "[General]", если она есть
        if(rdsINIOpenSection(ini,"General")) // Секция есть
          { // Освобождаем старое имя канала
            rdsFree(ChannelName);
            ChannelName=NULL;
            // Получаем у объекта указатель на строку с именем
            str=rdsINIReadString(ini,"Channel","",NULL);
            // Если такая строка есть в тексте, копируем ее в ChannelName
            if(str)
              ChannelName=rdsDynStrCopy(str);

            // Считываем режим для приемопередатчика структур
            if(StructInOut)
              Mode=rdsINIReadInt(ini,"Mode",Mode);
          }
        if(Mode==NETSRMODE_SENDER && // Передатчик
           rdsINIOpenSection(ini,"Timer")) // Есть секция "[Timer]"
          { LimitSpeed=rdsINIReadInt(ini,"On",LimitSpeed)!=0;
            Delay=rdsINIReadInt(ini,"Delay",Delay);
          }
        // Удаляем вспомогательный объект
        rdsDeleteObject(ini);
        // Поскольку имя канала и режим работы могли измениться,
        // соединяемся с сервером заново
        Disconnect(); // Разрываем старое соединение
        Connect();    // Создаем новое
      }
      //=========================================

Параметр Mode в этих функциях записывается и считывается только при истинном StructInOut, то есть только для блоков, принимающих и передающих структуры.

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

      // Прием/передача структуры
      extern "C" __declspec(dllexport)
        int RDSCALL NetSendRcvStruct(int CallMode,
            RDS_PBLOCKDATA BlockData,
            LPVOID ExtParam)
      { TNetSendRcvData *data=(TNetSendRcvData*)(BlockData->BlockData);
      // Макроопределения для первых двух переменных блока
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              BlockData->BlockData=data=
                  new TNetSendRcvData(NETSRMODE_SENDER);
              // Взводим флаг работы со структурами
              data->StructInOut=TRUE;
              break;

            // Очистка
            case RDS_BFM_CLEANUP:
              delete data;
              break;

            // Установлено соединение с сервером
            case RDS_BFM_NETCONNECT:
              data->Connected=TRUE;
              // Если были данные, ожидающие передачи – передаем их
              if(data->DataWaiting)
                data->SendStruct(BlockData->Block);
              break;

            // Соединение разорвано
            case RDS_BFM_NETDISCONNECT:
              data->Connected=FALSE;
              break;

            // Запуск расчета
            case RDS_BFM_STARTCALC:
              // При первом запуске передаем данные
              if(((RDS_PSTARTSTOPDATA)ExtParam)->FirstStart)
                data->SendStruct(BlockData->Block);
              break;

            // Срабатывание таймера
            case RDS_BFM_TIMER:
              data->WaitingForTimer=FALSE;
              // Передаем данные
              data->SendStruct(BlockData->Block);
              break;

            // Такт расчета
            case RDS_BFM_MODEL:
              if(data->CheckSendTimer())
                data->SendStruct(BlockData->Block);
              break;

            // Вызов функции настройки
            case RDS_BFM_SETUP:
              return data->Setup("Прием/передача структуры");

           // Сохранение параметров
           case RDS_BFM_SAVETXT:
              data->SaveText();
              break;

           // Загрузка параметров
           case RDS_BFM_LOADTXT:
              data->LoadText((char*)ExtParam);
              break;

            // По сети получены данные
            case RDS_BFM_NETDATARECEIVED:
              // Если данные совместимы с блоком – принимаем и
              // взводим сигнал готовности
              Ready=data->ReceiveStruct((RDS_NETRECEIVEDDATA*)ExtParam,
                BlockData->Block)?1:0;
              // Сбрасываем сигнал запуска
              Start=0;
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Фактически, эта модель является объединением моделей приемника и передатчика с небольшими отличиями. Прежде всего, в ней используются макроопределения только для двух обязательных сигналов Start и Ready – структура переменных этого блока произвольно задается пользователем, поэтому мы не можем ввести никаких макросов (на самом деле, в этой модели они нам и не нужны). По этой же причине в модели нет проверки типов переменных – блок будет работать с любой их структурой, и эта проверка не нужна.

При инициализации, как и в других моделях сетевых блоков, создается объект класса TNetSendRcvData, и указатель на него записывается в личную область данных блока. В параметре конструктора класса передается константа NETSRMODE_SENDER, поэтому исходно, при подключении модели, блок будет передатчиком. Позже, при загрузке параметров блока или при вызове функции настройки, его роль может измениться. После создания объекта полю StructInOut присваивается значение TRUE, чтобы функции класса знали, что этот блок передает и принимает всю свою структуру переменных. Все остальные реакции блока, кроме RDS_BFM_NETDATARECEIVED, отличаются от рассмотренных ранее моделей блоков-передатчиков только тем, что для передачи данных используется новая функция SendStruct. Эта функция сама проверит, настроен ли блок на передачу данных, и, если она по какой-либо причине будет вызвана для блока, настроенного на прием, она не выполнит никаких действий.

При получении данных по сети в реакции RDS_BFM_NETDATARECEIVED вызывается функция ReceiveStruct, которая вернет значение TRUE, если принятые данные совместимы со структурой переменных блока. Эта функция тоже проверяет текущие настройки блока, и, если сейчас он является передатчиком, принятые данные будут проигнорированы, и функция вернет FALSE. По результатам работы функции устанавливается сигнал готовности блока Ready: если данные успешно приняты, он будет взведен, и связи, подключенные к его выходам, сработают в ближайшем такте расчета. Сигнал Start при приеме данных сбрасывается – дело в том, что блок считывает из принятых данных значения всех своих переменных, включая и этот сигнал. Независимо от того, какое значение было передано по сети, запускать этот блок в следующем такте расчета не нужно (в режиме приемника блок вообще не работает в тактах расчета), поэтому этот сигнал принудительно обнуляется.

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

(а)

(б)

Рис. 116. Передача структуры по сети: передающая (а) и принимающая (б) схемы

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