Wikia

Delphi Programming

Create Your Own Paint Program

Talk4
2,909pages on
this wiki

This tutorial will help you create a paint program with basic tools like lines, circles, brushes and color choices. These tools are quite similar so after getting a lot of help with the first one, you will be able to do the others with less and less help - a nice way to ease into Delphi programming.



Starting the ProgramEdit

Create a folder named "Paint" for this project, possibly in the "My Documents" folder on your computer. Of course you can name it anything you like, but I will use the name "Paint" in this article.
Run Delphi and, in the file menu, choose "New Application"(Delphi 2010: File->New->VCL Froms Application - Delphi).
This should create two windows named "Form1" and "Unit1". You can switch between them quickly by pressing the F12 key.
Click on the unit so it is in front of everything else. Choose File - Save As, name it "main" and direct it into your paint folder.
Then choose File - Save Project As and name it "paint".

Click on the form. Press the F11 key to select the object inspector. Right under its title it should say "Form1" to indicate it is presently inspecting the details of the form. In the list below, find the item "name" and beside it change "Form1" to "MainForm". This will help keep things organized when you have lots of forms (windows and dialogs) in your project.

Exit from Delphi, saving if it asks. Look in your Paint folder. You should find 5 files related to this project: main.pas, main.dfm and several beginning with paint. You will normally begin work on your project by doubleclicking on Paint.dpr - the project file.

The Main MenuEdit

On the component bar at the top or right side of Delphi’s main window, you should find a series of tabs labelled "standard, additional, win32 ...". Click on "standard". Just below the tabs are icons of various standard components. Hold the mouse pointer over one for a second or two to see a brief description. The first one is described as "mainmenu". Click on the "mainmenu" component, then click anywhere on your form. The mainmenu icon should appear on your form. It doesn't matter where on the form you place this particular component.

Doubleclick on your mainmenu to open it. Nothing much there so far, except a blank entry which should be selected. Press the F11 key to inspect it. Find the "name" in the object inspector's list and enter the name "FileMenu". Find the "Caption" and enter "File". Click the x icon at the upper right corner of the mainmenu window to close it.

Your program should run now. Choose "Run" in the "Run" menu. After a moment of compiling, you should see your form appear with its main menu containing only the "File" item. Of course it doesn't work yet. Click the x button to close it.

Let's add an Exit item to the file menu. Doubleclick on your mainmenu object, select the blank item hanging under the word "File" and press F11 to edit it. Change the "Caption" to "Exit". The name should automatically change to "Exit1", which is as good as any. When you run your program, you should find that the file menu now has an exit command, but it still doesn’t work. Again you have to click x to close your program.

With your program NOT running, pull down the File menu and choose "Exit". This automatically creates an exit response procedure in your main unit. Here is where you write the code that makes your program work:

procedure TMainform.Exit1Click(Sender: TObject);
begin
  MainForm.Close;
end;

You just have to type MainForm.Close - Delphi has written the routine stuff for you automatically. The response procedure holds the detailed programming for an action like choosing a menu command. When the program is running and someone chooses "Exit", Windows gives control to your Exit1Click procedure and follows the instruction you entered.

Run your program again (choose run in Delphi’s run menu) and you will find that your file-exit command works. Congratulations on getting one of your program's commands to actually work!

The Toolbar PanelEdit

Our program needs a set of painting tools (pencil, line, brush, etc.) to choose from.
Begin by choosing a panel component to hold all the tool buttons. On the component bar at the top or right side of the mainform, click the panel component in the "standard" tab, then click it onto your form. Drag it wherever you like; you can always move it later. In fact, the purpose of the panel is to allow us to move all the tools as a group more easily. You can get rid of the "Panel1" caption in the middle by using the object inspector to change its caption to a blank. Let's leave the name as

Adding a SpeedButtonEdit

Delphi has a number of different buttons that could be used for the toolbar. The speedbutton on the "additional" tab has the advantage of being easy to put an icon on and to indicate when it is selected. The speedbutton looks like a lightning bolt - if you are in doubt hold the cursor over each component until you see its hint. Put a speedbutton on the panel. Use the object inspector to name it "Pencil", and to make the GroupIndex 1 and Enabled True.

We need to draw a picture of a pencil to put on the speedbutton. Delphi has a little icon editor built in. I'm using Delphi 5, so what I'm seeing may not be exactly the same as what you are seeing. In the Tools menu, choose "Image Editor". Pull down its file menu and choose new bitmap image. Make it 64 dots wide and 16 high in 16 colors. Use the pencil tool to make vertical dividing lines splitting the bitmap into 4 equal parts. Choose the magnifying glass and right click (choosing zoom in) a couple of times to make the 64x16 bitmap large enough to work with. Count 16 squares for each part. The first part is the normal appearance of your pencil tool. The third part is the appearance when clicked and the fourth part is what it looks like when held down. Draw them any way you like! You can simply use a different background color for the 3rd part. Erase or cover up the black lines to the right of the 1st and 3rd parts - they are part of the pictures. Save in your folder, using the name Pencil.bmp.

Now you need to tell Delphi that you want to use Pencil.bmp as the glyph for the pencil speedbutton. Click on the speedbutton. Press F11 to bring up the object inspector. Click on the blank to the right of "Glyph", then on the 3 dots that appear there. In the dialog that opens, choose "Load", then find pencil.bmp and doubleclick it. Presto, your button should have your picture on it.

Run your program and see what happens when you click on the pencil button. It should change appearance. If you want to change the picture, open it with the image editor (or any paint program), edit, save, then choose it again in the object inspector.

An Image to Paint onEdit

It is possible to paint on the bare form, but adding an image component makes it easy to load and save the image. On the component palette, click the "additional" tab and choose the image component. Place it on your form and adjust to the size you want. It should already be named "Image1".


test

Getting Ready to Draw: a Boolean VariableEdit

We need a variable to keep track of whether or not we have started using the pencil. Add the drawingNow variable near the top of your unit window, as shown below: (the first 2 lines are already there; just add the 3rd line)

var
  Mainform: TMainform;
  drawingNow: Boolean;

drawingNow is a Boolean variable. This means it can have only two possible values, either True or False. We want it to be False when the program first runs. To ensure this do the following: Double-clicking on the form away from any components will generate the OnCreate Handler. Another way of doing this goes as follows: click on your form, away from any components. Press F11. Confirm that the object inspector says "mainform" at the top. Click the "Events" tab just below this, and look for the "OnCreate" item in the list below that. Doubleclick in the blank to the right of "OnCreate." This should create a Procedure FormCreate in your unit. You need to add a line of programming code in this procedure:

procedure TMainform.FormCreate(Sender: TObject);
begin
  drawingNow := False;
end;

Click on Image1 on your form. Press F11 and, under the events tab, doubleclick to the right of "OnMouseDown". This creates a procedure in the unit. Windows directs control of the program to this procedure when someone clicks on the image. Type the 2 lines between begin and end to tell Windows to move its attention to where the mouse was clicked, and make drawingNow true if the pencil button has been depressed.

procedure TMainform.Image1MouseDown(Sender: TObject;  
  Button: TMouseButton; Shift: TShiftState; 
  X, Y: Integer);
begin
  Image1.Canvas.MoveTo(x, y);
  
  if Pencil.Down then 
    drawingNow := True;
end;

The Pencil.Down reference is a neat dividend of object oriented programming. We have an object named "Pencil" (the button you made) and it has a property called "Down". The name "Pencil.Down" makes sense and is easy to understand. If you happen to get an error to the effect that there is no such thing as "Pencil.Down", you would naturally check to make sure you did name the button "Pencil" and perhaps check the object inspector to make sure it is a type of object that actually has a "Down" property.

Let's Draw!Edit

The next step tells Windows to actually draw something on the image! Select image1 on the form, press F11 and doubleclick to the right of “OnMouseMove”. Add one line of code to the procedure that gets control when the user of your program moves the mouse:

procedure TMainform.Image1MouseMove(Sender: TObject;  
   Button: TMouseButton; Shift: TShiftState; 
  X, Y: Integer);
begin
  if drawingNow = True then 
    Image1.Canvas.LineTo(x, y);
end;

This line of code tells Windows to draw a line from where it was before to the current mouse location (x,y). The pencil button needs a bit of tweeking. Click on the pencil button, press F11, click the properties tab. Enter a "group index" of 1 and doubleclick to the right of AllowAllUp to make it say “true”. More on this later. Run your program. You should find that you can write on the image after clicking on the button to turn it on.


The pencil tool should turn off when you release the mouse button. Select image1, press F11, the events tab and doubleclick to the right of “on mouse up”.

procedure TMainform.Image1MouseUp(Sender: TObject;  
   Button: TMouseButton; Shift: TShiftState; 
  X, Y: Integer);
begin
  drawingNow := False;
end;

Understanding the CodeEdit

Try to understand everything you typed in the unit, so you can figure out another painting tool on your own. It might help to write in plain English what happens as the user clicks the pencil button, then draws on the image:
- When user clicks the pencil button: Pencil.Down becomes True
- When user clicks on the image: 
    - Move the current position to the mouse pointer location
    - If the pencil button is down, make drawingNow true
- When the mouse moves on the image:
    - If we are drawing, then draw a line from the previous position  
      to the present mouse position 
This kind of plan is called programming pseudocode.

Saving the PaintingEdit

Doubleclick on the mainmenu component on your mainform. Right click on "Exit" under the file menu and choose Insert (the exit command should be last in the file menu so we’ll insert other items above it). Enter "Save As" as the caption for the new item. Close the mainmenu editor.

Remember the "Save As" dialog is the same in all Windows programs - it is built in. Choose a "Save As" component from the dialogs tab on the component palette. Click it anywhere on your form. Notice that it is named "SaveDialog1". We have to add code to make the menu command use SaveDialog1 to write the image into a file. Create a response procedure for "Save As" by pulling down your form’s file menu and choosing "Save As". Look for the procedure in your main unit and fill it in with this code:
procedure TMainform.SaveAs1Click(Sender: TObject);
begin
  SaveDialog1.DefaultExt := 'BMP';  {set the default extension}
  if SaveDialog1.Execute then     {show dialog; if user clicks ok then ...}
  begin
    filename := SaveDialog1.Filename;  {remember the filename entered}
    Image1.Picture.SaveToFile(filename); {tell Image1 to save itself}
  end;
end;

Notice that each statement ends with a semicolon (a statement like if-then often extends over several lines). Begin-end is used to put several statements together in the then part. It is very helpful if the begin-end pairs are indented equally, this will help you to see the code block instantly. The comments in curly brackets are most helpful when reading the code months or years later. Study the code and comments until they make sense and you understand what the computer does when someone uses the save command.

It is interesting to attempt to run the program at this point. You will get an error message telling you that the compiler doesn't know what "filename" is. It knows the "filename" in "SaveDialog1.filename" but it doesn't recognize the other "filename" that is not part of the SaveDialog. We need to tell Delphi in advance that we are going to use a variable called "filename" to store the name of the file. Put

filename: string;
in your list of variables near the top of the unit (after drawingNow: Boolean). A string is a variable that holds a series of text characters. It probably is a little confusing that the program can have two variables with the same name, one that is part of the SaveDialog and another that is not, but it is a very useful feature of object oriented programming. And a bit dangerous - it might be better to use a different name for your own variable holding the file name.

Test your "save as" command by drawing something and saving it. The .bmp file created should open with Windows paint so you can see that it successfully saved.

At this stage you might be wondering how a programmer can possibly remember all the things that an object knows how to do. The answer is that most programmers do not; they have to refer to Delphi's help system. Click once on the image to select it, then hold down the control key and press F1 to see the help on TImage (that is, "Type Image" which is a class of objects). Click the "Methods" heading to see all the things that an Image knows how to do. No "SaveToFile" command is listed! Try clicking the "properties" heading and note that an Image has a "Picture". Click on "Picture", then on the "TPicture" link to see what methods or capabilities it has. You will find "SaveToFile" there and so the command must be entered as Image1.Picture.SaveToFile. The help entry on SaveToFile

procedure SaveToFile(const FileName: string);
shows that a filename string must be entered in brackets following the command name.

The help is very powerful, but a bit awkward. If you have Delphi 5 or later, try this technique. Erase the SaveToFile line in your program and type it again. If you pause after typing "Image1.", Delphi will prompt you with a list of the properties Image1 has, so you can choose "picture". Pause again after typing the dot after that, and it will again prompt with a list of possible commands so you can choose SaveToFile. This feature is called code completion.

Opening a fileEdit

Can you do the file - open command yourself? It will be very similar to the save command. You'll have to add an item to the menu, a file-open dialog from the component palette, and a bit of code to get the filename from the dialog and use it to load the file. After you get it working, you might add this statement

 if OpenDialog1.Execute then begin
 OpenDialog1.Filter := 'Bitmap (*.bmp)|*.BMP';
 FileName := OpenDialog1.FileName;
 image1.Picture.LoadFromFile(FileName);

to make the open dialog show only bmp files (the only kind we can open at present).

Color Chooser: a button and a dialogEdit

Delphi has a color selection component. On the dialogs tab of the component palette, choose a color dialog and add it to the form. All we have to do is add a button to activate it. On the "additional" tab, choose a "BitBtn" and put it on your form (it could be on the tool panel, but doesn't have to be). In the object inspector, name this button "ColorBtn" and add the caption "colors". If you wish, you can choose a glyph for this button as well.

Doubleclick on the color button to add a response method to the unit. Enter these statements in the new procedure: {C}
  ColorDialog1.Execute;
  Image1.Canvas.Pen.Color := ColorDialog1.Color;

The first line runs the dialog where the use chooses a color. The 2nd line tells the image what the chosen color was. An image has a pen for drawing lines and a brush for filling interiors, which we will work with later.

The Rectangle ToolEdit

Set up a Speedbutton on the panel for the rectangle tool. Name it boxtool. Make the group index 1 and enabled true so it will work with the other tool buttons as a group - when one button is pressed, the others un-press. You will probably want to make a glyph for it as you did for the pencil tool.

When the user draws a rectangle, the first click on the image determines the starting corner of the rectangle. We need variables to remember this and various other things. Add more to your list of variables near the top of the unit:

var
  Mainform: TMainform;
  drawingNow: Boolean;
  filename: string;
  x1,y1,x2,y2,toolInUse: integer;
  holdingArea: Tbitmap;
  holdingSomething: Boolean;
  r1,r2: Trect;

Each of the Trect variables holds a set of four integer variables for the left, top, right and bottom of a rectangle.

Now that we are going to have more tools, we need to keep track of which one is in use. We will replace the old drawingNow variable that only has two possible values with the integer variable toolInUse which can hold any positive or negative whole number. When the user first clicks the mouse on image1, we must remember which tool button is pressed. Change your existing procedure to:

procedure TMainform.Image1MouseDown(Sender: TObject;
          Button: TMouseButton;    Shift: TShiftState;
          X, Y: Integer);
begin
  with image1.canvas do moveto(x,y);
  x1 := x; y1 := y;    {remember starting point}
  toolInUse := 0;
  if pencil.down then toolInUse := 1;
  If boxtool.down then
   begin
    toolInUse := 2;  
    image1.canvas.brush.style := bsClear;
   end;
end;

Notice that the procedure header shows what information is available from Windows on entry - the state of the mouse button and x,y coordinates of the mouse pointer. Thus we can copy the starting coordinates to our own variables, x1 and y1. The line ending in "bsClear" sets the brush (for filling the interior of the rectangle) to clear or no fill color at all. We programmers must remember that the pencil tool is number 1 and the open box tool is number 2.

Now change procedure Image1MouseMove to use the new toolInUse variable, and do some things when we are using the new rectangle tool. Notice that you can put comments to humans in the code by putting curly brackets around it.

If toolInUse = 1 then With image1.canvas do lineto(x,y);
If toolInUse = 2 then
  begin               {-- rectangle tool --}
    if holdingSomething then
      begin
        with image1.canvas do
          copyrect(R1,holdingArea.canvas,R2);
        holdingArea.free;
      end;
    Capture(x1,y1,x,y);
    image1.canvas.rectangle(x1,y1,x2,y2);
  end;

The Capture statement makes a copy of the rectangular area where we temporarily draw a rectangle. This is done so that when the mouse moves on to a new place, we can restore whatever was covered up by the temporary rectangle - that’s what the copyrect statement does. The capture and rectangle statements need 4 numbers to specify their rectangular areas: left,top,right,bottom. A new Boolean variable (true or false) holdingSomething remembers whether or not we have copied all the pixels in a rectangular area into a holdingArea of memory. It may not be possible to understand all this just now!

The holdingArea is just a Windows bitmap, and the copyRect command is built into Delphi's image component, but we must write our own Capture procedure to copy the pixels in a rectangular area into the holdingArea. Begin by typing

Procedure Capture(x1,y1,x,y: integer);

in the list of procedures at the top of the unit (just before the word private would be fine). Now type the new procedure, at the end of the unit (but before the final end). This is difficult typing; you must be very careful to get it exactly right or your rectangle tool will do weird things. I assure you I have copied & pasted the code from a tested program so it does work.

Procedure TMainform.Capture(x1,y1,x,y: integer);
begin
  x2 := x; y2 := y; {remember this spot}
  holdingArea := Tbitmap.create;
  holdingArea.width := abs(x2-x1) + 2;
  holdingArea.height := abs(y2-y1) + 2;

  With R1 do
   begin
    {find left & right sides of rectangle to capture}
    if x1 < x2 then begin left := x1; right := x2+1 end
               else begin left := x2; right := x1+1 end;
    {find top & bottom of rectangle to capture}
    if y1 < y2 then begin top:=y1-1; bottom := y2+1 end
             else begin top := y2-1; bottom := y1+1 end;
   end;
  With R2 do
   begin
     left := 0; top := 0; right := R1.right-R1.left;
     bottom := R1.bottom-R1.top
   end;
  With holdingArea.canvas do
    copyrect(R2,Image1.canvas,R1);
  holdingSomething := true;
end;

Don’t worry if you do not understand this one! It really should be part of the invisible inner workings of Delphi. Basically, it just copies a rectangle of pixels from Image1 to the holdingArea but there is some tricky work figuring out the exact dimensions.

When the user releases the mouse after completing a rectangle, we need to restore the last temporary rectangular area and draw the final one. Your procedure Image1MouseUp should look like this:

if toolInUse = 2 then
  begin
    if holdingSomething then
      begin
        with image1.canvas do
          copyrect(R1,holdingArea.canvas,R2);
        holdingArea.free;  holdingsomething := false;
       end;
      Image1.canvas.rectangle(x1,y1,x2,y2);
  end;
toolInUse := 0;

To complete the rectangle tool, we need to make sure that when the program first runs it doesn’t think it has already captured a rectangular area. In the FormCreate procedure put:

holdingSomething := false;

That was quite a lengthy write, and you will no doubt have some errors to fix before you get it all working. However, we have developed a way of temporarily drawing a figure on the image as the mouse moves and restoring the image afterward. You can use this to do several other tools with much less work.

Filled Rectangle, Ellipse, Line and Brush ToolsEdit

Filled Rectangle ToolEdit

A filled rectangle tool is almost identical to our unfilled one. The unfilled rectangle tool had a section in MouseDown, MouseMove and MouseUp, and so must the filled rectangle too. When the user begins to draw with the filled rectangle tool, set the brush style to bsSolid instead of bsClear. Other than that, you can pretty much copy the code you had for the rectangle tool in each of the three procedures. It might help to see it written in pseudocode:
When the mouse button is pressed down
 - move to the starting point and remember it
 - remember which tool is in use
 - set the pen and brush appropriately for each tool
 
When the mouse pointer moves
 - if holding a captured area, restore it
 - capture the rectangular area from start to current position
 - draw the figure within the captured area

When the mouse button is released
 - if holding a captured area, restore it
 - draw the figure
The brush style controls the fill. The pen style controls the outline. You will want to add another color button so the user can choose the canvas.brush.color. This will be very similar to what you did for the first color chooser, which set the pen.color. ha

Ellipse ToolsEdit

For the ellipse and filled ellipse tools, you will again have to add sections in each of the three mouse response procedures that are similar to what was done for the rectangle. If you can't guess the name of the command that draws the ellipse, look in the help for Tcanvas.

Line ToolEdit

Curiously, the line tool code is almost exactly the same as for the rectangle tool. Just draw a line (use lineto) instead of a rectangle.

Save CommandEdit

You should be able to add a save command to the file menu. Since we are already remembering the file name, just use it to save instead of asking the user for another one. Save is simpler than Save As.

While you are at it, you might display the filename in the title bar. Just use mainform.caption := filename Find all the places where the file name changes and put this statement in each.

What if it doesn’t work?Edit

Longer programs are much, much more complex than short ones and it is unlikely that your new tool will work on the first try. You will have errors of three kinds:

  • compiler errors - prevent the program from running
  • runtime errors that make the program crash (stop unexpectedly)
  • errors in design that prevent it from doing what you want it to do when it does run.

Compiler errors are easiest to deal with because Delphi highlights the line where it notices the error (not necessarily where the error was made). If you forget a semicolon at the end of a statement, Delphi will highlight the following line and say that a semicolon was expected but not found. If you get "undeclared identifier: toolInUse" it simply means that you did not put toolInUse in the var section and specify that it is an integer. Undeclared identifier: Opendialog1 is quite different. Opendialog1 is a component that you click on the form - this error means it is not there or has been named something different. Strange errors (not one of the above) are often caused by unmatched begins and ends. Delphi programmers indent the stuff between begin and end so they can clearly see that each and every begin has exactly one matching end. Do this with your code before you ask for help!

Delphi usually indicates where runtime errors occur, but often gives little information about the kind of error. If you can’t find the solution by examining the code just before the crash point you may have to learn to use the debugging features of Delphi. You can put a break point in the program (click in the margin to the left of any line of code), then press F8 to advance one line at a time. While doing this you can watch the values of your variables (choose View-Watches to add a watch window; add variables to watch by clicking the cursor into the variable in the unit and pressing control+F5).

Errors in design are entirely up to you, the designer, to resolve. Remember the function of each procedure. If your pencil doesn’t stop drawing when you release the mouse button, the problem is likely to be in the Image1MouseUp procedure. That procedure is supposed to make toolInUse equal to zero. Image1MouseMove is supposed to do nothing when toolInUse is zero. You have to carefully try the tool and determine which function is not doing its job. When you deduce exactly what is wrong, you will know exactly how to fix it.

The Brush and Eraser ToolsEdit

An eraser is just a paintpbrush set to draw in white. Your users will expect to have two different tool buttons for these, but you only need to write mousemove and mouseup code for one.

When the user moves the mouse with the button down, the brush tool should simply draw a rectangle at the current (x,y). Rectangle(x-10,y-10,x,y) would be fine. Perhaps later you could add a feature to adjust the size of the brush to some value other than 10. Do not do the capture and restore when this tool is in use.

When the user moves the mouse with the brush selected, but the mouse button not held down, the program should display the rectangular brush at the mouse pointer. The brush should not leave a trail behind it, so you will have to capture the area before drawing the rectangle and restore it next time. This is very similar to the rectangle tool!

I found that the brush tool works best when the pen and brush colors are the same; when the paintbrush tool is selected and the user begins to paint, set the pen color to be the same as the brush color.

The eraser tool is the paint tool with both pen and brush colors set to white: Image1.canvas.pen.color := clWhite;

Where do we go from here?Edit

I hope you found the tutorial interesting and acquired some skill with Delphi.

If you wish to pursue Paint further, you will certainly want to get it to open JPG files. The BMP file format was used because it is the simplest to work with. If you wish to use JPG, put "JPEG" in the USES statement near the top of the program and give it a try. By a programming miracle called polymorphism, this is supposed to give LoadFromFile the ability to read a JPG file. (You might find that you have to load it into a separate Timage and then copy it to your Image1.)

TPicture has methods for copying to and from the Windows clipboard, so it should be straightforward to add those features - even selecting a rectangular area before copying. Perhaps you can even use the scrollbar properties for your mainform to accommodate images larger than the window (adjusting the image size upon opening a file or creating a new file). A clone brush for copying from one part of the image to another would make it useful for photoediting. If you do work further with Paint, I hope you will leave some messages or helpful notes in this wiki. I would be delighted to hear from you (harveybrown51 at gmail.com).

Around Wikia's network

Random Wiki