Developing with Graphics/zh TW
│
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 或是使用外部的函式庫,像是BGRABitmap,LazRGBGraphics與Graphics32 可以用來直接存取點陣圖。
註:當你建立一個點陣圖時,你必須指定他寬和高,不然你所繪製的東西都會歸零。
直接存取圖像圖素
在 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>
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 isn't buffered like in the TImage, and it needs to be fully redrawn in each call of the OnPaint event handler.
<delphi>procedure TForm.Form1Paint(Sender: TObject); begin
// paint a line Canvas.Pen.Color := clRed; Canvas.Line(0, 0, Width, Height);
end;</delphi>
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:
<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
// 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;</delphi>
and how we create it on the form: <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
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 |
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 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 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.