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.

11 octubre 2007

Trabajando con documentos XML (II)

Anteriormente vimos que cada documento XML podía llevar asociado un documento DTD. Los documentos DTD (Document Type Definition) definen la estructura y la sintaxis de un documento XML. Su misión es mantener la consistencia entre los elementos del documento XML para que todos los documentos de la misma clase mantengan un mismo estilo.

Nuestro documento cli.dtd va a ser el siguiente:

Veamos que hace cada línea:

!ELEMENT Clientes (Cliente*)

Aquí establecemos que el documento XML se va a llamar Clientes y que cada elemento se llamará Cliente. El asterístico significa que va a tener cero o más elementos de tipo Cliente. Si quisieramos que el nodo Cliente apareciese una o más veces utilizaríamos el caracter +.

!ELEMENT Cliente (nombre, nif, saldopte?, diaspago?)

La segunda línea nos dice de que campos está compuesto el nodo Cliente: nombre, nif, saldopte y diaspago. Todos los campos que no lleven el signo de interrogación son obligatorios (en este caso nombre y nif).

!ELEMENT nombre (#PCDATA)
!ELEMENT nif (#PCDATA)
!ELEMENT saldopte (#PCDATA)
!ELEMENT diaspago (#PCDATA)

Por último se describe el tipo de dato que van a contener los nodos finales. El atributo #PCDATA significa que puede contener cualquier valor.

Con este archivo al lado de nuestros documentos XML podemos hacer doble clic sobre los mismos para que se vean en cualquier navegador.

NAVEGANDO POR LOS NODOS DEL DOCUMENTO XML

Una vez que el documento ha sido analizado sintácticamente por la implementación DOM, los datos representados estarán disponibles en la jerarquía de nodos. Cada nodo corresponde a un elemento etiqueta del documento. Por ejemplo, para el documento de clientes.xml que hemos visto anteriormente tendríamos la siguiente jerarquía: El nodo raiz es Clientes que a su vez tiene nodos hijos con el nombre Cliente. Cada uno de esos nodos hijos tienen a su vez otros nodos hijos tales como (nombre, nif, importepte y diaspago). Esos nodos hijos actuan como los nodos finales de un arból ya no tienen más nodos hijos.

Para acceder a cada nodo se utiliza la interfaz IXMLNode, comenzando por el nodo raiz a través de la propiedad DocumentElement del componente XMLDocument.

LEYENDO EL VALOR DE LOS NODOS

Vamos a ver un ejemplo de cómo leer el saldo pentiende (saldopte) del primer cliente y mostrarlo por pantalla:

var
Cliente: IXMLNode;
begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
Cliente := XML.DocumentElement.ChildNodes[0];
ShowMessage( Cliente.ChildNodes['saldopte'].Text );
end;

Suponemos que el documento clientes.xml está en el mismo directorio que nuestro programa. Como se puede apreciar en el código hemos abierto el documento XML, lo hemos activado y a continuación hemos recogido el nodo del primer cliente dentro de la variable Cliente que es de tipo IXMLNode. Después hemos accedido a su nodo hijo llamado saldopte para leer su valor.

MODIFICANDO EL VALOR DE LOS NODOS

Utilizando el mismo sistema podemos modificar el valor de cada nodo. En el siguiente ejemplo voy a modificar el nombre del segundo cliente (ROSA MARTINEZ GUILLEN) por el de MARIA PEREZ ROJO:

var
Cliente: IXMLNode;
begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
Cliente := XML.DocumentElement.ChildNodes[1];
Cliente.ChildNodes['nombre'].Text := 'MARIA PEREZ ROJO';
XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes2.xml' );
end;

Una vez modificado el nombre grabo todo el documento XML en otro archivo llamado clientes2.xml (aunque podía haber utilizado el mismo).

AÑADIENDO Y BORRANDO NODOS

Mediante el método AddChild podemos crear nuevos nodos hijos dentro de un documento XML. Veamos como dar de alta un nuevo cliente y guardar el archivo como clientes3.xml:

var
Cliente, Nodo: IXMLNode;
begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
Cliente := XML.DocumentElement.AddChild( 'Cliente' );
Cliente.Attributes['id'] := '3';
Nodo := Cliente.AddChild( 'nombre' );
Nodo.Text := 'PABLO PALAZON ALCOLEA';
Nodo := Cliente.AddChild( 'nif' );
Nodo.Text := '79469163E';
Nodo := Cliente.AddChild( 'saldopte' );
Nodo.Text := '0.00';
Nodo := Cliente.AddChild( 'diaspago' );
Nodo.Text := '15';
XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes3.xml' );
end;

En esta ocasión utilizamos dos nodos: Cliente para crear el nodo padre y Nodo para crear cada uno de los nodos hijos.

Por último podemos eliminar elementos utilizando el método Delete cuyo parámetro es el número de nodo hijo que deseamos eliminar (siendo cero el primer elemento). Para eliminar el primer cliente de la lista hacemos los siguiente:

begin
XML.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'clientes.xml' );
XML.Active := True;
XML.DocumentElement.ChildNodes.Delete( 0 );
XML.SaveToFile( ExtractFilePath( Application.ExeName ) + 'clientes4.xml' );
end;

Este código coge la lista de los dos clientes iniciales, elimina el primero y guarda el documento como clientes4.xml. De esta manera podemos crear una pequeña base de datos utilizando documentos XML.

En el próximo artículo veremos como simplificar aún más el manejo de archivos XML utilizando el asistente XML Data Binding.

Pruebas realizadas en Delphi 7.

10 octubre 2007

Trabajando con documentos XML (I)

Un documento XML (Extensible Markup Language) contiene un lenguaje de etiquetas para almacenar información de forma estructurada. Es similar a las páginas web HTML, exceptuando que sólo guarda la información y no la forma de visualizarla. Los documentos XML proporcionan una forma simple para almacenar la información que permite ser comprendida fácilmente.

Además se ha convertido de hecho en un estándar, siendo utilizado en aplicaciones web, comunicaciones, etc. Se trata de un lenguaje de intercambio de datos que ya es soportado por la mayoría de lenguajes de programación.

Los documentos XML manejan la información de forma jerárquica, siendo las etiquetas las que describen la información de cada elemento así como los elementos hijos. Veamos un ejemplo de un documento XML:

Este ejemplo muestra como está almacenada la información en un documento XML. La primera línea contiene la declaración de un documento XML. Aunque declaración es opcional es recomendable introducirla en todos los documentos XML. Es este caso nos dice que es un documento XML con versión 1.0 y que utiliza la codificación de caracteres UTF-8.

La segunda línea nos informa que el documento se llama Clientes y que va a estar enlazado con un documento externo llamado cli.dtd. Los documentos DTD se utilizan para definir las reglas de cómo tienen que estar estructurados los documentos XML. En nuestro caso define la regla de cómo deben estar definida la estructura de un cliente. Sería algo así como la definición de un esquema para la jerarquía de los datos del cliente. No es obligatorio definir un DTD por cada documento XML.

La información que se guarda en los documentos XML comienza con un nodo raiz Clientes que a su vez tiene nodos hijos: Cliente, nombre, etc. Las etiquetas a su vez pueden contener dentro valores predeterminados, por ejemplo Cliente tiene el valor id="1".

Aunque es posible trabajar directamente con un documento XML utilizando cualquier procesador de textos (incluso el Bloc de Notas de Windows) existen aplicaciones para editar este tipo de documentos. El consorcio internacional W3C define un conjunto de reglas estándar que definen como debe crearse un documento XML cuyo nombre es DOM (Document Object Model).

USANDO DOM

Las interfaces denifidas por el estandar DOM contienen la implementación de diferentes documentos XML suministrados por terceras partes llamados vendedores (vendors). Si no deseamos utilizar la implementación DOM de estos vendedores, Delphi permite utilizar un mecanismo de registro adicional para crearnos nuestra propia implementación de documentos XML.

La unidad XMLDOM incluye declaraciones para todas las interfaces DOM definidas por las especificaciones de nivel 2 para documentos XML según el consorcio W3C. Cada vendedor DOM proporciona sus propias interfaces para manejar documentos XML. Una interfaz DOM viene a ser algo así como un driver para manejar documentos XML.

Para que Delphi pueda utilizar un documento DOM de algun vendedor en concreto hay que añadir la unidad correspondiente a dicha implementación DOM. Estas unidades finalizan con la cadena 'xmldom'. Por ejemplo, la unidad para la implementación de Microsoft es MSXMLDOM, la unidad para documentos de IBM es IBMXMLDOM y la unidad para implementaciones Open XML es OXMLDOM.

Si quisieramos crear nuestra propia implementación DOM tendríamos que crear una unidad que defina una clase descendiente de la clase TDOMVendor. Se haría de la siguiente manera:

- En nuestra clase descendiente, debemos sobrescribir dos métodos: el método Description, el cual devuelve una cadena con el identificador del venvedor y el método DOMImplementation donde define la interfaz IDOMImplementation.

- Después registramos nuestra nueva unidad llamando al procedimiento RegistrerDOMVendor y ya estaría lista para poder ser utilizada.

- Y si queremos eliminarla de las implementaciones estándar DOM debemos anular el registro llamando al procedimiento UnRegisterDOMVendor, el cual debería estar en la sección finalization.

Algunos vendedores suministran extensiones a las interfaces estandar DOM. Para que puedas usar esas extensiones la unidad XMLDOM define la interfaz IDOMNodeEx, la cual es descendiente de la interfaz estandar IDOMNode.

Podemos trabajar directamente con interfaces DOM para leer y editar documentos XML. Simplemente llamamos la la función GetDOM para obtener el intefaz IDOMImplementation, el cual proporciona un punto de partida.

TRABAJANDO CON COMPONENTES XML

El punto de partida para comenzar a trabajar con documentos XML es utilizando el componente de la clase TXMLDocument. Para poder crear un documento XML hay que seguir los siguientes pasos:

1. Añadimos el componente XMLDocument a nuestro formulario o módulo de datos. Este componente se encuentra en la pestaña Internet.

2. Seleccionamos en la propiedad DOMVendor la implementación del documento DOM que queremos usar. Por defecto aparece seleccionada la de Microsoft (MSXML).

3. Dependiendo de la implementación seleccionada, podemos modificar sus opciones en propiedad ParseOptions para configurar como va a procesarse el documento XML. Un parser viene a ser algo así como un analizador sintáctico.

4. Para trabajar con un documento XML que ya existe hay que especificar el documento de la siguiente manera:

- Si el documento esta almacenado en un archivo, ponemos en FileName el nombre del archivo.

- O bien podemos meter el texto XML dentro del mismo componente XMLDocument utilizando su propiedad XML.

5. Activamos el documento XML poniendo la propiedad Active a True.

Una vez que tenemos activo el documento, podremos navegar por su jerarquía de nodos leyendo o modificando sus valores. El nodo raiz está disponible en la propiedad DocumentElement.

En el próximo artículo veremos ejemplos de código fuente para leer y modificar documentos XML con Delphi.

Pruebas realizadas en Delphi 7.

09 octubre 2007

Leyendo los metadatos de una tabla de Interbase/Firebird

No hay nada que cause más pereza a un programador que la actualización de campos en las bases de datos de nuestros clientes. Hay muchas maneras de hacerlo: desde archivos de texto con metadatos, archivos SQL o incluso actualizaciones por Internet con FTP o correo electrónico.

Yo lo que suelo hacer es tener un archivo GDB o FDB (según sea Interbase o Firebird respectivamente) que contiene todas las bases de datos vacías. Si tengo que ampliar algún campo lo hago sobre esta base de datos.

Después cuando tengo que actualizar al cliente lo que hago es mandarle mi GDB vacío y mediante un pequeño programa que tengo hecho compara las estructuras de la base de datos de mi GDB vacío y las del cliente, creando tablas y campos nuevos según las diferencias sobre ambas bases de datos.

Ahora bien, ¿Cómo podemos leer el tipo de campos de una tabla? Pues en principio tenemos que el componente IBDatabase tiene la función GetTableNames que devuelve el nombre de las tablas de una base de datos y la función GetFieldNames que nos dice los campos pertenecientes a una tabla en concreto. El problema radica en que no me dice que tipo de campo es (float, string, blob, etc).

LEYENDO LOS CAMPOS DE UNA TABLA

Para leer los campos de una tabla utilizo el componente de la clase TIBQuery situado en la pestaña Interbase. Cuando este componente abre una tabla carga el nombre de los campos y su tipo en su propiedad FieldDefs. Voy a realizar una aplicación sencilla en la cual seleccionamos una base de datos Interbase o Firebird y cuando se elija una tabla mostrará sus metadatos de esta manera:


Va a contener los siguientes componentes:


- Un componente Edit con el nombre RUTADB que va a guardar la ruta de la base de datos.

- Un componente de la clase TOpenDialog llamado AbrirBaseDatos para buscar la base de datos Firebird/Interbase.

- Un botón llamado BExaminar.

- Un componente ComboBox llamado TABLAS que guardará el nombre de las tablas de la base de datos.

- Un componente IBDatabase llamado BaseDatos que utilizaremos para conectar.

- Un componente IBTransaction llamado Transaccion para asociarlo a la base de datos.

- Un componente IBQuery llamado IBQuery que utilizaremos para abrir una tabla.

- Y por último un campo Memo llamado Memo para mostrar la información de los metadatos.

Comenzemos a asignar lo que tiene que hacer cada componente:

- Hacemos doble clic sobre el componente BaseDatos y le damos de usuario SYSDBA y password masterkey. Pulsamos Ok y desactivamos su propiedad LoginPrompt.

- Asignamos el componente Transaccion a los componentes BaseDatos y IBQuery.

- Asignamos el componente BaseDatos a los componentes Transaccion y IBQuery.

- En la propiedad Filter del componente AbrirBaseDatos ponemos:

Interbase|*.gdb|Firebird|*.fdb

- Al pulsar el botón Examinar hacemos lo siguiente:

procedure TFormulario.BExaminarClick( Sender: TObject );
begin
if AbrirBaseDatos.Execute then
begin
RUTADB.Text := AbrirBaseDatos.FileName;
BaseDatos.DatabaseName := '127.0.0.1:' + RUTADB.Text;

try
BaseDatos.Open;
except
on E: Exception do
Application.MessageBox( PChar( E.Message ), 'Error al abrir base de datos',
MB_ICONSTOP );
end;

BaseDatos.GetTableNames( TABLAS.Items, False );
end;
end;

Lo que hemos hecho es abrir la base de datos y leer el nombre de las tablas que guardaremos dentro del ComboBox llamado TABLAS.

Cuando el usuario seleccione una tabla y pulse el botón Mostrar hacemos lo siguiente:

procedure TFormulario.BMostrarClick( Sender: TObject );
var
i: Integer;
sTipo: String;
begin
with IBQuery do
begin
Memo.Lines.Add( 'CREATE TABLE ' + TABLAS.Text + ' (' );

SQL.Clear;
SQL.Add( 'SELECT * FROM ' + TABLAS.Text );
Open;

for i := 0 to FieldDefs.Count - 1 do
begin
sTipo := '';

if FieldDefs.Items[i].FieldClass.ClassName = 'TIBStringField' then
sTipo := 'VARCHAR(' + IntToStr( FieldByName( FieldDefs.Items[i].Name ).Size ) + ')';

if FieldDefs.Items[i].FieldClass.ClassName = 'TFloatField' then
sTipo := 'DOUBLE PRECISION'; // También podría ser FLOAT (32 bits) aunque prefiero DOUBLE (64 bits)

if FieldDefs.Items[i].FieldClass.ClassName = 'TIntegerField' then
sTipo := 'INTEGER';

if FieldDefs.Items[i].FieldClass.ClassName = 'TDateField' then
sTipo := 'DATE';

if FieldDefs.Items[i].FieldClass.ClassName = 'TTimeField' then
sTipo := 'TIME';

if FieldDefs.Items[i].FieldClass.ClassName = 'TDateTimeField' then
sTipo := 'TIMESTAMP';

if FieldDefs.Items[i].FieldClass.ClassName = 'TBlobField' then
sTipo := 'BLOB';

// ¿Es un campo obligatorio?
if FieldByName( FieldDefs.Items[i].Name ).Required then
sTipo := sTipo + ' NOT NULL';

Memo.Lines.Add( ' ' + FieldDefs.Items[i].Name + ' ' + sTipo );

// Si no es el último campo añadimos una coma al final
if i < FieldDefs.Count - 1 then
Memo.Lines[Memo.Lines.Count-1] := Memo.Lines[Memo.Lines.Count-1] + ',';
end;

Memo.Lines.Add( ')' );
Close;
Transaction.Active := False;
end;
end;

Lo que hace este procedimiento es abrir con el componente IBQuery la tabla seleccionada y según los tipos de campos creamos la SQL de creación de la tabla.

Este método también nos podría ser útil para hacer un programa que copie datos entre tablas Interbase/Firebird.

Pruebas realizadas con Firebird 2.0 y Delphi 7.


PUBLICIDAD

lentillas

bajar de peso

bajar peso

08 octubre 2007

Generando números aleatorios

Las funciones de las que disponen los lenguajes de programación para generar números aleatorios se basan en una pequeña semilla según la fecha del sistema y a partir de ahí se van aplicando una serie de fórmulas se van generando números al azar según los milisegundos que lleva el PC arrancado.

Dephi dispone de la función Random para generar números aleatorios entre 0 y el parámetro que se le pase. Pero para que la semilla no sea siempre la misma es conveniente inicializarla utilizando el procedimiento Randomize.

Por ejemplo, si yo quisiera inventarme 10 números del 1 y 100 haría lo siguiente:

procedure TFormulario.Inventar10Numeros;
var
i: Integer;
begin
Randomize;

for i := 1 to 10 do
Memo.Lines.Add( IntToStr( Random( 100 ) + 1 ) );
end;

El resultado lo he volcado a un campo Memo.

INVENTANDO LOS NUMEROS DE LA LOTO

Supongamos que quiero hacer el típico programa que genera automáticamente las combinaciones de la loto. El método es tan simple como hemos visto anteriormente:

procedure TFormulario.InventarLoto;
var
i: Integer;
begin
Randomize;

for i := 1 to 6 do
Memo.Lines.Add( IntToStr( Random( 49 ) + 1 ) );
end;

Pero así como lo genera es algo chapucero. Primero tenemos el problema de que los números inventados no los ordena y luego podría darse el caso de el ordenador se invente un número dos veces.

Para hacerlo como Dios manda vamos a crear la clase TSorteo encargada de inventarse los 6 números de la loto y el complementario. Además lo vamos a hacer como si fuera de verdad, es decir, vamos a crear un bombo, le vamos a introducir las 49 bolas y el programa las va a agitar y sacará una al azar. Y por último también daremos la posibilidad de excluir ciertos números del bombo (por ejemplo el 1 y 49 son los que menos salen por estadística).

Comencemos creando la clase TSorteo en la sección type:

type
TSorteo = class
public
Fecha: TDate;
Numeros: TStringList;
Complementario: String;
Excluidos: TStringList;

constructor Create;
destructor Destroy; override;
procedure Inventar;
procedure Excluir( sNumero: String );
end;

Como podemos ver en la clase los números inventados y los excluidos los voy a meter en un StringList. El constructor de la clase TSorteo va a crear ambos StringList:

constructor TSorteo.Create;
begin
Numeros := TStringList.Create;
Excluidos := TStringList.Create;
end;

Y el destructor los liberará de memoria:

destructor TSorteo.Destroy;
begin
Excluidos.Free;
Numeros.Free;
end;

Nuestra clase TSorteo también va a incluir un método para excluir del sorteo el número que queramos:

procedure TSorteo.Excluir( sNumero: String );
begin
// Antes de excluirlo comprobamos si ya lo esta
if Excluidos.IndexOf( sNumero ) = -1 then
Excluidos.Add( sNumero );
end;

Y aquí tenemos la función que se inventa el sorteo evitando los excluidos:

procedure TSorteo.Inventar;
var
Bombo: TStringList;
i, iPos1, iPos2: Integer;
sNumero, sBola: String;
begin
// Metemos las 49 bolas en el bombo
Bombo := TStringList.Create;
Numeros.Clear;

for i := 1 to 49 do
begin
sNumero := CompletarCodigo( IntToStr( i ), 2 );

if Excluidos.IndexOf( sNumero ) = -1 then
Bombo.Add( sNumero );
end;

// Agitamos las bolas con el método de la burbuja
if Bombo.Count > 0 then
for i := 1 to 10000 + Random( 10000 ) do
begin
// Nos inventamos dos posiciones distintas en el bombo
iPos1 := Random( Bombo.Count );
iPos2 := Random( Bombo.Count );

if ( iPos1 >= 0 ) and ( iPos1 <= 49 ) and ( iPos2 >= 0 ) and ( iPos2 <= 49 ) then
begin
// Intercambiamos las bolas en esas dos posiciones inventadas
sBola := Bombo[iPos1];
Bombo[iPos1] := Bombo[iPos2];
Bombo[iPos2] := sBola;
end;
end;

// Vamos sacando las 6 bolas al azar + complementario
for i := 0 to 6 do
begin
if Bombo.Count > 0 then
iPos1 := Random( Bombo.Count )
else
iPos1 := -1;

if ( iPos1 >= 0 ) and ( iPos1 <= 49 ) then
sBola := Bombo[iPos1]
else
sBola := '';

// ¿Es el complementario?
if i = 6 then
// Lo sacamos aparte
Complementario := sBola
else
// Lo metemos en la lista de números
Numeros.Add( sBola );

// Sacamos la bola extraida del bombo
if ( iPos1 >= 0 ) and ( iPos1 <= 49 ) and ( Bombo.Count > 0 ) then
Bombo.Delete( iPos1 );
end;

// Ordenamos los 6 números
Numeros.Sort;

Bombo.Free;
end;

El procedimiento Inventar hace lo siguiente:

1º Crea un bombo dentro de un StringList y le mete las 49 bolas.

2º Elimina los número excluidos si los hay (los excluidos hay que meterlos en dos cifras, 01, 07, etc.)

3º Agita los números dentro del bombo utilizando del método de la burbuja para que queden todas las bolas desordenadas.

4º Extrae las 6 bolas y el complementario eligiendo a azar dos posiciones del StringList para hacerlo todavía mas rebuscado. Al eliminar la bola extraida evitamos así números repetidos, tal como si fuera el sorteo real.

5º Una vez inventados los números los deposita en el StringList llamado Numeros y elimina de memoria el Bombo.

Ahora vamos a utilizar nuestra clase TSorteo para generar una combinación:

procedure TFormulario.InventarSorteo;
var
S: TSorteo;
begin
Randomize;
S := TSorteo.Create;
S.Inventar;
Memo.Lines.Add( S.Numeros.Text );
S.Free;
end;

Si quisiera excluir del sorteo los número 1 y 49 haría lo siguiente:

var
S: TSorteo;
begin
Randomize;
S := TSorteo.Create;
S.Excluir( '01' );
S.Excluir( '49' );
S.Inventar;
Memo.Lines.Add( S.Numeros.Text );
S.Free;
end;

Este es un método simple para generar los números de la loto pero las variantes que se pueden hacer del mismo son infinitas. Ya depende de la imaginación de cada cual y del uso que le vaya a dar al mismo.

Igualmente sería sencillo realizar algunas modificaciones para inventar otros sorteos tales como el gordo de la primitiva, el sorteo de los euromillones o la quiniela de fútbol.

Pruebas realizadas en Delphi 7.

05 octubre 2007

Creando consultas SQL con parámetros

En el artículo anterior vimos como realizar consultas SQL para INSERT, DELETE, UPDATE y SELECT utilizando el componente IBSQL que forma parte de la paleta de componentes IBExpress.

También quedó muy claro que la velocidad de ejecución de consultas con este componente respecto a otros como IBQuery es muy superior. Todo lo que hemos visto esta bien para hacer consultas esporádicas sobre alguna tabla que otra, pero ¿que ocurre si tenemos que realizar miles de consultas SQL de una sola vez?

UTILIZANDO UNA TRANSACCION POR CONSULTA

Supongamos que tenemos que modificar el nombre de 1000 registros de la tabla CLIENTES:

var
i: Integer;
dwTiempo: DWord;
begin
with Consulta do
begin
////////////// METODO LENTO ////////////////

dwTiempo := TimeGetTime;

for i := 1 to 1000 do
begin
SQL.Clear;
SQL.Add( 'UPDATE CLIENTES' );
SQL.Add( 'SET NOMBRE = ' + QuotedStr( 'NOMBRE CLIENTE Nº ' + IntToStr( i ) ) );
SQL.Add( 'WHERE ID = ' + IntToStr( i ) );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

ShowMessage( 'Tiempo: ' + IntToStr( TimeGetTime - dwTiempo ) + ' milisegundos' );
end;
end;

Como puede verse arriba, por cada cliente actualizado he generado una SQL distinta abriendo y cerrando una transacción para cada registro. He utilizado la función TimeGetTime que se encuentra en la unidad MMSystem para calcular el tiempo que tarda en actualizarme el nombre de los 1000 clientes. En un PC con Pentium 4 a 3 Ghz, 1 GB de RAM y utilizando el motor de bases de datos Firebird 2.0 me ha tardado 4167 milisegundos.

Aunque las consultas SQL van muy rápidas con los componentes IBSQL aquí el fallo que cometemos es que por cada registro actualizado se abre y se cierra una transacción. En una base de datos local no se nota mucho pero en una red local con muchos usuarios trabajando a la vez le puede pegar fuego al concentrador.

Lo ideal sería poder modificar la SQL pero sin tener que cerrar la transacción. Como eso no se puede hacer en una consulta que esta abierta entonces hay que utilizar los parámetros. Los parámetros (Params) nos permiten enviar y recoger información de una consulta SQL que se esta ejecutando sin tener que cerrarla y abrila.

UTILIZANDO PARAMETROS EN LA CONSULTA

Para introducir parámetros en una consulta SQL hay que añadir dos puntos delante del parámetro. Por ejemplo:

UPDATE CLIENTES
SET NOMBRE = :NOMBRE
WHERE ID = :ID

Esta consulta tiene dos parámetros: ID y NOMBRE. Los nombres de los parámetros no tienen porque coincidir con el nombre del campo. Bien podrían ser así:

UPDATE CLIENTES
SET NOMBRE = :NUEVONOMBRE
WHERE ID = :IDACTUAL

De este modo se pueden modificar las condiciones de la consulta SQL sin tener que cerrar la transacción. Después de crear la consulta SQL hay que llamar al método Prepare para que prepare la consulta con los futuros parámetros que se le van a suministrar (no es obligatorio pero si recomendable). Veamos el ejemplo anterior utilizando parámetros y una sóla transacción para los 1000 registros:

var
i: Integer;
dwTiempo: DWord;
begin
with Consulta do
begin
////////////// METODO RÁPIDO ////////////////

dwTiempo := TimeGetTime;

Transaction.StartTransaction;

SQL.Clear;
SQL.Add( 'UPDATE CLIENTES' );
SQL.Add( 'SET NOMBRE = :NOMBRE' );
SQL.Add( 'WHERE ID = :ID' );
Prepare;

for i := 1 to 1000 do
begin
Params.ByName( 'NOMBRE' ).AsString := 'NOMBRE CLIENTE Nº '+ IntToStr( i );
Params.ByName( 'ID' ).AsInteger := i;
ExecQuery;
end;

try
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;

ShowMessage( 'Tiempo: ' + IntToStr( TimeGetTime - dwTiempo ) + ' milisegundos' );
end;
end;

En esta ocasión me ha tardado sólo 214 milisegundos, es decir, se ha reducido al 5% del tiempo anterior sin saturar al motor de bases de datos abriendo y cerrando transacciones sin parar.

Este método puede aplicarse también para consultas con INSERT, SELECT y DELETE. En lo único en lo que hay que tener precaución es en no acumular muchos datos en la transacción, ya que podría ser peor el remedio que la enfermedad.

Si teneis que actualizar cientos de miles de registros de una sola vez, recomiendo realizar un Commit cada 1000 registros para no saturar la memoria caché de la transacción. Todo depende del número de campos que tengan las tablas así como el número de registros a modificar. Utilizad la función TimeGetTime para medir tiempos y sacar conclusiones.

Y si el proceso a realizar va a tardar más de 2 o 3 segundos utilizar barras de progreso e hilos de ejecución, ya que algunos usuarios neuróticos podrían creer que nuestro programa se ha colgado y empezarían ha hacer clic como posesos (os aseguro que existe gente así, antes de que termine la consulta SQL ya te están llamando por teléfono echándote los perros).

Pruebas realizadas con Firebird 2.0 y Dephi 7.

04 octubre 2007

Creando consultas SQL rápidas con IBSQL

Cuando se crea un programa para el mantenimiento de tablas de bases de datos (clientes, artículos, etc.) el método ideal es utilizar componentes ClientDataSet como vimos anteriormente.

Pero hay ocasiones en las que es necesario realizar consultas rápidas en el servidor tales como incrementar existencias en almacén, generar recibos automáticamente o incluso incrementar nuestros contadores de facturas sin utilizar generadores.

En ese caso el componente más rápido para bases de datos Interbase/Firebird es IBSQL el cual permite realizar consultas SQL sin que estén vinculadas a ningún componente visual. Vamos a ver unos ejemplos de inserción, modificación y eliminación de registros utilizando este componente.

INSERTANDO REGISTROS EN UNA TABLA

Aquí tenemos un ejemplo de insertar un registro en una tabla llamada CLIENTES utilizando un objeto IBSQL llamado Consulta:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'INSERT INTO CLIENTES' );
SQL.Add( '( NOMBRE, NIF, IMPORTEPTE )' );
SQL.Add( 'VALUES' );
SQL.Add( '( ''ANTONIO GARCIA LOPEZ'', ''46876283D'', 140.23 )' );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

Como puede apreciarse hemos tenido que abrir nosotros a mano la transacción antes de ejecutar la consulta ya que el objeto IBSQL no la abre automáticamente tal como ocurre en los componentes IBQuery.

Una vez ejecutada la consulta, si todo ha funcionado correctamente enviamos la transacción al servidor mediante el método Commit. En el caso de que falle mostramos el error y cancelamos la transacción utilizando el método RollBack.

MODIFICANDO LOS REGISTROS DE UNA TABLA

El método para modificar los registros es el mismo que para insertarlos:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'UPDATE CLIENTES' );
SQL.Add( 'SET NOMBRE = ''MARIA GUILLEN ROJO'',' );
SQL.Add( 'NIF = ''69236724W'', ' );
SQL.Add( 'IMPORTEPTE = 80.65' );
SQL.Add( 'WHERE ID=21963' );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

ELIMINANDO REGISTROS DE LA TABLA

Al igual que para las SQL para INSERT y UPDATE el procedimiento es el mismo:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'DELETE FROM CLIENTES' );
SQL.Add( 'WHERE ID=21964' );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

CONSULTANDO LOS REGISTROS DE UNA TABLA

El método de consultar los registros de una tabla mediante SELECT difiere de los que hemos utilizado anteriormente ya que tenemos que dejar la transacción abierta hasta que terminemos de recorrer todos los registros:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'SELECT * FROM CLIENTES' );
SQL.Add( 'ORDER BY ID' );

Transaction.StartTransaction;

// Ejecutarmos consulta
try
ExecQuery;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;

// Recorremos los registros
while not Eof do
begin
Memo.Lines.Add( FieldByName( 'NOMBRE' ).AsString );
Next;
end;

// Cerramos la consulta y la transacción
Close;
Transaction.Active := False;
end;

Aunque pueda parecer un coñazo el componente de la clase TIBSQL respesto a los componentes TIBTable o TIBQuery, su velocidad es muy superior a ambos componentes, sobre todo cuando se ejecutan las consultas sucesivamente.

En el próximo artículo veremos cómo utilizar parámetros en las consultas.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

03 octubre 2007

Como poner una imagen de fondo en una aplicación MDI

En un artículo anterior vimos como crear aplicaciones MDI gestionando múltiples ventanas hijas dentro de la ventana padre.

Una de las cosas que más dan vistosidad a una aplicación comercial es tener un fondo con nuestra marca de fondo de la aplicación (al estilo Contaplus o Facturaplus).

Para introducir una imagen de fondo en la ventana padre MDI hay que hacer lo siguiente:

- Introducir en la ventana padre (la que tiene la propiedad FormStyle a MDIForm) un componente de la clase TImage situado en la pestaña Additional. Al componenente lo vamos a llamar Fondo.

- En dicha imagen vamos a cambiar la propidedad Align a alClient para que ocupe todo el fondo del formulario padre.

- Ahora sólo falta cargar la imagen directamente:

Fondo.Picture.LoadFromFile( 'c:\imagenes\fondo.bmp' );

El único inconveniente que tiene esto es que no podemos utilizar los eventos del formulario al estar la imagen encima (Drag and Drop, etc).

UTILIZANDO EL CANVAS

Otra forma de hacerlo sería poniendo el objeto TImage en medio del formulario pero de manera invisible (sin alClient). Después en el evento OnPaint del formulario copiamos el contenido de la imagen TImage al fondo del formulario:

procedure TFormulario.FormPaint( Sender: TObject );
var R: TRect;
begin
R.Left := 0;
R.Top := 0;
R.Right := Fondo.Width;
R.Bottom := Fondo.Height;
Canvas.CopyRect( R, Fondo.Canvas, R );
end;

Así podemos tener igualmente una imagen de fondo sin renunciar a los eventos del formulario (OnMouseMove, OnClick, etc.).

Pruebas realizadas en Dephi 7.

02 octubre 2007

La potencia de los ClientDataSet (y V)

Antes de proceder a crear el código para el botón Completar vamos a hacer que la conexión del programa con la base de datos sea algo más flexible.

Hasta ahora nuestro objeto IBDatabase está conectado directamente a una ruta fija donde tenemos el archivo BASEDATOS.FDB. Pero si cambiamos de ruta el programa dejará de funcionar provocando un fallo de conexión.

Para evitar esto vamos a hacer que el programa conecte con la base de datos al mostrar el formulario principal FClientes. Para ello en el evento OnShow de dicho formulario ponemos lo siguiente:

procedure TFClientes.FormShow( Sender: TObject );
begin
AccesoDatos.BaseDatos.DatabaseName := '127.0.0.1:' + ExtractFilePath( Application.ExeName ) + 'BASEDATOS.FDB';

try
AccesoDatos.BaseDatos.Open;
except
raise;
end;

LogicaNegocio.TLstClientes.Active := True;
end;

Esto hace que conecte con la base de datos que está al lado de nuestro ejecutable. Así hacemos que nuestro programa sea portable (a falta de instalarle el motor de bases de datos correspondiente). Si la conexión con la base de datos es correcta entonces abrimos la tabla del listado de clientes (TLstClientes).

Cuando se trata de una base de datos Interbase la conexión puede ser local o remota. Si es local no es necesario poner la IP:

AccesoDatos.BaseDatos.DatabaseName := 'C:\MiPrograma\BaseDatos.gdb';

Aunque viene a ser lo mismo que hacer esto:

AccesoDatos.BaseDatos.DatabaseName := '127.0.0.1:C:\MiPrograma\BaseDatos.gdb';

Se trata de una conexión remota aunque estemos accediendo a nuestro mismo equipo. Si se tratara de otro equipo de la red habría que hacer lo mismo:

AccesoDatos.BaseDatos.DatabaseName := '192.168.0.1:C:\MiPrograma\BaseDatos.gdb';

Para bases de datos Firebird no existe la conexión local, siempre es remota. Así que si vamos a conectar con nuestro equipo en local habría que hacerlo así:

AccesoDatos.BaseDatos.DatabaseName := '127.0.0.1:C:\MiPrograma\BaseDatos.fdb';

Yo recomiendo utilizar siempre la conexión remota utilizando para ello un archivo INI al lado de nuestro programa que contenga la IP del servidor. Por ejemplo:

[CONEXION]
IP=127.0.0.1

Así podemos hacer que nuestro programa se conecte en local o remoto sin tener que volver a compilarlo. Pero que no se os olvide dejar desconectado el componente IBDatabase en el módulo de acceso a datos AccesoDatos porque si no lo primero que ha a hacer es conectarse al arrancar el programa provocando un error.

DANDO DE ALTA CLIENTES DE FORMA MASIVA

Para probar el rendimiento de un programa no hay nada mejor que darle caña metiendo miles y miles de registros a la base de datos. Para ello voy a crear un procedimiento dentro del botón Completar que le preguntará al usuario cuandos clientes desea crear. Al pulsar Aceptar dará de alta todos esos registros mostrando el progreso en la barra de progreso que pusimos anteriormente:

procedure TFClientes.BCompletarClick( Sender: TObject );
var
sNumero: string;
i, iInventado: Integer;
begin
sNumero := InputBox( 'Nº de clientes', 'Rellenando clientes', '' );
Randomize; // Inicializamos el generador de números aleatorios

if sNumero <> '' then
begin
with LogicaNegocio do
begin
TClientes.Open;
Progreso.Max := StrToInt( sNumero );
Progreso.Visible := True;
TLstClientes.DisableControls;

for i := 1 to StrToInt( sNumero ) do
begin
iInventado := Random( 99999999 ) + 1000000; // Nos invertamos un número identificador de cliente
TClientes.Insert;
TClientesNOMBRE.AsString := 'CLIENTE Nº ' + IntToStr( iInventado );
TClientesNIF.AsString := IntToStr( iInventado );
TClientesDIRECCION.AsString := 'CALLE Nº ' + IntToStr( iInventado );
TClientesPOBLACION.AsString := 'POBLACION Nº ' + IntToStr( iInventado );
TClientesPROVINCIA.AsString := 'PROVINCIA Nº ' + IntToStr( iInventado );
iInventado := Random( 79999 ) + 10000; // Nos inventamos el código postal
TClientesCP.AsString := IntToStr( iInventado );
TClientesIMPORTEPTE.AsFloat := 0;
TClientes.Post;
Progreso.Position := i;
Application.ProcessMessages;
end;

TClientes.Close;
Progreso.Visible := False;
TLstClientes.EnableControls;
end;
end;
end;

Lo que hemos hecho es inventar el nombre de usuario, dirección, NIF, etc. También he desconectado y he vuelto a conectar la tabla TLstClientes de la rejilla utilizando los métodos DisableControls y EnableControls para ganar más velocidad. La barra de progreso llamada Progreso mostrará la evolución del alta de registros.

Realmente es una burrada dar de alta masivamente registros utilizando objetos ClientDataSet ya que están pensados para utilizarlos para altas, modificaciones y eliminación de registros de uno a uno y sin mucha velocidad. Si quereis hacer una inserción masiva de registros hay que realizar consultas SQL con INSERT utilizando el objetos de la clase TIBSQL que es mucho más rápido que los ClientDataSet. Ya explicaré en otro momento la ventaja de utilizar dichos componentes.

Con esto finalizamos la introducción a los componentes ClientDataSet.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

01 octubre 2007

La potencia de los ClientDataSet (IV)

Ya tenemos todo lo necesario para realizar el programa:

- La base de datos firebird: BASEDATOS.FDB

- La capa de acceso a datos en el módulo de datos: AccesoDatos.

- La lógica de negocio en el módulo de datos: LogicaNegocio.

CREANDO EL FICHERO GENERAL DE CLIENTES

El primer formulario que vamos a crear se va a llamar FClientes y va a contener el listado general de clientes:


Va a contener los siguientes componentes:

- 4 botones de la clase TButton para dar de alta clientes, modificarlos y eliminarlos. Habrá otro llamado BCompletar que utilizaremos más adelante para dar de alta clientes de forma masiva.

- Una rejilla de datos de la clase TDBGrid que va a contener 3 columnas para el listado del cliente: ID, NOMBRE y NIF. Su nombre va a ser ListadoClientes.

- Un componente DataSource (pestaña Data Access) llamado DSLstClientes que va a encargarse de suministrar datos a la rejilla.

- Un componente DBNavitator (pestaña Data Controls) para hacer pruebas con los registros. Lo llamaremos Navegador.

- Una barra de progreso a la izquierda del DBNavigator de la clase TProgressBar que mostrará el progreso de los clientes dados de alta automáticamente a través del botón BCompletar.

Ahora hay que vincular los componentes como corresponde:

- Lo primero es añadir en la sección uses el módulo de datos LogicaNegocio para poder vincular el listado de clientes a la rejilla.

- Vinculamos el componente DSLstClientes con el ClientDataSet llamado TLstClientes a través de su propiedad DataSet.

- En la propiedad DataSource de la rejilla de datos ListadoClientes asignamos el componente DSLstClientes.

- Vinculamos el componente Navegador al componente DSLstClientes mediante su propiedad DataSource.

Más adelante escribiremos código para cada uno de los botones.

CREANDO EL FORMULARIO DEL CLIENTE

Vamos a crear el siguiente formulario llamado FCliente:


Va a constar de tantos componentes de la clase TLabel y TDBEdit como campos tenga la tabla clientes. Todos los campos son modificables a excepción del ID que lo he puesto con su propiedad Enabled a False. También lo he oscurecido podiendo de Color el valor clBtnFace.

Para vincular los campos a la tabla real de clientes introducimos un componente DataSource llamado DSClientes, pero antes hay que añadir en la sección uses el módulo de datos LogicaNegocio.

Ahora se vincula el componente DSClientes con el ClientDataSet llamado TClientes situado en el módulo de datos LogicaNegocio a través de su propiedad Dataset. Con esto ya podemos vincular cada campo TDBEdit con el DataSource DSClientes y con su campo correspondiente especificado en la propiedad DataField.

Cuando se pulse el botón Aceptar ejecutamos el código:

procedure TFCliente.BAceptarClick( Sender: TObject );
begin
LogicaNegocio.TClientes.Post;
ModalResult := mrOk;
end;

Al botón BAceptar le ponemos en su propiedad ModalResult el valor mrNone. Esto tiene su explicación. Cuando pulsemos Aceptar, si el usuario no ha rellenado correctamente algún dato saltará un error definido en los Constraint y no hará nada, evitamos que se cierre el formulario. Así obligamos al usuario a introducir los datos correctamente. Sólo cuando el Post se realice correctamente le diremos al formulario que el resultado es mrOk para que pueda cerrarse.

En el botón BCancelar con sólo asignar en su propiedad ModalResult el valor mrCancel no será necesario hacer nada más.

Sólo una cosa más: si utilizais el teclado numérico en los campos de tipo Float vereis que la tecla decimal no funciona (aquí en España). Nos obliga a utilizar la coma que está encima de la barra de espacio. Para transformar el punto en coma yo lo que suelo hacer es lo siguiente (para el campo IMPORTEPTE):

procedure TFCliente.IMPORTEPTEKeyPress( Sender: TObject; var Key: Char );
begin
if key = '.' then
key := ',';
end;

Es decir, en el evento OnKeyPress cuando el usuario pulse el punto lo transformo en una coma. Si tuvieramos más campos float sólo habría que reasignar el mismo eventos al resto de campos.

Con esto ya tenemos terminado el formulario del cliente.

TERMINANDO EL FICHERO GENERAL DE CLIENTES

Una vez que tenemos la ficha del cliente vamos a terminar el mantenimiento de clientes asignado el código a cada botón. Comencemos con el botón Nuevo:

procedure TFClientes.BNuevoClick( Sender: TObject );
begin
LogicaNegocio.TClientes.Open;
LogicaNegocio.TClientes.Insert;
Application.CreateForm( TFCliente, FCliente );
FCliente.ShowModal;
LogicaNegocio.TClientes.Close;
end;

Como puede apreciarse abrimos la tabla TClientes sólo cuando hay que dar de alta un cliente o modificarlo. Después la cerramos ya que para ver el listado de clientes tenemos nuestra tabla TLstClientes.

Para el botón Modificar es muy parecido:

procedure TFClientes.BModificarClick( Sender: TObject );
begin
LogicaNegocio.TClientes.Params.ParamByName( 'ID' ).AsString := LogicaNegocio.TLstClientesID.AsString;
LogicaNegocio.TClientes.Open;
LogicaNegocio.TClientes.Edit;
Application.CreateForm( TFCliente, FCliente );
FCliente.ShowModal;
LogicaNegocio.TClientes.Close;
ListadoClientes.SetFocus;
end;

Aquí hay que detenerse para hablar de algo importante. Cuando abrimos una tabla por primera vez el cursor SQL dentro del motor de bases de datos se va al primer registro. ¿Cómo hacemos para ir al registro seleccionado por TLstClientes? Pues le tenemos que pasar el ID del cliente que queremos editar.

Esto se hace utilizando parámetros, los cuales hay que definirlos dentro del componente ClientDataSet llamado TClientes que está en el módulo de datos LogicaNegocio. Generalmente cuando se pulsa CTRL + A para traernos los campos de la tabla al ClientDataSet se dan de alta los parámetros. Como el componente TClientes está vinculado al IBQuery Clientes que tiene la SQL:

SELECT * FROM CLIENTES
WHERE ID=:ID

entonces al pulsar CTRL + A en el ClientDataSet nos da de alta automáticamente el parámetro ID. Si no fuera así, tendríamos que ir al componente TClientes, pulsar el botón [...] en su propiedad Params y dar de alta un parámetro con las propiedades:

DataType: ftInteger
Name: ID
ParamType: ptInput

Los parámetros dan mucha velocidad a un programa porque permiten modificar las opciones de la SQL sin tener que cerrar y abrir de nuevo la consulta, permitiendo saltar de un registro a otro dentro de una misma tabla a una velocidad impresionante.

Y por último introducimos el código correspondiente al botón Eliminar:

procedure TFClientes.BEliminarClick( Sender: TObject );
begin
with LogicaNegocio.TLstClientes do
if RecordCount > 0 then
if Application.MessageBox( '¿Desea eliminar este cliente?', 'Atención',
MB_ICONQUESTION or MB_YESNO ) = ID_YES then
Delete;

ListadoClientes.SetFocus;
end;

Antes de eliminar el registro nos aseguramos que de tenga algún dato y preguntamos al usuario si esta seguro de eliminarlo.

Con esto finalizamos nuestro mantenimiento de clientes a falta de utilizar el botón Completar donde crearemos un bucle que dará de alta tantos clientes como deseemos para probar la velocidad de la base de datos. Esto lo haremos en el próximo artículo.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

Publicidad