从非主线程绘制到主窗体画布 [英] Drawing to main form canvas from a non-main thread
问题描述
主窗体单元的重要部分看起来像这样
procedure TForm1.Button1Click(Sender:TObject);
开始
运行:=未运行;
如果运行AppTheard.Create(false);
end;
过程AppTheard.execute;
begin
form1.Button1.Caption:='running';
在运行时开始执行view.nextframe;结束;
form1.Button1.Caption:='不再运行';
end;
另一个单元中的nextframe过程看起来像这样
Camera =类
owner:Tform;
focus:GravityAffected;
墙壁:PBlankLevel;
Creeps:MonsterList;
FrameRateCap,lastframe:integer;
背景:TBitmap;
plocha:TBitmap;
RelativePosY,RelativePosX:integer;
构造函数create(owner:Tform; focus:GravityAffected; Walls:PBlankLevel; Creeps:MonsterList; FrameRateCap:integer; background:TBitmap);
程序nextframe;
end;
程序camera.nextframe;
var i,i1,top,topinfield,left,leftinfield:integer;
程序修复
//一些不重要的数学在这里
程序vykresli(co:vec);
begin
如果co受重力影响,则
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
//一些更不重要的数学
vykresli(focus);
对于i:=左+ 1到左+2 +(plocha.Width div字段大小)// //vykreslenízdí
对于i1:=上+ 1到上+ 2 + (i< Walls.LevelSizeX)和(i1< Walls.LevelSizeY)和(i> = 0)和(i1> = 0)以及walls.IsZed(i,i1),则执行
begin vykresli(walls ^ .GiveZed(i,i1)^); end; ((gettickcount()mod high(word)) - lastframe) $ b while abs (1000 div FrameRateCap)do sleep(1);
lastframe:= gettickcount mod high(word);
owner.Canvas.Draw(-fieldsize,-fieldsize,plocha);
end;
有人可以告诉我我做错了什么?
1)所有VCL必须在主线程内完成交互
您的线程直接访问VCL控件。你不能这样做,因为VCL不是线程安全的。您必须将所有事件同步回主线程,并让主线程完成这项工作。
2)所有自定义UI绘图)必须在表单的 OnPaint
事件中完成。
这解释了为什么它有时会起作用,而不是其他时间。表单将自动绘制,如果您不使用此事件,您的自定义绘图将仅由VCL绘制。
3)所有用户界面绘图必须在主线程中完成
这使我们回到了第1点和第2点。VCL不是线程安全的。您的辅助线程只应负责执行计算,但不能绘制UI。在执行一些计算或做了很长时间的工作之后,您必须将结果同步回主线程,并让该主线程完成绘图。
4)该线程应该完全独立
你不应该在这个辅助线程中放置任何代码,它知道它将如何显示。在你的情况下,你明确地引用了表单。你的线程不应该知道它是否被一个表单使用。您的线程只应执行冗长的计算工作,并且绝对不考虑用户界面。当您需要指示重新绘制时,将事件同步回主表单。
结论
您需要研究线程安全性。通过这样做,您将能够回答大部分自己的问题。严格来说,只有这样才能完成这个繁重的工作,否则会导致用户界面陷入困境。不要担心太慢的用户界面,大多数现代计算机都能够在很短的时间内执行复杂的绘图。这不需要在一个单独的线程中。
编辑
经过几年的经验,我发现上述#3不一定是真的。事实上,在很多情况下,从线程内部执行详细绘制是一种很好的方法,但是主线程只负责将该图像呈现给用户。
当然,这是一个完整的话题。您需要能够安全地将在一个线程中管理的图像绘制到另一个线程。这也需要使用 Synchronize
。
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?
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
.
这篇关于从非主线程绘制到主窗体画布的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!