Inicio > Delphi, FireMonkey, OOP, Threads > Multi-Threading for TBitmap, TCanvas, and TContext3D

Multi-Threading for TBitmap, TCanvas, and TContext3D

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.

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:

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.

{ 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:

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.

 

Vota este post
  1. domingo, 9 de abril de 2017 a las 00:47 | #1

    Hola Germán, que interesante artículo, primero que todo agradecerte por compartirlo, al igual que el resto de tus artículos, siempre son muy interesantes… en esta ocasión me gustaría hacer una pregunta al respecto del tema… entiendo que básicamente la cuestión es que podemos dibujar en el Canvas sin necesidad de hacer la sincronización, pues ella se hace internamente… la pregunta es…

    ¿Entonces puedo hacer por ejemplo un LoadFromStream en un objeto TImage.Bitmap desde cualquier hilo y sin necesidad de sincronizar?, gracias ;)

  2. FredyCC
    lunes, 10 de abril de 2017 a las 02:28 | #2

    Que interesante información Germán, muchas gracias por la explicación, demo y código fuente.

    Saludos

  3. lunes, 10 de abril de 2017 a las 08:24 | #3

    @Jhonny
    Hola Jhonny, muchas gracias por los comentarios.

    La respuesta a tu pregunta es que si. Es más, tenía entre las otras pruebas que finalmente no añadí al ejemplo, una que limpiaba el Bitmap, cargando una imagen en blanco. Por temas de claridad y sencillez, no está en el ejemplo, pero funciona sin problemas.

  4. martes, 11 de abril de 2017 a las 01:10 | #4

    Hola Germán, la verdad que cuando vi esta característica implementanda para ser usada «tal como si nada» me pareció una maravilla, y me encantaría tener el código fuente para poder estudiarlo.

    Como aun no hice ningún experimento, mucho más no puedo aportar. En realidad uno de mis puntos más flojos es el procesamiento de gráficos, así que es una buena ocasión para comenzar

    Buen artículo!

  5. martes, 11 de abril de 2017 a las 07:46 | #5

    @Agustin Ortu
    Hola Agustín, gracias por el comentario.
    Imagino que te refieres al código fuente de la VCL/FMX, el del artículo es el que muestro que es que no tiene mayor complicación y el proyecto completo está al final del artículo.

  6. martes, 18 de abril de 2017 a las 20:55 | #6

    @Germán Estévez

    ¡Genial!, gracias :)

  7. viernes, 28 de abril de 2017 a las 14:39 | #7

    Que mérito tienes German en ser de los primeros que implementan esta característica del nuevo Delphi Tokyo, fenomenal post !!!

  8. sábado, 29 de abril de 2017 a las 19:48 | #8

    @Javier Pareja
    Gracias Javier por el comentario.

  1. Sin trackbacks aún.