【问题标题】:Show activity indicator while the main thread is blocked (continue)在主线程被阻塞时显示活动指示器(继续)
【发布时间】:2011-12-26 18:21:37
【问题描述】:

继续previous question 我希望能够显示一些活动指示器即使主线程被阻塞。 (基于this article)。

根据附件代码的问题:

  • 使用Synchronize(PaintTargetWindow); 不会绘制窗口
  • 我有时会收到错误消息: Canvas does not allow drawing.在线:{FBitmap.}StretchDraw(Rect(Left, ImageRect.Top, Right, ImageRect.Bottom), FfgPattern)

这是我用来创建指标线程的代码:

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.

用法:在主窗体上放置一个TButton 和一个TPanel

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;

我知道你们中的许多人会不赞成这种方法。 但现在,让它运作良好对我来说主要是一个挑战。 对此问题的任何帮助将不胜感激。

编辑:

这是original article(作者:Peter 下面,TeamB)。我只实现了渐变画。

【问题讨论】:

  • 这不是批准或不批准该方法的问题。只是你不能从除主线程之外的任何东西上绘制 VCL 控件,而你正试图从你的第二个线程绘制。不要那样做——再读一遍:你不能从除主线程之外的任何地方绘制 VCL 控件。您需要正确使用Synchronize,或者将消息发回主窗口并让绘图代码在那里工作。
  • @Ken:“很简单,除了主线程,你不能从任何东西上绘制 VCL 控件”这与事实相去甚远。查看TCanvas.Lock 方法的Delphi 帮助,甚至还有一组方法可以帮助处理此类代码。您还忽略了抛出异常的行与绘制到 VCL 控件画布无关的事实,它绘制到位图(局部变量)的画布。顺便说一句:代码由 Peter below (TeamB) 编写,在这里工作得很好......
  • @mghie 窗口(即具有 HWND 的事物)与创建它们的线程具有亲和力。除非 MSDN 文档中另有明确说明,否则任何具有 HWND 参数的 win32 api 调用都必须遵守该参数。 TCanvas 包装了一个不受相同限制约束的 DC。但是,如果 DC 是窗口的 DC,那么您将回到 UI 线程限制。不仅如此,直接在 WM_PAINT 之外的窗口 DC 上绘画也是自找麻烦。如果数据库查询在工作线程上运行,而 UI 在主线程上运行,这个问题就变得微不足道了。
  • @David:不正确。来自不同线程的 GDI 调用完全有可能,只要它们正确同步 - 因此画布锁定。如果你相信这一点,你如何解释画布锁定的存在?
  • @mghie 有问题的 DC 归 Windows 所有,你认为它在窗口上绘画时会调用 TCanvas.Lock 吗?比如说,位图 DC 没问题。而且你真的是说windows对创建线程的亲和不是真的吗?

标签: multithreading delphi thread-safety delphi-5


【解决方案1】:

Canvas does not allow drawing. 异常在行中:

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

是由于TBitmap 画布不是线程安全的,除非您将其锁定(即使在主 UI 线程中)。根据我的经验,即使您将画布锁定在工作线程中,它的 DC 可能会被 Graphics.pas 垃圾收集/GDI 缓存释放,而消息在主 UI TWinControl.MainWndProc 中处理。正在访问的每个位图画布都需要锁定,包括我的代码中的 FBitmap + FbkPattern + FfgPattern

FreeMemoryContextsGraphis.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.
}

可能的解决方案不是直接使用TBitmap.Canvas,而是使用CreateCompatibleDC,如下所述:How to load images from disk in background (multiple threads) [AKA: TBitmap is not thread-safe] 或锁定您使用的每个TCanvas

更多参考:
How threadsafe is TBitmap
GDI handle leak using TGIFImage in a second thread
QC: TJPEGImage.Draw() is not thread safe


每个TBitmap.Canvas 为我投保的代码都被锁定在工作线程上下文中:
Working TAnimationThread
无论主 UI 线程是否被阻塞,这都能正常工作。

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;

现在,如果我省略例如锁定 FfgPattern.Canvas.Lock;,则在我移动 UI 表单时TBitmaps 的 DC 将被杀死(如果我不阻塞主线程,即不休眠 5 秒并且不终止线程)。

我的结论:

  1. “除了主线程之外,你不能在 VCL 控件上绘图”(来自 cmets)。不对!任何主 VCL 窗口控制 DC 都可以从工作线程毫无问题地访问(事实上,许多应用程序直接绘制到桌面窗口 DC)。

  2. TBitmap canvas 线程安全的,如果您知道在何处/何时锁定它。

  3. 由于我不确定在何处/何时锁定它,最好不要在工作线程中使用 TBitmap 画布。使用 API 位图操作,使用CreateCompatibleDC/CreateBitmap; TWICImage 位于 Windows 映像组件之上。 TBitmap垃圾回收是邪恶的!

  4. 我不推荐这种方法。更好的方法是在工作线程的上下文中创建一个纯 API 窗口并在那里显示活动指示器,例如Displaying splash screen in Delphi when main thread is busy

  5. 最好的方法(如 cmets 中已经提到的)是在工作线程中完成艰苦的工作,并在工作线程工作时在主 UI 线程中显示活动指示器。

【讨论】:

    【解决方案2】:

    同样,在窗口上绘制的唯一线程安全方式是从创建窗口的同一线程中绘制。其他任何东西都是不安全的。

    请阅读The Old New Thing article,作为您的代码在旧 Windows 版本上运行良好而在现代版本上运行良好的可能解释。

    【讨论】:

    • 该系列的第二部分更好读:blogs.msdn.com/b/oldnewthing/archive/2005/10/11/479587.aspx,因为它处理设备上下文。
    • 但是当我绘制到本地 TBitMap 时,为什么会出现 Canvas does not allow drawing 异常?
    • @mghie,根据您的链接:调用GetDC等函数的线程也必须是调用ReleaseDC的线程,但与窗口句柄一样,在DC的生命周期内,任何线程都可以使用它
    • @kobik:尝试在 Vista/Win7 上以兼容模式运行您的代码,也许会有所帮助。
    • @Serg,有一些红线我愿意跨越,为了让它工作......改变兼容模式不是其中之一:)
    【解决方案3】:

    最初这总是崩溃。然后我找到了解决方案:

    1) 用FBitmap.Canvas.Lock;while 循环包裹在try-finally 结构中:

    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) 在您的应用程序的FormCreate 中调用此过程:

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

    现在它可以完美运行 - 到目前为止从未崩溃过! 德尔福 XE2、Win7 x64

    【讨论】:

    • DisableProcessWindowsGhosting 对我来说似乎非常极端,因为一旦禁用它,就没有在进程中启用窗口重影的功能。您还需要为FbkPattern + FfgPattern 锁定画布。 TBitmap 不是线程安全的,它的 DC 在 MainWndProc (FreeMemoryContexts) 中管理。即使你锁定它的画布,你也需要祈祷它不会被 Graphics.pas 垃圾收集释放......无论如何 +1 为 FBitmap.Canvas.Lock。即使在主胎面中也总是需要使用它。
    猜你喜欢
    • 1970-01-01
    • 2021-03-11
    • 1970-01-01
    • 1970-01-01
    • 2012-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多