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

Описания RDS

Описание пользователя

Глава 3. Использование стандартных модулей автокомпиляции

§3.6. Принципы создания автокомпилируемых моделей блоков

§3.6.5. Блоки, программно рисующие свое изображение

# Описание пользователя

## Глава 3. Использование стандартных модулей автокомпиляции

### §3.6. Принципы создания автокомпилируемых моделей блоков

#### §3.6.5. Блоки, программно рисующие свое изображение

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

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

Для того, чтобы модель блока программно рисовала его изображение в подсистеме, должны быть выполнены два условия: в окне параметров блока на вкладке «внешний вид» должен быть включен флажок «внешний вид – определяется функцией DLL» (рис. 415), а в модели должна быть введена реакция на событие «рисование блока». Эта реакция находится на вкладке «события» левой панели окна редактора модели (см. рис. 339) в группе «внешний вид блока», и ей соответствует константа RDS_BFM_DRAW. Если разрешить программное рисование флажком в окне параметров, но не ввести соответствующую реакцию модели, блок станет невидимым: RDS будет вызывать его модель для рисования блока в окне подсистемы, а модель будет игнорировать этот вызов – в результате, ничего не будет нарисовано. Если ввести реакцию модели на рисование, но в окне параметров блока вместо флажка «внешний вид – определяется функцией DLL» оставить включенным «определяется картинкой» или «прямоугольник с текстом», RDS не будет вызывать модель для программного рисования, несмотря на то, что в ней есть соответствующая реакция, и блок будет выглядеть согласно установленным флажкам.

Рис. 415. Флажок, разрешающий программное рисование в окне параметров блока

Как правило, модели блоков, которые рисуют себя программно, пишут таким образом, чтобы изображение подстраивалось под размер блока, заданный пользователем. В этом случае вместе с флажком «внешний вид – определяется функцией DLL» на вкладке «внешний вид» окна параметров имеет смысл включить и флажок «разрешить масштабирование», чтобы пользователь мог задать размеры блока, растягивая мышью маркеры его выделения. Можно также, при желании, нажать кнопку «размер для функции DLL» и ввести точный размер блока в точках экрана для масштаба 100%.

В реакции модели блока на рисование можно пользоваться всеми графическими функциями Windows API, а также специальными графическими функциями RDS (все эти функции начинаются с символов «rdsXG»). Графические функции RDS позволяют проще задавать цвет заполнения, толщину линии, шрифт и другие параметры рисования, в остальном же они почти полностью повторяют функции Windows API. Разработчик модели выбирает, какими из них пользоваться, исходя из собственных предпочтений (допускается смешивать в одной модели вызовы и тех, и других, хотя это может запутать не очень опытного программиста). В функцию рисования, создаваемую для реакции, передается один параметр с именем DrawData – это указатель на стандартную структуру RDS RDS_DRAWDATA, в которой содержатся параметры, необходимые для выполнения рисования. Подробно эта структура рассматривается в разделе А.2.6.3 приложений, здесь же мы кратко перечислим ее поля и их назначение.

dc(HDC)
  Контекст устройства Windows (device context, HDC), на котором модель рисует изображение. Этот параметр используется только в графических функциях Windows API, в графических функциях RDS его указывать не нужно.
CalcMode(BOOL)
  TRUE, если RDS находится в режимах моделирования или расчета, и FALSE, если RDS находится в режиме редактирования (в режиме редактирования сложные блоки часто рисуют в наиболее общей их форме и со всеми включенными дополнительными элементами, если эти элементы могут исчезать в процессе расчета).
BlockX, BlockY(int)
  Координаты верхнего левого угла изображения блока в текущем масштабе окна его подсистемы. В этих координатах уже учтена возможная связь положения блока с его переменными, которая может быть задана в окне параметров на панели «координаты точки привязки».
DoubleZoom(double)
  Текущий масштаб окна родительской подсистемы блока в долях единицы: 1 – 100%, 0.5 – 50%, 2 – 200% и т.п. Если какие-то внутренние элементы программно рисуемого изображения блока (например, шрифт надписи) должны масштабироваться вместе с ним, при рисовании размеры этих элементов необходимо умножать на значение этого поля.
RectValid(BOOL)
  Признак изменения размеров прямоугольника блока. Если в результате рисования модель изменяет размер блока (например, если его высота или ширина отражают его внутреннее состояние), в это поле необходимо записать значение TRUE и занести в следующие четыре целых поля (Left, Top, Width и Height) новые координаты и размеры блока. В автокомпилируемых блоках эта возможность используется крайне редко.
Left, Top, Width, Height(int)
  Координаты левого верхнего угла (Left, Top) прямоугольной области, занимаемой блоком, ее ширина (Width) и высота (Height) в текущем масштабе с учетом возможной связи положения блока с его переменными. Эти значения можно непосредственно использовать в функциях рисования. В эти же поля модель может записать новые координаты и размеры блока, если, по какой-либо причине, она решит изменить их при рисовании (при этом также необходимо записать TRUE в поле RectValid).
VisibleRect(RECT*)
  Указатель на структуру RECT Windows API, в которой записаны координаты видимой в данный момент в окне части рабочего поля подсистемы. Модель может использовать их для того, чтобы уменьшить потери времени на обновление окна, не рисуя части блока, не попавшие в видимую в данный момент область. Делать это не обязательно – рисование за пределами видимой в окне области отсекается автоматически.
FullDraw(BOOL)
  Логическое поле, указывающее на необходимость полной перерисовки всего изображения блока (TRUE) или только тех его частей, которые изменились с момента последнего рисования (FALSE). Его использование позволяет снизить потери времени на обновление окна, не рисуя не изменившиеся части изображения блока без необходимости. В автокомпилируемых блоках эта возможность используется крайне редко, подробно она рассматривается в §2.10.2 руководства программиста.

Поскольку параметр функции реакции DrawData – это указатель на данную структуру, все обращения к полям структуры нужно предварять «DrawData->». Например, чтобы узнать масштаб окна подсистемы рисуемого блока, необходимо записать «DrawData->DoubleZoom».

Создадим модель простейшего блока-индикатора – индикатора уровня. Блок будет выглядеть как прямоугольник, разделенный по вертикали на две части: верхняя часть будет белой, нижняя – синей, причем высота синей части будет пропорциональна значению входа блока: при нуле на входе весь блок будет белым, при значении входа 100 весь блок будет синим, при значении 50 граница раздела будет находиться точно в середине блока. Таким образом, блок будет рисовать синий вертикальный столбик, высота которого в процентах относительно полной высоты блока равна значению входа (подобный пример рассматривается в §2.10.1 руководства программиста). Для того, чтобы блок лучше выглядел, будем рисовать вокруг него черную рамку, причем в масштабе 100% и меньше рамка будет иметь толщину в одну точку экрана, а в крупных масштабах ее толщина будет увеличиваться согласно выбранному масштабу (если этого не сделать, на печати в высоком разрешении рамка в одну точку будет практически не заметна).

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

Имя | Тип | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | ---
Start | Сигнал | Вход | ✓ | 0
Ready | Сигнал | Выход |  | 0
x | double | Вход |  | 0

Флаг «пуск» у входа «x» мы не устанавливаем, поскольку нам не нужно запускать модель при изменении этого входа. Модель нашего блока будет вызываться при обновлении окна его подсистемы, в этот момент она и будет считывать текущее значение входа.

Рис. 416. Реакция на рисование
блока в списке событий

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

Рисовать индикатор мы будем при помощи графических функций RDS (они несколько проще, чем стандартные функции Windows API) в следующей последовательности:

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

Выполняя эти действия, нам нужно будет проверять значение входа x на попадание в диапазон [0…100]: если x меньше нуля, весь блок будет белым, если больше ста – синим. Мы не будем разрешать границе раздела синей и белой частей выходить за пределы заданного в подсистеме размера блока.

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

Для выполнения указанных выше действий на вкладке «рисование» редактора модели нужно ввести следующий текст:

      // Вспомогательные переменные
      int right,bottom,h;

      // Правый нижний угол области блока
      right=DrawData->Left+DrawData->Width;
      bottom=DrawData->Top+DrawData->Height;

      // Проверка наличия ошибки на входе
      if(x==rdsbcppHugeDouble) // Заливаем красным
        { // Красный фон
          rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xff);
          // Черная линия
          rdsXGSetPenStyle(0,PS_SOLID,
                           DrawData->DoubleZoom,0,R2_COPYPEN);
          // Прямоугольник - черная рамка и красный фон
          rdsXGRectangle(DrawData->Left,DrawData->Top,right,bottom);
          // Завершаем реакцию, больше ничего не рисуем
          return;
        }

      // Высота синего столбика в точках экрана
      h=x*DrawData->Height/100.0;
      // Ограничение
      if(h<0)
        h=0;
      else if(h>DrawData->Height)
        h=DrawData->Height;

      // Отключаем рамку
      rdsXGSetPenStyle(RDS_GFSTYLE,PS_NULL,0,0,0);

      // Рисуем верхнюю (белую) часть
      if(h!=DrawData->Height) // Есть белое
        { // Белый фон
          rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xffffff);
          // Верхняя часть - на h меньше высоты блока
          rdsXGRectangle(DrawData->Left,
                         DrawData->Top,
                         right,
                         bottom-h+1);
        }
      // Рисуем нижнюю (синюю) часть
      if(h!=0)
        { // Синий фон
          rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xff0000);
          // Нижняя часть - от h до высоты блока
          rdsXGRectangle(DrawData->Left,
                         bottom-h,
                         right,
                         bottom);
        }

      // Рисуем рамку
      // Черная линия, толщина - с учетом масштаба
      rdsXGSetPenStyle(0,PS_SOLID,DrawData->DoubleZoom,0,R2_COPYPEN);
      // Отключаем заливку
      rdsXGSetBrushStyle(RDS_GFSTYLE,RDS_GFS_EMPTY,0);
      // Прямоугольник рамки
      rdsXGRectangle(DrawData->Left,DrawData->Top,right,bottom);

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

Сначала мы вычисляем координаты правого нижнего угла прямоугольной области, занимаемой блоком, и записываем их во вспомогательные целые переменные right (горизонтальная координата) и bottom (вертикальная координата). Координаты левого верхнего угла этой области у нас уже есть: они записаны RDS в поля Left и Top структуры, указатель на которую передан в нашу функцию реакции в параметре DrawData. В полях Width и Height этой структуры записаны ширина и высота области соответственно, поэтому для вычисления координаты правой границы области (right) нужно сложить DrawData->Left и DrawData->Width, а для вычисления координаты нижней границы (bottom) – DrawData->Top и DrawData->Height. Следует помнить, что рисование ведется в оконных координатах Windows, поэтому горизонтальная ось всегда направлена слева направо, а вертикальная – сверху вниз.

Далее мы сравниваем вход блока x с признаком ошибки rdsbcppHugeDouble. Если они равны, мы должны нарисовать красный прямоугольник. Для этого сначала мы устанавливаем красный цвет заливки вызовом

      rdsXGSetBrushStyle(0,              // Маска параметров
                         RDS_GFS_SOLID,  // Стиль
                         0xff);          // Цвет

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

Затем мы устанавливаем параметры линии рамки геометрических фигур вызовом

      rdsXGSetPenStyle(0,                    // Маска параметров
                       PS_SOLID,             // Стиль
                       DrawData->DoubleZoom, // Толщина
                       0,                    // Цвет
                       R2_COPYPEN);          // Режим

В первом параметре опять передается маска устанавливаемых параметров (0 – все параметры устанавливаются одновременно). Второй параметр задает стиль линии (PS_SOLID – сплошная линия), третий – ее толщину (в данном случае передается DrawData->DoubleZoom, то есть текущий масштаб подсистемы), в четвертом – цвет линии (переданный ноль означает черный цвет – все три компоненты цвета имеют нулевую интенсивность). Наконец, в пятом параметре передается режим рисования, или «растровая операция». Этот режим задает способ смешения цвета фона и цвета линии, переданная константа R2_COPYPEN указывает на то, что цвет фона должен просто заменяться на заданный цвет линии. После вызова rdsXGSetPenStyle с этими параметрами все рисуемые геометрические фигуры будут иметь видимую сплошную рамку черного цвета, толщина которой будет равна текущему масштабу подсистемы в долях единицы, то есть одной точке при масштабе 100%, двум точкам при масштабе 200% и т.д.

Может возникнуть вопрос: что будет с толщиной линии рамки при масштабах, меньших 100% – например, при 25%? В качестве толщины мы передаем масштаб подсистемы в долях единицы DrawData->DoubleZoom, для 25% это значение будет равно 0.25. Третий параметр функции rdsXGSetPenStyle имеет тип int, то есть 0.25 будет округлено до нуля и передано в функцию – получается, что мы устанавливаем толщину линии в ноль точек. В RDS ноль в качестве толщины линии указывает на минимально возможную толщину, то есть толщину в одну точку экрана. Таким образом, в крупных масштабах толщина рамки нашего прямоугольника будет увеличиваться вместе с масштабом, а в мелких (при значении DoubleZoom, меньшем единицы) – оставаться равной одной точке, как мы и хотели.

После того, как параметры заливки и рамки установлены, вызов функции rdsXGRectangle рисует прямоугольник с этими параметрами:

      rdsXGRectangle(DrawData->Left,  // Левая граница
                     DrawData->Top,   // Верхняя граница
                     right,           // Правая граница
                     bottom);         // Нижняя граница

В параметрах этой функции передаются координаты левого верхнего (DrawData->Left и DrawData->Top) и правого нижнего (right, bottom) углов рисуемого прямоугольника. Поскольку это рисование выполняется при ошибке на входе, после вызова rdsXGRectangle мы немедленно завершаем реакцию оператором return.

Если же на входе не было ошибки, мы вычисляем высоту синей части блока как x% от полной высоты блока и записываем ее во вспомогательную переменную h:

      h=x*DrawData->Height/100.0;

Чтобы ограничить рисование границами области блока, вместо того, чтобы проверять попадание x в диапазон [0…100], мы проверяем значение h. Если значение x отрицательно, значение h тоже будет отрицательным – в этом случае мы принудительно присваиваем h ноль, что не даст синей части блока выйти вниз за границы его области. Если значение x больше ста, значение h будет больше высоты блока DrawData->Height – в этом случае мы принудительно присваиваем h значение высоты блока, что не даст синей части выйти за границу области блока вверх.

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

      rdsXGSetPenStyle(RDS_GFSTYLE,PS_NULL,0,0,0);

В первом параметре функции передается битовый флаг RDS_GFSTYLE, указывающий на то, что мы меняем только стиль линии. Во втором параметре передается константа стиля PS_NULL, задающая невидимую линию. Остальные параметры функции – нулевые: из-за того, что в первом параметре передан единственный флаг установки стиля, они все равно будут проигнорированы RDS, и в них можно указать любые значения.

Если высота синей части h не равна полной высоте блока DrawData->Height, в верхней части области блока необходимо нарисовать белый прямоугольник. Белый цвет заливки мы устанавливаем вызовом

      rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xffffff);

в котором шестнадцатеричное число 0xffffff (десятичное 16777215 = 255×65536 + 255×256 + 255) задает белый цвет: все три байта этого числа равны 255, то есть красная, зеленая и синяя компоненты цвета имеют максимально возможные интенсивности). Затем мы рисуем прямоугольник, верхняя граница которого совпадает с верхней границей всей области блока, а нижняя отстоит от нижней границы блока на h:

      rdsXGRectangle(DrawData->Left,  // Левая граница блока
                     DrawData->Top,   // Верх блока
                     right,           // Правая граница блока
                     bottom-h+1);     // На h выше нижней границы

Прямоугольник будет нарисован без рамки, поскольку рамку мы отключили предыдущим вызовом rdsXGSetPenStyle. В последнем параметре функции передается «bottom-h+1», а не «bottom-h», чтобы не было зазора между верхней и нижней частями прямоугольника. Иногда по результатам рисования приходится увеличивать или уменьшать координаты рисуемых элементов на одну точку, чтобы точно подогнать их друг к другу – это как раз такой случай. Если убрать из параметра «+1», можно будет видеть, что между белым прямоугольником и синим, который мы нарисуем позже, окажется полоска цвета фона окна подсистемы высотой в одну точку экрана.

Если высота синей части h не равна нулю, мы точно так же рисуем в нижней части блока синий прямоугольник. Для этого мы устанавливаем синий цвет заливки вызовом

      rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xff0000);

где шестнадцатеричное число 0xff0000 (десятичное 16711680 = 255×65536) задает синий цвет: третий байт числа, указывающий синюю компоненту цвета, равен 255, остальные – нулевые, то есть красная и зеленая компоненты имеют нулевую интенсивность. Затем рисуется нижний прямоугольник блока:

      rdsXGRectangle(DrawData->Left,  // Левая граница блока
                     bottom-h,        // На h выше нижней границы
                     right,           // Правая граница блока
                     bottom);         // Нижняя граница

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

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

      rdsXGSetPenStyle(0,                     // Маска параметров
                       PS_SOLID,              // Стиль
                       DrawData->DoubleZoom,  // Толщина
                       0,                     // Цвет
                       R2_COPYPEN);           // Режим

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

      rdsXGSetBrushStyle(RDS_GFSTYLE,RDS_GFS_EMPTY,0);

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

      rdsXGRectangle(DrawData->Left,  // Левая граница блока
                     DrawData->Top,   // Верхняя граница блока
                     right,           // Правая граница блока
                     bottom);         // Нижняя граница блока

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

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

Рис. 417. Включение программного рисования в окне параметров блока

Рис. 418. Тестирование
индикатора уровня

После того, как окно будет закрыто кнопкой «OK», за рисование нашего блока будет отвечать его собственная модель. Чтобы проверить ее, можно собрать схему, изображенную на рис. 418. В ней блок-индикатор вытянут мышью по вертикали, а к его входу «x» подключено поле ввода. Если запустить расчет и вводить в это поле значения от 0 до 100, высота закрашенной части индикатора будет изменяться. При вводе в поле отрицательных значений весь индикатор будет белым, при вводе значений, больших 100 – синим.

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

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

- начало шкалы – вещественный вход «Min»;
- конец шкалы – вещественный вход «Max»;
- шаг чисел шкалы – вещественный вход «Step»;
- количество знаков в дробной части чисел шкалы – целый вход «Dec»;
- число дополнительных делений (мелких рисок) между числами шкалы – целый вход «Sub»;
- индицируемое значение (положение стрелки) – вещественный вход «x».

Рис. 419. Предполагаемый вид и параметры стрелочного индикатора

На рисунке индикатор изображен для значений Min=0, Max=6, Step=1, Dec=0, Sub=10 и x=3.28.

Кроме указанных выше, для рисования индикатора нам потребуются дополнительные параметры, задающие размер различных его элементов: рисок шкалы, стрелки, чисел и т.п. (все эти параметры изображены на рис. 419). Вместо того, чтобы делать их входами блока, мы просто опишем их как локальные переменные в модели. Это приведет к тому, что все индикаторы с этой моделью будут иметь одинаковый размер рисок и чисел шкалы. При желании, можно сделать эти параметры настраиваемыми пользователем (см. §3.6.6), но для упрощения примера мы не будем этого делать.

Внешний вид нашего индикатора будет определяться следующими дополнительными параметрами (все угловые величины будем задавать в радианах, все линейные – в точках экрана для масштаба 100%):

- StartAngle – угол между вертикалью и началом шкалы;
- EndAngle – угол между вертикалью и концом шкалы;
- ExcessAngle – дополнительный угол, на который мы разрешим стрелке отклоняться наружу от диапазона шкалы (это позволит пользователю увидеть, что значение на входе блока вышло за отображаемый диапазон);
- FontHeight – высота шрифта чисел шкалы;
- BigTick – размер больших рисок шкалы, около которых будут выводиться числа;
- SmallTick – размер маленьких рисок шкалы, располагающихся между большими;
- D_Tick – расстояние между внешним краем прибора и рисками;
- D_Num – расстояние между внутренним концом большой риски и числом;
- D_Arr – расстояние между внешним краем прибора и стрелкой;
- CenterDisk – диаметр центрального круга прибора.

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

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

Имя | Тип | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | ---
Start | Сигнал | Вход | ✓ | 0
Ready | Сигнал | Выход |  | 0
Min | double | Вход |  | 0
Max | double | Вход |  | 100
Step | double | Вход |  | 20
Dec | int | Вход |  | 0
Sub | int | Вход |  | 10
x | double | Вход |  | 0

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

      //--------- Параметры рисования ---------
      // Углы начала и конца шкалы от направления "вверх"
      double StartAngle=0.66*M_PI,   // Начало шкалы (+120гр)
             EndAngle=-0.66*M_PI;    // Конец шкалы (-120гр)
      double ExcessAngle=0.055*M_PI; // По краям шкалы (10гр)
      // Размеры цифр и рисок в точках экрана для масштаба 100%
      int FontHeight=17; // Высота шрифта
      int BigTick=10;    // Размер большой риски
      int SmallTick=4;   // Размер маленькой риски
      int D_Tick=5;      // Зазор между внешней границей и большой риской
      int D_Num=2;       // Зазор между числом и большой риской
      int D_Arr=2;       // Зазор между внешней границей и стрелкой
      int CenterDisk=10; // Диаметр центрального диска
      //---------------------------------------

      // Макрос для расчета угла от направления "вверх" по значению входа
      #define V_TO_A(v) \
        ( ((v)-Min)*(EndAngle-StartAngle)/(Max-Min)+StartAngle )

      // Макрос для расчета координат точки по радиусу и углу
      #define CALCPOINT(r,a,x,y) \
        x=CenterX-(r)*sin(a); \
        y=CenterY-(r)*cos(a);

      // Вспомогательные переменные
      int CenterX,CenterY,left,top,right,bottom,R,r1,r2,
          ix1,iy1,ix2,iy2,rf,w,rs,ra;
      double a,v,e_min,e_max;
      char *str;
      BOOL x_error,scale_error;

      // Допустимы ли значения шкалы?
      scale_error=(Min==rdsbcppHugeDouble ||
                   Max==rdsbcppHugeDouble ||
                   Step==rdsbcppHugeDouble ||
                   Max<=Min || Step<=0.0);
      // Допустимо ли значение на входе?
      x_error=(x==rdsbcppHugeDouble);

      // Центр изображения блока
      CenterX=DrawData->Left+DrawData->Width/2;
      CenterY=DrawData->Top+DrawData->Height/2;

      // Выделяем центральный квадрат блока
      if(DrawData->Width>DrawData->Height)
        { // Блок вытянут по ширине
          R=DrawData->Height/2;
          left=CenterX-R;
          right=CenterX+R;
          top=DrawData->Top;
          bottom=DrawData->Top+DrawData->Height;
        }
      else
        { // Блок вытянут по высоте
          R=DrawData->Width/2;
          left=DrawData->Left;
          right=DrawData->Left+DrawData->Width;
          top=CenterY-R;
          bottom=CenterY+R;
        }
      // R - радиус большого круга

      // Рисуем белый (красный при ошибке) круг с черной рамкой
      rdsXGSetPenStyle(0,PS_SOLID,DrawData->DoubleZoom,0,R2_COPYPEN);
      rdsXGSetBrushStyle(0,RDS_GFS_SOLID,
        (scale_error||x_error)?0xff:0xffffff);
      rdsXGEllipse(left,top,right,bottom);

      if(scale_error) // Без шкалы ничего больше не нарисовать
        return;

      // Расширенный диапазон (на ExcessAngle больше шкалы)
      a=ExcessAngle*(Max-Min)/fabs(EndAngle-StartAngle);
      e_min=Min-a;
      e_max=Max+a;

      // Отключаем заливку
      rdsXGSetBrushStyle(RDS_GFSTYLE,RDS_GFS_EMPTY,0);

      // Вычисляем вспомогательные радиусы
      r1=R-D_Tick*DrawData->DoubleZoom;     // Внешний радиус большой риски
      r2=r1-BigTick*DrawData->DoubleZoom;   // Внутр. радиус большой риски
      rs=r1-SmallTick*DrawData->DoubleZoom; // Внутренний радиус маленькой риски
      rf=r2-D_Num*DrawData->DoubleZoom;     // Внешний радиус чисел
      ra=R-D_Arr*DrawData->DoubleZoom;      // Радиус стрелки

      // Рисуем шкалу в цикле
      for(v=Min;v<=Max+Step*0.001;v+=Step)
        { // Угол, соответствующий v
          a=V_TO_A(v);
          // Расчет точек большой риски
          CALCPOINT(r1,a,ix1,iy1)
          CALCPOINT(r2,a,ix2,iy2)
          // Рисование большой риски
          rdsXGMoveTo(ix1,iy1);
          rdsXGLineTo(ix2,iy2);
          // Выводимое число (строку нужно потом освободить rdsFree)
          str=rdsDtoA(v,Dec,NULL);
          // Не повернутый шрифт
          rdsXGSetFont(RDS_GFFONTALLHEIGHT,"Arial",
                       FontHeight*DrawData->DoubleZoom,
                       0,DEFAULT_CHARSET,
                       0,
                       FALSE,FALSE,FALSE,FALSE);
          // Ширина числа
          rdsXGGetTextSize(str,&w,NULL);
          // Повернутый шрифт
          rdsXGSetFont(RDS_GFFONTALLHEIGHT,"Arial",
                       FontHeight*DrawData->DoubleZoom,
                       0,DEFAULT_CHARSET,
                       a*180.0/M_PI,
                       FALSE,FALSE,FALSE,FALSE);
          // Координаты центра верхней линии шрифта
          CALCPOINT(rf,a,ix1,iy1)
          // Вычисление левого верхнего угла текста числа
          ix2=ix1-0.5*w*cos(a);
          iy2=iy1+0.5*w*sin(a);
          // Вывод числа
          rdsXGTextOut(ix2,iy2,str);
          // Освобождение памяти, отведенной в rdsDtoA
          rdsFree(str);
          // Рисование маленьких рисок
          for(int i=1;i<Sub;i++)
            { a=v+i*Step/Sub; // Значение риски
              if(a>Max) break;
              a=V_TO_A(a); // Угол риски
              CALCPOINT(r1,a,ix1,iy1)
              CALCPOINT(rs,a,ix2,iy2)
              rdsXGMoveTo(ix1,iy1);
              rdsXGLineTo(ix2,iy2);
            } // for(int i=1;...)
        } // for(v=Min;...)

      // Рисуем стрелку
      if(!x_error)
        { // Устанавливаем красный цвет линии
          rdsXGSetPenStyle(0,PS_SOLID,
                           DrawData->DoubleZoom,0xff,R2_COPYPEN);
          // Ограничиваем по расширенному диапазону шкалы
          if(x<e_min)
            v=e_min;
          else if(x>e_max)
            v=e_max;
          else
            v=x;
          // Вычисляем координаты конца стрелки
          CALCPOINT(ra,V_TO_A(v),ix1,iy1)
          // Рисуем линию стрелки
          rdsXGMoveTo(CenterX,CenterY);
          rdsXGLineTo(ix1,iy1);
        }

      // Рисуем центральный диск
      w=0.5*CenterDisk*DrawData->DoubleZoom;
      rdsXGSetPenStyle(0,PS_SOLID,DrawData->DoubleZoom,0,R2_COPYPEN);
      rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xffffff);
      rdsXGEllipse(CenterX-w,CenterY-w,
                   CenterX+w,CenterY+w);

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

В самом начале модели описаны переменные, соответствующие параметрам внешнего вида индикатора, изображенным на рис. 419. Переменным StartAngle и EndAngle мы присваиваем значения 0.66π и –0.66π радиан соответственно – шкала нашего прибора будет начинаться в точке, отстоящей от вертикали на 120° против часовой стрелки, а заканчиваться – в точке, отстоящей от вертикали на 120° по часовой стрелке (значение StartAngle положительно, а EndAngle – отрицательно, потому что положительные углы во всех математических функциях откладываются против часовой стрелки). Переменной ExcessAngle присвоено значение 0.055π радиан: разрешенный выход стрелки за шкалу будет составлять примерно 10°. Далее задаются размеры в точках экрана (в масштабе подсистемы 100%) для всех элементов изображения.

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

Макрос V_TO_A(v) с единственным параметром v вычисляет угол относительно вертикали, который на шкале прибора соответствует отображаемому значению v. В макросе записано обычное линейное преобразование диапазона [Min…Max] в диапазон [StartAngle…EndAngle]. Макрос CALCPOINT(r,a,x,y) записывает в параметры x и y горизонтальную и вертикальную координаты точки, находящейся под углом a к вертикали на окружности радиуса r, центр которой совпадает с центром прибора (в макросе используются координаты центра прибора CenterX и CenterY, которые будут вычислены позже). С помощью этого макроса будут вычисляться и координаты концов всех рисок, и координаты конца стрелки. Сразу за макросами описывается некоторое количество вспомогательных переменных, которые будут использованы в программе.

Первое действие, которое мы делаем в программе модели, это проверка допустимости значений на входах блока. Если хотя бы один из входов Max, Min или Step, описывающих шкалу, будет равен признаку ошибки rdsbcppHugeDouble, или конец шкалы Max будет меньше или равен началу шкалы Min, или шаг шкалы Step будет равен нулю или отрицателен, логической (BOOL) переменной scale_error будет присвоено значение TRUE. Эту переменную мы будем использовать как признак ошибки шкалы – при такой ошибке мы не можем ни нарисовать шкалу на приборе, ни, очевидно, отобразить на нем значение входа. Если вход блока x равен признаку ошибки, значение TRUE получит переменная x_error – при такой ошибке мы можем нарисовать шкалу, но не сможем нарисовать стрелку.

Записав признаки ошибок в логические переменные (они потребуются нам позже, при рисовании), мы вычисляем CenterX и CenterY – координаты центра прямоугольной области, занимаемой блоком, которая будет также центром нашего круглого прибора. Для этого мы используем уже знакомые по прошлому примеру поля Left, Top, Width и Height из структуры описания события рисования RDS_DRAWDATA, указатель DrawData на которую передается в нашу функцию реакции. Горизонтальная координата центра – это левая граница плюс половина ширины блока, а вертикальная – верхняя граница плюс половина его высоты. Напомним, что в структуре RDS_DRAWDATA все поля заполнены уже с учетом текущего масштаба подсистемы, поэтому здесь нам не нужно умножать координаты на масштабный коэффициент (поле DoubleZoom той же структуры) самостоятельно.

Определив координаты центра прибора, мы вычисляем радиус R его круга и координаты left, top, right и bottom квадрата в центральной части блока, в который будет вписан этот круг. Для этого мы сравниваем ширину блока DrawData->Width с его высотой DrawData->Height. Если ширина блока больше высоты (то есть блок вытянут по горизонтали), верхняя граница top и нижняя граница bottom его центрального квадрата будут совпадать с границами всего блока, радиус круга будет равен половине высоты, а левая left и правая right границы квадрата будут отстоять от центра на R. Если же высота блока больше ширины (блок вытянут по вертикали), left и right будут совпадать с левой и правой границами блока, радиус круга R будет равен половине ширины, а top и bottom будут на R отстоять от центра прямоугольника.

Теперь мы можем нарисовать большой круг прибора, поверх которого будут выводиться шкала и стрелка. Сделаем этот круг красным, если на входах блока есть какие-либо ошибки (то есть либо x_error, либо scale_error имеют значение TRUE), а его рамку сделаем черной и сплошной, и будем, как и у рассмотренного ранее индикатора уровня, увеличивать ее толщину вместе с масштабом. Сначала уже рассматривавшейся ранее графической функцией RDS rdsXGSetPenStyle мы устанавливаем параметры рамки (в качестве толщины рамки передается текущий масштабный множитель подсистемы DrawData->DoubleZoom), затем функцией rdsXGSetBrushStyle устанавливаются параметры заливки (в качестве цвета заливки передается результат выполнения условного оператора «?:» проверяющего признаки ошибок, и возвращающего либо красный цвет 0xff, либо белый 0xffffff). После этого вызывается функция рисования эллипса rdsXGEllipse, очень похожая на рассматривавшуюся ранее функцию рисования прямоугольника rdsXGRectangle:

      rdsXGEllipse(left,    // Левая граница
                   top,     // Верхняя граница
                   right,   // Правая граница
                   bottom); // Нижняя граница

Поскольку left, top, right и bottom – границы центрального квадрата блока, эта функция нарисует эллипс с осями одинаковой длины, то есть круг.

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

Если же параметры шкалы прошли все проверки, мы продолжаем выполнение модели и вычисляем расширенный диапазон шкалы с учетом добавочного разрешенного угла отклонения стрелки ExcessAngle. Сначала в переменную a записывается интервал значений, соответствующий на шкале углу ExcessAngle (это линейное преобразование диапазона [StartAngle…EndAngle] в диапазон [Min…Max], обратное выполняемому макросом V_TO_A, но без знака и смещений). Вычитая этот интервал из Min и добавляя его к Max, мы получаем границы расширенного на дополнительный угол диапазона входных значений e_min и e_max соответственно. В этих пределах входное значение будет преобразовываться в угол отклонения стрелки.

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

- r1 – радиус окружности, на которой лежат внешние концы всех рисок;
- r2 – радиус окружности, на которой лежат внутренние концы больших рисок;
- rs – радиус окружности, на которой лежат внутренние концы маленьких рисок;
- rf – радиус окружности, на которой лежат внешние границы чисел шкалы;
- ra – радиус окружности, на которой лежит конец стрелки.

Все эти радиусы вычисляются согласно изображенным на рис. 419 параметрам рисования, но при этом эти параметры умножаются на DrawData->DoubleZoom для учета текущего масштаба подсистемы. Например, согласно рисунку, в масштабе 100% внешняя граница рисок отстоит от края прибора на D_Tick точек экрана. Масштаб 100% соответствует DrawData->DoubleZoom=1. Значит, в произвольном масштабе риски будут отстоять от края прибора на произведение D_Tick и DrawData->DoubleZoom – радиус окружности r1 будет короче радиуса всего прибора R на эту величину.

Теперь можно нарисовать шкалу. Мы будем рисовать ее в цикле по вещественной переменной v, изменяющейся от Min до Max с шагом Step:

      for(v=Min;v<=Max+Step*0.001;v+=Step)

Здесь в качестве конца цикла выбрано не Max, а значение, большее Max на одну тысячную шага изменения: если диапазон кратен шагу, из-за погрешностей вычисления последнее значение v может оказаться чуть больше Max, и при обычной проверке v<=Max самая последняя большая риска шкалы могла бы пропасть. Например, при Min=0, Max=100, Step=20 на шкале должно быть выведено шесть больших рисок с числами: 0, 20, 40, 60, 80 и 100. Однако, после пятого прибавления Step (20) к v может получиться не 100, а 100.00000001. При этом, поскольку это число больше Max, риска не была бы нарисована. Добавление к концу шкалы небольшой доли шага позволяет справиться с этой проблемой.

Внутри цикла мы при помощи макроса V_TO_A вычисляем угол a, который на нашей шкале соответствует величине v, и, по этому углу и радиусам r1 и r2, макросами CALCPOINT вычисляем координаты концов большой риски, соответствующей значению v. Эти координаты записываются в пары переменных (ix1, iy1) и (ix2, iy2). В RDS нет графической функции, позволяющей одним вызовом нарисовать линию между двумя точками, поэтому, чтобы нарисовать риску, приходится делать два вызова: сначала мы задаем координаты начала линии, вызвав функцию «rdsXGMoveTo(ix1,iy1)», а затем вызываем функцию «rdsXGLineTo(ix2,iy2)», которая нарисует линию из этой точки в точку (ix2, iy2). Линия будет нарисована с параметрами, заданными последним вызовом rdsXGSetPenStyle, то есть риска шкалы будет черной сплошной линией с толщиной, равной масштабу подсистемы.

Теперь рядом с этой риской нужно вывести число v, причем это число должно быть повернуто на угол риски a, как на рис. 419. Для этого придется последовательно выполнить следующие действия:

- преобразовать число v в текстовый вид, оставив в его дробной части Dec знаков;
- установить шрифт для вывода текста – пока без поворота (будем использовать шрифт «Arial», высота которого будет равна произведению параметра FontHeight на масштаб подсистемы);
- вычислить ширину числа в точках экрана, которую оно займет, если вывести его текущим установленным шрифтом;
- установить шрифт, повернутый на угол риски шкалы a;
- вычислить координаты левой верхней точки выводимого текста с учетом его поворота;
- вывести текст.

Для того, чтобы преобразовать вещественное число v в строку, можно, например, использовать стандартную функцию sprintf, как в примере из §3.6.2.5. Однако, ее использование требует добавления в раздел глобальных описаний команды для включения файла «stdio.h», а для этого придется открыть еще одну вкладку в редакторе модели. Чтобы не делать этого, воспользуемся сервисной функцией RDS rdsDtoA, которая формирует в динамической памяти строку с текстовым представлением числа с заданным количеством знаков в дробной части:

      str=rdsDtoA(v,     // Преобразуемое число
                  Dec,   // Знаков в дробной части
                  NULL); // Через этот указатель функция может вернуть
                         // длину созданной строки

В первом параметре функции передается преобразуемое число (v), во втором – необходимое число знаков после десятичной точки (Dec), в третьем можно было бы передать указатель на целую переменную, в которую функция запишет длину созданной ей строки, но нам эта длина не нужна, поэтому в третьем параметре мы передаем NULL. Функция возвращает указатель (char*) на созданную строку, который мы записываем во вспомогательную переменную str. Потом, когда эта строка уже не будет нам нужна, ее нужно будет обязательно удалить при помощи функции rdsFree.

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

      rdsXGSetFont(RDS_GFFONTALLHEIGHT,             // Маска установки
                   "Arial",                         // Имя шрифта
                   FontHeight*DrawData->DoubleZoom, // Размер шрифта
                   0,                               // Цвет шрифта
                   DEFAULT_CHARSET,                 // Набор символов
                   0,                               // Угол поворота (гр)
                   FALSE,                           // Жирность
                   FALSE,                           // Курсив
                   FALSE,                           // Подчеркивание
                   FALSE);                          // Перечеркивание

В первом параметре этой функции передается набор битовых флагов, указывающий на то, какие именно параметры шрифта мы устанавливаем. Константа RDS_GFFONTALLHEIGHT объединяет флаги всех параметров, причем размер шрифта при ее использовании задается в точках экрана, а не в типографских точках. Второй параметр функции задает имя шрифта (у нас – «Arial»), третий – размер (высоту FontHeight для масштаба 100% мы умножаем на масштаб подсистемы DrawData->DoubleZoom), четвертый – цвет символов (0 означает черный). В пятом параметре указывается используемый набор символов шрифта: мы будем выводить только цифры, десятичную точку и знак минуса, поэтому в этом параметре указана стандартная константа Windows API для набора по умолчанию – DEFAULT_CHARSET. В шестом параметре передается угол поворота шрифта в градусах – мы передаем ноль, поскольку мы его еще не поворачиваем, нам пока предстоит определить ширину не повернутого текста в точках экрана. Наконец, в последних четырех параметрах передаются логические признаки жирности, курсива, подчеркивания и перечеркивания шрифта.

Шрифт установлен, теперь мы вызываем функцию rdsXGGetTextSize, которая вернет нам ширину строки текста str, выведенной текущим шрифтом, в точках экрана:

      rdsXGGetTextSize(str,   // Текст
                       &w,    // Сюда запишется ширина
                       NULL); // Сюда могла бы записаться высота

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

Теперь можно установить повернутый шрифт. Для этого мы снова вызываем rdsXGSetFont с теми же параметрами, только в качестве угла поворота теперь передается не ноль, а a*180.0/M_PI (то есть угол риски a, переведенный в градусы). Затем, при помощи макроса CALCPOINT, мы вычисляем точку, находящуюся на одной линии с риской, но на расстоянии rf от центра круга, и записываем ее координаты в переменные (ix1, iy1). Чтобы число на шкале располагалось симметрично относительно риски, на эту точку должна приходиться середина верхней границы прямоугольной области, занимаемой его текстом. Поскольку в функциях вывода текста место вывода задается левым верхним углом первого символа строки, необходимо вычислить этот левый верхний угол. Если бы текст не был повернут, левый верхний угол первого символа отстоял бы от (ix1, iy1) влево на половину ширины строки, то есть на w/2. Чтобы учесть поворот строки на угол a, необходимо умножить w/2 на синус (для вертикальной координаты) и косинус (для горизонтальной) этого угла. Таким образом, левый верхний угол первого символа повернутого текста будет находиться в точке (ix2, iy2):

      ix2=ix1-0.5*w*cos(a);
      iy2=iy1+0.5*w*sin(a);

Теперь можно выводить число. Для этого используется функция rdsXGTextOut, в которую передаются вычисленные координаты ix2 и iy2 и выводимая строка str. После вызова этой функции строка с числом нам уже не нужна, и мы освобождаем ее память вызовом rdsFree.

Большая риска с числом нарисована – теперь необходимо вывести после нее мелкие риски. В одном большом интервале шкалы содержится Sub маленьких интервалов, то есть необходимо вывести Sub–1 мелких рисок (в начале самого первого маленького интервала уже выведена большая риска). Мы выводим их в цикле по целой переменной i, изменяющейся от 1 до Sub–1. Внутри цикла мы вычисляем значение, соответствующее i-й мелкой риске, по формуле v+i*Step/Sub: v – это значение большой риски шкалы в начале интервала, а Step/Sub – шаг мелких рисок. Если значение получилось больше верхнего предела шкалы Max, мы досрочно прерываем цикл, поскольку дальше риски рисовать не нужно. В противном случае мы вычисляем угол мелкой риски при помощи макроса V_TO_A, а затем макросами CALCPOINT рассчитываем координаты концов риски, лежащих на радиусах r1 и rs, после чего рисуем риску вызовами rdsXGMoveTo и rdsXGLineTo.

После того, как вся шкала нарисована, можно нарисовать стрелку, если, конечно, вход блока x не содержит признака ошибки rdsbcppHugeDouble (в этом случае переменная x_error будет иметь значение TRUE). Для рисования стрелки мы меняем цвет линии на красный (0xff) вызовом

      rdsXGSetPenStyle(RDS_GFCOLOR,0,0,0xff,0)

(константа RDS_GFCOLOR в первом параметре указывает на то, что мы меняем только цвет линии, а ее толщина, стиль и режим вывода остаются неизменными). После этого мы, как и для рисок шкалы, при помощи макросов V_TO_A и CALCPOINT вычисляем координаты конца стрелки, соответствующие ее положению для значения на входе блока x, и рисуем линию из центра прибора в эту точку. Нарисовав стрелку, мы снова устанавливаем черный цвет линии, включаем сплошную белую заливку и рисуем в центре прибора круг, радиус которого вычисляется как половина параметра CenterDisk (диаметр этого круга для масштаба 100%), умноженная на текущий масштаб подсистемы блока.

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

Для проверки работы модели можно собрать схему, изображенную на рис. 420. В ней к входам «x» и «Max» индикатора подключены поля ввода. Если запустить расчет и изменять значение «x», стрелка прибора будет двигаться. Если изменять значение «Max», будет меняться диапазон шкалы, а стрелка будет оставаться на одной и той же риске.

Рис. 420. Тестирование модели стрелочного индикатора

Стрелочный индикатор, похожий на созданный, можно было бы нарисовать и при помощи векторной картинки блока, но при этом его шкала была бы фиксированной, на ней не могли бы появляться новые риски и числа при изменении входов «Min», «Max» и «Step». В нашем же блоке шкала формируется программно и может быть какой угодно.

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

Моделью блока поддерживаются две реакции на события, связанные с изменениями размеров блока: реакция на перетаскивание маркеров выделения пользователем, и реакция на окончание изменения размеров (подробно они описаны в §2.12.8 руководства программиста). Первое событие возникает при каждом движении мыши в процессе перетаскивания маркеров, второе – когда пользователь отпускает кнопку мыши по окончании перетаскивания, или когда размер блока задан явно вводом ширины и высоты в окне его параметров. Для упрощения примера мы введем в нашу модель только реакцию на первое событие, поскольку оно, как и второе, позволяет скорректировать размер блока так, как нам надо, и дает пользователю визуальную обратную связь: если модель вмешается в изменение размеров блока, это немедленно отразится на размере прямоугольника, который пользователь видит при перетаскивании меток масштабирования.

Откроем вкладку события перетаскивания маркеров выделения: на левой панели окна редактора выберем вкладку «события» (см. §3.5.4), раскроем на ней раздел «внешний вид блока» и дважды щелкнем на его подразделе «проверка изменения размера (RDS_BFM_RESIZING)» (рис. 421). При этом значок подраздела станет желтым, а в правой части окна появится новая пустая вкладка «проверка размера».

Рис. 421. Реакция на изменение размеров блока в списке событий

Введем на этой вкладке следующий текст (точно такая же реакция используется в примере из §2.12.8 руководства программиста):

      if(ResizeData->HorzResize && (!ResizeData->VertResize))
        // Перетаскивается левый или правый маркер
        ResizeData->newHeight=ResizeData->newWidth;
      else if((!ResizeData->HorzResize) && ResizeData->VertResize)
        // Перетаскивается верхний или нижний маркер
        ResizeData->newWidth=ResizeData->newHeight;
      else // Перетаскивается угловой маркер
        { // Вычисляем среднее арифметическое размеров
          int avg=(ResizeData->newWidth+ResizeData->newHeight)/2;
          // Присваиваем его и ширине и высоте
          ResizeData->newWidth=ResizeData->newHeight=avg;
        }

В эту реакцию передается указатель ResizeData на структуру RDS_RESIZEDATA, которая описывает выполняемое изменение размеров блока. Нас в этой структуре будут интересовать только четыре поля: целые newWidth и newHeight, содержащие новые значения ширины и высоты блока, и логические HorzResize и VertResize, указывающие на то, какие маркеры выделения перетаскивает пользователь. Различные сочетания значений HorzResize и VertResize соответствует следующим ситуациям:

HorzResize | VertResize | Способ изменения
--- | --- | ---
TRUE | TRUE | Пользователь перетаскивает один из угловых маркеров выделения
TRUE | FALSE | Пользователь перетаскивает левый или правый маркер выделения
FALSE | TRUE | Пользователь перетаскивает верхний или нижний маркер выделения

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

Теперь, если скомпилировать модель, область, занимаемая блоком, при перетаскивании маркеров выделения будет все время оставаться квадратной, и круглый индикатор, нарисованный внутри нее, будет заполнять ее полностью. Пользователь все еще может сделать область блока прямоугольной, войдя в окно его параметров, выбрав там вкладку «внешний вид», нажав на панели «в подсистеме» кнопку «размер для функции DLL» (см. рис. 417) и введя произвольные размеры. Работу индикатора это не нарушит – его модель написана так, чтобы он всегда оставался круглым. Можно, при желании, заблокировать и такое изменение размеров, просто скопировав введенную нами реакцию в реакцию на событие «размер блока изменен», которое находится в том же разделе «внешний вид блока» (см. рис. 421) на первом месте. В эту реакцию передается точно такой же указатель ResizeData на структуру RDS_RESIZEDATA, поэтому в тексте программы ничего не нужно изменять.