Главная » C++, C++ Builder » Компонент LineGraph в CBuilder

0

 

Windows — это графическая операционная система, и поэтому программы, написанные под нее, являются графическими по определению. Одной из наиболее часто используемых графических возможностей является графическое отображение данных — в виде графиков, диаграмм и т. п. CBuilder предоставляет компонент ActiveX, который осуществляет работу с графиками, но он страдает от двух больших недостатков. Во-первых, это компонент ActiveX, что значит, что вам придется поставлять его отдельно от вашего приложения, инсталлировать на пользовательской машине и регистрировать в операционной  системе.  Во-вторых,  компонент  VCFormulaOne слишком громоздок для большинства приложений. Когда вы хотите лишь построить несколько простеньких графиков, вам не нужны трехмерные эффекты, символы в каждой точке, подписанные оси, и тому подобные излишества. То, что вам действительно надо — это простой и бесхитростный графический компонент.

Давайте займемся разработкой такого графического компонента.

Формулировка  проблемы

Цель нашего компонента — позволить пользователю рисовать сравниваемые графики для наборов данных, с которыми он работает. Этот компонент должен предоставлять возможности: масштабировать данные в соответствии с границами, задаваемыми пользователем; рисовать разные линии разными цветами; выводить на экран некоторое количество пометок. Главная цель компонента — позволить пользователю сравнивать данные, так что он должен поддерживать отображение нескольких графиков одновременно, кроме того, он должен сохранять информацию при всех перемещениях и изменениях размера.

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

Частное решение

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

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

Определение свойств компонента

После того, как проблема, которую разрешает компонент, определена, наступает следующий этап

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

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

Компонент, который мы собираемся создать, будет называться LibeGraph. При помощи Мастера компонентов CBuilder создайте новый компонент с таким именем, наследующий от класса TCustomControl. Мы используем этот класс, поскольку нам не понадобится от него ничего, кроме основных возможностей работы с окнами, но нам надо, чтобы у нашего компонента было свой поле (Canvas), на котором мы будет рисовать графики. Класс TCustomControl — это основной компонент, предоставляющий возможности работы с окнами в системе VCL.

Добавление свойств в компонент

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

public:

__property Graphics::TColor LineColors[int nIndex] =

{read=GetLineColor,  write=SetLineColor};

__property double XPoint[int nLine][int Index] =

{read=GetXPoint,  write=SetXPoint};

__property double YPoint[int nLine][int Index] =

{read=GetYPoint,  write=SetYPoint};

__published

__property int NumberXTicks =

{read=FNumXTicks, write=FNumXTicks, default=5};

__property int NumberYTicks =

{read=FNumYTicks, write=FNumYTicks, default=5};

__property double XStart = {read=FXStart, write=FXStart};

__property double YStart = {read=FYStart, write=FYStart};

__property double XIncrement = {read=FXInc, write=FXInc};

__property double YIncrement = {read=FYInc, write=FYInc};

__property int NumberOfPoints =

{read=FNumPoints, write=SetNumberOfPoints,  default=0};

__property bool XGrid = {read=FXGrid, write=FXGrid, default=true};

__property bool YGrid = {read=FYGrid, write=FYGrid,

default=true};

__property int NumXDecimals =

{read=FXNumDecimals, write=FXNumDecimals,  default=2};

__property int NumYDecimals =

{read=FYNumDecimals, write=FYNumDecimals,  default=2};

__property int NumberOfLines =

{read=FNumberOfLines,  write=SetNumberOfLines,  default=2};

Как  вы,  наверное,  отметили,  список  получился  довольно  длинный.  У  компонента  LineGraph

довольно  много  возможностей,  поэтому и  свойств  у него  достаточно  много.  Еще  вы  конечно

обратили  внимание  на  то,  что  не  все  свойства  компонента  сделаны свойства перечислены в секции public. Зачем нам это понадобилось?

__published.   Некоторые

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

Давайте для начала рассмотрим как раз свойства-массивы. У нас есть три таких свойства — для X- координат точек графиков, Y-координат точек графиков и цветов различных линий графиков. Координаты точек по X и по Y это двумерные массивы, поскольку мы рассматриваем линии как массивы точек. Так что эти два свойства это массивы линий, которые являются массивами точек. Цвета представлены одномерным массивом, поскольку каждой линии графика соответствует единственный цвет.

Как вы будете использовать эти точки в вашем коде? Вы будете их обращаться к ним как к массивам. В общем случае, вы будете использовать в своих приложениях примерно такую запись:

pLineGraph->XPoints[0][0] = 10.0;

pLineGraph->YPoints[0][0] = 10.0;

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

pLineGraph->LineColors[0] = clRed;

Замечание

В CBuilder индексы в массивах не обязательно должны быть целыми значениями, как это было в C и C++. Вы можете также использовать строки, объекты, или что-нибудь еще по своему желанию. Это является основой свойства FieldValue в классе компонента TDataSet.

Остальные свойства, как нетрудно догадаться,  также  относятся  к  графику.  Свойства NumberXTicks и NumberXTicks относятся к количеству отметок (делений), используемых для осей графика. Значения XStart и XStart представляют собой начало осей X и Y, а значения XIncrement и XIncrement являют размер «шага» осей, то есть расстояния между делениями на осях.

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

Предназначение большинства остальных свойств явно вытекает из их названий. Единственное, с чем может возникнуть небольшое затруднение — со свойствами XGrid и YGrid. Если эти свойства имеют значение «истина», то график будет изображен с наложенной на него сеткой (состоящей из горизонтальных и/или вертикальных линий). Если эти свойства установлены в значение «ложь» (одно или оба), линии не будут рисоваться. По-моему, график с наложенной на него сеткой воспринимается куда лучше, и поэтому значения этих свойств по умолчанию есть true (истина).

Воплощение управляющего элемента

После того, как свойства управляющего элемента определены, начинается следующая стадия разработки — его воплощение. Давайте начнем  с  определения  переменных-членов  класса, которые нам потребуются для воплощения свойств.

В случае свойств, относящихся к точкам и цвету, нам потребуется некие массивы для хранения данных. Это однозначно вытекает из определения этих свойств. Однако совершенно непонятным остается вопрос о том, насколько велик должен быть массив для того, чтобы хранить все данные. Откуда мы можем знать, насколько большие массивы точек захочет завести пользователь? Одни могут ограничиться десятком точек, другим же и тысячи может быть мало. Для того, чтобы у нас получился действительно полезный компонент, мы должны  избегать  конструктивных ограничений при проектировании и воплощении его до тех пор, пока такие ограничения не станут абсолютно необходимыми по логике. К счастью, ранее в книге мы уже говорили о библиотеке стандартных шаблонов (Standard Template Library, STL), библиотеке,  которая  предоставляет  в наше распоряжение структуры данных, среди которых есть и массивы, у которых нет фиксированного размера и которые могут наращиваться динамически во время исполнения. Мы используем класс vector из STL для хранения данных о точках в нашем компоненте.

Давайте посмотрим на изменения, которые необходимо внести в заголовочный файл для описания переменных-членов класса, описывающих свойства:

typedef std::vector<double> DblArray; class LineGraph : public TCustomControl

{

private:

int FNumberOfLines; int FNumXTicks;

int FNumYTicks;

double FXStart; double FYStart; double FXInc; double FYInc; int FNumPoints;

std::vector<DblArray>  FXPoints; std::vector<DblArray>  FYPoints; std::vector<Graphics::TColor>  FLineColors; bool FXGrid;

bool FYGrid;

int FXNumDecimals; int FYNumDecimals;

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

vector < vector<double> > FXPoints;

Это вызвало бы проблемы с использованием большого количества скобок  (<>).  Вместо  того, чтобы каждый раз писать эти скобки, мы описываем тип vector как новый тип DblArray, который является гибким массивом вещественных значений с двойной точностью.

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

__fastcall LineGraph::LineGraph(TComponent* Owner)

: TCustomControl(Owner)

{

// Инициализируем свойства компонента

FNumXTicks = 5;

FNumYTicks = 5;

FXStart = 0.0;

FYStart = 0.0;

FXInc = 10.0;

FYInc = 10.0;

FNumPoints = 0;

FNumberOfLines = 0;

// По умолчанию график отображается

// с наложенной на него координатной сеткой

FXGrid = true; FYGrid = true;

FXNumDecimals = 2;

FYNumDecimals = 2;

}

Как вы видите, в этом куске кода мы просто присваиваем резонные значения всем переменным класса. А что же с массивами точек и цветов? Они не могут быть инициализированы в конструкторе класса, поскольку мы не знаем их размеров. Когда же мы это узнаем? Когда пользователь установит количество линий и количество точек. Если вы вернетесь назад и посмотрите описание свойств NumberOfLines и NumberOfPoints, то увидите, что они  оба используют переменную для чтения и функцию для записи значений. Функции используются из- за того, что при изменении значений этих свойств возникают некие побочные эффекты. Здесь в очередной раз проявляется мощь свойств. Несмотря на то, что пользователь не подозревает (или, во всяком случае, его это не волнует) о том, что при изменении количества линий или точек где-то на заднем плане происходит выделение памяти, это тем не менее происходит. Вот как выглядят функции изменения значений этих свойств:

void __fastcall LineGraph::SetNumberOfLines( int nLines )

{

// Устанавливаем количество точек

// по X и по Y FXPoints.reserve( nLines ); FYPoints.reserve( nLines );

// Устанавливаем количество цветов

FLineColors.reserve( nLines );

// Цвет всех линий изначально

// устанавливаем в черный for ( int i=0; i<nLines; ++i ) FLineColors[i] = clBlack;

// Сохраняем количество линий

FNumberOfLines = nLines;

}

void __fastcall LineGraph::SetNumberOfPoints( int nPoints )

{

// Устанавливаем количество точек

// по X и по Y для каждой линии

for ( int i=0; int<FNumberOfLines; ++i )

{

FXPoints[i].reserve( nPoints ); FYPoints[i].reserve( nPoints );

// На всякий случай устанавливаем

// все точки в значение 0.0

for ( int nPt=0; nPt<nPoints; ++nPt )

{

FXPoints[i][nPt] = 0.0;

FYPoints[i][nPt] = 0.0;

}

}

// Сохраняем количество точек

FNumPoints = nPoints;

}

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

double __fastcall LineGraph::GetXPoints( int nLine, int nIndex )

{

return  FXPoints[nLine][nIndex];

}

void __fastcall LineGraph::SetXPoint( int nLine, int nIndex, double dPoint )

{

if ( nLine >= FXPoints.size() )

FXPoints.insert( FXPoints.end(), DblArray() );

FXPoints[ nLine ].inset(FXPoints[nLine].end(),dPoint);

}

double __fastcall LineGraph::GetYPoints( int nLine,

int nIndex )

{

return  FYPoints[nLine][nIndex];

}

void __fastcall LineGraph::SetYPoint( int nLine, int nIndex, double dPoint )

{

if ( nLine >= FYPoints.size() )

FYPoints.insert( FYPoints.end(), DblArray() );

FYPoints[ nLine ].inset(FYPoints[nLine].end(),dPoint);

}

void __fastcall LineGraph::SetLineColor( int nIndex, Graphics::TColor  clrNewColor)

{

FLineColors[ nIndex ] = clrNewColor;

}

Graphics::TColor __fastcall LineGraph::GetLineColor

( int nIndex )

{

return FLineColors[ nIndex ];

}

Вполне понятно, как написан этот код, но гораздо интереснее, как он используется. Когда у вас есть свойство-массив (то есть определенное как свойство[индекс]) ваши функции Get… должны получать по параметру для каждого индекса массива. Если массив одномерный, как в случае со свойством цвета линии, функция Get… имеет один параметр — индекс возвращаемого значения. Для двумерных массивов функции Get… имеют два параметра, и так далее.

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

pLineGraph->Xpoints[nLine][nPt] = x;

Предыдущая строка кода преобразуется в следующий вызов: pLineGraph->SetXPoints(nLine, nPt, x);

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

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

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

void __fastcall LineGraph::DoPaint(TCanvas *pCanvas)

{

int nYStart = Top + 20; int nXStart = 50;

int RightMargin = 40;

// Рисуем оси графика

pCanvas->MoveTo( nXStart, nYStart); pCanvas->LineTo( nXStart, Height-30 );

pCanvas->LineTo( Width-RightMargin,  Height-30);

// Наносим риски на оси

// Сначала горизонтальные

if (FNumXTicks > 0)

{

// Определяем промежутки

int nSpaceTicks = (Width-nXStart-RightMargin)

/ FNumXTicks; double xVal = FXStart;

for ( int i=0; i<=FNumXTicks; ++i )

{

pCanvas->MoveTo(  nXStart+(i*nSpaceTicks),Height-30); pCanvas->LineTo(  nXStart+(i*nSpaceTicks),Height-25);

// Метим риски

char szBuffer[ 20 ];

sprintf(szBuffer,  "%5.*lf",FXNumDecimals,xVal);

// Получаем ширину строки …

int nWidth = pCanvas->TextWidth( szBuffer );

// и помещаем ее в надлежащее место

pCanvas->Brush->Color = Color;

pCanvas->TextOut(nXStart+(i*nSpaceTicks)-nWidth/2, Height-20, szBuffer );

// Увеличиваем значение

xVal += FXInc;

// Если сетка требуется, отображаем ее

if ( FXGrid )

{

pCanvas->MoveTo(  nXStart+(i*nSpaceTicks),

nYStart );

pCanvas->LineTo(  nXStart+(i*nSpaceTicks), Height-30 );

}

}

}

// Теперь вертикальные

if (FNumYTicks > 0)

{

double yVal = FYStart;

// Определяем промежутки

int nSpaceTicks = (Height-30-nYStart) / FNumYTicks;

for ( int i=0; i<=FNumYTicks; ++i )

{

int nYPos = Height-30-(i*nSpaceTicks); pCanvas->MoveTo( nXStart-5, nYPos ); pCanvas->LineTo( nXStart, nYPos );

// Метим риски

char szBuffer[ 20 ];

sprintf(szBuffer,  "%5.*lf",FYNumDecimals,yVal);

// Получаем ширину строки …

int nWidth = pCanvas->TextWidth( szBuffer ); int nHeight = pCanvas->TextHeight( szBuffer );

// и помещаем ее в надлежащее место pCanvas->Brush->Color = Color; pCanvas->TextOut(nXStart-nWidth-7,

nYPos-nHeight/2, szBuffer );

// Увеличиваем значение

yVal += FXInc;

// Если требуется сетка, отображаем ее

if ( FYGrid )

{

pCanvas->MoveTo( nXStart, nYPos );

pCanvas->LineTo( Width-RightMargin, nYPos );

}

}

}

// Рисуем линии, соединяющие точки

if ( FNumPoints > 0 )

{

for ( int nLine = 0; nLine < FXPoints.size(); ++nLine )

{

// Устанавливаем цвета для этой линии

pCanvas->Pen->Color = FLineColors[ nLine ];

// Переводим в экранные единицы

int nXPos = XPointToScreen(FXPoints[nLine][0]); int nYPos = YPointToScreen(FYPoints[nLine][0]); for ( int i=1; i<NumberOfPoints; ++i )

{

pCanvas->MoveTo(nXPos,  nYPos);

nXPos = XPointToScreen(FXPoints[nLine][i]); nYPos = YPointToScreen(FYPoints[nLine][i]); pCanvas->LineTo(nXPos,  nYPos);

}

}

// Сбрасываем цвета

pCanvas->Pen->Color = clBlack;

}

}

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

void __fastcall LineGraph::Paint(void)

{

DoPaint(Canvas);

}

void __fastcall LineGraph::Print(void)

{

TPrinter *pPrinter; pPrinter = new TPrinter(); pPrinter->BeginDoc();

DoPaint(pPrinter->Canvas); pPrinter->EndDoc();

delete pPrinter;

}

На самом деле у нас нет особых причин создавать новый объект TPrinter. Мы могли бы запросто использовать в приведенном выше методе Print функцию Printer() вместо объекта printer. Наконец, пришло время представить две последние функции в нашем компоненте — для преобразования точек данных в точки дисплея. Вот они, во всей красе:

int __fastcall LineGraph::XPointToScreen(double pt)

{

int rightMargin = 40; int nXStart = 50;

// Рассчитываем ширину экрана

int nSpaceTicks = (Width-nXStart-RightMargin)

/ FNumXTicks;

int nNumPixels = nSpaceTicks * FNumXTicks;

// Рассчитываем ширину данных

double dWidth = (FNumXTicks * FXInc) – FXStart;

// Рассчитываем, какую часть экрана занимают данные

double dPercent = (pt-FXStart) / dWidth;

// Теперь переводим это в пикселы

int nX = dPercent * nNumPixels;

// Готово! Теперь откладываем это от начала

nX = nXStart + nX;

return nX;

}

//—————————————————–

int __fastcall LineGraph::YPointToScreen(double pt)

{

int nYStart = Top + 20;

// Рассчитываем ширину экрана

int nSpaceTicks = (Height-30-nYStart) / FNumYTicks; int nNumPixels = nSpaceTicks * FNumYTicks;

// Рассчитываем ширину данных

double dWidth = (FNumYTicks * FYInc) – FYStart;

// Рассчитываем, какую часть экрана занимают данные

double dPercent = (pt-FYStart) / dHeight;

// Теперь переводим это в пикселы

int nY = dPercent * nNumPixels;

// Готово! Теперь откладываем это от начала

nY = nYStart + nY;

return nY; last>}

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

Рис. 14.5. Компонент LineGraph в действии

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

Источник: Теллес М. – Borland C++ Builder. Библиотека программиста – 1998

По теме:

  • Комментарии