Difference between revisions of "Developing with Graphics"

From Free Pascal wiki
(added Draw TLazIntfImage to Canvas=)
(A fading example: - fixed Lazarus example: TempIntfImg.CreateBitmap -> TempIntfImg.CreateBitmaps)
Line 93: Line 93:
 
       end;
 
       end;
 
     end;
 
     end;
     TempIntfImg.CreateBitmap(ImgHandle,ImgMaskHandle,false);
+
     TempIntfImg.CreateBitmaps(ImgHandle,ImgMaskHandle,false);
 
     TempBitmap.Handle:=ImgHandle;
 
     TempBitmap.Handle:=ImgHandle;
 
     TempBitmap.MaskHandle:=ImgMaskHandle;
 
     TempBitmap.MaskHandle:=ImgMaskHandle;

Revision as of 01:13, 17 November 2008

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

  • 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

The first thing to remember is that Lazarus is meant to be platform independent, so any methods using Windows API functionality are out of the question. So a method like ScanLine is not supported by Lazarus because it is intended for Device Independant Bitmap and uses functions from the GDI32.dll.

Bear in mind that if you do not specify the height and width of your TBitmap it will have the standard one, which is quite small.

A fading example

Say you want to make a Fading picture. In Delphi you could do something like:

type
  PRGBTripleArray = ^TRGBTripleArray;
  TRGBTripleArray = array[0..32767] of TRGBTriple;

procedure TForm1.FadeIn(aBitMap: TBitMap);
var
  Bitmap, BaseBitmap: TBitmap;
  Row, BaseRow: PRGBTripleArray;
  x, y, step: integer;
begin
  Bitmap := TBitmap.Create;
  try
    Bitmap.PixelFormat := pf32bit;  //  or pf24bit
    Bitmap.Assign(aBitMap);
    BaseBitmap := TBitmap.Create;
    try
      BaseBitmap.PixelFormat := pf32bit;
      BaseBitmap.Assign(Bitmap);
      for step := 0 to 32 do begin
        for y := 0 to (Bitmap.Height - 1) do begin
          BaseRow := BaseBitmap.Scanline[y];
          Row := Bitmap.Scanline[y];
          for x := 0 to (Bitmap.Width - 1) do begin
            Row[x].rgbtRed := (step * BaseRow[x].rgbtRed) shr 5;
            Row[x].rgbtGreen := (step * BaseRow[x].rgbtGreen) shr 5; // Fading
            Row[x].rgbtBlue := (step * BaseRow[x].rgbtBlue) shr 5;
          end;
        end;
        Form1.Canvas.Draw(0, 0, Bitmap);
        InvalidateRect(Form1.Handle, nil, False);
        RedrawWindow(Form1.Handle, nil, 0, RDW_UPDATENOW);
      end;
    finally
      BaseBitmap.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

This function in Lazarus could be implemented like:

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;

The Lazarus code on this page has been taken from the $LazarusPath/examples/lazintfimage/fadein1.lpi project. So if you want a flying start with graphics programming take a closer look at this example.

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

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;

just don´t forget to destroy it:

procedure TMyForm.FormDestroy(Sender: TObject);
begin
  MyDrawingControl.Free;
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.

"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

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>