Developing with Graphics/zh TW

From Free Pascal wiki
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 下繪圖時所使用到的基本類別與技巧。更多特定的主題將另文說明。

其他繪圖文章

  • BGRABitmap - 繪製各種圖案,點陣圖加透明效果,直接存取圖像圖素等。
  • GLScene - 視覺的 OpenGL 圖形函式庫交流站 GLScene
  • TAChart - Lazarus 的圖表元件
  • PascalMagick - 使用 ImageMagick 應用程式介面的簡單範例,建立一個跨平台,可編輯點陣圖檔的自由軟體。
  • PlotPanel - 製作動態的圖表繪製
  • LazRGBGraphics - 該套件提供對記憶體影像的處理與圖像圖素的操作 (例如掃瞄線)。
  • Perlin Noise - 一篇於 LCL 使用 Perlin Noise 實作的應用程式。

使用 TBitmap 工作

在某些作業系統,圖陣圖資料並非儲存在記憶體裡,所以無法直接存取,當 Lazarus 想要做為一個可以跨平台獨立作業的應用軟體時,TBitmap 類別就無法提供像是掃瞄線這樣的內容。這裡還有個 GetDataLineStart 函式,相等於掃瞄線 (Scanline ) 的功能,但僅能利用於記憶體裡的影像,或像使用內建的 TrawImage TLazIntfImage。

總結說來,你只能透過記憶體的影像去間接去修改圖陣圖,然後再轉換成可繪製的點陣圖。這當然會比較慢。不然使用 Lazarus 內建的 TLazIntfImage 或是使用外部的函式庫,像是BGRABitmapLazRGBGraphicsGraphics32 可以用來直接存取點陣圖。

註:當你建立一個點陣圖時,你必須指定他寬和高,不然你所繪製的東西都會歸零。

直接存取圖像圖素

在 Delphi 裡,或用 TBitmap.Scanline 來存取圖像圖素。因為內容是無法再傳遞給別人的。Lazarus 有其他的辦法。可以參考 TLazIntfImage 而不是 TBitmap.Pixels,這非常慢。

在點陣圖裡繪製透明色

Lazarus 0.9.11 的特點之一,就是可以在點陣圖裡繪製透明色,點陣圖檔案 (*.BMP) 無法儲存透明的資訊,但若你的圖裡有透明色的設定,在 Win32 裡大多的應用程式都可以辨別的出來。

接下來的範例會從 Windows 的資源裡載入一張點陣圖,把其中一個顏色指定為透明 (clFuchsia),然後在畫面上繪圖。

<delphi>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; // 載入點陣圖檔出錯
 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; // 釋放資料分派的空間

end;</delphi>

注意到記憶體的操作用到 TMemoryStream。它在載入影像到記憶體裡的時候必須用到。

擷取螢幕畫面

從 Lazarus 0.9.16 開始之後你可以使用跨平台的 LCL 功能來擷取畫面,下面的範例可以達成此作業。(使用 gtk2 和 win32,但不是 gtk1):

<delphi>uses Graphics, LCLIntf, LCLType;

 ...

var

 MyBitmap: TBitmap;
 ScreenDC: HDC;

begin

 MyBitmap := TBitmap.Create;
 ScreenDC := GetDC(0);
 MyBitmap.LoadFromDevice(ScreenDC);
 ReleaseDC(ScreenDC);
 ...</delphi>

使用 TLazIntfImage 作業

淡出的範例

使用 TLazIntfImage 做出淡出效果的範例

<delphi>{ 這段程式碼可以在這個專案 $LazarusPath/examples/lazintfimage/fadein1.lpi 裡看到。 } uses LCLType, // HBitmap 類型

    IntfGraphics, // TLazIntfImage 類型
    fpImage; // TFPColor 類型

...

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


影像檔格式特定範例

如果你知道 TBitmap 的藍色使用 8bit,綠色 8bit,紅色 8bit,你可以直接對位元存取,這樣比較快:

<delphi>uses LCLType, // HBitmap 類型

    IntfGraphics, // TLazIntfImage 類型
    fpImage; // TFPColor 類型

... 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;
  
  //用到類似掃瞄線的功能
  for FadeStep:=1 to 32 do begin
    for py:=0 to IntfImg1.Height-1 do begin
      Row1 := IntfImg1.GetDataLineStart(py); //類似 Delphi TBitMap.ScanLine
      Row2 := IntfImg2.GetDataLineStart(py); //類似 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; // 淡出
        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;</delphi>

於 TLazIntfImage 與 TBitmap 之間轉換

自從 Lazarus 沒有 TBitmap.ScanLines 這內容後,想要最妥當的存取圖像圖素的方法就是讀跟寫都使用 TLazIntfImage。TBitmap 可以使用 TBitmap.CreateIntfImage() 轉換到 TLazIntfImage,再修改圖素後還可以再用TBitmap.LoadFromIntfImage() 轉回到 TBitmap; 這裡的範例就是從 TBitmap 建立一個 TLazIntfImage,修改後,再轉回到 TBitmap。

<delphi>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;
   // 對圖素讀或寫
   t.Colors[10,20] := colGreen;
   b.LoadFromIntfImage(t);
 finally
   t.Free;
   b.Free;
 end;

end;</delphi>

動態的圖形 - 要怎麼避免閃爍

許多程式在他們的 GUI 畫面用到 2D 的繪圖。但若這些圖像想要有動態的變化,你馬上就會面臨到一個問題:快速的圖片轉換你的螢幕會閃爍,這會讓你的使用者有時只看到你的圖的部份而不是全部。這一定會發生,因為處理圖片需要時間。

但我要如何才能使繪圖達到最佳的效果而避免閃爍呢?當然你首先可以利用 OpenGL 圖形加速器,但這對小程式來講,程式碼會負擔變重,這個範例我們使用 TCanvas 來繪圖。如果你有需要到 OpenGL 的協助,那請再參看 Lazarus 對於 OpenGL 的文件,你也可以用 A.J. Venter's gamepack,這個繪圖元件有用到雙緩衝的功能。

現在我們來看看這幾個繪圖選項:

TImage 繪圖

TImage 包含兩個部份:TGraphic,通常也就是 TBitmap,在每一個 OnPaint 事件中負起在畫面保持一個可以繪圖的區域。TImage 下進行重新取樣並不會真的將點陣圖變更尺寸。 圖形 (或說點陣圖) 可以透過 Image1.Picture.Graphic (或 Image1.Picture.Bitmap) 存取,但控制畫面畫布區為 Image1.Picture.Bitmap.Canvas。 TImage 畫布的可視範圍可以在 Image1.OnPaint 事件中透過 Image1.Canvas 存取。

重要:千萬別在 Image1 的 OnPaint 事件中繪製 TImage 點陣圖。TImage 的圖形是存在緩衝區中的,所以你要繪製它的時候就直接在那執行,也會即時生效,但如果你會需要常常重新繪製它,影像就會閃爍,這樣的話你就得選用另一個方法。TImage 繪圖被視為比其他的方法都慢。

TImage 的點陣圖重新取樣

註:請勿於 OnPaint 事件時使用。

<delphi>with Image1.Picture.Bitmap do begin

 Width:=100;
 Height:=120;

end;</delphi>

繪製 TImage 點陣圖

註:請勿於 OnPaint 事件時使用。

<delphi>with Image1.Picture.Bitmap.Canvas do begin

 // 將目前的區域填滿紅色
 Brush.Color := clRed;
 FillRect(0, 0, Width, Height);

end;</delphi>

註:在 Image1.OnPaint 裡,Image1.Canvas 指到的是有時效性的可見區域,而在 Image1.OnPaint 之外 Image1.Canvas 指到 Image1.Picture.Bitmap.Canvas。

另一個範例:

<delphi>procedure TForm1.BitBtn1Click(Sender: TObject); var

 x, y: Integer;

begin

 // 繪製背景
 MyImage.Canvas.Pen.Color := clWhite;
 MyImage.Canvas.Rectangle(0, 0, Image.Width, Image.Height);
  
 // 繪製方形
 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;</delphi>

於有時效性的可見區域繪製 TImage

在 OnPaint 裡你只能在固定區域裡作畫。當區域無法繪製時 OnPaint 最後會自動被 LCL 呼叫,你可以用 Image1.Invalidate 來自訂無效的區域,這不會立即呼叫 OnPaint,而且你可以多次地做無效的指定。

<delphi>procedure TForm.Image1Paint(Sender: TObject); begin

 // 畫一條線
 Canvas.Pen.Color := clRed;
 Canvas.Line(0, 0, Width, Height);

end;</delphi>

在 OnPaint 事件中繪圖

這裡的範例裡所有的繪圖作業都在表單的 OnPaint 事件發生時完成,或是某個另外的控制項。這無需像 TImage 的需要緩衝,它需要在事件處理器被呼叫的時候把所有工作一次做完。

<delphi>procedure TForm.Form1Paint(Sender: TObject); begin

 // 繪出一條線
 Canvas.Pen.Color := clRed;
 Canvas.Line(0, 0, Width, Height);

end;</delphi>

建立自訂一個控制項用來自動繪圖

建立一個自訂的控制項的好處在於加強你程式的結構,控制項也可以用來重覆利用。這很快就能做到,但如果你不是在 TBitmap 下先繪圖再轉移上畫布區,這個做法還是會閃爍。在這裡我們就用不到 OnPaint 這個事件的控制項了。

以下即為自訂控制項的範例:

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

 // 取消註解就能開啟預設是抺白的背景
 //繼承抺除背景 EraseBackground(DC);

end;

procedure TMyDrawingControl.Paint; var

 x, y: Integer;
 Bitmap: TBitmap;

begin

 Bitmap := TBitmap.Create;
 try
   // 初始點陣圖尺寸
   Bitmap.Height := Height;
   Bitmap.Width := Width;

   // 繪製背景
   Bitmap.Canvas.Pen.Color := clWhite;
   Bitmap.Canvas.Rectangle(0, 0, Width, Height);

   // 繪製方形
   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;</delphi>

然後我們在表單上建立它: <delphi>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;</delphi>

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

這個表提供每個影像格式所要使用的適當類別。

格式 影像類別 單元
游標 (cur) TCursor 圖形
點陣圖檔 (bmp) TBitmap 圖形
Windows 圖示 (ico) TIcon 圖形
Mac OS X 圖示 (icns) TicnsIcon 圖形
Pixmap (xpm) TPixmap 圖形
可傳遞網路圖形 (png) TPortableNetworkGraphic 圖形
JPEG (jpg, jpeg) TJpegImage 圖形
PNM (pnm) TPortableAnyMapGraphic 圖形

也可參見fcl-image 支援格式列表

轉換格式

有時候無法避免要將圖檔的格式轉換到另一個。 轉換圖檔有一個方法是使用中介格式,然後再轉換到 TBitmap。 大多數的格式都可以從 TBitmap 上再去建立。

轉換點陣圖檔到 PNG 格式然後再儲存它:

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

圖像圖素格式

TColor

在 LCL 裡,為 TColor 內建的圖素格式是為 XXBBGGRR 格式,其符合 Windows 的原生格式,且也與大部份的函式庫 AARRGGBB 格式對衝。XX 是用來定義如果顏色為固定的色盤顏色,像 XX 若為 00 則代表他是系統預設的顏色之一,並沒有為 Alpha 頻道預留任何空間。

要將各別的 RGB 頻道轉換到 TColor 時使用:

<delphi>RGBToColor(RedVal, GreenVal, BlueVal);</delphi>

分別使用 Red,Green,Blue 函式取得此三個頻道的 TColor 變數值。

<delphi>RedVal := Red(MyColor); GreenVal := Green(MyColor); BlueVal := Blue(MyColor);</delphi>

TFPColor

TFPColor 使用 AARRGGBB 格式,普遍見於各種函式庫。

使用 TCanvas 作業

使用預設的 GUI 字型

以下簡單的程式碼就可以達成:

<delphi>SelectObject(Canvas.Handle, GetStockObject(DEFAULT_GUI_FONT));</delphi>

在固定寬度內繪製文字

使用 DrawText,先加入 DT_CALCRECT 然後再排除它。

<delphi>// 首先要計算文字的尺寸才再進行繪製 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>

繪製有銳角的文字 (無反鋸齒補償)

某些工具可以達到這個效果

<Delphi>Canvas.Font.Quality := fqNonAntialiased;</Delphi>

有些工具,像是 gtk2 並不支援此效果,他繪製出來的永遠都有反鋸齒補償。這裡有一個簡單的方讓你使用 gtk2 畫出這種銳角。這並不代表所有的情況,只是其中一種概念:

<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
   // 在點陣圖中繪製文字
   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;
   // 取代灰色的圖素
   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;
   // 建立點陣圖
   Img.LoadFromIntfImage(IntfImg);
   // 繪製
   Canvas.Draw(x,y,Img);
 finally
   IntfImg.Free;
   Img.Free;
 end;

end;</Delphi>

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.