当主线程被阻塞时显示活动指示灯(继续) [英] Show activity indicator while the main thread is blocked (continue)

查看:239
本文介绍了当主线程被阻塞时显示活动指示灯(继续)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

继续


  • 最好的方法(在评论中已经提到)是在工作线程中做努力工作,并在主UI中显示活动指示器而工作线程正在工作。



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

    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;
    

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

    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.

    这篇关于当主线程被阻塞时显示活动指示灯(继续)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆