Continue with previous question I want to be able to show some activity indicator even if the main thread is blocked. (based on this article).
Problems based on the attached code:
- Using
Synchronize(PaintTargetWindow);
does not paint the window - I sometimes get an error:
Canvas does not allow drawing.
In the line:{FBitmap.}StretchDraw(Rect(Left, ImageRect.Top, Right, ImageRect.Bottom), FfgPattern)
here is the code I use to create the indicator thread:
unit AniThread;
interface
uses Windows, Classes, Graphics, Controls, Math;
const
ANI_GRAD_FG_COLOR_BAGIN = $00CDFFCD;
ANI_GRAD_FG_COLOR_END = $0024B105;
ANI_GRAD_BK_COLOR_BAGIN = $00F5F5F5;
ANI_GRAD_BK_COLOR_END = $00BDBDBD;
type
TAnimationThread = class(TThread)
private
FWnd: HWND;
FPaintRect: TRect;
FInterval: Integer;
FfgPattern, FbkPattern: TBitmap;
FBitmap: TBitmap;
FImageRect: TRect;
procedure UpdatePattern(Pattern: TBitmap; ColorBegin, ColorEnd: TColor);
function CreatePatternBitmap(AColorBegin, AColorEnd: TColor): TBitmap;
procedure PaintTargetWindow;
protected
procedure Execute; override;
public
procedure Animate;
constructor Create(PaintSurface: TWinControl; { Control to paint on }
PaintRect: TRect; { area for animation bar }
Interval: Integer { wait in msecs between paints}
);
destructor Destroy; override;
end;
implementation
constructor TAnimationThread.Create(PaintSurface: TWinControl;
PaintRect: TRect;
Interval: Integer);
begin
inherited Create(True); { suspended }
FreeOnterminate := True;
Priority := tpHigher;
FInterval := Interval;
FWnd := PaintSurface.Handle;
FPaintRect := PaintRect;
FfgPattern := CreatePatternBitmap(ANI_GRAD_FG_COLOR_BAGIN, ANI_GRAD_FG_COLOR_END);
FbkPattern := CreatePatternBitmap(ANI_GRAD_BK_COLOR_BAGIN, ANI_GRAD_BK_COLOR_END);
end;
destructor TAnimationThread.Destroy;
begin
inherited Destroy;
FfgPattern.Free;
FbkPattern.Free;
end;
procedure TAnimationThread.Animate;
begin
Resume;
Sleep(0);
end;
function TAnimationThread.CreatePatternBitmap(AColorBegin, AColorEnd: TColor): TBitmap;
begin
Result := TBitmap.Create;
Result.PixelFormat := pf24bit;
UpdatePattern(Result, AColorBegin, AColorEnd);
end;
type
PRGBTripleArray = ^TRGBTripleArray;
TRGBTripleArray = array[0..32767] of TRGBTriple;
TGradientColors = array[0..255] of TRGBTriple;
procedure PatternBuilder(const Colors: TGradientColors; Pattern: TBitmap);
var
Y: Integer;
Row: PRGBTripleArray;
begin
Pattern.Width := 1;
Pattern.Height := 256;
for Y := 0 to 127 do
begin
Row := PRGBTripleArray(Pattern.ScanLine[Y]);
Row[0] := Colors[Y];
Row := PRGBTripleArray(Pattern.ScanLine[Y + 128]);
Row[0] := Colors[255 - Y];
end;
end;
procedure TAnimationThread.UpdatePattern(Pattern: TBitmap; ColorBegin, ColorEnd: TColor);
var
Colors: TGradientColors;
dRed, dGreen, dBlue: Integer;
RGBColor1, RGBColor2: TColor;
RGB1, RGB2: TRGBTriple;
Index: Integer;
begin
RGBColor1 := ColorToRGB(ColorBegin);
RGBColor2 := ColorToRGB(ColorEnd);
RGB1.rgbtRed := GetRValue(RGBColor1);
RGB1.rgbtGreen := GetGValue(RGBColor1);
RGB1.rgbtBlue := GetBValue(RGBColor1);
RGB2.rgbtRed := GetRValue(RGBColor2);
RGB2.rgbtGreen := GetGValue(RGBColor2);
RGB2.rgbtBlue := GetBValue(RGBColor2);
dRed := RGB2.rgbtRed - RGB1.rgbtRed;
dGreen := RGB2.rgbtGreen - RGB1.rgbtGreen;
dBlue := RGB2.rgbtBlue - RGB1.rgbtBlue;
for Index := 0 to 255 do
with Colors[Index] do
begin
rgbtRed := RGB1.rgbtRed + (Index * dRed) div 255;
rgbtGreen := RGB1.rgbtGreen + (Index * dGreen) div 255;
rgbtBlue := RGB1.rgbtBlue + (Index * dBlue) div 255;
end;
PatternBuilder(Colors, Pattern);
end;
procedure TAnimationThread.PaintTargetWindow;
var
DC: HDC;
begin
DC := GetDC(FWnd);
if DC <> 0 then
try
BitBlt(DC,
FPaintRect.Left,
FPaintRect.Top,
FImageRect.Right,
FImageRect.Bottom,
FBitmap.Canvas.handle,
0, 0,
SRCCOPY);
finally
ReleaseDC(FWnd, DC);
end;
end;
procedure TAnimationThread.Execute;
var
Left, Right: Integer;
Increment: Integer;
State: (incRight, incLeft, decLeft, decRight);
begin
InvalidateRect(FWnd, nil, True);
FBitmap := TBitmap.Create;
try
with FBitmap do
begin
Width := FPaintRect.Right - FPaintRect.Left;
Height := FPaintRect.Bottom - FPaintRect.Top;
FImageRect := Rect(0, 0, Width, Height);
end;
Left := 0;
Right := 0;
Increment := FImageRect.Right div 50;
State := Low(State);
while not Terminated do
begin
with FBitmap.Canvas do
begin
StretchDraw(FImageRect, FbkPattern);
case State of
incRight:
begin
Inc(Right, Increment);
if Right > FImageRect.Right then begin
Right := FImageRect.Right;
Inc(State);
end;
end;
incLeft:
begin
Inc(Left, Increment);
if Left >= Right then begin
Left := Right;
Inc(State);
end;
end;
decLeft:
begin
Dec(Left, Increment);
if Left <= 0 then begin
Left := 0;
Inc(State);
end;
end;
decRight:
begin
Dec(Right, Increment);
if Right <= 0 then begin
Right := 0;
State := incRight;
end;
end;
end;
StretchDraw(Rect(Left, FImageRect.Top, Right, FImageRect.Bottom), FfgPattern);
end; { with }
// Synchronize(PaintTargetWindow); // not painting when the main thread is blocked
PaintTargetWindow;
SleepEx(FInterval, False);
end; { While }
finally
FBitmap.Free;
end;
end;
end.
Usage: drop a TButton
and a TPanel
on the main form.
uses AniThread;
procedure TForm1.Button1Click(Sender: TObject);
var
at: TAnimationThread;
begin
at := TAnimationThread.Create(Panel1, Panel1.ClientRect, 10);
Button1.Enabled := False;
try
at.Animate;
Sleep(3000); // sleep 3 sec. block main thread
finally
at.Terminate;
Button1.Enabled := True;
end;
end;
I know many of you will disapprove with this approach. But now it's mainly a challenge for me to MAKE IT WORK well. Any help with this issue will be much appreciated.
EDIT:
This is the original article (by Peter Below, TeamB). I only implemented the gradient painting.
Canvas does not allow drawing.
Exception In the line:Is caused by the fact that
TBitmap
canvas is not thread safe unless you lock it (even in the main UI thread). in my experience even if you do Lock the canvas in a worker thread it's DC might be freed by Graphics.pas Garbage collection/GDI caching, while messages are processed in the main UITWinControl.MainWndProc
. Every bitmap canvas that is being accessed needs to be locked includingFBitmap
+FbkPattern
+FfgPattern
in my code.See
FreeMemoryContexts
inGraphis.pas
:Possible solution is NOT using
TBitmap.Canvas
directly and use aCreateCompatibleDC
as described here: How to load images from disk in background (multiple threads) [AKA: TBitmap is not thread-safe] or lock everyTCanvas
you use.More references:
How threadsafe is TBitmap
GDI handle leak using TGIFImage in a second thread
QC: TJPEGImage.Draw() is not thread safe
The code that worked for me insured every
TBitmap.Canvas
is being locked in the worker thread context:Working TAnimationThread
This works solid whether the main UI thread is blocked or not.
Now, if I omit for example locking
FfgPattern.Canvas.Lock;
, the DC of theTBitmap
s is being killed while I move the UI form (in case where I do NOT block the main thread i.e not Sleeping for 5 seconds and not terminating the threads).My conclusions:
"you cannot draw on a VCL control from anything but the main thread" (From the comments). Not true! Any main VCL windowed control DC can bee accessed from a worker thread without any problems (in fact, many applications draw directly to the Desktop window DC for example).
TBitmap
canvas is thread safe if you know where/when to lock it.Since I'm not sure where/when to lock it, better NOT to use
TBitmap
canvas in a worker thread. use API bitmap manipulations, useCreateCompatibleDC/CreateBitmap
; TWICImage which stands on top of Windows Imaging Components.TBitmap
garbage collection is evil!I do not recommend this method. a better method would be to create a pure API Window in the context of the worker thread and show activity indicator there e.g. Displaying splash screen in Delphi when main thread is busy
The best approach (as already mentioned in the comments) is to do the hard work in a worker thread and show activity indicator in the main UI tread while the worker thread is working.
Again, the only threadsafe way to draw on a window is to draw from the same thread that created a window; anything else is unsafe.
As a possible explanation why your code worked well with old Windows versions and does not work with modern versions read this The Old New Thing article.
Initially this always crashed. Then I found the solution:
1) Wrap the
while
loop inside a try-finally structure withFBitmap.Canvas.Lock;
:2) In
FormCreate
of your application call this procedure:Now it works perfectly - never crashed so far! Delphi XE2, Win7 x64