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.
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
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 ;)
Que interesante información Germán, muchas gracias por la explicación, demo y código fuente.
Saludos
@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.
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!
@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.
@Germán Estévez
¡Genial!, gracias :)
Que mérito tienes German en ser de los primeros que implementan esta característica del nuevo Delphi Tokyo, fenomenal post !!!
@Javier Pareja
Gracias Javier por el comentario.