6.11.2013 How to create a Basic Paint Software in Lazarus

Today, in our special post, we see how to create a basic paint software in Lazarus with some basic drawing tools and Open, Save, Resize options. A must see for Lazarians!


Painting is a fun exercise of creativity. People form kids to professionals like to draw something just to have fun. Creating a drawing software is more fun than drawing (at least to me ;-) ). As the 50th post of LazPlanet, I am honored to present to you a simple paint software code made in Lazarus (Free Pascal). Enjoy!

The software answers the following questions:
  • How to open an image in a canvas?
  • How to create a pencil tool to draw in a canvas (for scribbling)?
  • How to create a line tool for drawing lines?
  • How to create a (Color) Flood Fill tool?
  • How to create a toolset with toggling buttons?
  • How to create a color picker tool?
  • How to make the tools work with the selected color?
  • How to make the tools work with the selected border/pen width?
  • ...and many more according to how used to coding you are.

The tools in the software are:
  • Pencil tool
  • Line tool
  • Rectangle tool
  • Circle tool
  • Triangle tool
  • Color dropper tool
  • Flood Fill (Color) tool

The toolbar buttons are:
  • New Image
  • Open Image
  • Resize Canvas
  • Paste Image from Clipboard
  • Save Image As
* The software only supports Bitmap files (.bmp). But you can extend the support by adding code for working with .jpg, .gif, .png, .ico etc.

I sure had fun making it. And while making I understood again that how Lazarus / FreePascal is helpful in making modern day graphical applications.

Now I want to share my enjoyment with you.

Oh! And you can also check out this post : How to scribble with a virtual pencil to have a knowledge on drawing on a virtual canvas that the computer offers. It will certainly make this project seem easier to you. It also has an excellent explanation of how to draw something when the user drags the mouse cursor.

Quick Tutorial

This is a big project having a slightly bigger collection of components. So I will discuss in brief about the creation process, so that this post does not become gigantic to swallow for Lazarians.

Form Design and Properties

Start Lazarus.

Here is a screenshot of the component's names and their type straight from the Object Inspector.

Objects for a basic paint project in Lazarus IDE

That seems to be a lot of components! But remember the impressive result that you will get after doing such a hard work. (Plus, keep imagining what the Photoshop and Gimp programmers has done for those software! They are indeed hard workers.)

Add those components and name them according to the screenshot. Here is a screenshot of the form's design view to make positioning the components easier for you:

Form layout for a basic paint project in Lazarus IDE form view


Use appropriate Glyphs/Icons for the toolbar icons. (I have used icons from Silk Companion and famfamfam Mini Icons collection. I have used these because they free for any project, even commercial ones. Cool, right?) I have included the icons that I have used in the sample code zip file available from below this article. They are in the "icons4u" folder.

Icons used in the basic paint project


Now we would make the tools' buttons to toggle on click. Have you noticed that when you select a tool in a drawing software (such as MS Paint/Gimp/Photoshop/Illustrator etc.) another tool gets de-selected. Select all the Tool Buttons and set its GroupIndex to 1. Select the first tool (Pencil tool SpeedButton). Make its Down property to True.


Select the TScrollbox. Make its HorzScrollBar->Tracking and VertScrollBar->Tracking to True. This will smoothen the scrolling of the canvas when its bigger than the ScrollBox's area. Set its Anchors->AkBottom and Anchors->AkRight to True. This will resize the ScrollBox with the form resize. Set its Color to clAppWorkspace.

You have created the TPaintbox *inside* the TScrollBox, right? If you haven't done so, then right click the TPaintbox/MyCanvas and select Change Parent-> CanvasScroller/TScrollbox. Then set its Left and Top to 0 (zero).

Set the "Value" and "MinValue" property of TSpinButton to 1.

Set Filter property of both OpenDialog1 and SaveDialog1 to:
Bitmap Files (*.bmp)|*.bmp

Without further ado let's get to coding--

Coding

Before proceeding with code, add these units to the uses clause: , Clipbrd, LCLIntf, LCLType.

uses
..., Clipbrd, LCLIntf, LCLType;

These are for using the clipboard.

Declare some variables after the line "Form1: TForm1;":
  paintbmp: TBitmap;

  MouseIsDown: Boolean;
  PrevX, PrevY: Integer;

paintbmp is our "virtual" canvas in which we will draw things. We will draw this exact TBitmap on TPaintBox's OnPaint event. If we don't do this, the TPaintbox will be blank when we move the window or resize it. MouseIsDown variable is to determine whether the user has pressed the mouse. PrevX and PrevY is where the user started the drag (on the canvas).

TIP: You may have to use Toggle Form/Unit button on the toolbar or press F12 to switch between Code and Form view.


Select MyCanvas and go to Object Inspector-> Events-> OnPaint-> [...] and enter:
procedure TForm1.MyCanvasPaint(Sender: TObject);
begin

  if MyCanvas.Width<>paintbmp.Width then begin
    MyCanvas.Width:=paintbmp.Width;
    // the resize will run this function again
    // so we skip the rest of the code
    Exit;

  end;

  if MyCanvas.Height<>paintbmp.Height then begin
    MyCanvas.Height:=paintbmp.Height;
    // the resize will run this function again
    // so we skip the rest of the code
    Exit;

  end;


  MyCanvas.Canvas.Draw(0,0,paintbmp);

end;

Again go to Object Inspector-> Events-> OnMouseDown-> [...] and enter:
procedure TForm1.MyCanvasMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  MouseIsDown := True;
  PrevX := X;
  PrevY := Y;

end;

Again go to Object Inspector-> Events-> OnMouseMove-> [...] and enter:
procedure TForm1.MyCanvasMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);

begin
  if MouseIsDown = true then begin

    //// Pencil Tool ////
    if ToolPencil.Down = true then begin
      paintbmp.Canvas.Line(PrevX, PrevY, X, Y);
      MyCanvas.Canvas.Line(PrevX, PrevY, X, Y);

      PrevX:=X;
      PrevY:=Y;

    //// Line Tool ////
    end else if ToolLine.Down = true then begin

      // we are clearing previous preview drawing
      MyCanvasPaint(Sender);

      // we draw a preview line
      MyCanvas.Canvas.Line(PrevX, PrevY, X, Y);


    //// Rectangle Tool ////
    end else if ToolRect.Down = true then begin

      MyCanvasPaint(Sender);
      MyCanvas.Canvas.Rectangle(PrevX, PrevY, X, Y);


    //// Oval Tool ////
    end else if ToolOval.Down = true then begin

      MyCanvasPaint(Sender);
      MyCanvas.Canvas.Ellipse(PrevX, PrevY, X, Y);


    //// Triangle Tool ////
    end else if ToolTriangle.Down = true then begin

      MyCanvasPaint(Sender);
      MyCanvas.Canvas.Line(PrevX,Y,PrevX+((X-PrevX) div 2), PrevY);
      MyCanvas.Canvas.Line(PrevX+((X-PrevX) div 2),PrevY,X,Y);
      MyCanvas.Canvas.Line(PrevX,Y,X,Y);

      //MyCanvas.Canvas.Ellipse(PrevX, PrevY, X, Y);


    end;


  end;


end;

Again go to Object Inspector-> Events-> OnMouseUp-> [...] and enter:
procedure TForm1.MyCanvasMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  TempColor: TColor;

begin

  if MouseIsDown then begin

    //// Line tool
    if ToolLine.Down = true then begin
      paintbmp.Canvas.Line(PrevX, PrevY, X, Y);
    //// Rect
    end else if ToolRect.Down = true then begin
      paintbmp.Canvas.Rectangle(PrevX, PrevY, X, Y);
    //// Oval tool
    end else if ToolOval.Down = true then begin
      paintbmp.Canvas.Ellipse(PrevX, PrevY, X, Y);

    //// Triangle tool
    end else if ToolTriangle.Down = true then begin
      paintbmp.Canvas.Line(PrevX,Y,PrevX+((X-PrevX) div 2), PrevY);
      paintbmp.Canvas.Line(PrevX+((X-PrevX) div 2),PrevY,X,Y);
      paintbmp.Canvas.Line(PrevX,Y,X,Y);

    //// Color Dropper Tool ////
    end else if ToolColorDropper.Down = true then begin
      LineColor.ButtonColor:=MyCanvas.Canvas.Pixels[X,Y];

    //// (Flood) Fill Tool ////
    end else if ToolFill.Down = true then begin
      TempColor := paintbmp.Canvas.Pixels[X, Y];
      paintbmp.Canvas.Brush.Style := bsSolid;
      paintbmp.Canvas.Brush.Color := LineColor.ButtonColor;
      paintbmp.Canvas.FloodFill(X, Y, TempColor, fsSurface);
      paintbmp.Canvas.Brush.Style := bsClear;
      MyCanvasPaint(Sender);

    end;

  end;

  MouseIsDown:=False;

end;

Double click btnNew and enter:
procedure TForm1.btnNewClick(Sender: TObject);
begin

    // if our bitmap is already Create-ed (TBitmap.Create)
    // then start fresh
    if paintbmp <> nil then
      paintbmp.Destroy;

    paintbmp := TBitmap.Create;

    paintbmp.SetSize(Screen.Width, Screen.Height);
    paintbmp.Canvas.FillRect(0,0,paintbmp.Width,paintbmp.Height);

    paintbmp.Canvas.Brush.Style:=bsClear;
    MyCanvas.Canvas.Brush.Style:=bsClear;

    MyCanvasPaint(Sender);

end;

Double click btnOpen and enter:
procedure TForm1.btnOpenClick(Sender: TObject);
begin

  OpenDialog1.Execute;

  if (OpenDialog1.Files.Count > 0) then begin

    if (FileExistsUTF8(OpenDialog1.FileName)) then begin
      paintbmp.LoadFromFile(OpenDialog1.FileName);
      MyCanvasPaint(Sender);

    end else begin
      ShowMessage('File is not found. You will have to open an existing file.');

    end;

  end;

end;

Double click btnResize and enter:
procedure TForm1.btnResizeClick(Sender: TObject);
var
  ww, hh: string;
  ww2, hh2: Integer;
  Code: Integer;

begin

  ww:=InputBox('Resize Canvas', 'Please enter the desired new width:', IntToStr(paintbmp.Width));

  Val(ww, ww2, Code);

  if Code <> 0 then begin
    ShowMessage('Error! Try again with an integer value of maximum '+inttostr(High(Integer)));
    Exit; // skip the rest of the code

  end;

  hh:=InputBox('Resize Canvas', 'Please enter the desired new height:', IntToStr(paintbmp.Height));

  Val(hh, hh2, Code);

  if Code <> 0 then begin
    ShowMessage('Error! Try again with an integer value of maximum '+inttostr(High(Integer)));
    Exit; // skip the rest of the code

  end;

  paintbmp.SetSize(ww2, hh2);
  MyCanvasPaint(Sender);

end;

Double click btnCopy and enter:
procedure TForm1.btnCopyClick(Sender: TObject);
begin
  Clipboard.Assign(paintbmp);

end;

Double click btnPaste and enter:
procedure TForm1.btnPasteClick(Sender: TObject);
var
  tempBitmap: TBitmap;
  PictureAvailable: boolean = False;

begin

  // we determine if any image is on clipboard
  if (Clipboard.HasFormat(PredefinedClipboardFormat(pcfDelphiBitmap))) or
    (Clipboard.HasFormat(PredefinedClipboardFormat(pcfBitmap))) then
    PictureAvailable := True;


  if PictureAvailable then
  begin

    tempBitmap := TBitmap.Create;

    if Clipboard.HasFormat(PredefinedClipboardFormat(pcfDelphiBitmap)) then
      tempBitmap.LoadFromClipboardFormat(PredefinedClipboardFormat(pcfDelphiBitmap));

    if Clipboard.HasFormat(PredefinedClipboardFormat(pcfBitmap)) then
      tempBitmap.LoadFromClipboardFormat(PredefinedClipboardFormat(pcfBitmap));

    // so we use assign, it works perfectly
    paintbmp.Assign(tempBitmap);

    MyCanvasPaint(Sender);

    tempBitmap.Free;

  end else begin

    ShowMessage('No image is found on clipboard!');

  end;

end;

Double click btnSave and enter:
procedure TForm1.btnSaveClick(Sender: TObject);
begin

  SaveDialog1.Execute;

  if SaveDialog1.Files.Count > 0 then begin
    // if the user enters a file name without a .bmp
    // extension, we will add it
    if RightStr(SaveDialog1.FileName, 4) <> '.bmp' then
      SaveDialog1.FileName:=SaveDialog1.FileName+'.bmp';

    paintbmp.SaveToFile(SaveDialog1.FileName);

  end;

end;

Select the LineColor (TColorButton) and go to Object Inspector-> Events-> OnColorChanged-> [...] and enter:
procedure TForm1.LineColorColorChanged(Sender: TObject);
begin

  paintbmp.Canvas.Pen.Color:=LineColor.ButtonColor;
  MyCanvas.Canvas.Pen.Color:=LineColor.ButtonColor;

end;

Select the SpinButton1 (TSpinButton) and go to Object Inspector-> Events-> OnChange-> [...] and enter:
procedure TForm1.SpinEdit1Change(Sender: TObject);
begin

  paintbmp.Canvas.Pen.Width:=SpinEdit1.Value;
  MyCanvas.Canvas.Pen.Width:=SpinEdit1.Value;

end;

Double click on the form to create a procedure for OnCreate event. Now enter the code below (the code will run on startup):
procedure TForm1.FormCreate(Sender: TObject);
begin

  // We create a new file/canvas/document when
  // it starts up
  btnNewClick(Sender);

end;

Select the form. Then go to Object Inspector-> Events-> OnClose-> [...] and enter the code below (This will run when the user exits the software):
procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin

  paintbmp.Free;

end;

Run it!

Run the project (F9 or Run-> Run).

Basic paint program with many drawing tools made with Lazarus IDE

Now, do your thing! Create your next masterpiece!

Further Practice

You can improve the code / project further by adding following features:
  • Multiple format support (.jpg, .png, .gif, .ico, .tif and more) Look here for how to
  • Gradient tool, Crop tool, Polygon tool, Eraser tool, Curve tool
  • Drawing by angle when Shift is pressed while drawing
  • Selection tool (Rectangle, Circle, Polygon, FreeHand)
  • Moving the selection area
  • Magnifier tool
  • Undo, Redo options
  • Filters

You can create practically a second Photoshop if you want!

Download Sample Code ZIP

You can download an example source code from here: http://db.tt/kZsqnbzw
Or here: http://bit.ly/ZHUouW
Size: 682 KB
The package includes compiled executable EXE file inside.

18 comments:

Carmelo Privitera said...

Nice tutorial!
But, how can i drawing a rectangle using real coordinates and not integer?
regards!
Carmelo

Adnan Shameem said...

Carmelo,
Thanks for reading the tutorial.

Can you specify what you mean by coordinates? May be providing some examples would help.

-Adnan

Carmelo Privitera said...

with this code i draw a rectangle:
panel1.canvas.Rectangle (x1, y1, x2, y2);
but, x1, x2, y1, y2 are integer.
In my program, this coordinates are real values.
Is it possible to draw a rectangle with real values?
regards ;)

Adnan Shameem said...

Carmelo,
Well, real value is not allowed in the Rectangle procedure. Here is the declaration from the Graphics unit:
procedure Rectangle(X1,Y1,X2,Y2: Integer); virtual; {$IFDEF HasFPCanvas1}reintroduce;{$ENDIF}
procedure Rectangle(const ARect: TRect); {$IFDEF HasFPCanvas1}reintroduce;{$ENDIF}


You can either pass integer values or TRect (which is ultimately a collection of 4 Integers).

The procedure doesn't accept floats due to the obvious reason that a pixel in the screen is, well, a pixel. You cannot divide it to make it half or something. Rectangle() draws the rectangle on pixels of the screen. That's why it can't accept a floating value.

What do you want to do exactly? Are you drawing a graph that shows the result of a solved formula, that returns a real?

As far as my understanding goes you have 2 options:

(1) If the decimal points are not that important to you then may be you should try round() to lose the decimals to make it an integer.

(2) Or if it's important then use that real number to generate integer that will be proportionate to the canvas size.

Consider the range that the real might have.

Let me know if it helps!

Carmelo Privitera said...

Hi Adnan!
Tanks to try to help me ;)
In my project its important to use decimal number, because, i want to draw section of concrete beam or pillar and they dimensions can have not only integer values.
Maybe, the right way is use Tchart component.
What do you think about Tchart?
Regards, and sorry for my bad english!
Carmelo

Adnan Shameem said...

"In my project its important to use decimal number, because, i want to draw section of concrete beam or pillar and they dimensions can have not only integer values."
-- I assume that you want to work with feet, for example, 12.23 feet. Now, let me echo myself again that you have to draw your rectangle in pixels and they cannot be divided. So, what to do? Well, the data will be as is. 12.23 ft will stay as 12.23 ft. But when drawing, you are drawing on a canvas, right? and a canvas has a dimension in integers. So why not think of the canvas as, for example, 20 ft wide, although in reality it is may be, say, 500px wide. So our measurement is 500px=20ft. It is somewhat like you see legends in a map which has a kilometer and miles indicated. But we see it as some inches long on the map. (For now we are only thinking of width, and leaving out height, for simplicity.)

Now, how much wide the rectangle should be? We know that our data says that it is 12.23 ft wide. So how to draw it in a 20ft canvas?

Easy, simple math! How did the 20ft became 500px in case of canvas? We have multiplied some number with 20 to get 500px. See the calculation below:

20x = 500 [x is our magic number]
=> 20x / 20 = 500 / 20
=> x = 25 [we got it!]

So, the factor is 25. Whenever you need to convert your feet data into integers for drawing in the canvas, you will have to multiply it with 25. That's it!

You can test out the above theory with the code below. Create a new project. Drop a TButton in your form, double click it and enter the code:

procedure TForm1.Button1Click(Sender: TObject);
var
factor: double;
begin
Width := 500;
factor := Width / 20; // So... 500 / 20

Canvas.Brush.Color:=clWhite;
Canvas.FillRect(0,0,Width,Height);

Canvas.Brush.Color:=clRed;
Canvas.FillRect(0, 0, round(12.23 * factor), 30);
// (30 is assumed height, just for testing.)
end;


The whole canvas will be white. Form will be resized to 500px. And a 12.23 ft rectangle will be drawn in an assumed 20ft wide canvas. Height is not implemented in the code (so I have assumed a height of 30).

I have not tested the code with many values. So go ahead, test it.

I have used round(), but it should not affect the measurements. Have a test with your values to see if it works flawlessly.

Good luck! Let me know if it works!

Carmelo Privitera said...

Dear Adnan,
thank to you i solved using round().
Here the code i used:


procedure TForm1.Button1Click(Sender: TObject);
begin
base:=strtofloat(edit1.Text);
altezza:=strtofloat(edit2.Text);
fx:=400;
fy:=400;
sc1:=fx/base;
sc2:=fy/altezza;
sc:=min(sc1, sc2);
lx:=base*sc;
ly:=altezza*sc;
xc:=200;
yc:=200;
x1:=xc-(lx/2); x2:=xc+(lx/2); y1:=yc-(ly/2); y2:=yc+(ly/2);
panel1.Repaint;
panel1.Canvas.Pen.Width:=2;
panel1.Canvas.Brush.color:= clblack;
panel1.canvas.brush.style:=bsclear;
panel1.canvas.Rectangle(round(x1)+25, round(y1)+25, round(x2)-25,round(y2)-25);
end;

regards
carmelo

Adnan Shameem said...

Carmelo,
Congrats that you got it right.
But in your code I see that you are using panel1.Canvas.Pen.Width:=2. Now, you are working in a project which requires precision. And the pen.width can visually increase the dimensions of the rectangle.

For example,
Canvas.Pen.Width:=2;
Canvas.Rectangle(10,10,60,60);


Should produce a 50x50 pixels rectangle, but it actually produces 51x51 px rectangle. Go ahead and try this code, then you can measure the dimensions of it in MS Paint, with the zoom tool then select tool. The statusbar shows the measurement of the selection.

If you set it to 1, it would be precise. But if you increase it, the actual border of the rectangle would be at the center of the border drawn. That means roughly half of the border would go outside the actual border.

If you want to set it to 2, then you will have to reduce the x1 and y1 values by 1.

Test it yourself and see what happens.

Carmelo Privitera said...

Thank you Adnan!
Your observation is correct, in fact, i already modified that width to 1 pixel!
Anyway, because my project is more complicated, i tried to use Tchart component, and maybe this way is better than Tcanvas.
Now i waiting for your new tutorial about Tchart and Ttreeview component ;)
Im glad if you want to help me..
Regards
Carmelo

Adnan Shameem said...

Carmelo,
That's a good thing that you found out your solution. I haven't worked with TChart before. But if it suits your needs then it's good for you.

Looking forward to learning usage of TChart. Also I will try to post about TTreeview.

If you face any further problem(s), I'll be happy to help you through the comments or the Facebook page.

Good luck!
With regards
Adnan

Jeremiah said...
This comment has been removed by the author.
st....t said...

This project is very nice.... but I noticed a problem: when drawing an object after scrolling the canvas, this is not placed in the correct position ... You can fix this error? Thank you very much!

Anonymous said...

Zoom
Olá muito obrgado pelo tutorial, tenho uma enorme duvida, como dar zomm??, muito obrigado!

Hello thank you for the tutorial, I have a huge doubts as to zomm ??, thank you!

Adnan Shameem said...

@st....t

Thanks for pointing that out. I will look into it for an update.

@O Tinteiro Digital

I have to admit this feature is a bit tricky. If I remember correctly Lazpaint has this feature. You can look into its source code for help:
https://sourceforge.net/projects/lazpaint/files/src/

Unknown said...
This comment has been removed by the author.
Unknown said...
This comment has been removed by the author.
Unknown said...

How with zoom code for mouse wheel?

Adnan Shameem said...

@Achmad Davit
I had some spare time. So you are lucky to get the solution in a tutorial:
http://lazplanet.blogspot.com/2017/05/make-simple-image-zoomer-in-5-minutes.html

Thanks for posting the problem.
Have a good day :)

 
Copyright 2013 LazPlanet
Carbon 12 Blogger template by Blogger Bits. Supported by Bloggermint