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.

Publicidad