Developing with Graphics
│
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 will be the start of tutorials with regard to manipulating Bitmaps and other graphics. As I am not a graphics programmer, I invite all who are to share their expertise! Just add a link to the next section, add a page and create your own WiKi article.
On this page some general information will be given.
Other graphics articles
- BGRABitmap - Drawing shapes and bitmaps with transparency, direct access to pixels, etc.
- GLScene - A port of the visual OpenGL graphics Library GLScene
- TAChart - Charting component for Lazarus
- PascalMagick - an easy to use API for interfacing with ImageMagick, a multiplatform free software suite to create, edit, and compose bitmap images.
- PlotPanel - A plotting and charting component for animated graphs
- LazRGBGraphics - A package for fast in memory image processing and pixel manipulations (like scan line).
- Perlin Noise - An article about using Perlin Noise on LCL applications.
Working with TBitmap
On some operating systems, the bitmap data is not stored in memory so it cannot be accessed directly. As Lazarus is meant to be platform independent, the TBitmap class does not provide a property like Scanline. There is a GetDataLineStart function, equivalent to Scanline, but only available for memory images like TLazIntfImage which internally uses TRawImage.
To sum it up, you can only change a bitmap indirectly, by modifying a memory bitmap, then convert it to a drawable bitmap. This process is of course slower. There are some library that allows you to do direct access to bitmaps, but they may need to implicitely convert bitmap data (see BGRABitmap, LazRGBGraphics and maybe some day Graphics32).
Note : when you create a bitmap, you must specify the height and width, otherwise it will be zero and nothing will be drawn.
A fading example
Say you want to make a Fading picture. In Delphi you could do something like:
procedure TForm1.FadeIn(ABitmap: TBitmap); Const NbSteps = 32; var FadeBmp, CopyBmp: TBitmap; PFade, PCopy: PRGBTriple; x, y: integer; Step: Longword; //to simplify 'div' instruction begin //make a 24-bit copy of the image to be faded CopyBmp := TBitmap.Create; CopyBmp.PixelFormat := pf24bit; CopyBmp.Width := ABitmap.Width; CopyBmp.Height := ABitmap.Height; CopyBmp.Canvas.Draw(0,0,ABitmap); //create a fading bitmap FadeBmp := TBitmap.Create; FadeBmp.PixelFormat := pf24bit; FadeBmp.Width := ABitmap.Width; FadeBmp.Height := ABitmap.Height; for step := 0 to NbSteps do //do the fade in begin for y := 0 to FadeBmp.Height - 1 do begin PCopy := CopyBmp.Scanline[y]; PFade := FadeBmp.Scanline[y]; for x := 0 to FadeBmp.Width - 1 do begin PFade^.rgbtRed := (step * PCopy^.rgbtRed) div NbSteps; PFade^.rgbtGreen := (step * PCopy^.rgbtGreen) div NbSteps; // Fading PFade^.rgbtBlue := (step * PCopy^.rgbtBlue) div NbSteps; inc(PCopy); // go to next pixel inc(PFade); end; end; Form1.Canvas.Draw(0, 0, FadeBmp); Sleep(16); // wait for screen refresh Application.ProcessMessages; // handles windows events end; FadeBmp.Free; CopyBmp.Free; end;
This function in Lazarus could be implemented like:
{ This code has been taken from the $LazarusPath/examples/lazintfimage/fadein1.lpi project. } uses LCLType, // HBitmap type IntfGraphics, // TLazIntfImage type fpImage; // TFPColor type ... procedure TForm1.FadeIn(ABitMap: TBitMap); var SrcIntfImg, TempIntfImg: TLazIntfImage; ImgHandle,ImgMaskHandle: HBitmap; FadeStep: Integer; px, py: Integer; CurColor: TFPColor; TempBitmap: TBitmap; begin SrcIntfImg:=TLazIntfImage.Create(0,0); SrcIntfImg.LoadFromBitmap(ABitmap.Handle,ABitmap.MaskHandle); TempIntfImg:=TLazIntfImage.Create(0,0); TempIntfImg.LoadFromBitmap(ABitmap.Handle,ABitmap.MaskHandle); TempBitmap:=TBitmap.Create; for FadeStep:=1 to 32 do begin for py:=0 to SrcIntfImg.Height-1 do begin for px:=0 to SrcIntfImg.Width-1 do begin CurColor:=SrcIntfImg.Colors[px,py]; CurColor.Red:=(CurColor.Red*FadeStep) shr 5; CurColor.Green:=(CurColor.Green*FadeStep) shr 5; CurColor.Blue:=(CurColor.Blue*FadeStep) shr 5; TempIntfImg.Colors[px,py]:=CurColor; end; end; TempIntfImg.CreateBitmaps(ImgHandle,ImgMaskHandle,false); TempBitmap.Handle:=ImgHandle; TempBitmap.MaskHandle:=ImgMaskHandle; Canvas.Draw(0,0,TempBitmap); end; SrcIntfImg.Free; TempIntfImg.Free; TempBitmap.Free; end;
A faster version :
//--------------------------------------------------------------------------- //With Scanline-like by bruce0829@yahoo.com.tw //example(win32) download : //http://digitraveler.homelinux.com/down_load/LazScanLineTest.zip //--------------------------------------------------------------------------- uses LCLType, // HBitmap type IntfGraphics, // TLazIntfImage type fpImage; // TFPColor type ... type PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[0..32767] of TRGBTriple; procedure TForm1.FadeIn2(aBitMap: TBitMap); var IntfImg1, IntfImg2: TLazIntfImage; ImgHandle,ImgMaskHandle: HBitmap; FadeStep: Integer; px, py: Integer; CurColor: TFPColor; TempBitmap: TBitmap; Row1, Row2: PRGBTripleArray; begin IntfImg1:=TLazIntfImage.Create(0,0); IntfImg1.LoadFromBitmap(aBitmap.Handle,aBitmap.MaskHandle); 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;
Using BGRABitmap, the fade in can be implemented this way :
procedure TForm1.FadeIn3(ABitmap: TBitmap); Const NbSteps = 32; var CopyBmp,FadeBmp: TBGRABitmap; PFade, PSource: PBGRAPixel; n, step: Longword; begin CopyBmp := TBGRABitmap.Create(ABitmap.Width,ABitmap.Height); CopyBmp.GetImageFromCanvas(ABitmap.Canvas,0,0); //create a fading bitmap FadeBmp := TBGRABitmap.Create(ABitmap.Width,ABitmap.Height); for step := 0 to NbSteps do //do the fade in begin PSource := CopyBmp.Data; PFade := FadeBmp.Data; for n := 0 to FadeBmp.NbPixels - 1 do begin PFade^.red := (step * PSource^.red) div NbSteps; PFade^.green := (step * PSource^.green) div NbSteps; // Fading PFade^.blue := (step * PSource^.blue) div NbSteps; inc(PSource); // go to next pixel inc(PFade); end; FadeBmp.InvalidateBitmap; FadeBmp.Draw(Form1.Canvas,0,0,true); Sleep(16); // wait for screen refresh Application.ProcessMessages; // handles events end; FadeBmp.Free; CopyBmp.Free; end;
Drawing color transparent bitmaps
A new feature, implemented on Lazarus 0.9.11, is color transparent bitmaps. Bitmap files (*.BMP) cannot store any information about transparency, but they can work as they had if you select a color on them to represent the transparent area. This is a common trick used on Win32 applications.
The following example loads a bitmap from a Windows resource, selects a color to be transparent (clFuchsia) and then draws it to a canvas.
procedure MyForm.MyButtonOnClick(Sender: TObject); var buffer: THandle; bmp: TBitmap; memstream: TMemoryStream; begin bmp := TBitmap.Create; buffer := Windows.LoadBitmap(hInstance, MAKEINTRESOURCE(ResourceID)); if (buffer = 0) then exit; // Error loading the bitmap bmp.Handle := buffer; memstream := TMemoryStream.create; try bmp.SaveToStream(memstream); memstream.position := 0; bmp.LoadFromStream(memstream); finally memstream.free; end; bmp.Transparent := True; bmp.TransparentColor := clFuchsia; MyCanvas.Draw(0, 0, bmp); bmp.Free; // Release allocated resource end;
Notice the memory operations performed with the TMemoryStream. They are necessary to ensure the correct loading of the image.
Taking a screenshot of the screen
Since Lazarus 0.9.16 you can use LCL to take screenshots of the screen on a cross-platform way. The following example code does it (works on gtk2 and win32, but not gtk1 currently):
uses LCLIntf, LCLType; ... var MyBitmap: TBitmap; ScreenDC: HDC; begin MyBitmap := TBitmap.Create; ScreenDC := GetDC(0); MyBitmap.LoadFromDevice(ScreenDC); ReleaseDC(ScreenDC); ...
Draw TLazIntfImage to Canvas
Since ScanLines property has been temporary removed from TBitmap class, the only way to access Bitmap scanline data, is using TLazIntfImage. Here's the sample on how to create TLazIntfImage from TBitmap, and then draw TLazIntfImage to the Canvas.
uses ...GraphType, IntfGraphics, LCLType, LCLProc, LCLIntf ... procedure TForm1.Button4Click(Sender: TObject); var b: TBitmap; t: TLazIntfImage; bmp, old: HBitmap; msk: HBitmap; tmpDC: HDC; begin b := TBitmap.Create; try b.LoadFromFile('test.bmp'); t := b.CreateIntfImage; t.CreateBitmaps(bmp, msk, false); tmpDC := CreateCompatibleDC(Canvas.Handle); old := SelectObject(tmpDC, bmp); BitBlt(Canvas.Handle, 0, 0, t.Width, t.Height, tmpDC, 0, 0, SRCCOPY); DeleteObject(SelectObject(tmpDC, old)); DeleteObject(msk); DeleteDC(tmpDC); finally t.Free; b.Free; end; end;
Motion Graphics - How to Avoid flickering
Many programs draw their output to the GUI as 2D graphics. If those graphics need to change quickly you will soon face a problem: quickly changing graphics often flicker on the screen. This happens when users sometimes sees the whole images and sometimes only when it is partially drawn. It occurs because the painting process requires time.
But how can I avoid the flickering and get the best drawing speed? Of course you could work with hardware acceleration using OpenGL, but this approach is quite heavy for small programs or old computers. This tutorial will focus on 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.
Now we will examine the options we have for drawing to a Canvas:
- 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
A TImage consists of 2 parts: A TGraphic, usually a TBitmap, holding the persistent picture and the visual area, which is repainted on every OnPaint. Resizing the TImage does not resize the bitmap. The graphic (or bitmap) is accessible via Image1.Picture.Graphic (or Image1.Picture.Bitmap). The canvas is Image1.Picture.Bitmap.Canvas. The canvas of the visual area of a TImage is only accessible during Image1.OnPaint via Image1.Canvas.
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.
Resizing the bitmap of a TImage
Note: Do not use this during OnPaint.
with Image1.Picture.Bitmap do begin Width:=100; Height:=120; end;
Painting on the bitmap of a TImage
Note: Do not use this during OnPaint.
with Image1.Picture.Bitmap.Canvas do begin // fill the entire bitmap with red Brush.Color:=clRed; FillRect(0,0,Width,Height); end;
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:
procedure TForm1.BitBtn1Click(Sender: TObject);
var
x, y: Integer;
begin
// Draws the backgroung
MyImage.Canvas.Pen.Color := clWhite;
MyImage.Canvas.Rectangle(0, 0, Image.Width, Image.Height);
// 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;
Painting on the volatile visual area of the TImage
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.
procedure TForm.Image1Paint(Sender: TObject); begin with Image1.Canvas do begin // paint a line Pen.Color:=clRed; Line(0,0,Width,Height); end; end;
Draw on the OnPaint event
In this case all the drawing has to be done on the OnPaint event of the form. It doesn't remain on the buffer, like on the TImage.
Create a custom control which draws itself
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.
Here is an example custom control:
uses
Classes, SysUtils, Controls, Graphics, LCLType;
type
TMyDrawingControl = class(TCustomControl)
public
procedure EraseBackground(DC: HDC); override;
procedure Paint; override;
end;
implementation
procedure TMyDrawingControl.EraseBackground(DC: HDC);
begin
// Uncomment this to enable default background erasing
//inherited EraseBackground(DC);
end;
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 the background
Bitmap.Canvas.Pen.Color := clWhite;
Bitmap.Canvas.Rectangle(0, 0, Width, Height);
// 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;
and how we create it on the form:
procedure TMyForm.FormCreate(Sender: TObject);
begin
MyDrawingControl:= TMyDrawingControl.Create(Self);
MyDrawingControl.Height := 400;
MyDrawingControl.Width := 500;
MyDrawingControl.Top := 0;
MyDrawingControl.Left := 0;
MyDrawingControl.Parent := Self;
MyDrawingControl.DoubleBuffered := True;
end;
It is destroyed automatically, because we use Self as owner.
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.
"MyDrawingControl.DoubleBuffered := True;" is required to avoid flickering on Windows. It has no effect on gtk.
Using A.J. Venter's gamepack
The gamepack approach is to draw everything to one double-buffered canvas, which only gets updated to the visible canvas when you are ready. This takes quite a bit of code, but it has the advantage of being able to do large rapidly changing scenes with multiple sprites on them. If you wish to use this approach, you may be interested in A.J. Venter's gamepack, a set of components for game development in Lazarus, which provides a double-buffered display area component as well as a sprite component, designed to integrate well with one another. You can get gamepack via subversion:
svn co svn://silentcoder.co.za/lazarus/gamepack
You can get more information, documentation and downloads the homepage.
Image formats
Here is a table with the adequate class to use for each image format.
Format | Image class | Unit |
---|---|---|
Cursor (cur) | TCursor | Graphics |
Bitmap (bmp) | TBitmap | Graphics |
Windows icon (ico) | TIcon | Graphics |
Mac OS X icon (icns) | TicnsIcon | Graphics |
Pixmap (xpm) | TPixmap | Graphics |
Portable Network Graphic (png) | TPortableNetworkGraphic | Graphics |
JPEG (jpg, jpeg) | TJpegImage | Graphics |
PNM (pnm) | TPortableAnyMapGraphic | Graphics |
Converting formats
Sometimes it must be necessary to convert one graphic type to another. One of the way is convert a graphic to intermediate format, and the convert it to TBitmap. Most of format can create an image from TBitmap.
Converting Bitmap to PNG and saving it to a file:
<delphi> procedure SaveToPng(const bmp: TBitmap; PngFileName: String); var
png : TPortableNetworkGraphic;
begin
png := TPortableNetworkGraphic.Create; try png.Assign(bmp); png.SaveToFile(PngFileName); finally png.Free; end;
end; </delphi>
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 convert from separate RGB channels to TColor use:
<delphi> RGBToColor(RedVal, GreenVal, BlueVal); </delphi>
To get each channel of a TColor variable use the Red, Green and Blue functions:
<delphi>
RedVal := Red(MyColor); GreenVal := Green(MyColor); BlueVal := Blue(MyColor);
</delphi>
TFPColor
TFPColor uses the AARRGGBB format common to most libraries.
Working with TCanvas
Using the default GUI font
This can be done with this simple code:
<delphi>
SelectObject(Canvas.Handle, GetStockObject(DEFAULT_GUI_FONT));
</delphi>
Drawing a text limited on the width
Use the DrawText routine, first with DT_CALCRECT and then without it.
<delphi>
// First calculate the text size then draw it TextBox := Rect(0, currentPos.Y, Width, High(Integer)); DrawText(ACanvas.Handle, PChar(Text), Length(Text), TextBox, DT_WORDBREAK or DT_INTERNAL or DT_CALCRECT);
DrawText(ACanvas.Handle, PChar(Text), Length(Text), TextBox, DT_WORDBREAK or DT_INTERNAL);
</delphi>
Drawing text with sharp edges (non antialiased)
Some widgetsets support this via
<Delphi> Canvas.Font.Quality:=fqNonAntialiased </Delphi>
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:
<Delphi> procedure PaintAliased(Canvas: TCanvas; x,y: integer; const TheText: string); var
w,h: integer; IntfImg: TLazIntfImage; Img: TBitmap; dy: Integer; dx: Integer; col: TFPColor; FontColor: TColor; c: TColor;
begin
w:=0; h:=0; Canvas.GetTextSize(TheText,w,h); if (w<=0) or (h<=0) then exit; Img:=TBitmap.Create; IntfImg:=nil; try // paint text to a bitmap Img.Masked:=true; Img.SetSize(w,h); Img.Canvas.Brush.Style:=bsSolid; Img.Canvas.Brush.Color:=clWhite; Img.Canvas.FillRect(0,0,w,h); Img.Canvas.Font:=Canvas.Font; Img.Canvas.TextOut(0,0,TheText); // get memory image IntfImg:=Img.CreateIntfImage; // replace gray pixels FontColor:=ColorToRGB(Canvas.Font.Color); for dy:=0 to h-1 do begin for dx:=0 to w-1 do begin col:=IntfImg.Colors[dx,dy]; c:=FPColorToTColor(col); if c<>FontColor then IntfImg.Colors[dx,dy]:=colTransparent; end; end; // create bitmap Img.LoadFromIntfImage(IntfImg); // paint Canvas.Draw(x,y,Img); finally IntfImg.Free; Img.Free; end;
end; </Delphi>
Drawing without LCL
You can draw without the LCL. For example a program running on a webserver generating graphics should work without any full blown visual library. For this you can use FPImage (alias fcl-image) a very generic image and drawing library written completely in pascal. In fact the LCL uses FPImage too and implements the drawing function through calls to the widgetset (winapi, gtk, carbon, ...).
Here is an example, how to create a 200x100 image, painting a white background and some text and saving it as .png: <Delphi> program fontdraw;
{$mode objfpc}{$H+}
uses
Classes, SysUtils, FPimage, FPImgCanv, ftfont, FPWritePNG, FPCanvas;
procedure TestFPImgFont; var
Img: TFPMemoryImage; Writer: TFPWriterPNG; ms: TMemoryStream; ImgCanvas: TFPImageCanvas; fs: TFileStream; AFont: TFreeTypeFont;
begin
Img:=nil; ImgCanvas:=nil; Writer:=nil; ms:=nil; fs:=nil; AFont:=nil; try // initialize free type font manager ftfont.InitEngine; FontMgr.SearchPath:='/usr/share/fonts/truetype/ttf-dejavu/'; AFont:=TFreeTypeFont.Create;
// create an image of width 200, height 100 Img:=TFPMemoryImage.Create(200,100); Img.UsePalette:=false; // create the canvas with the drawing operations ImgCanvas:=TFPImageCanvas.create(Img);
// paint white background ImgCanvas.Brush.FPColor:=colWhite; ImgCanvas.Brush.Style:=bsSolid; ImgCanvas.Rectangle(0,0,Img.Width,Img.Height);
// paint text ImgCanvas.Font:=AFont; ImgCanvas.Font.Name:='DejaVuSans'; ImgCanvas.Font.Size:=20; ImgCanvas.TextOut(10,30,'Test');
// write image as png to memory stream Writer:=TFPWriterPNG.create; ms:=TMemoryStream.Create; writer.ImageWrite(ms,Img); // write memory stream to file ms.Position:=0; fs:=TFileStream.Create('testfont.png',fmCreate); fs.CopyFrom(ms,ms.Size); finally AFont.Free; ms.Free; Writer.Free; ImgCanvas.Free; Img.Free; fs.Free; end;
end;
begin
TestFPImgFont;
end. </Delphi>