5

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.

Community
  • 1
  • 1
kobik
  • 20,439
  • 4
  • 54
  • 115
  • 5
    It's not a matter of approving or disapproving of the approach. It's simply that you *cannot* draw on a VCL control from anything but the main thread, and you're trying to draw from your second thread. Don't do that - read again: you **cannot** draw on a VCL control from anything but the main thread. You need to use `Synchronize` properly, or post messages back to the main window and let the drawing code do it's work there. – Ken White Dec 26 '11 at 18:32
  • Put the blocking code in a different thread from the UI. That's always the answer to this question. – David Heffernan Dec 26 '11 at 18:52
  • 1
    Ken is right. Only your main thread paints correct. Only thing you can do is: let the thread work in the background an let the main thread paint your activity symbol. Once I tried to paint directliy to the screen, but that gives a real mess... – Andreas Dec 26 '11 at 18:55
  • @Ken According to previous question, the main thread is not pumping and is blocked on database query so no amount of Synchronize, PostMessage etc. is going to help. – David Heffernan Dec 26 '11 at 19:15
  • @David, you're right. But regardless of that, you still can't draw on a VCL control from a secondary thread. :) – Ken White Dec 26 '11 at 19:19
  • @Ken Yes, which leads one to reach a single conclusion...... – David Heffernan Dec 26 '11 at 19:19
  • @David, like "move the query to a secondary thread instead"? – Ken White Dec 26 '11 at 19:21
  • 2
    @Ken: "It's simply that you cannot draw on a VCL control from anything but the main thread" is very far from the truth. Check the Delphi help for the `TCanvas.Lock` method, there's even a set of methods to help with such code. You also ignore the fact that the line throwing the exception has nothing to do with painting to a VCL control canvas, it paints to the canvas of a bitmap (local variable). BTW: the code is by Peter Below (TeamB) and works just fine here... – mghie Dec 26 '11 at 19:25
  • 2
    @mghie windows (i.e. things with HWNDs) have affinity to the thread that creates them. Any win32 api call which has an HWND parameter must abide by that unless explicitly stated otherwise in the MSDN docs. `TCanvas` wraps a DC which is not bound by the same restrictions. However, if the DC is the DC of a window then you are back to the UI thread restriction. Not only all that, but it is asking for trouble to paint directly onto a window DC outside of WM_PAINT. This issue becomes trivial with db query running on worker thread and UI on main thread as nature intended. – David Heffernan Dec 26 '11 at 19:29
  • 1
    @kobik The other huge problem you face here is that blocking your UI thread will result in your windows being ghosted: they go grey and the caption has *(not responding)* suffixed. Good luck fixing that from the depths of a blocking DB query. – David Heffernan Dec 26 '11 at 19:33
  • @mghie, yes the code is not entirely mine. I only implemented the gradient painting. It worked for ages. but now with with Vista (mainly with Auro) and Win7 I get these rejects... maybe moving the `GetDC` to the constructor can fix the problem? – kobik Dec 26 '11 at 19:33
  • 2
    @David: Not true. GDI calls are difinitely possible from different threads, as long as they are properly synchronized - hence the canvas locking. How do you explain the existence of canvas locking if you believe this? – mghie Dec 26 '11 at 19:33
  • 2
    @mghie The DC in question is owned by Windows and do you think it calls `TCanvas.Lock` when it's painting on the window? It's fine for a bitmap DC, say. And are you really saying that the affinity of windows to the creating thread is not true? – David Heffernan Dec 26 '11 at 19:36
  • @David Heffernan, I use this indicator on the surface of a popup window. in the meantime it use `DisableTaskWindows` and then restore them. – kobik Dec 26 '11 at 19:36
  • @kobik: Again, the exception has nothing to do with the window dc, it's thrown when the bitmap dc is (re)created. `GetDC()` doesn't enter into it. Also, since device contexts are shared resources you should keep them for as short as possible. – mghie Dec 26 '11 at 19:37
  • @David: Since the main thread is inside a `Sleep()` call there is no way it can paint to any canvas. Synchronization is implicit here. I would properly free the thread instead of inviting problems with auto-destruction, but other than that this code is fine. – mghie Dec 26 '11 at 19:38
  • 2
    @mghie Well, I give up at this point. I'm happy for you to lead kobik to a solution that gives responsive UI whilst the UI thread is blocked. I think that route is folly, but please go ahead and prove me wrong. – David Heffernan Dec 26 '11 at 19:42
  • @mghie Did you try that code of PeterB's? Try moving the form from within the Sleep. Or clicking on the minimise button. Or minimising the form from the buttons in the non-client area. Do you really recommend this approach over doing it properly? – David Heffernan Dec 26 '11 at 20:03
  • 1
    @David: No, I don't recommend that approach, and I never said so in my comments. Of course painting from a secondary thread won't make the app magically responsive. But that's not what this question is about. – mghie Dec 26 '11 at 20:12
  • @kobik Hello, in the meantime did you solve the problems with the code in your question? If yes: Please let us know how you solved them. Thank you! – user1580348 Sep 23 '14 at 16:06
  • @user1580348, Sorry I dumped the whole idea of updating UI thread while it is blocked. I'll check your answer when I'm home. – kobik Sep 24 '14 at 15:19
  • @Ken, "you cannot draw on a VCL control from anything but the main thread" is NOT true. in fact, i DO draw directly on the VCL control DC from outside the main thread. – kobik Sep 25 '14 at 15:21
  • @David Heffernan, +1 on *"The other huge problem you face here is that blocking your UI thread will result in your windows being ghosted"* – kobik Sep 25 '14 at 15:43
  • @kobik: The fact you do it in spite of the fact you're not supposed to is meaningless. "I can indeed eat rotting meat. I just did." "I can indeed drive very fast without a seat belt." – Ken White Sep 25 '14 at 15:43
  • @KenWhite, who said I'm not suppose to? who said I'm no supposed to draw on the Desktop DC if I need to? Is that "restriction" documented? is Delphi VCL window control any different from other system windowed controls? – kobik Sep 25 '14 at 15:46
  • @kobik: You said "I DO draw directly on the VCL control DC". The Desktop DC is not a VCL control DC, because the Windows Desktop is not a VCL control. As far as "Who said you're not supposed to draw on a VCL control from other than the main thread?", the answer to that would be the authors of the VCL (at this point, Embarcadero) and Microsoft, who says you shouldn't draw on a visual control from anywhere except the thread that created it in the Win32 documentation (which covers your "system windowed control"). – Ken White Sep 25 '14 at 16:06
  • @kobik (continued): Want to see where EMBT says it? File->New->Other->Delphi Files->Thread Object, and read the large comment block at the top, or read the documentation for TThread in the help file, or read the hundreds of other questions about not accessing VCL controls from a thread other than the main GUI thread. – Ken White Sep 25 '14 at 16:08
  • @kobik: It also seems pretty silly to resume a comment discussion from nearly **three years ago** over this topic, which has been discussed many, many times before. As I said, because you **can** do something wrong doesn't mean you **should**. – Ken White Sep 25 '14 at 16:09

3 Answers3

3

Canvas does not allow drawing. Exception In the line:

FBitmap.StretchDraw(Rect(Left, ImageRect.Top, Right, ImageRect.Bottom), FfgPattern)

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 UI TWinControl.MainWndProc. Every bitmap canvas that is being accessed needs to be locked including FBitmap + FbkPattern + FfgPattern in my code.

See FreeMemoryContexts in Graphis.pas:

{ FreeMemoryContexts is called by the VCL main winproc to release
  memory DCs after every message is processed (garbage collection).
  Only memory DCs not locked by other threads will be freed.
}

Possible solution is NOT using TBitmap.Canvas directly and use a CreateCompatibleDC as described here: How to load images from disk in background (multiple threads) [AKA: TBitmap is not thread-safe] or lock every TCanvas 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.

procedure TForm1.Button1Click(Sender: TObject);
var
  at1, at2, at3, at4, at5: TAnimationThread;
begin
  at1 := TAnimationThread.Create(Panel1, Panel1.ClientRect, 10);
  at2 := TAnimationThread.Create(Panel2, Panel2.ClientRect, 10);
  at3 := TAnimationThread.Create(Panel3, Panel3.ClientRect, 10);
  at4 := TAnimationThread.Create(Panel4, Panel4.ClientRect, 10);
  at5 := TAnimationThread.Create(Panel5, Panel5.ClientRect, 10);
  // Sleep(5000); // do some work for 5 seconds, block main thread
  // at1.Terminate; at2.Terminate; at3.Terminate; at4.Terminate; at5.Terminate;
end;

enter image description here

Now, if I omit for example locking FfgPattern.Canvas.Lock;, the DC of the TBitmaps 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).

enter image description here

My conclusions:

  1. "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).

  2. TBitmap canvas is thread safe if you know where/when to lock it.

  3. Since I'm not sure where/when to lock it, better NOT to use TBitmap canvas in a worker thread. use API bitmap manipulations, use CreateCompatibleDC/CreateBitmap; TWICImage which stands on top of Windows Imaging Components. TBitmap garbage collection is evil!

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

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

Community
  • 1
  • 1
kobik
  • 20,439
  • 4
  • 54
  • 115
0

Initially this always crashed. Then I found the solution:

1) Wrap the while loop inside a try-finally structure with FBitmap.Canvas.Lock;:

FBitmap.Canvas.Lock;
try
  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.Canvas.Unlock;
end;

2) In FormCreate of your application call this procedure:

procedure DisableProcessWindowsGhosting;
var
  DisableProcessWindowsGhostingProc: procedure;
begin
  DisableProcessWindowsGhostingProc := GetProcAddress(GetModuleHandle('user32.dll'), 'DisableProcessWindowsGhosting');
  if Assigned(DisableProcessWindowsGhostingProc) then
    DisableProcessWindowsGhostingProc;
end;

Now it works perfectly - never crashed so far! Delphi XE2, Win7 x64

user1580348
  • 4,654
  • 2
  • 27
  • 73
  • 1
    `DisableProcessWindowsGhosting` seems very extreme to me since there is no function to enable window ghosting in a process once it has been disabled. You also need to lock canvas for `FbkPattern` + `FfgPattern` too. TBitmap is NOT thread safe, and it's DC is managed in the `MainWndProc` (`FreeMemoryContexts`). even if you Lock it's Canvas you need to pray it wont be freed by Graphics.pas garbage collection... Anyway +1 for FBitmap.Canvas.Lock. which ALWAYS needs to be used even in the main tread. – kobik Sep 25 '14 at 13:09
0

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.

kludg
  • 26,590
  • 4
  • 63
  • 115
  • 1
    A much better read would be the second part of that series: http://blogs.msdn.com/b/oldnewthing/archive/2005/10/11/479587.aspx since it deals with device contexts. – mghie Dec 26 '11 at 20:29
  • but why do I get that `Canvas does not allow drawing` exception, when I draw to a local TBitMap? – kobik Dec 26 '11 at 20:34
  • @mghie, according to your link: _The thread that calls functions such as GetDC must also be the one that calls ReleaseDC, but as with window handles, during the lifetime of the DC, **any thread can use it**_ – kobik Dec 26 '11 at 20:39
  • @kobik: try to run your code on Vista/Win7 in compatibility mode, maybe it will help. – kludg Dec 26 '11 at 20:50
  • @Serg, there are some red lines I'm willing to cross, in order for this to work... changing compatibility mode is not one of them :) – kobik Dec 26 '11 at 20:58
  • I implemented the code from the question in a test program (Delphi XE2, Win7) and noted a weird behavior: As soon as the window with the progress panel loses the focus while the thread is running (just click somewhere else), the application crashes! Does anybody know why this happens and how it could be avoided? – user1580348 Sep 23 '14 at 18:01