Developing With Graphics - Free Pascal Wiki
Developing With Graphics - Free Pascal Wiki
Developing With Graphics - Free Pascal Wiki
│ Deutsch (de) │ English (en) │ español (es) │ français (fr) │ italiano (it) │ ⽇本語 (ja) │ 한국어 (ko)
│ Nederlands (nl) │ português (pt) │ русский (ru) │ slovenčina (sk) │ 中⽂(中国⼤陆) (zh_CN) │
中⽂(台灣) (zh_TW) │
This page describes the basic classes and techniques regarding drawing graphics with
Lazarus. Other more specific topics are in separate articles.
Contents
▪ 1
Libraries
▪ 2
Introduction to the Graphics model of the LCL
▪ 3
Drawing a rectangle
▪ 4
Using the default GUI font
▪ 5
Drawing text to an exactly fitting width
▪ 6
Drawing text with sharp edges (non antialiased)
▪ 7
Drawing a polygon
▪ 7.1 Simple polygon
▪ 7.2 Self-overlapping polygons
▪ 7.3 Polygon with a hole
▪ 7.4 Polygon with several holes
▪ 8 Working with TBitmap and other TGraphic descendents
▪ 8.1 Loading/Saving an image from/to the disk
▪ 8.2 Additional file formats for TImage
▪ 8.3 Indirect pixel access
▪ 8.4 Direct pixel access
▪ 8.5 Drawing color transparent bitmaps
▪ 8.6 Taking a screenshot of the screen
▪ 9 Working with TLazIntfImage, TRawImage and TLazCanvas
▪ 9.1 Initializing a TLazIntfImage
▪ 9.2 TLazIntfImage.LoadFromFile
▪ 9.3 Loading a TLazIntfImage into a TImage
▪ 9.4 Fading example
▪ 9.5 Image format specific example
▪ 9.6 Conversion between TLazIntfImage and TBitmap
▪ 9.7 Using the non-native StretchDraw from LazCanvas
▪ 10 Motion Graphics - How to Avoid flickering
▪ 10.1 Draw to a TImage
▪ 10.1.1 Resizing the bitmap of a TImage
▪ 10.1.2 Painting on the bitmap of a TImage
▪ 10.1.3 Painting on the volatile visual area of the TImage
▪ 10.2 Draw on the OnPaint event
▪ 10.3 Create a custom control which draws itself
▪ 11 Image formats
▪ 11.1 Converting formats
▪ 12 Pixel Formats
▪ 12.1 TColor
▪ 12.2 TFPColor
▪ 13 Drawing with fcl-image
▪ 14 Common OnPaint Error
▪ 15 Some useful examples
▪ 15.1 Example 1: Drawing on loaded JPEG with TImage
▪ 15.2 Example 2: Drawing on controls of Form
▪ 16 See also
▪ 16.1 2D drawing
▪ 16.2 3D drawing
▪ 16.3 Charts
Libraries
Graphics libraries - here you can see the main graphic libraries you can use to develop.
TCanvas is a class capable of executing drawings. It cannot exist alone and must either
be attached to something visible (or at least which may possibly be visible), such as a
visual control descending from TControl, or be attached to an off-screen buffer from a
TRasterImage descendent (TBitmap is the most commonly used). TFont, TBrush and
TPen describe how the drawing of various operations will be executed in the Canvas.
TRasterImage (usually used via its descendant TBitmap) is a memory area reserved for
drawing graphics, but it is created for maximum compatibility with the native Canvas
and therefore in LCL-Gtk2 in X11 it is located in the X11 server, which makes pixel
access via the Pixels property extremely slow. In Windows it is very fast because
Windows allows creating a locally allocated image which can receive drawings from a
Windows Canvas.
Besides these there are also non-native drawing classes located in the units:
The main difference between the native classes and the non-native ones is that the
native ones do not perform exactly the same in all platforms, because the drawing is
done by the underlying platform itself. The speed and also the exact final result of the
image drawing can have differences. The non-native classes are guaranteed to perform
exactly the same drawing in all platforms with a pixel level precision and they all
perform reasonably fast in all platforms.
In the widgetset LCL-CustomDrawn the native classes are implemented using the non-
native ones.
Drawing a rectangle
Many controls expose their canvas as a public Canvas property (or via an OnPaint
event). Such controls include TForm, TPanel and TPaintBox. Let's use TForm as an
example to demonstrate how to paint on a canvas.
Suppose we want to draw a red rectangle with a 5-pixel-thick blue border in the center
of the form, and the the rectangle should be half the size of the form. For this purpose
we must add code to the OnPaint event of the form. Never paint in an OnClick handler,
because this painting is not persistent and will be erased whenever the operating
system requests a repaint. Always paint in the OnPaint event!
The TCanvas method for painting a rectangle is named very logically: Rectangle(). You
can pass rectangle's edge coordinates to the method either as four separate x/y values,
or as a single TRect record. The fill color is determined by the color of the canvas's
Brush, and the border color is given by the color of the canvas's Pen:
SelectObject(Canvas.Handle, GetStockObject(DEFAULT_GUI_FONT));
or:
Canvas.Font.Name := 'default';
Canvas.Font.Quality := fqNonAntialiased;
Some widgetsets like the gtk2 do not support this and always paint antialiased. Here is
a simple procedure to draw text with sharp edges under gtk2. It does not consider all
cases, but it should give an idea:
Drawing a polygon
Simple polygon
A polygon is drawn by the Polygon method of the canvas. The polygon is defined by an
array of points (TPoint) which are connected by straight lines drawn with the current Pen,
and the inner area is filled by the current Brush. The polygon is closed automatically, i.e.
the last array point does not necessarily need to coincide with the first point (although
there are cases where this is required -- see below).
Example: Pentagon
begin
for i := 0 to 4 do
begin
phi := 2.0 * pi / 5 * i + pi * 0.5;;
P[i].X := round(100 * cos(phi) + 110);
P[i].Y := round(100 * sin(phi) + 110);
end;
Canvas.Brush.Color := clRed;
Canvas.Polygon(P);
end;
Self-overlapping polygons
procedure TForm1.FormPaint(Sender:
TObject);
var
P: Array[0..4] of TPoint;
P1, P2: Array[0..4] of TPoint;
i: Integer;
phi: Double;
begin
for i := 0 to 4 do
begin
phi := 2.0 * pi / 5 * i + pi *
0.5;;
P[i].X := round(100 * cos(phi) +
110);
P[i].Y := round(100 * sin(phi) +
110);
end;
P1[0] := P[0];
P1[1] := P[2];
P1[2] := P[4];
P1[3] := P[1];
P1[4] := P[3];
for i:= 0 to 4 do P2[i] :=
Point(P1[i].X + 200, P1[i].Y); //
offset polygon
Canvas.Brush.Color := clRed;
Canvas.Polygon(P1, false);
Canvas.Polygon(P2, true);
end;
Suppose you want to draw the shape of a country with a large lake inside from both of
which you have some boundary points. Basically the Polygon() method of the LCL
canvas is ready for this task. However, you need to consider several important points:
▪ You must prepare the array of polygon vertices such that each polygon is closed
(i.e. last point = first point), and that both first and last polygon points are
immediately adjacent in the array.
▪ The order of the inner and outer polygon points in the array does not matter.
▪ Make sure that both polygons have opposite orientations, i.e. if the outer polygon
has its vertices in clockwise order, then the inner polygon must have the points in
counter-clockwise order.
Example:
const
P: array of [0..8] of TPoint = (
// outer polygon: a rectangle
(X: 10; Y: 10), // <--- first point of the rectangle
(X:190; Y: 10),
(X:190; Y:190), // (clockwise orientation)
(X: 10; Y:190),
(X: 10; Y: 10), // <--- last point of the rectangle = first point
You may notice that there is a line connecting the starting point of the inner triangle
back to the starting point of the outer rectangle (marked by a blue circle in the
screenshot). This is because the Polygon() method closes the entire polygon, i.e. it
connects the very first with the very last array point. You can avoid this by drawing the
polygon and the border separately. To draw the fill the Pen.Style should be set to psClear
to hide the outline. The PolyLine() method can be used to draw the border; this method
needs arguments for the starting point index and also a count of the array points to be
drawn.
Canvas.Pen.Style := psSolid;
Canvas.Pen.Color := clBlack;
Canvas.Polyline(Pts, 0, 5); // rectangle starts at index 0 and
consists of 5 array elements
Canvas.Polyline(Pts, 5, 4); // triangle starts at index 5 and
consists of 4 array elements
end;
Applying the rules for the single hole in a polygon, we extend the example from the
previous section by adding two more triangles inside the outer rectangle. These
triangles have the same orientation as the first triangle, opposite to the outer rectangle,
and thus should be considered to be holes.
const
Pts: array[0..16] of TPoint = (
// outer polygon: a rectangle
(X: 10; Y: 10), // clockwise
(X:190; Y: 10),
(X:190; Y:190),
(X: 10; Y:190),
(X: 10; Y: 10),
Rendering this by a simple Polygon() fill is disappointing because there are new
additional areas with are not expected. The reason is that this model does not return to
the starting point correctly. The trick is to add two further points (one per shape). These
are added to the above single-hole-in-polygon case: the first additional point duplicates
the first point of the 2nd inner triangle, and the second additional point duplicates the
first point of the 1st inner triangle. By so doing, the polygon is closed along the
imaginary path the holes were connected by initially, and no additional areas are
introduced:
const
Pts: array[0..18] of TPoint = (
// outer polygon: a rectangle
(X: 10; Y: 10), // clockwise
(X:190; Y: 10),
(X:190; Y:190),
(X: 10; Y:190),
(X: 10; Y: 10),
var
MyBitmap: TBitmap;
begin
MyBitmap := TBitmap.Create;
try
// Load from disk
MyBitmap.LoadFromFile(MyEdit.Text);
When using any other format the process is completely identical, just use the adequate
class. For example, for PNG images:
var
MyPNG: TPortableNetworkGraphic;
begin
MyPNG := TPortableNetworkGraphic.Create;
try
// Load from disk
MyPNG.LoadFromFile(MyEdit.Text);
If you do not know beforehand the format of the image, use TPicture which will
determine the format based in the file extension. Note that TPicture does not support all
formats supported by Lazarus, as of Lazarus 0.9.31 it supports BMP, PNG, JPEG,
Pixmap and PNM while Lazarus also supports ICNS and other formats:
var
MyPicture: TPicture;
begin
MyPicture := TPicture.Create;
try
// Load from disk
MyPicture.LoadFromFile(MyEdit.Text);
The canvas of a form cannot be saved directly into a file and must fist be copied into an
appropriate image. The following procedure copies a rectangle of the form's canvas into
a .PNG file (which compresses text and graphs with good quality). If you wish to copy
into a .JPG file (ideal for compressing photographs) instead of
TPortableNetworkGraphic use tJPEGimage. If you wish to copy into a .BMP file (which
does not compress the canvas) instead of TPortableNetworkGraphic use tBitmap:
procedure saveCanvasToPNGfile(fromCanvas:tCanvas;left,bottom,right,top:integer;toFileName:string);
//left,bottom,right,top define the rectangle of the canvas to be save into disk
var rectangleFrom,rectangleTo: TRect;
myImage: TPortableNetworkGraphic; //use tJPEGimage to create JPGfile, or tBitmap to create BMP file
begin
rectangleFrom:=rect(left,bottom,right,top);
rectangleTo:=rect(0,0,right-left+1,top-bottom+1);
myImage:=tPortableNetworkGraphic.create;
myImage.width:=right-left+1;
myImage.height:=top-bottom+1;
myImage.canvas.copyRect(rectangleTo,fromCanvas,rectangleFrom);
myImage.SaveToFile(toFileName);
myImage.free;
end;
You can add additional file format support by adding the fcl-image fpread* and/or
fpwrite* units to your uses clause. In this way, you can e.g. add support for TIFF for
TImage
Color for pixels can be simply read or written using TBitmap.Canvas.Pixels property.
Access using Pixels property is generally slow as lot of internal management operations
need to be done on each execution. This can be speed up little bit be enclosing access
to Pixels with TBitmap BeginUpdate and EndUpdate.
On some Lazarus widgetsets (notably LCL-Gtk2), the bitmap data is not stored in
memory location which can be accessed by the application and in general the LCL
native interfaces draw only through native Canvas routines, so each SetPixel / GetPixel
operation involves a slow call to the native Canvas API. In LCL-CustomDrawn this is
not the case since the bitmap is locally stored for all backends and SetPixel / GetPixel
is fast. For obtaining a solution which works in all widgetsets one should use
TLazIntfImage. As Lazarus is meant to be platform independent and work in gtk2.
There is a GetDataLineStart function, equivalent to Scanline, but only available for
memory images like TLazIntfImage which internally uses TRawImage.
The following example loads a bitmap from a Windows resource, selects a color to be
transparent (clFuchsia) and then draws it to a canvas.
bmp.Transparent := True;
bmp.TransparentColor := clFuchsia;
MyCanvas.Draw(0, 0, bmp);
Notice the memory operations performed with the TMemoryStream. They are
necessary to ensure the correct loading of the image.
Since Lazarus 0.9.16 you can use LCL to take screenshots of the screen in a cross-
platform way. The following example code does it:
Taking a screen shot but excluding your application from the screenshot:
uses
Graphics, LCLIntf, LCLType, ...;
TRawImage is of the type object and therefore does not need to be created nor freed. It
can either allocate the image memory itself when one calls TRawImage.CreateData or
one can pass a memory block allocated for examply by a 3rd party library such as the
Windows API of the Cocoa Framework from Mac OS X and pass the information of the
image in TRawImage.Description, TRawImage.Data and TRawImage.DataSize.
Instead of attaching it to a RawImage one could also load it from a TBitmap which will
copy the data from the TBitmap and won't be syncronized with it afterwards. The
TLazCanvas cannot exist alone and must always be attached to a TLazIntfImage.
The example below shows how to choose a format for the data and ask the
TRawImage to create it for us and then we attach it to a TLazIntfImage and then attach
a TLazCanvas to it:
var
AImage: TLazIntfImage;
ACanvas: TLazCanvas;
lRawImage: TRawImage;
begin
lRawImage.Init;
lRawImage.Description.Init_BPP32_A8R8G8B8_BIO_TTB(AWidth, AHeight);
lRawImage.CreateData(True);
AImage := TLazIntfImage.Create(0,0);
AImage.SetRawImage(lRawImage);
ACanvas := TLazCanvas.Create(AImage);
Initializing a TLazIntfImage
One cannot simply create an instance of TLazIntfImage and start using it. It needs to
add a storage to it. There are 3 ways to do this:
1. Attach it to a TRawImage
2. Load it from a TBitmap. Note that it will copy the memory of the TBitmap so it won't
remain connected to it.
SrcIntfImg:=TLazIntfImage.Create(0,0);
SrcIntfImg.LoadFromBitmap(ABitmap.Handle,ABitmap.MaskHandle);
TLazIntfImage.LoadFromFile
Here is an example how to load an image directly into a TLazIntfImage. It initializes the
TLazIntfImage to a 32bit RGBA format. Keep in mind that this is probably not the native
format of your screen.
The pixel data of a TImage is the TImage.Picture property, which is of type TPicture.
TPicture is a multi format container containing one of several common image formats
like Bitmap, Icon, Jpeg or PNG . Usually you will use the TPicture.Bitmap to load a
TLazIntfImage:
Image1.Picture.Bitmap.LoadFromIntfImage(IntfImg);
Notes:
Fading example
A fading example with TLazIntfImage
If you know that the TBitmap is using blue 8bit, green 8bit, red 8bit you can directly
access the bytes, which is somewhat faster:
IntfImg2:=TLazIntfImage.Create(0,0);
IntfImg2.LoadFromBitmap(aBitmap.Handle,aBitmap.MaskHandle);
TempBitmap:=TBitmap.Create;
//with Scanline-like
for FadeStep:=1 to 32 do begin
for py:=0 to IntfImg1.Height-1 do begin
Row1 := IntfImg1.GetDataLineStart(py); //like Delphi TBitMap.ScanLine
Row2 := IntfImg2.GetDataLineStart(py); //like Delphi TBitMap.ScanLine
for px:=0 to IntfImg1.Width-1 do begin
Row2^[px].rgbtRed:= (FadeStep * Row1^[px].rgbtRed) shr 5;
Row2^[px].rgbtGreen := (FadeStep * Row1^[px].rgbtGreen) shr 5; // Fading
Row2^[px].rgbtBlue := (FadeStep * Row1^[px].rgbtBlue) shr 5;
end;
end;
IntfImg2.CreateBitmaps(ImgHandle,ImgMaskHandle,false);
TempBitmap.Handle:=ImgHandle;
TempBitmap.MaskHandle:=ImgMaskHandle;
Canvas.Draw(0,0,TempBitmap);
end;
IntfImg1.Free;
IntfImg2.Free;
TempBitmap.Free;
end;
Since Lazarus has no TBitmap.ScanLines property, the best way to access the pixels of
an image in a fast way for both reading and writing is by using TLazIntfImage. The
TBitmap can be converted to a TLazIntfImage by using TBitmap.CreateIntfImage() and
after modifying the pixels it can be converted back to a TBitmap by using
TBitmap.LoadFromIntfImage(); Here's the sample on how to create TLazIntfImage from
TBitmap, modify it and then go back to the TBitmap.
uses
...GraphType, IntfGraphics, LCLType, LCLProc, LCLIntf ...
b.LoadFromIntfImage(t);
finally
t.Free;
b.Free;
end;
end;
TLazCanvas.Interpolation := TFPSharpInterpolation.Create;
DestCanvas := TLazCanvas.Create(DestIntfImage);
SourceIntfImage.Free;
DestCanvas.Interpolation.Free;
DestCanvas.Free;
DestIntfImage.Free;
end;
Bmp := TBitmap.Create;
Bmp.Width := 10;
Bmp.Height := 10;
Bmp.Canvas.Pen.Color := clYellow;
Bmp.Canvas.Brush.Color := clYellow;
Bmp.Canvas.Rectangle(0, 0, 10, 10);
StretchDrawBitmapToBitmap(Bmp, DestBitmap, 100, 100);
Canvas.Draw(0, 0, Bmp);
Canvas.Draw(100, 100, DestBitmap);
end;
Another solution is drawing to a TCanvas. If you need help with OpenGL, take a look at
the example that comes with Lazarus. You can also use A.J. Venter's gamepack, which
provides a double-buffered canvas and a sprite component.
▪ Draw to a TImage
▪ Draw on the OnPaint event of the form, a TPaintBox or another control
▪ Create a custom control which draws itself
▪ Using A.J. Venter's gamepack
Draw to a TImage
Important: Never use the OnPaint of the Image1 event to draw to the graphic/bitmap of
a TImage. The graphic of a TImage is buffered so all you need to do is draw to it from
anywhere and the change is there forever. However, if you are constantly redrawing,
the image will flicker. In this case you can try the other options. Drawing to a TImage is
considered slower then the other approaches.
Note: Inside of Image1.OnPaint the Image1.Canvas points to the volatile visible area.
Outside of Image1.OnPaint the Image1.Canvas points to Image1.Picture.Bitmap.Canvas.
Another example:
// Draws squares
MyImage.Canvas.Pen.Color := clBlack;
for x := 1 to 8 do
for y := 1 to 8 do
MyImage.Canvas.Rectangle(Round((x - 1) * Image.Width / 8), Round((y - 1) * Image.Height / 8),
Round(x * Image.Width / 8), Round(y * Image.Height / 8));
end;
You can only paint on this area during OnPaint. OnPaint is eventually called
automatically by the LCL when the area was invalidated. You can invalidate the area
manually with Image1.Invalidate. This will not immediately call OnPaint and you can call
Invalidate as many times as you want.
In this case all the drawing has to be done on the OnPaint event of the form, or of
another control. The drawing isn't buffered like in the TImage, and it needs to be fully
redrawn in each call of the OnPaint event handler.
Creating a custom control has the advantage of structuring your code and you can
reuse the control. This approach is very fast, but it can still generate flickering if you
don't draw to a TBitmap first and then draw to the canvas. On this case there is no
need to use the OnPaint event of the control.
uses
Classes, SysUtils, Controls, Graphics, LCLType;
type
TMyDrawingControl = class(TCustomControl)
public
procedure EraseBackground(DC: HDC); override;
procedure Paint; override;
end;
implementation
procedure TMyDrawingControl.Paint;
var
x, y: Integer;
Bitmap: TBitmap;
begin
Bitmap := TBitmap.Create;
try
// Initializes the Bitmap Size
Bitmap.Height := Height;
Bitmap.Width := Width;
// Draws squares
Bitmap.Canvas.Pen.Color := clBlack;
for x := 1 to 8 do
for y := 1 to 8 do
Bitmap.Canvas.Rectangle(Round((x - 1) * Width / 8), Round((y - 1) * Height / 8),
Round(x * Width / 8), Round(y * Height / 8));
Canvas.Draw(0, 0, Bitmap);
finally
Bitmap.Free;
end;
inherited Paint;
end;
Setting Top and Left to zero is not necessary, since this is the standard position, but is
done so to reinforce where the control will be put.
"MyDrawingControl.Parent := Self;" is very important and you won't see your control if
you don't do so.
Image formats
Here is a table with the correct class to use for each image format.
Converting formats
Sometimes it is necessary to convert one graphic type to another. One of the ways is to
convert a graphic to intermediate format, and then convert it to TBitmap. Most of the
formats can create an image from TBitmap.
Pixel Formats
TColor
The internal pixel format for TColor in the LCL is the XXBBGGRR format, which
matches the native Windows format and is opposite to most other libraries, which use
AARRGGBB. The XX part is used to identify if the color is a fixed color, which case XX
should be 00 or if it is an index to a system color. There is no space reserved for an
alpha channel.
To get each channel of a TColor variable use the Red, Green and Blue functions:
RedVal := Red(MyColor);
GreenVal := Green(MyColor);
BlueVal := Blue(MyColor);
TFPColor
TFPColor uses the AARRGGBB format common to most libraries, but it uses 16-bits for
the depth of each color channel, totaling 64-bits per pixel, which is unusual. This does
not necessarily mean that images will consume that much memory, however. Images
created using TRawImage+TLazIntfImage can have any internal storage format and
then on drawing operations TFPColor is converted to this internal format.
The unit Graphics provides routines to convert between TColor and TFPColor:
This is bad:
This is good:
Add procedure LoadAndDraw to the public section of your form, and paste next code to
implemantation section:
Image.Picture.Bitmap.SetSize(jpg.Width, jpg.Height);
Image.Picture.Bitmap.Canvas.Draw(0, 0, jpg.Bitmap);
Image.Picture.Bitmap.Canvas.Pen.Color := clRed;
Image.Picture.Bitmap.Canvas.Line(0, 0, 140, 140);
finally
jpg.Free;
end;
end;
1) Create a New project - Application, add to uses section next modules if needed:
Types, Controls, Graphics.
RadioGroup1.Items.AddObject(Button2.Name,Button2);
end;
2D drawing
3D drawing
Charts