HOME > > TUTORIALS TABLE OF CONTENTS - - - / - - / - - / - - / - - - Other material for programmers

Delphi tutorial: Color Graphics. Dynamic Resizing. (Level 4)

This is a draft... it is probably right, but I make no promises!

There's a search button at the bottom of the page.

Please don't dismiss it because it isn't full of graphics, scripts, cookies, etc!

Click here if you want to know more about the source and format of these pages.

IGNORE ANY PERIODS (.) AT THE START OF ANY LINE


Dt4e: Color Graphics (and a bit about dynamic sizing)



This tutorial explores drawing an array of colored rectangles on the screen. The primary issue is how to get the colors you want. Along the way, some attention will be given to the problems inherent in Windows programming which arise from the fact that you don't know what screen resolution your user has in effect, and that users like to be able to resize windows. The program is written for (256 and higher) color displays.

We will not be using the Delphi grid objects, althought they might well be appropriate.

The application will be split into two parts: a fairly trivial "main" program to "drive" the second part, a unit which can be used with other programs. The subordinate unit will contol its own window, which will have be a eight rows of eight rectangles. The use of subordinate units like this is explained in more detail in other tutorials. Although this tutorial will be self contained, if you want more explanation of the multiple unit aspect, it is available elsewhere.

The main unit will have a form with buttons for selecting various color schemes. It will have an array (baRectColo) of 64 bytes, one for each rectangle in the display. It will be responsible for designating the mapping between numbers and colors. The mapping will be held in a 256 longint array called liaColoMap. That is to say if you want rectangle 0,0 to be bright red when baRectColo has 25 in it, then you have to put the code for "bright red" in liaColoMap[25].

The subordinate unit will be called DD14sau ("Stand Alone Unit")

The main unit will...
Establish initial values for liaColoMap
Establish initial values for baRectColo
Call dd14.setupPalette... this will prepare the way for...
Call dd14.drawRects... which draws rectangles of the specified colors.

After that, you can "play" with what has been created, the "work" is done!

Perhaps it is already clear, but just in case....

TWO things determine what you will see on the screen in the first rectangle...

The value in baRectColo[0,0]. If it is 25 then the value in liaColoMap[25] is the other determinant. Typically, the user will, once and for all, prepare a palette of all the colors potentially wanted by filling liaColoMap with suitable values and calling dd14sau.setupPalette. He/she will then fill baRectColo with the colors wanted in the rectangles at the moment, and call d14sau.drawRects. Changing the display thereafter will usually be a simple question of changing values in baRectColo and calling again dd14sau.DrawRects.

To start the the program's delevopment, we are going to build something that draws just a square of one color. The arrays are going to be part of the program from the beginning, but we're going to use just ba[0,0] and liaColoMap[0].

Set up a folder called dd14.
Start a project, name the form dd14f1, save the unit as dd14u1 and the project as dd14

Click on File|New Form, select a blank form. Name it dd14sauf. Resave the project. You should get the "Save As" dialog box again, asking for a name for "unit1". This isn't the unit1 you saved as dd14u1 a moment ago; it is the code for the form you added and named dd14sauf, so name the unit dd14sau.

Add a label to dd14sauf called laTmp. Add dd14sauf to the uses clause of the main form (dd14f1). Add a button to the main form. Name it buDoIt, caption "Do It". Give it an OnClick handler that says...

dd14sauf.show;
if dd14sauf.laTmp.caption<>'Mary' then dd14sauf.laTmp.caption:='Mary' (*no ; here*)
. else dd14sauf.laTmp.caption:='Fred';

Once you position the windows suitably, you should be able to click on buDoIT and see the laTmp change from Fred to Mary to Fred....

So far so good. (This might be a good time to re-save the project.)

In dd14sau, revise the object declaration as follows. (You have to add the two "... of byte" lines. We're putting the declarations in the public section so that we can access the arrays from dd14u1. I believe it is correct to call the arrays "fields" of the object. They behave like what in the old days I called a variable. "Proper" OOP practice, I believe, entails avoiding direct changes to what is stored in the fields, but that's something for another day!
type
  Tdd14sauf = class(TForm)
    laTmp: TLabel;
  private
    { Private declarations }
  public
    { Public declarations }
    liaColoMap:array [0..255] of longint;
    baRectColo: array[0..7,0..7] of byte;
  end;

Once you've done this, to the OnClick handler in dd14u1, you should be able to add..

dd14sauf.liaColoMap[0]:=5;
dd14sauf.baRectColo[0,0]:=5;

... and compile without errors. (The new stuff doesn't do anything you can see yet.)

From the "Additional" tab of the component palette, add a TShape object to dd14sauf. Name it shTmp.

In dd14u1's buDoItClick, replace the

if dd14sauf.laTmp.caption<>'Mary'...

with...

if dd14sauf.shTmp.brush.color<>0 then dd14sauf.shTmp.brush.color:=0 (*no ; here*)
. else dd14sauf.shTmp.brush.color:=random(255*255)+255*255;

Remove laTmp from the dd14sauf. Save the project and run it.

You will now PROBABLY see the color change when you click DoIt.

Here begins a little digression. Windows is wonderful. I write this out ten time before every Windows programming session, just to try to eep myself convinced. The trouble is, it is SO wonderful that sometimes it won't do the same thing twice. (And no, I'm not talking about anything connected with my use of the Random function in the code above.) Just before I descend into the Sough of Despond, let me assure you that what follows "should" work... most of the time... but: If you're getting weird results, don't immediately conclude that you are doing something wrong.

(Continuing digression) Windows tries to be all things to all people and pcbs. You "should" be able to plug any video card into your computer and see what you saw before. To achieve this, color management has evolved. When I started, you had two colours: the paper in your KSR-33 teleprinter, and the ink in the ribbon. Then things got better: store 4 in the right place, and things were red. You could choose between 16 colors! Red was always 4, and it was always the same red.

(Continuing digression) Under modern color management, first, the system sends some codes to the graphics card. These say "From now on, when I send you (say) 56, use the bluey-greeny yellow I just described" The describing can be a little complex, and the card is CAPABLE of far more colors, in general, that it is CON-CURRENTLY capable at any particular instant. Hence the "When I say 56..." system. Inside Windows, there are sophisticated permutations of this basic idea.

(Continuing digression) All this is well and good... when it works. Of course, when it doesn't, the Windows people say that the card isn't responding as it should, and the makers of the card will tell you that something must be wrong with the messages the card is being sent.

(Ending digression) So! You have been warned and, I hope, (somewhat) enlightened. By the way- the system we are building in software works in a manner analogous to what was just described. The "color 56 is a yellow for the moment" data is held in what is called a palette. Several are involved, so I'll try to avoid using the term for anything in our program, but be sure when consulting other refernce material that you keep clear in your mind WHICH palette you are reading about. In a moment, we are going to look at using numbers to specify colors. The Delphi Help entry for TColor talks about part of the number as follows... I'm going to ignore this stuff and hope for the best, I recommend the same to you! (I don't mean to reject the efforts Borland made to help me... but life is only so long. In Windows programming, you have to walk a tightrope between delving deeply enough to get the job done, and trying to understand thihngs. Learning to spot the bit that needs further exploration when something doesn't work is a major part of the art of programming.) Enough philosphy! What the Delphi Help says: "If the highest-order byte is zero ($00), the color obtained is the closest matching color in the system palette. If the highest-order byte is one ($01), the color obtained is the closest matching color in the currently realized palette. If the highest-order byte is two ($02), the value is matched with the nearest color in the logical palette of the current device context. To work with logical palettes, you must select the palette with the Windows API function SelectPalette. To realize a palette, you must use the Windows API function RealizePalette."

Back to work!

Our program at the moment makes the square a random color. (I hope! If it doesn't, you need to get it that far before proceeding!)

Change...

random(255*255)+255*255

to

256*256*256

Your square should now alternate between black and blue.

Also try

256*255

and

255

They should give black/green and black/red, respecively.

(All of this is a little easier if you understand hexadecimal numbers, by the way... by we'll struggle on in decimal for those who don't. If you see, say, $00FF0000 in other reference materials, that's just a more suitable way of writing 16711680 which is 256*256*255)

In general: With bb,br and bg each equal to something from 0-255 (inclusive), you can descrive a color with...

(256*256*bb) + (256*bg) + br

The higher each variable part is, the more blue, green, and red (respecively) there wil be in the resulting color. For black, make all 0. For while make all 255.

You could have a little fun writing a program with three edit boxes on dd14f1. They would accept values for how much blue, green, and red you wanted, and then your DoIt button would display a square of the color you'd described... but I'm not going to explore that here!

I am going to put a function in dd14sau which can be used to "assemble" color description numbers:

In dd14sau, just after the "baRectColo: array[0..7,0..7] of byte;" in the type declaration, add..

function liColorNumber(r,g,b:longint):longint;

and just before the "end." of dd14sau, add...

function Tdd14sauf.liColorNumber(r,g,b:longint):longint;
begin
result:=65536*b+256*g+r;
end;

(Note that r,g and b are declared as type longint. If they are, as might seem reasonable, byte type variables, the result will not be calculated as you would like.... Prior to the final transfer to "result", the number could be altered.)

Then go into dd14u1, and change...

else dd14sauf.shTmp.brush.color:=....

to read...

else dd14sauf.shTmp.brush.color:=dd14sauf.liColorNumber(0,255,255);

Run the program; the square should alternate black/cyan (light blue). Whatever three numbers you supply to ColorNumber will determine how much red, green and blue there is in the alternative to black.

So much for colors! Now for color management!

Then go into dd14u1, and change...

else dd14sauf.shTmp.brush.color:=....

to read...

else dd14sauf.shTmp.brush.color:=dd14sauf.liaColorMap[0];

and, a few lines above, change

dd14sauf.liaColoMap[0]:=5;

to...

dd14sauf.liaColoMap[0]:=dd14sauf.liColorNumber(0,255,255);

(Run thr program to be sure it survived this trivial change.)

In our progress to our final goal, we've now reached the point were we're going to generate a list of the colors we want to use. This topic will reward many hours of exploration, so I'm going to just give you an answer. There are many, many solutions.

OnFormCreate add.....

Just before dd14f1.buDoItClick's "begin" add...

var c1,c2:byte;

(we won't need c2 for a while, but will later) and just after it's "begin" add
begin
for c1:=0 to 255 do begin
 if c1<80 then dd14sauf.liaColoMap[c1]:=
     dd14sauf.liColorNumber(40,c1*2+80,40);
 if (c1>79) and (c1<160) then dd14sauf.liaColoMap[c1]:=
     dd14sauf.liColorNumber(random(255),0,255);
 if c1>159 then dd14sauf.liaColoMap[c1]:=
     dd14sauf.liColorNumber(255,random(255),0);
 end;

Take out the old line saying

dd14sauf.liaColoMap[0]:=.....mber(random(256),random(256),random(256));

An aside: It is a pity that this code, and the "dd14sauf.show;" just after it cannot be put in dd14f1's OnFormCreate. Both do not need to be re-executed every time you click on "Do It". However, there is a problem with accessing dd14sau objects before dd14f1's OnCreate has been completed, and I don't know where to put these things. (End aside)

Also, further down dd14f1.buDoItClick make...

else dd14sauf.shTmp.brush.color:=dd14sauf.liaColoMap[0];

into...

else dd14sauf.shTmp.brush.color:=dd14sauf.liaColoMap[random(80)];

(For the moment I'm not using the colors in the map at locations 81-255)

Run the program again. Clicking DoIt gives you (I hope!) black and various shared of green.

Tired of clicking "DoIt"? So am I. Time for something better.

(You can skip from here down to "End of "You can skip"" if you wish...)

First a simple answer....

Add 8 more TShape objects to dd14say. They can keep their system assigned names. Arrange them and shTmp ROUGHLY in three rows of three. (The result is nicer if they are not all exactly the same size and shape, and not all lined up precisely.)

Replace DoIt's old
if dd14sauf.shTmp.brush.color<>0
   then dd14sauf.shTmp.brush.color:=0 (*no ; here*)
    else dd14sauf.shTmp.brush.color:=dd14sauf.liaColoMap[random(80)];


with... (the word "with" is not a typo! Use copy/paste to generate most of this)...

with dd14sauf do begin
shTmp.brush.color:=liaColoMap[random(80)];
Shape1.brush.color:=liaColoMap[random(80)];
Shape2.brush.color:=liaColoMap[random(80)];
Shape3.brush.color:=liaColoMap[random(80)];
Shape4.brush.color:=liaColoMap[random(80)];
Shape5.brush.color:=liaColoMap[random(80)];
Shape6.brush.color:=liaColoMap[random(80)];
Shape7.brush.color:=liaColoMap[random(80)];
Shape8.brush.color:=liaColoMap[random(80)];
end;(*with*)

(The "with dd14sauf.. do .. end" saves you specifying dd14sauf repeatedly.)

Run this, and you should get the nine rectangles in different shades of green each time you click "Do It".

When all is well, from DoIt's code, delete what we just added, and from dd14sauf remove all 9 shapes.

(End of "You can skip")

If you skipped the simple answer, delete DoIt's old
if dd14sauf.shTmp.brush.color<>0
   then dd14sauf.shTmp.brush.color:=0 (*no ; here*)
    else dd14sauf.shTmp.brush.color:=dd14sauf.liaColoMap[random(80)];

before proceeding.

Now we need to create an array of shapes of type TShape. (If the following is too streamlined, see Dt3a.htm "Creating an array of edit boxes", which covers the issues more throughly. By the way, sorry: Unless I've edited that since writing this, there's an error about a quarter of the way through that. Where it says...

"Use the Object Inspector to create an OnCreate event handler for TEdit",

it should say

"Use the Object Inspector to create an OnCreate event handler for Demof1")

All of the following are in dd14sau...

In the public section, between

baRectColo: array[0..7,0..7] of byte;

and...

function liColorNumber(r,g,b:longint):longint;

add...

ashRect:array[0..4,0..4] of TShape;

(N.B.: This is not exactly the approach used in my "Creating an array of edit boxes" tutorial.

Also N.B.: The line cannot go after the "Function..." line. (I would guess that the rule is that all fields have to appear after "Public" before any function or procedure headers, although the Delphi Help file would seem to imply that they can be intermingled.)

(Further details of "Also N.B.": In Delphi I, anyway...
 public
    liaColoMap:array [0..255] of longint;
    baRectColo: array[0..7,0..7] of byte;
    ashRect:array[0..4,0..4] of TShape;
    function liColorNumber(r,g,b:longint):longint;
  end;


compiles, but...
 public
    liaColoMap:array [0..255] of longint;
    baRectColo: array[0..7,0..7] of byte;
    function liColorNumber(r,g,b:longint):longint;
    ashRect:array[0..4,0..4] of TShape;
  end;


complains "END expected". Can anyone shed light on this?)

Use the Object Inspector to create an OnCreate event handler for dd14sauf1. Make it...
procedure Tdd14sauf.FormCreate(Sender: TObject);
var c1,c2:byte;
begin
for c1:=0 to 3 do
  for c2:=0 to 3 do begin
  ashRect[c1,c2]:=TShape.create(self);
  ashRect[c1,c2].parent:=TWinControl(self);
  ashRect[c1,c2].top:=c2*24;
  ashRect[c1,c2].left:=c1*24;
  ashRect[c1,c2].width:=24;
  ashRect[c1,c2].height:=24;
  end;
end;


.. and run it. When you click "DoIt", you should get 16 neatly spaced white boxes on dd14sauf1.

Now go back to dd14u1. In DoItClick, just after dd14sauf.show, just to get started, add....

dd14sauf.ashRect[0,0].brush.color:=256*255;

Once that works, replace the previous feeble line with....
for c1:=0 to 3 do
  for c2:=0 to 3 do
    application.processmessages;
    dd14sauf.ashRect[c1,c2].brush.color:=dd14sauf.liaColoMap[random(80)];


Clicking DoIt now draws and redraws a grid of green squares... but it does it more

elegantly than by the means used in the "You can skip this" section!!

_________
Before we forge ahead, we ought to go back and change something to make it more elegant. Our program, as written, produces a 4x4 array of TShape objects. To change the number of elements in the grid would be a pain.

After dd14sau's "Uses" clause, add...

const kx=4;
ky=2;

and change

ashRect:array[0..3,0..3] of TShape;

to

ashRect:array[0..kx,0..ky] of TShape;

and go through the rest of the program (both units) changing 4 hard coded "3"s to kx, ky, dd14sau.kx or dd14sau.ky as appropriate. After several hours' work, I changed the kx, ky settings from 4/2 to 10/5.. and had a crash, by the way. There may be a problem with what I've told you... create may need to be balanced by an explicit destroy... but it may just be that dear old Windows 3.11 does crash from time to time. I'd saved the delphi work just before the crash... but had to re-write a chunk of this, sigh. Oddly(?) enough, I later had a similar crash as soon as I re-ran the application having just changed the values of kx and ky. The overhead of all those TShape objects may be too much for my poor little system, but I'd prefer that Delphi decile more graciously! Any ideas? By the way... if you want to use something like this program, but without the TShape objects... that aspect can be written out quite easily.

_____________
In a similar "let's make it elegant" vein: In dd14sau, just after "Public", add...

bRectHeight,bRectWidth:byte;

and change the four 24s in the OnCreate handler to bRectHeight or bRectWidth as appropriate.

Alter kx,ky,bRectHeight,bRectWidth to taste... after saving your open files!!
_____________

I did't like the black lines between rectangles. I tried adding

ashRect[c1,c2].pen.width:=0;

to the ashRect[] init section, but it didn't do a good job. I made it

ashRect[c1,c2].pen.width:=1;

and then went to the place in dd14u1 which did

for c2:=0 to dd14sau.ky do
dd14sauf.ashRect[c1,c2].brush.color:=dd14sauf.liaColoMap[random(80)];

and made it (note the added "begin")..

for c2:=0 to dd14sau.ky do begin
application.processmessages;
liTmp:=dd14sauf.liaColoMap[random(80)];
dd14sauf.ashRect[c1,c2].brush.color:=liTmp;
dd14sauf.ashRect[c1,c2].pen.color:=liTmp;
end;

===============
Well! We've got a pretty program. To make it look nice, all of the rectangles change color whenever we click "DoIt", but it would be fine to change just one at a time from code in dd14u1. It is as simple as...

x:={some value};
y:={some value};
liTmp:=dd14sauf.liaColoMap[{some value}];
dd14sauf.ashRect[x,y].brush.color:=liTmp;
dd14sauf.ashRect[x,y].pen.color:=liTmp;

============================

Two problems remain:

a) We re-initialise all of aliColoMap and call dd14sauf.show every time we call "DoIt". These two jobs should be done just once, as the program lauches... unless for some reason we want to change the range of colors available at some point in the program. I don't know how to fix this at present. (Putting the relevant code into dd14f1's OnFormCreate triggered error messages.) I can live with this.

b) When I started, I said that this tutorial would also illustrate some of the solutoins to the fact that the programmer does not know what resolution the user will have in effect, nor what window size will be used. These issues are not tightly tied to the rest of what we've done, but this is a reasonable chance to address them.

_________
In the application's present form, the size of the second window (dd14sauf) and the size of the rectangles in it are not connected. It would be nice if the rectangles changed size to use as much of the window as possible. I must admit that when I set out to accomplish this I didn't "know" exactly what to do. By dumb luck, I rapidly found that Windows makes it all very easy... at least that seems to be the case.

First, for dd14sauf, create an OnResize event, as follows. Most of the lines can be takes straight from the existing OnCreate event. Remove them from the OnCreate handler. It seems that, in effect, at least, the OnResize code is called at some point during the form's creation. It is also called any time the form is resized, e.g. by the user.
var c1,c2:byte;
begin
for c1:=0 to kx do
  for c2:=0 to ky do begin
  ashRect[c1,c2].top:=c2*bRectHeight;
  ashRect[c1,c2].left:=c1*bRectWidth;
  ashRect[c1,c2].width:=bRectWidth;
  ashRect[c1,c2].height:=bRectHeight;
  end;
end;


Even though it will not behave differently than the previous version of the program, get this much "working".

Now, from OnCreate, remove the...

bRectHeight:=40;
bRectWidth:=20;

and in OnResize add...

bRectHeight:=clientheight div (ky+1);
bRectWidth:=clientwidth div (kx+1);

dd14sau's form can now be maximized, minimized, restored, and made any size or shape you like... and the rectangles adjust to fill the available space!! Amazing.

One small blemish remains: at certain sizes, there's a small border at the bottom and/or at the right. This arises as follows: suppose there are six rectangles across the window. Suppose the window is 62 units wide. If each rectangle is 10 units wide, they won't quite fill the window. If each is 11 units wide, they will more than fill it.

Add the following two lines at the beginning of OnResize...

while (clientheight mod (ky+1)>0) and (height>0) do height:=height-1;
while (clientwidth mod (kx+1)>0) and (width>1) do width:=width-1;

Hard to believe that's all it takes... but it works!

=====
That covers the material of the tutorial. Before I close, in less detail (and less thoroughly checked for complete correctness!): Some of the games I played with the application, simply by making changes in dd14u1. dd14sau was left unchanged.

Added Start and stop buttons and a timer. All were disabled at design. The buttons were enabled by buDoIt. (This ensured a dd14sau.show and filling of liaColoMap before Start could be clicked.) The start button enabled the timer. (Stop diabled it). The timer's event handler changed the colors of all the squares, just as DoIt changed their colors. (In fact, I moved the "Change the colors" code out into a proc ("ChangeAll") that could be called from more than one place.)

Added combobox, with possible values "Greens", "Band 1", "Band 2" and "All" Created combobox.OnChange consisting of bCS:=combobox.itemindex;ChangeAll;

Revised ChangeAll, making it
procedure Tdd14f1.ChangeAll;
var c1,c2:byte;
begin
for c1:=0 to dd14sau.kx do
  for c2:=0 to dd14sau.ky do begin
  application.processmessages;
  if bCS=0 then liTmp:=dd14sauf.liaColoMap[random(80)];
  if bCS=1 then liTmp:=dd14sauf.liaColoMap[random(80)+80];
  if bCS=2 then liTmp:=dd14sauf.liaColoMap[random(96)+160];
  if bCS=3 then liTmp:=dd14sauf.liaColoMap[random(256)];
    dd14sauf.ashRect[c1,c2].brush.color:=liTmp;
    dd14sauf.ashRect[c1,c2].pen.color:=liTmp;
    end;
end;
(N.B. The change of colors used only becomes evident on next ChangeAll)

_____________
Sorry to end a bit abruptly... but if you want this on the web instead of languishing in my computer.....

   Search this site or the web        powered by FreeFind
 
  Site search Web search
Site Map    What's New    Search


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... hope they are helpful. However.. this doesn't pay my bills!!! If you find this stuff useful, (and you run an MS-DOS or Windows 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
Here is how you can contact this page's author, 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 .....