由雙緩衝繪圖技術談起到Delphi源碼實現
來源:互聯網
上載者:User
由雙緩衝繪圖技術談起到Delphi源碼實現(本文原創,轉載請申明)xiaoyunsoft@163.com 摘要:雙緩衝繪圖技術在Delphi中的實現說明:假設讀者熟悉VCL 雙緩衝繪圖也不是什麼新技術,簡單的說:在繪圖實現時不直接繪在視窗上,而是先繪在記憶體裡,再一起“拷貝”至視窗。實現起來也不複雜,建立一相容HDC,在此相容HDC上繪圖,最後拷貝到視窗HDC就行了。本人前段時間把一C++實現該技術的代碼改成了Delphi代碼,都是用Win32API寫的。今改成了使用Delphi內建的類,試了一下(視窗類別Canvas與TImage的Canvas)。實現方式大同小異,但不得不提的是在視窗中直接使用Canvas繪圖與TImage.Canvas卻不相同。使用TImage.Canvas繪圖時,自動使用了雙緩衝技術,而視窗的Canvas對像卻未實現。怎麼回事呢?看一下代碼吧,“源碼面前沒有秘密”! 一.TImage類的CanvasTImage = class(TGraphicControl)...property Canvas: TCanvas read GetCanvas;...function TImage.GetCanvas: TCanvas;var Bitmap: TBitmap;begin if Picture.Graphic = nil then begin Bitmap := TBitmap.Create; try Bitmap.Width := Width; Bitmap.Height := Height; Picture.Graphic := Bitmap; finally Bitmap.Free; end; end; if Picture.Graphic is TBitmap then Result := TBitmap(Picture.Graphic).Canvas else raise EInvalidOperation.Create(SImageCanvasNeedsBitmap);end; 可知TImage.Canvas來自Bitmap.Canvas,好,那來看看TBitmap.Canvasfunction TBitmap.GetCanvas: TCanvas;begin if FCanvas = nil then begin HandleNeeded; if FCanvas = nil then // possible recursion begin FCanvas := TBitmapCanvas.Create(Self); FCanvas.OnChange := Changed; FCanvas.OnChanging := Changing; end; end; Result := FCanvas;end; 顯而易見TBitmap.Canvas = TBitmapCanvas.Create;也就是說TImage.Canvas=TBitmapCanvas.Create.即使用TImage.Canvas繪圖時,實際是在TBitmapCanvas上繪圖的。讓我們再來看看TBitmapCanvas類: TBitmapCanvas = class(TCanvas) private FBitmap: TBitmap; FOldBitmap: HBITMAP; FOldPalette: HPALETTE; procedure FreeContext; protected procedure CreateHandle; override; public constructor Create(ABitmap: TBitmap); destructor Destroy; override; end; 關注一下CreateHandle函數:procedure TBitmapCanvas.CreateHandle;var H: HBITMAP;begin if FBitmap <> nil then begin Lock; try FBitmap.HandleNeeded; DeselectBitmap(FBitmap.FImage.FHandle);//!! DeselectBitmap(FBitmap.FImage.FMaskHandle); FBitmap.PaletteNeeded; H := CreateCompatibleDC(0); if FBitmap.FImage.FHandle <> 0 then FOldBitmap := SelectObject(H, FBitmap.FImage.FHandle) else FOldBitmap := 0; if FBitmap.FImage.FPalette <> 0 then begin FOldPalette := SelectPalette(H, FBitmap.FImage.FPalette, True); RealizePalette(H); end else FOldPalette := 0; Handle := H; BitmapCanvasList.Add(Self); finally Unlock; end; end;end; 讀起來也不困難,FBitmap是Create建構函式傳進來的。而我們應該關注的代碼位於斜體部份,也很好理解:建立相容DC,並選進裝置。要的就是這個效果,現在知道為什麼使用TImage.Canvas來繪圖是使用的雙緩衝技術的了吧?那麼這個相容DC是如何從記憶體“拷貝”到視窗的呢? 我們使用上面的分析方法,當TImage基類TGraphicControl收到WM_PAINT訊息時,將執行下面的代碼:procedure TGraphicControl.WMPaint(var Message: TWMPaint);begin if Message.DC <> 0 then begin Canvas.Lock; try Canvas.Handle := Message.DC; try Paint; finally Canvas.Handle := 0; end; finally Canvas.Unlock; end; end;end; (在此先僅關注Paint函數) 而TImage覆蓋了此Paint虛函數:procedure TImage.Paint;var Save: Boolean;begin if csDesigning in ComponentState then with inherited Canvas do begin Pen.Style := psDash; Brush.Style := bsClear; Rectangle(0, 0, Width, Height); end; Save := FDrawing; FDrawing := True; try with inherited Canvas do//祖先的Canvas StretchDraw(DestRect, Picture.Graphic); finally FDrawing := Save; end;end; 拋開枝節,關注兩個地方,一是斜體部份的Canvas對像,二是StrectchDraw函數。先看看此Canvas對像,它被顯示聲明為基類的Canvas對像。不得不提,此Canvas.Handle即控制代碼的賦值代碼:procedure TGraphicControl.WMPaint(var Message: TWMPaint);begin if Message.DC <> 0 then begin Canvas.Lock; try Canvas.Handle := Message.DC; try Paint; finally Canvas.Handle := 0; end; finally Canvas.Unlock; end; end;end; 是訊息傳遞進來的,這裡的DC為此TGraphicControl.Parent的DC。至於如何傳遞進來的請參考《VCL構架剖析》,在此不費話了。 再看第二個關注點StrectDraw函數:procedure TCanvas.StretchDraw(const Rect: TRect; Graphic: TGraphic);begin if Graphic <> nil then begin Changing; RequiredState(csAllValid); Graphic.Draw(Self, Rect); Changed; end;end; 這裡的Graphic是什麼呢?這裡是TBitmap!看看第一塊代碼。那再看TBitmap.Draw函數吧:procedure TBitmap.Draw(ACanvas: TCanvas; const Rect: TRect);var OldPalette: HPalette; RestorePalette: Boolean; DoHalftone: Boolean; Pt: TPoint; BPP: Integer; MaskDC: HDC; Save: THandle;begin with Rect, FImage do begin ACanvas.RequiredState(csAllValid); PaletteNeeded; OldPalette := 0; RestorePalette := False; if FPalette <> 0 then begin OldPalette := SelectPalette(ACanvas.FHandle, FPalette, True); RealizePalette(ACanvas.FHandle); RestorePalette := True; end; BPP := GetDeviceCaps(ACanvas.FHandle, BITSPIXEL) * GetDeviceCaps(ACanvas.FHandle, PLANES); DoHalftone := (BPP <= 8) and (BPP < (FDIB.dsbm.bmBitsPixel * FDIB.dsbm.bmPlanes)); if DoHalftone then begin GetBrushOrgEx(ACanvas.FHandle, pt); SetStretchBltMode(ACanvas.FHandle, HALFTONE); SetBrushOrgEx(ACanvas.FHandle, pt.x, pt.y, @pt); end else if not Monochrome then SetStretchBltMode(ACanvas.Handle, STRETCH_DELETESCANS); try { Call MaskHandleNeeded prior to creating the canvas handle since it causes FreeContext to be called. } if Transparent then MaskHandleNeeded; Canvas.RequiredState(csAllValid); if Transparent then begin Save := 0; MaskDC := 0; try MaskDC := GDICheck(CreateCompatibleDC(0)); Save := SelectObject(MaskDC, FMaskHandle); TransparentStretchBlt(ACanvas.FHandle, Left, Top, Right - Left, Bottom - Top, Canvas.FHandle, 0, 0, FDIB.dsbm.bmWidth, FDIB.dsbm.bmHeight, MaskDC, 0, 0); finally if Save <> 0 then SelectObject(MaskDC, Save); if MaskDC <> 0 then DeleteDC(MaskDC); end; end else StretchBlt(ACanvas.FHandle, Left, Top, Right - Left, Bottom - Top, Canvas.FHandle, 0, 0, FDIB.dsbm.bmWidth, FDIB.dsbm.bmHeight, ACanvas.CopyMode); finally if RestorePalette then SelectPalette(ACanvas.FHandle, OldPalette, True); end; end; 不要再深挖了,斜體部份很明了,功能就是將繪圖內容從記憶體拷貝至視窗。ACanvas.FHandle即上面所說的訊息傳遞進來的HDC。(ACanvas是TImage的祖先TGraphicControl的內部對像,Canvas在此為TBitmapCanvas執行個體)。可能有點亂,因為我整理好了之後,再次閱讀時,自已也迷糊了,仔細多看兩遍吧。再提一下:TGraphicControl.Canvas與TImage.Canvas是兩個執行個體,雖然TImage繼承自TGraphicControl。好了,我們再來看看為何使用視窗Canvas屬性進行繪畫時,沒有使用雙緩衝技術吧 二.視窗類別的Canvas其實也不能決對說視窗Canvas沒有使用雙緩衝技術,它有使用,但有限制。條件是在將視窗TForm.DoubleBuffered設為TRUE的前提下,在Paint事件函數裡使用Canvas對像進行繪圖動作。下面還是按照上面的方法來找出其中的緣由。先看一下TCustomForm.WMPaint訊息處理函數: procedure TCustomForm.WMPaint(var Message: TWMPaint);var DC: HDC; PS: TPaintStruct;begin if not IsIconic(Handle) then begin ControlState := ControlState + [csCustomPaint]; inherited; ControlState := ControlState - [csCustomPaint]; end else begin DC := BeginPaint(Handle, PS); DrawIcon(DC, 0, 0, GetIconHandle); EndPaint(Handle, PS); end;end; 這個簡單,基本只用考濾斜體部份代碼,即調用基類同名函數,在此要追溯到TWinControl.WMPaint函數:procedure TWinControl.WMPaint(var Message: TWMPaint);var DC, MemDC: HDC; MemBitmap, OldBitmap: HBITMAP; PS: TPaintStruct;begin if not FDoubleBuffered or (Message.DC <> 0) then begin if not (csCustomPaint in ControlState) and (ControlCount = 0) then inherited else PaintHandler(Message); end else begin DC := GetDC(0); MemBitmap := CreateCompatibleBitmap(DC, ClientRect.Right, ClientRect.Bottom); ReleaseDC(0, DC); MemDC := CreateCompatibleDC(0); OldBitmap := SelectObject(MemDC, MemBitmap); try DC := BeginPaint(Handle, PS); Perform(WM_ERASEBKGND, MemDC, MemDC); Message.DC := MemDC; WMPaint(Message); Message.DC := 0; BitBlt(DC, 0, 0, ClientRect.Right, ClientRect.Bottom, MemDC, 0, 0, SRCCOPY); EndPaint(Handle, PS); finally SelectObject(MemDC, OldBitmap); DeleteDC(MemDC); DeleteObject(MemBitmap); end; end;end; 好,先看下面的斜體部份,若將TForm.DoubleBuffered設為TRUE,那麼將建立相容DC即使用雙緩衝技術。完了之後還是將會調用此函數,這時將會執行第一個斜體部份代碼。也就是說,不管是否設定Form.DoubleBuffered為何值,始終會執行PaintHandler函數。只不過傳遞的參數的屬性(DC)不同罷了。那就看看PaintHandler都幹了些什麼吧: procedure TWinControl.PaintHandler(var Message: TWMPaint);var I, Clip, SaveIndex: Integer; DC: HDC; PS: TPaintStruct;begin DC := Message.DC; if DC = 0 then DC := BeginPaint(Handle, PS); try if FControls = nil then PaintWindow(DC) else begin SaveIndex := SaveDC(DC); Clip := SimpleRegion; for I := 0 to FControls.Count - 1 do with TControl(FControls[I]) do if (Visible or (csDesigning in ComponentState) and not (csNoDesignVisible in ControlStyle)) and (csOpaque in ControlStyle) then begin Clip := ExcludeClipRect(DC, Left, Top, Left + Width, Top + Height); if Clip = NullRegion then Break; end; if Clip <> NullRegion then PaintWindow(DC); RestoreDC(DC, SaveIndex); end; PaintControls(DC, nil); finally if Message.DC = 0 then EndPaint(Handle, PS); end;end; PaintWindow是虛函數,TCustomForm覆蓋了它:procedure TCustomForm.PaintWindow(DC: HDC);begin FCanvas.Lock; try FCanvas.Handle := DC; try if FDesigner <> nil then FDesigner.PaintGrid else Paint; finally FCanvas.Handle := 0; end; finally FCanvas.Unlock; end;end; 可以看到,DC值賦給了FCanvas,觸發了Paint事件。由此可以知,只有在將TForm.DoubleBuffered設為TRUE的情況下,並在OnPaint事件中調用Canvas屬性時,才會使用雙緩衝技術。 那在TForm的其它事件中使用Canvas繪圖是什麼情況呢:constructor TCustomForm.CreateNew(AOwner: TComponent; Dummy: Integer);begin inherited Create(AOwner); ControlStyle := [csAcceptsControls, csCaptureMouse, csClickEvents, csSetCaption, csDoubleClicks]; Left := 0; Top := 0; Width := 320; Height := 240; FIcon := TIcon.Create; FIcon.Width := GetSystemMetrics(SM_CXSMICON); FIcon.Height := GetSystemMetrics(SM_CYSMICON); FIcon.OnChange := IconChanged; FCanvas := TControlCanvas.Create; FCanvas.Control := Self; FBorderIcons := [biSystemMenu, biMinimize, biMaximize]; FBorderStyle := bsSizeable;… 再看一下TControlCanvas類的定義:procedure TControlCanvas.CreateHandle;begin if FControl = nil then inherited CreateHandle else begin if FDeviceContext = 0 then begin with CanvasList.LockList do try if Count >= CanvasListCacheSize then FreeDeviceContext; FDeviceContext := FControl.GetDeviceContext(FWindowHandle); Add(Self); finally CanvasList.UnlockList; end; end; Handle := FDeviceContext; UpdateTextFlags; end;end; 上面代碼中的FControl此時為TCustomForm,那麼再看看TCustomForm.GetDeviceContext函數,它位於TWinControl類中定義:function TWinControl.GetDeviceContext(var WindowHandle: HWnd): HDC;begin if csDesigning in ComponentState then Result := GetDCEx(Handle, 0, DCX_CACHE or DCX_CLIPSIBLINGS) else Result := GetDC(Handle); if Result = 0 then raise EOutOfResources.CreateRes(@SWindowDCError); WindowHandle := FHandle;end; 這段代碼不解釋也能看的明白了,只是使用了GetDC函數,而未使用我們想要的雙緩衝。 結尾:在Delphi中繪圖時可以使用TImage類來實現,比較方便。若要追求效率,推薦使用Win32Api。聲明:轉載請保持此文檔的完整性。參考文獻: 1.李維 《VCL構架剖析》 2.周愛民 《Delphi原始碼分析》