Difference between revisions of "Developing with Graphics/zh CN"

From Lazarus wiki
m (其它关于图形开发的文章)
m (其它关于图形开发的文章)
Line 9: Line 9:
 
==其它关于图形开发的文章==  
 
==其它关于图形开发的文章==  
 
* [[GLScene/zh_CN|GLScene]] - 一个可视化的OpenGL图形库的接口 [http://www.glscene.org GLScene]
 
* [[GLScene/zh_CN|GLScene]] - 一个可视化的OpenGL图形库的接口 [http://www.glscene.org GLScene]
* [[TAChart]] - Lazarus图表绘制组件
+
* [[TAChart/zh_CN|TAChart]] - Lazarus图表绘制组件
* [[PascalMagick]] - 一个针对于[http://www.imagemagick.org ImageMagick]的易于使用的API, 在多平台下用来创建、编辑及合成位图文件的自由软件包
+
* [[PascalMagick/zh_CN|PascalMagick]] - 一个针对于[http://www.imagemagick.org ImageMagick]的易于使用的API, 在多平台下用来创建、编辑及合成位图文件的自由软件包
 
* [[PlotPanel]] - 一个绘制动画的组件
 
* [[PlotPanel]] - 一个绘制动画的组件
 
* [[LazRGBGraphics]] - 一个快速进行内存图像处理与像素操作
 
* [[LazRGBGraphics]] - 一个快速进行内存图像处理与像素操作

Revision as of 11:56, 26 April 2014

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)

这个页面是关于掌控位图及其他图形之教程的开始。由于我本人不是一个图形开发程序员,我在此邀请所有人来分享一下大家的专业知识。只需在下一部分加个链接,然后新建一个页面并编辑您自己的维基文章。

此页会提供一些基本的信息

其它关于图形开发的文章

  • GLScene - 一个可视化的OpenGL图形库的接口 GLScene
  • TAChart - Lazarus图表绘制组件
  • PascalMagick - 一个针对于ImageMagick的易于使用的API, 在多平台下用来创建、编辑及合成位图文件的自由软件包
  • PlotPanel - 一个绘制动画的组件
  • LazRGBGraphics - 一个快速进行内存图像处理与像素操作
  • Perlin Noise - 一篇关于在LCL上使用柏林噪声的文章

工作于TBitmap

首先需要记住的事情是Lazarus是独立于使用平台的,所以任何使用Windows API功能的问题都不值得讨论。比如像ScanLine这样的方法并不为Lazarus所支持,因为它是为设备独立的位图文件而设计的,并且使用了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.CreateBitmap(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);

  ...

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 manually the area with Image1.Invalidate. This will not call immediately OnPaint and you can call Invalidate as mny 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
Bitmap (bmp) TBitmap Graphics
Windows icon (ico) TIcon Graphics
Mac OS X icon (icns) TIcon 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:

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