30 octubre 2007

Como manejar excepciones en Delphi (y II)

Una vez que hemos visto lo que es una excepción y cómo proteger nuestro código usando bloques protegidos vamos a pasar a ver como pueden meterse unas excepciones dentro de otras para dar más seguridad a nuestro código. Es lo que se llaman excepciones anidadas.

EXCEPCIONES ANIDADAS

Vamos a ver un ejemplo de como anidar una excepción dentro de otra:

var
F: TextFile;
begin
AssignFile( F, 'C:\noexiste.txt' );

try
Reset( F );

try
CloseFile( F );
except
on E: Exception do
ShowMessage( 'Excepción 2: ' + E.Message );
end;

except
on E: Exception do
ShowMessage( 'Excepción 1: ' + E.Message );
end;
end;

En este ejemplo hemos metido un bloque protegido dentro de otro, donde cada uno controla su propia excepción. En este caso provocaría la excepción 1 ya que el archivo no existe.

DETENIENDO LA EXCEPCION

Cuando se provoca una excepción, una vez la hemos procesado con la sentencia on E: Exception, la ejecución continua hacia el siguiente bloque de código. Si queremos detener la ejecución del programa debemos utilizar el comando raise:

var
F: TextFile;
begin
AssignFile( F, 'C:\noexiste.txt' );
ShowMessage( '1' );

try
Reset( F );

except
on E: Exception do
raise;
end;

ShowMessage( '2' );
end;

En este ejemplo nunca llegaría a ejecutarse el segundo ShowMessage ya que raise detiene la ejecución del procedimiento.

FORZANDO A QUE FINALICE LA EJECUCION

Hay bloques de código en los cuales cuando se provoca una excepción ni podemos continuar con la ejecución ni podemos cortar la ejecución. Por ejemplo, supongamos que abro un archivo en módo sólo lectura e intento escribir en el mismo. Esto provocaría una excepción, pero lo que no puedo hacer es detener en seco la ejecución del programa ya que hay que cerrar el archivo que hemos abierto.

Para solucionar esto, los bloques protegidos permiten finalizar la ejecución en el caso de que se produzca una excepción mediante la claúsula finally. En nuestro ejemplo nos interesa que se cierre el archivo abierto:

var
F: TextFile;
begin
AssignFile( F, 'C:\prueba.txt' );

try
Reset( F );

try
WriteLn( F, 'intentando escribir' );
finally
ShowMessage( 'Finalizando...' );
CloseFile( F );
end;

except
on E: Exception do
raise;
end;
end;

Tenemos dos excepciones anidadas: una para abrir el archivo con una sentencia except que detiene la ejecución y otra dentro que utiliza la sentencia finally para cerrar el archivo en el caso de que se produzca un error.

TRATANDO EXCEPCIONES EN COMPONENTES VCL

Los componentes VCL también pueden provocar muchas excepciones si no sabemos utilizarlos correctamente. Un error típico es el intentar acceder a un elemento que no existe dentro de un componente ListBox llamado Lista:

begin
Lista.Items.Add( 'PABLO' );
Lista.Items.Add( 'MARIA' );
Lista.Items.Add( 'CARLOS' );

try
ShowMessage( Lista.Items[4] );
except
on E: EStringListError do
ShowMessage( 'La lista sólo tiene tres elementos.' );
end;
end;

En este caso se ha provocado una excepción de la clase EStringListError, aunque bien se podría haber controlado de este modo:

try
ShowMessage( Lista.Items[4] );
except
on E: Exception do
ShowMessage( 'La lista sólo tiene tres elementos.' );
end;

Los componentes VCL disponen principalmente de estas clases de excepciones:

EAbort: Finaliza la secuencia de eventos sin mostrar el mensaje de error.

EAccessViolation: Comprueba errores de acceso a memoria inválidos.

EBitsError: Previene intentos para acceder a arrays de elementos booleanos.

EComponentError: Nos informa de un intento inválido de registar o renombar un componente.

EConvertError: Muestra un error al convertir objetos o cadenas de texto string.

EDatabaseError: Especifica un error de acceso a bases de datos.

EDBEditError: Error al introducir datos incompatibles con una máscara de texto.

EDivByZero: Errores de división por cero.

EExternalException: Significa que no reconoce el tipo de excepción (viene de fuera).

EIntOutError: Representa un error de entrada/salida a archivos.

EIntOverflow: Especifica que se ha provocado un desbordamiento de un tipo de dato.

EInvalidCast: Comprueba un error de conversión de tipos.

EInvalidGraphic: Indica un intento de trabajar con gráficos que tienen un formato desconocido.

EInvalidOperation: Ocurre cuando se ha intentado realizar una operación inválida sobre un componente.

EInvalidPointer: Se produce en operaciones con punteros inválidos.

EMenuError: Controla todos los errores relacionados con componentes de menú.

EOleCtrlError: Detecta problemas con controles ActiveX.

EOleError: Especifica errores de automatización de objetos OLE.

EPrinterError: Errores al imprimir.

EPropertyError: Ocurre cuando se intenta asignar un valor erroneo a una propiedad del componente.

ERangeError: Indica si se intenta asignar un número entero demasiado grande a una propiedad.

ERegistryExcepcion: Controla los errores en el resigtro.

EZeroDivide: Controla los errores de división para valores reales.

EXCEPCIONES SILENCIOSAS

Para dar un toque profesional a un programa hay ocasiones en que nos interesa controlar la excepción pero que no se entere el usuario del programa. Lo que no se puede hacer es abandonar la excepción con los comandos Break o con Exit ya que puede ser peor el remedio que la enfermedad.

Para salir elegantemente de una excepción hay que utilizar el comando Abort:

try
{ sentencias }
except
Abort;
end;

De este modo se controla la excepción y el usuario no ve nada en pantalla.

Con esto finalizamos el tratamiento de excepciones en Delphi.

Pruebas realizadas en Delphi 7.

29 octubre 2007

Como manejar excepciones en Delphi (I)

Las excepciones son condiciones excepcionales que se producen en nuestra aplicación en tiempo de ejecución y que requieren un tratamiento especial. Un ejemplo de excepciones podría ser las divisiones por cero o los desbordamientos de memoria. El tratamiento de excepciones proporciona una forma estándar de controlar los errores, descubriendo anticipadamente los problemas y posibilitando al programador anticiparse a los fallos que puedan ocurrir.

Cuando ocurre un error en un programa, se produce una excepción, lo que significa que crea un objeto excepción y situa el puntero de la pila en el primer punto donde se ha provocado la excepción. El objeto excepción contiene información sobre todo lo que ha ocurrido.

Esto nos permite crear aplicaciones más robustas ya que se puede llegar a averiguar el lugar en concreto donde se ha producido el error, particularmente en áreas donde los errores puedan causar la perdida de datos y recursos del sistema.

Cuando creamos una respuesta a la excepción tenemos que hacerlo en dentro de bloques de código, los cuales se llaman bloques de código protegidos.

DEFINIENDO BLOQUES DE CODIGO PROTEGIDOS

Los bloques de código protegidos comienzan con la palabra reservada try. Si ocurre algún error dentro del bloque de código protegido, el tratamiento del error se introduce en un bloque de código que comienza con except.

Vamos a ver un ejemplo que provoca una excepción al abrir un archivo que no existe:

var
F: TextFile;
begin
AssignFile( F, 'c:\nosexiste.txt' );

try
Reset( F );
except
on E: Exception do
Application.MessageBox( PChar( E.Message ), 'Error', MB_ICONSTOP );
end;
end;

La primera parte de un bloque protegido comienza con la palabra try. El bloque try contiene el código que potencialmente puede provocar la excepción. Al provocar la excepción saltará directamente al comienzo del bloque de código que comienza con la palabra reservada except.

Como puede apreciarse en el código anterior hemos creado un objeto E que representa la excepción creada. El objeto E pertenece a la clase Exception que a su vez hereda directamente de la clase TObject. Este objeto contiene propiedades y métodos para manejar la excepción provocada.

PROVOCANCO NUESTRA PROPIA EXCEPCION

Nosotros también podemos crear nuestras propias excepciones que hereden de la clase Exception. Por ejemplo, voy a crear una excepción si una variable de tipo string está vacía. Primero defino el tipo especial de excepción:

type
ECadenaVacia = class( Exception );

Y ahora la provoco en el programa:

var
sCadena: String;
begin
if sCadena = '' then
raise ECadenaVacia.Create( 'Cadena vacia.' );
end;

El comando raise provoca a propósito la excepción para detener la ejecución del programa. No es necesario que creemos nuestros tipos de excepción. También podía haber sido así:

if sCadena = '' then
raise Exception.Create( 'cadena vacia' );

Cuando se provoca una excepción la variable global ErrorAddr declarada dentro de la unidad System contiene un puntero a la dirección de memoria donde se ha provocado el error. Esta variable es de sólo lectura a título informativo.

CONTROLANDO UNA EXCEPCION SEGUN SU TIPO

Dentro de un mismo bloque de código podemos controlar que tipo de excepción queremos controlar. Por ejemplo, si ejecutamos el código:

var
s: string;
i: Integer;
begin
s := 'prueba';
i := StrToInt( s );
end;

Mostrará el error:

'prueba' not is a valid integer value

Si queremos saber el tipo de excepción podemos sacarla por pantalla:

var
s: string;
i: Integer;
begin
s := 'prueba';

try
i := StrToInt( s );
except
on E: Exception do
Application.MessageBox( PChar( E.ClassName + ': ' + E.Message ), 'Error', MB_ICONSTOP );
end;
end;

Hemos incluido en el mensaje de la excepción la clase y el mensaje de error. Este sería el resultado:

EConvertError: 'prueba' not is a valid integer value

Así, mediante la propiedad ClassName de la clase Exception podemos averiguar la clase a la que pertenece la excepción. Ahora mediante la sentencia on podemos aislar la excepción de la forma:

on tipo do sentencia

En nuestro caso sería así:

try
i := StrToInt( s );
except
on E: EConvertError do
Application.MessageBox( 'Error de conversión', 'Error', MB_ICONSTOP )
else
Application.MessageBox( 'Error desconocido', 'Error', MB_ICONSTOP );
end;

Si se produjera una excepción que no fuese de la clase EConvertError mostraría el mensaje Error desconocido.

De este modo podemos aislar dentro de un mismo bloque de código los distintos tipos de excepción que se puedan producir.

Otro ejemplo podría ser la división de dos números enteros:

try
Resultado = b div c;
except
on EZeroDivide do Resultado := MAXINT;
on EIntOverflow do Resultado := 0;
on EIntUnderflow do Resultado := 0;
end;

Aquí hemos aislado cada uno de los casos que se puedan producir al dividir dos números enteros, alterando el resultado a nuestro antojo.

En el próximo artículo seguiremos viendo más características de las excepciones.

Pruebas realizadas en Delphi 7.

26 octubre 2007

Moviendo sprites con el teclado y el ratón

Basándome en el ejemplo del artículo anterior que mostraba como realizar el movimiento de sprites con fondo vamos a ver como el usuario puede mover los sprites usando el teclado y el ratón.

CAPTURANDO LOS EVENTOS DEL TECLADO

La clase TForm dispone de dos eventos para controlar las pulsaciones de teclado: OnKeyDown y OnKeyUp. Necesitamos ambos eventos porque no sólo me interesa saber cuando un usuario ha pulsado una tecla sino también cuando la ha soltado (para controlar las diagonales).

Para hacer esto voy a crear cuatro variables booleanas en la sección private el formulario que me van a informar de cuando están pulsadas las teclas del cursor:

type
private
{ Private declarations }
Sprite: TSprite;
Buffer, Fondo: TImage;
bDerecha, bIzquierda, bArriba, bAbajo: Boolean;

Estas variables las voy a actualizar en el evento OnKeyDown:

procedure TFormulario.FormKeyDown( Sender: TObject; var Key: Word; Shift: TShiftState );
begin
case key of
VK_LEFT: bIzquierda := True;
VK_DOWN: bAbajo := True;
VK_UP: bArriba := True;
VK_RIGHT: bDerecha := True;
end;
end;

y en el evento OnKeyUp:

procedure TFormulario.FormKeyUp( Sender: TObject; var Key: Word; Shift: TShiftState );
begin
case key of
VK_LEFT: bIzquierda := False;
VK_DOWN: bAbajo := False;
VK_UP: bArriba := False;
VK_RIGHT: bDerecha := False;
end;
end;

Al igual que hice con el ejemplo anterior voy a utilizar un temporizador (TTimer) llamado TmpTeclado con un intervalo que va a ser también de 10 milisegundos y cuyo evento OnTimer va a encargarse de dibujar el sprite en pantalla:

procedure TFormulario.TmpTecladoTimer( Sender: TObject );
begin
// ¿Ha pulsado la tecla izquierda?
if bIzquierda then
if Sprite.x > 0 then
Dec( Sprite.x );

// ¿Ha pulsado la tecla arriba?
if bArriba then
if Sprite.y > 0 then
Dec( Sprite.y );

// ¿Ha pulsado la tecla derecha?
if bDerecha then
if Sprite.x + Sprite.Imagen.Width < ClientWidth then
Inc( Sprite.x );

// ¿Ha pulsado la tecla abajo?
if bAbajo then
if Sprite.y + Sprite.Imagen.Height < ClientHeight then
Inc( Sprite.y );

DibujarSprite;
end;

Este evento comprueba la pulsación de todas las teclas controlando que el sprite no se salga del formulario. El procedimiento de DibujarSprite sería el siguiente:

procedure TFormulario.DibujarSprite;
var
Origen, Destino: TRect;
begin
// Copiamos el fondo de pantalla al buffer
Origen.Left := Sprite.x;
Origen.Top := Sprite.y;
Origen.Right := Sprite.x + Sprite.Imagen.Width;
Origen.Bottom := Sprite.y + Sprite.Imagen.Height;
Destino.Left := 0;
Destino.Top := 0;
Destino.Right := Sprite.Imagen.Width;
Destino.Bottom := Sprite.Imagen.Height;
Buffer.Canvas.CopyMode := cmSrcCopy;
Buffer.Canvas.CopyRect( Destino, Fondo.Canvas, Origen );

// Dibujamos el sprite en el buffer encima del fondo copiado
Sprite.Dibujar( 0, 0, Buffer.Canvas );

// Dibujamos el contenido del buffer a la pantalla
Canvas.Draw( Sprite.x, Sprite.y, Buffer.Picture.Graphic );
end;

Prácticamente es el mismo visto en el artículo anterior. Ya sólo hace falta poner el marcha el mecanismo que bien podría ser en el evento OnCreate del formulario:

begin
TmpTeclado.Enabled := True;
Sprite.x := 250;
Sprite.y := 150;
end;

CAPTURANDO LOS EVENTOS DEL RATON

Para capturar las coordenadas del ratón vamos a utilizar el evento OnMouseMove del formulario:

procedure TFormulario.FormMouseMove( Sender: TObject; Shift: TShiftState; X, Y: Integer );
begin
Sprite.x := X;
Sprite.y := Y;
end;

Para controlar los eventos del ratón voy a utilizar un temporizador distinto al del teclado llamado TmpRaton con un intervalo de 10 milisegundos. Su evento OnTimer sería sencillo:

procedure TFormulario.TmpRatonTimer( Sender: TObject );
begin
DibujarSprite;
end;

Aquí nos surge un problema importante: como los movimientos del ratón son más bruscos que los del teclado volvemos a tener el problema de que el sprite nos va dejando manchas en pantalla. Para solucionar el problema tenemos que restaurar el fondo de la posición anterior del sprite antes de dibujarlo en la nueva posición..

Para ello voy a guardar en la clase TSprite las coordenadas anteriores:

type
TSprite = class
public
x, y, xAnterior, yAnterior: Integer;
ColorTransparente: TColor;
Imagen, Mascara: TImage;
constructor Create;
destructor Destroy; override;
procedure Cargar( sImagen: string );
procedure Dibujar( x, y: Integer; Canvas: TCanvas );
end;

Al procedimiento DibujarSprite le vamos a añadir que restaure el fondo del sprite de la posición anterior:

procedure TFormulario.DibujarSprite;
var
Origen, Destino: TRect;
begin
// Restauramos el fondo de la posición anterior del sprite
if ( Sprite.xAnterior <> Sprite.x ) or ( Sprite.yAnterior <> Sprite.y ) then
begin
Origen.Left := Sprite.xAnterior;
Origen.Top := Sprite.yAnterior;
Origen.Right := Sprite.xAnterior + Sprite.Imagen.Width;
Origen.Bottom := Sprite.yAnterior + Sprite.Imagen.Height;
Destino := Origen;
Canvas.CopyMode := cmSrcCopy;
Canvas.CopyRect( Destino, Fondo.Canvas, Origen );
end;

// Copiamos el fondo de pantalla al buffer
Origen.Left := Sprite.x;
Origen.Top := Sprite.y;
Origen.Right := Sprite.x + Sprite.Imagen.Width;
Origen.Bottom := Sprite.y + Sprite.Imagen.Height;
Destino.Left := 0;
Destino.Top := 0;
Destino.Right := Sprite.Imagen.Width;
Destino.Bottom := Sprite.Imagen.Height;
Buffer.Canvas.CopyMode := cmSrcCopy;
Buffer.Canvas.CopyRect( Destino, Fondo.Canvas, Origen );

// Dibujamos el sprite en el buffer encima del fondo copiado
Sprite.Dibujar( 0, 0, Buffer.Canvas );

// Dibujamos el contenido del buffer a la pantalla
Canvas.Draw( Sprite.x, Sprite.y, Buffer.Picture.Graphic );

Sprite.xAnterior := Sprite.x;
Sprite.yAnterior := Sprite.y;
end;

Y finalmente activamos el temporizador que controla el ratón y ocultamos el cursor del ratón para que no se superponga encima de nuestro sprite:

begin
TmpRaton.Enabled := True;
Sprite.x := 250;
Sprite.y := 150;
ShowCursor( False );
end;

Al ejecutar el programa podeis ver como se mueve el sprite como si fuera el cursor del ratón.

Aunque se pueden hacer cosas bonitas utilizando el Canvas no os hagais muchas ilusiones ya que si por algo destaca la librería GDI de Windows (el Canvas) es por su lentitud y por la diferencia de velocidad entre ordenadores.

Para hacer cosas serías habría que irse a la librerías SDL (mi favorita), OpenGL o DirectX ( aunque hay decenas de motores gráficos 2D y 3D para Delphi en Internet que simplifican el trabajo).

Pruebas realizadas en Delphi 7.

25 octubre 2007

Mover sprites con doble buffer

En el artículo anterior creamos la clase TSprite encargada de dibujar figuras en pantalla. Hoy vamos a reutilizarla para mover sprites, pero antes vamos a hacer una pequeña modificación:

type
TSprite = class
public
x, y: Integer;
ColorTransparente: TColor;
Imagen, Mascara: TImage;
constructor Create;
destructor Destroy; override;
procedure Cargar( sImagen: string );
procedure Dibujar( x, y: Integer; Canvas: TCanvas );
end;

Sólo hemos modificado el evento Dibujar añadiendo las coordenadas de donde se va a dibujar (independientemente de las que tenga el sprite). La implementación de toda la clase TSprite quedaría de esta manera:

{ TSprite }

constructor TSprite.Create;
begin
inherited;
Imagen := TImage.Create( nil );
Imagen.AutoSize := True;
Mascara := TImage.Create( nil );
ColorTransparente := RGB( 255, 0, 255 );
end;

destructor TSprite.Destroy;
begin
Mascara.Free;
Imagen.Free;
inherited;
end;

procedure TSprite.Cargar( sImagen: string );
var
i, j: Integer;
begin
Imagen.Picture.LoadFromFile( sImagen );
Mascara.Width := Imagen.Width;
Mascara.Height := Imagen.Height;

for j := 0 to Imagen.Height - 1 do
for i := 0 to Imagen.Width - 1 do
if Imagen.Canvas.Pixels[i, j] = ColorTransparente then
begin
Imagen.Canvas.Pixels[i, j] := 0;
Mascara.Canvas.Pixels[i, j] := RGB( 255, 255, 255 );
end
else
Mascara.Canvas.Pixels[i, j] := RGB( 0, 0, 0 );
end;

procedure TSprite.Dibujar( x, y: Integer; Canvas: TCanvas );
begin
Canvas.CopyMode := cmSrcAnd;
Canvas.Draw( x, y, Mascara.Picture.Graphic );
Canvas.CopyMode := cmSrcPaint;
Canvas.Draw( x, y, Imagen.Picture.Graphic );
end;

CREANDO EL DOBLE BUFFER

Cuando se mueven figuras gráficas en un formulario aparte de producirse parpadeos en el sprite se van dejando rastros de las posiciones anteriores. Sucede algo como esto:


Para evitarlo hay muchísimas técnicas tales como el doble o triple buffer. Aquí vamos a ver como realizar un doble buffer para mover sprites. El formulario va a tener el siguiente fondo:


El fondo tiene unas dimensiones de 500x300 pixels. Para ajustar el fondo al formulario configuramos las siguientes propiedades en el inspector de objetos:

Formulario.ClientWidth = 500
Formulario.ClientHeight = 300

Se llama doble buffer porque vamos a crear dos imágenes:

Fondo, Buffer: TImage;

El Fondo guarda la imagen de fondo mostrada anteriormente y el Buffer va a encargarse de mezclar el sprite con el fondo antes de llevarlo a la pantalla.

Los pasos para dibujar el sprite serían los siguientes:


Se copia un trozo del fondo al buffer.

Se copia el sprite sin fondo encima del buffer.

Se lleva el contenido del buffer a pantalla.

Lo primero que vamos a hacer es declarar en la sección private del formulario los objetos:

private
{ Private declarations }
Sprite: TSprite;
Buffer, Fondo: TImage;

Después los creamos en el evento OnCreate del formulario:

procedure TFormulario.FormCreate( Sender: TObject );
begin
Sprite := TSprite.Create;
Sprite.Cargar( ExtractFilePath( Application.ExeName ) + 'sprite.bmp' );
Buffer := TImage.Create( nil );
Buffer.Width := Sprite.Imagen.Width;
Buffer.Height := Sprite.Imagen.Height;
Fondo := TImage.Create( nil );
Fondo.Picture.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'fondo.bmp' );
end;

El fondo también lo he creado como imagen BMP en vez de JPG para poder utilizar la función CopyRect del Canvas. Nos aseguramos de que en el evento OnDestroy del formulario se liberen de memoria:

procedure TFormulario.FormDestroy( Sender: TObject );
begin
Sprite.Free;
Buffer.Free;
Fondo.Free;
end;

Aunque podemos mover el sprite utilizando un bucle for esto podría dejar nuestro programa algo pillado. Lo mejor es moverlo utilizando un objeto de la clase TTimer. Lo introducimos en nuestro formulario con el nombre Temporizador. Por defecto hay que dejarlo desactivado (Enabled = False) y vamos a hacer que se mueva el sprite cada 10 milisegundos (Invertal = 10).

En el evento OnTimer hacemos que se mueva el sprite utilizando los pasos mencionados:

procedure TFSprites.TemporizadorTimer( Sender: TObject );
var
Origen, Destino: TRect;
begin
if Sprite.x < 400 then
begin
Inc( Sprite.x );

// Copiamos el fondo de pantalla al buffer
Origen.Left := Sprite.x;
Origen.Top := Sprite.y;
Origen.Right := Sprite.x + Sprite.Imagen.Width;
Origen.Bottom := Sprite.y + Sprite.Imagen.Height;
Destino.Left := 0;
Destino.Top := 0;
Destino.Right := Sprite.Imagen.Width;
Destino.Bottom := Sprite.Imagen.Height;
Buffer.Canvas.CopyMode := cmSrcCopy;
Buffer.Canvas.CopyRect( Destino, Fondo.Canvas, Origen );

// Dibujamos el sprite en el buffer encima del fondo copiado
Sprite.Dibujar( 0, 0, Buffer.Canvas );

// Dibujamos el contenido del buffer a la pantalla
Canvas.Draw( Sprite.x, Sprite.y, Buffer.Picture.Graphic );
end
else
Temporizador.Enabled := False;
end;

En el evento OnPaint del formulario tenemos que hacer que se dibuje el fondo:

procedure TFormulario.FormPaint( Sender: TObject );
begin
Canvas.Draw( 0, 0, Fondo.Picture.Graphic );
end;

Esto es necesario por si el usuario minimiza y vuelve a mostrar la aplicación, ya que sólo se refresca la zona por donde está moviéndose el sprite.

Por fin hemos conseguido el efecto deseado:



Pruebas realizadas en Delphi 7.

24 octubre 2007

Como dibujar sprites transparentes

Un Sprite es una figura gráfica móvil utilizada en los videojuegos de dos dimensiones. Por ejemplo, un videojuego de naves espaciales consta de los sprites de la nave, los meteoritos, los enemigos, etc., es decir, todo lo que sea móvil en pantalla y que no tenga que ver con los paisajes de fondo.

Anteriormente vimos como copiar imágenes de una superficie a otra utilizando los métodos Draw o CopyRect que se encuentran en la clase TCanvas. También se vió como modificar el modo de copiar mediante la propiedad CopyMode la cual permitia los valores cmSrcCopy, smMergePaint, etc.

El problema radica en que por mucho que nos empeñemos en dibujar una imagen transparente ningún tipo de copia funciona: o la hace muy transparente o se estropea el fondo.

DIBUJAR SPRITES MEDIANTE MASCARAS

Para dibujar sprites transparentes hay que tener dos imágenes: la original cuyo color de fondo le debemos dar alguno como común (como el negro) y la imagen de la máscara que es igual que la original pero como si fuera un negativo.

Supongamos que quiero dibujar este sprite (archivo BMP):


Es conveniente utilizar de color transparente un color de uso como común. En este caso he elegido el color rosa cuyos componentes RGB son (255,0,255). La máscara de esta imagen sería la siguiente:


Esta máscara no es necesario crearla en ningún programa de dibujo ya que la vamos a crear nosotros internamente. Vamos a encapsular la creación del sprite en la siguiente clase:

type
TSprite = class
public
x, y: Integer;
ColorTransparente: TColor;
Imagen, Mascara: TImage;
constructor Create;
destructor Destroy; override;
procedure Cargar( sImagen: string );
procedure Dibujar( Canvas: TCanvas );
end;

Esta clase consta de las coordenadas del sprite, el color que definimos como transparente y las imágenes a dibujar incluyendo su máscara. En el constructor de la clase creo dos objetos de la clase TImage en memoria:

constructor TSprite.Create;
begin
inherited;
Imagen := TImage.Create( nil );
Imagen.AutoSize := True;
Mascara := TImage.Create( nil );
ColorTransparente := RGB( 255, 0, 255 );
end;

También he definido el color rosa como color transparente. Como puede apreciarse he utilizado el procedimiento RGB que convierte los tres componentes del color al formato de TColor que los guarda al revés BGR. Así podemos darle a Delphi cualquier color utilizando estos tres componentes copiados de cualquier programa de dibujo.

En el destructor de la clase nos aseguramos de que se liberen de memoria ambos objetos:

destructor TSprite.Destroy;
begin
Mascara.Free;
Imagen.Free;
inherited;
end;

Ahora implementamos la función que carga de un archivo la imagen BMP y a continuación crea su máscara:

procedure TSprite.Cargar( sImagen: string );
var
i, j: Integer;
begin
Imagen.Picture.LoadFromFile( sImagen );
Mascara.Width := Imagen.Width;
Mascara.Height := Imagen.Height;

for j := 0 to Imagen.Height - 1 do
for i := 0 to Imagen.Width - 1 do
if Imagen.Canvas.Pixels[i,j] = ColorTransparente then
begin
Imagen.Canvas.Pixels[i,j] := 0;
Mascara.Canvas.Pixels[i,j] := RGB( 255, 255, 255 );
end
else
Mascara.Canvas.Pixels[i,j] := RGB( 0, 0, 0 );
end;

Aquí nos encargamos de dejar la máscara en negativo a partir de la imagen original.

Para dibujar sprites recomiendo utilizar archivos BMP en lugar de JPG debido a que este último tipo de imágenes pierden calidad y podrían afectar al resultado de la máscara, dando la sensación de que el sprite tiene manchas. Hay otras librerías para Delphi que permiten cargar imágenes PNG que son ideales para la creación de videojuegos, pero esto lo veremos en otra ocasión.

Una vez que ya tenemos nuestro sprite y nuestra máscara asociada al mismo podemos crear el procedimiento encargado de dibujarlo:

procedure TSprite.Dibujar( Canvas: TCanvas );
begin
Canvas.CopyMode := cmSrcAnd;
Canvas.Draw( x, y, Mascara.Picture.Graphic )
Canvas.CopyMode := cmSrcPaint;
Canvas.Draw( x, y, Imagen.Picture.Graphic );}
end;

El único parámetro que tiene es el Canvas sobre el que se va a dibujar el sprite. Primero utilizamos la máscara para limpiar el terreno y después dibujamos el sprite sin el fondo. Vamos a ver como utilizar nuestra clase para dibujar un sprite en el formulario:

procedure TFormulario.DibujarCoche;
var
Sprite: TSprite;
begin
Sprite := TSprite.Create;
Sprite.x := 100;
Sprite.y := 100;
Sprite.Cargar( ExtractFilePath( Application.ExeName ) + 'sprite.bmp' );
Sprite.Dibujar( Canvas );
Sprite.Free;
end;

Este sería el resultado en el formulario:


Si el sprite lo vamos a dibujar muchas veces no es necesario crearlo y destruirlo cada vez. Deberíamos crearlo en el evento OnCreate del formulario y en su evento OnDestroy liberarlo (Sprite.Free).

En el próximo artículo veremos como mover el sprite en pantalla utilizando una técnica de doble buffer para evitar parpadeos en el movimiento.

Pruebas realizadas en Delphi 7.

23 octubre 2007

Dibujando con la clase TCanvas (y III)

Vamos a terminar de ver lo más importante de las operaciones que se pueden realizar con el objeto Canvas.

DIBUJAR UN RECTANGULO SIN FONDO


El procedimiento FrameRect permite crear rectángulos sin fondo teniendo en cuenta el valor de la propiedad Brush:

procedure FrameRect( const Rect: TRect );

Veamos un ejemplo:

with Canvas do
begin
R.Left := 300;
R.Top := 250;
R.Right := 350;
R.Bottom := 300;
Brush.Color := clGreen;
FrameRect( R );
end;

Este sería el dibujo resultante:


COMO COPIAR IMAGENES DE UN BITMAP A OTRO

La manera más utilizada de copiar imágenes es mediante el método Draw:

procedure Draw( X, Y: Integer; Graphic: TGraphic );

Vamos a ver un ejemplo de como copiar la imagen de un objeto de la clase TImage a nuestro formulario utilizando el procedimiento Draw:

var
R: TRect;
begin
with Canvas do
begin
R.Left := 300;
R.Top := 250;
R.Right := 350;
R.Bottom := 300;
Brush.Color := clGreen;
FrameRect( R );
Draw( 100, 100, Imagen.Picture.Graphic );
end;
end;

Hemos supuesto que el objeto de la clase TImage se llama Imagen. Este sería el resultado:


La imagen copiada consta de cuatro iconos y un fondo de color blanco. Otra cosa que podemos hacer es modificar el tamaño de la imagen a copiar utilizando el procedimiento:

procedure StretchDraw( const Rect: TRect; Graphic: TGraphic );

El parámetro Rect determina las nuevas coordenadas así como en ancho y alto de la imagen a copiar. En el siguiente ejemplo voy a copiar la misma imagen pero voy a reducirla un tamaño de 100x100:

var
R: TRect;
begin
with Canvas do
begin
R.Left := 100;
R.Top := 100;
R.Right := 200;
R.Bottom := 200;
StretchDraw( R, Imagen.Picture.Graphic );
end;
end;

Quedaría de la siguiente manera:


También se pueden copiar imágenes utilizando el procedimiento:

procedure CopyRect( const Dest: TRect; Canvas: TCanvas; const Source: TRect );

cuyos parámetros son:

Dest: Coordenadas del rectángulo destino.
Canvas: Referencia al Canvas del origen a copiar.
Source: Coordenadas del rectángulo origen.

Para el ejemplo anterior voy a copiar la imagen en su tamaño original al formulario:

var
Origen, Destino: TRect;
begin
with Canvas do
begin
Origen.Left := 0;
Origen.Top := 0;
Origen.Right := Imagen.Picture.Width;
Origen.Bottom := Imagen.Picture.Height;
Destino.Left := 100;
Destino.Top := 100;
Destino.Right := 100 + Imagen.Width;
Destino.Bottom := 100 + Imagen.Height;
CopyRect( Destino, Imagen.Canvas, Origen );
end;
end;

Entonces, ¿que diferencia hay entre Draw o StretchDraw y CopyRect? La diferencia es que CopyRect sólo puede utilizarse cuando el objeto TImage ha cargado un bitmap (*.BMP) porque si es un JPG provoca una excepción. En cambio las funciones Draw o StretchDraw siempre funcionan independientemente del tipo de imagen que sea.

LOS MODOS DE COPIA DE UNA IMAGEN

En principio cuando se realiza la copia de una imagen a otra, la copia de los pixels es exacta. Pero si queremos modificar el modo de copiar entonces la clase TCanvas tiene de la propiedad CopyMode que establece que tipo de operación que se va a realizar. Estos son sus posibles valores:

cmBlackness: pinta la imagen destino de color negro, independientemente del origen.

cmDstInvert: Invierte los colores de la imagen según la imagen destino.

cmMergeCopy: mezcla la imagen origen y destino utilizando el operador binario AND.

cmMergePaint: mezcla la imagen origen y destino utilizando el operador binario OR.

cmNotSrcCopy: copia la imagen origen invertida a la imagen destino.

cmNotSrcErase: mezcla las imagenes origen y destino y después las invierte con el operador binario OR.

cmPatCopy: copia la imagen según el valor de la propiedad Brush.Style del Canvas.

cmPatInvert: copia la imagen invertida según el valor de la propiedad Brush.Style del Canvas.

cmPatPaint: combina las imágenes origen y destino utilizando la operación binaria OR para luego invertirlas.

cmSrcAnd: combina la imagen origen con la imagen destino utilizando el operador binario AND.

cmSrcCopy: realiza una copia exacta de la imagen origen a la imagen destino (por defecto).

cmSrcErase: invierte la imagen destino y copia el origen utilizando el operador binario AND.

cmSrcInvert: invierte las imágenes origen y destino utilizando el operador binario XOR.

cmSrcPaint: combina las imágenes destino y origen utilizando el operador binario AND.

cmWhiteness: pinta toda la imagen destino de color blanco ignorando la imagen origen.

Por ejemplo voy a crear un efecto fantasma con la imagen origen:

with Canvas do
begin
CopyMode := cmMergePaint;
Draw( 100, 100, Imagen.Picture.Graphic );
end;

El efecto sería el siguiente:


COMO ACCEDER A LOS PIXELS DE LA IMAGEN

Si no son suficientes las operaciones que podemos realizar en una imagen también podemos acceder directamente a los pixels de una imagen a traves de su propiedad Pixels:

property Pixels[ X, Y: Integer ]: TColor;

Esta propiedad nos sirve igualmente para leer y para escribir pixels en la imagen. El componente TColor es un tipo entero de 32 bits definido en la unidad Graphics del siguiente modo:

type
TColor = -$7FFFFFFF-1..$7FFFFFFF;

Dentro del mismo número entero están definidos los colores básicos RGB los
cuales son:

R = Red (Rojo)
G = Green (Verde)
B = Blue (Azul)

Combinando estos tres colores se puede crear cualquier otro color. Estos colores estan dentro del valor TColor del siguiento modo:

$00GGBBRR

Donde GG es el byte en hexadecimal que representa el color verde, BB es el color azul y RR es el color rojo. Aquí tenemos unos ejemplos de los números en hexadecimal:

Rojo := $000000FF;
Azul := $0000FF00;
Verde := $00FF0000;
Blanco := $00FFFFFF;
Negro := $00000000;

Para no complicarnos mucho la vida Delphi ya dispone de colores predeterminados tales como clRed (Rojo), clBlue (Azul), etc. Sabiendo esto imaginaos que deseo hacer un programa que convierta todos los colores blancos de la imagen en amarillo:

var
i, j: Integer;
begin
with Canvas do
for j := 0 to ClientHeight - 1 do
for i := 0 to ClientWidth - 1 do
if Pixels[i,j] = clWhite then
Pixels[i,j] := clYellow;
end;

Mediante un doble bucle recorro toda la imagen y compruebo si el pixel donde estoy es blanco para sustituirlo por el amarillo. Este sería el resultado:


Con este método se podemos realizar nuestros propios filtros o crear cualquier tipo de efecto.

Con esto finalizamos el apartado dedicado al objeto Canvas.

Pruebas realizadas en Delphi 7.

22 octubre 2007

Dibujando con la clase TCanvas (II)

Una vez que ya sabemos como dibujar las figuras básicas con el objeto Canvas pasemos ahora a ver algunas más complicadas.


DIBUJAR UN POLIGONO SOLO CON LINEAS


Para dibujar un polígono transparente utilizando líneas tenemos el procedimiento:

procedure Polyline( Points: array of TPoint );

Funciona exactamente igual que el procedimiento Polygon salvo que no cierra el polígono automáticamente, es decir, el último punto y el primero tiene que ser el mismo (para crear el polígono, aunque puede realizarse cualquier otra figura). Este procedimiento es equivalente a crear un trazado de líneas utilizando el procedimiento LineTo. Veamos un ejemplo:

with Canvas do
begin
Pen.Color := clBlack;
SetLength( Puntos, 4 );
Puntos[0].x := 200;
Puntos[0].y := 250;
Puntos[1].x := 250;
Puntos[1].y := 300;
Puntos[2].x := 150;
Puntos[2].y := 300;
Puntos[3].x := 200;
Puntos[3].y := 250;
PolyLine( Puntos );
end;

Este sería el resultado:


DIBUJAR POLIGONOS BEZIER

Los polígonos de Bezier se dibujan trazando curvas entre todos los puntos del polígono, es decir, son polígonos con las esquinas muy redondeadas. El procedimiento para dibujarlos es el siguiente:

with Canvas do
begin
Pen.Color := clBlack;
SetLength( Puntos, 4 );
Puntos[0].x := 80;
Puntos[0].y := 250;
Puntos[1].x := 150;
Puntos[1].y := 300;
Puntos[2].x := 10;
Puntos[2].y := 300;
Puntos[3].x := 80;
Puntos[3].y := 250;
PolyBezier( Puntos );
end;

Este sería el resultado:


Aunque he dado las mismas coordenadas que un triángulo, tiene las esquinas inferiores redondeadas.

ESCRIBIENDO TEXTO

Para escribir texto encima de una imagen con la clase TCanvas tenemos el método:

procedure TextOut( X, Y: Integer; const Text: string );

Este método toma como parámetros las coordenadas donde va a comenzar a escribirse el texto y el texto a escribir. Por ejemplo:

with Canvas do
begin
TextOut( 10, 10, 'Escribiendo texto mediante el Canvas del formulario' );
end;

Al no especificar fuente ni color quedaría del siguiente modo:


Cuando se escribe texto en un formulario la fuente, el color del texto y el color del fondo vienen predeterminados por las propiedades Brush y Font del Canvas. Supongamos que quiero escribir texto con fuente Tahoma, negrita, 10 y de color azul con fondo transparente:

with Canvas do
begin
Brush.Style := bsClear;
Font.Color := clBlue;
Font.Name := 'Tahoma';
Font.Size := 10;
TextOut( 200, 50, 'Otro texto de prueba' );
end;

Aquí tenemos nuestro texto personalizado:


Pero nos puede surgir un pequeño problema. ¿Como podemos averiguar lo que va a ocupar en pixels el texto que vamos a escribir? Pues para ello tenemos la siguiente función:

function TextWidth( const Text: string ): Integer;

Nos devuelve el ancho en pixels del texto que le pasamos como parámetro según como esté la propiedad Font antes de llamar a esta función. Y si queremos saber su altura entonces tenemos esta otra función:

function TextHeight( const Text: string ): Integer;

Lo que afecta a esta función principalmente es la propiedad Font.Size del Canvas. También tenemos esta otra función para obtener simultáneamente el ancho y alto del texto a escribir:

function TextExtent( const Text: string ): TSize;

Donde TSize es una estructura definida en la unidad Types del siguiente modo:

type
tagSIZE = packed record
cx: Longint;
cy: Longint;
end;
TSize = tagSIZE;

COMO RELLENAR LAS FIGURAS DE UN COLOR

Todos los programas de dibujo incorporan la función de rellenar con pintura una imagen hasta que encuentre bordes. Es como volcar un bote de pintura sobre las superficie hasta que choque con algo. El Canvas del formulario dispone del procedimiento:

procedure FloodFill( X, Y: Integer; Color: TColor; FillStyle: TFillStyle );

Los parámetros X, Y especifican donde se va a comenzar a pintar, el parámetro Color establece el color del borde donde va a chocar la pintura y FillStyle define el modo de pintar. Veamos un ejemplo de cómo pintar un triángulo de rojo con bordes negros:

with Canvas do
begin
Brush.Color := clRed;
FloodFill( 200, 270, clBlack, fsBorder );
end;

El último parámetro (FillStyle) se utiliza para decirle a la función si queremos que pinte hasta el borde de un color en concreto (clBlack y fsBorder) o si deseamos que dibuje toda la superficie de un color hasta que encuentre un color distinto. Por ejemplo:

with Canvas do
begin
Brush.Color := clRed;
FloodFill( 200, 270, clSilver, fsSurface );
end;

En este caso le he dicho que me pinte de rojo toda la superficie cuyo fondo es de color clSilver. Si encuentra un color que no sea clSilver entonces se para. Esa es la diferencia entre los valores fsBorder (un sólo borde) y fsSurface (cualquier borde pero la misma superficie). En ambos ejemplos he rellenado de color rojo el polígono creado al principio de este artículo:


En el próximo artículo terminaremos de ver las propiedades de la clase TCanvas.

Pruebas realizadas en Delphi 7.

19 octubre 2007

Dibujando con la clase TCanvas (I)

La unidad Graphics dispone de la clase TCanvas dedicada al dibujo de objetos sobre la superficie de un control visual. Los controles estándar de Windows tales como Edit o Listbox no requieren canvas, ya que son dibujados por el sistema operativo.


Un objeto Canvas proporciona propiedades, métodos y eventos para realizar tareas como pueden ser:

- Escribir texto especificando fuente, color y estilo.

- Copiar imágenes de una superficie a otra.

- Dibujar líneas, rectángulos y circulos con distintos patrones de color y estilo.

- Leer y modificar cada pixel de la imagen dibujada.

COMO DIBUJAR UN RECTANGULO

Antes de dibujar una figura se pueden seleccionar los colores del margen y de fondo. Supongamos que quiero hacer un rectángulo con un marco de 3 pixels de ancho y de color rojo. El fondo va a ser de color azul. Para ello nos vamos al evento OnPaint del formulario y escribimos lo siguiente:

procedure TFormulario.FormPaint( Sender: TObject );
begin
with Canvas do
begin
Pen.Color := clRed;
Pen.Width := 3;
Brush.Color := clBlue;
Rectangle( 10, 10, 110, 110 );
end;
end;

El resultado es el siguiente:


Hemos utilizado la propiedad Pen para seleccionar el color del marco y su ancho. Con la propiedad Brush establecemos el color de fondo del rectángulo. Y mediante el procedimiento Rectangle hemos dibujado un rectángulo en la posición x = 10 e y = 10 teniendo un ancho y alto de 100x100. El procedimiento Rectangle tiene los siguientes parámetros:

procedure Rectangle( X1, Y1, X2, Y2: Integer );
procedure Rectangle( const Rect: TRect );

X1, Y1 -> Son las coordenadas de la esquina superior izquierda del rectángulo
X2, Y2 -> Son las coordenadas de la esquina inferior derecha del rectángulo

También se podría haber utilizado la estructura de datos TRect (rectángulo) para dibujar el rectángulo del siguiente modo:

var
R: TRect;
begin
with Canvas do
begin
Pen.Color := clRed;
Pen.Width := 3;
Brush.Color := clBlue;
R.Left := 10;
R.Top := 10;
R.Right := 110;
R.Bottom := 110;
Rectangle( R );
end;
end;

La estructura TRect esta definida dentro de la unidad Type del siguiente modo:

TRect = packed record
case Integer of
0: (Left, Top, Right, Bottom: Integer);
1: (TopLeft, BottomRight: TPoint);
end;

donde los valores Left y Top determinan la posición superior izquierda del rectángulo y los valores Right y Bottom la esquina inferior derecha.

También existe otro procedimiento para hacer rectángulos sólo con fondo (sin borde) utilizando el procedimiento:

procedure FillRect( const Rect: TRect );

Si lo hubiésemos utilizado este procedimiento sólo tendríamos un rectángulo con fondo azul. Lo único que tiene en cuenta es la propiedad Brush ignorando el valor de Pen.

Para el caso anterior no es necesario utilizar la estructura TRect, pero si se van a dibujar muchos rectángulos utilizando las mismas rutinas si es interesante utilizarla, evitándonos el tener que declarar las variables x1, y1, x2, y2 para las coordenadas. La estructura TRect es de uso general y no tiene porque estar asociada sólo al dibujo de rectángulos como veremos más adelante.

Las propiedades Pen y Brush son permantentes, es decir, que mientras no se modifiquen todo lo que se dibuje posteriormente tendrá los colores y estilo especificado por ambas. Si las figuras que se van a dibujar tienen mismo color de borde y fondo no es necesario especificar de nuevo el valor de Pen y Brush.

DIBUJANDO UN RECTANGULO CON ESQUINAS REDONDEADAS

Mediante el método RoundRect se pueden crear rectángulos con esquinas suavizadas. Veamos un ejemplo:

with Canvas do
begin
Pen.Color := clGray;
Pen.Width := 3;
Brush.Color := clWhite;
RoundRect( 300, 150, 380, 200, 30, 30 );
end;

Cuyo resultado sería:

El procedimiento RoundRect tiene los siguientes parámetros:

procedure RoundRect( X1, Y1, X2, Y2, X3, Y3: Integer );

donde:

X1, Y1 -> Son las coordenadas de la esquina superior izquierda
X2, Y2 -> Son las coordenadas de la esquina inferior derecha
X3, Y3 -> Es grado de redondeo de las esquinas (cuanto más grande más redondeado)

COMO DIBUJAR UN CIRCULO

Para dibujar un círculo se utiliza el procedimiento Ellipse. Vamos a dibujar un círculo con un borde de color azul oscuro y un fondo de color amarillo:

with Canvas do
begin
Pen.Color := clNavy;
Pen.Width := 5;
Brush.Color := clYellow;
Brush.Style := bsDiagCross;
Ellipse( 160, 10, 260, 110 );
end;

Quedaría así:


A la propiedad Brush le he especificado que me dibuje el fondo amarillo utilizando un patrón de líneas cruzadas diagonales. Al igual que el procedimiento Rectangle, el procedimiento Ellipse utiliza los mismos parámetros:

procedure Ellipse( X1, Y1, X2, Y2: Integer );
procedure Ellipse( const Rect: TRect );

En este otro ejemplo voy a dibujar un círculo chafado horizontalmente (una elipse) con un borde de 1 pixel de color blanco y un fondo gris:

with Canvas do
begin
Pen.Color := clWhite;
Pen.Width := 1;
Brush.Color := clGray;
Brush.Style := bsSolid;
Ellipse( 300, 10, 330, 110 );
end;

Este sería el resultado:


He tenido que volver a dejar la propiedad Style del Brush con el valor bsSolid para que vuelva a dibujarme el fondo sólido porque sino me lo hubiera hecho con líneas cruzadas como lo dejé anteriormente.

COMO DIBUJAR LINEAS

La clase TCanvas dispone de un puntero invisible donde dibujará las líneas si no se especifica posición. Para modificar la posición de dicho puntero existe el método MoveTo:

procedure MoveTo( X, Y: Integer );

Si queremos averiguar donde se ha quedado el puntero disponemos de la propiedad PenPos que es de tipo TPoint:

type TPoint = packed record
X: Longint;
Y: Longint;
end;

Al igual que la estructura de datos TRect está definida en la unidad Types. Veamos como realizar un triángulo de color negro y sin fondo:

with Canvas do
begin
Pen.Color := clBlack;
Pen.Width := 3;
MoveTo( 50, 150 );
LineTo( 100, 220 );
LineTo( 10, 220 );
LineTo( 50, 150 );
end;

El triángulo quedaría así:

Hemos situado el puntero en un punto y a partir de ahí hemos ido tranzando líneas hasta cerrar el triángulo. En este caso la propiedad Brush no tiene ningún valor para el procedimiento LineTo.

COMO DIBUJAR POLIGONOS

El triángulo que hemos hecho anteriormente también podría haberse realizado mediante un polígono. Además los polígonos permiten especificar el color de fondo mediante la propiedad Brush. Por ejemplo vamos a crear un triángulo con un borde amarillo y fondo de color verde:

var
Puntos: array of TPoint;
begin
with Canvas do
begin
Pen.Color := clYellow;
Pen.Width := 3;
Brush.Color := clGreen;
SetLength( Puntos, 3 );
Puntos[0].x := 150;
Puntos[0].y := 150;
Puntos[1].x := 250;
Puntos[1].y := 200;
Puntos[2].x := 150;
Puntos[2].y := 200;
Polygon( Puntos );
end;
end;

Quedaría así:


He creado un array dinámico del tipo TPoint con una longitud de 3. Después he ido especificando cada punto del polígono (en este caso un triángulo) y por último le paso dicho array al procedimiento Polygon.

En el próximo artículo seguiremos exprimiendo las características del Canvas.

Pruebas realizadas en Delphi 7.

18 octubre 2007

Implementando interfaces en Delphi (y III)

Una de las propiedades más interesantes que incorporan las interfaces es la de añadir un identificador único que la diferencie del resto.

IDENTIFICACION DE INTERFACES


Una interfaz puede tener un identificador unico a nivel global llamado GUID. Este identificador tiene la siguiente forma:

['{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}']

donde cada X es un dígito hexadecimal. Cada GUID es un valor binario de 16 bytes que hace que cada interfaz sea única. Los tipos TGUID y PGUID están declarados dentro de la unidad System del siguiente modo:

type
PGUID = ^TGUID;
TGUID = record
D1: Integer;
D2: Word;
D3: Word;
D4: array[0..7] of Byte;

Este sería un ejemplo de declaración de interfaz con GUID:

type
IFactura = interface
['{78EAC667-ED60-443B-B84B-5D7AF161B28B}']
procedure Calcular;
end;

¿De donde sacamos ese código? Pues cuando en el editor de Delphi se pulsa la combinación de teclas CTRL + MAYÚSCULAS + G entonces nos genera una GUID aleatoria.

CREANDO PROPIEDADES EN LA INTERFAZ

Pese que la declarión de interfaces es similar a la de las clases hay que tener varias cosas presentes:

- No permiten declarar variables internas.

- No pueden ser instanciadas.

- No se permite especificar la visibilidad de los métodos (private, protected o public).

- Las propiedades no pueden apuntar a variables sino sólo a métodos.

Entonces, ¿como podemos definir propiedades? Hay que definirlas utilizando métodos en vez de variables cuando se definen los parámetros read y write de la propiedad. Sería de este modo:

type
IAlbaran = interface
function Get_Numero: Integer;
function Get_Importe: Real;
procedure Set_Numero( iNumero: Integer );
procedure Set_Importe( rImporte: Real );
procedure Imprimir;

property Numero: Integer read Get_Numero write Set_Numero;
property Importe: Real read Get_Importe write Set_Importe;
end;

y su implementación a través de la clase:

TAlbaran = class( TInterfacedObject, IAlbaran )
private
iNumero: Integer;
rImporte: Real;

public
function Get_Numero: Integer;
function Get_Importe: Real;
procedure Set_Numero( iNumero: Integer );
procedure Set_Importe( rImporte: Real );
procedure Imprimir;

property Numero: Integer read Get_Numero write Set_Numero;
property Importe: Real read Get_Importe write Set_Importe;
end;

implementation

{ TAlbaran }

function TAlbaran.Get_Importe: Real;
begin
Result := rImporte;
end;

function TAlbaran.Get_Numero: Integer;
begin
Result := iNumero;
end;

procedure TAlbaran.Imprimir;
begin
ShowMessage( 'Imprimiendo...' );
end;

procedure TAlbaran.Set_Importe( rImporte: Real );
begin
Self.rImporte := rImporte;
end;

procedure TAlbaran.Set_Numero( iNumero: Integer );
begin
Self.iNumero := iNumero;
end;

Esto nos permite utilizar nuestra interfaz sin llamar a los procedimientos Set y Get de cada campo:

var
Albaran: IAlbaran;
begin
Albaran := TAlbaran.Create;
Albaran.Numero := 1;
Albaran.Importe := 68.21;

ShowMessage( 'Numero=' + IntToStr( Albaran.Numero ) );
ShowMessage( 'Importe=' + FloatToStr( Albaran.Importe ) );
end;

Con esta comodidad se pueden instanciar clases a partir de interfaces manteniendo la claridad de código tanto dentro como fuera de la implementación.

Con esto finalizamos la parte básica de implementación de interfaces en Delphi.

Pruebas realizadas en Delphi 7.

17 octubre 2007

Implementando interfaces en Delphi (II)

Cuando creamos clases para nuestros programas de gestión uno se pregunta que ventajas pueden aportar las interfaces dentro de nuestro programa. Pues una de las ventajas es que encapsula aún más nuestra clase dejando sólo accesible los métodos y no las variables internas (ya sean private, protected o public). Por ejemplo, supongamos que deseo implementar una interfaz y una clase para el control de mis clientes:

type
ICliente = interface
function Get_ID: Integer;
function Get_Nombre: string;
function Get_NIF: string;
function Get_Saldo: Real;
procedure Set_ID( ID: Integer );
procedure Set_Nombre( sNombre: string );
procedure Set_NIF( sNIF: string );
procedure Set_Saldo( rSaldo: Real );
end;

TCliente = class( TInterfacedObject, ICliente )
private
ID: Integer;
sNombre, sNIF: string;
rSaldo: Real;
public
function Get_ID: Integer;
function Get_Nombre: string;
function Get_NIF: string;
function Get_Saldo: Real;
procedure Set_ID( ID: Integer );
procedure Set_Nombre( sNombre: string );
procedure Set_NIF( sNIF: string );
procedure Set_Saldo( rSaldo: Real );
end;

Esta clase se puede instanciar de dos formas. Si se hace en una variable de clase:

var
Cliente: TCliente;
begin
Cliente := TCliente.Create;
Cliente.Set_ID( 1 );
Cliente.Set_Nombre( 'CARLOS MARTINEZ RUIZ' );
Cliente.Set_NIF( '67876453F' );
Cliente.Set_Saldo( 145.87 );
end;

entonces se comporta como una clase normal, permitiendo acceder incluso a las variables privadas cuando en el editor de Delphi ponemos:

Cliente.

y esperamos a que el asistente nos muestre las propiedades y métodos de la clase. Y no sólo eso, sino que además nos muestra todas propiedades y métodos que hemos heredado de TObject haciendo más engorrosa la búsqueda de variables y métodos.

Pero si implementamos la clase a partir de la interfaz:

var
Cliente: ICliente;
begin
Cliente := TCliente.Create;
Cliente.Set_ID( 1 );
Cliente.Set_Nombre( 'CARLOS MARTINEZ RUIZ' );
Cliente.Set_NIF( '67876453F' );
Cliente.Set_Saldo( 145.87 );
end;

al teclear en el editor de Delphi:

Cliente.

no sólo hemos eliminado las variables privadas de la lista, sino que además sólo muestra nuestros métodos y los de las interfaz (QueryInterface, _AddRef y _Release). Esto aporta mucha mayor claridad al programador a la hora de instanciar sus clases, sobre todo cuando tenemos que distribuir nuestra librería a otros programadores (ellos sólo tienen que fijarse como se utiliza la interfaz, ni siquiera tienen porque saber como está implementada la clase).

La única desventaja es que hay que crear tantas funciones y procedimientos Set y Get como propiedades tenga nuestra clase. Pero es bueno acostumbrarse a hacerlo así como ocurre con los Beans de Java.

USANDO INTERFACES COMO PARAMETROS EN PROCEDIMIENTOS

Utilizando el polimorfismo mediante interfaces ahora podemos crear procedimientos genéricos que manejen objetos de distinta clase de una manera simple. Usando las interfaces IArticulo e IVehiculo definidas en artículo anterior podemos escribir los siguientes procedimientos:

procedure FacturarArticulos( Articulos: array of IArticulo );
var
i: Integer;
begin
for i := Low( Articulos ) to High( Articulos ) do
Articulos[i].Facturar;
end;

procedure MatricularVehiculos( Vehiculos: array of IVehiculo );
var
i: Integer;
begin
for i := Low( Vehiculos ) to High( Vehiculos ) do
Vehiculos[i].Matricular;
end;

El procedimiento FacturarArticulos no tiene porque saber como se factura internamente cada artículo. Lo mismo ocurre con el procedimiento MatricularVehiculos, ya sea de la clase TCoche o TCamion ya se encargará internamente el objeto de llamar a su método correspondiente, abstrayéndonos a nosotros de como funciona internamente cada clase.

LA INTERFAZ IINTERFACE

Al igual que todos los objetos heredan directa o indirectamente de TObject, todas las interfaces heredan de la interfaz IInterface. Esta interfaz incorpora el método QueryInterface el cual es muy útil para descubrir y usar otras interfaces que implementan el mismo objeto.

Para contar el número de referencias introduce también los métodos _AddRef y _Release. El compilador de Delphi automáticamente proporciona llamadas a estos métodos cuando las interfaces son utilizadas. Para no tener que implementar nosotros a mano estos métodos (ya que una interfaz nos obliga a implementar todos sus métodos), para ello heredamos de la clase TInterfaceObject que proporciona una implementación base para interfaces. Heredar de TInterfaceObject no es obligatorio pero muy útil.

La clase TInterfacedObject está declarada en la unidad System de la siguiente manera:

type
TInterfacedObject = class ( TObject, IInterface )
protected
FRefCount: Integer;
function QueryInterface( const IID: TGUID; out Obj ): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
class function NewInstance: TObject; override;
property RefCount: Integer; read FRefCount;
end;

Por ello en los ejemplos que he mostrado heredo de esa clase aparte de la interfaz:

type
TCliente = class( TInterfacedObject, ICliente )
...

Como puede apreciarse la clase TInterfacedObject hereda de la clase TObject y de la interfaz IUnknown. La interfaz IUnknown es utilizada para crear objetos COM.

En el próximo artículo terminaremos de ver las características más importantes de las interfaces.

Pruebas realizadas en Delphi 7.

15 octubre 2007

Implementando interfaces en Delphi (I)

Delphi es un lenguaje que utiliza la herencia simple al contrario de C++ que permite herencia múltiple. Esto significa que cualquier clase sólo puede heredar de una clase padre. Por lo tanto, si queremos que una clase herede métodos de más de una clase entonces hay que utilizar interfaces (interface).

Una interfaz es como una clase que contiene sólo métodos abstractos (métodos sin implentación) definiendo limpiamente su funcionalidad. Por convención, los nombres de las interfaces comienzan con la letra mayúscula I.

Veamos un ejemplo de definición de interfaz para un artículo:

type
IArticulo = interface
procedure IncrementarExistencias( rCantidad: Real );
procedure DecrementarExistencias( rCantidad: Real );
function Facturar: Integer;
end;

Las interfaces nunca pueden ser instanciadas. Es decir, no se puede hacer:

var
Articulo: IArticulo;
begin
Articulo := IArticulo.Create;
end;

Al compilar nos daría el error:

Object or class typed required (se requiere una clase u objeto)

Para utilizar una interfaz necesitamos implementarla a través de una clase. Se haría de la siguiente manera:

type
TArticulo = class( TInterfacedObject, IArticulo )
procedure IncrementarExistencias( rCantidad: Real );
procedure DecrementarExistencias( rCantidad: Real );
function Facturar: Integer;

¿Que ventajas aporta esto respecto a una clase abstracta? Pues en este caso la definición es más limpia, nada más. Ahora veremos que ventaja de utilizar interfaces.

UTILIZANDO EL POLIMORFIRMO PARA LA CREACION DE CLASES

Las clases polimórficas son aquellas clases que teniendo una definición común se pueden utilizar para distintos tipos de objeto. Supongamos que tenemos la siguiente interfaz:

IVehiculo = interface
procedure Matricular;
end;

Utilizando la misma interfaz vamos a definir una clase para cada tipo de vehículo:

type
TCoche = class( TInterfacedObject, IVehiculo )
procedure Matricular;
end;

TCamion = class( TInterfacedObject, IVehiculo )
procedure Matricular;
end;

Ahora instanciamos ambas clases utilizando la misma interfaz:

var
Vehiculo: IVehiculo;
begin
Vehiculo := TCoche.Create;
Vehiculo.Matricular;
Vehiculo := TCamion.Create;
Vehiculo.Matricular;
end;

En este caso todas las clases que implementan la interfaz IVehiculo tienen un método común pero podría darse el caso de que tuvieran métodos distintos, lo cual significa que hay que utilizar la herencia múltiple.

UTILIZANDO HERENCIA MULTIPLE MEDIANTE INTERFACES

Supongamos que tenemos estas dos interfaces:

type
IVehiculo = interface
procedure Matricular;
end;

IRemolque = interface
procedure Cargar;
end;

El vehículo tiene el método Matricular y el remolque tiene el método Cargar. A mi me interesa que un coche utilice el método Matricular, pero sólo el camión tiene que poder Cargar. Entonces la implementación de las clases de haría así:

TCoche = class( TInterfacedObject, IVehiculo )
procedure Matricular;
end;

TCamion = class( TInterfacedObject, IVehiculo, IRemolque )
procedure Matricular;
procedure Cargar;
end;

Como puede apreciarse la clase TCoche sólo implementa la interfaz IVehiculo pero la clase TCamion hereda implementa simultáneamente las interfaces IVehiculo y IRemolque pudiendo utilizar sus métodos indistintamente. Esto es lo que se conoce como herencia múltiple.

En el próximo artículo seguiremos viendo más características sobre las interfaces.

Pruebas realizadas en Delphi 7.

12 octubre 2007

Trabajando con documentos XML (y III)

Aunque es posible trabajar con un documento XML usando solamente el componente XMLDocument y la interfaz IXMLNode es mucho más fácil utilizar el asistente XML Data Binding.

Dicho asistente toma los datos del esquema de un archivo XML a través de su archivo asociado DTD y realiza una nueva unidad con una interfaz que maneja nuestro documento XML sin tener que navegar por los nodos.

CREANDO UNA INTERFAZ PARA CLIENTES.XML

Para crear una unidad nueva para nuestro documento clientes.xml hay que hacer lo siguiente:

1. Seleccionamos en Delphi la opción File -> New -> Other...

2. Estando situados en la pestaña New seleccionamos el icono XML Data Binding y pulsamos OK.


3. En la ventana siguiente seleccionamos el archivo cli.dtd y pulsamos Next.


4. Después nos aparece otra ventana donde se nos permite modificar el nombre de la clase a crear así como los nombres que van tener las propiedades y métodos de la nueva clase. Lo dejamos como está y pulsamos Next.



5. La última pantalla del asistente nos muestra como va a ser definitivamente la interfaz que va a crear. Sólo nos queda pulsar Finish.



Va a generar la siguiente unidad:

{**************************************************************************}
{ }
{ XML Data Binding }
{ }
{ Generated on: 11/10/2007 11:15:57 }
{ Generated from: D:\Desarrollo\DelphiAlLimite\XML\cli.dtd }
{ Settings stored in: D:\Desarrollo\DelphiAlLimite\XML\cli.xdb }
{ }
{**************************************************************************}

unit UInterfazClientes;

interface

uses xmldom, XMLDoc, XMLIntf;

type

{ Forward Decls }

IXMLClientesType = interface;
IXMLClienteType = interface;

{ IXMLClientesType }

IXMLClientesType = interface(IXMLNodeCollection)
['{A21F5A80-EBA7-48F5-BFE1-EB9F390DFBBD}']
{ Property Accessors }
function Get_Cliente(Index: Integer): IXMLClienteType;
{ Methods & Properties }
function Add: IXMLClienteType;
function Insert(const Index: Integer): IXMLClienteType;
property Cliente[Index: Integer]: IXMLClienteType read Get_Cliente; default;
end;

{ IXMLClienteType }

IXMLClienteType = interface(IXMLNode)
['{624B6C2E-4E94-4CE0-A8D4-FE719AA7D975}']
{ Property Accessors }
function Get_Nombre: WideString;
function Get_Nif: WideString;
function Get_Saldopte: WideString;
function Get_Diaspago: WideString;
procedure Set_Nombre(Value: WideString);
procedure Set_Nif(Value: WideString);
procedure Set_Saldopte(Value: WideString);
procedure Set_Diaspago(Value: WideString);
{ Methods & Properties }
property Nombre: WideString read Get_Nombre write Set_Nombre;
property Nif: WideString read Get_Nif write Set_Nif;
property Saldopte: WideString read Get_Saldopte write Set_Saldopte;
property Diaspago: WideString read Get_Diaspago write Set_Diaspago;
end;

{ Forward Decls }

TXMLClientesType = class;
TXMLClienteType = class;

{ TXMLClientesType }

TXMLClientesType = class(TXMLNodeCollection, IXMLClientesType)
protected
{ IXMLClientesType }
function Get_Cliente(Index: Integer): IXMLClienteType;
function Add: IXMLClienteType;
function Insert(const Index: Integer): IXMLClienteType;
public
procedure AfterConstruction; override;
end;

{ TXMLClienteType }

TXMLClienteType = class(TXMLNode, IXMLClienteType)
protected
{ IXMLClienteType }
function Get_Nombre: WideString;
function Get_Nif: WideString;
function Get_Saldopte: WideString;
function Get_Diaspago: WideString;
procedure Set_Nombre(Value: WideString);
procedure Set_Nif(Value: WideString);
procedure Set_Saldopte(Value: WideString);
procedure Set_Diaspago(Value: WideString);
end;

{ Global Functions }

function GetClientes(Doc: IXMLDocument): IXMLClientesType;
function LoadClientes(const FileName: WideString): IXMLClientesType;
function NewClientes: IXMLClientesType;

const
TargetNamespace = '';

implementation

{ Global Functions }

function GetClientes(Doc: IXMLDocument): IXMLClientesType;
begin
Result := Doc.GetDocBinding('Clientes', TXMLClientesType, TargetNamespace) as IXMLClientesType;
end;

function LoadClientes(const FileName: WideString): IXMLClientesType;
begin
Result := LoadXMLDocument(FileName).GetDocBinding('Clientes', TXMLClientesType, TargetNamespace) as IXMLClientesType;
end;

function NewClientes: IXMLClientesType;
begin
Result := NewXMLDocument.GetDocBinding('Clientes', TXMLClientesType, TargetNamespace) as IXMLClientesType;
end;

{ TXMLClientesType }

procedure TXMLClientesType.AfterConstruction;
begin
RegisterChildNode('Cliente', TXMLClienteType);
ItemTag := 'Cliente';
ItemInterface := IXMLClienteType;
inherited;
end;

function TXMLClientesType.Get_Cliente(Index: Integer): IXMLClienteType;
begin
Result := List[Index] as IXMLClienteType;
end;

function TXMLClientesType.Add: IXMLClienteType;
begin
Result := AddItem(-1) as IXMLClienteType;
end;

function TXMLClientesType.Insert(const Index: Integer): IXMLClienteType;
begin
Result := AddItem(Index) as IXMLClienteType;
end;

{ TXMLClienteType }

function TXMLClienteType.Get_Nombre: WideString;
begin
Result := ChildNodes['nombre'].Text;
end;

procedure TXMLClienteType.Set_Nombre(Value: WideString);
begin
ChildNodes['nombre'].NodeValue := Value;
end;

function TXMLClienteType.Get_Nif: WideString;
begin
Result := ChildNodes['nif'].Text;
end;

procedure TXMLClienteType.Set_Nif(Value: WideString);
begin
ChildNodes['nif'].NodeValue := Value;
end;

function TXMLClienteType.Get_Saldopte: WideString;
begin
Result := ChildNodes['saldopte'].Text;
end;

procedure TXMLClienteType.Set_Saldopte(Value: WideString);
begin
ChildNodes['saldopte'].NodeValue := Value;
end;

function TXMLClienteType.Get_Diaspago: WideString;
begin
Result := ChildNodes['diaspago'].Text;
end;

procedure TXMLClienteType.Set_Diaspago(Value: WideString);
begin
ChildNodes['diaspago'].NodeValue := Value;
end;

end.

UTILIZANDO LA NUEVA INTERFAZ PARA CREAR DOCUMENTOS XML

La unidad creada por el asistente la he guardado con el nombre UInterfazClientes.pas lo que significa que hay que introducirla en la sección uses del formulario donde vayamos a utilizarla.

Vamos a ver como damos de alta un cliente utilizando dicha interfaz:

var
Clientes: IXMLClientesType;
begin
XML.FileName := ExtractFilePath( Application.ExeName ) + 'clientes.xml';
XML.Active := True;
Clientes := GetClientes( XML );

with Clientes.Add do
begin
Attributes['id'] := '3';
Nombre := 'FERNANDO RUIZ BERNAL';
Nif := '58965478T';
Saldopte := '150.66';
Diaspago := '45';
end;

XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes5.xml' );
end;

He cargado el documento XML como siempre y utilizando la interfaz IXMLClientesType cargo la lista de clientes del documento clientes.xml. Después doy de alta un cliente y grabo el documento como clientes5.xml.

Una cosa cosa interesante que se puede hacer es activar la propiedad doAutoSave que está dentro de la propiedad Options del componente XMLDocument lo que implica que el documento se guardará automáticamente conforme vayamos modificando los elementos del documento XML.

La ventaja de convertir nuestro documento XML en una interfaz persistente es que ahora podemos manejar la lista de clientes como si fuera una base de datos utilizando los métodos get y set de cada uno de los campos así como las rutinas para alta, baja o modificación de elementos dentro del documento. Sólo es cuestión de echarle una mirada a la nueva unidad creada para ver las posibiliades de la misma.

Pruebas realizadas en Delphi 7.

Publicidad