Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

delphi - How to correctly implement threads for drawing on a canvas?

I want to create a custom control derived from TCustomControl which I will be overriding the Paint method and drawing things such as a gradient background, graphics and shapes etc and then finally a grid over the top of it all.

I know all this is likely to be slow, so to optimize it all I thought about using threads, eg one thread to paint the background, one thread for painting the shapes and one thread for painting the grid, but I am not too confident in understanding and implementing it all correctly.

Through trial and error and looking at some thread examples (though I could never find any good thread painting examples) I managed to come up with the following which would be my general purpose thread class:

type
  TCanvasThread = class(TThread)
  private
    FOnThreadPaint: TNotifyEvent;
    FCanvas: TCanvas;
  protected
    procedure Execute; override;
    procedure Sync;
  public
    constructor Create(Canvas: TCanvas; OnPaint: TNotifyEvent);
    destructor Destroy; override;

    property Canvas: TCanvas read FCanvas;
  end;

constructor TCanvasThread.Create(Canvas: TCanvas; OnPaint: TNotifyEvent);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FCanvas := Canvas;
  FOnThreadPaint := OnPaint;
end;

destructor TCanvasThread.Destroy;
begin
  inherited Destroy;
end;

procedure TCanvasThread.Execute;
begin
  if Assigned(FOnThreadPaint) then
    Synchronize(Sync);
end;

procedure TCanvasThread.Sync;
begin
  FOnThreadPaint(Self);
end;

And the above is implemented into the custom control like so:

type
  TMyControl = class(TCustomControl)
  private
    procedure OnClientPaint(Sender: TObject); // paint gradient
    procedure OnShapesPaint(Sender: TObject); // paint shapes etc
  protected
    procedure Paint; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

constructor TMyControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Width := 600;
  Height := 400;
end;

destructor TMyControl.Destroy;
begin
  inherited Destroy;
end;

procedure TMyControl.OnClientPaint(Sender: TObject);
begin
  GradientFillCanvas(TCanvasThread(Sender).Canvas, clSilver, clWhite, ClientRect, gdVertical);
end;

procedure TMyControl.OnShapesPaint(Sender: TObject);
begin
  TCanvasThread(Sender).Canvas.Rectangle(50, 50, 100, 100);
end;

procedure TMyControl.Paint;
begin
  TCanvasThread.Create(Canvas, OnClientPaint);
  TCanvasThread.Create(Canvas, OnShapesPaint);

  // implement other paint threads etc..
  // TCanvasThread.Create(Canvas, OnGridPaint);
  // ...

  // using regular canvas drawing here seems to be blocked too?
end;

With the above I can see the gradient painted and I can see a white rectangle shape been drawn, but there is loads of flicker when resizing the controls window (eg when aligned to client), I had thought of double buffering with a bitmap but if possible would prefer to use just the canvas only. I also can no longer draw using the regular controls canvas as highlighted by the commented line in TMyControl.Paint.

Have I misunderstood something basic here and I've implemented it all wrong? I read things like critical sections and thread pools etc but it is somewhat overwhelming. I experimented with Canvas.Lock and Canvas.UnLock but everything flickers regardless when resizing and I cannot draw on the regular canvas after creating my threads in the Paint method.

So my question is how do I correctly implement threads for drawing on a canvas? Is the code all wrong and I need to start again and implement it in a correct way? Im really lost off at this point and finding it rather confusing, I even tried moving where I create the threads in the Paint method to a intercepted WM_SIZE message method which did reduce the flicker somewhat but not completely, I am worried I may have missed something bigger here so would appreciate some feedback and guidance please.

Thanks.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

1) You can not use VCL in multithreaded fashion. It just is not designed for it. The overhead for pervasive latches and locks for multithreading would be huge, and the benefit - unmeasurably infinitesimal for 99% of applications.

2) And you DO NOT use your Canvas in multithreading way anyway. See this your code:

procedure TCanvasThread.Execute;
begin
  if Assigned(FOnThreadPaint) then
    Synchronize(Sync);

What does it mean? It mean exactly "I do not want to do multithreading, I want to run all my work in the single Main VCL Thread".

What does Synchronize call do? Read the documentation, Synchronize in its basic form means "temporarily stop this thread and do its work in a single-threaded way". Now, if ALL AND EVERY of your background worker threads are doing "stop me and do my work in the single thread" - then that is what it says. You create the threads only to immediately stop them, but all the work is transmitted to the single Main VCL Thread instead. You only allocated damn lot of resources that you do not use. You made single-threaded application with lot of burden created-only-to-be-stopped extra threads. You additionally killed the predictability and created the - google the term! - race condition. You now have several "pseudo-background threads" but you never can tell which of them would work first, and which would work second.

So what are the options then?

3) first of all, only go multithreading when you have no other option left. And when you have chunks of task that's are 100% isolated, not aa single shared variable.

procedure TMyControl.Paint;
begin
  TCanvasThread.Create(Canvas, OnClientPaint);
  TCanvasThread.Create(Canvas, OnShapesPaint);

Rule is violated, you take one and the same Canvas variable and put it into both threads. You should not. IF you can not separate threads to be totally isolated - then you most probably don't have a task that is multi-threadable.

Oookay, I am too rigorous, there are tasks where some SMALL amount on variables can be shared, on the premise ANY access to them is BRILLIANTLY ARBITRATED so never ever 2+ threads would do it at once. But for any novice the thumb rule is like I said: 100% isolation or no multi-threading, not even 99%.

So, generally you can want to make use of a single Canvas and that means you only can have one thread doing it. Oookay. Try to use some FASTER canvas instead of standard VCL one. Like http://graphics32.org maybe. There were also TCanvas implementations over Direct 2D and GDI+ - I do not know if they are faster or not. Or some other 2D graphics library from http://torry.net/ and similar catalogues.

All in all - before trying to make slow multi-threading application - invest your time and effort into making fast single-threaded application.

4) Sometimes you really can split your picture into layers, those like Photoshop layers. This time you can hope to multithread it. You create SEVERAL different bitmaps, one per thread. You fill them with TRANSPARENT color. Then you make your threads draw their required pieces into their own bitmaps. Then in the main thread you see when ALL the threads done their work, and then in the single main thread you fuse those many transparent bitmaps one after another upon the target form's it TPainBox canvas, and you do it in the proper order. But even then you better still drop them stock TCanvas and TBitmap and use faster libraries. If nothing else, I never had reliable and fast work of stock VCL TBitmap with transparent images, they just were not designed for true transparency. And it manifests itself by some unexpected shortcomings and glitches time and again.

5) Another thing with those threads, apart races, you just do not have any right to paint on the GDI windows outside of WM_PAINT event, or in VCL terms, you just violate the contract when you paint the form (or any TWinControl) outside its Paint method (or OnPaint handler called within the base Paint method). It is just the breach of the MS Windows laws. You may offset into background threads some data cache filling, calculating or downloading some invisible data. Maybe in an extreme case even rendering of that data into those monopolized one-per-thread temporary bitmaps. But rendering the form itself, painting upon its canvas - can only be done strictly WITHIN the Paint/OnPaint, and can NOT be offloaded into any entity running after the Paint method exited. The control flow of rendering should all inside the Paint, never outside. So, threads are not applicable here: being executed outside the Paint method they do not have legal right to touch your form's canvas. You have to read some tutorials on MS Windows GDI windows and messages and how the invalidation-recreation cycles work.

6) and last thing, go find OmniThreadingLibrary and read all the tutorials and explanations you can find about it. You have to get the simple idea - multithreading always is expensive (always works less efficient than single-thread program, if calculating per-processor) and only some part of the program can be extracted into multi-threading, never a whole program, and that only 100% isolated parts of any work are truly-multithreadable. The parts of works that have any relations to one another are not getting 100% multithreaded no matter what you do. In other words, read as much of OTL tutorials and FAQs as you can to understand the simple idea: you do NOT want to multithread for most of your life. Multithreading is an exception from norm that only is worth it in some specific situations. When you are in doubt if you need multithreading or not - then you need single-threading. You only go multithreading as a last chance resort when no normal and legit mean works. That was half-joking, but only half.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...