Multi-Threading for TBitmap, TCanvas, and TContext3D

Posted by on in Blogs

Mucho se ha hablado de las características de Tokyo en cuanto al soporte de nuevas plataformas. Está claro que la irrupción de Linux entre las plataformas destino de nuestras aplicaciones, es el gran atractivo de esta nueva versión. Otros compañeros de la comunidad han hablado del tema y han publicado vídeos al respecto.

 

En mi caso voy a hablar de otra de las novedades de la versión Tokyo. Se trata del soporte de multithread para las clases TBitmap, TCanvas y TContext3D en Firemonkey para poder trabajar con un único elemento desde diferentes threads.

Tal y como se explica en el enlace de la wiki, internamente las clases realizan la sincronización de forma automátca, así que aunque no ganemos en rendimiento, si podemos ganar en organización y en claridad a la hora de escribir nuestro código y clases. Imaginad que tenemos que dibujar diferentes objetos o elementos en un TCanvas. Ahora podamos organizar el trabajo en diferentes clases (Threads) que se encarguen de dibujar cada uno de ellos. De otra forma tendríamos que tener un único código o clase donde se dibujaran todos ellos (por lo tanto menos estructurado y organizado).

Antes hubiéramos utilizado un código similar a este para dibujar figuras sobre un TCanvas.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
procedure TFormMain.Button4Click(Sender: TObject);
var
  p1, p2:TPointF;
  pBrush:TStrokeBrush;
  pStroke:TBrush;
  i:integer;
  rect:TRectF;
begin
  // numeros aleatorios 
  Randomize;
  // Preparamos las caracteristicas del pintado
  pBrush := TStrokeBrush.Create(TBrushKind.Solid, GetRandomColor);
  pStroke := image1.Bitmap.Canvas.Fill;
  try
	pBrush.Dash := TStrokeDash.Solid;  // Otros
	pStroke.Kind := TBrushKind.Solid;
	//------------------------------------------------------------------
	for i := 0 to NUMBER_PIEZAS do begin
	  pBrush.Color := GetRandomColor;
	  pStroke.Color := GetRandomColor;
	  // RECTANGULO
	  rect := GetRamdomRect;
	  image1.Bitmap.Canvas.BeginScene;
	  try
		image1.Bitmap.Canvas.DrawRect(rect, Random(Trunc(rect.Width) DIV 2), 
Random(Trunc(rect.Height) DIV 2), [], Random(200), pBrush); image1.Bitmap.Canvas.FillRect(rect, Random(Trunc(rect.Width) DIV 2),
Random(Trunc(rect.Height) DIV 2), [], Random(200), pBrush); finally image1.Bitmap.Canvas.EndScene; end; Self.Caption := Format('Dibujados %d rectángulos',[i]); Self.Update; end; //------------------------------------------------------------------ for i := 0 to NUMBER_PIEZAS do begin pBrush.Color := GetRandomColor; pStroke.Color := GetRandomColor; // ELIPSES rect := GetRamdomRect;   image1.Bitmap.Canvas.BeginScene; try image1.Bitmap.Canvas.DrawEllipse(rect, Random(200), pBrush); image1.Bitmap.Canvas.FillEllipse(rect, Random(200), pBrush); finally image1.Bitmap.Canvas.EndScene; end; Self.Caption := Format('Dibujadas %d elipses',[i]); Self.Update; end; //------------------------------------------------------------------ for i := 0 to NUMBER_PIEZAS do begin pBrush.Thickness := Random(4); p1 := GetRandomPoint; p2 := GetRandomPoint; pBrush.Color := GetRandomColor;   image1.Bitmap.Canvas.BeginScene; try // LINEA image1.Bitmap.Canvas.DrawLine(p1, p2, Random(200), pBrush); finally image1.Bitmap.Canvas.EndScene; end; Self.Caption := Format('Dibujadas %d lineas',[i]); Self.Update; end; finally FreeAndNil(pBrush); end; end;

En este caso he separado por bloques (para que se vea más claro) el pintado de rectángulos, elipses y líneas.

¿Qué podemos hacer en la versión Tokyo? En mi caso he creado un Thread “base” que realiza todas las operaciones necesarias para pintar sobre un Canvas:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
procedure TPaintThread.Execute;
var
  i:Integer;
begin
  inherited;
  // Elementos dibujados
  iElems := 0;
  // Preparar las propiedades de pintado
  Brush.Dash := TStrokeDash.Solid;	// Otros
  brush.Color := 0;
  Fill.Kind := TBrushKind.Solid;
  Fill.Color := 0;
  // Lanzar la creación de elementos
  for i := 0 to NumberElements do begin
	Fill.Color := GetRandomColor;
	Brush.Color := GetRandomColor;
	Brush.Thickness := Random(6);
	FCanvas.BeginScene;
	try
	  PaintElement;    // redefinido en las clases derivadas
	finally
	  FCanvas.EndScene;
	end;
	// Sincronizamos el caption del Form (NO el pintado)
	Synchronize(UpdateCaption);
  end;
end;

Y he redefinido en clases derivadas el método de pintar los diferentes objetos. De esta forma conceptualmente tenemos una clase para cada tipo de objeto que queremos dibujar, con sus propiedades y métodos especiales si los necesitara.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{ TPaintLineThread }
procedure TPaintLineThread.PaintElement;
var
  p1, p2:TPointF;
begin
  p1.x := Random(Trunc(FMaxWidth));
  p1.y := Random(Trunc(FMaxHeight));
  p2.x := Random(Trunc(FMaxWidth));
  p2.y := Random(Trunc(FMaxHeight));
  // LINEA
  FCanvas.DrawLine(p1, p2, Random(200), Brush);
end;
 
{ TPaintRectThread }
procedure TPaintRectThread.PaintElement;
var
  rect:TRectF;
begin
  rect := GetRamdomRect;
  // RECTANGULO
  FCanvas.DrawRect(rect, Random(Trunc(rect.Width) DIV 2), Random(Trunc(rect.Height) DIV 2), 
[], Random(200), Brush); FCanvas.FillRect(rect, Random(Trunc(rect.Width) DIV 2), Random(Trunc(rect.Height) DIV 2),
[], Random(200), Brush); end;   { TPaintEllipseThread } procedure TPaintEllipseThread.PaintElement; var rect:TRectF; begin rect := GetRamdomRect; // ELIPSES FCanvas.DrawEllipse(rect, Random(200), Brush); FCanvas.FillEllipse(rect, Random(200), Brush); end;

Con este código podemos lanzar el pintado de los diferentes objetos con la siguiente sentencia:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
procedure TFormMain.Button5Click(Sender: TObject);
var
  th1:TPaintLineThread;
  th2:TPaintRectThread;
  th3:TPaintEllipseThread;
begin
  // Crear los threads
  th1 := TPaintLineThread.Create(Self, Image1.Bitmap.Canvas,
	  NUMBER_PIEZAS, Trunc(Image1.Bitmap.Width), Trunc(Image1.Bitmap.Height));
  th2 := TPaintRectThread.Create(Self, Image1.Bitmap.Canvas,
 	  NUMBER_PIEZAS, Trunc(Image1.Bitmap.Width), Trunc(Image1.Bitmap.Height));
  th3 := TPaintEllipseThread.Create(Self, Image1.Bitmap.Canvas,
	  NUMBER_PIEZAS, Trunc(Image1.Bitmap.Width), Trunc(Image1.Bitmap.Height));
  // Iniciarlos
  th1.Resume;
  th2.Resume;
  th3.Resume;
end;

El tiempo en ambos casos (como se puede ver en el vídeo que hay más abajo) no varía mucho, pero sí que ganamos en claridad y encapsulamiento.
Nuestro código ahora está más organizado, y además tenemos “separadas” partes de código que hacen “cosas diferentes”. Conceptualmente es mucho más claro.
Os adjunto un vídeo con la ejecución en ambos casos.

Además en el ejemplo que he utilizado threads se produce el efecto en la imagen, de que las figuras se pintan “mezcladas” en cuanto al tipo. Es decir, a la izquierda (secuencial) primero se pintan los recuadros, luego las elipses y finalmente las líneas. En la parte derecha, con threads, vemos que todas las figuras van apareciendo al mismo tiempo (mezcladas).
Digamos que este efecto no es ni mejor ni peor, pues es un ejemplo ilustrativo y no tenemos necesidad de hacerlo de una forma o de otra; Pero la posibilidad de usar threads facilita el segundo caso, si fuese necesario.

 

Os dejo el código fuente del proyecto.

Un saludo y hasta la próxima.

Blog posted from L'Hospitalet de Llobregat, Barcelona, Spain View larger map


About
Gold User, Rank: 73, Points: 21
Programmer and Delphi enthousiastic

Comments

Check out more tips and tricks in this development video: