问题
I am trying to make an arcade game for my school project. The basic idea is to do all the math and drawing in other thread than the main, and to use the main thread only for input routines. Drawing is done by a procedure saved in an external unit, and is done by creating a bitmap, then drawing parts of the environment on the bitmap and finally dawing the bitmap on the main form's canvas. When I finished the drawing procedure, I tried to run it from the main thread, and managed to make everythink work as expected (except for the fact that the whole application window was frozen, but since the main thread was working without stopping, sommething like that was expected). Then i tried to put the procedure in other thread, and it stopped working (it didnt draw a single thing despite debug routines reporting that the procedure was repeatedly executed). After a few added and then deleted debug routnes, it started to work for no apparent reason, but not reliably. in about 80% of cases it runs smoothly, but in the rest it stops after ten to thirty frames, sometimes even not draving some of the environment parts in the last frame where it gets stuck.
The important part of the main form unit looks like this
procedure TForm1.Button1Click(Sender: TObject);
begin
running:=not running;
if running then AppTheard.Create(false);
end;
Procedure AppTheard.execute;
begin
form1.Button1.Caption:='running';
while running do begin view.nextframe; end;
form1.Button1.Caption:='no longer running';
end;
and the nextframe procedure in the other unit looks like this
Camera = class
owner:Tform;
focus:GravityAffected;
Walls:PBlankLevel;
Creeps:MonsterList;
FrameRateCap,lastframe:integer;
Background:TBitmap;
plocha:TBitmap;
RelativePosY,RelativePosX:integer;
constructor create(owner:Tform; focus:GravityAffected; Walls:PBlankLevel; Creeps:MonsterList; FrameRateCap:integer; background:TBitmap);
procedure nextframe;
end;
procedure camera.nextframe;
var i,i1,top,topinfield, left,leftinfield: integer ;
procedure Repair
//some unimportant math here
Procedure vykresli(co:vec);
begin
if co is gravityaffected then
plocha.Canvas.Draw(co.PositionX*fieldsize+Gravityaffected(co).PosInFieldX-Left*fieldsize+leftinfield-co.getImgPosX,
co.PositionY*fieldsize+Gravityaffected(co).PosInFieldY-top*fieldsize+topinfield-co.getImgPosY,
co.image)
else
plocha.Canvas.Draw(co.PositionX*fieldsize-Left*fieldsize+leftinfield-co.getImgPosX,
co.PositionY*fieldsize-top*fieldsize+topinfield-co.getImgPosY,
co.image);
end;
begin
// some more unimportant math
vykresli(focus);
For i:= Left+1 to left+2+(plocha.Width div fieldsize) do //vykreslení zdí
For i1:= Top+1 to top+2+(plocha.Height div fieldsize) do
if (i< Walls.LevelSizeX) and (i1< Walls.LevelSizeY) and (i>=0) and (i1>=0) and walls.IsZed(i,i1) then
begin vykresli(walls^.GiveZed(i,i1)^);end;
while abs((gettickcount() mod high(word))-lastframe) < (1000 div FrameRateCap) do sleep(1);
lastframe:=gettickcount mod high (word);
owner.Canvas.Draw(-fieldsize,-fieldsize,plocha);
end;
Can someone please tell me what am I doing wrong?
Edit: I got the help I asked for, but after a few more years, I realized that the advice I really needed was not to use threads at all and try something like this instead.
回答1:
I see a number of things wrong in your approach at this.
1) All VCL interaction must be done from within the main thread
Your thread is directly accessing VCL controls. You cannot do this, as VCL is not thread-safe. You have to synchronize all your events back to the main thread, and let the main thread do this work.
2) All custom UI drawing (to the form) must be done from within the form's OnPaint
event.
This explains why it works sometimes and not other times. The form is automatically painted, and if you don't use this event, your custom drawing will just be drawn over by the VCL.
3) All UI drawing must be done from within the main thread
This brings us back to points 1 and 2. VCL is not thread-safe. Your secondary thread should only be responsible for performing calculations, but not drawing the UI. After performing some calculation or doing some lengthy work, you must synchronize the results back to the main thread, and let that main thread do the drawing.
4) The thread should be entirely self-contained
You shouldn't put any code in this secondary thread which has any knowledge of how it will be displayed. In your case, you are explicitly referencing the form. Your thread should not even know if it's being used by a form. Your thread should only perform the lengthy calculation work, and have absolutely 0 consideration of the user interface. Synchronize events back to your main form when you need to instruct it to redraw.
Conclusion
You need to research thread safety. You will be able to answer most of your own questions by doing so. Make this thread strictly only to take care of the heavy work which would otherwise bog down the UI. Don't worry much about a slow UI, most modern computers are able to perform complex drawing in a small fraction of a second. That doesn't need to be in a separate thread.
EDIT
After a few more years of experience, I've come to realize that #3 above is not necessarily true. In fact, in many cases, it's a great approach to perform detailed drawing from within a thread, but then the main thread would only be responsible for rendering that image to the user.
That, of course, is a whole topic of its own. You need to be able to safely paint the image which is managed in one thread to the other thread. This, also, requires use of Synchronize
.
回答2:
- Create a TThread subclass and pass all variables needed in the constructor.
- Store these variables in the private section of your TThread subclass.
- Create and Free them as needed in the constructor and destructor.
- Create an event handler (ex OnThreadPaint). You may pass it in the Constructor too.
- As mentioned your thread must be entirely self-contained (variables, code, etc).
- Place your code in the Execute procedure of the thread. You may construct a Bitmap or whatever, draw on it (as you would do in your actual (ex TForm's) canvas. Remember to resize the TBitmap instance.
- Finally, Call the
Synchronized
method passing the event handler (OnThreadPaint). This will execute the event in main thread. This can be considered as 'double buffer' method, which will prevent any flickering on drawing. So...
TDrawThread = class(TThread) private FOnThreadPaint: TNotifyEvent; FVar: Integer; FBitamp: TBitmap; protected procedure Execute; override; procedure SynchProc; public constructor Create(aVar: Integer; onPaint:TNotifyEvent); destructor Destroy; override; property Bmp:TBitMap read FBitMap; end; constructor TDrawThread.Create(aVar: Integer; onPaint:TNotifyEvent); begin inherited Create(False); FreeOnTerminate := True; FVar := aVar; FOnThreadPaint := onPain; FBitMap := TBitMap.Create; // FVarOther := TVarOther.Create; // FVarOther.. assign end; destructor TDrawThread.Destroy; begin FBitMap.Free; // FVarOther.Free; inherited; end; procedure TDrawThread.Execute; begin FBitMap.width := .. FBitMap.height := .. // do more Drawing on the FBitmpap here if Assigned(FOnThreadPaint) then Synchronize(SynchProc); end; procedure TDrawThread.SynchProc; begin FOnThreadPaint(Self); end;
And in your main form...
TForm1 = class(TForm)
private
{ Private declarations }
procedure onMyPaint(Sender: TObject);
...
procedure TForm1.onMyPaint(Sender: TObject);
begin
with Sender as TDrawThread do begin
Canvas.Draw(0, 0, Bmp);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
TDrawThread.Create(30, onMyPaint);
end;
来源:https://stackoverflow.com/questions/26725563/drawing-to-main-form-canvas-from-a-non-main-thread