Delphi Tutorials Index - - - - - - - Home - - - - - - - - - - - - Other material for programmers

Delphi programming: Drawing Graphs

This tutorial is longer than some, and does not focus on and answer to one particular "How do I...." question. I think it will be helpful to people who are becoming comfortable with Delphi programming, and are able to follow the story of how something was built up from a blank form to a finished application. It may also interest, or even be of use!, to mathematics teachers.

One thing it does show you that I haven't used much in other tutorials is a way to use one bit of code to handle OnChange events from several edit boxes.

A host of other useful little odds and ends come up along the way.

The sourcecode and the compiled exe of the application which the tutorial discusses is available in a zip file. I would suggest you download that now with the link at the start of this paragraph, and run the application. Just click "Do It"; you should see a graph. If you change the x or y axis scales, any already-plotted lines won't be redrawn, but the next time you click "Do It", the graph will be right for the axis scales you have specified. Change the numbers in the other edit boxes to change the graph which is plotted.


There are several places you can stop in this tutorial.

I wanted a little program to plot mappings. Remember good old "y=mx+c" from school?

You pick fixed numbers for "m" and "c", and the work through a bunch of values for x. Do the sums, and for each x you get a value for y. Then you can draw a graph using the pairs of x and y values you've gathered. The points you get, if you're using y=mx+c, will be a straight line.

Use different "m"s and "c"s and you get a different line.

I needed to know what sort of line a different mathematical rule would build, hence the need for the program.

If you see this tutorial through to the end, eventually you will be able to plot a bunch of bell shaped curves, just by specifying three constants.

In a really clever program you could change the mathematical rule without changing the program. This program isn't going to be that clever. But it will allow for the user....

a) To enter the lowest and highest "x" to be tried.

b) Change the "y" axis of graph so that if the line doesn't "fit" the page, the "zoom" can be changed.

This isn't as easy as it sounds.

Building a program quickly, with limited false starts is an art that you gradually develop. There are rules to follow, but you also need to apply experience and judgment.

One of the rules is:

Always have a very clear idea of EXACTLY what the user will see BEFORE you start writing code.

AFTER you are clear on that, build the application up by making the "bricks", and then putting them together to create the "building".

Often, one of the first things you create when you start the programming is the form and it's controls.... even thought they won't do anything at first.

I can never remember the details of drawing graphics, so I always consult my own tutorial: http://sheepdogguides.com/dt3b.htm. (You can look at that to learn more, but you don't need to... I've copied the essential bits across.)


Start a new application.

Add a TImage object to the form... you will find it on the 'Additional' tab of the components palette. Make it about 300 wide, 200 high, put top at 10 and left at 100. Leave it with its default name of Image1.

Put the following in the form's OnCreate handler:

var Bitmap:TBitmap;(*Provide a suitable variable*)
begin(*main of OnCreate*)
Bitmap:=TBitmap.create;(*Create a bitmap object*)
Bitmap.width:=400;(*Assign dimensions*)
Bitmap.height:=100;
Image1.Picture.Graphic:=Bitmap;(*Assign the bitmap
                  to the image component*)
Image1.Picture.Bitmap.canvas.pen.color:=clRed;
   (*Have a look at the canvas Help to see all
                  the properties*)
Image1.Picture.Bitmap.canvas.moveto(0,0);
Image1.Picture.Bitmap.canvas.lineto(80,15);
end;

Name the form DD71f1. Save everything in a folder of its own. Name the unit DD71u1, and the project DD71.

Make sure that much runs, that you get a line.

Put a button on the form. Name it buDoIt. Caption it &Do It. Double click on the button to create a shell for its OnClick handler. Move the lines of "draw a line" code to the button's OnClick handler. Run the program again. If all is not well, fix it!

Add four edit boxes, call them eXLo, eXHi, eYLo and eYHi. Fill them with 0, 100, 0, 100 respectively. These will be for the user to put values in to define the range of x's tried, and the range of y's displayed on the graph. Note that these are quite different concepts.

The final program will have more inputs. Note that I have already PLANNED these, but will defer describing them until later, just to make following the text easier.

Put the four edit boxes at the logical places around the image. (eYHi at upper left, eXHi at lower right, other two near lower left.)

Make a function. We'll make it fancier later, and then you'll see why we bothered, but for now it is quite simple:

function TDD71f1.siEditToFloat(sTmp:string):single;
begin
result:=strtofloat(sTmp);
end;

You put the above code just before the "end." at the end of the code, and you must put...

function siEditToFloat(sTmp:string):single;

...just after the "{ Private declarations }" remark near the top of the program.

By the way, the type "real" has disadvantages; don't it. (See help file.)

You may be wondering why we've made everything so complicated. I hope that the reasons will become clear, as we develop what we have so far.

Make the buDoItClick handler, as follows....

procedure TDD71f1.buDoItClick(Sender: TObject);
begin
with Image1.Picture.Bitmap.canvas do begin
moveto(round(siEditToFloat(eXLo.text)),
   round(siEditToFloat(eYLo.text)));
lineto(round(siEditToFloat(eXHi.text)),
   round(siEditToFloat(eYHi.text)));
end;//with
end;

And have a little play. Be careful ONLY to put numbers in the edit boxes. Don't worry too much about the graph being "upside down", etc, for the moment. Don't worry either about what the relationship is between the four edit boxes and the lines on the graph. FOR NOW we're merely using the four edit boxes to get two pairs of figures, figures we're using to define the ends of a straight line. THIS IS NOT OUR EVENTUAL GOAL. For the moment, we're still building "bricks", and the interface.


Don't try to run the program after the first change that follows. Do the other changes down to "Now run it" first.

At the top of the program, add a CONST section, as follows...

unit DD71u1;

interface

uses
  Windows, Messages, SysUtils, Classes,
  Graphics, Controls, Forms, Dialogs,
  ExtCtrls, StdCtrls;

const ver='12june07';
  kBitMapWidth=300;
  kBitMapHeight=200;

Throughout the application, any time we are dealing with the width or height of the image object, where the mapping is going to appear, we will use the constants kBitMapWidth and kBitMapHeight. In various places, we need to know these dimensions. It will also make changing various things easy- we just change, say, kBitMapWidth and anything connected to it will adjust automatically.

Expand the "private" section of the interface to make it...

private
    { Private declarations }
    rScaleX:real;
    function siEditToFloat(sTmp:string):single;
    function iScaleX(rTmp:real):integer;
    function iScaleAndClipY(rTmp:real):integer;

Replace the heart of the form's OnCreate handler with...

Bitmap:=TBitmap.create;(*Create a bitmap object*)
Bitmap.width:=kBitMapWidth;(*Assign dimensions*)
Bitmap.height:=kBitMapHeight;
Image1.Picture.Graphic:=Bitmap;(*Assign the bitmap
           to the image component*)
Image1.width:=kBitMapWidth;
Image1.height:=kBitMapHeight;
rScaleX:=kBitMapWidth/round(siEditToFloat(eXHi.text)-
   siEditToFloat(eXLo.text));

Just before the program's final "end.", add...

function TDD71f1.iScaleX(rTmp:real):integer;
//This simple version only works if rXLo=0
begin
result:=trunc(rTmp*rScaleX);
end;

function TDD71f1.iScaleAndClipY(rTmp:real):integer;
//This simple version only works if rYLo=0,
//  and doesn't (yet!) clip.
begin
result:=trunc(rTmp);
end;

procedure TDD71f1.eXSpecChange(Sender: TObject);
begin
rScaleX:=kBitMapWidth/round(siEditToFloat(eXHi.text)-
   siEditToFloat(eXLo.text));
end;

Just before the word "private", add....

procedure eXSpecChange(Sender: TObject);

You don't ordinarily add things to the class declaration outside of the "private" and "public" sections. Delphi usually takes care of everything there. The reasons for doing it "by hand" here are not worth going into. Just do it?

We'll talk about what rScaleX and eXSpecChange do in a moment, but first, try running the program as it now stands It should still "work"... even if it doesn't appear to do more than what we had before did. But we are laying the groundwork for a program which won't run out of steam around the next corner.

Stop the program again, and click on eXHi. Go over to the Object Inspector (pressing F11 is one way to get there.) Select the "Events" tab. Click on the small downward pointing arrow at the right hand end of the listbox to the right of "OnChange", and you should see a list of possible subroutines to handle the OnChange event. Select "eXSpecChange". Do the same thing for eXLo.

Now that's pretty cool. We're using one subroutine, to handle two different events!

Be careful not to enter in eXHi a number that is smaller than the number in eXLo.

Try running the program again. Remember that FOR NOW the program is not using the numbers in the four edit boxes the way they will eventually be used.

Although it has no visible effect yet, take a look at eXSpecChange. It is calculating a value for rScaleX, which holds a number used in "spreading" or "squeezing" numbers across the graph, so that the full width is used, regardless of the XLo and XHi specified. Determining rScaleX is done from XHi-Xlo, and then the width of the graph in pixels (from kBitMapWidth) is added to the mix. But, as I said, all that is yet to be fully used in the rest of the program. But it is in place, so we can continue building now.


Replace the whole of the current buDoIt OnClick handler with....

procedure TDD71f1.buDoItClick(Sender: TObject);
var si1, si2, si3:single;
begin
si1:=siEditToFloat(eXLo.text);
si2:=siEditToFloat(eXHi.text);
si3:=(si2-si1)/80;//The divisor determines how
   //many steps there will be across graph.
with Image1.Picture.Bitmap.canvas do begin
  repeat
    pixels[iScaleX(si1),iScaleAndClipY(40)]:=clBlue;
    si1:=si1+si3;
  until si1>si2;
end;//with
end;

Now when you click on "Do It", you should get a neat line of 80 blue dots across the full width of the image. So far so good. But so boring, too!

Apart from allowing for cases where rXLo does not equal 0, that code pretty much finishes what we have to do with the "X" dimension.

However, at the moment, regardless of "X", each point on the graph is 40 pixels DOWN from the top of the image area. Change the 40 in the code to 5, and run the application again.

See what's happening? The native coordinates in the image object have Y=0 at the TOP of the image, and increasing Y moves you down it. There's nothing "wrong" with this, of course, but we're all used to having our Y scales DECREASE as we go down the page. Now, among other things, we'll "flip" the Y scale around in the iScaleAndClipY function. (The underlying scale of the image object won't have changed, but the number we get back from iScaleAndClipY will "translate" from, say, 10, 20, 30... to the numbers needed by the image object to plot three points going UP the screen.

Replace all of the current eScaleAndClipY with....

function TDD71f1.iScaleAndClipY(rTmp:real):integer;
(*The many calls to eXLo, etc, in this could be replaced
by value held in more convenient places, places that
would only need updating if the contents of the edit
boxes changed.*)
var iTmp:integer;
begin
 iTmp:=kBitMapHeight-round((rTmp-
       siEditToFloat(eYLo.text)));
 if iTmp> kBitMapHeight
   then iTmp:= kBitMapHeight;
 if iTmp< 0
   then iTmp:=0;
result:=iTmp;
end;

Note how the function iScaleAndClipY consults eYHi and eYLo, and if the initial "answer" for the number to return would be outside the designated range, the number is made equal to whichever limit would otherwise be exceeded.

Note the difference between this and the "X" axis. On the latter, we say that we'll start at rXLo and go up to rXHi. We control this. On the other hand, the values calculated (more on this later) for "Y" from the different "X"s we plug into the formula could be almost anything. The variables rYLo and rYHi define the range of values we want to see on the screen. Anything outside that range should be suppressed. In all of this, of course, we must also allow for the fact that going from, say, the right column for "X=20" to the column for "X=30" won't necessarily mean moving 10 PIXELS to the right.

Before we go further, we're going to make another function like eXSpecChange, but this time it's eYSpecChange. Don't bother trying to run this until you are told to do so.

Add the following just after the existing eXSpecChange....

procedure TDD71f1.eYSpecChange(Sender: TObject);
begin
iYLo:=round(siEditToFloat(eYLo.text));
iYHi:=round(siEditToFloat(eYHi.text));
rScaleY:=kBitMapHeight/round(siEditToFloat(eYHi.text)-
  siEditToFloat(eYLo.text));
//Note that this could give rise to a "divide by zero".
//     Put in code to handle that.
end;

Add rScaleY just after the declaration of rScaleX, e.g....

private
    { Private declarations }
    rScaleX, rScaleY:real;

... and add....

iXLo, iXHi, iYLo, iYHi:integer;

... just after the rScale... line.

Revise iScaleX, making it....

iXLo:=round(siEditToFloat(eXLo.text));
iXHi:=round(siEditToFloat(eXHi.text));
result:=trunc((rTmp-siEditToFloat(eXLo.text))*rScaleX);

(Study that, figure out all the clever things it does!)

Add...

procedure eYSpecChange(Sender: TObject);

... just before the word "private", remembering again that this is an unusual thing to do.

Set the OnChange handler for eYLo and eYHi to eYSpecChange.

And add....

rScaleY:=kBitMapHeight/round(siEditToFloat(eYHi.text)-
  siEditToFloat(eYLo.text));

... in the OnFormCreate handler, just after the similar line initializing rScaleX.

NOW try the code again! Should "work" still... and still dull!

However! All we have to do is change the...

pixels[iScaleX(si1),iScaleAndClipY(40)]:=clBlue;

...line in buDoItClick to...

pixels[iScaleX(si1),iScaleAndClipY(si1*2)]:=clBlue;

...and we'll get a more interesting graph... diagonal line. Notice that when Y gets "too high", the program plots the point on the edge of the image area.

Change the 2 to a 3, run the program again. Another diagonal line, a different slope. See what's happening in buDoItClick? We work our way through a set of X values. For each we plot a point. As we access the pixels property, we calculate a value for Y, based on the X we're using at the moment.

A little detail: In iScaleAndClipY(rTmp:real), there's a line that looks almost like...

iTmp:=kBitMapHeight-round((rTmp-
        siEditToFloat(eYLo.text))*rScaleY);

Make it look exactly like that. All you need to do is add the "*rScaleY" and a crucial pair of quote marks, e.g. ( and ).

This most recent tweak "finishes" the first version of the graph plotting program.

Try changing the values in the edit boxes. (Be careful only to enter "sensible" values.) You may have to think a bit about what's going on. Old lines will not be re-drawn to reflect new axes. But the line you get should still show the y:=x*2 relationship... and it will show it for different ranges of X, and across different portions of the potential Y scale.

Because of the code in eXSpecChange and eYSpecChange, in most places where you have, say, round(siEditToFloat(eXLo.text)), you can just use iXLo. I haven't made all of the relevant substitutions in the code. The clumsy way things have been put together was necessary as I wanted to defer talking about eXSpecChange and eYSpecChange until other parts of the code had already been introduced.

Now we're going to move on to something a bit more fun before going back to take care of a boring bit of necessary "user-proofing".


Add three edit boxes. Call them eA, eB and eC. Put 1 in each of them. (We're only using the first one for the moment, but wait for it!)

Change....

pixels[iScaleX(si1),iScaleAndClipY(si1*2)]:=clBlue;

... to ...

pixels[iScaleX(si1),
   round(iScaleAndClipY(si1*siEditToFloat(eA.text)))]:=clBlue;

... and run the program. See what's happening? Whatever you put into eA, when you click "Do It", the graph is drawn, according to y=x* the_number_in_eA.

Now... a reward for all your hard work on this... so far... rather dull program...

Just replace the whole of the buDoItClick procedure with....

procedure TDD71f1.buDoItClick(Sender: TObject);
var si1, si2, si3, siA, siB, siC, siY:single;
begin
si1:=siEditToFloat(eXLo.text);
si2:=siEditToFloat(eXHi.text);
si3:=(si2-si1)/80;//The divisor determines how many
   //steps there will be across graph.
siA:=siEditToFloat(eA.Text);
siB:=siEditToFloat(eB.Text);
siC:=siEditToFloat(eC.text);
(*Those three could be changed with "OnChange" event
triggered procedures tied to the edit boxes. It would
be a better way to do it, but would make the tutorial
longer!*)

with Image1.Picture.Bitmap.canvas do begin
  repeat
    siY:=(si1-siB)*(si1-siB)*-1;
    siY:=exp(siY);
    siY:=(siA*siY)+siC;

    pixels[iScaleX(si1),
       iScaleAndClipY(siY)]:=clBlue;
    si1:=si1+si3;
  until si1>si2;
end;//with
end;

... and run the program. Make both the X and Y axes go from 0 to 10. Set A to 5, B to 2 and C to 1... and click "Do It"... you should get a nice bell shaped curve! Change the numbers in the A, B, C boxes, click "Do It again, and you'll see new curves drawn. (Change them by 1s, or fractions, until you get the hang of things.)

Fun? I hope it was!


Now for that bit of "user-proofing" that I mentioned...

In "DoIt", we're going to isolate the main "draw the graph" inside a begin... end block. And that will only be executed if various tests have been successfully passed.

We're also going to move what is in eXSpecChange and eYSpecChange out of their current locations, to near the start of buDoItClick. When we've done that, we'll take those procedures off of handling the OnChange events for the four X/ Y axis edit boxes. (Perhaps that code should never have been tied to those events... but it was worth doing, to illustrate the technique.)

Start by making the siEditToFloat function...

function TDD71f1.siEditToFloat(sTmp:string):single;
var siTmp:single;
begin
try
siTmp:=strtofloat(sTmp);
except
on EConvertError do siTmp:=999999;
//999999: rogue value to say "Could not convert".
end;
result:=siTmp;
end;

Note that when you run the application from within Delphi, you will still see error messages if you put a non-number in one of the edit boxes... and there are a few other things we need to do first, anyway, before the "user-proof" application will work.

Then proceed by revising the buDoItClick procedure. It won't be our final revision, but for now...

procedure TDD71f1.buDoItClick(Sender: TObject);
var si1, si2, si3, siA, siB, siC, siY, siTestsPassed:single;
begin
siTestsPassed:=0;//The purpose of this variable will be explained shortly.
iXLo:=round(siEditToFloat(eXLo.text));
iXHi:=round(siEditToFloat(eXHi.text));
rScaleX:=kBitMapWidth/round(siEditToFloat(eXHi.text)-
   siEditToFloat(eXLo.text));

iYLo:=round(siEditToFloat(eYLo.text));
iYHi:=round(siEditToFloat(eYHi.text));
rScaleY:=kBitMapHeight/round(siEditToFloat(eYHi.text)-
  siEditToFloat(eYLo.text));

si1:=siEditToFloat(eXLo.text);
si2:=siEditToFloat(eXHi.text);
si3:=(si2-si1)/80;//The divisor determines how many steps
   //there will be across graph.
siA:=siEditToFloat(eA.Text);
siB:=siEditToFloat(eB.Text);
siC:=siEditToFloat(eC.text);

with Image1.Picture.Bitmap.canvas do begin
  repeat
    siY:=(si1-siB)*(si1-siB)*-1;
    siY:=exp(siY);
    siY:=(siA*siY)+siC;

pixels[iScaleX(si1),
       iScaleAndClipY(siY)]:=clBlue;
    si1:=si1+si3;
  until si1>si2;
end;//with
end;

Use the Object Inspector to take the OnChange handlers off of eXLo, eXHi, eYLo and eYHi.

Check that the program is still "running"... it should be... but still in a non-user-proof manner. Don't put any non-numbers into any edit boxes yet.

Take out everything between, but not including, the begin and end of eXSpecChange. Run the program. You should find that eXSpecChange has been removed. (Don't try to remove something like this "by hand"... it is easy to leave damaging bits behind.)

Remove eYSpecChange by the same method.

Check application runs.

Now we're going to change buDoItClick again.

Replace the "with..." and everything after it with....

if iXLo=999999 then siTestsPassed:=1;
if iXHi=999999 then siTestsPassed:=2;
if iYLo=999999 then siTestsPassed:=3;
if iYHi=999999 then siTestsPassed:=4;
if siA=999999 then siTestsPassed:=5;
if siB=999999 then siTestsPassed:=6;
if siC=999999 then siTestsPassed:=7;

if siTestsPassed=0 then begin

with Image1.Picture.Bitmap.canvas do begin
  repeat
    siY:=(si1-siB)*(si1-siB)*-1;
    siY:=exp(siY);
    siY:=(siA*siY)+siC;
    pixels[iScaleX(si1),
       iScaleAndClipY(siY)]:=clBlue;
    si1:=si1+si3;
  until si1>si2;
end;//with
end // no ; here
else begin
//xx
end; //else

end; //buDoItClick

Now when you run the program, you can enter non-numbers into edit boxes without immediately having problems. If you run the program by double clicking on the .exe file in, say, a Windows Explorer display, then the program will run. It doesn't complain if you put a non-number in an edit box, and click "Do It". However, it doesn't give you any indication of why it hasn't done anything.


Adding error reporting.

Use the Delphi menu. Start with "File". And then add a new form to the project. Name it "DD71ErrMsgF".

Add a memo to it. Put the following text into the memo:

"There's a non- number in one of the edit boxes.

This message will go away as soon as you change the contents of an edit box."

Make the memo's Enabled property false.

Re-save your work, calling the new unit "DD71uErrMsg.pas"

Add a label to the form. Name it laErr.

Adjust the size and position of DD71ErrMsgF, and the components on it, so that it doesn't obscure any of the fields.

In the code for DD71u1, just after the existing var line at the top of buDoItClick, add....

sTmp:string;

In place of the "//xx" that is the else clause at the moment, insert....

sTmp:='unexpected error code';
case round(siTestsPassed) of
1:sTmp:='Problem is in the low value for the X axis';
2:sTmp:='Problem is in the high value for the X axis';
3:sTmp:='Problem is in the low value for the Y axis';
4:sTmp:='Problem is in the high value for the Y axis';
5:sTmp:='Problem is in the value for the first constant';
6:sTmp:='Problem is in the value for the second constant';
7:sTmp:='Problem is in the value for the third constant';
end;//case
DD71ErrMsgF.visible:=true;
DD71ErrMsgF.laErr.caption:=sTmp;

Save your work.

Run the application. It should work. It should even give a report when there's a non-number in an edit box... but the report doesn't go away.

Add a button to the form, call it buClearMsg

Make its OnClick handler....

DD71ErrMsgF.visible:=true;

Run the program, be sure the button works.

Set the OnChange and OnClick event handlers for eYHi to buClearMsgClick. Be sure that clicking on, or changing the contents of eYHi will make the error message window go away. When that's working properly, delete the button, but don't delete the code that went with it. In fact, assign that code to the OnChange and OnClick event handlers for the other 6 edit boxes besides eYHi.




            powered by FreeFind
  Site search Web search
Site Map    What's New    Search This search merely looks for the words you enter. It won't answer "Where can I download InpOut32?"


Click here if you're feeling kind! (Promotes my site via "Top100Borland")
Ad from page's editor: Yes.. I do enjoy compiling these things for you. I hope they are helpful. However... this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run a Windows or MS-DOS PC) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!
Click here to visit editor's freeware, shareware page.

Link to Tutorials main page
How to email or write this page's editor, Tom Boyd



Valid HTML 4.01 Transitional Page WILL BE tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....