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

Описания RDS

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

Глава 3. Создание модулей автоматической компиляции

§3.3. Подключение моделей к блокам и вызов редактора

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

## Глава 3. Создание модулей автоматической компиляции

### §3.3. Подключение моделей к блокам и вызов редактора

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

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

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

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

Прежде чем вводить новые реакции в нашу функцию модуля, нужно придумать формат файла модели. В §3.2 мы решили включать в автоматически компилируемую модель структуру переменных блока и реакцию на такт расчета. Будем хранить их в текстовом файле (с расширением «.txt»), чтобы, при желании, файл модели можно было открыть в обычном текстовом редакторе. Чтобы модуль автокомпиляции мог отличить файл модели от обычного текстового файла, наш файл модели всегда будет начинаться со строки «$TESTCMODEL». Если пользователь попытается подключить к блоку текстовый файл без этого слова в первой строке, функция модуля выведет ему сообщение об ошибке. Структуру статических переменных блока мы будем записывать в том же виде, в каком она сохраняется в текстовом формате схем и блоков (текст такого описания легко получить у RDS при помощи сервисных функций), предваряя это описание строкой «$VARS». После описания переменных будет располагаться строчка со словом «$PROG» и текст на языке C, представляющий собой реакцию нашей модели на один такт расчета, при этом в этом тексте мы дадим пользователю возможность обращаться к переменным блока по именам. Для простоты примера в моделях, компилируемых нашим модулем, мы ограничимся только простыми переменными (логическими, целыми, вещественными и т.п.) и запретим использовать сложные: структуры, матрицы, строки и переменные произвольного типа.

Таким образом, модель блока, выдающая на вещественный выход «y» сумму вещественных входов «x1» и «x2», в нашем формате будет выглядеть так:

      $TESTCMODEL
      $VARS
      struct
       begin
        signal name "Start" in run default 1
        signal name "Ready" out default 0
        double name "x1" in menu run default 0
        double name "x2" in menu run default 0
        double name "y" out menu default 0
       end
      $PROG
        y=x1+x2;

Разумеется, в описании переменных должны присутствовать обязательные для каждого простого блока сигналы «Start» и «Ready».

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

      // Класс личной области данных модуля автокомпиляции
      class TCAutoCompData
      { private:
          // Пути
          char *CompPath;     // к компилятору
          char *LinkPath;     // к редактору связей (link)
          char *IncludePath;  // к файлам заголовков
          char *LibPath;      // к библиотекам
          // Нужно ли добавлять путь к exe-файлу компилятора и редактора связей
          BOOL CompSetPath,LinkSetPath;
          // Параметры командной строки
          char *CompParams;   // компилятора
          char *LinkParams;   // редактора связей
          // Параметры исходного текста
          char *DllMainName;  // имя главной функции DLL
          char *ModelFuncHdr; // заголовок функции модели
          char *Exported;     // экспортированное имя функции

          // Освободить все динамические строки
          void FreeAllStrings(void)
            { rdsFree(CompPath);
              rdsFree(LinkPath);
              rdsFree(IncludePath);
              rdsFree(LibPath);
              rdsFree(CompParams);
              rdsFree(LinkParams);
              rdsFree(DllMainName);
              rdsFree(ModelFuncHdr);
              rdsFree(Exported);
            };

          // Сообщение об ошибке в модели
          static void ModelErrorMsg(char *modelname,int errorcode);
        public:
          // Чтение параметров модуля из INI-файла
          void ReadFromIni(const char *IniFileName);
          // Запись параметров модуля в INI-файл
          void WriteToIni(const char *IniFileName);
          // Настройка параметров модуля
          void Setup(const char *IniFileName);

          // Создать новую пустую модель
          void CreateEmptyModel(void);
          // Выбрать существующий файл модели
          void ConnectExistingModel(const char *oldmodel);
          // Проверить возможность подключения модели к блоку
          int CanAttachBlock(RDS_COMPCANATTACHBLKDATA *param);
          // Открыть редактор модели
          void OpenEditor(RDS_OPENEDITORDATA *param);
          // Конструктор класса
          TCAutoCompData(void)
          { IncludePath=LibPath=NULL;
            CompSetPath=LinkSetPath=TRUE;
            #ifdef RDS_WIN64
              CompPath=rdsDynStrCopy(
                "$RDS$\\mingw-w64\\bin\\g++.exe");
              LinkPath=rdsDynStrCopy(CompPath);
              CompParams=rdsDynStrCopy(
                "-Wall -m64 -g -I\"$RDSINCLUDE$\" -c \"$TEMP$\\$NAME$.cpp\" -o \"$TEMP$\\$NAME$.o\""
                );
              LinkParams=rdsDynStrCopy(
                "-shared -static -static-libgcc -static-libstdc++ -Wl,--dll \"$TEMP$\\$NAME$.o\" -o \"$TEMP$\\$NAME$.dll\" -s -m64 -luser32"
                );
              DllMainName=rdsDynStrCopy("extern \"C\" __declspec(dllexport) BOOL APIENTRY DllMain");
              ModelFuncHdr=rdsDynStrCopy(
                "extern \"C\" __declspec(dllexport) int RDSCALL autocompModelFunc"
                );
              Exported=rdsDynStrCopy("autocompModelFunc");
            #else
              CompPath=rdsDynStrCopy(
                "$RDS$\\tdm-gcc\\bin\\g++.exe");
              LinkPath=rdsDynStrCopy(CompPath);
              CompParams=rdsDynStrCopy(
                "-shared -fpermissive -c -I\"$RDSINCLUDE$\" \"$TEMP$\\$NAME$.cpp\""
                );
              LinkParams=rdsDynStrCopy(
                "-shared -static -static-libgcc -static-libstdc++ -o \"$TEMP$\\$NAME$.dll\" \"$TEMP$\\$NAME$.o\""
                );
              DllMainName=rdsDynStrCopy("extern \"C\" BOOL WINAPI DllMain");
              ModelFuncHdr=rdsDynStrCopy(
                "extern \"C\" __declspec(dllexport) int RDSCALL autocompModelFunc");
              Exported=rdsDynStrCopy("autocompModelFunc@12");
            #endif
          };
          // Деструктор класса
          ~TCAutoCompData(){ FreeAllStrings(); };
      };
      //=========================================

Кроме того, введем макроопределения для специальных слов «$TESTCMODEL», «$VARS» и «$PROG», которые мы решили использовать в текстовых файлах моделей:

      // Имена секций файла модели
      // Начало файла
      #define TCTEXTSECTION_START  "$TESTCMODEL"
      // Секция переменных
      #define TCTEXTSECTION_VARS   "$VARS"
      // Секция исходного текста
      #define TCTEXTSECTION_PROG   "$PROG"
      //=========================================

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

      // Коды ошибок
      #define MEC_SIMPLEBLOCK 1 // Только для простых блоков
      #define MEC_READERROR   2 // Ошибка чтения файла
      #define MEC_NOTAMODEL   3 // Файл - не модель
      #define MEC_NOSECTIONS  4 // Нет нужных разделов в модели
      #define MEC_MODELWRITEERROR 5 // Ошибка записи файла модели
      //=========================================

      // Сообщение об ошибке в модели
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void TCAutoCompData::ModelErrorMsg(
        char *modelname,int errorcode)
      { const char *errortext;
        // Сообщение по коду ошибки
        switch(errorcode)
          { case MEC_SIMPLEBLOCK:
              errortext="Модель может подключаться только к простому блоку";
              break;
            case MEC_READERROR:
              errortext="Ошибка чтения файла";
              break;
            case MEC_NOTAMODEL:
              errortext="Файл не является моделью блока";
              break;
            case MEC_NOSECTIONS:
              errortext="В файле нет необходимых разделов";
              break;
            case MEC_MODELWRITEERROR:
              errortext="Ошибка записи файла модели";
              break;
            default:
              errortext="Неизвестная ошибка";
          }
        // Название модели, если есть
        if(modelname)
          { char *msgtext; // Здесь формируется динамический текст
            msgtext=rdsDynStrCat("Модель: ",modelname,FALSE);
            rdsAddToDynStr(&msgtext,"\n",FALSE);
            // Описание ошибки
            rdsAddToDynStr(&msgtext,errortext,FALSE);
            // Показываем сообщение
            rdsMessageBox(msgtext,"Автокомпиляция",MB_OK | MB_ICONWARNING);
            // Освобождаем память, занятую динамическим текстом
            rdsFree(msgtext);
          }
        else
          rdsMessageBox(errortext,"Автокомпиляция",MB_OK | MB_ICONWARNING);
      }
      //=========================================

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

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

      // Функция модуля автокомпиляции
      extern "C" __declspec(dllexport) int RDSCALL TestCAutoComp(
          int CallMode,                   // Событие
          RDS_PCOMPMODULEDATA ModuleData, // Данные модуля
          LPVOID ExtParam)                // Дополнительные параметры
      { // Приведение указателя на личную область данных
        // к правильному типу
        TCAutoCompData *data=(TCAutoCompData*)(ModuleData->ModuleData);
        // Вспомогательная переменная – указатель на структуру
        // функции, вызванной из окна параметров
        RDS_PCOMPEXECFUNCDATA funcdata;
        switch(CallMode)
          { // Инициализация модуля
            case RDS_COMPM_INIT:
              // Создание личной области данных модуля
              ModuleData->ModuleData=data=new TCAutoCompData();
              // Чтение параметров из INI-файла
              data->ReadFromIni(ModuleData->DataFile);
              break;

            // Очистка данных модуля
            case RDS_COMPM_CLEANUP:
              delete data;	// Удаление личной области модуля
              break;

            // Вызов окна настройки модуля
            case RDS_COMPM_SETUP:
              data->Setup(ModuleData->DataFile);
              break;

            // Получение поддерживаемых модулем функций
            // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
            //   в противном случае необходимо использовать версии функций
            //   с суффиксом "W" и символьные константы с префиксом "L"
            case RDS_COMPM_GETOPTIONS:
              // Название поля ввода имени модели
              rdscompReturnModelNameLabel("Файл исходного текста:");
              // Разрешенные кнопки
              return RDS_COMPFLAG_FUNCMODELBROWSE | // Обзор
                     RDS_COMPFLAG_FUNCMODELCREATE;  // Новый
            // Выполнить функцию
            case RDS_COMPM_EXECFUNCTION:
              // Что произошло в окне параметров
              funcdata=(RDS_PCOMPEXECFUNCDATA)ExtParam;
              switch(funcdata->Function)
                { // Нажата кнопка "Новый"
                  case RDS_COMPFLAG_FUNCMODELCREATE:
                    data->CreateEmptyModel();
                    break;
                  // Нажата кнопка "Обзор"
                  case RDS_COMPFLAG_FUNCMODELBROWSE:
                    data->ConnectExistingModel(funcdata->ModelName);
                    break;
                }
              break;
            // Проверка возможности присоединения модели к блоку
            case RDS_COMPM_CANATTACHBLK:
              return data->CanAttachBlock((RDS_PCOMPCANATTACHBLKDATA)ExtParam);
            // Открыть окно редактора
            case RDS_COMPM_OPENEDITOR:
              data->OpenEditor((RDS_POPENEDITORDATA)ExtParam);
              break;
          }
        return RDS_COMPR_DONE;
      }
      //=========================================

В функцию добавлены четыре новых реакции. Когда пользователь выберет наш модуль автокомпиляции в выпадающем списке в окне параметров блока, функция TestCAutoComp будет вызвана с параметром CallMode, равным RDS_COMPM_GETOPTIONS. Реагируя на этот вызов, мы устанавливаем название поля ввода для имени модели функцией rdscompReturnModelNameLabel и возвращаем целое число, составленное из битовых флагов разрешенных кнопок. В данном случае мы возвращаем всего два флага: RDS_COMPFLAG_FUNCMODELBROWSE (разрешена кнопка «Обзор») и RDS_COMPFLAG_FUNCMODELCREATE (разрешена кнопка «Новый»). Таким образом, на вкладке «Компиляция» окна параметров блока будет запрещена кнопка «Сохранить как» и ввод имени модели вручную: пользователь сможет только либо создать для блока новую пустую модель, либо подключить к нему уже существующую.

Если пользователь нажмет на одну из этих кнопок, функция модуля будет вызвана с CallMode, равным RDS_COMPM_EXECFUNCTION, при этом в параметре ExtParam будет передан указатель на структуру RDS_COMPEXECFUNCDATA, описывающую нажатую кнопку:

      typedef struct
      { RDSINT32 Function;     // Функция (RDS_COMPFLAG_FUNC*)

        RDSCSTR ModelNameA;    // Содержимое строки имени модели (UTF8)
        RDSWCSTR ModelNameW;   // Содержимое строки имени модели (UTF16)
        // RDSXCSTR ModelName; // Содержимое строки имени модели (поле-псевдоним)

        // Следующие параметры могут быть не определены
        // для некоторых функций
        RDS_HOBJECT BlockVars; // Переменные блока (только для
                               // RDS_COMPFLAG_FUNCMODELCREATE)
        RDSINT32 BlockType;    // Тип блока
      } RDS_COMPEXECFUNCDATA;
      typedef RDS_COMPEXECFUNCDATA *RDS_PCOMPEXECFUNCDATA;// Указатель

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

Если поле Function равно константе RDS_COMPFLAG_FUNCMODELCREATE, значит, пользователь нажал на кнопку «Новый» – в этом случае нам необходимо создать новый файл модели, запросив предварительно у пользователя имя файла для него, и присоединить эту модель к блоку. Этим будет заниматься функция CreateEmptyModel. Если же Function равно RDS_COMPFLAG_FUNCMODELBROWSE, пользователь нажал на кнопку «Обзор», и нам нужно дать ему возможность выбрать модель для блока из находящихся на диске файлов. Этим будет заниматься функция ConnectExistingModel, в нее мы передадим текущее имя модели funcdata->ModelName, чтобы она использовала путь из этого имени в качестве начальной папки для диалога открытия. Все остальные кнопки на вкладке «Компиляция» у нас запрещены, поэтому и реакция на их нажатие нам не потребуется.

Перед присоединением автоматически компилируемой модели к блоку функция модуля будет вызвана с параметром RDS_COMPM_CANATTACHBLK, и в параметре ExtParam будет передан указатель на структуру RDS_COMPCANATTACHBLKDATA:

      typedef struct
      { RDSCSTR ModelNameA;       // Имя модели (UTF8)
        RDSWCSTR ModelNameW;      // Имя модели (UTF16)
        // RDSXCSTR ModelName;    // Имя модели (поле-псевдоним)

        RDSCSTR ModelNameUCA;     // Имя модели в верхнем регистре (UTF8)
        RDSWCSTR ModelNameUCW;    // Имя модели в верхнем регистре (UTF16)
        // RDSXCSTR ModelNameUC;  // Имя модели в верхнем регистре (поле-псевдоним)

        RDSCSTR AltModelNameA;    // Альтернативное имя модели (UTF8)
        RDSWCSTR AltModelNameW;   // Альтернативное имя модели (UTF16)
        // RDSXCSTR AltModelName; // Альтернативное имя модели (поле-псевдоним)

        RDS_BHANDLE Block;        // Идентификатор подключаемого блока
        RDSINT32 AttachReason;    // Причина подключения модели (RDS_COMP_AR_*)
        BOOL ChangeModel;         // Команда на подключение к блоку другой модели
      } RDS_COMPCANATTACHBLKDATA;
      // Указатель
      typedef RDS_COMPCANATTACHBLKDATA *RDS_PCOMPCANATTACHBLKDATA;

Этот вызов производится перед тем, как для модели будет создана структура параметров и личная область данных (см. рис. 122), поэтому функция модуля должна принять решение о возможности подключения модели к блоку, зная только идентификатор блока Block, имя модели ModelName (в поле ModelNameUC находится то же самое имя, но преобразованное в верхний регистр) и альтернативное имя модели AltModelName, если оно есть. Реагируя на этот вызов, функция должна вернуть константу RDS_COMPR_DONE, если данную модель можно подключить к данному блоку, RDS_COMPR_ERROR, если подключение невозможно и нужно сообщить об этом пользователю, или RDS_COMPR_ERRORNOMSG, если подключение невозможно, но сообщать пользователю об этом не нужно (например, если функция уже вывела сообщение об ошибке). Кроме того, функция может записать в поле ChangeModel значение TRUE и установить новое имя модели вызовом rdscompAttachDifferentModel, если вместо запрошенной нужно подключить к блоку какую-то другую модель. В нашем случае возможность подключения модели к блоку будет проверять функция нашего класса CanAttachBlock, которая сама сообщит пользователю об ошибке и вернет нужную константу. Внутри нее мы просто будем проверять тип блока: наши модели могут быть подключены только к простым блокам, поэтому, если блок в поле Block структуры RDS_COMPCANATTACHBLKDATA будет иметь другой тип, функция выведет сообщение об ошибке.

Наконец, если пользователь скомандует открыть окно редактора подключенной к блоку автокомпилируемой модели, функция модуля будет вызвана с параметром RDS_COMPM_OPENEDITOR, и в ExtParam будет находиться указатель на структуру RDS_OPENEDITORDATA, в полях которой содержится указатель на структуру данных модели, для которой нужно вызвать редактор, и идентификатор блока, с которым в данный момент работает пользователь (в подавляющем большинстве случаев эта информация не нужна):

      typedef struct
      { RDS_PCOMPMODELDATA Model; // Данные модели
        RDS_BHANDLE Block;        // Идентификатор блока
      } RDS_OPENEDITORDATA;
      typedef RDS_OPENEDITORDATA *RDS_POPENEDITORDATA; // Указатель

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

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

      // Проверка возможности назначения модели блоку
      int TCAutoCompData::CanAttachBlock(RDS_COMPCANATTACHBLKDATA *param)
      { RDS_BLOCKDESCRIPTION blockdescr;
        BOOL ok;

        // Получаем описание блока, к которому подключается модель
        blockdescr.servSize=sizeof(blockdescr);
        rdsGetBlockDescription(param->Block,&blockdescr);
        // Блок должен быть простого типа, иначе наша модель со
        // статическими переменными не сможет сним работать
        ok=(blockdescr.BlockType==RDS_BTSIMPLEBLOCK);
        // Если модель подключается вручную, выводим сообщение
        if(param->AttachReason==RDS_COMP_AR_MANUALSET && (!ok))
          { ModelErrorMsg(param->ModelName,MEC_SIMPLEBLOCK);
            return RDS_COMPR_ERRORNOMSG; // Без сообщения пользователю
          }
        return ok?RDS_COMPR_DONE:RDS_COMPR_ERROR;
      }
      //=========================================

Для того, чтобы узнать тип блока, к которому подключается модель (его идентификатор находится в поле Block структуры RDS_COMPCANATTACHBLKDATA, указатель на которую передан в параметре функции param), мы должны заполнить структуру его описания blockdescr функцией rdsGetBlockDescription. Если это не простой блок, то есть если поле BlockType структуры описания блока не равно RDS_BTSIMPLEBLOCK, модель не сможет с ним работать – переменной ok будет присвоено значение FALSE. В этом случае наша функция должна вернуть RDS одну из двух констант, сигнализирующих об ошибке: RDS_COMPR_ERROR или RDS_COMPR_ERRORNOMSG. Если модель вручную подключается к блоку пользователем, мы можем сами вывести ему сообщение об ошибке, указав ее конкретную причину. При ручном подключении модели поле AttachReason структуры, переданной в параметрах функции, будет содержать константу RDS_COMP_AR_MANUALSET – в этом случае мы показываем пользователю сообщение об ошибке при помощи написанной нами ранее функции ModelErrorMsg (кроме кода ошибки в нее передается имя модели, взятое из поля param->ModelName) и возвращаем константу RDS_COMPR_ERRORNOMSG, поскольку сообщение уже выведено и RDS уже не нужно этого делать. При подключении модели к блоку в процессе загрузки блока или схемы выводить сообщение пользователю в отдельном окне не следует, поскольку загрузка приостановится, пока он не нажмет «OK» в модальном окне сообщения. Тем не менее, предупредить его об ошибке надо, поэтому в этом случае мы возвращаем константу RDS_COMPR_ERROR: RDS добавит эту ошибку в общий список ошибок и покажет их пользователю по окончании загрузки. Это сообщение будет не таким конкретным, как то, которое мы выводим вручную, тем не менее, оно укажет пользователю на блок, в котором возникла ошибка.

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

      // Записать строку текста в файл
      BOOL WriteString(HANDLE file,const char *text)
      { DWORD res,size;
        size=strlen(text);
        if(!WriteFile(file,text,size,&res,NULL))
          return FALSE;
        return (res==size);
      }
      //=========================================

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

Кроме нее, нам нужна будет функция, которая загрузит в память текстовый файл с заданным именем. Причем нужно написать ее так, чтобы, при желании, можно было загрузить только часть файла, чтобы проверить, начинается ли он с «$TESTCMODEL» (то есть является ли он файлом модели в нашем формате).

      // Загрузка текстового файла в память
      // filename – имя файла, maxread – сколько читать или 0
      // для чтения всего файла
      char *ReadTextFile(const char *filename,DWORD maxread)
      { HANDLE f;
        DWORD size,actread;
        char *fullpath,*buffer;
        BOOL ok=TRUE;
        RDSWSTR fullpath_w;

        // Получаем полный путь к файлу
        fullpath=rdsGetFullFilePath(filename,NULL,NULL);
        if(fullpath==NULL) // Нет пути - ошибка
          return NULL;

        // Преобразуем путь в UTF16 для Windows
        fullpath_w=rdsUTF8toUTF16(fullpath,FALSE);

        // Открываем файл для чтения
        f=CreateFileW(fullpath_w,GENERIC_READ,0,NULL,OPEN_EXISTING,0,NULL);
        rdsFree(fullpath); // Имя файла больше не нужно
        rdsFree(fullpath_w);
        if(f==INVALID_HANDLE_VALUE) // Ошибка открытия
          return NULL;

        // Определяем размер файла
        size=GetFileSize(f,NULL);
        if(size==0xFFFFFFFF) // Ошибка или слишком большой
          { CloseHandle(f);
            return NULL;
          }

        // Если есть ограничение, читаем только часть файла
        if(maxread!=0 && maxread<size)
          size=maxread;

        // Отводим память для загрузки файла
        buffer=(char*)rdsAllocate(size+1);
        if(buffer==NULL) // Не удалось отвести
          { CloseHandle(f);
            return NULL;
          }

        // Считываем файл в память, если он не пустой
        if(size)
          { if(ReadFile(f,buffer,size,&actread,NULL))
             ok=(actread==size);
            else
             ok=FALSE;
          }
        // Закрываем файл
        CloseHandle(f);
        if(!ok) // Ошибка чтения
          { rdsFree(buffer);
            return NULL;
          }
        // Дописываем после конца считанного текста нулевой байт
        buffer[size]=0;
        return buffer;
      }
      //=========================================

В эту функцию передается имя файла filename (возможно, с символическими константами, обозначающими стандартные пути) и максимальный размер считываемого текста maxread (чтобы считать весь файл, нужно передать в этом параметре 0). Функция возвращает указатель на динамически отведенную функцией rdsAllocate строку, в которой находится текст, загруженный из файла, либо NULL при ошибке.

Мы не будем подробно разбирать эту функцию – в основном, она состоит из различных вызовов Windows API для работы с файлами. Следует только обратить внимание на то, что перед открытием файла переданное в параметре filename имя преобразуется в динамическую строку с полным путем к файлу fullpath, которая после использования освобождается вызовом rdsFree. Таким образом, если в эту функцию будет передано имя файла без пути, она автоматически добавит к нему путь к загруженной в данный момент схеме, а если в имени файла будут присутствовать символические обозначения стандартных путей RDS («$DLL$», «$INI$» и т.п.), они будут заменены на сами эти пути. Кроме того, перед вызовом функции Windows API CreateFileW для открытия файла, полный путь преобразуется в принятую в Windows кодировку UTF16 при помощи сервисной функции RDS rdsUTF8toUTF16.

Теперь мы можем написать функцию CreateEmptyModel, которая вызывается при нажатии пользователем кнопки «Новый» на вкладке «Компиляция» окна параметров блока:

      // Создать новый пустой файл модели
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void TCAutoCompData::CreateEmptyModel(void)
      { // Текст пустой модели
        const char *text=TCTEXTSECTION_START "\r\n"
                         TCTEXTSECTION_VARS  "\r\n"
                         "struct\r\nbegin\r\n"
                         " signal name \"Start\" in run default 1\r\n"
                         " signal name \"Ready\" out default 0\r\n"
                         "end\r\n"
                         TCTEXTSECTION_PROG  "\r\n";
        char *relpath,*fullpath;
        HANDLE file;
        BOOL ok;
        RDSWSTR fullpath_w;

        // Вызываем диалог сохранения
        relpath=rdsCallFileDialog("",
                    RDS_CFD_SAVE|RDS_CFD_OVERWRITEPROMPT,
                    "Текстовые файлы (*.txt)|*.txt\nВсе файлы|*.*",
                    "txt",
                    "Новая модель");
        if(relpath==NULL) // Пользователь нажал "Отмена"
          return;

        // Преобразуем относительный путь в полный
        fullpath=rdsGetFullFilePath(relpath,NULL,NULL);
        // Преобразуем путь в UTF16 для Windows
        fullpath_w=rdsUTF8toUTF16(fullpath,FALSE);

        // Открываем файл для записи
        file=CreateFileW(fullpath_w,GENERIC_WRITE,0,NULL,CREATE_ALWAYS,0,NULL);
        rdsFree(fullpath); // Полный путь больше не нужен
        rdsFree(fullpath_w);
        if(file==INVALID_HANDLE_VALUE) // Ошибка открытия
          ok=FALSE;
        else
          { ok=WriteString(file,text);
            CloseHandle(file);
          }
        if(!ok) // Ошибка
          { ModelErrorMsg(relpath,MEC_MODELWRITEERROR);
            return;
          }

        // Пустой файл модели записан – устанавливаем его имя
        // в качестве имени модели блока
        rdscompReturnModelName(relpath);
        // Освобождаем динамическую строку
        rdsFree(relpath);
      }
      //=========================================

В самом начале функции мы объявляем указатель text на текст, который мы будем использовать в качестве пустой модели блока. Секция переменных этой модели «$VARS» содержит только два обязательных для простого блока сигнала, а секция исходного текста «$PROG» пуста. Затем мы вызываем сервисную функцию rdsCallFileDialog, которая откроет диалог выбора файла (в данном случае – диалог сохранения) и вернет динамическую строку с именем файла, выбранного пользователем:

      RDSSTR RDSCALL rdsCallFileDialogA(
        RDSCSTR initialfile,   // Исходное имя файла (UTF8)
        DWORD flags,           // Флаги
        RDSCSTR filter,        // Фильтры файлов (UTF8)
        RDSCSTR defext,        // Расширение по умолчанию (UTF8)
        RDSCSTR title);        // Название диалога (UTF8)
      RDSWSTR RDSCALL rdsCallFileDialogW(
        RDSWCSTR initialfile,  // Исходное имя файла (UTF16)
        DWORD flags,           // Флаги
        RDSWCSTR filter,       // Фильтры файлов (UTF16)
        RDSWCSTR defext,       // Расширение по умолчанию (UTF16)
        RDSWCSTR title);       // Название диалога (UTF16)
      // Функция-псевдоним
      RDSXSTR RDSCALL rdsCallFileDialog(
        RDSXCSTR initialfile,  // Исходное имя файла (кодировка по умолчанию)
        DWORD flags,           // Флаги
        RDSXCSTR filter,       // Фильтры файлов (кодировка по умолчанию)
        RDSXCSTR defext,       // Расширение по умолчанию (кодировка по умолчанию)
        RDSXCSTR title);       // Название диалога (кодировка по умолчанию)

В первом параметре этой функции передается имя файла, который должен быть выбран в диалоге в момент его открытия. В данном случае мы передаем пустую строку – файл пока не выбран. Во втором параметре (flags) указываются битовые флаги, определяющие внешний вид и поведение диалога. Мы передаем два флага: RDS_CFD_SAVE (нам нужен диалог сохранения) и RDS_CFD_OVERWRITEPROMPT (при выборе уже существующего файла предупреждать пользователя о том, что он будет перезаписан). В третьем параметре передается строка шаблонов имен файлов, которые будут показаны в диалоге – мы уже встречались с такими строками в полях ввода для выбора файлов в окнах настройки блоков (см. §2.13.6). Здесь нас интересуют текстовые файлы, поэтому в этой строке мы записали два шаблона: текстовые файлы (*.txt) и все файлы (*.*). В двух последних параметрах передаются расширение выбранного файла по умолчанию (оно будет автоматически добавлено к имени файла, если пользователь не укажет расширение) и название диалога, которое будет видеть пользователь.

Если пользователь выйдет из диалога, не выбрав файл, функция rdsCallFileDialog вернет значение NULL, в противном случае она преобразует имя выбранного пользователем файла в динамическую строку, заменив в ней стандартные пути на символические константы и убрав из нее путь, если он совпадает с путем к загруженной в данный момент схеме, сформировав таким образом относительный путь к выбранному пользователем файлу. Этот относительный путь мы записываем в переменную relpath (он нам еще понадобится), после чего вызовом rdsGetFullFilePath преобразуем его в полный путь, который записываем в переменную fullpath, после чего переводим его в кодировку UTF16 и записываем в переменную fullpath_w. Может возникнуть вопрос: зачем мы получаем из функции rdsCallFileDialog относительный путь, если потом мы все равно преобразуем его в полный? Можно было бы передать в rdsCallFileDialog флаг RDS_CFD_ABSPATH – в этом случае она сразу вернула бы полный путь. Однако, нам нужны будут оба этих пути: относительный мы используем в качестве имени автокомпилируемой модели, которое будет храниться в параметрах блока (так нам не потребуется корректировать имена моделей при переносе схемы в другую папку или на другую машину, если схема и модели будут находится в одной папке), а абсолютный – для непосредственной работы с файлом.

Далее вызовом Windows API CreateFileW мы открываем для записи файл, путь к которому находится в переменной fullpath_w, и присваиваем его дескриптор переменной file (сразу после этого строки fullpath и fullpath_w мы освобождаем – они больше не нужны). Если при открытии файла не возникло никаких ошибок, мы записываем в него текст пустой модели text при помощи написанной нами ранее функции WriteString, после чего закрываем файл функцией Windows API CloseHandle. Если в процессе записи возникли ошибки, мы выводим об этом сообщение пользователю.

Теперь, когда новый файл модели записан, нужно установить его имя relpath в качестве имени модели данного блока. Для этого используется функция rdscompReturnModelName – RDS реагирует на ее вызов только в реакциях на действия пользователя на вкладке «Компиляция» окна параметров блока, то есть только при вызове функции модуля с параметром RDS_COMPM_EXECFUNCTION. Наша функция CreateEmptyModel вызывается именно из такой реакции, поэтому вызов rdscompReturnModelName(relpath) выполнится правильно: текст relpath будет записан в поле ввода имени модели в окне параметров блока.

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

      // Выбрать файл модели
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void TCAutoCompData::ConnectExistingModel(const char *oldmodel)
      { char *relpath,*buf;
        BOOL ok=FALSE;
        // Длина текста "$TESTCMODEL"
        int prefixlen=strlen(TCTEXTSECTION_START);

        // Вызываем диалог открытия файла
        relpath=rdsCallFileDialog(oldmodel,
                    RDS_CFD_OPEN|RDS_CFD_MUSTEXIST,
                    "Текстовые файлы (*.txt)|*.txt\nВсе файлы|*.*",
                    "txt",
                    "Файл модели");
        if(relpath==NULL) // Пользователь нажал "Отмена"
          return;

        // Читаем начало файла: является ли он нашей моделью?
        buf=ReadTextFile(relpath,prefixlen);
        if(buf==NULL) // Ошибка чтения
          ModelErrorMsg(relpath,MEC_READERROR);
        else if(strcmp(buf,TCTEXTSECTION_START)) // Плохой префикс
          ModelErrorMsg(relpath,MEC_NOTAMODEL);
        else
          ok=TRUE;
        rdsFree(buf); // Считанный текст больше не нужен

        if(ok) // Делаем выбранный файл именем модели блока
          rdscompReturnModelName(relpath);
        rdsFree(relpath);
      }
      //=========================================

В этой функции мы тоже вызываем rdsCallFileDialog для открытия диалога выбора файла, но здесь мы передаем исходное имя файла oldmodel (диалог откроется в папке этого файла) и другие флаги: RDS_CFD_OPEN (нам нужен диалог открытия) и RDS_CFD_MUSTEXIST (выбранный файл должен существовать). Имя файла relpath, возвращенное функцией rdsCallFileDialog, мы передаем в написанную нами ранее функцию ReadTextFile, которая считает в память начало этого файла (эту функцию мы написали так, чтобы она могла работать с относительными путями к файлам). Размер считываемой части файла мы ограничиваем длиной строки TCTEXTSECTION_START («$TESTCMODEL»). Если данные из файла прочитаны успешно, мы сравниваем их с «$TESTCMODEL» и при несовпадении сообщаем пользователю, что указанный файл не является моделью блока в нашем формате. Если же начало файла совпало со строкой «$TESTCMODEL», мы делаем имя этого файла именем модели блока вызовом rdscompReturnModelName.

У написанной функции есть один недостаток: если файл модели будет создан в стороннем текстовом редакторе и записан с начальным маркером кодировки UTF8 («UTF8-BOM»), файл не будет опознан как допустимый, хотя и будет им. Нам следовало бы сначала проверить первые три байта на этот маркер (последовательность 0xEF, 0xBB, 0xBF) и пропустить ее при обнаружении. Однако, мы не будем этого делать, чтобы не усложнять пример. Функция редактирования модели будет записывать текстовый файл без маркера, поэтому проверка в функции ConnectExistingModel будет работать нормально.

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

Осталось добавить в наш модуль редактор моделей, то есть написать функцию OpenEditor. Но прежде мы напишем еще две вспомогательных функции, которые понадобятся нам для разбора текстового файла модели. Первая из них будет искать в тексте заданное ключевое слово, которое должно обязательно находиться в начале строки (мы будем использовать ее для поиска в тексте файла модели слов «$VARS» и «$PROG») и возвращать указатель на него:

      // Найти ключевое слово в начале строки текста
      char *FindKeywordAtLineStart(char *text,const char *word)
      { char *s=text; // Устанавливаем s на начало текста
        if(text==NULL) return NULL;
        // Ищем в цикле
        for(;;)
          { s=strstr(s,word); // Ищем word начиная с s
            if(s==NULL) // Не найдено
              return NULL;
            if(s==text) // Найдено в начале текста - годится
              return s;
            // Найдено в середине текста - перед s должен находиться
            // перевод строки
            if(s[-1]=='\r' || s[-1]=='\n') // Есть перевод строки
              return s;
            // Перед s - другой символ. Продолжаем поиск
          }
      }
      //=========================================

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

Вторая необходимая нам вспомогательная функция будет разбивать загруженный в память текст файла модели на описание переменных (текст после строки «$VARS») и исходный текст реакции на такт моделирования (текст после строки «$PROG»):

      // Разбить текст модели на описание переменных и текст программы
      BOOL ProcessModelText(
               char *text,   // текст модели в памяти
               char **pVars, // возвращаемое начало переменных
               char **pProg) // возвращаемое начало программы
      { char *s,*s1;

        if(text==NULL || pVars==NULL || pProg==NULL)
          return FALSE;

        // Ищем "$VARS" в начале строки
        s=FindKeywordAtLineStart(text,TCTEXTSECTION_VARS);
        if(s==NULL) // Нет такой строки
          return FALSE;
        // Найдено - записываем в pVars начало текста после этого
        // слова с пропуском всех пустых строк
        s+=strlen(TCTEXTSECTION_VARS); // Пропускаем название секции
        s+=strspn(s,"\r\n"); // Пропускаем пустые строки после названия
        *pVars=s;

        // Ищем "$PROG" в начале строки оставшегося текста
        s1=FindKeywordAtLineStart(s,TCTEXTSECTION_PROG);
        if(s1==NULL) // Нет такой строки
          return FALSE;
        // Записываем в pProg начало текста после этого слова
        // с пропуском всех пустых строк
        s=s1+strlen(TCTEXTSECTION_PROG); // Пропускаем название
        s+=strspn(s,"\r\n"); // Пропускаем пустые строки после названия
        *pProg=s;
        // Для завершения предыдущей секции записываем нулевой байт
        // вместо '$' в "$PROG"
        *s1=0;
        return TRUE;
      }
      //=========================================

Эта функция возвращает TRUE, если в переданном ей тексте удалось найти оба описания, и FALSE, если хотя бы одно из них отсутствует. Она использует другую нашу вспомогательную функцию – FindKeywordAtLineStart. Сначала мы ищем в переданном в функцию тексте text ключевое слово TCTEXTSECTION_VARS («$VARS»). Если оно отсутствует, функция возвращает FALSE: текст модели имеет неправильный формат. В противном случае в переменную s записывается указатель на начало текста после слова «$VARS» и всех пустых строк, которые могут за ним следовать. Для пропуска самого ключевого слова мы просто добавляем к s длину этого слова, а для пропуска всех переводов строк и возвратов каретки после него используем библиотечную функцию strspn. После этого найденное начало описания переменных мы записываем в переменную, на которую указывает параметр функции pVars.

Затем мы точно так же ищем в оставшейся части текста ключевое слово TCTEXTSECTION_PROG («$PROG») и записываем в переменную, на которую указывает параметр функции pProg, указатель на начало текста после этого ключевого слова и всех пустых строк, которые могут за ним следовать. Кроме того, знак «$» в слове «$PROG» мы заменяем на нулевой байт, чтобы описание переменных, расположенное перед этим словом, завершалось нулем, и с ним можно было работать как с отдельной строкой.

Окно редактора модели мы будем открывать с помощью того же самого вспомогательного объекта RDS, который мы использовали в окнах настройки блоков и для настройки модуля автокомпиляции. Редактор модели должен позволять вводить структуру переменных блока и текст реакции на такт моделирования, поэтому мы сделаем в нем кнопку для вызова редактора переменных RDS (мы уже делали так в §2.16.1) и многострочное поле ввода для текста реакции. Для реакции на нажатие кнопки нам потребуется функция обратного вызова объекта-окна (см. §2.7.3), поэтому кроме самой функции OpenEditor нам придется написать еще и эту функцию обратного вызова. Пока напишем только ее прототип:

      // Прототип функции обратного вызова окна редактора
      void RDSCALL TCAutoCompData_EditorCallback(
          RDS_HOBJECT window,          // Объект-окно
          RDS_PFORMSERVFUNCDATA data); // Данные

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

      // Открыть редактор модели
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void TCAutoCompData::OpenEditor(RDS_OPENEDITORDATA *param)
      { char *modelpath,*modeltext;
        char *vars,*prog;
        RDS_HOBJECT win;
        BOOL ok;

        // Полный путь к файлу модели
        modelpath=rdsGetFullFilePath(param->Model->ModelName,NULL,NULL);

        // Читаем весь файл модели в память
        modeltext=ReadTextFile(modelpath,0);
        if(modeltext==NULL) // Ошибка чтения
          { ModelErrorMsg(param->Model->ModelName,MEC_READERROR);
            return;
          }

        // Разбиваем загруженный текст на описание переменных и программу
        if(!ProcessModelText(modeltext,&vars,&prog))
          { rdsFree(modeltext);
            ModelErrorMsg(param->Model->ModelName,MEC_NOSECTIONS);
            return;
          }

        // Создаем объект-окно редактора
        win=rdsFORMCreate(FALSE,-1,-1,param->Model->ModelName);

        // Создаем в нем невизуальное поле и записываем в
        // него описание переменных
        rdsFORMAddEdit(win,0,1000,RDS_FORMCTRL_NONVISUAL,NULL,0);
        rdsSetObjectStr(win,1000,RDS_FORMVAL_VALUE,vars);

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

        // Создаем многострочное поле ввода для текста программы
        rdsFORMAddEdit(win,0,2,RDS_FORMCTRL_MULTILINE,
            "Такт расчета:",600);
        rdsSetObjectStr(win,2,RDS_FORMVAL_VALUE,prog);
        rdsSetObjectInt(win,2,RDS_FORMVAL_MLHEIGHT,5*24);// Высота
        rdsSetObjectInt(win,2,RDS_FORMVAL_MLRETURNS,1); // Enter

        // Загруженный текст модели больше не нужен – мы все переписали в поля окна
        rdsFree(modeltext);

        // Открываем окно (с функцией обратного вызова)
        ok=rdsFORMShowModalServ(win,TCAutoCompData_EditorCallback);
        if(ok)
          { // Нажата кнопка "OK" - записываем текст модели в файл
            HANDLE file;
            RDSWSTR modelpath_w=rdsUTF8toUTF16(modelpath,FALSE);
            file=CreateFileW(modelpath_w,GENERIC_WRITE,0,NULL,
                     CREATE_ALWAYS,0,NULL);
            rdsFree(modelpath_w);
            if(file==INVALID_HANDLE_VALUE) // Ошибка
              ok=FALSE;
            else // Записываем в файл
              { // Заголовок и секция переменных
                ok=WriteString(file,TCTEXTSECTION_START "\r\n"
                                    TCTEXTSECTION_VARS  "\r\n");
                // Описание переменных
                ok=ok && WriteString(file,rdsGetObjectStr(win,1000,RDS_FORMVAL_VALUE));
                // Секция текста программы
                ok=ok && WriteString(file,TCTEXTSECTION_PROG "\r\n");
                // Текст программы
                ok=ok && WriteString(file,rdsGetObjectStr(win,2,RDS_FORMVAL_VALUE));
                CloseHandle(file);
              }
            if(!ok)
              ModelErrorMsg(param->Model->ModelName,MEC_MODELWRITEERROR);
          }

        // Уничтожаем окно
        rdsDeleteObject(win);
        // Освобождаем строку с именем файла модели
        rdsFree(modelpath);
      }
      //=========================================

В параметре param этой функции передается указатель на структуру RDS_OPENEDITORDATA, в поле Model которой содержится, в свою очередь, указатель на структуру данных модели RDS_COMPMODELDATA. Таким образом, для получения имени модели блока внутри функции OpenEditor нам нужно обратиться к param->Model->ModelName. Имена моделей у нас представляют собой имена файлов с относительными или символическими путями, поэтому для работы с файлом модели мы преобразуем ее имя в полный путь к файлу сервисной функцией rdsGetFullFilePath и записываем его в переменную modelpath (эту динамическую строку нам нужно будет потом освободить вызовом rdsFree). Затем мы загружаем весь текст файла модели в динамическую строку modeltext при помощи написанной нами ранее функции ReadTextFile. Функция ReadTextFile может работать и с относительными путями, поэтому в ее параметре можно передать как полный путь к файлу модели modelpath, так и относительный param->Model->ModelName. Полный путь нам обязательно потребуется позже, когда мы будем записывать внесенные пользователем изменения обратно в файл модели: там мы будем пользоваться функциями Windows API.

Если текст модели считан в память без ошибок (значение modeltext не равно NULL), мы выделяем из нее текст описания переменных vars и текст реакции на такт расчета prog вызовом функции ProcessModelText. Если эта функция вернет TRUE, vars и prog будут указывать на соответствующие блоки текста, каждый из которых является независимой строкой, то есть завершается нулевым байтом (текст реакции prog располагается в конце файла модели, поэтому нулевой байт в его конце обеспечивает функция загрузки текста ReadTextFile, а нулевой байт в конце описания переменных vars вставляет функция ProcessModelText).

Разобрав текст модели на две части, мы создаем объект-окно win и добавляем в него три поля: невидимое поле RDS_FORMCTRL_NONVISUAL с идентификатором 1000, в которое мы записываем текст описания переменных vars (оно нужно нам только для того, чтобы иметь доступ к этому тексту из функции TCAutoCompData_EditorCallback, которая будет вызываться при нажатии на кнопку в нашем окне), кнопку RDS_FORMCTRL_BUTTON с идентификатором 1 для вызова редактора переменных, и многострочное поле ввода RDS_FORMCTRL_MULTILINE с идентификатором 2, в которое мы записываем текст реакции модели на такт расчета prog. В многострочном поле мы разрешаем вставку перевода строки клавишей Enter, вызвав для него функцию rdsSetObjectInt с параметрами RDS_FORMVAL_MLRETURNS и 1 – без этого вызова нажатие клавиши Enter приводило бы к автоматическому нажатию кнопки окна по умолчанию, то есть «OK». Хотя в этом случае пользователь все равно мог вставить перевод строки в поле, нажав Ctrl + Enter, использование одной клавиши Enter будет для него более привычным. После создания полей ввода мы открываем получившееся модальное окно функцией rdsFORMShowModalServ.

Если пользователь закрыл окно кнопкой «OK» (rdsFORMShowModalServ вернула TRUE), мы должны записать изменения, внесенные пользователем, обратно в файл модели. Для этого мы переводим полный путь к файлу модели modelpath в кодировку UTF16 функцией rdsUTF8toUTF16, открываем файл для записи функцией Windows API CreateFileW и записываем туда последовательно заголовок файла («$TESTCMODEL»), заголовок секции переменных («$VARS»), текст описания переменных из поля 1000 (редактированием этого текста занимается еще не написанная нами функция обратного вызова TCAutoCompData_EditorCallback), заголовок секции исходного текста («$PROG») и сам текст реакции на такт расчета из поля ввода 2. Если при записи возникли ошибки, мы сообщаем об этом пользователю. В конце функции OpenEditor мы уничтожаем все динамически созданные объекты.

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

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

        switch(data->Event)
          { case RDS_FORMSERVEVENT_CLICK: // Нажата кнопка
              // Создаем объект для редактирования переменных
              dv=rdsVSCreateEditor();
              // Заполняем объект описанием переменных (поле 1000)
              if(!rdsVSCreateByDescr(dv,
                      rdsGetObjectStr(window,1000,RDS_FORMVAL_VALUE)))
                break;
              // Вызываем для объекта редактор переменных
              if(!rdsVSExecuteEditor(dv,TRUE,RDS_HVAR_FALLPLAIN,
                      0,"Переменные блока"))
                break;
              // Пользователь нажал "OK" – получаем текст описания
              // измененных переменных
              vdescr.servSize=sizeof(vdescr);
              if(!rdsVSGetVarDescription(dv,-1,&vdescr))
                break;
              varstr=rdsCreateVarDescriptionString(vdescr.Var,TRUE,0,NULL);
              if(varstr)
                { // Заносим обратно в поле 1000
                  rdsSetObjectStr(win,1000,RDS_FORMVAL_VALUE,vars);
                  rdsFree(varstr);
                }
              break;
          }
        // Уничтожаем объект-редактор, если он был создан
        rdsDeleteObject(dv);
      }
      //=========================================

В этой функции мы проверяем событие (data->Event), из-за которого она вызвана окном редактора модели, и, если это нажатие кнопки (RDS_FORMSERVEVENT_CLICK), начинаем подготовку к вызову редактора переменных. Мы не проверяем, какая именно кнопка нажата – в нашем окне кнопка всего одна. Сначала мы создаем вспомогательный объект-редактор dv функцией rdsVSCreateEditor, после чего загружаем в него текстовое описание структуры переменных, находящееся в невидимом поле 1000 при помощи функции rdsVSCreateByDescr (идентификатор объекта-окна, из которого вызвана функция TCAutoCompData_EditorCallback и которому принадлежит это поле, передается в параметре window). Теперь можно открыть редактор переменных функцией rdsVSExecuteEditor. В наших моделях мы разрешаем пользователю использовать только простые переменные, поэтому при вызове этой функции мы указываем флаг RDS_HVAR_FALLPLAIN. Если пользователь нажал «OK», функция rdsVSExecuteEditor вернет TRUE – в этом случае мы получим идентификатор структуры, находящейся в объекте dv, при помощи функции rdsVSGetVarDescription, после чего сформируем динамическую строку с ее описанием функцией rdsCreateVarDescriptionString и запишем ее обратно в поле окна с идентификатором 1000. Теперь в поле 1000 будет находиться описание структуры переменных блока, отредактированное пользователем.

Наш модуль автокомпиляции еще не умеет делать то, для чего он предназначен – компилировать модели блоков. Тем не менее, некоторые его функции уже можно проверить. Поскольку наш модуль уже зарегистрирован в RDS (мы сделали это в §3.2), мы можем создать для какого-нибудь блока новую модель и ввести в нее переменные и текст реакции на такт расчета. Для этого следует создать новый блок, в окне его параметров выбрать вкладку «Компиляция» (рис. 125), включить флаг «Функция блока компилируется автоматически» и выбрать в выпадающем списке модулей название, которое мы дали нашему модулю при регистрации.

Рис. 125. Подключение модели к блоку через созданный модуль автокомпиляции

Поле ввода имени модели и кнопка «Сохранить как» при этом станут запрещенными: при вызове нашего модуля с параметром RDS_COMPM_GETOPTIONS мы разрешили пользователю использовать только кнопки «Обзор» и «Новый». Нажатие на кнопку «Новый» вызовет диалог сохранения файла, и, при вводе в нем какого-либо имени, будет записан пустой файл модели. Если у нас уже есть какой-нибудь файл модели (например, набранный в любом текстовом редакторе текст), можно подключить его к блоку нажатием кнопки «Обзор». В любом из этих случаев после выбора имени файла оно должно появиться в поле ввода имени модели. Если после этого закрыть окно параметров блока кнопкой «ОК», RDS автоматически откроет созданный нами редактор модели (рис. 126).

Рис. 126. Редактор модели

В нижней части окна можно вводить текст реакции блока на такт расчета в синтаксисе языка C, в верхней находится кнопка для вызова редактора переменных. Если нажать на нее, откроется обычное окно редактора переменных блока (рис. 127).

Рис. 127. Редактор переменных, вызванный из редактора модели

При закрытии окна редактора модели все внесенные в структуру переменных и в текст реакции изменения должны записаться обратно в файл – это можно увидеть, открыв этот файл в каком-либо текстовом редакторе.