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

Описания RDS

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

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

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

§2.13.4. Пример использования функций блоков для поиска пути в графе

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

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

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

#### §2.13.4. Пример использования функций блоков для поиска пути в графе

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

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

Рис. 87. Длина дуги графа
в рассматриваемом примере

Длиной дуги графа будем считать общую длину связи, к которой, поскольку блок в RDS – не точечный объект, добавлены расстояния между геометрическими центрами блоков и точками присоединения связи к этим блокам (рис. 87). Если блоки 1 и 2 соединены связью общей длины LС, расстояние между геометрическим центром изображения первого блока и точкой присоединения этой связи к нему – L1, и расстояние между центром второго блока и точкой присоединения связи – L2, то длиной дуги, соединяющей узлы графа, представленные блоками 1 и 2, мы будем считать сумму этих трех величин, то есть LС+L1+L2. Конечно, можно было бы пренебречь расстояниями L1 и L2, сделав размеры блоков достаточный маленькими (существенно меньшими расстояний между ними), но, для большей универсальности примера, мы все-таки будем их учитывать. Вычисление длины дуги мы вынесем в отдельную функцию, чтобы ее, при необходимости, можно было изменить.

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

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

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

1. Необходимо дать пользователю возможность указывать блок начала маршрута, блок конца маршрута, а также сбрасывать выделенный маршрут. Будем использовать для этого контекстное меню блока.
2. Блоки, принадлежащие и не принадлежащие маршруту, должны выглядеть по-разному. Начальный и конечный блоки маршрута тоже должны иметь специальные отметки. Мы решим эту задачу двумя способами: во-первых, мы сделаем в модели программное рисование, которое будет все это учитывать, и, во-вторых, мы введем в блок статические логические переменные, которые будут отражать принадлежность блока к маршруту, его началу и концу. Если пользователю по какой-то причине не понравится программное рисование, он сможет задать блокам векторную картинку, связав ее элементы с этими переменными.
3. Необходимо изменять внешний вид связи, изображающей дугу графа на выделенном маршруте. При изменении или сбросе маршрута необходимо восстанавливать ее прежний вид. Для этого мы воспользуемся специальным механизмом RDS, позволяющим временно менять параметры внешнего вида связи.
4. Необходимо уметь вычислять длину дуги между двумя блоками. При этом надо учитывать, что связь в RDS может представлять собой набор последовательно соединенных отрезков прямых и кривых Безье. Для вычисления общей длины связи мы напишем специальную функцию.
5. Наконец, самое важное: если в схеме заданы и начальный, и конечный блок, необходимо найти кратчайший маршрут между ними. Мы будем использовать для этого алгоритм, похожий на алгоритм Дейкстры, но несколько упрощенный для большей наглядности примера. Упростив алгоритм, мы, вероятно, увеличим время, необходимое на поиск маршрута, но в данном примере это не особенно важно. Способ поиска маршрута, который мы будем использовать, описан ниже.

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

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

- В блоке нет метки. В этом случае блоку присваивается метка M, все блоки, в которые можно попасть из данного за один переход по связи, помечаются числом (M + длина дуги к блоку-соседу), то есть данная операция выполняется уже для них, но с числом, увеличившимся на расстояние до нового помечаемого блока.
- В блоке есть метка K, K≤M (метка, уже имеющаяся в блоке, меньше или равна той, которой мы пытаемся его пометить). Это значит, что этот блок раньше уже помечался, причем тогда расстояние до начального блока было меньше. Такая ситуация возможна при наличии нескольких параллельных путей разной длины между начальным блоком и данным, причем данный блок уже получил метку по более короткому пути. В этом случае метка блока не изменяется, никаких дополнительных пометок блоков не производится.
- В блоке есть метка K, K>M (метка, уже имеющаяся в блоке, больше той, которой мы пытаемся его пометить). Это значит, что мы только что нашли более короткий путь от начального блока к данному. В этом случае метка блока заменяется на M и все блоки, в которые можно попасть из данного за один переход по связи, помечаются числом (M + длина дуги к блоку-соседу).

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

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

Рис. 88. Координаты кривой Безье

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

Кривая Безье параметрически задается следующими формулами:

где t – параметр кривой.

Коэффициенты a, b и c вычисляются по координатам точек рис. 88 следующим образом:

Длина любой плоской кривой, заданной функциями x(t) и y(t) в диапазоне 0≤t≤1 вычисляется так:

Для кривой Безье:

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

      // Вычисление длины кривой Безье численным интегрированием
      // (метод Гаусса)
      double BezierLengthGauss(double x1,double y1,double dx1,double dy1,
                               double x2,double y2,double dx2,double dy2,
                               double delta)
      { double ax,bx,cx,ay,by,cy;
        double a,b,c,d,e,q,n,k,T,l,h,I,w;
        int m;
        double x[3],s[3];

        // Вычисление коэффициентов параметрического вида
        ax=2*(x1-x2)+3*(dx1-dx2);
        bx=3*(x2-x1+dx2-2*dx1);
        cx=3*dx1;
        ay=2*(y1-y2)+3*(dy1-dy2);
        by=3*(y2-y1+dy2-2*dy1);
        cy=3*dy1;

        // Интегрирование
        b=1.0;
        n=delta*60.0; m=1; k=0.0;
        do
          { m*=2; a=0.0; T=sqrt(0.6); I=0.0;
            h=(b-a)/m;
            for(int j=1;j<=m;j++)
              { w=a+h;
                c=(w+a)/2; d=(w-a)/2; e=d*5.0/9.0;
                l=d*8.0/9.0; d*=T;
                x[0]=c-d; x[1]=c; x[2]=c+d;
                s[0]=e;   s[1]=l; s[2]=e;
                for(int i=0;i<3;i++)
                  { // Вычисление подынтегральной функции (3 раза)
                    double vx=3*ax*x[i]*x[i]+2*bx*x[i]+cx,
                           vy=3*ay*x[i]*x[i]+2*by*x[i]+cy;
                    double f=sqrt(vx*vx+vy*vy);
                    I+=s[i]*f;
                  }
                a=w;
              }
            l=k; k=I;
          }
        while(fabs(I-l)>n);
        return I;
      }
      //=========================================

Для вычисления длины дуги графа нам также потребуется функция, вычисляющая расстояние между геометрическим центром блока и точкой присоединения связи к нему (L1 и L2 на рис. 87). Эта функция будет принимать один параметр: указатель на структуру описания точки связи RDS_POINTDESCRIPTION. Мы уже встречались с этой структурой, когда проверяли существование связи, соединенной с конкретной переменной блока, она используется во всех сервисных функциях, работающих со связями и их точками. В структуре находятся все данные, которые необходимы нам для вычисления расстояния: идентификатор блока, к которому присоединена связь (поле Block) и координаты самой точки (поля x и y). Следует помнить, что координаты точки присоединения связи к блоку всегда даются относительно точки привязки изображения этого блока (для программно рисуемых блоков и блоков без векторной картинки – относительно верхнего левого угла, для блоков с картинкой – относительно начала координат векторного изображения), и это нужно учитывать при вычислении расстояния от этой точки до центра блока. Функция, вычисляющая это расстояние, будет выглядеть так:

      // Определение расстояния между точкой связи и геометрическим
      // центром блока по структуре RDS_POINTDESCRIPTION
      double DistanceFromBlockCenterToPoint(RDS_PPOINTDESCRIPTION point)
      { RDS_BLOCKDIMENSIONS dim; // Структура описания размеров блока
        double dx,dy,xc,yc,xp,yp;

        if(point==NULL)	// Указатель не передан - ошибка
          return -1.0;
        // Проверка – точка ли соединения с блоком передана?
        if(point->PointType!=RDS_PTBLOCK)
          return -1.0;

        // Определение размеров блока point->Block
        dim.servSize=sizeof(dim);	// Размер структуры
        if(!rdsGetBlockDimensionsEx(point->Block,&dim,RDS_GBD_NONE))
          return -1.0; // Не удалось получить размеры блока

        // Геометрический центр изображения блока
        xc=dim.Left+dim.Width/2.0;
        yc=dim.Top+dim.Height/2.0;
        // Абсолютные координаты точки связи
        xp=dim.BlockX+point->x;
        yp=dim.BlockY+point->y;
        // Вычисление расстояния между этими точками
        dx=xp-xc; dy=yp-yc;
        return sqrt(dx*dx+dy*dy);
      }
      //=========================================

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

Далее структура dim типа RDS_BLOCKDIMENSIONS заполняется данными о размерах и положении изображения блока, с которым соединена точка связи. Для этого вызывается сервисная функция RDS rdsGetBlockDimensionsEx, в которую передается идентификатор интересующего нас блока point->Block, указатель на заполняемую структуру &dim и константа RDS_GBD_NONE, указывающая, что при определении размеров и положения блока не нужно учитывать ни масштаб подсистемы, ни возможную связь этих параметров с переменными блока. Таким образом, в структуру dim будут записаны размеры блока в режиме редактирования в масштабе 100%.

Структура RDS_BLOCKDIMENSIONS описана в «RdsDef.h» следующим образом:

      typedef struct
      { DWORD servSize;    // Размер этой структуры
        int BlockX,BlockY; // Точка привязки блока
        int Left,Top;      // Верхний левый угол блока
        int Width,Height;  // Ширина и высота блока
      } RDS_BLOCKDIMENSIONS;
      typedef RDS_BLOCKDIMENSIONS *RDS_PBLOCKDIMENSIONS;

Перед вызовом функции в поле servSize (первое поле структуры, как и в большинстве структур, используемых сервисными функциями RDS) необходимо занести размер этой структуры для проверки правильности переданных параметров. После заполнения этой структуры функцией rdsGetBlockDimensionsEx мы вычисляем координаты центра изображения блока (xc,yc) и абсолютные координаты точки связи (xp,yp). Центр блока – это верхний левый угол изображения, смещенный на половину его ширины и высоты, а абсолютные координаты точки связи вычисляются добавлением к точке привязки блока (dim.BlockX,dim.BlockY) относительных координат точки связи (point-point->xx,point->y). Далее вычисляется расстояние между этими точками, и функция возвращает полученное значение.

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

      // Проверка связи (должно быть два блока на концах) и вычисление
      // длины дуги графа, соответствующей этой связи
      double CalcArcLength(RDS_CHANDLE Conn)
      { RDS_CONNDESCRIPTION ConnDescr;   // Структура описания связи
        RDS_POINTDESCRIPTION PointDescr; // Структура описания точки
        RDS_LINEDESCRIPTION LineDescr;   // Структура описания линии
        int BlockCnt;
        double len=0.0; // Общая длина дуги
        double x1,y1,x2,y2;

        // Заполнение служебных полей размеров структур
        ConnDescr.servSize=sizeof(ConnDescr);
        PointDescr.servSize=sizeof(PointDescr);
        LineDescr.servSize=sizeof(LineDescr);

        // Получаем описание связи – нам нужно число точек и линий в ней
        if(!rdsGetConnDescription(Conn,&ConnDescr))
          return -1.0;

        if(ConnDescr.ConnType!=RDS_CTCONNECTION)
          return -1.0; // Это не связь, а шина – шины нам не годятся

        // Проверяем число блоков на концах связи (должно быть ровно 2)
        BlockCnt=0;
        for(int i=0;i<ConnDescr.NumPoints;i++)
          { // Получаем описание точки связи i
            rdsGetPointDescription(Conn,i,&PointDescr);
            // Проверяем тип точки
            switch(PointDescr.PointType)
              { case RDS_PTBUS: // Соединение с шиной – связь не годится
                  return -1.0;
                case RDS_PTBLOCK: // Соединение с блоком
                  BlockCnt++;
                  if(BlockCnt>2) // Связь разветвлена
                    return -1.0;
                  // Найдена точка соединения с блоком. Добавляем к len
                  // расстояние между точкой и центром блока
                  len+=DistanceFromBlockCenterToPoint(&PointDescr);
                  break;
              } // switch(PointDescr.PointType)
          } // for(int i=0;...)
        if(BlockCnt!=2) // Связь соединяет менее двух блоков
         return -1.0;

        // Связь соединена ровно с двумя блоками – суммируем длину всех
        // ее линий
        for(int i=0;i<ConnDescr.NumLines;i++)
          { // Получаем описание линии связи i
            rdsGetLineDescription(Conn,i,&LineDescr,NULL,NULL);
            // Переводим целые координаты концов линии в double
            // для большей точности вычисления
            x1=LineDescr.x1; y1=LineDescr.y1;
            x2=LineDescr.x2; y2=LineDescr.y2;
            // В зависимости от типа линии, вычисляем ее длину и
            // добавляем к len
            switch(LineDescr.LineType)
              { case RDS_LNLINE: // Отрезок прямой
                  len+=sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
                  break;
                case RDS_LNBEZIER: // Кривая Безье
                  len+=BezierLengthGauss(x1,y1,
                         LineDescr.dx1,LineDescr.dy1,
                         x2,y2,
                         LineDescr.dx2,LineDescr.dy2,
                         2);
                  break;
              } // switch(LineDescr.LineType)
          } // for(int i=0;...)
        return len;
      }
      //=========================================

В этой функции мы перебираем все точки связи, идентификатор которой передан в параметре Conn, чтобы найти среди них точки соединения с блоками и подсчитать их. Кроме того, мы перебираем все отрезки линий связи и суммируем их длину. Для того, чтобы организовать циклы по всем точкам и всем линиям, нам нужно узнать общее число точек и линий в этой связи. Для этого мы вызываем сервисную функцию rdsGetConnDescription, которая заполняет структуру описания связи ConnDescr типа RDS_CONNDESCRIPTION. В этой структуре много полей, но нас будет интересовать поле типа связи ConnType, число точек в ней NumPoints и число отрезков NumLines. Поле ConnType нам нужно только для того, чтобы проверить, не передан ли случайно в нашу функцию идентификатор шины вместо идентификатора связи (в RDS шина является разновидностью связи). Шина, очевидно, не может изображать дугу графа, поэтому если тип связи не равен RDS_CTCONNECTION, функция возвращает отрицательное значение, сигнализирующее об ошибке.

Далее мы в цикле перебираем все точки исследуемой связи, подсчитывая в процессе перебора количество соединяемых связью блоков. Внутри цикла мы заполняем структуру PointDescr описанием очередной (i-й) точки связи Conn при помощи функции rdsGetPointDescription, после чего при помощи оператора switch(PointDescr.PointType) анализируем тип этой точки. Если это точка соединения связи с шиной (поле PointDescr.PointType имеет значение RDS_PTBUS), функция немедленно возвращает отрицательное значение: связь, соединенная с шиной, не может изображать дугу графа. Если это точка соединения с блоком (тип точки – RDS_PTBLOCK), мы увеличиваем на единицу предварительно обнуленную целую переменную BlockCnt, в которой подсчитывается число соединенных блоков. Если значение этой переменной превысило 2, значит, связь соединяет три или более блоков, и она не может быть дугой графа – дальнейший перебор точек не имеет смысла, и функция немедленно возвращает отрицательное значение. В противном случае значение расстояния от данной точки связи до центра соединенного с ней блока добавляется к переменной len, для этого вызывается ранее написанная функция DistanceFromBlockCenterToPoint, в которую передается указатель на заполненную структуру описания точки.

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

После перебора всех точек связи в переменной BlockCnt будет находиться число блоков, с которыми соединена данная связь, а в переменной len – сумма расстояний от центра блока до точки связи для всех соединенных блоков (L1+L2 на рис. 87). Случай BlockCnt>2 мы уже обработали внутри цикла, но связь может оказаться оборванной, то есть соединенной только с одним блоком, или не соединенной ни с одним. В этом случае она тоже не может служить дугой графа, поэтому, если BlockCnt не равно двум, функция возвращает отрицательное значение.

Теперь, когда мы убедились, что связь соединяет ровно два блока, можно добавить к len сумму длин всех ее отрезков, что даст нам длину дуги графа. Для этого мы в цикле перебираем все линии связи. Внутри цикла мы заполняем структуру LineDescr типа RDS_LINEDESCRIPTION описанием i-й линии связи Conn при помощи сервисной функции rdsGetLineDescription. В двух последних параметрах этой функции можно было бы передать указатели на структуры описания точек связи, которые эта линия соединяет, чтобы rdsGetLineDescription их тоже заполнила, но нам эти описания не требуются, поэтому вместо обоих указателей передается значение NULL. Нам нужно только описание самой линии, чтобы вычислить ее длину и добавить к переменной len. Структура RDS_LINEDESCRIPTION описана в «RdsDef.h» следующим образом:

      typedef struct
      { DWORD servSize;     // Размер этой структуры
        int LineType;       // Тип линии (RDS_LN*)
        int nPoint1,nPoint2;// Номера соединенных ей точек
        int x1,y1;          // Абсолютные координаты точки nPoint1
        int x2,y2;          // Абсолютные координаты точки nPoint2
        int dx1,dy1;        // Смещения управляющей точки 1 для кривой Безье
        int dx2,dy2;        // Смещения управляющей точки 2 для кривой Безье
        RDS_CHANDLE Owner;  // Связь-владелец линии
      } RDS_LINEDESCRIPTION;
      typedef RDS_LINEDESCRIPTION *RDS_PLINEDESCRIPTION;

Нас в этой структуре интересует поле LineType, определяющее тип отрезка (RDS_LNLINE – отрезок прямой, RDS_LNBEZIER – кривая Безье), координаты его конечных точек (x1,y1) и (x2,y2), а также, в случае кривой Безье, смещения управляющих точек (dx1,dy1) и (dx2,dy2). В зависимости от типа линии мы добавляем к переменной len либо длину отрезка прямой, либо длину кривой Безье, для вычисления которой мы вызываем ранее написанную функцию BezierLengthGauss, задав точность вычисления (последний параметр функции) в две точки экрана.

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

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

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

      // Визуально выделить связь
      void MarkConnection(RDS_CHANDLE Conn)
      { // Определяем число альтернативных внешних видов
        int StylesCount=rdsAltConnAppearanceOp(Conn,RDS_CAOCOUNT,0,NULL);

        if(StylesCount<1)
          { // Для связи еще не определено ни одного внешнего вида
            // Создаем внешний вид 0 и делаем его толще текущего
            RDS_CONNAPPEARANCE style; // Структура описания стиля связи
            // Получаем описание текущего внешнего вида связи
            style.servSize=sizeof(style);
            rdsGetConnAppearance(Conn,&style);
            // Увеличиваем толщину и размер стрелки
            style.LineWidth*=3;
            style.ArrowLength*=2;
            style.ArrowWidth*=3;
            // Запоминаем эти параметры как альтернативный вид 0
            rdsAltConnAppearanceOp(Conn,RDS_CAOSET,0,&style);
          }
        // Устанавливаем альтернативный вид 0
        rdsAltConnAppearanceOp(Conn,RDS_CAOSETCURRENT,0,NULL);
      }
      //=========================================

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

- rdsAltConnAppearanceOp(Conn,RDS_CAOSET,n,&style) – установить параметры из структуры style в качестве альтернативного внешнего вида номер n для связи Conn;
- rdsAltConnAppearanceOp(Conn,RDS_CAOCOUNT,0,NULL) – вернуть число альтернативных внешних видов, созданных для связи Conn;
- rdsAltConnAppearanceOp(Conn,RDS_CAOGET,n,&style) – считать параметры альтернативного вида номер n связи Conn в структуру style;
- rdsAltConnAppearanceOp(Conn,RDS_CAOSETCURRENT,n,NULL) – активировать альтернативный внешний вид номер n в связи Conn (связь будет выглядеть согласно параметрам, запомненным для этого внешнего вида);
- rdsAltConnAppearanceOp(Conn,RDS_CAODELETE,n,NULL) – удалить альтернативный внешний вид номер n в связи Conn (все наборы с номерами, большими n, сдвинутся вниз на 1);
- rdsAltConnAppearanceOp(Conn,RDS_CAORESTORE,0,NULL) – восстановить исходный внешний вид связи Conn.

В функции MarkConnection мы, прежде всего, определяем число уже существующих в связи Conn альтернативных наборов параметров внешнего вида и записываем его в переменную StylesCount. Если это значение больше или равно 1, значит, мы уже создали для этой связи альтернативный внешний вид, нужно только активировать его. В противном случае параметры текущего, то есть заданного пользователем при создании связи, внешнего вида считываются функцией rdsGetConnAppearance в структуру style типа RDS_CONNAPPEARANCE. Эта структура описана в «RdsDef.h» следующим образом:

      typedef struct
      { DWORD servSize;     // Размер этой структуры
        COLORREF LineColor; // Цвет линии
        int LineWidth;      // Толщна линии
        int LineStyle;      // Стиль линии (константа WinAPI)
        int ArrowLength;    // Длина стрелки (0...255)
        int ArrowWidth;     // Ширина выступа стрелки (0...255)
        int DotSize;        // Размер точки в месте ветвления
      } RDS_CONNAPPEARANCE;
      typedef RDS_CONNAPPEARANCE *RDS_PCONNAPPEARANCE;

Для визуального выделения связи мы увеличиваем толщину линии и ширину стрелки в 3 раза, а длину стрелки – в 2 раза. Затем, при помощи вызова rdsAltConnAppearanceOp с константой RDS_CAOSET, мы создаем в связи альтернативный внешний вид номер 0 с параметрами из структуры style.

В самом конце функции вызовом rdsAltConnAppearanceOp с константой RDS_CAOSETCURRENT в связи Conn включается альтернативный внешний вид номер 0, созданный при самом первом вызове функции MarkConnection для данной связи.

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

      // Снять выделение связи
      void UnmarkConnection(RDS_CHANDLE Conn)
      { // Восстанавливаем исходный внешний вид связи
        rdsAltConnAppearanceOp(Conn,RDS_CAORESTORE,0,NULL);
      }
      //=========================================

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

      // Функция сброса параметров в узле графа
      #define PROGGUIDEGRAPHPATHFUNC_RESET \
                "ProgrammersGuide.GraphPath.Reset"
      // Структура парметров функции
      typedef struct
      { DWORD servSize;  // Размер этой структуры
        BOOL ResetMark;  // TRUE – сбросить метку узла
        BOOL ResetBegin; // TRUE – сбросить флаг начала маршрута
        BOOL ResetEnd;   // TRUE – сбросить флаг конца маршрута
      } TProgGuideFuncResetParams;

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

Нам нужно уметь не только сбрасывать параметры узла, но и считывать их. Создадим для этого функцию «ProgrammersGuide.GraphPath.GetParams» со следующими описаниями:

      // Функция получения параметров узла графа
      #define PROGGUIDEGRAPHPATHFUNC_GETPARAMS \
                "ProgrammersGuide.GraphPath.GetParams"
      // Среагировавший на функцию блок должен вернуть значение 1
      // Структура парметров функции
      typedef struct
      { DWORD servSize; // Размер этой структуры
        BOOL Marked;    // В узле есть метка
        double Mark;    // Значение метки
        BOOL Begin;     // Этот узел – начало маршрута
        BOOL End;       // Этот узел – конец маршрута
      } TProgGuideFuncGetParams;

Структура параметров этой функции содержит поля, в которые реагирующий на вызов блок запишет свои параметры. Кроме того, эту же функцию мы будем использовать для того, чтобы выяснить, является ли данный блок узлом графа. Для этого потребуем, чтобы среагировавший на функцию блок-узел графа возвращал значение 1. Функции моделей блоков, в которых не предусмотрена реакция на эту функцию, вернут стандартное значение RDS_BFR_DONE, то есть 0 – так мы сможем отличить узлы графа от посторонних блоков. При этом будем считать допустимым и вызов этой функции без параметров, то есть с указателем на параметры, равным NULL – если параметры узла графа нам не нужны, а нужно просто выяснить, является ли блок узлом графа, будем вызывать функцию без параметров и анализировать возвращенное значение.

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

      // Функция поиска начала и конца маршрута
      // (вызывается у всех блоков подсистемы)
      #define PROGGUIDEGRAPHPATHFUNC_FIND \
                "ProgrammersGuide.GraphPath.Find"
      // Структура параметров функции
      typedef struct
      { DWORD servSize;         // Размер этой структуры
        RDS_BHANDLE BeginBlock; // Найденный идентификатор начала
        RDS_BHANDLE EndBlock;   // Найденный идентификатор конца
      } TProgGuideFuncFindParams;

Работать эта функция будет следующим образом. Чтобы найти идентификаторы крайних блоков маршрута, необходимо, как обычно, присвоить полю servSize структуры TProgGuideFuncFindParams размер этой структуры, а в поля BeginBlock и EndBlock записать значение NULL. После этого следует вызвать данную функцию для всех блоков подсистемы, в которой собран граф, при помощи rdsBroadcastFunctionCallsEx. Реагируя на вызов этой функции, блок-узел графа должен записать свой идентификатор в поле BeginBlock, если он является началом маршрута, и в поле EndBlock, если он является концом. Таким образом, после завершения rdsBroadcastFunctionCallsEx в поле BeginBlock окажется идентификатор начала маршрута, а в поле EndBlock – идентификатор конца. Если начало или конец маршрута в графе не заданы, соответствующее поле останется равным NULL.

Конечно, можно было бы найти начало и конец маршрута и без этой функции – например, можно просто перебрать все блоки подсистемы, вызывая у каждого «ProgrammersGuide.GraphPath.GetParams» и запоминая идентификаторы блоков, вернувших TRUE в полях Begin и End структуры TProgGuideFuncGetParams. Однако, такой способ был бы более громоздким, чем использование специальной функции.

Для пометки узла графа вещественным числом нам тоже потребуется функция:

      // Пометить узел графа указанным вещественным числом и вызвать
      // эту же функцию у его соседей
      #define PROGGUIDEGRAPHPATHFUNC_MARK \
                "ProgrammersGuide.GraphPath.Mark"
      // Структура параметров функции
      typedef struct
      { DWORD servSize;       // Размер этой структуры
        double Mark;          // Устанавливаемое значение метки
        RDS_BHANDLE Previous; // Блок, от которого пришла метка
      } TProgGuideFuncMarkParams;

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

Наконец, после разметки графа нам потребуется функция, которая выделит кратчайший маршрут между началом и концом. Поскольку она будет отслеживать маршрут от конечного блока к начальному, назовем ее «ProgrammersGuide.GraphPath.BackTrace»:

      // Выделить маршрут от данного блока к началу
      #define PROGGUIDEGRAPHPATHFUNC_BACKTRACE \
                "ProgrammersGuide.GraphPath.BackTrace"

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

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

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

      int GraphFuncFind=0,     // ProgrammersGuide.GraphPath.Find
          GraphFuncGetParams=0,// ProgrammersGuide.GraphPath.GetParams
          GraphFuncReset=0,    // ProgrammersGuide.GraphPath.Reset
          GraphFuncMark=0,     // ProgrammersGuide.GraphPath.Mark
          GraphFuncBackTrace=0;// ProgrammersGuide.GraphPath.BackTrace

Регистрировать функции мы будем в момент инициализации модели блока.

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

      // Сбросить у узлов графа в заданной подсистеме заданные маркеры
      void GraphPath_Reset(RDS_BHANDLE Sys, // Подсистема
                           BOOL mark,  // Сбросить метки узлов
                           BOOL begin, // Сбросить начало маршрута
                           BOOL end)   // Сбросить конец маршрута
      { // Структура параметров функции ProgrammersGuide.GraphPath.Reset
        TProgGuideFuncResetParams params;
        // Заполняем поле размера структуры парааметров
        params.servSize=sizeof(params);
        // Заполняем поля структуры параметров
        params.ResetMark=mark;
        params.ResetBegin=begin;
        params.ResetEnd=end;
        // Вызываем ProgrammersGuide.GraphPath.Reset у всех
        // блоков подсистемы Sys
        rdsBroadcastFunctionCallsEx(Sys,GraphFuncReset,&params,0);
      }
      //=========================================

Эта функция принимает четыре параметра: идентификатор подсистемы, в которой находится граф, параметры которого сбрасываются, и три логических значения, указывающих, какие именно параметры графа сбрасываются. Внутри функции заполняется структура TProgGuideFuncResetParams и для всех блоков указанной подсистемы вызывается функция «ProgrammersGuide.GraphPath.Reset», идентификатор которой находится в переменной GraphFuncReset (мы считаем, что функция уже зарегистрирована).

Напишем такую же функцию-оболочку для поиска блоков начала и конца маршрута:

      // Найти блоки начала и конца маршрута в заданной подсистеме
      BOOL GraphPath_GetTerminalBlocks(
        RDS_BHANDLE Sys,     // Подсистема с графом
        RDS_BHANDLE *pBegin, // Возвращаемый идентификатор начала
        RDS_BHANDLE *pEnd)   // Возвращаемый идентификатор конца
      { TProgGuideFuncFindParams params;
        // Заполняем поле размера структуры паараметров
        params.servSize=sizeof(params);
        // Обнуляем поля идентификаторов начала и конца
        params.BeginBlock=params.EndBlock=NULL;
        // Вызываем ProgrammersGuide.GraphPath.Find у всех блоков в
        // подсистеме Sys, разрешая блокам остановить вызовы
        rdsBroadcastFunctionCallsEx(Sys,GraphFuncFind,&params,
            RDS_BCALL_ALLOWSTOP);
        // Копируем найденные идентификаторы в переданные указатели
        if(pBegin) *pBegin=params.BeginBlock;
        if(pEnd) *pEnd=params.EndBlock;
        // Возвращаем TRUE, если установлены и начало, и конец
        return params.BeginBlock!=NULL && params.EndBlock!=NULL;
      }
      //=========================================

В эту функцию передается три параметра – идентификатор подсистемы с графом Sys и два указателя pBegin и pEnd, по которым возвращаются найденные начало и конец маршрута соответственно. Внутри функции обнуляются поля BeginBlock и EndBlock структуры TProgGuideFuncFindParams, после чего она используется как параметр при вызове функции «ProgrammersGuide.GraphPath.Find» у всех блоков подсистемы Sys. В сервисную функцию rdsBroadcastFunctionCallsEx передается флаг RDS_BCALL_ALLOWSTOP, что позволяет любому из вызываемых блоков досрочно прекратить вызовы. Этим можно воспользоваться для ускорения работы функции: если какой-либо блок обнаружит, что и поле BeginBlock, и поле EndBlock структуры параметров функции уже содержат идентификаторы, можно прекратить перебор блоков, поскольку и начало, и конец маршрута уже найдены. После завершения работы rdsBroadcastFunctionCallsEx найденные идентификаторы передаются в вызвавшую функцию через указатели pBegin и pEnd, и функция возвращает TRUE, если в графе заданы и начало, и конец маршрута.

Третья функция будет вызывать сразу две функции блоков: функцию разметки графа «ProgrammersGuide.GraphPath.Mark» и, после нее, функцию отслеживания маршрута по разметке «ProgrammersGuide.GraphPath.BackTrace». Мы будем использовать ее для поиска маршрута в графе, если на нем уже отмечены начало и конец, а маркировка графа сброшена:

      // Поиск маршрута в графе в заданной подсистеме
      void GraphPath_FindPath(RDS_BHANDLE System)
      { RDS_BHANDLE StartBlock,EndBlock;
        TProgGuideFuncMarkParams markparams;

        // Считаем, что маркировка всего графа сброшена

        // Ищем начальную и конечную точку маршрута
        if(!GraphPath_GetTerminalBlocks(System,&StartBlock,&EndBlock))
          return; // Начало или конец не найдены
        // Начало маршрута – StartBlock, конец - EndBlock

        // Маркируем граф от начала маршрута
        markparams.servSize=sizeof(markparams);
        markparams.Mark=0.0; // Начало маркируется значением 0
        markparams.Previous=NULL; // Это значение не пришло от какого-то
                                  // соседнего блока
        // Вызываем функцию маркировки для начального блока
        rdsCallBlockFunction(StartBlock,GraphFuncMark,&markparams);

        // Теперь отслеживаем кратчайший путь в обратном направлении
        // (от конечного блока)
        rdsCallBlockFunction(EndBlock,GraphFuncBackTrace,NULL);
      }
      //=========================================

Сначала мы, используя уже написанную функцию GraphPath_GetTerminalBlocks, ищем в графе внутри подсистемы System начало и конец маршрута – их идентификаторы записываются в переменные StartBlock и EndBlock соответственно. Если GraphPath_GetTerminalBlocks вернет FALSE, значит, в графе не отмечены начало или конец маршрута, и поиск невозможен – функция немедленно завершается. В противном случае мы подготавливаем структуру markparams типа TProgGuideFuncMarkParams для пометки начального блока маршрута числом 0: для этого мы записываем 0 в поле Mark и NULL в поле Previous. Поле Prevoius в процессе разметки графа мы используем для того, чтобы, для ускорения работы, при пометке соседей очередного узла не помечать тот узел, из которого мы попали в данный (его метка заведомо меньше метки данного, и, согласно алгоритму разметки, он все равно не будет помечен). При пометке самого первого, начального, узла у нас нет блока, из которого мы попадаем в него, поэтому вместо идентификатора блока мы используем значение NULL.

Далее сервисной функцией rdsCallBlockFunction мы вызываем у блока StartBlock функцию пометки «ProgrammersGuide.GraphPath.Mark» (ее идентификатор должен быть записан в глобальную переменную GraphFuncMark), передавая в качестве параметров функции указатель на структуру markparams. Это приведет к тому, что начальный блок маршрута будет помечен числом 0 и пометит своих соседей расстояниями до них. Они, в свою очередь, пометят своих соседей, те – своих, и т.д., пока все узлы графа не будут помечены расстояниями до начала маршрута.

Когда rdsCallBlockFunction завершится, весь граф окажется размеченным. Теперь мы вызываем функцию «ProgrammersGuide.GraphPath.BackTrace» (ее идентификатор находится в GraphFuncBackTrace) у блока конца маршрута EndBlock: он должен перейти в выделенное состояние, выделить связь, ведущую к нему от блока с наименьшей меткой и вызвать у него ту же функцию. Тот, в свою очередь, тоже перейдет в выделенное состояние, найдет среди блоков, соединенных с ним, блок с наименьшей меткой, выделит связь, ведущую от него к себе и вызовет эту функцию у него, и т.д. Таким образом, весь маршрут от начала до конца окажется выделенным.

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

Теперь пришло время подумать о переменных, которые необходимы блоку-узлу графа для работы. Нам потребуются две логических переменных для хранения флагов начала и конца маршрута (назовем их «sBegin» и «sEnd» соответственно), логическая переменная, указывающая на принадлежность блока к найденному маршруту (назовем ее «sInPath»), вещественная переменная для хранения метки узла графа («sPathMark»), и еще одна логическая переменная, указывающая на наличие этой метки («sMarked»). Эти переменные будут управлять состоянием блока следующим образом:

- sBegin==1 – данный узел графа является началом маршрута;
- sEnd==1 – данный узел графа является концом маршрута;
- sInPath==1 – данный узел графа принадлежит к найденному маршруту и должен визуально выделяться;
- sMarked==0 – у данного узла графа нет метки (расстояние до начала маршрута еще не найдено);
- sMarked==1 – узел графа помечен вещественным числом sPathMark.

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

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | sBegin | Логический | 1 | Внутренняя |  | 0
3 | sEnd | Логический | 1 | Внутренняя |  | 0
4 | sInPath | Логический | 1 | Внутренняя |  | 0
5 | sMarked | Логический | 1 | Внутренняя |  | 0
6 | sPathMark | double | 8 | Внутренняя |  | 0

Блок-узел графа не работает в режиме расчета и не передает никаких данных по связям, поэтому все переменные, кроме двух обязательных сигналов «Start» и «Ready», описаны как внутренние. Блоку все равно нужен по крайней мере один вход и один выход, чтобы можно было соединять такие блоки связями, символизирующими дуги графа – для этой цели мы будем использовать «Start» и «Ready».

Может возникнуть вопрос: почему все имена переменных мы начинаем с буквы «s»? Почему бы не назвать переменную для флага начала маршрута просто «Begin», а не «sBegin»? Дело в том, что в структуре TProgGuideFuncGetParams, описанной нами, есть поля Begin, End и Marked. Если мы хотим использовать для доступа к переменным блока макросы с теми же названиями, эти названия не должны совпадать с именами других переменных и полей структур и классов, которые используются в тех же функциях (эта проблема уже упоминалась в §2.12.1). Например, если бы мы назвали переменную блока «Begin», а не «sBegin», макрос для нее выглядел бы так:

      #define pStart ((char *)(BlockData->VarTreeData))
      // …
      #define Begin (*((char *)(pStart+2)))

В результате, встретив в тексте программы конструкцию вида

      TProgGuideFuncGetParams data;
      data.Begin=TRUE;

препроцессор посчитал бы слово Begin именем макроса и развернул бы его:

      TProgGuideFuncGetParams data;
      data.(*((char *)(((char *)(BlockData->VarTreeData))+2)))=TRUE;

Чтобы исключить такую возможность, мы, на всякий случай, сделали так, чтобы имена всех переменных блока не совпадали с полями используемых нами структур.

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

      // Макроопределения для статических переменных
      #define pStart    ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define sBegin (*((char *)(pStart+2*RDS_VSZ_S)))
      #define sEnd (*((char *)(pStart+2*RDS_VSZ_S+RDS_VSZ_L)))
      #define sInPath (*((char *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_L)))
      #define sMarked (*((char *)(pStart+2*RDS_VSZ_S+3*RDS_VSZ_L)))
      #define sPathMark (*((double *)(pStart+2*RDS_VSZ_S+4*RDS_VSZ_L)))

Теперь запишем прототипы функций, которые мы будем вызывать из функции модели (сами функции мы напишем позже):

      // Сделать этот блок началом маршрута
      void GraphNode_SetBlockAsBegin(RDS_PBLOCKDATA BlockData);
      // Сделать этот блок концом маршрута
      void GraphNode_SetBlockAsEnd(RDS_PBLOCKDATA BlockData);
      // Реакция блока на функцию ProgrammersGuide.GraphPath.Reset
      void GraphNode_OnReset(RDS_PBLOCKDATA BlockData,
                             TProgGuideFuncResetParams *reset);
      // Реакция блока на функцию ProgrammersGuide.GraphPath.Mark
      void GraphNode_OnMarkBlock(RDS_PBLOCKDATA BlockData,
                                 TProgGuideFuncMarkParams *params);
      // Реакция блока на функцию ProgrammersGuide.GraphPath.BackTrace
      void GraphNode_OnBackTracePath(RDS_PBLOCKDATA BlockData);
      // Программное рисование внешнего вида блока
      void GraphNode_Draw(RDS_PBLOCKDATA BlockData,RDS_PDRAWDATA draw);

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

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

      // Модель блока-узла графа
      extern "C" __declspec(dllexport)
        int RDSCALL GraphNode(int CallMode,
                RDS_PBLOCKDATA BlockData,
                LPVOID ExtParam)
      { // Вспомогательная - указатель на данные функции блока
        RDS_PFUNCTIONCALLDATA func;

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              // Регистрация функций
              if(GraphFuncGetParams==0)
                GraphFuncGetParams=rdsRegisterFunction(
                  PROGGUIDEGRAPHPATHFUNC_GETPARAMS);
              if(GraphFuncReset==0)
                GraphFuncReset=rdsRegisterFunction(
                  PROGGUIDEGRAPHPATHFUNC_RESET);
              if(GraphFuncFind==0)
                GraphFuncFind=rdsRegisterFunction(
                  PROGGUIDEGRAPHPATHFUNC_FIND);
              if(GraphFuncMark==0)
                GraphFuncMark=rdsRegisterFunction(
                  PROGGUIDEGRAPHPATHFUNC_MARK);
              if(GraphFuncBackTrace==0)
                GraphFuncBackTrace=rdsRegisterFunction(
                  PROGGUIDEGRAPHPATHFUNC_BACKTRACE);
              break;

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

            // Вызов контекстного меню блока
            case RDS_BFM_CONTEXTPOPUP:
              rdsAdditionalContextMenuItemEx("Начало маршрута",
                sBegin?RDS_MENU_DISABLED:0,0,0);
              rdsAdditionalContextMenuItemEx("Конец маршрута",
                sEnd?RDS_MENU_DISABLED:0,1,0);
              rdsAdditionalContextMenuItemEx("Сбросить все",0,2,0);
              break;

            // Быбор пункта меню
            case RDS_BFM_MENUFUNCTION:
              switch(((RDS_PMENUFUNCDATA)ExtParam)->Function)
                { case 0: // Начало маршрута
                    GraphNode_SetBlockAsBegin(BlockData);
                    rdsRefreshBlockWindows(BlockData->Parent,FALSE);
                    break;
                  case 1: // Конец маршрута
                    GraphNode_SetBlockAsEnd(BlockData);
                    rdsRefreshBlockWindows(BlockData->Parent,FALSE);
                    break;
                  case 2: // Сбросить все
                    GraphPath_Reset(BlockData->Parent,TRUE,TRUE,TRUE);
                    rdsRefreshBlockWindows(BlockData->Parent,FALSE);
                    break;
                }
              break;

            // Вызов функции блока
            case RDS_BFM_FUNCTIONCALL:
              func=(RDS_PFUNCTIONCALLDATA)ExtParam;
              if(func->Function==GraphFuncReset) // Сброс параметров
                GraphNode_OnReset(BlockData,
                    (TProgGuideFuncResetParams*)(func->Data));
              else if(func->Function==GraphFuncGetParams)
                { // Получение параметров узла
                  TProgGuideFuncGetParams *get=
                      (TProgGuideFuncGetParams*)(func->Data);
                  if(get!=NULL &&
                     get->servSize>=sizeof(TProgGuideFuncGetParams))
                    { // Допустимый размер структуры параметров
                      get->Marked=(sMarked!=0);
                      get->Mark=sPathMark;
                      get->Begin=(sBegin!=0);
                      get->End=(sEnd!=0);
                    }
                  return 1; // Блок является узлом графа
                }
              else if(func->Function==GraphFuncFind)
                { // Поиск начала и конца маршрута
                  TProgGuideFuncFindParams *find=
                      (TProgGuideFuncFindParams*)(func->Data);
                  if(find==NULL) break; // Нет параметров
                  if(find->servSize<sizeof(TProgGuideFuncFindParams))
                    break; // Недопустимый размер структуры параметров
                  if(sBegin) // Этот блок – начало маршрута
                    find->BeginBlock=BlockData->Block;
                  if(sEnd) // Этот блок – конец маршрута
                    find->EndBlock=BlockData->Block;
                  if(find->BeginBlock!=NULL && find->EndBlock!=NULL)
                    func->Stop=TRUE; // Оба конца маршрута найдены
                }
              else if(func->Function==GraphFuncMark) // Пометить граф
                GraphNode_OnMarkBlock(BlockData,
                    (TProgGuideFuncMarkParams*)(func->Data));
              else if(func->Function==GraphFuncBackTrace)
                GraphNode_OnBackTracePath(BlockData); // Выделить маршрут
              break;

            // Рисование
            case RDS_BFM_DRAW:
              GraphNode_Draw(BlockData,(RDS_PDRAWDATA)ExtParam);
              break;
          }
        return RDS_BFR_DONE;
      }
      //=========================================

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

Реакция модели на вызов RDS_BFM_VARCHECK несколько отличается от аналогичных реакций в других примерах: здесь мы анализируем не всю переданную в функцию модели строку типов переменных, а только восемь первых ее символов. Таким образом мы позволяем добавлять в конец списка переменных блока любое количество новых переменных любых типов – нас интересуют только первые семь. Хотя мы и договорились использовать для рисования дуг графа связи, соединенные со входами Start и Ready, пользователю, привыкшему к использованию этих переменных для управления расчетом, это может показаться нелогичным. При желании, он может добавить в блок дополнительные вход и выход, к которым будут подключаться связи, изображающие дуги графа.

В момент открытия контекстного меню блока (при вызове модели в режиме RDS_BFM_CONTEXTPOPUP) мы добавляем в меню три временных пункта: «Начало маршрута» (идентификатор пункта 0), «Конец маршрута» (идентификатор 1) и «Сбросить все» (идентификатор 2). Первые два пункта будут помечать блок, для которого вызвано меню, как начало и конец маршрута соответственно, при этом у блока, который уже помечен как начало или конец, соответствующий пункт меню будет запрещенным. Третий пункт меню сбрасывает маршрут во всем графе и приводит граф в исходное состояние.

При выборе пользователем одного из добавленных пунктов меню (режим RDS_BFM_MENUFUNCTION) мы вызываем функцию GraphNode_SetBlockAsBegin (для пункта «Начало маршрута»), GraphNode_SetBlockAsEnd (для пункта «Конец маршрута») или GraphPath_Reset (для «Сбросить все»). Функцию GraphPath_Reset мы уже написали ранее – в нее передается идентификатор родительской подсистемы данного блока BlockData->Parent и три значения TRUE, указывающие, что в графе внутри этой подсистемы необходимо сбросить и флаги начала маршрута, и флаги его конца, и все метки узлов. Функции GraphNode_SetBlockAsBegin и GraphNode_SetBlockAsEnd, которые должны делать данный блок началом или концом маршрута, мы напишем позже (пока мы написали только их прототипы). После вызова любой из этих трех функций мы перерисовываем окно родительской подсистемы блока сервисной функцией rdsRefreshBlockWindows, чтобы отразить изменения, которые, возможно, произошли в состоянии блоков и внешнем виде связей из-за изменения или сброса маршрута. Параметр FALSE, переданный в функцию, запрещает обновлять окна подсистем, вложенных в BlockData->Parent: изменения в маршруте графа никак не могут отразиться на других подсистемах, и перерисовывать их не нужно.

В реакции на вызов функции блока (RDS_BFM_FUNCTIONCALL) мы, прежде всего, выясняем, какая именно функция вызвана, сравнивая переданный идентификатор с глобальными переменными, в которые мы записали идентификаторы зарегистрированных функций. Для функций «ProgrammersGuide.GraphPath.Reset» (идентификатор хранится в глобальной переменной GraphFuncReset), «ProgrammersGuide.GraphPath.Mark» (идентификатор в GraphFuncMark) и «ProgrammersGuide.GraphPath.BackTrace» (идентификатор в GraphFuncBackTrace) мы вызываем одну из внешних функций, для которых пока написаны только прототипы. На две оставшиеся функции мы реагируем прямо внутри модели.

Если идентификатор совпал с переменной GraphFuncGetParams (вызвана функция «ProgrammersGuide.GraphPath.GetParams» для получения параметров узла графа), мы приводим переданный указатель на параметры функции к типу TProgGuideFuncGetParams* и записываем его во вспомогательную переменную get. Мы решили, что эту функцию можно вызывать и без параметров, в этом случае значение get окажется равным NULL. Если get – не NULL, и поле servSize структуры, на которую указывает get, больше или равно размеру типа TProgGuideFuncGetParams, значит, функция вызвана с параметрами, и эти параметры переданы правильно: нужно записать в поля структуры, на которую указывает get, значения переменных состояния блока. Затем функция возвращает значение 1, указывающее на то, что данный блок является узлом графа.

Если идентификатор совпал с переменной GraphFuncFind, значит, вызвана функция «ProgrammersGuide.GraphPath.Find». В этом случае мы приводим переданный указатель на параметры функции к типу TProgGuideFuncFindParams* и записываем его в переменную find. Если поле servSize переданной в параметрах структуры меньше размера типа TProgGuideFuncFindParams, значит, функция вызвана с неправильными параметрами, и ее работа немедленно завершается оператором break. В противном случае, если данный блок является началом маршрута, мы записываем его идентификатор в поле BeginBlock переданной структуры, если он является концом – в поле EndBlock (мы договорились, что перед вызовом этой функции для всех блоков графа эти поля должны быть обнулены). Затем, если оба поля имеют ненулевые значения (то есть если в структуре уже отметились и блок начала маршрута, и блок его конца), полю Stop структуры описания вызванной функции (RDS_FUNCTIONCALLDATA) присваивается значение TRUE. Это остановит перебор блоков подсистемы, если функция «ProgrammersGuide.GraphPath.Find» была вызвана через сервисную функцию rdsBroadcastFunctionCallsEx (в уже написанной нами функции GraphPath_GetTerminalBlocks мы делаем именно так).

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

Будем изображать блоки выделенного маршрута зелеными прямоугольниками, а все остальные – белыми. На блоке начала и конца маршрута будем выводить буквы «Н» и «К» соответственно. Никаких настроек для программного рисования мы предусматривать не будем: при необходимости, пользователь сможет подключить к блоку векторную картинку любого удобного ему вида. Функция GraphNode_Draw будет выглядеть так:

      // Рисование узла графа
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      void GraphNode_Draw(RDS_PBLOCKDATA BlockData,RDS_PDRAWDATA draw)
      { static char beg[]="Н",end[]="К"; // Метки начала и конца
        int w,h;

        // Рисуем прямоугольник, цвет которого зависит от переменной
        // блока sInPath
        rdsXGSetPenStyle(0,PS_SOLID,1,0,R2_COPYPEN);
        rdsXGSetBrushStyle(0,RDS_GFS_SOLID,sInPath?0xff00:0xffffff);
        rdsXGRectangle(draw->Left,draw->Top,
          draw->Left+draw->Width,draw->Top+draw->Height);

        if(sBegin==0 && sEnd==0) // Нет флагов начала и конца маршрута
          return;
        // Устанавливаем шрифт высотой в весь блок
        rdsXGSetBrushStyle(0,RDS_GFS_EMPTY,0);
        rdsXGSetFont(0,"Arial Cyr",
          draw->Height,0,RUSSIAN_CHARSET,0,FALSE,FALSE,FALSE,FALSE);

        if(sBegin) // Рисуем метку начала
          { rdsXGGetTextSize(beg,&w,&h);
            rdsXGTextOut(draw->Left+(draw->Width-w)/2,
                         draw->Top+(draw->Height-h)/2,
                         beg);
          }
        if(sEnd) // Рисуем метку конца
          { rdsXGGetTextSize(end,&w,&h);
            rdsXGTextOut(draw->Left+(draw->Width-w)/2,
                         draw->Top+(draw->Height-h)/2,
                         end);
          }
      }
      //=========================================

В этой функции мы используем переменные блока, поэтому в нее передается указатель на структуру данных блока RDS_PBLOCKDATA BlockData, используемый в макросах для переменных. Кроме того, как и в другие функции рисования, которые мы писали в предыдущих примерах, в нее передается указатель на структуру типа RDS_DRAWDATA, в которой находятся параметры, необходимые для рисования изображения блока. Внутри функции мы сначала рисуем прямоугольник размером в весь блок. Цвет этого прямоугольника выбирается согласно значению логической переменной блока sInPath: при ее истинном, то есть ненулевом, значении (узел графа находится на выделенном маршруте) прямоугольник будет зеленым, при ложном – белым. Затем, если обе переменных sBegin и sEnd равны нулю, то есть в блоке нет ни флага начала, ни флага конца маршрута, функция рисования завершается. Если же одна из этих переменных не равна нулю, устанавливается шрифт «Arial» с русским набором символов высотой в весь прямоугольник блока (draw->Height) и выводится соответствующая буква – «Н» или «К». Технически в блоке могут быть установлены оба флага, тогда обе буквы выведутся одна поверх другой, но при поиске маршрута такая ситуация вряд ли встретится (зачем искать маршрут от какого-то узла графа к нему же?) и мы не будем предпринимать по этому поводу каких-либо действий.

Теперь напишем функцию GraphNode_OnReset, которая обеспечивает реакцию нашего блока на вызов функции «ProgrammersGuide.GraphPath.Reset»:

      // Реакция блока на функцию ProgrammersGuide.GraphPath.Reset
      void GraphNode_OnReset(RDS_PBLOCKDATA BlockData,
              TProgGuideFuncResetParams *reset)
      { if(reset==NULL) return; // Нет параметров функции
        if(reset->servSize<sizeof(TProgGuideFuncResetParams))
          return; // Размер переданной структуры меньше ожидаемого

        if(reset->ResetMark)
          { // Сбрасываем выделение всех присоединенных к блоку связей
            RDS_CHANDLE c=NULL;
            for(;;) // Перебираем все связи, подключенные к блоку
              { c=rdsGetBlockLink(BlockData->Block,c,TRUE,TRUE,NULL);
                if(c==NULL) break; // Все связи перебраны
                // Снимаем выделение связи c
                UnmarkConnection(c);
              }
            // Сбрасываем флаги наличия метки и принадлежности
            // к выделенному маршруту
            sMarked=sInPath=0;
          }

        if(reset->ResetBegin) // Сбрасываем флаг начала маршрута
          sBegin=0;
        if(reset->ResetEnd) // Сбрасываем флаг конца маршрута
          sEnd=0;
      }
      //=========================================

В эту функцию передается указатель на структуру данных блока BlockData (для обеспечения работы макросов переменных) и указатель на структуру параметров функции «ProgrammersGuide.GraphPath.Reset». В этой структуре указано, какие именно переменные блока нужно сбросить.

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

Если поле ResetMark истинно, нужно сбросить метку узла графа и отменить выделение маршрута. Чтобы отменить выделение нужно, кроме изменения переменных данного блока, снять выделение со связей, подключенных к этому блоку, если они изображают дуги, попавшие на маршрут. Мы не будем разбираться, какие связи находятся на маршруте, а какие – нет, мы просто снимем выделение со всех связей, соединенных с этим блоком. Для этого переберем их все функцией rdsGetBlockLink (мы уже встречались с ней в §2.7.4) и для каждой вызовем уже написанную нами функцию UnmarkConnection. Переменным блока sMarked (флаг наличия метки) и sInPath (флаг принадлежности к маршруту) мы присваиваем значение 0.

Если в переданной структуре истинно поле ResetBegin, нужно сбросить у блока флаг начала маршрута: переменной sBegin присваивается значение 0. Если истинно поле ResetEnd, то сбрасывается флаг конца: обнуляется переменная sEnd.

Функция GraphNode_SetBlockAsBegin делает данный блок началом маршрута:

      // Сделать данный блок началом маршрута
      void GraphNode_SetBlockAsBegin(RDS_PBLOCKDATA BlockData)
      { if(sBegin) // Блок уже является началом маршрута
          return;
        // Сбрасываем старый флаг начала маршрута и разметку всего графа
        GraphPath_Reset(BlockData->Parent,TRUE,TRUE,FALSE);
        // Устанавливаем флаг начала маршрута у данного блока
        sBegin=1;
        // Ищем маршрут в графе
        GraphPath_FindPath(BlockData->Parent);
      }
      //=========================================

Прежде всего мы, на всякий случай, проверяем, не является ли уже данный блок (то есть блок, к которому относится переданный в функцию указатель на структуру данных BlockData) началом маршрута. Если это так, никаких действий предпринимать не нужно – функция немедленно завершается. Если данный блок началом маршрута не является, мы вызываем для родительской подсистемы данного блока ранее написанную нами функцию GraphPath_Reset, которая сбросит во всем графе разметку и старый флаг начала маршрута. Флаг конца маршрута в графе не сбрасывается, об этом говорит переданное в четвертом параметре функции значение FALSE. Затем мы устанавливаем флаг начала маршрута у данного блока, присвоив его переменной sBegin значение 1 (это обязательно делать после вызова функции GraphPath_Reset – если сделать это до ее вызова, она, работая со всеми узлами графа, сбросит только что установленный нами флаг и в данном блоке тоже). После этого мы пытаемся найти в графе маршрут от начала к концу, вызывая GraphPath_FindPath. Мы не проверяем, установлен ли флаг конца маршрута: если он не установлен, GraphPath_FindPath просто завершится, не выполнив никаких действий.

Функция, делающая данный блок концом маршрута, устроена аналогично:

      // Сделать данный блок концом маршрута
      void GraphNode_SetBlockAsEnd(RDS_PBLOCKDATA BlockData)
      { if(sEnd) // Блок уже является концом маршрута
          return;
        // Сбрасываем старый флаг конца маршрута и разметку всего графа
        GraphPath_Reset(BlockData->Parent,TRUE,FALSE,TRUE);
        // Устанавливаем флаг конца маршрута у данного блока
        sEnd=1;
        // Ищем маршрут в графе
        GraphPath_FindPath(BlockData->Parent);
      }
      //=========================================

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

Осталось написать две самых важных функции, которые, собственно, и реализуют описанный выше алгоритм разметки графа и прослеживание в нем кратчайшего маршрута. Начнем с функции GraphNode_OnMarkBlock, реализующей реакцию блока на вызов «ProgrammersGuide.GraphPath.Mark»: она должна пометить данный блок указанным в параметрах числом, если оно меньше текущей метки блока, а также пометить все соседние узлы графа суммой метки данного блока и расстояния до соседнего узла. Для перебора соседей блока мы будем использовать сервисную функцию RDS rdsEnumConnectedBlocks (она уже встречалась нам в §2.13.2), поэтому сначала напишем прототип функции, которая будет вызываться для каждого обнаруженного соседа (саму функцию мы напишем позже):

      // Прототип функции обратного вызова для перечисления соседей блока
      BOOL RDSCALL GraphPath_MarkBlock_Callback(
        RDS_PPOINTDESCRIPTION src,
        RDS_PPOINTDESCRIPTION dest,
        LPVOID data);

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

      // Пометить данный блок и его соседей
      void GraphNode_OnMarkBlock(RDS_PBLOCKDATA BlockData,
              TProgGuideFuncMarkParams *params)
      { if(params==NULL) return; // Нет параметров функции
        if(params->servSize<sizeof(TProgGuideFuncMarkParams))
          return; // Размер переданной структуры меньше ожидаемого

        if(sMarked!=0 && sPathMark<=params->Mark)
          return; // Блок уже помечен меньшим или таким же числом

        // Блок не помечен вообще или помечен большим числом -
        // помечаем его и его соседей новыми числами
        sMarked=1; // У блока есть метка
        sPathMark=params->Mark; // Новая метка блока

        if(sEnd) // Это – конец маршрута, дальше помечать незачем
          return;

        // Помечаем всех соседей блока, кроме params->Previous,
        // суммой метки этого блока и длины дуги к соседу
        rdsEnumConnectedBlocks(BlockData->Block,
          RDS_BEN_INPUTS|RDS_BEN_OUTPUTS,
          GraphPath_MarkBlock_Callback,params);
      }
      //=========================================

Сначала мы, как обычно, проверяем размер переданной структуры параметров. Если он нам подходит, мы сравниваем число, которым предлагается пометить этот блок, с уже имеющейся в нем меткой. Если новая метка больше старой, ничего помечать заново не нужно. Если же новая метка меньше, блок помечается новым, меньшим, значением: оно переносится в переменную блока sPathMark (при этом взводится флаг наличия метки sMarked). Если данный блок является концом маршрута, помечать его соседей не требуется – достигнув конца, нам не нужно идти дальше. В противном случае при помощи вызова rdsEnumConnectedBlocks для каждого из блоков, соединенных с данным, вызывается функция обратного вызова GraphPath_MarkBlock_Callback, в которую будут передан указатель params, а также описания двух крайних точек связи, соединяющей эти два блока (см. §2.13.2). Эта функция должна проверить допустимость направления связи (мы можем переходить от блока к блоку только по стрелкам), вычислить длину дуги и попытаться пометить соседний блок новым числом. Напишем эту функцию:

      // Функция обратного вызова для GraphNode_OnMarkBlock
      BOOL RDSCALL GraphPath_MarkBlock_Callback(
          RDS_PPOINTDESCRIPTION src,
          RDS_PPOINTDESCRIPTION dest,LPVOID data)
      { TProgGuideFuncMarkParams *src_params=
          (TProgGuideFuncMarkParams*)data;
        TProgGuideFuncMarkParams dest_params;
        double ArcLen;

        // Функция вызвана для пары блоков: src->Block – данный блок,
        // dest->Block – его сосед. Их соединяет связь dest->Owner (или
        // src->Owner – поля Owner у структур src и dest равны, поскольку
        // обе точки принадлежат одной и той же связи)

        // Сравниваем найденного соседа с блоком,
        // который не нужно помечать
        if(src_params->Previous==dest->Block)
          return TRUE;

        // Связь должна подходить ко входу блока dest->Block
        // (движение в графе возможно только по стрелкам)
        if(dest->Source) // Точка dest соединена с выходом блока
          return TRUE;

        // Является ли найденный сосед узлом графа?
        if(!rdsCallBlockFunction(dest->Block,GraphFuncGetParams,NULL))
          return TRUE; // Не является

        // Вычисляем длину дуги между блоками
        ArcLen=CalcArcLength(dest->Owner);
        if(ArcLen<0.0) // Связь разветвленная или оборванная
          return TRUE; // Такая связь не может быть дугой

        // Помечаем найденный соседний блок суммой маркировки данного
        // блока (src_params->Mark) и длины дуги к найденному (ArcLen)
        dest_params.servSize=sizeof(dest_params);
        dest_params.Mark=src_params->Mark+ArcLen;
        dest_params.Previous=src->Block; // Блок, от которого пришла метка
        rdsCallBlockFunction(dest->Block,GraphFuncMark,&dest_params);
        return TRUE;
      }
      //=========================================

В параметре data в эту функцию передается последний параметр функции rdsEnumConnectedBlocks. В функции GraphNode_OnMarkBlock мы подставили туда params – указатель на структуру параметров функции «ProgrammersGuide.GraphPath.Mark», имеющую тип TProgGuideFuncMarkParams. Поэтому в данной функции мы, прежде всего, приводим параметр data, имеющий тип void*, обратно к типу «указатель на TProgGuideFuncMarkParams» и записываем его во вспомогательную переменную src_params. Теперь мы имеем доступ к метке блока, соседей которого мы помечаем (src_params->Mark) и к идентификатору блока, от которого пришла эта метка, и который помечать бессмысленно (src_params->Previous). Мы можем сравнить идентификатор найденного соседа (dest->Block) с этим идентификатором и завершить функцию, если они совпали – этого соседа помечать не нужно. Завершая функцию обратного вызова мы всегда будем возвращать TRUE, поскольку возврат FALSE прервал бы работу rdsEnumConnectedBlocks, а нам этого не нужно.

Далее мы вызываем в найденном блоке-соседе функцию блока «ProgrammersGuide.GraphPath.GetParams», которая должна вернуть единицу, если этот блок является узлом графа. Если она вернет ноль, мы не будем работать с этим блоком, поскольку в поиске маршрута участвуют только блоки-узлы графа.

Теперь, когда мы знаем, что найденный сосед dest->Block является узлом графа, который нужно попробовать пометить, мы можем вычислить длину дуги, ведущей к нему. Для этого вызывается написанная ранее функция CalcArcLength, в которую передается идентификатор связи dest->Owner, изображающей эту дугу, и возвращенное ей значение записывается в переменную ArcLen. Функция CalcArcLength не только вычислит длину дуги, но и проверит связь на разветвленность и обрыв – мы работаем только со связями, соединяющими два блока. Если значение ArcLen отрицательно, связь не прошла эту проверку, и мы завершаем работу функции.

Далее нам нужно вызвать у найденного соседа функцию блока «ProgrammersGuide.GraphPath.Mark», передав в ее параметрах новое значение метки, полученное сложением метки данного блока и длины дуги к соседу. Мы не можем использовать для этого структуру, указатель на которую находится в src_params: там хранятся параметры, с которыми был вызван данный блок, и они нам будут нужны при обработке следующего соседа – мы не имеем права их изменять. Поэтому при вызове функции блока мы будем использовать другую структуру – dest_params. Ее поля нам сейчас предстоит заполнить.

В поле servSize мы, как обычно, записываем размер самой структуры для проверки правильности передачи параметров. В поле Mark мы записываем новое значение метки соседа: сумму src_params->Mark и длины дуги ArcLen. В поле Previous мы записываем идентификатор данного блока src->Block: когда вызванный нами сосед начнет помечать своих соседей, данный блок будет из этого процесса исключен. Теперь мы вызываем у найденного соседа функцию «ProgrammersGuide.GraphPath.Mark» (ее идентификатор хранится в глобальной переменной GraphFuncMark), передав ей в качестве параметров указатель на структуру dest_params. Сосед, реагируя на этот вызов, вызовет функцию GraphNode_OnMarkBlock, которая начнет перебирать уже его соседей, вызывая для каждого GraphPath_MarkBlock_Callback, и т.д., пока весь граф не будет размечен.

Последняя функция, которую мы должны написать – это GraphNode_OnBackTracePath, реакция на вызов функции блока «ProgrammersGuide.GraphPath.BackTrace». Эта функция должна пометить данный блок как принадлежащий к выделенному маршруту, найти среди соседей блок с наименьшей меткой, выделить связь, идущую от него, и тоже вызвать для него «ProgrammersGuide.GraphPath.BackTrace». Так, начав с конца маршрута, блок за блоком, будет помечен весь маршрут.

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

      // Проследить и выделить маршрут от данного блока в
      // обратном направлении
      void GraphNode_OnBackTracePath(RDS_PBLOCKDATA BlockData)
      { RDS_BHANDLE PrevBlock;
        RDS_CHANDLE PrevConn,c;
        double minMark;
        RDS_CONNDESCRIPTION conndescr;
        RDS_POINTDESCRIPTION pointdescr;
        TProgGuideFuncGetParams params;

        if(!sMarked) // У блока нет метки
          return;

        sInPath=1; // Помечаем данный блок как принадлежащий маршруту

        if(sBegin) // Это начало маршрута – мы выделили его весь
          return;

        // Заполняем служебные поля размера у всех структур,
        // которые нам потребуются
        conndescr.servSize=sizeof(conndescr);
        pointdescr.servSize=sizeof(pointdescr);
        params.servSize=sizeof(params);

        // Ищем среди соседей данного блока блок с наименьшей меткой
        PrevBlock=NULL;
        // Перебираем все связи блока
        c=NULL;
        for(;;)
          { RDS_BHANDLE connBlock;
            double connMark;
            int numBlocks;
            // Получаем очередную связь блока, соединенную с его входом
            c=rdsGetBlockLink(BlockData->Block,c,TRUE,FALSE,NULL);
            if(c==NULL) break; // Связи кончились
            // Получаем параметры этой связи
            rdsGetConnDescription(c,&conndescr);
            // Перебираем все точки связи c и ищем среди них соединение
            // с узлом графа на другом ее конце
            connBlock=NULL;
            numBlocks=0;
            for(int i=0;i<conndescr.NumPoints;i++)
              { // Получаем описание точки связи i
                rdsGetPointDescription(c,i,&pointdescr);
                // Проверяем тип точки
                if(pointdescr.PointType==RDS_PTBUS)
                  { // Связь, соединенная с шиной, нам не годится
                    connBlock=NULL;
                    break;
                  }
                if(pointdescr.PointType!=RDS_PTBLOCK)
                  continue;
                // Найдена точка соединения с блоком
                numBlocks++;
                if(numBlocks>2) // Разветвленная связь – не годится
                  { connBlock=NULL;
                    break;
                  }
                if(pointdescr.Block==BlockData->Block)
                  continue; // Это не сосед, а сам данный блок
                // Получаем параметры узла блока-соседа
                if(!rdsCallBlockFunction(pointdescr.Block,
                                         GraphFuncGetParams,&params))
                  { // Блок не является узлом графа
                    connBlock=NULL;
                    break;
                  }
                // Нашли соседний блок, являющийся узлом графа
                if(params.Marked) // У соседнего узла есть метка
                  { connBlock=pointdescr.Block; // Запоминаем
                    connMark=params.Mark;
                  }
              } // for(int i=0;...)
            // Цикл перебора точек связи закончен. В cоnnBlock должен
            // находиться идентификатор узла, который эта связь соединяет
            // с данным блоком, а в connMark – его метка.
            if(connBlock) // Есть узел графа на другом конце связи
              { if(PrevBlock==NULL || minMark>connMark)
                  { // Найден узел с меньшей маркировкой
                    PrevBlock=connBlock;
                    minMark=connMark;
                    PrevConn=c;
                  }
              }
          } // for(;;)

        // Перебор связей данного блока закончен. В PrevBlock должен
        // находиться идентификатор соседа с наименьшей меткой, из
        // которого можно попасть в данный блок, а в PrevConn -
        // идентификатор соединяющей их связи

       if(PrevBlock)
         { // Визуально выделяем связь
           MarkConnection(PrevConn);
           // Вызываем функцию обратного прослеживания маршрута
           // от найденного блока
           rdsCallBlockFunction(PrevBlock,GraphFuncBackTrace,NULL);
         }
      }
      //=========================================

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

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

В найденной связи c мы будем перебирать точки в поисках точек соединения с блоками, поэтому нам необходимо узнать общее число точек в связи. Для этого мы считываем параметры связи в структуру conndescr при помощи сервисной функции rdsGetConnDescription. Теперь мы можем организовать цикл по всем точкам связи, чтобы найти блок на ее конце: счетчик цикла i будет изменяться от 0 до conndescr.NumPoints-1. Идентификатор найденного блока будет записан в переменную connBlock.

В цикле мы, прежде всего, вызываем сервисную функцию rdsGetPointDescription, чтобы считать параметры i-й точки связи с в структуру pointdescr. Затем мы проверяем тип этой точки. Если это точка соединения связи с шиной (RDS_PTBUS), эта связь не может быть дугой графа, даже если где-то, на одном из ее концов, будет находиться блок. В этом случае мы обнуляем переменную connBlock и прекращаем перебор точек связи оператором break. Если это внутренняя точка связи, мы пропускаем ее и переходим к следующей. Если же это точка соединения с блоком, мы увеличиваем счетчик найденных блоков numBlocks. Если его значение превысит 2, связь разветвлена, и она не может быть дугой графа – мы прекращаем перебор точек. В противном случае мы сравниваем идентификатор соединенного блока с идентификатором данного, и пропускаем эту точку, если они совпали – нас интересует блок на другом конце этой связи. Если идентификаторы не совпали, мы вызываем у найденного блока функцию «ProgrammersGuide.GraphPath.GetParams» (ее идентификатор хранится в глобальной переменной GraphFuncGetParams), которая заполнит структуру params типа TProgGuideFuncGetParams параметрами узла графа, если, конечно, найденный блок им является. Если блок не является узлом графа (функция вернула 0), мы обнуляем connBlock и прекращаем перебор точек – нам нужны только связи, идущие от узлов графа. В противном случае мы запоминаем его идентификатор в переменной connBlock, а его метку – в connMark.

После завершения цикла по всем точкам связи в переменной connBlock должен находится идентификатор найденного на противоположном конце связи узла графа или NULL, если такого узла нет, а в переменной connMark – значение метки этого узла. Теперь мы можем сравнить найденное значение метки с найденным на данный момент минимальным значением minMark и, если найденное значение окажется меньше, заменить значение в minMark на connMark и запомнить идентификатор найденного блока узла в переменной PrevBlock, а идентификатор идущей от него связи – в PrevConn.

Таким образом, после завершения перебора всех связей в переменной PrevBlock должен оказаться идентификатор блока-соседа с наименьшей меткой, то есть идентификатор предыдущего блока кратчайшего маршрута, а в PrevConn – связь, соединяющая этот блок с данным. Нам остается только визуально выделить эту связь, вызвав для нее ранее написанную функцию MarkConnection, и вызвать для найденного блока функцию «ProgrammersGuide.GraphPath.BackTrace», реагируя на которую он выделит себя, найдет среди своих соседей блок с минимальной меткой, вызовет для него «ProgrammersGuide.GraphPath.BackTrace» и т.д., пока не будет достигнут начальный блок маршрута.

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

      // Отмена макроопределений переменных блока
      #undef sPathMark
      #undef sMarked
      #undef sInPath
      #undef sEnd
      #undef sBegin
      #undef Ready
      #undef Start
      #undef pStart

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

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

Теперь пользователь нажимает правую кнопку мыши на другом блоке и выбирает в контекстном меню пункт «Конец маршрута». Модель блока вызывает функцию GraphNode_SetBlockAsEnd, которая сбрасывает в графе метки и флаги конца маршрута (опять ничего не изменится – взведен только флаг начала маршрута, а его функция не трогает), взводит в своем блоке флаг конца маршрута sEnd и опять вызывает GraphPath_FindPath. Теперь в графе уже установлены оба флага, поэтому функция GraphPath_FindPath вызовет «ProgrammersGuide.GraphPath.Mark» в блоке начала маршрута, передав ему метку 0. Это приведет к цепной реакции: блок примет метку и пометит своих соседей расстояниями до себя, те, в свою очередь, пометят своих соседей, и так будет продолжаться до тех пор, пока все достижимые из начального блока узла графа не будут помечены кратчайшим расстоянием до начала маршрута. После этого GraphPath_FindPath вызовет «ProgrammersGuide.GraphPath.BackTrace» у блока конца маршрута. Это тоже приведет к цепочке вызовов функций: блок конца маршрута выделится, взведя у себя флаг sInPath, просмотрев метки своих соседей, найдет среди них ближайшего к началу и передаст управление ему, тот – своему соседу и т.д. пока не будет достигнут блок начала маршрута. В результате все блоки на кратчайшем маршруте (а, заодно, и связи, их соединяющие) окажутся выделенными.

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

Рис. 89. Кратчайший путь в графе

Конечно, этот пример весьма несовершенен, и его можно существенно улучшить. Можно, например, при разметке графа запоминать в личной области данных каждого блока идентификатор узла графа, от которого пришло значение метки (при вызове функции «ProgrammersGuide.GraphPath.Mark» он передается в поле Previous структуры параметров) и связь, соединяющую с ним, тогда при прослеживании маршрута функцией «ProgrammersGuide.GraphPath.BackTrace» не придется перебирать все соседние блоки в поисках узла с наименьшей меткой. Можно вместо используемого упрощенного алгоритма разметки графа реализовать полноценный алгоритм Дейкстры. Но, в любом случае, для взаимодействия узлов между собой нужно будет использовать вызов функций блоков.