Developing with Graphics/ru

From Free Pascal wiki
Revision as of 08:19, 13 December 2018 by Zoltanleo (talk | contribs)
Jump to navigationJump to search

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)

Эта страница описывает основные классы и технологии рисования графики в Lazarus. Какие-то специфические вещи ищите в других статьях.

Графические библиотеки

Графические библиотеки - здесь вы можете посмотреть, какие есть основные графические библиотеки.

Другие статьи по графике

2D-рисование

  • ZenGL - кроссплатформенная библиотека для разработки игр на основе OpenGL.
  • BGRABitmap - Рисование фигур, прозрачных изображений, прямой доступ к пикселям и др.
  • LazRGBGraphics - Пакет для быстрой обработки изображения в памяти и работы с пикселями (например, scan line).
  • fpvectorial - Предоставляет возможность работы с векторной графикой.
  • Double Gradient - Draw 'double gradient' & 'n gradient' bitmaps easy.
  • Gradient Filler - TGradientFiller is the best way to create custom n gradients in Lazarus.
  • PascalMagick - an easy to use API for interfacing with ImageMagick, a multiplatform free software suite to create, edit, and compose bitmap images.
  • Sample Graphics - graphics gallery created with Lazarus and drawing tools
  • Fast direct pixel access - speed comparison of some methods for direct bitmap pixel access
  • AggPas - AggPas is an Object Pascal native port of the Anti-Grain Geometry library. It is fast and very powerful with anti-aliased drawing and subpixel accuracy. You can think of AggPas as of a rendering engine that produces pixel images in memory from some vectorial data.

3D-рисование

Диаграммы

  • TAChart - компонента для рисования диаграмм в Lazarus
  • PlotPanel - Создание и черчение анимированных графиков
  • Perlin Noise - статья об использовании Perlin Noise в LCL приложениях.

Введение в графическую модель LCL

LCL предоставляет два вида классов рисования: нативные [(собственные)] классы и не-нативные [(сторонние)] классы. Нативные графические классы являются наиболее традиционным способом рисования графики в LCL, а также являются наиболее важными, в то время как не-нативные классы являются дополнительными, но также очень важными. Собственные классы в основном расположены в модуле Graphics LCL и являются хорошо известными классами: TBitmap, TCanvas, TFont, TBrush, TPen, TPortableNetworkGraphic и т.д.

TCanvas - это класс, способный выполнять рисование. Он не может существовать один и должен быть либо прикреплен к чему-то видимому (или, по крайней мере, который может быть видимым), например, к визуальному элементу управления, происходящему из TControl, либо должен быть присоединен к внеэкранному буферу потомка от TRasterImage (TBitmap является наиболее часто использующимся). TFont, TBrush и TPen описывают, как рисование различных операций будет выполняться в Canvas.

TRasterImage (обычно используется через его потомка TBitmap) - это область памяти, зарезервированная для рисования графики, но она создана для максимальной совместимости с собственным Canvas и, следовательно, в LCL-Gtk2 в X11 она расположена на сервере X11, который обеспечивает попиксельный доступ через свойство Pixels чрезвычайно медленно. В Windows это очень быстро, потому что Windows позволяет создавать локально выделенное изображение, которое может получать рисунки из Windows Canvas.

Помимо них существуют также не-нативные классы рисования, расположенные в графическом типе модулей (TRawImage), intfgraphics (TLazIntfImage) и lazcanvas (TLazCanvas, этот существует в Lazarus [с версии] 0.9.31+). TRawImage - это хранилище и описание области памяти, которая содержит изображение. TLazIntfImage - это изображение, которое присоединяется к TRawImage и обеспечивает преобразование между TFPColor и форматом настоящего пикселя TRawImage. TLazCanvas - это не нативный Canvas, который может рисовать изображение в TLazIntfImage.

Основное различие между нативными и не-нативными классами состоит в том, что нативные классы не выполняются одинаково на всех платформах, потому что рисование выполняется самой базовой платформой. Скорость, а также точный конечный результат рисования изображения могут иметь различия. Не-нативные классы гарантированно выполняют одинаковое рисование на всех платформах с точностью до пикселя, и все они работают достаточно быстро на всех платформах.

В наборе виджетов LCL-CustomDrawn нативные классы реализованы с использованием не-нативных классов.

Все эти классы будут лучше описаны в разделах ниже.

Working with TCanvas

Using the default GUI font

This can be done with this simple code:

SelectObject(Canvas.Handle, GetStockObject(DEFAULT_GUI_FONT));

Drawing a text limited on the width

Use the DrawText routine, first with DT_CALCRECT and then without it.

// 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);

Drawing text with sharp edges (non antialiased)

Some widgetsets support this via

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:

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;

Working with TBitmap and other TGraphic descendents

The TBitmap object stores a bitmap where you can draw before showing it to the screen. When you create a bitmap, you must specify the height and width, otherwise it will be zero and nothing will be drawn. And in general all other TRasterImage descendents provide the same capabilities. One should use the one which matches the format desired for output/input from the disk or TBitmap in case disk operations will not be performed as well as for the Windows Bitmap (*.bmp) format.

Loading/Saving an image from/to the disk

To load an image from the disk use TGraphic.LoadFromFile and to save it to another disk file use TGraphic.SaveToFile. Use the appropriate TGraphic descendent which matches the format expected. See Developing_with_Graphics#Image_formats for a list of available image format classes.

var
  MyBitmap: TBitmap;
begin
  MyBitmap := TBitmap.Create;
  try
    // Load from disk
    MyBitmap.LoadFromFile(MyEdit.Text);

    // Here you can use MyBitmap.Canvas to read/write to/from the image

    // Write back to another disk file
    MyBitmap.SaveToFile(MyEdit2.Text);
  finally
    MyBitmap.Free;
  end;
end;

When using any other format the process is compelely 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);

    // Here you can use MyPNG.Canvas to read/write to/from the image

    // Write back to another disk file
    MyPNG.SaveToFile(MyEdit2.Text);
  finally
    MyPNG.Free;
  end;
end;

If you don't 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);

    // Here you can use MyPicture.Graphic.Canvas to read/write to/from the image

    // Write back to another disk file
    MyPicture.SaveToFile(MyEdit2.Text);
  finally
    MyPicture.Free;
  end;
end;

Additional file formats for TImage

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

Direct pixel access

To do directly access the pixels of bitmaps one can either use external libraries, such as BGRABitmap, LazRGBGraphics and Graphics32 or use the Lazarus native TLazIntfImage. For a comparison of pixel access methods, see fast direct pixel access.

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, 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, with the standard TBitmap, you can only change pixels indirectly, by using TCanvas.Pixels. Calling a native API to draw / read an individual pixel is course slower than direct pixel access, notably so in LCL-gtk2 and LCL-Carbon.

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 Graphics, LCLIntf, LCLType;

  ...

var
  MyBitmap: TBitmap;
  ScreenDC: HDC;
begin
  MyBitmap := TBitmap.Create;
  ScreenDC := GetDC(0);
  MyBitmap.LoadFromDevice(ScreenDC);
  ReleaseDC(0,ScreenDC);

  ...

Working with TLazIntfImage, TRawImage and TLazCanvas

TLazIntfImage is a non-native equivalent of TRasterImage (more commonly utilized in the form of it's descendent TBitmap). The first thing to be aware about this class is that unlike TBitmap it will not automatically allocate a memory area for the bitmap, one should first initialize a memory area and then give it to the TLazIntfImage. Right after creating a TLazIntfImage one should either connect it to a TRawImage or load it from a TBitmap.

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:

uses graphtype, intfgraphics, lazcanvas;

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);

3. Load it from a raw image description, like this:

   IntfImg := TLazIntfImage.Create(0,0);
   IntfImg.DataDescription:=GetDescriptionFromDevice(0);
   IntfImg.SetSize(10,10);

The 0 device in GetDescriptionFromDevice(0) uses the current screen format.

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.

uses LazLogger, Graphics, IntfGraphics, GraphType;
procedure TForm1.FormCreate(Sender: TObject);
var
  AImage: TLazIntfImage;
  lRawImage: TRawImage;
begin
  // create a TLazIntfImage with 32 bits per pixel, alpha 8bit, red 8 bit, green 8bit, blue 8bit,
  // Bits In Order: bit 0 is pixel 0, Top To Bottom: line 0 is top
  lRawImage.Init;
  lRawImage.Description.Init_BPP32_A8R8G8B8_BIO_TTB(0,0);
  lRawImage.CreateData(false);
  AImage := TLazIntfImage.Create(0,0);
  try
    AImage.SetRawImage(lRawImage);
    // Load an image from disk.
    // It uses the file extension to select the right registered image reader.
    // The AImage will be resized to the width, height of the loaded image.
    AImage.LoadFromFile('lazarus/examples/openglcontrol/data/texture1.png');
    debugln(['TForm1.FormCreate ',AImage.Width,' ',AImage.Height]);
  finally
    AImage.Free;
  end;
end;

Loading a TLazIntfImage into a TImage

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:

  • To load a transparent TLazIntfImage you have to set the Image1.Transparent to true.
  • TImage uses the screen format. If the TLazIntfImage has a different format then the pixels will be converted. Hint: You can use IntfImg.DataDescription:=GetDescriptionFromDevice(0); to initialize the TLazIntfImage with the screen format.

Fading example

A fading example with TLazIntfImage

{ 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;

Image format specific example

If you know that the TBitmap is using blue 8bit, green 8bit, red 8bit you can directly access the bytes, which is somewhat faster:

uses LCLType, // HBitmap type
     IntfGraphics, // TLazIntfImage type
     fpImage; // TFPColor type
...
type
  TRGBTripleArray = array[0..32767] of TRGBTriple;
  PRGBTripleArray = ^TRGBTripleArray;

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;

Conversion between TLazIntfImage and TBitmap

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 ...

procedure TForm1.Button4Click(Sender: TObject);
var
  b: TBitmap;
  t: TLazIntfImage;
begin
  b := TBitmap.Create;
  try
    b.LoadFromFile('test.bmp');
    t := b.CreateIntfImage;

    // Read and/or write to the pixels
    t.Colors[10,20] := colGreen;

    b.LoadFromIntfImage(t);
  finally
    t.Free;
    b.Free;
  end;
end;

Using the non-native StretchDraw from LazCanvas

Just like TCanvas.StretchDraw there is TLazCanvas.StretchDraw but you need to specify the interpolation which you desire to use. The interpolation which provides a Windows-like StretchDraw with a very sharp result (the opposite of anti-aliased) can be added with: TLazCanvas.Interpolation := TFPSharpInterpolation.Create;

There are other interpolations available in the unit fpcanvas.

uses intfgraphics, lazcanvas;

procedure TForm1.StretchDrawBitmapToBitmap(SourceBitmap, DestBitmap: TBitmap; DestWidth, DestHeight: integer);
var
  DestIntfImage, SourceIntfImage: TLazIntfImage;
  DestCanvas: TLazCanvas;
begin
  // Prepare the destination

  DestIntfImage := TLazIntfImage.Create(0, 0);
  DestIntfImage.LoadFromBitmap(DestBitmap.Handle, 0);

  DestCanvas := TLazCanvas.Create(DestIntfImage);

  //Prepare the source
  SourceIntfImage := TLazIntfImage.Create(0, 0);
  SourceIntfImage.LoadFromBitmap(SourceBitmap.Handle, 0);

  // Execute the stretch draw via TFPSharpInterpolation
  DestCanvas.Interpolation := TFPSharpInterpolation.Create;
  DestCanvas.StretchDraw(0, 0, DestWidth, DestHeight, SourceIntfImage);

  // Reload the image into the TBitmap
  DestBitmap.LoadFromIntfImage(DestIntfImage);

  SourceIntfImage.Free;
  DestCanvas.Interpolation.Free;  
  DestCanvas.Free;
  DestIntfImage.Free;
end;

procedure TForm1.FormPaint(Sender: TObject);
var
  Bmp, DestBitmap: TBitmap;
begin
  // Prepare the destination
  DestBitmap := TBitmap.Create;
  DestBitmap.Width := 100;
  DestBitmap.Height := 100;

  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;

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.

A brief and very helpful article on avoiding flicker can be found at http://delphi.about.com/library/bluc/text/uc052102g.htm. Although written for Delphi, the techniques work well with Lazarus.

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

Light bulb  Примечание: Do not use this during OnPaint.
with Image1.Picture.Bitmap do begin
  Width:=100;
  Height:=120;
end;

Same in one step:

with Image1.Picture.Bitmap do begin
  SetSize(100, 120);
end;

Painting on the bitmap of a TImage

Light bulb  Примечание: 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;
Light bulb  Примечание: 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
  // paint a line
  Canvas.Pen.Color := clRed;
  Canvas.Line(0, 0, Width, Height);
end;

Draw on the OnPaint event

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.

procedure TForm.Form1Paint(Sender: TObject);
begin
  // paint a line
  Canvas.Pen.Color := clRed;
  Canvas.Line(0, 0, Width, Height);
end;

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.

Image formats

Here is a table with the correct 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
Tiff (tif) TTiffImage Graphics

See also the list of fcl-image supported formats.

Converting formats

Sometimes it must be 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.

Converting Bitmap to PNG and saving it to a file:

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;

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:

RGBToColor(RedVal, GreenVal, BlueVal);

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:

function FPColorToTColorRef(const FPColor: TFPColor): TColorRef;
function FPColorToTColor(const FPColor: TFPColor): TColor;
function TColorToFPColor(const c: TColorRef): TFPColor; overload;
function TColorToFPColor(const c: TColor): TFPColor; overload; // does not work on system color

Drawing with fcl-image

You can draw images which won't be displayed in the screen without the LCL, by just using fcl-image directly. For example a program running on a webserver without X11 could benefit from not having a visual library as a dependency. FPImage (alias fcl-image) is a very generic image and drawing library written completely in pascal. In fact the LCL uses FPImage too for all the loading and saving from/to files and implements the drawing function through calls to the widgetset (winapi, gtk, carbon, ...). Fcl-image on the other hand also has drawing routines.

For more information, please read the article about fcl-image.