Компонент LineGraph в CBuilder

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-&gtXPoints[0][0] = 100

pLineGraph-&gtYPoints[0][0] = 100

Представлені рядки коду описують одну точку на графіку з координатами 10,10 – першу точку першої лінії Для того, щоб встановити колір, ви повинні просто встановити його для бажаної лінії по індексу Наприклад, для того, щоб встановити колір першої лінії в червоний, ви повинні написати наступний рядок коду:

pLineGraph-&gtLineColors[0] = clRed

У CBuilder індекси в масивах не обовязково повинні бути цілими значеннями, як це було в C і C + + Ви можете також використовувати рядки, обєкти, або що-небудь ще за своїм бажанням Це є основою властивості FieldValue в класі компонента TDataSet

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

Коли ви будете визначати точки на графіку, вам буде потрібно два блоки інформації Властивість NumberOfPoints зберігає сумарна кількість точок, що виводяться для кожної лінії, а властивість NumberOfLines представляє собою кількість ліній на графіку Іншими словами, графік буде двовимірної сіткою ліній (у кількості NumberOfLines), які з точок в кількості NumberOfPoints Якщо ви не хочете обмежувати користувача в можливій кількості точок для ліній, буде не так важко надати йому можливість самостійно задавати ці значення, що ми побачимо, коли почнемо безпосередньо втілювати компонент

Призначення більшості інших властивостей явно випливає з їхніх назв Єдине, з чим може виникнути невелике утруднення – з властивостями XGrid і YGrid Якщо ці властивості мають значення «істина», то графік буде зображений з накладеною на нього сіткою (що складається з горизонтальних і / або вертикальних ліній) Якщо ці властивості встановлені в значення «брехня» (одне або обидва), лiнiї не будуть малюватися По-моєму, графік з накладеною на нього сіткою сприймається куди краще, і тому значення цих властивостей за замовчуванням є true (істина)

Після того, як властивості керуючого елемента визначені, починається наступна стадія розробки – його втілення Давайте почнемо з визначення змінних-членів класу, які нам будуть потрібні для втілення властивостей

У разі властивостей, що відносяться до точок і кольором, нам буде потрібно якісь масиви для зберігання даних Це однозначно випливає із визначення цих властивостей Проте абсолютно незрозумілим залишається питання про те, наскільки великий повинен бути масив для того, щоб зберігати всі дані Звідки ми можемо знати, наскільки великі масиви точок захоче завести користувач Одні можуть обмежитися десятком точок, іншим ж і тисячі може бути мало Для того, щоб у нас вийшов дійсно корисний компонент, ми повинні уникати конструктивних обмежень при проектуванні та втіленні його до тих пір, поки такі обмеження не стануть абсолютно необхідними за логікою На щастя, раніше в книзі ми вже говорили про бібліотеку стандартних шаблонів (Standard Template Library, STL), бібліотеці, яка надає в наше розпорядження структури даних, серед яких є і масиви, у яких немає фіксованого розміру і які можуть нарощуватися динамічно під час виконання Ми використовуємо клас vector з STL для зберігання даних про точки в нашому компоненті

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

typedef std::vector&ltdouble&gt DblArray class LineGraph : public TCustomControl

{

private:

int FNumberOfLines int FNumXTicks

int FNumYTicks

double FXStart double FYStart double FXInc double FYInc int FNumPoints

std::vector&ltDblArray&gt  FXPoints std::vector&ltDblArray&gt  FYPoints std::vector&ltGraphics::TColor&gt  FLineColors bool FXGrid

bool FYGrid

int FXNumDecimals int FYNumDecimals

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

vector &lt vector&ltdouble&gt &gt FXPoints

Це викликало б проблеми з використанням великої кількості дужок (<>) Замість того, щоб кожен раз писати ці дужки, ми описуємо тип vector як новий тип DblArray, який є гнучким масивом речових значень з подвійною точністю

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

__fastcall LineGraph::LineGraph(TComponent* Owner)

: TCustomControl(Owner)

{

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

FNumXTicks = 5

FNumYTicks = 5

FXStart = 00

FYStart = 00

FXInc = 100

FYInc = 100

FNumPoints = 0

FNumberOfLines = 0

/ / За замовчуванням графік відображається

/ / З накладеною на нього координатної сіткою

FXGrid = true FYGrid = true

FXNumDecimals = 2

FYNumDecimals = 2

}

Як ви бачите, в цьому шматку коду ми просто присвоюємо резонні значення всім змінним класу А що ж з масивами точок і квітів Вони не можуть бути ініціалізовані в конструкторі класу, оскільки ми не знаємо їхніх розмірів Коли ж ми це дізнаємося Коли користувач встановить кількість ліній і кількість точок Якщо ви повернетеся назад і подивіться опис властивостей NumberOfLines і NumberOfPoints, то побачите, що вони обидва використовують змінну для читання і функцію для запису значень Функції використовуються з-за того, що при зміні значень цих властивостей виникають якісь побічні ефекти Тут в черговий раз проявляється міць властивостей Незважаючи на те, що користувач не підозрює (або, у всякому разі, його це не хвилює) про те, що при зміні кількості ліній або точок десь на задньому плані відбувається виділення памяті, це проте відбувається Ось як виглядають функції зміни значень цих властивостей:

void __fastcall LineGraph::SetNumberOfLines( int nLines )

{

/ / Встановлюємо кількість точок

/ / По X і по Y FXPointsreserve (nLines) FYPointsreserve (nLines)

/ / Встановлюємо кількість квітів

FLineColorsreserve( nLines )

/ / Колір всіх ліній спочатку

/ / Встановлюємо в чорний for (int i = 0 i

/ / Зберігаємо кількість ліній

FNumberOfLines = nLines

}

void __fastcall LineGraph::SetNumberOfPoints( int nPoints )

{

/ / Встановлюємо кількість точок

/ / По X і по Y для кожної лінії

for ( int i=0 int&ltFNumberOfLines ++i )

{

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

/ / На всякий випадок встановлюємо

/ / Всі крапки в значення 00

for ( int nPt=0 nPt&ltnPoints ++nPt )

{

FXPoints[i][nPt] = 00

FYPoints[i][nPt] = 00

}

}

/ / Зберігаємо кількість точок

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 &gt= FXPointssize() )

FXPointsinsert( FXPointsend(), 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 &gt= FYPointssize() )

FYPointsinsert( FYPointsend(), 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-&gtXpoints[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-&gtMoveTo( nXStart, nYStart) pCanvas-&gtLineTo( nXStart, Height-30 )

pCanvas-&gtLineTo( Width-RightMargin,  Height-30)

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

/ / Спочатку горизонтальні

if (FNumXTicks &gt 0)

{

/ / Визначаємо проміжки

int nSpaceTicks = (Width-nXStart-RightMargin)

/ FNumXTicks double xVal = FXStart

for ( int i=0 i&lt=FNumXTicks ++i )

{

pCanvas-&gtMoveTo(  nXStart+(i*nSpaceTicks),Height-30) pCanvas-&gtLineTo(  nXStart+(i*nSpaceTicks),Height-25)

/ / Мітимо ризики

char szBuffer[ 20 ]

sprintf(szBuffer,  &quot%5*lf&quot,FXNumDecimals,xVal)

/ / Отримуємо ширину рядка ..

int nWidth = pCanvas-&gtTextWidth( szBuffer )

/ / І поміщаємо її в належне місце

pCanvas-&gtBrush-&gtColor = Color

pCanvas-&gtTextOut(nXStart+(i*nSpaceTicks)-nWidth/2, Height-20, szBuffer )

/ / Збільшуємо значення

xVal += FXInc

/ / Якщо сітка потрібна, відображаємо її

if ( FXGrid )

{

pCanvas-&gtMoveTo(  nXStart+(i*nSpaceTicks),

nYStart )

pCanvas-&gtLineTo(  nXStart+(i*nSpaceTicks), Height-30 )

}

}

}

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

if (FNumYTicks &gt 0)

{

double yVal = FYStart

/ / Визначаємо проміжки

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

for ( int i=0 i&lt=FNumYTicks ++i )

{

int nYPos = Height-30-(i*nSpaceTicks) pCanvas-&gtMoveTo( nXStart-5, nYPos ) pCanvas-&gtLineTo( nXStart, nYPos )

/ / Мітимо ризики

char szBuffer[ 20 ]

sprintf(szBuffer,  &quot%5*lf&quot,FYNumDecimals,yVal)

/ / Отримуємо ширину рядка ..

int nWidth = pCanvas-&gtTextWidth( szBuffer ) int nHeight = pCanvas-&gtTextHeight( szBuffer )

/ / І поміщаємо її в належне місце pCanvas-> Brush-> Color = Color pCanvas-> TextOut (nXStart-nWidth-7,

nYPos-nHeight/2, szBuffer )

/ / Збільшуємо значення

yVal += FXInc

/ / Якщо потрібно сітка, відображаємо її

if ( FYGrid )

{

pCanvas-&gtMoveTo( nXStart, nYPos )

pCanvas-&gtLineTo( Width-RightMargin, nYPos )

}

}

}

/ / Малюємо лінії, що зєднують точки

if ( FNumPoints &gt 0 )

{

for ( int nLine = 0 nLine &lt FXPointssize() ++nLine )

{

/ / Встановлюємо кольору для цієї лінії

pCanvas-&gtPen-&gtColor = FLineColors[ nLine ]

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

int nXPos = XPointToScreen(FXPoints[nLine][0]) int nYPos = YPointToScreen(FYPoints[nLine][0]) for ( int i=1 i&ltNumberOfPoints ++i )

{

pCanvas-&gtMoveTo(nXPos,  nYPos)

nXPos = XPointToScreen(FXPoints[nLine][i]) nYPos = YPointToScreen(FYPoints[nLine][i]) pCanvas-&gtLineTo(nXPos,  nYPos)

}

}

/ / Скидаємо кольори

pCanvas-&gtPen-&gtColor = clBlack

}

}

Маючи цю функцію, ми легко можемо втілити дві функції для виведення графіка на екран або на принтер, не змінюючи для цього екран:

void __fastcall LineGraph::Paint(void)

{

DoPaint(Canvas)

}

void __fastcall LineGraph::Print(void)

{

TPrinter *pPrinter pPrinter = new TPrinter() pPrinter-&gtBeginDoc()

DoPaint(pPrinter-&gtCanvas) pPrinter-&gtEndDoc()

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&gt}

На цьому наш графічний керуючий елемент закінчений, залишилося тільки протестувати його Ви знайдете тестову програму для цього компонента на прикладеному до книги компакт-диску На її прикладі ви зможете подивитися, як спочатку динамічно створюється керуючий елемент, а потім в нього завантажуються дані Якщо ви запустите цю програму, у вас на екрані зявиться вікно, показане на рис 145

Рис 145 Компонент LineGraph в дії

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

Джерело: Теллес М – Borland C + + Builder Бібліотека програміста – 1998

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*