PDA

Ver la Versión Completa : Líneas [TCanvas]


dec
30-06-2006, 19:16:46
Algo tan sencillo como trazar una línea puede dar motivo para un artículo. Sobre todo cuando lo intentamos en Windows. No pretendo contar todas las posibles combinaciones de funciones y constantes que se pueden utilizar para este propósito. Mi objetivo es, simplemente, destacar un par de curiosidades que me han venido a la mente hace poco más de quince minutos.

¿PASARSE O QUEDARSE CORTO?<br>

Si lo que deseamos es, sencillamente, dibujar una sola línea, la técnica adecuada consiste en combinar los métodos MoveTo y LineTo, que pertenecen a la clase TCanvas. El prototipo de los mismos es el siguiente:


procedure TCanvas.MoveTo(X, Y: Integer);
procedure TCanvas.LineTo(X, Y: Integer);


Se supone que toda superficie de dibujo en Windows (device context, en jerga técnica) recuerda una determinada posición en su interior, a la que se le llama "posición actual". Algunas de las funciones de dibujo de Windows, no todas, utilizan esta posición como punto de partida, ahorrándonos un par de parámetros en su ejecución.

Parece algo elemental, pero el siguiente hecho es ignorado por muchos programadores:

WINDOWS SE QUEDA CORTO

Las líneas dibujadas con LineTo no incluyen el punto terminal pasado como parámetro.

El fragmento de código que muestro a continuación traza un segmento de línea desde el punto (0, 0) hasta el punto (99, 99), en vez de llegar al (100, 100):


procedure TForm1.FormPaint(Sender: TObject);
begin
Canvas.MoveTo(0, 0);
Canvas.LineTo(100, 100);
end;


Podemos observar que ninguno de estos métodos indica el color de la línea, ni su grosor o estilo. ¡Bien, esta es una característica de la Programación Orientada a Objetos! (a veces, hasta Microsoft acierta). En vez de declarar una función con montones de parámetros inútiles que cambian con muy poca frecuencia, un objeto puede almacenar estos valores más o menos estables como parte de su estado interno. En la encapsulación del API de Windows que hace la VCL de Delphi, por ejemplo, el aspecto de la línea dibujada depende de la propiedad Pen de la superficie de dibujo. Aunque sería curioso investigar todas las posibilidades, dejaremos esa tarea para un mejor día.

MAS SOBRE LA POSICION ACTUAL

Los metodos presentados anteriormente (MoveTo y LineTo) encapsulan las siguientes funciones del API de Windows:


function MoveToEx(DC: HDC; X, Y: Integer; P: PPoint): Bool;
function LineTo(DC: HDC; X, Y: Integer): Bool;


Es interesante comprobar que el equivalente a MoveTo en el API es más potente que su traducción a Delphi. En particular, MoveToEx permite pasar un puntero a una variable de tipo TPoint, para recuperar la posición del cursor gráfico antes del movimiento.

¿Puede averiguarse la posición del cursor gráfico desde Delphi? Sí, si utilizamos la propiedad PenPos, también perteneciente a TCanvas. Es interesante saber que dicha propiedad permite también asignaciones, de modo que podemos llamar indirectamente a MoveToEx modificando PenPos. Para recuperar la posición actual en la implementación de PenPos, sin embargo, se utiliza la siguiente función del API:


function GetCurrentPositionEx(DC: HDC; Point: PPoint): BOOL; stdcall;


LINEAS MAS COMPLEJAS

¿Y si tenemos que dibujar más de un segmento de línea? Parece una pregunta tonta, pues al parecer basta con llamar consecutivamente al método LineTo:


Canvas.MoveTo(X0, Y0);
Canvas.LineTo(X1, Y1);
Canvas.LineTo(X2, Y2);


Como cada ejecución de LineTo deja el cursor gráfico en el punto final, la llamada que sigue a LineTo toma como punto de partida esa misma posición, y los segmentos quedan enlazados.

Aunque la técnica es correcta, tiene un grave problema: nos obliga a atravesar la "barrera" del sistema operativo una y otra vez. Es ligeramente más rápido, por lo general, llamar a una rutina de nuestra propia aplicación que llamar a una rutina equivalente perteneciente a las DLLs del sistema. Es cierto que se trata de una demora pequeña, pero recuerde que las funciones de dibujo suponen en muchas ocasiones un cuello de botella para nuestros programas. Además, es posible que nuestro ordenador esté equipado con una tarjeta aceleradora por hardware que ofrezca una operación especial para optimizar el trazado de varias líneas enlazadas.

Entonces es el momento de llamar a Polyline.


procedure TCanvas.Polyline(const Points: array of TPoint);


A Polyline debemos pasarle un vector "abierto" de puntos, para que Windows trace segmentos de recta entre cada par consecutivo de puntos. La ventaja del uso de esta función se explica porque toda la operación se realiza con una sola llamada al sistema operativo, y así podemos eliminar el coste asociado al paso por la barrera antes mencionada.


La versión original de Polyline, en el API de Windows, es la siguiente:


function Polyline(DC: HDC; var Points; Count: Integer): Bool;


Podemos observar que la lista de puntos se pasa como un parámetro "sin tipo", pues se ha utilizado únicamente la palabra clave var en su declaración formal. Esta técnica es sumamente peligrosa, pues permite suministrar "cualquier cosa" en el parámetro Points.


PASANDO UNA LISTA DE PUNTOS

Si no ha trabajado antes con vectores de longitud dinámica en parámetros, puede que le cueste un poco acostumbrarse a funciones como Polyline. Le muestro a continuación un pequeño ejemplo que dibuja un hexágono en el área interior de una ventana:


procedure TForm1.FormPaint(Sender: TObject);
var
Pts: array [0..6] of TPoint;
R, PiDiv3, Sin, Cos: Extended;
I: Integer;
begin
PiDiv3 := Pi / 3;
R := Min(ClientWidth, ClientHeight) div 2;
for I := 0 to 5 do
with Pts[I] do
begin
SinCos(I * PiDiv3, Sin, Cos);
X := Round(R * Cos) + ClientWidth div 2;
Y := Round(R * Sin) + ClientHeight div 2;
end;
Pts[6] := Pts[0];
Canvas.Polyline(Pts);
end;


El ejemplo ha resultado muy sencillo: sabemos siempre que necesitamos 6 puntos, así que la memoria para el array se ha reservado automáticamente, en una variable local. Si la cantidad de puntos hubiese sido variable, la solución habría consistido en pedir memoria dinámicamente:


var
Pts: array of TPoint;
begin
// ...
SetLength(Pts, CantidadPuntos);
// ... llenar la lista ...
Canvas.Polyline(Pts);
end;


¿POR QUE POLYLINE, Y NO POLYGON?

Podíamos haber utilizado el método Polygon, y habernos ahorrado un punto. Pero Polygon rellena el interior de la figura, mientras que Polyline no.

MAS DE UNA LINEA A LA VEZ

Llevemos ahora la técnica anterior a su máximo desarrollo. Supongamos esta vez que sí, efectivamente, queremos dibujar muchos segmentos de recta, pero esta vez los segmentos no van a ser consecutivos. Piense en cualquier dibujo formado por rectas que no se pueda reproducir sin levantar el lápiz del papel. Por ejemplo, las líneas de una rejilla. Claro que podemos llamar varias veces a Polyline, pero queremos algo más potente. Y ese algo más potente existe.

La función en cuestión se llama (cacofónicamente) PolyPolyline. Lamentablemente, la clase TCanvas no soporta directamente esta función, y tenemos que "bajar" al nivel del API para poder emplearla. He aquí su declaración:


function PolyPolyline(DC: HDC; const PointStructs; const Points; p4: DWORD): Bool;


El primer parámetro es el handle del contexto de dispositivo: ahí debemos pasar el valor de la propiedad Handle del objeto Canvas sobre el cual queremos dibujar. El segundo parámetro es ya un poco más complicado. Su declaración comienza con el modificador const, y no utiliza ningún identificador de tipo después del nombre del parámetro. Esto quiere decir que ahí podemos pasar la dirección de cualquier tipo de variable. Teóricamente. En la práctica, debemos pasar la dirección inicial de un array con TODOS los puntos del dibujo final concatenados. Queríamos trazar varias polilíneas, ¿no? Pues nos montamos un supervector con todos esos puntos.

Naturalmente, hay que indicar dónde comienza y dónde termina la definición de cada polilínea. Esa es la función del tercer parámetro, donde debemos pasar otro array, esta vez de valores enteros. Cada elemento suyo debe corresponder a la cantidad de puntos de la polilínea correspondiente. Si, por ejemplo, queremos dibujar una polilínea de 5 puntos, otra de 10 y finalizar con una de 4, el tercer parámetro debe apuntar a un vector con 3 elementos: 5, 10 y 4 serán los valores de sus elementos.

Así que el tercer parámetro sirve para establecer las dimensiones del segundo. Entonces, el cuarto parámetro es el que indica las dimensiones del tercero. En nuestro ejemplo teníamos 3 polilíneas: hay que pasar el valor 3 en el último parámetro.

El siguiente ejemplo dibuja dos hexágonos en la superficie de un formulario. El segundo está rotado 30 grados en relación con el primero:


procedure TForm1.FormPaint(Sender: TObject);
var
Pts: array [0..13] of TPoint;
Longitudes: array [0..1] of Cardinal;
R, PiDiv3, Sin, Cos: Extended;
I: Integer;
begin
PiDiv3 := Pi / 3;
R := Min(ClientWidth, ClientHeight) div 2;
// Preparar el primer polígono, de 7 puntos
Longitudes[0] := 7;
for I := 0 to 5 do
with Pts[I] do
begin
SinCos(I * PiDiv3, Sin, Cos);
X := Round(R * Cos) + ClientWidth div 2;
Y := Round(R * Sin) + ClientHeight div 2;
end;
Pts[6] := Pts[0];
// El segundo polígono, también de 7 puntos
Longitudes[1] := 7;
for I := 7 to 12 do
with Pts[I] do
begin
SinCos(I * PiDiv3 + PiDiv3 / 2, Sin, Cos);
X := Round(R * Cos) + ClientWidth div 2;
Y := Round(R * Sin) + ClientHeight div 2;
end;
Pts[13] := Pts[7];
// Dibujar el doble hexágono
PolyPolyline(Canvas.Handle, Pts[0], Longitudes, 2);
end;


SENO Y COSENO

La función SinCos está declarada en la unidad Math, y es otro claro ejemplo de la política "mientras menos viajes, mejor". La instrucción de la FPU (floating point unit) que calcula el seno, también calcula de paso el coseno. Si llamamos a las conocidas funciones Sin y Cos por separado, estamos desaprovechando esta oportunidad de ahorrar tiempo.

VARIABLES "SEMIDINAMICAS"

Ahora ya puedo confesar por qué se me ocurrió escribir acerca de las líneas. La culpa la tienen las rejillas de datos. Más exactamente: la técnica de dibujo que utilizan los componentes TCustomGrid y sus descendientes. ¿Cómo se dibuja una rejilla? Evidentemente, hay muchas líneas por medio, y no puede utilizarse Polyline, sencillamente, para dibujarlas. Hay que utilizar PolyPolyline.

En cualquier caso, hay que pasar una lista con cantidad variable de puntos a cualquiera de estas dos funciones. La cantidad de puntos es variable porque depende principalmente del tamaño de la rejilla, que puede variar en tiempo de ejecución, pero también de varias opciones de dibujo. Así que no podemos reservar un array en la pila de un procedimiento, como hemos hecho antes. ¿Qué podemos hacer?

Quizás la solución más obvia, a partir de Delphi 4, sería recurrir a vectores dinámicos para almacenar los extremos de los segmentos. Supongamos que necesitamos un array con N puntos:


var
Pts: array of TPoint;
begin
// ... ya hemos visto este ejemplo ...
SetLength(Pts, N);
// ... llenar la lista y llamar a PolyPolyline ...
end;


Lo malo es que SetLength reserva memoria de la zona conocida como heap. Y la operación de asignación de memoria dinámica es un poco lenta. Para empeorar las cosas, la llamada a SetLength debe hacerse dentro del procedimiento de dibujo de la rejilla; esto es, dentro de una zona que puede ocasionar un cuello de botella.

De modo que necesitamos memoria dinámica, pero el comportamiento de la misma se parecerá al de la memoria automática (o local, o de pila, como prefiera denominarla): dentro de un mismo procedimiento se reservará y se liberará. ¿Por qué no pedir memoria dinámica dentro de la propia pila del programa? Pues eso es lo que hace Delphi. Abra el fichero grids.pas, del código fuente de la VCL, y busque las siguientes dos funciones:


function StackAlloc(Size: Integer): Pointer; register;
asm
{ Guarrerías en ensamblador }
end;

procedure StackFree(P: Pointer); register;
asm
{ Cochinadas misceláneas, que no vienen a cuento }
end;


Ahí está: la función StackAlloc "roba" un trozo de memoria del stack frame (pido perdón a los puristas del lenguaje) del procedimiento activo. La forma en la que ocurre el robo está diseñada para que sea compatible con el código generador por el compilador de Delphi. StackAlloc, además, devuelve el puntero a la zona "robada". Trabajamos con el área reservada y, finalmente, debemos liberarla con StackFree (aunque este paso, en realidad, no es necesario según la propia Borland).