Руководство программиста
Глава 2. Создание моделей блоков
§2.10. Программное рисование внешнего вида блока
Рассматриваются способы программного рисования внешнего вида блоков в окне подсистемы и вывод дополнительных изображений поверх блоков. Также рассматривается создание в окнах подсистем специальных панелей, на которых блоки блоки могут размещать элементы управления Windows или использовать их для вывода изображений, формируемых сторонними библиотеками. В частности, приводится пример построения на панели трехмерного изображения при помощи библиотеки OpenGL.
§2.10.1. Рисование изображения блока в окне подсистемы
Рассматривается программное рисование внешнего вида блоков на примере простого вертикального индикатора уровня и построения графика зависимости значения входа блока от системной переменной времени.
Внешний вид блока в RDS задается одним из трех способов: прямоугольником с текстом внутри, векторной картинкой с возможностью анимации, или программой рисования в функции модели блока. Прямоугольник с текстом – самое простое изображение блока. Цвет прямоугольника, текст внутри него и шрифт, которым выводится этот текст, задаются жестко и не могут отображать состояние блока или значения каких-либо его переменных. Этого вполне достаточно для большинства блоков, занимающихся исключительно расчетом, и ничего не индицирующих.
Использование векторной картинки дает больше свободы в выборе внешнего вида блока. Она может состоять из геометрических фигур, положение, относительные размеры, угол поворота, цвет и видимость которых могут быть связаны с различными переменными. Кроме того, картинка может включать текст, который также может быть связан со строковыми или числовыми переменными и отображать их значения. С помощью векторных картинок можно создавать как простые индикаторы, так и довольно сложные анимированные изображения. Даже если изображение блока должно быть статичным, ему иногда задают векторную картинку, чтобы иметь возможность включить в изображение символы разного цвета и начертания, стрелки, геометрические фигуры и т.п.
Самый сложный, но, при этом, самый богатый возможностями способ создания изображения блока – программное рисование из его модели. Используя стандартные функции рисования API Windows или сервисные графические функции RDS (фактически, представляющие собой оболочки функций API) модель блока может формировать произвольное изображение на рабочем поле окна подсистемы. Каждый раз, когда окно подсистемы, в которой находится блок, обновляется, функция модели блока вызывается с константой RDS_BFM_DRAW, при этом ей передается положение и размер прямоугольной области, которую занимает блок, контекст устройства Windows, на котором необходимо построить изображение, и некоторые другие параметры, которые могут потребоваться для рисования. Многие сложные изображения (графики, диаграммы и т.п.) могут быть построены только таким образом.
В качестве первого примера рассмотрим один из простейших программно рисуемых индикаторов – индикатор уровня. Прямоугольное изображение блока будет разделено на две части по вертикали: нижняя часть, высота которой пропорциональна значению входа, будет закрашена одним цветом (например, синим), верхняя – другим (например, белым). Для определенности будем считать, что при нулевом значении входа раздел будет проходить по нижней границе прямоугольника блока, а при значении, равном 100 – по верхней. Таким образом, блок будет рисовать вертикальный столбик, высота которого в процентах относительно полной высоты блока равна значению входа. Такие индикаторы применяются довольно часто, причем обычно максимальное и минимальное отображаемое значение у них настраивается пользователем, но, для упрощения примера, мы будем считать их константами 0 и 100 соответственно. По этой же причине мы не будем делать настраиваемыми цвета блока.
Для такого индикатора нужен единственный вещественный вход (назовем его «x»), поэтому структура переменных блока будет выглядеть следующим образом:
| Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение |
|---|---|---|---|---|---|---|
| 0 | Start | Сигнал | 1 | Вход | ✓ | 0 |
| 1 | Ready | Сигнал | 1 | Выход | 0 | |
| 2 | x | double | 8 | Вход | 0 |
Реакции на такт расчета у этого блока не будет (он ничего не считает, только индицирует), поэтому состояние флага «» для его входа не важно, но лучше его сбросить, и установить для блока запуск по сигналу, чтобы модель вообще не запускалась в тактах расчета.
Для большей ясности примера, вынесем рисование в отдельную функцию, но писать ее пока не будем – ограничимся ее прототипом перед функцией модели. Параметры передаваемые в функцию рисования, можно описать уже сейчас: это указатель на структуру RDS_DRAWDATA, передаваемую в функцию модели блока при ее вызовах для рисования изображений, и вещественное значение входа блока (функция рисования, в отличие от функции модели, не будет иметь доступа к переменным блока, поэтому отображаемое значение нужно передать ей явно).
Модель блока с прототипом функции рисования будет иметь следующий вид:
// Прототип функции рисования void SimpleLevelIndicatorDraw(RDS_PDRAWDATA draw,double val); // Модель простого индикатора уровня extern "C" __declspec(dllexport) int RDSCALL SimpleLevelIndicator( int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { // Макроопределения для статических переменных #define pStart ((char *)(BlockData->VarTreeData)) #define Start (*((char *)(pStart))) #define Ready (*((char *)(pStart+RDS_VSZ_S))) #define x (*((double *)(pStart+2*RDS_VSZ_S))) switch(CallMode) { // Проверка допустимости типа переменных case RDS_BFM_VARCHECK: if(strcmp((char*)ExtParam,"{SSD}")) return RDS_BFR_BADVARSMSG; return RDS_BFR_DONE; // Рисование внешнего вида блока case RDS_BFM_DRAW: SimpleLevelIndicatorDraw((RDS_PDRAWDATA)ExtParam,x); break; } return RDS_BFR_DONE; // Отмена макроопределений #undef x #undef Ready #undef Start #undef pStart } //=========================================
Модель содержит всего две реакции – обычную проверку допустимости типа статических переменных (RDS_BFM_VARCHECK) и реакцию на вызов в режиме рисования изображения блока RDS_BFM_DRAW. В этом режиме в параметре ExtParam передается указатель на структуру RDS_DRAWDATA, содержащую все необходимые для рисования данные: контекст устройства Windows типа HDC, на котором нужно рисовать, размеры описывающего прямоугольника блока, текущий масштаб и т.п. В «RdsDef.h» эта структура описана следующим образом:
typedef struct { HDC dc; // Контекст устройства Windows (где рисовать) BOOL CalcMode; // Режим моделирования/расчета (TRUE) или // редактирования (FALSE) int BlockX,BlockY; // Координаты точки привязки блока // с учетом связи с переменными double DoubleZoom; // Масштаб в долях единицы // Данные описывающего прямоугольника блока в текущем масштабе BOOL RectValid; // TRUE если по результатам рисования // описывающий прямоугольник нужно изменить, // FALSE в противном случае (по умолчанию) int Left,Top; // Левый верхний угол прямоугольника int Width,Height; // Размеры прямоугольника //--------- RECT *VisibleRect; // Видимая в окне часть рабочего поля // подсистемы (только чтение) BOOL FullDraw; // TRUE – необходимо нарисовать все, // FALSE – только изменения с прошлого рисования } RDS_DRAWDATA; typedef RDS_DRAWDATA *RDS_PDRAWDATA;
Сейчас нам из этой структуры понадобятся только поля Left, Top, Width и Height, в которых содержатся координаты описывающего прямоугольника блока в текущем масштабе, с учетом положения полос прокрутки окна, а также связи координат блока с какими-либо переменными, если таковая имеется (в нашем блоке такой связи нет). Именно эти координаты определяют прямоугольную область в окне подсистемы, занимаемую нашим блоком, внутри нее мы и будем рисовать индикатор уровня.
Указатель на структуру RDS_DRAWDATA (предварительно приводя его к правильному типу, поскольку ExtParam имеет тип void*) мы передаем в функцию рисования SimpleLevelIndicatorDraw. Вторым параметром в эту функцию передается текущее значение входа x.
Теперь напишем функцию рисования:
// Функция рисования простого индикатора уровня void SimpleLevelIndicatorDraw(RDS_PDRAWDATA draw,double val) { // Диапазон допустимых значений входа const double Min=0.0,Max=100.0; // Цвета индикатора const COLORREF empty=0xffffff, // Верхняя часть (белый) fill=0xff0000, // Нижняя часть (синий) border=0; // Рамка вокруг (черный) // Вспомогательные переменные int height,fullheight,x1,y1,x2,y2; // Координаты прямоугольника внутри рамки (отступ в 1 точку) x1=draw->Left+1; y1=draw->Top+1; x2=draw->Left+draw->Width-1; y2=draw->Top+draw->Height-1; // Выстота блока без толщины рамки (==draw->Height-2) fullheight=(y2-y1); // Высота столбика (от нижней границы до линии раздела) height=(val-Min)*fullheight/(Max-Min); // Ограничения сверху и снизу if(height>fullheight) height=fullheight; else if(height<0) height=0; // Рисование рамки rdsXGSetPenStyle(0,PS_SOLID,1,border,R2_COPYPEN); rdsXGSetBrushStyle(0,RDS_GFS_EMPTY,0); rdsXGRectangle(draw->Left,draw->Top, draw->Left+draw->Width,draw->Top+draw->Height); // Закраска верхней части цветом empty if(height!=fullheight) { rdsXGSetBrushStyle(0,RDS_GFS_SOLID,empty); rdsXGFillRect(x1,y1,x2,y2-height); } // Закраска нижней части цветом fill if(height!=0) { rdsXGSetBrushStyle(0,RDS_GFS_SOLID,fill); rdsXGFillRect(x1,y2-height,x2,y2); } } //=========================================
Первым параметром в функцию передается указатель на структуру RDS_DRAWDATA, полученный от RDS. Хотя в одном из полей этой структуры и передается контекст устройства, на котором блок должен нарисовать свое изображение, он нам не понадобится – вместо стандартных функций Windows API мы будем пользоваться сервисными функциями рисования RDS, поскольку работать с ними несколько проще. Точно так же мы поступили в примере со сложной функцией настройки. Рисование выполняется в три приема: сначала мы рисуем черную рамку толщиной в одну точку по размеру блока. Затем, отступив одну точку от границ блока внутрь, чтобы не перекрыть рамку, закрашиваем верхнюю часть прямоугольника белым цветом, а нижнюю – синим. Координаты границы раздела мы вычисляем, зная значение входа, переданное в параметре val, и высоту прямоугольника блока.
В начале функции описаны константы, определяющие диапазон возможных значений входа блока (Min и Max), а также цвета рамки, верхней и нижней части индикатора (border, empty и fill соответственно). Все цвета, как и везде в Windows API, задаются целыми числами в формате 0x00bbggrr, где bb – байт интенсивности синего канала цвета, gg – зеленого, а rr – красного (константы заданы в шестнадцатеричном виде). Мы уже решили не делать функцию настройки для этих параметров, чтобы не усложнять пример, поэтому эти цвета и объявлены как константы. При необходимости, можно хранить цвета в личной области данных или в статических переменных блока и разрешить пользователю изменять их (примеры функций настройки приведены в §2.7).
Далее вспомогательным переменным x1 и y1 присваиваются координаты левого верхнего угла закрашиваемой области индикатора – они отстоят от левого верхнего угла всего изображения на одну точку, поскольку по краю блока пройдет рамка толщиной в одну точку, и закрашивать эту границу не нужно. Координаты левого верхнего угла изображения блока берутся из полей Left и Top структуры RDS_DRAWDATA, указатель на которую передан в функцию в параметре draw. В этих параметрах уже учтен текущий масштаб окна подсистемы и положение его полос прокрутки. Например, если координаты левого верхнего угла блока – (15,40), масштаб окна установлен в 200%, горизонтальная полоса прокрутки сдвинута до упора влево, а вертикальная – до упора вверх (то есть в окне видна левая верхняя часть рабочего поля), Left будет равно 30, а Top – 80. Если при этом начать двигать горизонтальную полосу прокрутки вправо (рабочее поле начнет «смещаться» влево), Left начнет уменьшаться. В общем, при любых изменениях масштаба и прокрутке рабочей области точка (Left,Top) будет соответствовать левому верхнему углу прямоугольной области, занимаемой блоком в окна в данный момент.
Вспомогательным переменным x2 и y2 в функции присваиваются координаты правого нижнего угла закрашиваемой области – они тоже отстоят от правого нижнего угла всего изображения на одну точку. В структуре RDS_PDRAWDATA передаются ширина и высота изображения в текущем масштабе, поэтому для получения правого нижнего угла к левому верхнему добавляют ширину и высоту соответственно (и вычитают 1, чтобы отступить внутрь блока на одну точку, необходимую для рисования рамки). Затем, когда все четыре координаты закрашиваемой области индикатора вычислены, дополнительно вычисляется высота этой области fullheight – она понадобится для вычисления координаты границы раздела цветов.
Граница раздела цветов, а точнее, высота нижней закрашиваемой части height, вычисляется из следующих соображений: вещественное значение val может изменяться от Min до Max, при этом высота нижней части линейно меняется от 0 до fullheight соответственно. Таким образом,
height=(val-Min)*fullheight/(Max-Min);
Теперь важно ограничить значение height так, чтобы граница раздела всегда оставалась внутри изображения блока. Если значение val будет больше выбранного нами ограничения Max, height будет больше fullheight, что недопустимо – закрашенная часть при этом будет выходить за границы блока сверху. Поэтому, если переменная height превышает fullheight, мы ограничиваем ее значением fullheight. Точно так же при отрицательных значениях переменной height мы принудительно присваиваем ей ноль.
Теперь можно приступать к рисованию. Сначала мы рисуем рамку – для этого предварительно функцией rdsXGSetPenStyle устанавливается стиль линии и функцией rdsXGSetBrushStyle отключается заливка (эти функции уже знакомы нам по примеру функции настройки блока-генератора). Далее функцией rdsXGRectangle рисуется прямоугольная рамка. Функция принимает четыре целых параметра – левый верхний и правый нижний углы прямоугольника – и рисует прямоугольник с использованием текущего стиля линии и заливки. В данном случае мы установили черную линию толщиной в одну точку и отключили заливку, поэтому будет нарисована пустая внутри прямоугольная рамка вокруг блока.
Осталось закрасить верхнюю часть прямоугольника цветом empty (белым), а нижнюю – цветом fill (синим). Отступ границы раздела от нижней части блока у нас уже вычислен и находится в переменной height, поэтому верхняя закрашенная часть будет располагаться между вертикальными координатами y1 и y2-height (нужно всегда помнить, что в окнах вертикальная координатная ось направлена сверху вниз, а не снизу вверх). Закрашивать верхнюю часть мы будем только тогда, когда height не равно fullheight, так как при их равенстве весь прямоугольник блока нужно закрасить синим (столбик индикатора имеет максимальную высоту). Для закраски используется уже знакомая нам функция заливки прямоугольника rdsXGFillRect – она не использует стиль линии, поэтому перед ее вызовом мы устанавливаем только стиль заливки (сплошная, цвет empty).
Точно так же мы закрашиваем нижнюю часть прямоугольника (между y2-height и y2) цветом fill в том случае, если height не равно нулю (столбик имеет ненулевую высоту).
Для того, чтобы проверить эту модель, следует подключить ее к блоку, в параметрах которого на вкладке «» задано «». Также желательно разрешить масштабирование блока на той же вкладке, чтобы пользователь мог менять его размер мышью, перетаскивая один из восьми прямоугольных маркеров. Ко входу блока «x» можно подключить поле ввода (рис. 58), при изменении его значения от 0 до 100 в режиме расчета высота столбика в блоке будет изменяться от нулевой до максимальной. При подаче на вход отрицательных значений весь блок будет залит белым, при подаче значений, больших 100 – синим.
Рис. 58. Индикатор уровня и параметры его внешнего вида
Можно заметить, что модель индикатора уровня получилась довольно простой – в том числе, и за счет использования графических сервисных функций RDS. В большинстве случаев удается обойтись ими и не использовать функции Windows API для рисования изображений блоков – это делает модели несколько более наглядными и гарантирует их совместимость с разными версиями RDS.
На самом деле, точно такой же индикатор можно было бы сделать и не прибегая к программному рисованию внешнего вида блоков – в редакторе векторной картинки можно связать вертикальный масштаб прямоугольника с какой-либо переменной блока и, меняя эту переменную, изменять высоту прямоугольника. Останется только ввести в модель блока приведение входа к диапазону 0…1 и запись получившегося значения в переменную, связанную с картинкой, и индикатор уровня готов. Тем не менее, в некоторых случая без программного рисования не обойтись.
Создадим блок, который будет строить график зависимости значения входа от времени. Блок будет довольно сложным – мы будем отображать не только сам график (для чего блоку потребуются динамически отводимые массивы для хранения запомненных отсчетов), но и координатные оси с разметкой и числами на них. Кроме того, в этом блоке мы предусмотрим настройку диапазонов обеих осей, цвета и толщины линии графика, цвета осей и фона, шрифта чисел на осях и т.д. Такой блок можно создать только с использованием программного рисования внешнего вида.
Чтобы совсем уж не усложнять пример, мы не будем делать в блоке автоматическую настройку диапазонов горизонтальной и вертикальной осей, автоматическое увеличение массива, в котором хранятся отсчеты графика, при его переполнении и т.д. Разумеется, серьезный, удобный в использовании блок-график должен иметь эти возможности, но цель этого примера – демонстрация возможностей программного рисования, поэтому диапазоны осей будут задаваться пользователем в настройках блока и останутся неизменными в процессе расчета. Вместо размера массива мы дадим пользователю задать шаг записи графика, то есть интервал времени между записью в массив отсчетов. Размер массива при этом можно вычислить автоматически: максимально возможное число отсчетов в массиве будет равно диапазону горизонтальной оси, деленному на шаг записи графика. Значение времени наш блок будет брать из стандартной динамической переменной «DynTime» (как и многие другие рассмотренные блоки, например, блок-генератор), что сделает его совместимым с блоками, входящими в стандартную библиотеку RDS.
Для вывода графика будем рисовать внутри прямоугольника блока прямоугольник меньшего размера, на который будет наложена пунктирная сетка разметки (рис. 59). Слева и снизу от этого прямоугольника будем выводить числа, соответствующие делениям горизонтальной и вертикальной осей. Размер внутреннего прямоугольника модель блока будет автоматически подбирать таким образом, чтобы числа на осях, выведенные выбранным пользователем шрифтом, уместились внутрь прямоугольника блока. Из рисунка видно, что расстояние между левой границей внутреннего прямоугольника и левой границей внешнего прямоугольника блока (на рисунке это расстояние обозначено как «Gr_x1») должно равняться ширине самого большого числа на вертикальной оси (на рисунке – «400»), иначе числа вертикальной оси не уместятся внутрь внешнего прямоугольника. «Gr_x1» также не должно быть меньше половины ширины минимального числа горизонтальной оси, иначе первое число этой оси также не уместится во внешний прямоугольник. Правая граница внутреннего прямоугольника «Gr_x2» должна вычисляться так, чтобы во внешний прямоугольник уместилось максимальное число горизонтальной оси (на рисунке – «40»), таким образом, расстояние между правыми границами внешнего и внутреннего прямоугольников должно равняться половине ширины максимального числа горизонтальной оси.
Рис. 59. Предполагаемый внешний вид блока-графика
Расстояние между верхними границами прямоугольников («Gr_y1») должно равняться половине высоты числа, чтобы во внешний прямоугольник уместилось самое верхнее число вертикальной оси. Расстояние между нижними границами должно равняться полной высоте числа, чтобы уместились числа горизонтальной оси. Интервал следования чисел на горизонтальной и вертикальной осях, а также дробная часть этих чисел будут задаваться пользователем в настройках блока.
Теперь, когда мы представляем себе, что и как должна рисовать модель блока, можно приступать к ее написанию. Начнем с личной области данных блока, в которой мы будем хранить все задаваемые пользователем цвета, диапазоны и другие настроечные параметры. Также в ней будут находиться указатели на динамически отводимые массивы отсчетов графика и указатель на структуру подписки, с помощью которой блок будет обращаться к динамической переменной времени «DynTime». Оформим личную область данных как класс C++:
//========================================= // Простой график – личная область данных //========================================= class TSimplePlotData { private: // Настроечные параметры графика (цвета, шаг и т.п.) double TimeStep; // Шаг записи отсчетов RDS_SERVFONTPARAMS Font; // Шрифт чисел на осях COLORREF BorderColor; // Цвет рамки вокруг блока COLORREF FillColor; // Цвет фона блока COLORREF PlotBorderColor;// Цвет рамки поля графика COLORREF PlotFillColor; // Цвет фона поля графика COLORREF LineColor; // Цвет лини графика int LineWidth; // Толщина линии графика // Ось X double Xmin,Xmax; // Диапазон double XGridStep; // Шаг чисел на осях int XNumDecimal; // Дробная часть чисел на осях // Ось Y double Ymin,Ymax; // Диапазон double YGridStep; // Шаг чисел на осях int YNumDecimal; // Дробная часть чисел на осях // Массивы для хранения отсчетов графика double *Times; // Массив отсчетов времени double *Values; // Массив значений int Count; // Размер массивов int NextIndex; // Индекс для записи следующего значения double NextTime; // Время записи следующего значения RDS_PDYNVARLINK Time; // Связь с динамической переменной // времени ("DynTime") public: // Функция отведения массивов отсчетов void AllocateArrays(void); // Функция освобождения массивов отсчетов void ClearArrays(void); // Добавление очередной точки в массив отсчетов графика void AddPoint(double v); int Setup(void); // Функция настройки параметров void SaveText(void); // Функция сохранения параметров void LoadText(char *text); // Функция загрузки параметров void Draw(RDS_PDRAWDATA DrawData); // Функция рисования TSimplePlotData(void); // Конструктор класса ~TSimplePlotData(); // Деструктор класса }; //=========================================
Все параметры блока описаны в закрытой области (private) – обращение к ним будет вестись только из функций-членов класса. В начале области располагаются настроечные параметры: цвета, шаг записи, диапазоны осей и т.д. В тексте описания класса содержатся комментарии, поясняющие назначение каждого параметра, поэтому нет смысла подробно описывать каждый из них. Остановимся только на параметрах шрифта чисел на осях, поскольку описания шрифтов ранее в примерах моделей блоков не встречалось. Описание выглядит так:
RDS_SERVFONTPARAMS Font;
RDS_SERVFONTPARAMS – это структура, используемая некоторыми сервисными функциями RDS при получении или установке параметров шрифта: начертания, размера, жирности и т.п. Она описана в «RdsDef.h» и устроена, с учетом использования строкового поля-псевдонима, следующим образом:
#define RDS_SERVFONTPARAMSNAMESIZE 256 // Размер массива // имени шрифта typedef struct { DWORD servSize; // Размер структуры в байтах char NameA[RDS_SERVFONTPARAMSNAMESIZE]; // Имя шрифта (UTF8) WCHAR NameW[RDS_SERVFONTPARAMSNAMESIZE]; // Имя шрифта (UTF16) //RDSXCHAR Name[RDS_SERVFONTPARAMSNAMESIZE]; // Поле-псевдоним для имени шрифта int CharSet; // Набор символов int Height; // Высота шрифта в точках экрана int Size; // Кегль шрифта BOOL SizePriority;// При установке шрифта использовать // кегль (TRUE), или высоту в точках (FALSE) COLORREF Color; // Цвет шрифта BOOL Bold; // Жирный BOOL Italic; // Курсив BOOL Underline; // Подчеркнутый BOOL StrikeOut; // Зачеркнутый } RDS_SERVFONTPARAMS;
В этой структуре содержатся основные параметры шрифта, которые могут потребоваться для его установки. Кроме того, как и в большинстве структур, с которыми работают сервисные функции RDS, в ней есть дополнительное служебное поле servSize, которому необходимо присвоить размер этой структуры в байтах, то есть sizeof(RDS_SERVFONTPARAMS). Это поле используется для контроля правильности переданной в какую-либо функцию структуры: если его значение, то есть размер структуры, будет неверным, значит, модель использует неправильные описания, и RDS не будет с ней работать.
Имя шрифта задается одним из двух полей-массивов NameA или NameW, можно также использовать поле-псевдоним Name, обращение к которому, в зависомости от наличия специальных макроопределений, будет эквивалентно обращению либо к NameA, либо к NameW. Если имя шрифта задается в кодировке UTF-8, его нужно записывать в поле NameA, и использовать для установки параметров сервисную функцию rdsXGSetFontByParStrA. Если имя шрифта задается в кодировке UTF-16, используются поле NameW и функция rdsXGSetFontByParStrW. По умолчанию, то есть без специальных макроопределений, псевдонимы Name и rdsXGSetFontByParStr используются для кодировки UTF-8. В данной модели рассматривается именно этот случай.
Для задания высоты шрифта в структуре предусмотрено два поля: Height, задающее высоту шрифта в точках экрана, и Size, задающее высоту в типографских единицах (кегль), более привычных для пользователя. Если структура используется для установки параметров шрифта (например, при рисовании), значение берется только из одного из этих полей: при SizePriority равном TRUE – из поля Size, при SizePriority равном FALSE – из поля Height. Если же в структуру записываются параметры какого-либо шрифта при вызове одной из сервисных функций получения параметров, эта функция заполняет оба этих поля независимо от значения SizePriority. В нашей модели эта структура будет использоваться как при установке параметров шрифта (при рисовании осей), так и при их получении (из диалога выбора шрифта при настройке параметров пользователем, а также при загрузке параметров блока).
После настроечных параметров блока в классе описываются массивы отсчетов графика. Отсчеты хранятся в двух вещественных массивах, каждый из которых содержит Count элементов: в массиве Times хранятся значения времени, а в массиве Values – значения входа блока, соответствующие этим моментам. Таким образом, целому индексу i, находящемуся в диапазоне [0…Count-1], соответствует точка графика (Times[i],Values[i]).
В процессе работы системы массивы Times и Values будут постепенно заполняться отсчетами. Заполнение массивов управляется полем NextIndex, в котором находится индекс первой свободной ячейки. Таким образом, в любой момент времени элементы массивов [0…NextIndex-1] заполнены, а [NextIndex…Count-1] – свободны. Значение времени, по достижении которого в массив будет записан очередной отсчет, задается полем NextTime.
В общих чертах, запись отсчетов в массивы для последующего построения графика будет работать следующим образом:
- Исходно массивы Times и Values не отведены – оба поля содержат значения NULL.
- При первом запуске расчета вычисляется требуемый размер массивов (диапазон оси времени, деленный на шаг записи) и записывается в поле Count. Отводится место под массивы Times и Values – оба будут содержать по Count вещественных чисел двойной точности. Полю NextTime присваивается значение начала оси времени (Xmin), полю NextIndex – значение 0. Теперь блок ждет наступления времени NextTime, чтобы записать первый отсчет в массивы.
- Как только текущее время, получаемое из динамической переменной «DynTime», станет большим или равным NextTime, значение входа блока запишется в Values[NextIndex], а значение времени – в Times[NextIndex]. После этого NextIndex увеличится на 1, а к NextTime будет прибавлен шаг записи TimeStep, и блок снова будет ждать наступления времени NextTime для записи очередного отсчета.
- Последний пункт будет повторяться до тех пор, пока NextIndex не станет равным Count, что укажет на заполнение всего массива. После этого новые отсчеты никуда писаться не будут. В настоящем блоке-графике в этот момент следовало бы увеличить размер массива или сдвинуть диапазон, но, для упрощения примера, мы решили этого не делать.
Разобравшись с принципом записи отсчетов в график и необходимыми для этого полями, вернемся к классу личной области данных. В открытой области (public) находятся описания функций-членов, которые будут вызываться из модели блока. Это пара функций для отведения и освобождения памяти под описанные выше массивы отсчетов, функция вызова окна настроек блока, в котором пользователь сможет вводить значения параметров, функции сохранения и загрузки параметров блока (раз мы сделали функцию настройки, необходимо записывать введенные пользователем значения при сохранении схемы и считывать их при ее загрузке), функция рисования внешнего вида блока (ради которой и рассматривается этот пример), а также функция записи очередного отсчета графика в массив, реализующая описанный выше алгоритм работы блока. И, разумеется, у класса будет конструктор, в котором будут устанавливаться начальные значения параметров и запрашиваться подписка на динамическую переменную «DynTime», и деструктор, в котором будет освобождаться память, занятая массивами отсчетов, и прекращаться подписка на «DynTime». Далее мы по очереди рассмотрим все эти функции, но сначала запишем собственно функцию модели, которая будет создавать и уничтожать объект описанного класса и вызывать его функции-члены.
Для работы блоку будут нужны будут следующие переменные:
| Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение |
|---|---|---|---|---|---|---|
| 0 | Start | Сигнал | 1 | Вход | ✓ | 0 |
| 1 | Ready | Сигнал | 1 | Выход | 0 | |
| 2 | x | double | 8 | Вход | 0 |
Заметим, что для входа блока «x», значение которого будет строиться на графике, не задан флаг «», то есть при поступлении на вход блока нового значения модель не будет запускаться автоматически. Согласно описанному выше алгоритму работы, блок записывает новый отсчет в массивы при изменении времени. Время блок получает через стандартную динамическую переменную, поэтому в модели необходима реакция на ее изменение (RDS_BFM_DYNVARCHANGE), а на такт расчета (RDS_BFM_MODEL) реакция не нужна. таким образом, модели блока не нужен ни запуск при срабатывании связи, подключенной ко входу «x», ни запуск каждый такт (в параметрах блока следует выбрать запуск по сигналу).
Модель блока выглядит следующим образом:
// Простой график extern "C" __declspec(dllexport) int RDSCALL SimplePlot( int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { // Макроопределения для статических переменных #define pStart ((char *)(BlockData->VarTreeData)) #define Start (*((char *)(pStart))) #define Ready (*((char *)(pStart+RDS_VSZ_S))) #define x (*((double *)(pStart+2*RDS_VSZ_S))) // Указатель на личную область, приведенный к правильному типу TSimplePlotData *data=(TSimplePlotData*)(BlockData->BlockData); switch(CallMode) { // Инициализация блока case RDS_BFM_INIT: BlockData->BlockData=new TSimplePlotData(); break; // Очистка данных блока case RDS_BFM_CLEANUP: delete data; break; // Проверка типов статических переменных case RDS_BFM_VARCHECK: if(strcmp((char*)ExtParam,"{SSD}")==0) return RDS_BFR_DONE; return RDS_BFR_BADVARSMSG; // Функция настройки параметров case RDS_BFM_SETUP: return data->Setup(); // Загрузка параметров в текстовом формате case RDS_BFM_LOADTXT: data->LoadText((char*)ExtParam); break; // Созранение параметров в текстовом формате case RDS_BFM_SAVETXT: data->SaveText(); break; // Рисование внешнего вида блока case RDS_BFM_DRAW: data->Draw((RDS_PDRAWDATA)ExtParam); break; // Запуск расчета case RDS_BFM_STARTCALC: if(((RDS_PSTARTSTOPDATA)ExtParam)->FirstStart) data->AllocateArrays(); // Первый запуск break; // Сброс расчета case RDS_BFM_RESETCALC: data->ClearArrays(); break; // Реакция на изменение динамической переменной case RDS_BFM_DYNVARCHANGE: data->AddPoint(x); break; } return RDS_BFR_DONE; // Отмена макроопределений для переменных #undef x #undef Ready #undef Start #undef pStart } //=========================================
Большинство реакций в этой модели уже неоднократно описывалось: при инициализации (RDS_BFM_INIT) модель создает объект класса TSimplePlotData, при очистке (RDS_BFM_CLEANUP) – уничтожает его, при проверке типа статических переменных (RDS_BFM_VARCHECK) – сравнивает переданную строку с правильной строкой типа. В модели также присутствуют реакции на сохранение данных блока в текстовом формате (RDS_BFM_SAVETXT), загрузку этих данных (RDS_BFM_LOADTXT) и вызов пользователем окна настройки (RDS_BFM_SETUP). Все они вызывают соответствующие функции-члены класса, которые будут рассмотрены позднее. Для рисования блока в модель, как и в предыдущем примере, введена реакция RDS_BFM_DRAW, только теперь функция рисования Draw является членом класса TSimplePlotData. Следует помнить, что в режиме RDS_BFM_DRAW вызываются только модели тех блоков, для которых в параметрах задано рисование с помощью функции DLL (см. рис. 58). И, наконец, для записи данных в массивы, из которых строится график, и для обеспечения работы этих массивов в модель введены реакции на запуск расчета RDS_BFM_STARTCALC, сброс расчета RDS_BFM_RESETCALC и изменение динамической переменной RDS_BFM_DYNVARCHANGE (в данном случае у блока есть единственная динамическая переменная – «DynTime», то есть время). Остановимся на этих реакциях подробнее.
Согласно алгоритму записи отсчетов в массивы, описанному немного выше, память под массивы Times и Values должна отводиться при запуске расчета. Однако, не следует делать это при каждом запуске. Представим себе, что пользователь запустил расчет, после чего остановил его в тот момент, когда была построена только половина графика. Изменив какие-либо переменные системы (введя новые значения в поля ввода, передвинув рукоятки и т.п.) он решил продолжить работу и, не сбрасывая расчет, снова запустил его. Естественно, он будет ожидать, что график будет достроен до конца, и его первая половина, построенная до промежуточной остановки расчета, не сотрется. Если же отводить память под массивы при каждом запуске расчета, запомненные до остановки данные будут потеряны. Чтобы избежать этого, память под массивы нужно отводить только при первом после сброса (или после загрузки схемы) запуске расчета.
Для того, чтобы отличить первый запуск расчета от повторного, можно обратиться к полю FirstStart структуры RDS_STARTSTOPDATA, указатель на которую передается в функцию модели в параметре ExtParam при вызове ее в режиме RDS_BFM_STARTCALC. Если это поле равно TRUE, значит, расчет запущен в первый раз, и нужно вызвать функцию отведения массивов AllocateArrays (при этом ExtParam, как всегда, приходится предварительно приводить к правильному типу, в данном случае, к RDS_PSTARTSTOPDATA, то есть к указателю на RDS_STARTSTOPDATA).
Освобождение памяти (то есть стирание массивов) логично выполнять в момент сброса расчета. Это будет соответствовать ожиданиям пользователя – после сброса поле графика будет очищаться. Поэтому вызов функции освобождения массивов ClearArrays производится в реакции модели на событие RDS_BFM_RESETCALC. Необходимо будет вставить вызов этой функции еще и в деструктор класса, поскольку пользователь может стереть блок или закрыть RDS не сбрасывая расчет, и в этом случае отведенную память также нужно освободить.
Наконец, при любом изменении единственной динамической переменной блока (событие RDS_BFM_DYNVARCHANGE) будет вызываться функция AddPoint, в которую передается текущее значение входа блока x. Внутри этой функции, при необходимости, пара «время-значение» будет добавлена в массивы отсчетов графика.
Рассмотрение функций-членов класса TSimplePlotData начнем с конструктора класса. Он выглядит следующим образом:
// Конструктор класса личной области данных графика TSimplePlotData::TSimplePlotData(void) { // Присвоение начальных значений параметрам TimeStep=0.1; // Шаг записи BorderColor=0; // Цвет рамки вокруг блока FillColor=0xffffff; // Цвет фона блока PlotBorderColor=0; // Цвет рамки окна графика и сетки PlotFillColor=0xffffff; // Цвет окна графика LineColor=0; // Цвет линии графика LineWidth=1; // Толщина линии графика // Параметры шрифта Font.servSize=sizeof(Font); strcpy(Font.Name,"Arial"); Font.SizePriority=FALSE; Font.Height=15; Font.Color=0; Font.Bold=Font.Italic=Font.Underline=Font.StrikeOut=FALSE; Font.CharSet=DEFAULT_CHARSET; // Диапазоны осей, шаг сетки, число десятичных знаков // в числах на осях Xmin=0.0; Xmax=10.0; XGridStep=5.0; XNumDecimal=0; Ymin=-1.0; Ymax=1.0; YGridStep=0.5; YNumDecimal=1; // Обнуление указателей на массивы и их размера // (массивы еще не отведены) Times=Values=NULL; Count=NextIndex=0; NextTime=Xmin; // Подписка на динамическую переменную времени Time=rdsSubscribeToDynamicVar( RDS_DVPARENT, // В родительской подсистеме "DynTime", // Имя переменной "D", // Тип переменной (double) TRUE); // Искать по иерархии } //=========================================
В конструкторе присваиваются начальные значения всем полям класса, описывающим внешний вид графика, обнуляются указатели массивов (массивы будут отведены позже) и осуществляется подписка на стандартную переменную времени «DynTime» – она понадобится блоку для записи моментов времени в массив Times.
В деструкторе класса необходимо освободить память, занятую массивами (для этого будет использоваться функция ClearArrays, которую мы напишем позже) и прекратить подписку на переменную «DynTime»:
// Деструктор класса TSimplePlotData::~TSimplePlotData() { rdsUnsubscribeFromDynamicVar(Time); // Прекратить подписку ClearArrays(); // Освободить массивы } //=========================================
Следующей рассмотрим функцию рисования Draw – в конце концов, этот пример посвящен программному рисованию, и все остальные функции играют вспомогательные роли. Она выглядит следующим образом:
// Рисование внешнего вида блока void TSimplePlotData::Draw(RDS_PDRAWDATA DrawData) { // Вспомогательные переменные int Gr_x1,Gr_x2,Gr_y1,Gr_y2; int x1,y1,x2,y2,textheight,w1,w2; char buf[80]; // Рамка графика rdsXGSetPenStyle(0,PS_SOLID,1,BorderColor,R2_COPYPEN); rdsXGSetBrushStyle(0,RDS_GFS_SOLID,FillColor); rdsXGRectangle(DrawData->Left,DrawData->Top, DrawData->Left+DrawData->Width, DrawData->Top+DrawData->Height); // Необходимо вычислить координаты поля графика относительно // верхнего левого угла блока // Установка параметров шрифта с учетом масштаба rdsXGSetFontByParStr(&Font,DrawData->DoubleZoom); // Зазор сверху – половина высоты цифры + 1 точка rdsXGGetTextSize("0",NULL,&textheight); Gr_y1=textheight/2+1; // Зазор снизу – полная высота цифры + 1 точка Gr_y2=DrawData->Height-textheight-1; // Зазор слева – ширина самого длинного числа вертикальной // оси или половина ширины Xmin sprintf(buf," %.*lf ",YNumDecimal,Ymin); rdsXGGetTextSize(buf,&w1,NULL); // Ширина Ymin sprintf(buf," %.*lf ",YNumDecimal,Ymax); rdsXGGetTextSize(buf,&w2,NULL); // Ширина Ymax if(w2>w1) w1=w2; sprintf(buf," %.*lf ",XNumDecimal,Xmin); rdsXGGetTextSize(buf,&w2,NULL); // Ширина Xmin w2/=2; if(w2>w1) w1=w2; Gr_x1=w1; // Зазор справа – половина ширины Xmax sprintf(buf," %.*lf ",XNumDecimal,Xmax); rdsXGGetTextSize(buf,&w2,NULL); // Ширина Xmax w2/=2; Gr_x2=DrawData->Width-w2; // Абсолютные (на рабочем поле) координаты поля графика x1=DrawData->Left+Gr_x1; x2=DrawData->Left+Gr_x2; y1=DrawData->Top+Gr_y1; y2=DrawData->Top+Gr_y2; if(x1>=x2 || y1>=y2) // Негде рисовать return; // Прямоугольник поля графика rdsXGSetPenStyle(0,PS_SOLID,1,PlotBorderColor,R2_COPYPEN); rdsXGSetBrushStyle(0,RDS_GFS_SOLID,PlotFillColor); rdsXGRectangle(x1,y1,x2,y2); // Установка пунктирного стиля линии rdsXGSetPenStyle(0,PS_DOT,1,PlotBorderColor,R2_COPYPEN); rdsXGSetBrushStyle(0,RDS_GFS_EMPTY,0); // Без заливки // Горизонтальная ось с сеткой for(double x=Xmin;x<=Xmax+XGridStep*0.5;x+=XGridStep) { // ix - координата линии на рабочем поле int ix=x1+(x-Xmin)*(x2-x1)/(Xmax-Xmin); if(ix>x1 && ix<x2) // Чертим вертикальную линию { rdsXGMoveTo(ix,y1); rdsXGLineTo(ix,y2); } // Вывод числа на оси под полем sprintf(buf,"%.*lf",XNumDecimal,x); rdsXGGetTextSize(buf,&w1,NULL); rdsXGTextOut(ix-w1/2,y2,buf); } // Вертикальная ось с сеткой for(double y=Ymin;y<=Ymax+YGridStep*0.5;y+=YGridStep) { // iy - координата линии на рабочем поле int iy=y2-(y-Ymin)*(y2-y1)/(Ymax-Ymin); if(iy>y1 && iy<y2) // Чертим горизонтальную линию { rdsXGMoveTo(x1,iy); rdsXGLineTo(x2,iy); } // Вывод числа на оси слева от поля sprintf(buf,"%.*lf ",YNumDecimal,y); rdsXGGetTextSize(buf,&w1,&textheight); rdsXGTextOut(x1-w1-2,iy-textheight/2,buf); } // Если массивы не пустые – рисовать график if(Count) { RECT r; // Установить область отсечения рисования по полю графика r.left=x1+1; r.top=y1+1; r.right=x2-1; r.bottom=y2-1; rdsXGSetClipRect(&r); // Установить сплошной стиль линии, заданный для // графика цвет и толщину линии с учетом масштаба rdsXGSetPenStyle(0,PS_SOLID, LineWidth*DrawData->DoubleZoom, LineColor,R2_COPYPEN); // Строим ломанную линию по отсчетам из массивов for(int i=0;i<NextIndex;i++) { // Преобразуем вещественные отсчеты в целочисленные // координаты на рабочем поле int ix=x1+(Times[i]-Xmin)*(x2-x1)/(Xmax-Xmin), iy=y2-(Values[i]-Ymin)*(y2-y1)/(Ymax-Ymin); if(i) // Не первая точка – строим линию от предыдущей rdsXGLineTo(ix,iy); else // Первая точка графика – делаем ее текущей rdsXGMoveTo(ix,iy); } // Отмена отсечения rdsXGSetClipRect(NULL); } } //=========================================
Примерный вид изображения, которое строит эта функция, представлен на рис. 59, там же объясняется, как вычислить координаты поля графика таким образом, чтобы числа на осях уместились между этим полем и внешними границами блока. Этим координатам соответствуют переменные функции Gr_x1, Gr_x2, Gr_y1 и Gr_y2. В функции объявлены и другие локальные переменные, которые потребуются в процессе работы.
Рисование внешнего вида блока начинается с рамки. Функцией rdsXGSetPenStyle устанавливается стиль линии (сплошная, толщиной в одну точку, цвет – поле класса BorderColor), функцией rdsXGSetBrushStyle – тип заливки фигур (сплошная, цвет – поле класса FillColor), после чего функцией rdsXGRectangle рисуется прямоугольник во весь размер блока. В дальнейшем на этот прямоугольник будет наложено поле графика – другой прямоугольник меньшего размера, на котором будет рисоваться сетка и линия графика. Слева и снизу от этого меньшего прямоугольника будут выведены числовые метки осей.
Перед рисованием поля графика необходимо вычислить его координаты внутри большого прямоугольника. Для этого необходимо знать линейные размеры чисел на осях, выраженные в точках экрана. Эти размеры зависят от диапазонов осей (число «100» займет больше места, чем число «10»), числа знаков после десятичной точки («10.0» длиннее «10») и параметров шрифта, которым изображаются числа на осях. Для определения размеров чисел используется сервисная функция RDS rdsXGGetTextSize. В первом параметре этой функции передается указатель на произвольную строку текста (типа char*), а во втором и третьем – указатели на целые переменные, в которые функция запишет ширину и высоту прямоугольной области экрана, которую займет переданная строка, если ее вывести текущим шрифтом. Таким образом, перед вызовом этой функции нужно, во-первых, установить параметры шрифта согласно полю класса Font, и, во-вторых, преобразовать число, размеры которого мы хотим получить, в строку.
Для установки параметров шрифта по структуре Font используется сервисная функция RDS rdsXGSetFontByParStr, в которую передаются два параметра: указатель на структуру описания шрифта (&Font) и масштабный коэффициент (DrawData->DoubleZoom), на который умножается размер шрифта при установке. Таким образом, размер шрифта, которым будут выводиться числа на осях графика, будет зависеть от выбранного пользователем масштаба схемы. Это логично, поскольку в противном случае на мелких масштабах метки осей заняли бы большую часть площади блока, и на сам график места бы не осталось. Шрифт устанавливается в функции один раз – все числа на осях выводятся одним и тем же шрифтом, поэтому изменять его не придется.
Далее, в соответствие с описанными выше вычислениями, определяются размеры интересующих нас чисел, и по этим размерам вычисляются относительные координаты поля графика Gr_x1, Gr_x2, Gr_y1 и Gr_y2. Каждое число предварительно преобразуется в строку при помощи функции sprintf из стандартной библиотеки C (для того, чтобы можно было использовать эту функцию, в исходный текст программы должен быть включен файл заголовков «stdio.h»). Описание этой функции есть в каждом руководстве по языку C. Следует обратить внимание на то, что в строке формата функции sprintf « %.*lf » не указано число знаков числа после десятичной точки – вместо него стоит символ «*» («звездочка»). Этот символ указывает на то, что число знаков необходимо взять из следующего аргумента функции. То есть, вызов функции
sprintf(buf," %.*lf ",YNumDecimal,Ymin);
сформирует в массиве buf символьное представление вещественного числа Ymin, при этом в этом представлении будет YNumDecimal знаков после десятичной точки. Кроме того, поскольку строка начинается и заканчивается пробелом, сформированное число также будет окружено пробелами. Эти пробелы нужны для того, чтобы между выводимыми числами и рамкой графика и рабочего поля оставался небольшой зазор. Массив buf размером в 80 символов, в котором формируются все строки, объявлен в начале функции, его с большим запасом хватит для представления любого вещественного числа двойной точности.
Для преобразованных в строки чисел вызывается функция rdsXGGetTextSize, и полученные с ее помощью размеры прямоугольной области используются для вычисления координат поля графика. Эти вычисления занимают довольно большую часть функции. Их результатом будут координаты левого верхнего (Gr_x1,Gr_y1) и правого нижнего (Gr_x2,Gr_y2) углов поля графика относительно левого верхнего угла всей прямоугольной области, занятой блоком. Затем эти относительные координаты переводятся в абсолютные координаты на рабочем поле подсистемы (x1,y1) и (x2,y2). Если окажется, что x2 меньше x1 или y2 меньше y1, значит, поле графика не умещается в текущие размеры блока. Такое может произойти, если выбрать слишком большой размер шрифта для чисел на осях и слишком маленький размер самого блока. Например, очевидно, что если высота шрифта чисел больше высоты блока, то график просто негде рисовать. В этом случае функция завершается, не нарисовав ничего, кроме рамки графика. В принципе, вместо завершения функции можно было бы все равно нарисовать график, не выводя числа на осях, но мы не будем этого делать, чтобы не усложнять пример дополнительными условными операторами.
После того, как координаты поля графика вычислены, и мы убедились, что это поле вместе с числами осей умещается в прямоугольник блока, можно приступать к рисованию поля и его оформления. Сначала рисуется прямоугольник поля (x1,y1) – (x2,y2), залитый цветом PlotFillColor со сплошной рамкой цвета PlotBorderColor. Затем устанавливается пунктирный стиль линии и отключается заливка, после чего в двух циклах рисуется сначала горизонтальная сетка (вертикальные пунктирные линии с шагом XGridStep и числа под полем графика рядом с каждой из этих линий), а затем – вертикальная (горизонтальные пунктирные линии с шагом YGridStep и числа слева от поля графика рядом с ними). Подробно рассмотрим цикл рисования горизонтальной сетки – цикл вертикальной сетки будет аналогичен ему.
Вещественная переменная цикла x изменяется от начала диапазона горизонтальной оси Xmin до конца диапазона Xmax с шагом сетки XGridStep. Можно заметить, что проверкой выполнения цикла является не выражение x<=Xmax, как можно было бы ожидать, а x<=Xmax+XGridStep*0.5. То есть, к концу диапазона добавлено значение, заведомо меньшее шага изменения x. Это сделано из-за того, что x – вещественная переменная, а точное сравнение вещественных чисел крайне нежелательно. Допустим, мы хотим рисовать график с горизонтальной осью от 0 до 10 и шагом 2.5, таким образом, на горизонтальной оси должно быть пять меток: 0.0, 2.5, 5.0, 7.5 и 10.0. Однако, когда в процессе рисования оси мы прибавим шаг 2.5 к метке 7.5, чтобы получить последнюю метку, из-за погрешностей вычисления мы можем получить не 10.0, а 10.00…001. В результате последнее число на оси выведено не будет, поскольку оно окажется больше Xmax, и цикл завершится слишком рано. Чтобы избежать этого, нужно добавить к верхней границе цикла число, заведомо большее, чем возможная погрешность, но меньшее шага цикла. Половина шага цикла, в данном случае, вполне подходит.
Внутри цикла вещественное значение x, соответствующее очередной метке на оси, преобразуется в целую координату этой метки на рабочем поле ix по формуле:
ix=x1+(x-Xmin)*(x2-x1)/(Xmax-Xmin);
Это стандартная формула преобразования диапазона: разница между вещественным значением и началом его диапазона (x–Xmin) делится на весь диапазон изменения (Xmax-Xmin) и умножается на новый диапазон (x2-x1), после чего к получившемуся значению прибавляется начало нового диапазона x1. Таким образом, значению x=Xmin будет соответствовать ix=x1, значению x=Xmax будет соответствовать ix=x2, а между ними ix будет изменяться пропорционально изменению x.
После вычисления целой координаты ix ее значение сравнивается с допустимым диапазоном x1…x2, и, если координата попадает в этот диапазон, рисуется вертикальная пунктирная линия из конца в конец поля графика. Проверка на диапазон нужна из-за возможной погрешности вычисления, описанной выше: к концу диапазона эта погрешность может накопиться, и последняя вертикальная линия может оказаться на одну точку экрана правее границы поля графика, что будет выглядеть не очень хорошо. Для рисования линии используется пара сервисных функций rdsXGMoveTo – rdsXGLineTo, которые уже рассматривались ранее в примере в §2.7.3.
Независимо от того, попала ли координата ix в допустимый диапазон, ниже поля графика выводится значение x. Оно преобразуется в строку в массиве buf при помощи функции sprintf, а затем функция rdsXGGetTextSize записывает ширину получившейся строки, выведенной текущим шрифтом, в переменную w1. Ширина строки нужна для того, чтобы выровнять выводимое число по горизонтали так, чтобы его середина пришлась на нарисованную пунктирную линию, то есть на координату ix. Вывод строки осуществляется сервисной функцией RDS rdsXGTextOut, в первых двух параметрах которой указываются координаты верхнего левого угла выводимой строки, а в третьем – сама строка. Таким образом, если нужно вывести строку, имеющую на экране ширину w1 точек, так, чтобы ее центр пришелся на координату ix, и она размещалась ниже нижней границы поля графика y2, координаты левого верхнего угла этой строки должны быть (ix-w1/2,y2). Именно эти значения передаются в функцию rdsXGTextOut.
Мы не будем подробно рассматривать цикл рисования меток вертикальной оси, поскольку он похож на уже описанный. В нем по вещественной переменной цикла y вычисляется целая координата iy, рисуются горизонтальные пунктирные линии, и выводятся числовые метки слева от левой границы поля графика.
После того, как все оформление поля графика нарисовано, можно приступать к рисованию самого графика. Разумеется, рисовать график нужно только в том случае, если массивы времени и отсчетов отведены – это проверяется оператором if(Count). Можно было бы проверить указатели Times и Values на значение NULL, но проще сравнить с нулем поле Count, в котором должен храниться размер обоих массивов. В конструкторе мы присвоили Count значение 0, так же будем поступать и в еще не написанной функции очистки массивов ClearArrays. Таким образом, отличие Count от нуля можно использовать как признак существования массивов.
Если массивы отведены, первое, что необходимо сделать – это установить область отсечения рисования. В параметрах блока мы задаем диапазоны горизонтальной (Xmin…Xmax) и вертикальной (Ymin…Ymax) осей, точки вне этих диапазонов будут находиться за пределами изображаемого поля графика. Если не принять мер, выход значения времени за диапазон горизонтальной оси, или выход значения входа блока за диапазон вертикальной, приведет к тому, что рисуемая линия выйдет за пределы поля графика и затронет рамку графика, или даже другие блоки на рабочем поле. Чтобы не допустить этого, проще всего временно ограничить область экрана, в которой можно рисовать. Любые изображения, вышедшие за пределы этой области, будут автоматически отсекаться, причем отсекаться корректно: если, например, одна точка отрезка линии находится внутри области отсечения, а другая – снаружи, будет нарисована только часть отрезка вплоть до границы области. Чтобы программно реализовать такую возможность в функции рисования, пришлось бы вычислять точку пересечения отрезка с границей области. Использование областей отсечения позволяет переложить эти вычисления на Windows API.
Для задания прямоугольной области отсечения используется сервисная функция RDS rdsXGSetClipRect, которая принимает единственный параметр – указатель на структуру типа RECT, описывающую прямоугольник. RECT – стандартная структура Windows API, часто используемая в различных графических функциях – имеет четыре целых поля, задающих левый верхний (left,top) и правый нижний (right,bottom) углы прямоугольника. Мы будем задавать отсечение по прямоугольнику поля графика с отступом на одну точку внутрь поля, чтобы рисуемая линия не наложилась на его рамку. Таким образом, левым верхним углом области, в которой разрешено рисование, будет (x1+1,y1+1), а правым нижним – (x2-1,y2-1). Теперь можно рисовать линию графика, предварительно установив цвет линии (LineColor) и ее толщину. Толщина линии устанавливается с учетом текущего масштабного коэффициента, таким образом, в функцию rdsXGSetPenStyle в качестве толщины линии передается произведение заданной в параметрах блока толщины LineWidth и масштаба DrawData->DoubleZoom. Это приводит к тому, что при увеличении масштаба окна подсистемы линия графика будет становиться толще, а при уменьшении – тоньше. Если бы толщина линии не зависела от масштаба, в мелких масштабах графики с толстыми линиями становились бы нечитаемыми. Следует отметить, что, несмотря на то, что в мелких масштабах произведение LineWidth*DrawData->DoubleZoom может принимать значения, меньшие единицы, и при округлении этих значений до целого внутри функции rdsXGSetPenStyle будет получаться нулевое значение толщины линии, это не вызовет проблем. Установка нулевой толщины линии в сервисных функциях RDS приводит к рисованию линий толщиной в одну точку, то есть наиболее тонкой линии из возможных.
Согласно описанной выше логике работы блока, в любой момент времени массивы отсчетов графика будут заполнены данными до индекса NextIndex-1 включительно. Необходимо построить ломаную линию (Times[0],Values[0]) – (Times[1],Values[1]) – … – (Times[NextIndex-1],Values[NextIndex-1]), переводя вещественные значения из массивов Times и Values в целые координаты поля графика согласно диапазонам осей. Для этого используется цикл по целой переменной i, принимающей значения от 0 до NextIndex-1, внутри которого вычисляются целые координаты ix и iy очередной точки ломаной по формулам, аналогичным использованным при построении горизонтальной и вертикальной сеток графика. Для самой первой точки ломаной (при i равном 0) вызывается функция rdsXGMoveTo, для всех остальных – rdsXGLineTo. Таким образом, начиная со второй точки массива, каждая очередная точка будет соединяться линией с предыдущей.
После того, как цикл завершится, необходимо отменить использование области отсечения, вызвав функцию rdsXGSetClipRect с параметром NULL. На этом рисование графика заканчивается.
Следующие по важности после рисования – функции работы с массивами отсчетов. Рассмотрим сначала функцию отведения памяти под массивы, которая вызывается из функции модели блока SimplePlot при первом запуске расчета:
// Отведение памяти под массивы void TSimplePlotData::AllocateArrays(void) { // Сначала нужно очистить массивы, если они были отведены ранее ClearArrays(); // При нулевом или отрицательном шаге записи отсчетов // работа блока невозможна if(TimeStep<=0.0) return; // Вычисление требуемого числа отсчетов по диапазону оси // времени и шагу записи Count=(Xmax-Xmin)/TimeStep+1; // Число отсчетов должно быть положительным if(Count<=0) {Count=0; return; } // Отведение памяти – по Count чисел double Times=new double[Count]; Values=new double[Count]; // Первый свободный индкс массива - 0 NextIndex=0; // Момент записи отсчета – начало диапазона оси времени NextTime=Xmin; } //=========================================
Первое, что делает эта функция – освобождает память, занятую массивами, если она уже отведена. Это позволяет избежать утечек памяти если, по ошибке, мы вызовем функцию AllocateArrays два раза подряд. Затем вычисляется размер массива, необходимый для записи всего графика. Для этого диапазон оси времени графика (Xmax-Xmin) делится на шаг записи, и к получившемуся результату добавляется единица, чтобы в графике был один лишний отсчет, так как отсчетов на один больше, чем интервалов. Например, если мы хотим строить график с горизонтальным диапазоном 0…5 и шагом записи 1, нам потребуется массив на 6 отсчетов: 0, 1, 2, 3, 4, 5. Если вычисленный размер массива отрицателен или равен нулю, поле Count обнуляется и функция завершается без отведения массивов. Если же он положителен, он записывается в поле Count, после чего при помощи оператора C++ new отводится память под массивы Times и Values размером в Count вещественных чисел двойной точности. Затем инициализируются переменные NewIndex (следующий свободный индекс массивов) и NextTime (момент времени, после которого будет записан очередной отсчет). Поскольку оба массива полностью пусты, в NewIndex записывается 0 (запись будет производиться с начала массива), а в NewtTime – начало диапазона оси времени Xmin.
Теперь рассмотрим функцию освобождения памяти ClearArrays. Мы уже вызывали ее в двух местах (в деструкторе класса и в функции AllocateArrays), пришло время записать ее код. Он будет простым:
// Освобождение массивов void TSimplePlotData::ClearArrays(void) { if(Count) // Массивы были отведены { delete[] Times; delete[] Values; } // Обнуление указателей и Count Times=Values=NULL; Count=NextIndex=0; } //=========================================
Эта функция довольно проста: если значение поля Count не нулевое (мы договорились использовать его в качестве признака наличия массивов), память, на которую ссылаются указатели Times и Values, освобождается оператором delete[]. Затем обнуляются все поля класса, управляющие массивами отсчетов, включая Count.
Наконец, запишем функцию AddPoint, которая будет добавлять к массивам очередной отсчет, если пришло его время (общий принцип ее работы уже описан выше). Функция принимает единственный параметр – значение, которое, возможно, нужно добавить в массив Values – и вызывается из модели блока при любом изменении динамической переменной. Значение времени функция берет из динамической переменной «DynTime».
// Добавление отсчета в массив void TSimplePlotData::AddPoint(double v) { double t; if(NextIndex>=Count) // Весь массив заполнен return; if(Time==NULL || Time->Data==NULL) // Нет доступа к "DynTime" return; // Получение значения времени из "DynTime" t=*((double*)Time->Data); if(t<NextTime) // Еще не пришло время писать отсчет return; // Достигнуто время записи Values[NextIndex]=v; Times[NextIndex]=t; NextIndex++; // Следующий отсчет – в следующий индекс NextTime+=TimeStep; // Время записи следующего отсчета } //=========================================
Сначала в функции проверяется, не заполнен ли весь массив (эта же проверка сработает, если массивы отсчетов на отведены) и есть ли доступ к динамической переменной времени. Если доступ есть и в массиве еще есть место, значение текущего времени считывается во вспомогательную переменную t и сравнивается со временем записи следующего отсчета NextTime. Если t окажется меньше NextTime, значит, время записи очередного отсчета еще не пришло, и функция завершается. В противном случае значение, переданное функции, записывается в текущий элемент массива Values, а время – в текущий элемент массива Times. После этого текущий индекс массива увеличивается на 1, а время записи – на шаг записи TimeStep. Теперь функция готова к записи следующего отсчета, которая произойдет, как только значение времени превысит новое время записи.
Из функций-членов, объявленных в классе TSimplePlotData, остались не написанными только функции сохранения, загрузки и настройки параметров блока. Эти функции достаточно громоздки, поскольку у блока много параметров, но в них не будет ничего принципиально нового – подобные функции рассматривались в §2.7 и §2.8. Мы не будем разбирать их подробно по выполняемым действиям, обратим внимание только на некоторые ранее не встречавшиеся сервисные функции RDS, которые в них используются.
Функция сохранения параметров блока будет записывать их в текстовом виде в формате INI-файлов Windows:
// Сохранение параметров в текстовом виде void TSimplePlotData::SaveText(void) { RDS_HOBJECT ini; char *str; // Создание объекта для работы с текстом ini=rdsINICreateTextHolder(TRUE); // Создание в тексте секции "[General]" rdsSetObjectStr(ini,RDS_HINI_CREATESECTION,0,"General"); // Запись в секцию различных параметров rdsINIWriteDouble(ini,"TimeStep",TimeStep); rdsINIWriteDouble(ini,"Xmin",Xmin); rdsINIWriteDouble(ini,"Xmax",Xmax); rdsINIWriteDouble(ini,"XGridStep",XGridStep); rdsINIWriteInt(ini,"XNumDecimal",XNumDecimal); rdsINIWriteDouble(ini,"Ymin",Ymin); rdsINIWriteDouble(ini,"Ymax",Ymax); rdsINIWriteDouble(ini,"YGridStep",YGridStep); rdsINIWriteInt(ini,"YNumDecimal",YNumDecimal); // Создание в тексте секции "[Visuals]" rdsSetObjectStr(ini,RDS_HINI_CREATESECTION,0,"Visuals"); // Запись в секцию различных параметров rdsINIWriteInt(ini,"BorderColor",(int)BorderColor); rdsINIWriteInt(ini,"FillColor",(int)FillColor); rdsINIWriteInt(ini,"PlotBorderColor",(int)PlotBorderColor); rdsINIWriteInt(ini,"PlotFillColor",(int)PlotFillColor); rdsINIWriteInt(ini,"LineColor",(int)LineColor); rdsINIWriteInt(ini,"LineWidth",LineWidth); // Преобразование описания шрифта в строку для сохранения str=rdsStructToFontText(&Font,NULL); // Запись строки с описанием шрифта rdsINIWriteString(ini,"Font",str); rdsFree(str); // Освобождение памяти, занятой строкой // Запись сформированного текста в файл схемы или буфер обмена rdsCommandObject(ini,RDS_HINI_SAVEBLOCKTEXT); // Уничтожение вспомогательного объекта rdsDeleteObject(ini); } //=========================================
Эта функция устроена так же, как и функция сохранения параметров в примере в §2.8.5: создается вспомогательный объект для работы с текстом, в нем создаются секции, в них записываются параметры, после чего сформированный текст передается в RDS для записи и вспомогательный объект уничтожается. Следует отметить только два новых момента: во-первых, все параметры, описывающие цвета различных элементов изображения, приводятся к типу int и заносятся в текст как целые числа функцией rdsINIWriteInt. Тип COLORREF, используемый в Windows API для хранения цветов, допускает такое преобразование. Во-вторых, параметры шрифта, которые хранятся в структуре Font, сохраняются не по отдельности, а преобразуются в одну строку описания шрифта при помощи сервисной функции rdsStructToFontText. Эта функция формирует динамическую строку, в которой параметры шрифта перечислены после стандартных ключевых слов. Например, для шрифта, заданного в конструкторе класса, будет сформирована строка
font "Arial" height 15 charset 1 color 0
Поскольку строка, которую возвращает функция, отводится в динамической памяти, после использования ее необходимо освободить функцией rdsFree, как и все динамические строки, используемые в RDS.
Функция загрузки параметров блока тоже выглядит знакомо:
// Загрузка параметров в текстовом виде из строки text void TSimplePlotData::LoadText(char *text) { RDS_HOBJECT ini; char *str; // Создание объекта для работы с текстом ini=rdsINICreateTextHolder(TRUE); // Загрузка текста в объект rdsSetObjectStr(ini,RDS_HINI_SETTEXT,0,text); // Если в тексте есть секция "General", загрузить из нее данные if(rdsINIOpenSection(ini,"General")) { TimeStep=rdsINIReadDouble(ini,"TimeStep",TimeStep); Xmin=rdsINIReadDouble(ini,"Xmin",Xmin); Xmax=rdsINIReadDouble(ini,"Xmax",Xmax); XGridStep=rdsINIReadDouble(ini,"XGridStep",XGridStep); XNumDecimal=rdsINIReadInt(ini,"XNumDecimal",XNumDecimal); Ymin=rdsINIReadDouble(ini,"Ymin",Ymin); Ymax=rdsINIReadDouble(ini,"Ymax",Ymax); YGridStep=rdsINIReadDouble(ini,"YGridStep",YGridStep); YNumDecimal=rdsINIReadInt(ini,"YNumDecimal",YNumDecimal); } // Если в тексте есть секция "Visuals", загрузить из нее данные if(rdsINIOpenSection(ini,"Visuals")) { BorderColor=(COLORREF)rdsINIReadInt(ini,"BorderColor", (int)BorderColor); FillColor=(COLORREF)rdsINIReadInt(ini,"FillColor", (int)FillColor); PlotBorderColor=(COLORREF)rdsINIReadInt(ini, "PlotBorderColor",(int)PlotBorderColor); PlotFillColor=(COLORREF)rdsINIReadInt(ini,"PlotFillColor", (int)PlotFillColor); LineColor=(COLORREF)rdsINIReadInt(ini,"LineColor", (int)LineColor); LineWidth=(COLORREF)rdsINIReadInt(ini,"LineWidth",LineWidth); str=rdsINIReadString(ini,"Font","",NULL); if(str) rdsFontTextToStruct(str,NULL,&Font); } // Уничтожение вспомогательного объекта rdsDeleteObject(ini); } //=========================================
Точно так же, как и функция сохранения параметров, эта функция приводит все данные типа COLORREF к типу int и работает с цветами как с целыми числами. Для разбора сохраненной строки параметров шрифта используется сервисная функция rdsFontTextToStruct, которая по этой строке заполняет отдельные поля структуры Font. Функция rdsINIReadString, которая считывает строку из вспомогательного объекта, не отводит память под новую строку, а возвращает указатель на строку из своего внутреннего буфера, поэтому здесь не требуется освобождать какую-либо память функцией rdsFree.
Функция настройки параметров будет создавать окно с двумя вкладками: «» и «», на которых встретится несколько не использовавшихся ранее типов полей ввода:
// Функция настройки параметров блока // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" int TSimplePlotData::Setup(void) { RDS_HOBJECT window; BOOL ok; char *str; // Создание окна window=rdsFORMCreate(TRUE,-1,-1,"Простой график"); // Вкладка "Оси" rdsFORMAddTab(window,1,"Оси"); rdsFORMAddEdit(window,1,100, RDS_FORMCTRL_EDIT | RDS_FORMFLAG_LINE,"Шаг записи",50); rdsSetObjectDouble(window,100,RDS_FORMVAL_VALUE,TimeStep); // Текстовая метка без возможности ввода rdsFORMAddEdit(window,1,1,RDS_FORMCTRL_LABEL,"Ось X:",0); // Диапазон (два поля ввода в одной строке) rdsFORMAddEdit(window,1,2,RDS_FORMCTRL_RANGEEDIT,"Диапазон",90); rdsSetObjectDouble(window,2,RDS_FORMVAL_VALUE,Xmin); rdsSetObjectDouble(window,2,RDS_FORMVAL_RANGEMAX,Xmax); rdsFORMAddEdit(window,1,3,RDS_FORMCTRL_EDIT,"Шаг сетки",50); rdsSetObjectDouble(window,3,RDS_FORMVAL_VALUE,XGridStep); // Поле ввода со стрелками увеличения/уменьшения rdsFORMAddEdit(window,1,4, RDS_FORMCTRL_UPDOWN | RDS_FORMFLAG_LINE, "Дробная часть чисел",50); rdsSetObjectInt(window,4,RDS_FORMVAL_VALUE,XNumDecimal); rdsSetObjectInt(window,4,,0); rdsSetObjectInt(window,4,,5); rdsSetObjectInt(window,4,,1); // Текстовая метка без возможности ввода rdsFORMAddEdit(window,1,5,RDS_FORMCTRL_LABEL,"Ось Y:",0); rdsFORMAddEdit(window,1,6,RDS_FORMCTRL_RANGEEDIT,"Диапазон",90); rdsSetObjectDouble(window,6,RDS_FORMVAL_VALUE,Ymin); rdsSetObjectDouble(window,6,RDS_FORMVAL_RANGEMAX,Ymax); rdsFORMAddEdit(window,1,7,RDS_FORMCTRL_EDIT,"Шаг сетки",50); rdsSetObjectDouble(window,7,RDS_FORMVAL_VALUE,YGridStep); rdsFORMAddEdit(window,1,8,RDS_FORMCTRL_UPDOWN, "Дробная часть чисел",50); rdsSetObjectInt(window,8,RDS_FORMVAL_VALUE,YNumDecimal); rdsSetObjectInt(window,8,RDS_FORMVAL_UPDOWNMIN,0); rdsSetObjectInt(window,8,RDS_FORMVAL_UPDOWNMAX,5); rdsSetObjectInt(window,8,RDS_FORMVAL_UPDOWNINC,1); // Вкладка "Внешний вид" rdsFORMAddTab(window,2,"Внешний вид"); rdsFORMAddEdit(window,2,9,RDS_FORMCTRL_COLOR, "Цвет рамки блока",50); rdsSetObjectInt(window,9,RDS_FORMVAL_VALUE,(int)BorderColor); rdsFORMAddEdit(window,2,10, RDS_FORMCTRL_COLOR | RDS_FORMFLAG_LINE,"Цвет фона блока",50); rdsSetObjectInt(window,10,RDS_FORMVAL_VALUE,(int)FillColor); rdsFORMAddEdit(window,2,11,RDS_FORMCTRL_COLOR, "Цвет рамки графика и сетки",50); rdsSetObjectInt(window,11,RDS_FORMVAL_VALUE, (int)PlotBorderColor); rdsFORMAddEdit(window,2,12,RDS_FORMCTRL_COLOR, "Цвет фона графика",50); rdsSetObjectInt(window,12,RDS_FORMVAL_VALUE,(int)PlotFillColor); rdsFORMAddEdit(window,2,13,RDS_FORMCTRL_COLOR, "Цвет линии графика",50); rdsSetObjectInt(window,13,RDS_FORMVAL_VALUE,(int)LineColor); rdsFORMAddEdit(window,2,14, RDS_FORMCTRL_UPDOWN | RDS_FORMFLAG_LINE, "Толщина линии графика",50); rdsSetObjectInt(window,14,RDS_FORMVAL_VALUE,LineWidth); rdsSetObjectInt(window,14,RDS_FORMVAL_UPDOWNMIN,0); rdsSetObjectInt(window,14,RDS_FORMVAL_UPDOWNMAX,5); rdsSetObjectInt(window,14,RDS_FORMVAL_UPDOWNINC,1); // Кнопка открытия диалога выбора шрифта rdsFORMAddEdit(window,2,15,RDS_FORMCTRL_FONTSELECT, "Шрифт чисел",0); // Преобразование шрифта в строку и занесение в поле ввода str=rdsStructToFontText(&Font,NULL); rdsSetObjectStr(window,15,RDS_FORMVAL_VALUE,str); rdsFree(str); // Открытие окна ok=rdsFORMShowModalEx(window,NULL); if(ok) { // Нажата кнопка OK - запись параметров обратно в блок Xmin=rdsGetObjectDouble(window,2,RDS_FORMVAL_VALUE); Xmax=rdsGetObjectDouble(window,2,RDS_FORMVAL_RANGEMAX); XGridStep=rdsGetObjectDouble(window,3,RDS_FORMVAL_VALUE); XNumDecimal=rdsGetObjectInt(window,4,RDS_FORMVAL_VALUE); TimeStep=rdsGetObjectDouble(window,100,RDS_FORMVAL_VALUE); Ymin=rdsGetObjectDouble(window,6,RDS_FORMVAL_VALUE); Ymax=rdsGetObjectDouble(window,6,RDS_FORMVAL_RANGEMAX); YGridStep=rdsGetObjectDouble(window,7,RDS_FORMVAL_VALUE); YNumDecimal=rdsGetObjectInt(window,8,RDS_FORMVAL_VALUE); BorderColor=(COLORREF)rdsGetObjectInt(window,9,RDS_FORMVAL_VALUE); FillColor=(COLORREF)rdsGetObjectInt(window,10,RDS_FORMVAL_VALUE); PlotBorderColor=(COLORREF)rdsGetObjectInt(window,11,RDS_FORMVAL_VALUE); PlotFillColor=(COLORREF)rdsGetObjectInt(window,12,RDS_FORMVAL_VALUE); LineColor=(COLORREF)rdsGetObjectInt(window,13,RDS_FORMVAL_VALUE); LineWidth=rdsGetObjectInt(window,14,RDS_FORMVAL_VALUE); // Получение параметров шрифта из строки str=rdsGetObjectStr(window,15,RDS_FORMVAL_VALUE); rdsFontTextToStruct(str,NULL,&Font); } // Уничтожение окна rdsDeleteObject(window); // Возвращаемое значение return ok?RDS_BFR_MODIFIED:RDS_BFR_DONE; } //=========================================
Как и функции сохранения и загрузки параметров, эта функция работает с цветами как с целыми числами. Для задания цвета используется специальный тип поля ввода RDS_FORMCTRL_COLOR, выглядящий как кнопка с цветным прямоугольником. При нажатии на эту кнопку открывается стандартный диалог Windows для выбора цвета. Для визуального отделения параметров горизонтальной и вертикальной осей друг от друга на вкладке «» использованы текстовые метки (RDS_FORMCTRL_LABEL), которые отображают названия осей и никак не реагируют на действия пользователя. Также для большей наглядности и удобства ввода диапазоны осей Xmin…Xmax и Ymin…Ymax задаются в специальных двойных полях ввода (тип RDS_FORMCTRL_RANGEEDIT), в которых в одной строке задаются начало и конец диапазона. Толщина линии графика вводится в поле ввода со стрелками для увеличения и уменьшения значения (RDS_FORMCTRL_UPDOWN) с заданием максимального и минимального возможного значения. Наконец, для задания шрифта используется специальная кнопка (RDS_FORMCTRL_FONTSELECT), нажатие на которую открывает стандартный диалог выбора шрифта Windows. Для работы с этим полем-кнопкой параметры шрифта переводятся в строку уже знакомой нам функцией rdsStructToFontText, а при закрытии окна кнопкой «» заносятся обратно в структуру Font функцией rdsFontTextToStruct. Внешний вид обеих вкладок окна настройки блока приведен на рис. 60.

(а)

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