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

Описания RDS

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

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

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

§2.16.2. Добавление и удаление блоков и связей

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

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

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

#### §2.16.2. Добавление и удаление блоков и связей

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

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

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

Сначала определимся с форматом текстовых файлов. Будем использовать формат CSV – comma separated values («значения, разделенные запятой»). Это очень простой формат, в нем на каждой строчке текстового файла перечисляются какие-либо значения, разделенные запятыми. Он часто используется в разных программах, и в RDS есть вспомогательный объект для его автоматического разбора.

Строки файла со списком блоков будут иметь следующий вид:

      имя_блока, x, y

где x и y – координаты точки привязки блока (для блоков с векторной картинкой – начало координат этой картинки, для остальных – верхний левый угол описывающего прямоугольника). Например, строчка «Block1, 10, 50» означает добавление в подсистему блока с именем «Block1» и координатами (10, 50). Здесь указывается только имя добавляемого блока, и нет никакой информации о том, что это за блок. Для простоты мы будем добавлять в схему одинаковые блоки из какого-либо файла. Имя файла с описанием блока нужно будет задавать в настройках нашего изменяющего схему блока.

В строках файла со списком связей будут указываться имена соединяемых блоков и переменных:

      имя_блока_1, имя_переменной_1, имя_блока_2, имя_переменной_2

Например, строка «Block1, x, Block2, y» описывает связь, соединяющую переменную «x» блока «Block1» с переменной «y» блока «Block2». Чтобы не усложнять пример, мы будем соединять блоки прямыми связями, проходящими через их геометрические центры. При этом мы будем начинать и заканчивать связь не в самом геометрическом центре, а на границе блока (рис. 117), иначе схема будет выглядеть неряшливо.

Рис. 117. Соединение блоков прямой связью

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

Допустим, (x1, y1) – координаты центра блока, описывающий прямоугольник которого имеет ширину w и высоту h. Линия связи будет идти от этого блока в направлении точки (x2, y2) – это координаты центра второго блока, но сейчас нам это не важно. Необходимо найти координаты точки (xT, yT), в которой отрезок (x1, y1)–(x2, y2) пересекает границу нашего блока.

В зависимости от того, как точка (x2, y2) расположена относительно (x1, y1), отрезок пересечет одну из четырех сторон прямоугольника блока. Очевидно, что если x2 больше x1, то есть (x2, y2) находится правее (x1, y1), то отрезок не может пересекать левую сторону прямоугольника – он уходит вправо. Аналогично, если x2 меньше x1, отрезок не может пересекать правую сторону. Таким образом, сравнив x2 и x1, мы можем сразу выбросить из рассмотрения одну из двух вертикальных сторон и не искать пересечения с ней отрезка, соединяющего (x1, y1) и (x2, y2). Обозначим горизонтальную координату оставшейся вертикальной стороны через xВ (xВ = x1 ± w/2).

Точно так же мы можем выбросить из рассмотрения и одну из горизонтальных сторон: если отрезок уходит вверх (y2 меньше y1, не забываем о том, что вертикальная ось оконных координат перевернута), он не пересечет нижнюю сторону прямоугольника блока, если же он уходит вниз (y2 больше y1), можно не искать его пересечение с верхней стороной. Таким образом, из двух горизонтальных сторон мы будем проверять только одну – обозначим ее вертикальную координату через yГ (yГ = y1 ± h/2).

Теперь нам нужно определить, с какой из двух оставшихся сторон прямоугольника (одной вертикальной x=xВ и одной горизонтальной y=yГ) пересекается наш отрезок, и найти точку этого пересечения (xT, yT). На рис. 117 отрезок пересекает вертикальную сторону, однако, если бы точка (x2, y2) находилась выше, он мог бы пересечь горизонтальную, или обе сразу, если бы прошел через угол прямоугольника. Проще всего определить пересечение отрезка с обеими прямыми x=xВ и y=yГ, и выбрать из двух полученных точек ближайшую к центру блока, то есть к точке (x1, y1) – она и будет точкой пересечения отрезка со стороной прямоугольника.

Прямая, проходящая через точки (x1, y1) и (x2, y2), описывается следующими параметрическими уравнениями:

,

где t – параметр. Подстановка нулевого значения t в эти уравнения дает точку (x1, y1), подстановка t=1 – точку (x2, y2). Чем ближе значение t к нулю, тем ближе точка прямой будет к центру прямоугольника. Таким образом, если мы найдем точки пересечения отрезка с горизонтальной и вертикальной прямыми, нам будет нужна та из них, параметр t которой будет меньше.

Найдем параметр tВ точки пересечения отрезка (x1, y1)–(x2, y2) с вертикальной прямой x=xВ. В этой точке

,

то есть

.

Аналогично, параметр tГ точки пересечения отрезка с горизонтальной прямой y=yГ будет равен

.

Интересующая нас точка пересечения (xТ, yТ) будет иметь меньший из этих двух параметров:

,

и ее координаты будут вычисляться по уже приведенным выше уравнениям:

Отдельно нужно рассмотреть случаи, когда x1=x2 (строго горизонтальный отрезок) и y1=y2 (строго вертикальный отрезок). В обоих этих случаях отрезок будет параллелен одной из двух прямых, и точка пересечения с ней не будет существовать (знаменатель в выражении для вычисления соответствующего t обращается в 0). При этом нам нужно просто взять в качестве (xТ, yТ) точку пересечения с другой прямой – стороны перпендикулярны, поэтому эта точка гарантированно существует.

Наша вспомогательная функция будет выглядеть так:

      // "Обрезание" отрезка по прямоугольнику
      void ClipLineByRect(
          int x1,int y1,   // Центр прямоугольника
          int w,int h,     // Размеры прямоугольника
          int x2,int y2,   // Конец отрезка
          int *px,int *py) // Координаты точки на границе
      { int xv,yg,xres,yres;
        double tv,tg,t;
        int status=0; // Наличие точек пересечения

        if(x2!=x1) // Отрезок не строго вертикален
          { if(x2>x1) // Отрезок уходит вправо
              xv=x1+w/2;
            else // Отрезок уходит влево
              xv=x1-w/2;
            // Параметр точки пересечения с вертикальной прямой
            tv=((double)(xv-x1))/((double)(x2-x1));
            status=1; // Есть точка пересечения с вертикалью
          }

        if(y2!=y1) // Отрезок не строго горизонтален
          { if(y2>y1) // Отрезок уходит вниз
              yg=y1+h/2;
            else // Отрезок уходит вверх
              yg=y1-h/2;
            // Параметр точки пересечения с горизонтальной прямой
            tg=((double)(yg-y1))/((double)(y2-y1));
            status+=10; // Есть пересечение с горизонталью
          }

        switch(status)
          { case 0:  // Ошибка: (x1,y1)==(x2,y2)
              *px=x1; *py=y1; return;
            case 1:  // Строго горизонтальный отрезок
              t=tv; break;
            case 10: // Строго вертикальный отрезок
              t=tg; break;
            default: // Диагональный отрезок
             t=(tv<tg)?tv:tg;
          }
        // Вычисление координат по параметру t
        *px=x1+t*(x2-x1);
        *py=y1+t*(y2-y1);
      }
      //=========================================

Названия параметров этой функции соответствуют рис. 117, через указатели px и py она возвращает координаты точки (xТ, yТ). Внутри функции выполняются описанные выше вычисления. Для отдельного рассмотрения горизонтальных и вертикальных отрезков в ней вводится вспомогательная переменная status с нулевым начальным значением. Если отрезок не вертикальный, переменной присваивается значение 1, если он не горизонтальный, к ней прибавляется 10. Таким образом, строго горизонтальному отрезку соответствует значение 1, строго вертикальному – 10, диагональному (ни горизонтальному, ни вертикальному) – 11. Нулевое значение status укажет на совпадение точек (x1, y1) и (x2, y2) – в этом случае отрезок вырождается в точку, и определить координаты (xТ, yТ) невозможно. При этом функция возвращает координаты точки (x1, y1) – связь будет начинаться в геометрическом центре блока, но для такого вырожденного случая это уже не важно.

Теперь займемся самим блоком. Для работы ему нужны имена трех файлов: файла со списком блоков, файла со списком связей и файла с описанием блока, копии которого мы будем вставлять по координатам из списка блоков. В качестве файла с описанием блока можно использовать любой блок, сохраненный из RDS в отдельный файл (обычно такие файлы имеют расширение «blk»). Все эти три имени мы будем хранить в значениях по умолчанию статических переменных блока (см. §2.7.4). В контекстном меню блока будет два дополнительных пункта: один для добавления в подсистему блоков и связей из файла, другой – для удаления добавленного. Чтобы блок мог удалить добавленные им объекты, мы предусмотрим в его личной области данных список блоков и связей, который будем заполнять по мере их добавления (в RDS есть специальный вспомогательный объект для хранения таких списков).

Личную область данных блока мы, как обычно, оформим в виде класса:

      // Личная область данных блока
      class TLoadGraphData
      { public:
          RDS_HOBJECT List; // Объект-список добавленных блоков и связей

          // Функция добавления блоков по списку из файла
          BOOL LoadBlocks(RDS_BHANDLE thisblock,char *blockfile,
                          char *blocklist);
          // Функция добавления связей по списку из файла
          void LoadConnections(RDS_BHANDLE thisblock,char *connlist);
          // Функция удаления добавленного
          void DeleteByList(void);

          // Функция настройки (в нее передаются номера статических
          // переменных, в которых хранятся параметры блока)
          int Setup(RDS_BHANDLE Block,int BlockFileVar,
                    int BlockLstVar,int ConnLstVar);

          // Конструктор класса
          TLoadGraphData(void){List=NULL;};
          // Деструктор класса
          ~TLoadGraphData(){rdsDeleteObject(List);};
      };
      //=========================================

В поле List этого класса будет храниться идентификатор вспомогательного объекта со списком добавленных блоков и связей. Модель блока будет заполнять этот список в процессе работы, и просматривать его при удалении добавленного по команде пользователя. В конструкторе класса мы присваиваем этому полю значение NULL (объект не создан), а в деструкторе – удаляем объект сервисной функцией rdsDeleteObject (ее можно безопасно вызывать и для нулевых идентификаторов).

Основные действия, выполняемые блоком, реализованы в трех функциях-членах класса. Функция LoadBlocks добавляет в родительскую подсистему нашего блока новые блоки по списку, функция LoadConnections создает между ними связи, а функция DeleteByList удаляет добавленные блоки и связи по команде пользователя. Для настройки параметров блока, а именно ввода трех имен файлов, в класс включена функция Setup, в нее будет передаваться идентификатор блока, параметры которого настраиваются, и номера статических переменных этого блока, в которых хранятся имена файлов.

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

      // Вывод сообщения об ошибке в файле
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void FileErrorMessage(int line,      // Номер строки или 0
                            char *file,    // Имя файла или NULL
                            char *message) // Описание ошибки
      { char *msg=NULL; // Здесь будет формироваться текст

        if(message) // Есть текст описания
          rdsAddToDynStr(&msg,message,FALSE); // Копируем в msg
        if(file) // Есть имя файла
          { // Добавляем к динамической строке msg
            rdsAddToDynStr(&msg,"\nФайл: ",FALSE);
            rdsAddToDynStr(&msg,file,FALSE);
          }
        if(line>0) // Есть номер строки
          { char buf[80]; // Преобразуем в текст и добавляем к msg
            sprintf(buf,"\nСтрока: %d",line);
            rdsAddToDynStr(&msg,buf,FALSE);
          }
        // Выводим сообщение пользователю
        rdsMessageBox(msg,"Ошибка",MB_OK|MB_ICONWARNING);
        // Освобождаем динамическую строку msg
        rdsFree(msg);
      }
      //=========================================

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

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

      // Функция настройки параметров блока
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      int TLoadGraphData::Setup(
        RDS_BHANDLE Block, // Идентификатор блока
        int BlockFileVar,  // Номер переменной с файлом блока
        int BlockLstVar,   // Номер переменной со списком блоков
        int ConnLstVar)    // Номер переменной со списком связей
      { RDS_HOBJECT window; // Идентификатор вспомогательного объекта
        BOOL ok;            // Пользователь нажал "OK"
        char *defval;

        // Создаем окно
        window=rdsFORMCreate(FALSE,-1,-1,"Добавление блоков");

        //----- Файл с описанием блока -----
        // Значение по умолчанию переменной с номером BlockFileVar
        defval=rdsGetBlockVarDefValueStr(Block,BlockFileVar,NULL);
        // Поле ввода с возможностью выбора файла
        rdsFORMAddEdit(window,0,1,RDS_FORMCTRL_OPENDIALOG,
            "Добавляемый блок:",300);
        // Фильтр имен файлов для поля
        rdsSetObjectStr(window,1,RDS_FORMVAL_LIST,
            "Файлы блоков (*.blk)|*.blk\nВсе файлы|*.*");
        // Значение поля ввода
        rdsSetObjectStr(window,1,RDS_FORMVAL_VALUE,defval);
        // Динамическая строка defval больше не нужна
        rdsFree(defval);

        // Список блоков
        defval=rdsGetBlockVarDefValueStr(Block,BlockLstVar,NULL);
        rdsFORMAddEdit(window,0,2,RDS_FORMCTRL_OPENDIALOG,
            "Список блоков:",300);
        rdsSetObjectStr(window,2,RDS_FORMVAL_LIST,
            "Текстовые файлы (*.txt)|*.txt\nВсе файлы|*.*");
        rdsSetObjectStr(window,2,RDS_FORMVAL_VALUE,defval);
        rdsFree(defval);

        // Список связей
        defval=rdsGetBlockVarDefValueStr(Block,ConnLstVar,NULL);
        rdsFORMAddEdit(window,0,3,RDS_FORMCTRL_OPENDIALOG,
            "Список связей:",300);
        rdsSetObjectStr(window,3,RDS_FORMVAL_LIST,
            "Текстовые файлы (*.txt)|*.txt\nВсе файлы|*.*");
        rdsSetObjectStr(window,3,RDS_FORMVAL_VALUE,defval);
        rdsFree(defval);

        // Открытие окна
        ok=rdsFORMShowModalEx(window,NULL);
        if(ok)
          { // Запись значений в переменных блока
            defval=rdsGetObjectStr(window,1,RDS_FORMVAL_VALUE);
            rdsSetBlockVarDefValueStr(Block,BlockFileVar,defval);
            defval=rdsGetObjectStr(window,2,RDS_FORMVAL_VALUE);
            rdsSetBlockVarDefValueStr(Block,BlockLstVar,defval);
            defval=rdsGetObjectStr(window,3,RDS_FORMVAL_VALUE);
            rdsSetBlockVarDefValueStr(Block,ConnLstVar,defval);
          }
        // Уничтожение окна
        rdsDeleteObject(window);
        return ok?1:0;
      }
      //=========================================

В этой функции для ввода имен файлов используются поля с возможностью выбора файла (тип RDS_FORMCTRL_OPENDIALOG). Внешне они идентичны уже использовавшимся ранее полям RDS_FORMCTRL_SAVEDIALOG (см. §2.13.6), но вместо стандартного диалога сохранения файла при нажатии на кнопку рядом с таким полем вызывается стандартный диалог открытия. Поля для выбора файлов автоматически заменяют стандартные пути принятыми в RDS сокращениями: например, в окне на рис. 118 в качестве добавляемого блока выбран стандартный блок умножения на константу («K.blk») с панели блоков, при этом вместо полного пути к RDS подставлено стандартное сокращение «$RDS$». Таким образом, при переносе схемы на другую машину, где папка RDS может размещаться в другом месте, никакой корректировки путей не потребуется. Файлы списка блоков и списка связей на рисунке не имеют путей – это значит, что они находятся в одной папке с загруженной схемой.

Рис. 118. Окно настроек блока создания блоков и связей

Теперь можно приступать к написанию основных функций блока. Начнем с функции создания блоков LoadBlocks, в параметрах которой передаются идентификатор нашего блока (thisblock), имя файла с описанием добавляемого блока (blockfile) и имя файла списка добавляемых блоков (blocklist). Функция будет возвращать TRUE, если в родительскую подсистему блока thisblock удалось добавить блоки из списка, и FALSE в противном случае.

      // Создание блоков по списку
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      BOOL TLoadGraphData::LoadBlocks(
        RDS_BHANDLE thisblock, // Идентификатор этого блока
        char *blockfile,       // Файл добавляемого блока
        char *blocklist)       // Файл списка блоков
      { RDS_BHANDLE block=NULL;
        RDS_HOBJECT csv;
        RDS_BLOCKDESCRIPTION descr;
        BOOL ok=TRUE;
        BOOL Modified=FALSE; // Флаг внесения изменений в схему
        int line=0;          // Номер строки в списке блоков

        if(List) // Уже есть список объектов – очишаем его
          rdsCommandObject(List,RDS_HBCL_CLEAR);
        else // Списка нет – создаем новый (пустой)
          { List=rdsBCLCreateList(NULL,0,FALSE);
            // Включаем автоматическое обнуление удаленных блоков и связей
            rdsSetObjectInt(List,RDS_HBCL_AUTODELETE,0,1);
          }

        // Получаем описание нашего блока (нужны его родитель и имя)
        descr.servSize=sizeof(descr);
        rdsGetBlockDescription(thisblock,&descr);

        // Создаем объект для разбора формата CSV
        csv=rdsCSVCreate();

        // Открываем файл со списком блоков для чтения в csv
        rdsSetObjectStr(csv,RDS_CSV_OPENFILEREAD,0,blocklist);

        // Проверяем, открылся ли файл
        if(!rdsGetObjectInt(csv,RDS_CSV_FILEISOPEN,0))
          { FileErrorMessage(0,blocklist,
                "Невозможно открыть список блоков");
            rdsDeleteObject(csv);
            return FALSE;
          }

        // Читаем строки файла в цикле
        for(;;)
          { char *name;
            int x,y;
            // Читаем очередную строку из файла в строку объекта 0
            if(!rdsCommandObjectEx(csv,RDS_CSV_STRFROMFILE,0,NULL))
              break; // Строки в файле кончились
            line++;
            name=rdsCSVGetItem(csv,0,0); // Имя блока (элемент 0)
            if(*name==0) // Имя блока пустое
              { FileErrorMessage(line,blocklist,"Пустое имя блока");
                ok=FALSE;
                break;
              }
            // Читаем из объекта координаты блока (элементы 1 и 2)
            x=atoi(rdsCSVGetItem(csv,0,1));
            y=atoi(rdsCSVGetItem(csv,0,2));
            // Если имя блока, который нужно добавить, совпадает с именем
            // нашего блока – переименовываем наш
            if(strcmp(name,descr.BlockName)==0)
              { // Имена совпали – подбираем новое для нашего
                char *newname=rdsMakeUniqueBlockName(
                                  descr.Parent,descr.BlockName);
               // Переименовываем наш блок и получаем его новое описание
                rdsRenameBlock(thisblock,newname,&descr);
                rdsFree(newname); // Освобождаем строку имени
                Modified=TRUE;
              }

            if(block==NULL) // Добавляем самый первый блок (из файла)
              block=rdsCreateBlockFromFile(blockfile,descr.Parent,x,y,NULL);
            else // Уже добавляли блок раньше – копируем добавленный
              block=rdsDuplicateBlock(block,descr.Parent,x,y,NULL);
            if(block==NULL) // Ошибка при добавлении блока
              { FileErrorMessage(line,blocklist,
                    "Не удалось добавить блок");
                ok=FALSE;
                break;
              }
            Modified=TRUE; // Схема изменилась
            // Заносим добавленный блок в список List
            rdsBCLAddBlock(List,block,FALSE);
            // Даем добавленному блоку имя, считанное из списка
            if(!rdsRenameBlock(block,name,NULL))
              { FileErrorMessage(line,blocklist,"Повтор имени блока");
                ok=FALSE;
                break;
              }
          } // for(;;)

        // Удаляем объект csv (файл закроется автоматически)
        rdsDeleteObject(csv);
        if(Modified) // Добавлены блоки
          rdsSetModifiedFlag(TRUE);
        return ok;
      }
      //=========================================

Прежде всего нам нужно очистить список добавленных блоков и связей, если он уже создан (мы будем заполнять его заново) или создать его, если его еще нет. Если в поле List находится ненулевой идентификатор, значит, мы уже создали список, и нужно очистить его, передав ему команду RDS_HBCL_CLEAR. Если же значение List нулевое, список создается при помощи сервисной функции rdsBCLCreateList. Эта функция может создавать как пустой список, так и список, уже заполненный блоками и связями какой-либо подсистемы:

      RDS_HOBJECT RDSCALL rdsBCLCreateList(
        RDS_BHANDLE sys, // Добавить блоки и связи подсистемы sys
        DWORD mask,      // Маска типов блоков и связей
        BOOL Recursive); // Добавлять из вложенных подсистем

Нам нужен именно пустой список, поэтому в первом параметре вместо идентификатора подсистемы мы передаем NULL. При этом остальные параметры, определяющие, какие именно блоки добавляются в список, могут быть любыми – в данном случае мы передаем 0 и FALSE.

Далее для переданного в параметрах функции идентификатора нашего блока thisblock вызывается функция rdsGetBlockDescription, которая заполняет описанием этого блока структуру descr. В этой структуре нас будет, прежде всего, интересовать идентификатор родительской подсистемы нашего блока – именно в нее мы будем добавлять новые блоки. Нам также понадобится имя блока: поскольку имена двух блоков в подсистеме не могут быть одинаковыми, при обнаружении такого же имени в списке блоков нам придется как-нибудь переименовать наш блок.

Теперь можно начинать читать файл со списком блоков. Мы уже решили, что этот список будет текстом в формате CSV, где первым значением в каждой строке будет имя блока, вторым – горизонтальная координата, третьим – вертикальная. Для работы с таким текстовым файлом мы будем использовать специальный объект RDS, создаваемый функцией rdsCSVCreate. Идентификатор этого объекта присваивается переменной csv. После создания мы сообщаем объекту, что файл с именем blocklist необходимо открыть для чтения – для объекта csv вызывается функция rdsSetObjectStr с константой RDS_CSV_OPENFILEREAD. Имя файла передается в объект в том виде, в котором оно записано в настройках блока, то есть у него может не быть полного пути, а стандартные пути могут быть заменены специальными сокращениями RDS. Объект самостоятельно добавит к имени файла путь к загруженной схеме, если в этом имени путь вообще не указан, и заменит сокращения на настоящие пути к стандартным папкам.

Функция rdsSetObjectStr, которая используется для передачи строк всем вспомогательным объектам RDS, не возвращает никаких значений, поэтому, вызвав ее, мы не можем быть уверены, что файл успешно открыт. Для проверки мы вызываем для объекта функцию rdsGetObjectInt с константой RDS_CSV_FILEISOPEN – если при открытии файла произошла какая-либо ошибка (например, файла с таким именем не существует), она вернет нулевое значение. В этом случае мы выводим пользователю сообщение об ошибке с указанием имени файла, используя для этого написанную ранее функцию FileErrorMessage, удаляем созданный объект, и возвращаем FALSE: добавление блоков не удалось.

Объект, создаваемый функцией rdsCSVCreate, позволяет работать с данными в формате CSV как с матрицей строк, обращаясь к каждому значению по номеру строки и номеру элемента в этой строке. В принципе, мы можем сразу считать в объект весь файл, а потом в цикле по строкам получать у объекта имя каждого блока и его координаты. Однако, поскольку мы добавляем блоки последовательно, нам нет нужды считывать в память сразу все строки файла – список блоков может оказаться достаточно длинным, и файл займет много место в памяти. Гораздо лучше считывать из файла по одной строке за раз, и, считав эту строку, тут же добавлять в подсистему новый блок. После этого считанная строка будет уже не нужна, и на ее место можно будет загрузить новую. Так мы и поступим – будем каждый раз считывать из файла одну строку и записывать ее в объект под номером 0.

Для чтения строк файла мы используем «бесконечный» цикл for(;;) – мы принудительно прервем его когда весь файл будет прочитан. В этом цикле для чтения очередной строки в объект мы вызываем для этого объекта функцию rdsCommandObjectEx, передавая ей константу RDS_CSV_STRFROMFILE и целое значение 0 (мы считываем строку из файла в нулевую строку внутренней памяти объекта). Если эта функция вернет нулевое значение, значит, строки в файле кончились, и мы прерываем цикл оператором break. В противном случае строка из файла считана успешно, и мы увеличиваем счетчик считанных строк line на единицу – номер строки нам понадобится для вывода сообщений об ошибках.

Теперь мы можем получить у объекта три значения, которые должны находиться в считанной строке. Для чтения строк из объекта используется функция rdsCSVGetItem, в которую передается идентификатор объекта (csv), номер строки (в нашем случае – 0, поскольку мы каждый раз считываем строку из файла в нулевую строку объекта) и номер элемента в строке. Функция возвращает указатель на строку, представляющую запрошенное значение и находящуюся во внутренней памяти объекта, поэтому после получения эти строки не нужно освобождать при помощи rdsFree. Однако, следует помнить, что возвращенный функцией указатель ссылается на значение только до тех пор, пока оно не изменится. После этого указатель на значение нужно получать заново. В нашем случае мы не меняем нулевую строку объекта csv до конца тела цикла, поэтому, получив указатель на значение в нулевой строке объекта, мы можем не заботиться о его правильности.

Самое первое значение (то есть значение с нулевым индексом) в считанной строке – это имя блока, мы получаем указатель на него вызовом rdsCSVGetItem(csv,0,0) и запоминаем его в переменной name. Функция rdsCSVGetItem устроена так, что она всегда возвращает указатель на строку, даже если мы попытаемся считать из объекта элемент, которого в нем нет. В случае каких-либо ошибок функция вернет указатель на пустую строку (то есть на нулевой байт), поэтому ее результат можно использовать в вызовах функций обработки строк без каких-либо дополнительных проверок. В данном случае мы проверяем полученное имя блока на пустоту: сравниваем первый символ строки (*name) с нулем. Имя блока не может быть пустым – в этом случае мы выводим сообщение об ошибке, прерываем цикл чтения файла и записываем FALSE в переменную ok – функция вернет это значение, указывая на то, что чтение списка блоков не удалось.

Если имя блока не пустое, мы считываем элементы строки с индексами 1 и 2 в целые переменные x и y, предварительно преобразовав их в целые числа функцией atoi – это координаты блока, который мы должны добавить в подсистему. Но прежде мы должны проверить, не совпадает ли имя добавляемого блока name с именем нашего блока, которое запомнено в поле BlockName структуры descr. Если они совпадают (функция strcmp возвращает 0), мы переименуем наш блок: имя блока из списка изменять нельзя (на него будут ссылки из списка связей), а как называется блок, выполняющий добавление блоков в подсистему, на самом деле, не важно.

Для того, чтобы переименовать блок мы, прежде всего, должны подобрать ему новое имя, которое не будет совпадать ни с одним из имен блоков, уже присутствующих в подсистеме. В RDS для этого предусмотрена специальная функция rdsMakeUniqueBlockName. В параметрах этой функции передается идентификатор подсистемы, внутри которой нужно подобрать уникальное имя блока, и исходное имя, на базе которого подбирается новое. Функция разбивает это исходное имя на буквенную и цифровую части, а затем меняет цифровую часть так, чтобы имя стало уникальным в подсистеме. Например, если в подсистеме есть блоки с именами «Block1», «Block2» и «Block3», при вызове этой функции с исходным именем «Block1» она вернет имя «Block4». Если же вызвать ее с исходным именем «Block100», она вернет это имя, не изменив его, поскольку оно и так не совпадает ни с одним из имен блоков. Возвращаемое функцией имя представляет собой динамически отведенную строку, поэтому после использования ее необходимо освободить при помощи rdsFree.

В данном случае мы подбираем новое имя для нашего блока на основе его же собственного старого имени. Сформированное функцией rdsMakeUniqueBlockName новое имя записывается в переменную newname и используется в вызове функции rdsRenameBlock, которая переименовывает блок и записывает его новое описание в структуру descr (нам важно все время хранить в этой структуре актуальное описание блока, поскольку она постоянно используется при проверке совпадения имен). Результат выполнения функции можно не проверять, поскольку, подобрав уникальное имя, мы гарантировали успешность переименования. После вызова rdsRenameBlock строка в переменной newname нам уже не нужна (новое имя блока теперь можно получить через структуру descr), и мы освобождаем эту динамическую строку функцией rdsFree.

Теперь можно создавать в подсистеме новый блок. Чтобы каждый раз не загружать блок из файла blockfile, мы сделаем это только один раз и запомним идентификатор загруженного блока, а затем будем просто копировать этот блок снова и снова. В начале функции мы ввели дополнительную переменную block и присвоили ей NULL – в ней мы будем хранить идентификатор загруженного блока. Если эта переменная сохранила нулевое значение, значит, мы еще не добавили в подсистему ни одного блока. В этом случае мы вызываем функцию создания блока из файла rdsCreateBlockFromFile, в которую передаем имя файла с описанием блока (blockfile), подсистему, в которой его нужно создать (descr.Parent, родительская подсистема нашего блока) и координаты, по которым он должен располагаться (x и y). В последнем параметре функции передается указатель на структуру, которая заполняется описанием загруженного блока, но нам это описание не нужно, поэтому мы передаем NULL. Функция возвращает идентификатор добавленного блока или NULL в случае ошибки, этот идентификатор записывается в переменную block.

Если же идентификатор в переменной block не равен NULL, значит, мы уже вызывали rdsCreateBlockFromFile по крайней мере один раз. В этом случае мы делаем копию блока block функцией rdsDuplicateBlock: в нее передается идентификатор копируемого блока (block), подсистема, в которую вставляется копия (descr.Parent) и координаты копии (x и y). В последнем параметре этой функции тоже можно передать указатель на структуру, которая будет заполнена описанием созданной копии, но мы опять передаем NULL. Идентификатор созданной копии блока, который возвращает функция, мы записываем в ту же самую переменную block.

Теперь, независимо от того, каким именно образом мы создали в подсистеме новый блок, в переменной block содержится его идентификатор. Если вместо идентификатора там окажется NULL, это будет означать, что при создании блока произошла ошибка. В этом случае мы выводим пользователю сообщение об этом с указанием имени файла и номера строки в нем, заносим FALSE в ok (возникла ошибка) и прерываем цикл. Если же создание нового блока выполнено успешно, мы присваиваем переменной Modified значение TRUE (схема изменена) и заносим добавленный блок block в список List функцией rdsBCLAddBlock. В последнем параметре этой функции мы передаем FALSE, поскольку нам не нужно проверять, есть ли уже добавляемый блок в списке: при работе нашей функции не может возникнуть ситуации, в которой мы два раза добавим в список один и тот же блок.

Теперь осталось только дать блоку имя name: функции rdsCreateBlockFromFile и rdsDuplicateBlock автоматически обеспечивают создаваемый блок уникальным именем, но принудительно указать это имя в них нельзя. Для переименования блока мы снова вызываем rdsRenameBlock, но нам необходимо проверить результат ее выполнения: может оказаться, что блок с именем name уже есть в подсистеме. В этом случае мы выводим пользователю сообщение об ошибке и прерываем цикл – дальнейшее добавление блоков бессмысленно, пользователь должен поправить имена в списках блоков и связей или стереть лишние блоки в подсистеме.

На этом тело цикла чтения файла завершается. Перечисленные выше действия будут выполняться до тех пор, пока строки в файле не закончатся или не пока возникнет ошибка. После завершения цикла мы удаляем вспомогательный объект csv (открытый для чтения файл при этом автоматически закроется), взводим в RDS флаг наличия изменений в схеме, если мы добавили в нее блоки, вызовом rdsSetModifiedFlag и возвращаем значение ok. Таким образом, если в подсистему были добавлены все блоки из списка, функция вернет TRUE, если при добавлении возникли ошибки – FALSE.

Функция создания связей LoadConnections тоже будет разбирать файл формата CSV, поэтому она будет в целом похожа на LoadBlocks. В нее будет передаваться идентификатор нашего блока thisblock и имя файла со списком связей connlist:

      // Функция создания связей по списку
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void TLoadGraphData::LoadConnections(
        RDS_BHANDLE thisblock, // Идентификатор этого блока
        char *connlist)        // Файл списка связей
      { RDS_HOBJECT csv;
        RDS_BLOCKDESCRIPTION descr;
        BOOL ok=TRUE;
        BOOL Modified=FALSE;
        RDS_HOBJECT editor=NULL;
        RDS_BLOCKDIMENSIONS dim1,dim2;
        int line=0; // Счетчик считанных строк

        // Заполнение поля размера служебных структур
        dim1.servSize=sizeof(dim1);
        dim2.servSize=sizeof(dim2);

        // Получаем описание нашего блока (нужен его родитель)
        descr.servSize=sizeof(descr);
        rdsGetBlockDescription(thisblock,&descr);

        // Создаем объект для разбора формата CSV
        csv=rdsCSVCreate();

        // Открываем файл со списком связей для чтения в csv
        rdsSetObjectStr(csv,RDS_CSV_OPENFILEREAD,0,connlist);

        // Проверяем, открылся ли файл
        if(!rdsGetObjectInt(csv,RDS_CSV_FILEISOPEN,0))
          { FileErrorMessage(0,connlist,
                "Невозможно открыть список связей");
            rdsDeleteObject(csv);
            return;
          }

        // Читаем строки файла в цикле
        for(;;)
          { char *name1,*name2,*var1,*var2;
            RDS_BHANDLE block1,block2;
            int xc1,xc2,yc1,yc2,pnum1,pnum2,x1,y1,x2,y2;
            RDS_CHANDLE conn;
            // Читаем очередную строку из файла в строку объекта 0
            if(!rdsCommandObjectEx(csv,RDS_CSV_STRFROMFILE,0,NULL))
              break;
            line++;
            // Считываем из строки имена соединяемых блоков и переменных
            name1=rdsCSVGetItem(csv,0,0); // Имя блока 1
            var1=rdsCSVGetItem(csv,0,1);  // Имя переменной 1
            name2=rdsCSVGetItem(csv,0,2); // Имя блока 2
            var2=rdsCSVGetItem(csv,0,3);  // Имя переменной 2
            if(*name1==0 || *name2==0 || *var1==0 || *var2==0)
              { FileErrorMessage(line,connlist,
                    "Мало данных в строке связи");
                break;
              }
            // Ищем в подсистеме блоки с указанными именами
            block1=rdsGetChildBlockByName(descr.Parent,name1,NULL);
            block2=rdsGetChildBlockByName(descr.Parent,name2,NULL);
            if(block1==NULL || block2==NULL) // Какого-то нет
              { FileErrorMessage(line,connlist,
                    "Не найден один из блоков");
                break;
              }
            // Получаем координаты и размеры блоков
            rdsGetBlockDimensions(block1,&dim1,FALSE);
            rdsGetBlockDimensions(block2,&dim2,FALSE);
            // Вычисляем координаты центров блоков
            xc1=dim1.Left+dim1.Width/2;
            yc1=dim1.Top+dim1.Height/2;
            xc2=dim2.Left+dim2.Width/2;
            yc2=dim2.Top+dim2.Height/2;
            // "Обрезаем" прямую по границам первого блока
            ClipLineByRect(xc1,yc1,dim1.Width,dim1.Height,xc2,yc2,
              &x1,&y1);
            // "Обрезаем" прямую по границам второго блока
            ClipLineByRect(xc2,yc2,dim2.Width,dim2.Height,xc1,yc1,
              &x2,&y2);
            // Создаем или очищаем объект для редактирования связи
            if(editor==NULL) // Нужно создать
              editor=rdsCECreateEditor();
            else // Очищаем ранее созданный
              rdsCommandObject(editor,RDS_HCE_RESET);
            // Добавляем в объект editor две точки
            pnum1=rdsCEAddBlockPoint(editor,block1,var1,
                      x1-dim1.BlockX,y1-dim1.BlockY,FALSE);
            pnum2=rdsCEAddBlockPoint(editor,block2,var2,
                      x2-dim2.BlockX,y2-dim2.BlockY,FALSE);
            // Добавляем соединяющую их линию
            rdsCEAddLine(editor,pnum1,pnum2);
            // Создаем по данным объекта editor новую связь
            conn=rdsCECreateConnBus(editor,descr.Parent,
                     RDS_CTCONNECTION,NULL);
            if(conn!=NULL) // Создание связи удалось
              { Modified=TRUE;
                // Добавляем связь в список
                rdsBCLAddConn(List,conn,FALSE);
              }
            else // Ошибка при создании связи
              { FileErrorMessage(line,connlist,
                    "Не удалось создать связь");
                break;
              }
          } // for(;;)

        // Удаляем объект csv (файл закроется автоматически)
        rdsDeleteObject(csv);
        // Удаляем объект-редактор
        rdsDeleteObject(editor);
        if(Modified) // Добавлены связи
          rdsSetModifiedFlag(TRUE);
      }
      //=========================================

В этой функции, как и в предыдущей, мы используем для чтения файла объект, создаваемый вызовом rdsCSVCreate, поэтому их начала практически совпадают. Точно так же, как и в LoadBlocks, мы в цикле считываем очередную строку файла в нулевую строку объекта, но после этого выполняются совершенно другие действия: теперь мы должны создать связи между блоками, которые, на данный момент, уже должны находиться в подсистеме (LoadConnections мы будем вызывать после того, как LoadBlocks успешно завершилась).

В списке связей каждая строка содержит четыре значения: две пары «имя блока, имя переменной». Мы сразу считываем имена соединяемых блоков в name1 и name2, а имена переменных в этих блоках – в var1 и var2. Затем мы проверяем все эти четыре строки на пустоту: если хотя бы одна из них пуста, мы выводим сообщение об ошибке и прерываем цикл: ни имена блоков, ни имена переменных пустыми быть не могут.

Затем мы ищем идентификаторы обоих соединяемых блоков в родительской подсистеме нашего блока и присваиваем их переменным block1 и block2. Для этого используется функция rdsGetChildBlockByName: она позволяет найти в подсистеме блок по его имени. Если вместо одного из идентификаторов функция вернет NULL, значит, такого блока в подсистеме нет – мы выводим сообщение об ошибке и прерываем цикл. Если же оба блока найдены, мы при помощи функции rdsGetBlockDimensions заполняем структуры dim1 и dim2 их геометрическими размерами. Размеры блоков нам нужны для вычисления координат точек создаваемой между ними связи (см. рис. 117). Затем мы вычисляем координаты геометрических центров блоков (xc1, yc1) и (xc2, yc2) – отрезок связи, который мы создаем, будет лежать на прямой, соединяющей эти точки. Чтобы определить точки пересечения этой прямой с границами описывающего прямоугольника блока, мы вызываем написанную ранее вспомогательную функцию ClipLineByRect. В результате двух ее вызовов в переменных x1 и y1 окажутся координаты точки пересечения прямой с границами первого блока, в x2 и y2 – с границами второго. Эти точки будут началом и концом нашей связи.

Связь в RDS – сложная конструкция, она может состоять из множества точек и линий, как прямых, так и кривых, поэтому, в отличие от блока, ее нельзя создать одним вызовом. Для создания новых и изменения существующих связей в RDS предусмотрен специальный вспомогательный объект: сначала в него записываются координаты и параметры точек связей и указываются номера точек, соединенных линиями, а затем по данным этого объекта в подсистеме создается настоящая связь. Такой объект создается сервисной функцией rdsCECreateEditor. В данном случае в функции введена переменная editor с нулевым начальным значением для хранения идентификатора такого объекта-редактора связи. Если в editor находится значение NULL, значит, сейчас мы будем добавлять в подсистему самую первую связь из списка. В этом случае мы создаем новый объект вызовом rdsCECreateEditor. Если же в editor уже хранится какой-то идентификатор, значит, объект уже был создан ранее в цикле, и мы просто очищаем его, вызвав для него функцию rdsCommandObject с константой RDS_HCE_RESET.

Теперь мы должны заполнить объект editor точками связи и соединяющей их линией. Обе точки будут точками соединения связи с блоком, поэтому для их добавления мы вызываем функцию rdsCEAddBlockPoint:

      int RDSCALL rdsCEAddBlockPointA(
        RDS_HOBJECT editor, // Объект-редактор связи
        RDS_BHANDLE block,  // Блок, с которым связана точка
        RDSCSTR var,        // Имя переменной в блоке (UTF8)
        int x,int y,        // Координаты относительно блока
        BOOL displayname);  // Показывать ли имя переменной у точки
      int RDSCALL rdsCEAddBlockPointW(
        RDS_HOBJECT editor, // Объект-редактор связи
        RDS_BHANDLE block,  // Блок, с которым связана точка
        RDSWCSTR var,       // Имя переменной в блоке (UTF16)
        int x,int y,        // Координаты относительно блока
        BOOL displayname);  // Показывать ли имя переменной у точки
      // Функция-псевдоним
      int RDSCALL rdsCEAddBlockPoint(
        RDS_HOBJECT editor, // Объект-редактор связи
        RDS_BHANDLE block,  // Блок, с которым связана точка
        RDSXCSTR var,       // Имя переменной в блоке (кодировка по умолчанию)
        int x,int y,        // Координаты относительно блока
        BOOL displayname);  // Показывать ли имя переменной у точки

При вызове этой функции координаты точки x и y указываются не относительно верхнего левого угла рабочего поля подсистемы, как обычно, а относительно точки привязки блока, с которым эта точка соединяет связь. Координаты точек привязки обоих блоков находятся в полях BlockX и BlockY структур с их размерами dim1 и dim2, поэтому в вызове rdsCEAddBlockPoint для первой точки мы используем не ее абсолютные координаты x1 и y1, а x1-dim1.BlockX и y1-dim1.BlockY. Точно так же вычисляются относительные координаты второй точки. Функция rdsCEAddBlockPoint возвращает внутренний номер добавленной точки в объекте-редакторе, эти номера запоминаются в переменных pnum1 и pnum2, они потребуются нам при добавлении в объект линии, соединяющей эти точки.

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

Точки добавлены в объект, теперь нужно добавить в него соединяющую их линию. Поскольку мы решили, что связи у нас будут прямые, мы вызываем функцию rdsCEAddLine, в которую передаем внутренние номера соединяемых точек pnum1 и pnum2. Если бы мы вместо прямой линии захотели бы добавить кривую Безье, нужно было бы вызвать rdsCEAddBezier и указать, кроме номеров соединяемых точек, еще и координаты касательных кривой (см. рис. 88). Функция rdsCEAddLine (как и rdsCEAddBezier) возвращает внутренний номер добавленной линии, но, в данном случае, он нам не нужен, и мы не присваиваем его никакой переменной.

Теперь объект editor полностью описывает связь, которую нам нужно создать. Для создания по этому описанию связи в подсистеме вызывается функция rdsCECreateConnBus:

      RDS_CHANDLE RDSCALL rdsCECreateConnBus(
        RDS_HOBJECT editor, // Объект-редактор связи
        RDS_BHANDLE parent, // Подсистема, в которой создается связь
        int type,           // Связь или шина (константа RDS_CT*)
        int *perror);       // Возвращаемый код ошибки

В первом параметре функции передается идентификатор объекта, по данным которого нужно создать связь, во втором – идентификатор подсистемы, в которой эта связь создается. Эта функция может создавать как связи, так и шины, поэтому в ее третьем параметре передается тип создаваемого объекта: RDS_CTCONNECTION для связи, RDS_CTBUS – для шины. В четвертом параметре можно передать указатель на целую переменную, в которую функция запишет код ошибки, но здесь нам это не нужно – мы передаем NULL. Функция возвращает идентификатор созданной связи или NULL при ошибке, мы записываем это значение в переменную conn. Если conn не равно NULL, значит, связь создана успешно, и мы добавляем ее в список List (он был создан в функции LoadBlocks) вызовом rdsBCLAddConn, в противном случае мы выводим сообщение пользователю и прерываем цикл.

Цикл for(;;) завершится тогда, когда в списке связей закончатся строки, или при возникновении какой-либо ошибки. После его завершения мы удаляем вспомогательные объекты csv и editor и взводим флаг наличия изменений в схеме, если в подсистему была добавлена хотя бы одна связь. После этого функция завершается.

Функция удаления добавленных объектов DeleteByList будет работать с полем класса List, в котором после вызова LoadBlocks и LoadConnections должен находиться идентификатор объекта со списком добавленных ими блоков и связей.

      // Удаление добавленных блоков и связей
      void TLoadGraphData::DeleteByList(void)
      { RDS_CHANDLE *conns;
        RDS_BHANDLE *blocks;
        int count;

        if(List==NULL) // Списка нет
          return;

        // Отключаем автоматическое обнуление удаляемых объектов
        rdsSetObjectInt(List,RDS_HBCL_AUTODELETE,0,0);

        // Получаем указатель на массив идентификаторов связей и его размер
        conns=(RDS_CHANDLE*)rdsGetObjectArray(List,RDS_HBCL_CONNARRAY,
                            0,&count);
        // Удаляем все связи из этого массива
        for(int i=0;i<count;i++)
          rdsDeleteConnection(conns[i]);

        // Получаем указатель на массив идентификаторов блоков и его размер
        blocks=(RDS_BHANDLE*)rdsGetObjectArray(List,RDS_HBCL_BLOCKARRAY,
                             0,&count);
        // Удаляем все связи из этого массива
        for(int i=0;i<count;i++)
          rdsDeleteBlock(blocks[i]);

        // Удаляем объект-список и обнуляем List
        rdsDeleteObject(List);
        List=NULL;
        // Взводим флаг измененности схемы
        rdsSetModifiedFlag(TRUE);
      }
      //=========================================

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

Сначала мы удалим все связи. Для этого мы получим указатель на внутренний массив объекта, в котором содержатся идентификаторы связей, при помощи функции rdsGetObjectArray:

      LPVOID RDSCALL rdsGetObjectArray(
        RDS_HOBJECT Object, // Идентификаор объекта
        int ObjOp,          // Тип массива
        int OpParam,        // Дополнительный параметр
        int *pSize);        // Возвращаемый размер массива

Объект-список позволяет получить доступ к массиву блоков (тип RDS_HBCL_BLOCKARRAY) и массиву связей (тип RDS_HBCL_CONNARRAY), дополнительный параметр, который можно передать в функции, в этом объекте не используется. Нам нужен массив связей, поэтому мы используем константу RDS_HBCL_CONNARRAY, и приводим указатель, возвращенный функцией, к типу RDS_CHANDLE* («массив идентификаторов типа RDS_CHANDLE») и записываем его в переменную conns. Размер массива записывается в целую переменную count. Далее мы в цикле вызываем для каждого элемента массива функцию удаления связи rdsDeleteConnection. Эту функцию можно безопасно вызывать для нулевых идентификаторов, поэтому если в массиве встретятся элементы, обнуленные RDS из-за удаления соответствующих связей пользователем, ничего страшного не случится.

После удаления связей мы точно таким же образом удаляем все блоки, идентификаторы которых находятся в списке. Указатель на массив идентификаторов мы записываем в переменную blocks, а затем в цикле вызываем для каждого элемента этого массива функцию удаления блока rdsDeleteBlock. После этого мы удаляем список List (он нам больше не нужен) и взводим флаг наличия изменений в схеме функцией rdsSetModifiedFlag.

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

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение | Назначение | Номер
--- | --- | --- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0 | Стандартный сигнал запуска (здесь не используется) | 0
1 | Ready | Сигнал | 1 | Выход |  | 0 | Стандартный сигнал готовности (здесь не используется) | 1
2 | BlockFile | Строка | 8 | Внутренняя |  |  | Имя файла с описанием добавляемого блока | 2
10 | BlockList | Строка | 8 | Внутренняя |  |  | Имя файла списка блоков | 3
18 | ConnList | Строка | 8 | Внутренняя |  |  | Имя файла списка связей | 4

Модель блока будет иметь следующий вид:

      // Добавление в схему блоков и связей
      extern "C" __declspec(dllexport)
        int RDSCALL LoadGraph(int CallMode,
              RDS_PBLOCKDATA BlockData,
              LPVOID ExtParam)
      { // Приведение указателя на личную область данных к правильному типу
      TLoadGraphData *data=(TLoadGraphData*)(BlockData->BlockData);
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define BlockFile (*((char **)(pStart+2*RDS_VSZ_S)))
      #define BlockList (*((char **)(pStart+2*RDS_VSZ_S+RDS_VSZ_A)))
      #define ConnList (*((char **)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_A)))
        switch(CallMode)
          { // Инициализация модели
            case RDS_BFM_INIT:
              BlockData->BlockData=new TLoadGraphData();
              break;

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

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

            // Настройка параметров блока
            case RDS_BFM_SETUP:
              return data->Setup(BlockData->Block,
                                    2,  // BlockFile
                                    3,  // BlockList
                                    4); // ConnList

            // Вызов контекстного меню блока
            // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
            //   в противном случае необходимо использовать версии функций
            //   с суффиксом "W" и символьные константы с префиксом "L"
            case RDS_BFM_CONTEXTPOPUP:
              rdsAdditionalContextMenuItemEx(
                  "Добавить блоки и связи",0,1,0);
              rdsAdditionalContextMenuItemEx(
                  "Удалить добавленное",
                   data->List==NULL?RDS_MENU_DISABLED:0,2,0);
              break;

            // Выбор пункта в контекстном меню
            case RDS_BFM_MENUFUNCTION:
              switch(((RDS_PMENUFUNCDATA)ExtParam)->Function)
                { case 1: // Добавить блоки и связи
                    // Подготовка к серьезным изменениям
                    rdsSetSystemUpdate(FALSE);
                    if(data->LoadBlocks(BlockData->Block,
                                        BlockFile,BlockList))
                      data->LoadConnections(BlockData->Block,ConnList);
                    // Серьезные изменения завершены
                    rdsSetSystemUpdate(TRUE);
                    break;
                  case 2: // Удалить добавленное
                    // Подготовка к серьезным изменениям
                    rdsSetSystemUpdate(FALSE);
                    data->DeleteByList();
                    // Серьезные изменения завершены
                    rdsSetSystemUpdate(TRUE);
                    break;
                }
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef ConnList
      #undef BlockList
      #undef BlockFile
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Реакции на события RDS_BFM_INIT, RDS_BFM_CLEANUP и RDS_BFM_VARCHECK в этой модели похожи на все ранее рассмотренные реакции моделей, в которых личная область данных представляет собой объект какого-либо класса. В реакции на событие RDS_BFM_SETUP вызывается функция класса Setup, в которую передаются номера переменных, в которых хранятся параметры блока. Остальные реакции нужно рассмотреть подробнее.

При открытии контекстного меню блока (реакция RDS_BFM_CONTEXTPOPUP) блок добавляет в него два пункта: «Добавить блоки и связи» с идентификатором 1 и «Удалить добавленное» с идентификатором 2, причем последний пункт будет разрешенным только в том случае, если поле List класса личной области данных блока не будет равно NULL, то есть если есть список добавленных блоков и связей.

При выборе пользователем одного из добавленных пунктов меню модель вызовется в режиме RDS_BFM_MENUFUNCTION. Если идентификатор выбранного пункта – 1, нужно добавить в подсистему блоки и связи из файлов, указанных в настроечных параметрах блока. Для этого нужно последовательно вызвать функции LoadBlocks и LoadConnections класса личной области данных. Однако, перед их вызовом лучше всего проинформировать RDS о том, что сейчас модель будет производить изменения в схеме. Дело в том, что добавление и удаление блоков и связей, а также изменение их параметров, приводит к изменениям во многих служебных, автоматически поддерживаемых RDS структурах, необходимых для работы схемы. Если в схему вносятся крупные изменения, например, добавляется и удаляется множество блоков и связей, целесообразно вносить изменения в эти структуры не после каждого небольшого изменения, а после завершения всех изменений. Перед добавлением блоков и связей лучше всего заблокировать изменения в служебных структурах RDS вызовом функции rdsSetSystemUpdate с параметром FALSE. Это не обязательно, но в противном случае RDS будет обновлять свои структуры после добавления каждого блока и каждой связи, что приведет к существенному замедлению работы. При блокировке изменений функцией rdsSetSystemUpdate крайне важно помнить, что после каждого ее вызова с параметром FALSE обязательно должен следовать вызов с параметром TRUE, иначе RDS не сможет работать правильно.

Заблокировав изменения в служебных структурах RDS, мы вызываем функцию LoadBlocks, передавая ей идентификатор нашего блока BlockData->Block, имя файла с описанием блока из переменной BlockFile и имя списка блоков из переменной BlockList. Если функция вернет TRUE (блоки добавлены успешно), мы вызовем LoadConnections, передав ей идентификатор блока и имя списка связей из переменной ConnList. После этого мы снова разрешаем обновление служебных структур RDS вызовом rdsSetSystemUpdate(TRUE).

Если идентификатор выбранного пункта меню – 2, мы блокируем обновление служебных структур, удаляем добавленные блоки и связи вызовом написанной нами функции DeleteByList, а затем снова разрешаем обновление служебных структур.

Теперь можно протестировать созданную модель. Подключим ее к блоку – назовем его, например, «Block1» (рис. 119 а) – и создадим текстовый файл «blocklist.txt» со списком блоков следующего вида:

      Block1, 50,  100
      Block2, 100, 100
      Block3, 200, 200
      Block4, 100, 200
      Block5, 200, 100
      Block6, 200, 25

Нам также потребуется текстовый файл «connlist.txt» со списком связей:

      Block1, x, Block2, y
      Block2, y, Block3, x
      Block2, x, Block4, y
      Block5, y, Block4, x
      Block6, x, Block5, x

(а)

(б)

Рис. 119. Программное добавление блоков и связей: схема перед (а) и после (б) добавления

Теперь, если настроить блок, как показано на на рис. 118, и выбрать в контекстном меню блока пункт «Добавить блоки и связи», подсистема с этим блоком примет вид, показанный на рис. 119 б. Можно заметить, что созданный нами блок был автоматически переименован в «Block7», поскольку имена, начиная с «Block1» и заканчивая «Block6», встретились в списке блоков, и ему пришлось переименовываться, чтобы добавить их в подсистему. Связи между блоками, как мы и планировали, начинаются и заканчиваются на их границах.

Если теперь выбрать в меню блока пункт «Удалить добавленное», все добавленные блоки и связи будут удалены.

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