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.

28 septiembre 2007

La potencia de los ClientDataSet (III)

Después de crear la base de datos y la capa de acceso a datos dentro del DataModule vamos a crear la lógica de negocio.

Antes de seguir tenemos que hacer doble clic en los componente IBQuery de la capa de acceso a datos y pulsar la combinación de teclas CTRL + A para introducir todos los campos en el módulo de datos. Y en cada una de ellas hay que seleccionar el campo ID y quitar la propiedad Required ya que el propio motor de bases de datos va a meter el campo ID con un disparador.

Pero tenemos un problema, y es que no hemos seleccionado donde esta la base de datos. Para ello hacemos doble clic en el objeto BaseDatos situado módulo de datos AccesoDatos. Seleccionamos una base de datos Remota (Remote) con IP 127.0.0.1. Y la base de datos donde la tengamos, por ejemplo:

D:\Desarrollo\Delphi7\ClientDataSet\BaseDatos.fdb

De este modo, al hacer CTRL + A sobre las tablas conectará automáticamente sobre la base de datos para traerse los campos. No se os olvide luego desconectarla.

CREANDO LA LOGICA DE NEGOCIO

La capa de lógica de negocio también la vamos a implementar dentro de un objeto DataModule y va a constar de los siguientes componentes:


- Un DataModule llamado LogicaNegocio.

- Dos componentes DataSetProvider llamados DSPLstClientes y DSPClientes.

- Dos componentes ClientDataSet llamados TLstClientes y TClientes.

Ahora vamos a configurar cada uno de estos componentes:

- Enlazamos el DataModule LogicaNegocio con el DataModule AccesoDatos en la sección uses.

- Al componente DSPLstClientes le asignamos en su propiedad DataSet el componente AccesoDatos.LstClientes.

- Al componente DSPClientes le asignamos en su propiedad DataSet el componente AccesoDatos.Clientes.

- El componente TLstClientes lo vamos a vincular con DSPLstClientes mediante su propiedad ProviderName.

- El componente TClientes lo vamos a vincular con DSPClientes mediante su propiedad ProviderName.

- Debemos hacer doble clic en ambos ClientDataSets y pulsar la combinación de teclas CTRL + A para meter todos los campos.

CONTROLANDO EL NUMERO DE REGISTROS CARGADOS EN MEMORIA

Los componentes ClientDataSet tienen una propiedad llamada PacketRecord la cual determina cuandos registros se van a almacenar en memoria. Por defecto tiene configurado -1 lo que significa que se van a cargar todos los registros en la tabla. Como eso no me interesa en el listado general del formulario principal lo que vamos a hacer es poner esta propiedad a 100.

Por eso he ordenado la lista por el campo ID descendentemente para que se vean sólo los últimos 100 registros insertados. Una de las cosas que más me gustan de los componentes ClientDataSet es que se trae los 100 últimos registros y desconecta la tabla y la transacción quitándole trabajo al motor de bases de datos. Si el usuario que maneja el programa llega hasta el registro número 100 el propio componente conecta automáticamente con el servidor, se trae otros 100 registros y vuelve desconectar.

Lo único en lo que hay que tener cuidado es no acumular demasiados registros en memoria ya que puede relentizar el programa e incluso el sistema operativo si el PC no tiene mucha potencia.

El componente ClientDataSet llamado TClientes lo dejamos como está en -1 ya que sólo lo vamos a utilizar para dar de alta un registro o modificarlo.

ENVIANDO LAS TRANSACCIONES AL SERVIDOR

Cuando se utilizan los clásicos métodos Insert, Append, Post y Delete con los objetos de la clase TClientDataSet el resultado de las operaciones con registros no tiene lugar en la base de datos hasta que envíamos la transacción al servidor con el método ApplyUpdates.

Por ello vamos a utilizar el evento OnAfterPost para enviar la transacción al servidor en el caso que haya sucedido alguna modificación en el registro:

procedure TLogicaNegocio.TClientesAfterPost( DataSet: TDataSet );
begin
if TClientes.ChangeCount > 0 then
begin
TClientes.ApplyUpdates( 0 );
TClientes.Refresh;

if TLstClientes.Active then
TLstClientes.Refresh;
end;
end;

Después de enviar la transacción hemos refrescado la tabla TClientes y también la tabla TLstClientes para que se actualicen los cambios en la rejilla. Por último, cuando en el listado de clientes se elimine un registro también hay que enviar la trasacción al servidor en su evento OnAfterDelete:

procedure TLogicaNegocio.TLstClientesAfterDelete( DataSet: TDataSet );
begin
TLstClientes.ApplyUpdates( 0 );
end;

Con esto ya tenemos controlada la inserción, modificación y eliminación de registros hacia el motor de bases de datos.

ESTABLECIENDO REGLAS DE NEGOCIO EN NUESTRAS TABLAS

Las reglas de negocio definidas en el módulo de datos le quitan mucho trabajo al formulario que esté vinculado con la tabla. En un primer ejemplo vamos a hacer que cuando se demos de alta un cliente su importe pendiente sea cero. Esto se hace en el evento OnNewRecord del componente TClientes:

procedure TLogicaNegocio.TClientesNewRecord( DataSet: TDataSet );
begin
TClientesIMPORTEPTE.AsFloat := 0;
end;

Otra de las malas costumbres que solemos cometer en los programas es controlar los datos que introduce o no el usuario en el registro, cuya lógica la hacemos en el formulario. Esto tiene el inconveniente en que si en otro formulario hay que acceder a la misma tabla hay que volver a controlar las acciones del usuario.

Para evitar esto, tenemos que definir también en la capa de lógica de negocio las reglas sobre las tablas y los campos, lo que se llama comunmente validación de campos. El componente ClientDataSet dispone de la propiedad Constraints donde pueden definirse tantas reglas como queramos. Para verlo con un ejemplo, vamos a hacer que el usuario no pueda guardar el cliente si no ha introducido su nombre.

Para definir una regla en un ClientDataSet hay que hacer lo siguiente (con TClientes):

- Pulsamos el botón [...] en la propiedad Constraints.

- Pulsamos el botón Add New.

- En la propiedad CustomConstraint definimos la condición de error mediante SQL:

NOMBRE IS NOT NULL

- En el campo ErrorMessage del mismo Constraint ponemos el mensaje de error:

No ha introducido el nombre del cliente

Con esta regla definida, si en cualquier parte de nuestro programa hacemos un Post de la tabla clientes y esta vacío el campo NOMBRE el programa lanzará un mensaje de error sin tener que programar nada. Antes teníamos que hacer esto en el botón Aceptar del formulario para validar los campos. Con los Constraints las validadiones las hacemos en sin tener que programar.

Ahora vamos a definir otra regla la cual establece que un cliene no puede tener un importe pendiente superior a 2000 €. Creamos un nuevo Constraint con la propiedad CustomConstrait definida con:

IMPORTEPTE <= 2000

y con la propiedad ErrorMessage que tenga:

El importe pendiente no puede ser superior a 2000 €

Se pueden definir tantas reglas como deseemos. En el próximo artículo vamos a hacer los formularios de mantenimiento de clientes.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

27 septiembre 2007

La potencia de los ClientDataSet (II)

Después de haber creado la base de datos en Firebird 2.0 ya podemos comenzar a crear un nuevo proyecto que maneje dicha información. El objetivo del proyecto es hacer el siguiente mantenimiento de clientes:


CREANDO LA CAPA DE ACCESO A DATOS

La capa de acceso a datos encargada de conectar con Firebird va a incorporar los siguientes componentes:


- Un contenedor DataModule llamado AccesoDatos.

- Un compomente IBDatabase (pestaña Interbase) llamado BaseDatos.

- Un componente IBTransaction (pestaña Interbase) llamado Transaccion.

- Dos componentes IBQuery (pestaña Interbase) llamados LstClientes y Clientes.

Se sopone que la base de datos BASEDATOS.FDB esta al lado de nuestro ejecutable. Vamos a comenzar a configurar cada uno de estos componentes.

Hacemos doble clic sobre el componente IBDatabase y en el campo User Name le ponemos SYSDBA. En en password le ponemos masterkey:


Pulsamos OK, y después en la propiedad DefaultTransaction del componente IBDatabase seleccionamos Transaccion, desactivamos la propiedad LoginPrompt y nos aseguramos que en SQLDialect ponga un 3.

Para el componente IBTransaction llamado Transaccion seleccionaremos en su propiedad DefaultDatabase la base de datos BaseDatos.

Como nuestra intención es tener una rejilla que muestre el listado general de clientes y un formulario para dar de alta clientes, lo que vamos a hacer es crear una tabla IBQuery para la rejilla llamada LstClientes y otra tabla para el formulario del cliente llamada Clientes. Sería absurdo utilizar un mismo mantenimiento para ambos casos ya que si tenemos miles de clientes en la rejilla, el tener cargados en memoria todos los campos del cliente podría relentizar el programa.

En los componentes LstClientes y Clientes vamos a configurar también:

Database: BaseDatos
Transaction: Transaccion
UniDirectional: True

El motivo de activar el campo UniDirectional es para que el cursor SQL trabaje más rápido en el servidor ya que los componentes ClientDataSet gestionan en memoria los registros leidos anteriormente.

Como en la rejilla sólo quiero mostrar los campos ID, NOMBRE y NIF entonces en el componente LstClientes en su propiedad SQL vamos a definir:

SELECT ID,NOMBRE,NIF FROM CLIENTES
ORDER BY ID DESC

y para el componente Clientes:

SELECT * FROM CLIENTES
WHERE ID=:ID

En la condición WHERE de la SQL hemos añadido el parámetro :ID para que se pueda más adelante acceder directamente a un registro en concreto a partir de su campo ID.

Una vez definida nuestra capa de acceso a datos en el siguiente artículo nos encargaremos de definir nuestra lógica de negocio.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

26 septiembre 2007

La potencia de los ClientDataSet (I)

El mundo de la programación de bases de datos cambia tan rápidamente que casi no nos da tiempo a asimilar los nuevos protocolos. Comenzamos con DBase, Clipper, FoxPro, .., hasta hoy en día que tenemos Microsoft SQL Server, Interbase, Firebird, etc.

Luego tenemos el maremagnum de lenguajes de programación donde cada cual se come el acceso a datos a su manera: Java con sus infinitos frameswork tales como hibernate, struts, etc., Ruby con el archiconocido Ruby On Rails que utiliza el paradigma MVC (Modelo, Vista, Controlador), Microsoft a su rollo con ADO y su plataforma Microsoft.NET. Todo eso sin contar con los potentes lenguajes script que no tienen la atención que merecen como son PHP, Ruby, Python, TCL/TK, Groovy, etc. Hasta la mismísima CodeGear nos ha sorprendido con su IDE 3rdRails para programación en Ruby On Rails.

Pero si hay algo que hace que Delphi destaque sobre el resto de entornos de programación es su acceso a múltiples motores de bases de datos utilizando una misma lógica de negocio que abstrae al programador de las rutinas a bajo nivel. En la conocida tecnología de acceso a datos llamada MIDAS.

Las primeras versiones de Delphi contaban con el veterano controlador de bases de datos BDE (Borland Database Engine) que permitía acceder a las clásicas bases de datos DBASE, PARADOX, etc. En la versión 5 de Delphi se incluyeron los componentes IBExpres (IBX) que permitían tener un acceso directo a bases de datos Interbase y Firebird. Fue a partir de Delphi 6 cuando Borland apostó por la tecnología DBExpress, un sistema rápido mediante drivers que utilizando un mismo protocolo permitía conectividad con múltiples motores de bases de datos tales como Interbase, Firebird, Oracle, MySQL, Informix, Microsoft SQL Server, etc. Incluso en su última versión (DBX4) permite acceder a su nuevo motor de bases de datos multiplataforma llamado BlackFish programado integramente en .NET y Java (anteriormente se llamaba JDataStore y estaba programado en Java).

Luego tenemos también otros componentes muy buenos de acceso a datos tales como Zeos, Interbase Objects (IBO), FIBPlus, etc. Y por supuesto el conocido protocolo de acceso a datos de Microsoft llamado ADO, siendo su última versión ADO.NET la que tiene bastantes posibilidades de convertirse en un estandar para todos los entornos Windows.

Pero si hay un componente de acceso a bases de datos en Delphi que destaque sobre todos los demás ese es el ClientDataSet. Combina la facilidad de acceso a datos a través de la clase TDataSet y la potencia de controlar automáticamente las transacciones al motor de bases de datos, las SQL de consulta, actualización y eliminación así como la conexión y desconexión de las tablas con el servidor haciendo que el programador no tenga que preocuparse de las particularidades del motor de bases de datos.

El componente de la clase TClientDataSet no conecta directamente sobre una base de datos en concreto, si no que utiliza el componente DataSetProvider que actua de intermediario haciendo de puente entre los componentes de bases de datos (IBX,IBO,etc) y nuestra tabla ClientDataSet. El componente ClientDataSet es algo así como una tabla de memoria (como la que tienen los componentes RX) que se trae y lleva datos a las tablas de la base de datos encargándose automáticamente de las transacciones.

LA ESTRUCTURA CORRECTA DE UN PROGRAMA

Para crear una buena aplicación con acceso a bases de datos hay que dividir nuestro programa en tres partes principales:

Capa de acceso a datos: se encarga de conectar con un motor de bases de datos en concreto ya sea con componentes IBX, ADO, BDE, etc.

Lógica de negocio: aquí se definen como son nuestras tablas (CLIENTES, ARTICULOS,etc), los campos que contienen así como el comportamiento al dar de alta registros, modificarlos, realización de cálculos internos, etc. Todo esto lo haremos con componentes de la clase TClientDataSet y TDataSetProvider.

Interfaz de usuario: se compone de los formularios, informes y menús de opciones que va a visualizar el usuario y que estarán internamente conectados con los ClientDataSet.

La interfaz de usuario sólo podrá acceder a la lógica de negocio y esta última sólo a la capa de acceso a datos. Así, si en un futuro queremos cambiar la interfaz de usuario (por ejemplo para Windows Vista) o el motor de base de datos no afectaría al resto de las capas del programa.

Para ello vamos a utilizar los componentes contenedores de la clase TDataModule para alojar la lógica de negocio y el acceso a datos. En nuestro ejemplo crearemos una base de datos de clientes definiendo las tres capas.

CREANDO LA BASE DE DATOS

En este ejemplo voy a crear una base de datos de clientes utilizando el motor de bases de datos Firebird 2.0 y con los componentes IBX. Las tablas las voy a crear con el programa IBExpert (http://www.ibexpert.com) cuya versión personal es gratis y muy potente. La base de datos se va a llamar BASEDATOS.FDB, pero si haceis las pruebas con Interbase entonces sería BASEDATOS.GDB. No voy a explicar aquí como funciona el programa IBExpert o IBConsole ya que hay en la red información abundate sobre ambos programas.

Creamos la tabla de clientes:

CREATE TABLE CLIENTES (
ID INTEGER NOT NULL,
NOMBRE VARCHAR(100),
NIF VARCHAR(15),
DIRECCION VARCHAR(100),
POBLACION VARCHAR(50),
CP VARCHAR(5),
PROVINCIA VARCHAR(50),
IMPORTEPTE DOUBLE PRECISION,
PRIMARY KEY (ID)
)

Como quiero que el ID sea autonumérico voy a crear un generador:

CREATE GENERATOR IDCLIENTE

Y un disparador para que cuando demos de alta el registro rellene automáticamente el ID y autoincremente el contador del generador:

CREATE TRIGGER CONTADOR_CLIENTES FOR CLIENTES
ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
NEW.ID = GEN_ID( IDCLIENTE, 1 );
END

NOTA IMPORTANTE: Hay algunas versiones de Delphi 7 que tienen un error en los componentes IBExpress (IBX) que hacen que los componentes ClientDataSet no funcionen correctamente. Recomiendo actualizar los componentes IBX a la versión 7.04 que podeis encontrarla en:

http://codecentral.borland.com/Item.aspx?id=18893

En el siguiente artículo comenzaremos a realizar el programa utilizando esta base de datos.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

25 septiembre 2007

La barra de estado

Es raro encontrar una aplicación que no lleve en alguno de sus formularios la barra de estado. Hasta ahora he utilizado ejemplos sencillos de meter texto en la barra de estado de manera normal, pero el componente StatusBar permite meter múltiples paneles dentro de la barra de estado e incluso podemos cambiar el formato de la fuente en cada uno de ellos.

ESCRIBIENDO TEXTO SIMPLE

El componente de la clase TStatusBar tiene dos estados: uno para escribir texto simple en un sólo panel y otro para escribir en múltiples paneles. Supongamos que el componente de la barra de estado dentro de nuestro formulario de llama BarraEstado. Para escribir en un sólo panel se hace de la siguiente manera:

BarraEstado.SimplePanel := True;
BarraEstado.SimpleText := 'Texto de prueba';

Se puede escribir tanto texto como longitud tenga la barra de estado, o mejor dicho, tanto como sea la longitud del formulario.

ESCRIBIENDO TEXTO EN MULTIPLES PANELES

Para escribir en múltiples paneles dentro de la misma barra de estado hay que crear un panel por cada apartado. En este ejemplo voy a crear en la barra de estado tres paneles y en cada uno de ellos voy a poner un formato diferente.

begin
BarraEstado.SimplePanel := False;
BarraEstado.Panels.Clear;

with BarraEstado.Panels.Add do
begin
Text := 'x=10';
Width := 50;
Style := psOwnerDraw;
Alignment := taRightJustify;
end;

with BarraEstado.Panels.Add do
begin
Text := 'y=50';
Width := 50;
Style := psOwnerDraw;
Alignment := taRightJustify;
end;

with BarraEstado.Panels.Add do
begin
Text := 'Texto seleccionado';
Style := psText;
Width := 50;
end;
end;

La propiedad Style de cada panel determina si es psText o psOwnerDraw. Por defecto todos los paneles que se crean tiene el estilo psText (texto normal). Si elegimos el estilo psOwnerDraw significa que vamos a ser nosotros los encargados de dibujar el contenido del mismo. Ello se hace en el evento OnDrawPanel de la barra de estado:

procedure TFormulario.BarraEstadoDrawPanel( StatusBar: TStatusBar; Panel: TStatusPanel; const Rect: TRect );
begin
case Panel.ID of
0: with BarraEstado.Canvas do
begin
Font.Name := 'Tahoma';
Font.Size := 10;
Font.Style := [fsBold];
Font.Color := clNavy;
TextOut( Rect.Left + 2, Rect.Top, Panel.Text );
end;

1: with BarraEstado.Canvas do
begin
Font.Name := 'Tahoma';
Font.Size := 10;
Font.Style := [fsBold];
Font.Color := clRed;
TextOut( Rect.Left + 2, Rect.Top, Panel.Text );
end;
end;
end;

Cuando se van creando paneles dentro de una barra de estado, a cada uno de ellos se le va asignado la propiedad ID a 0, 1, 2, etc, la cual es de sólo lectura. Como puede verse en el evento OnDrawPanel si el ID es 0 lo pinto de azul y si es 1 de rojo. Pero sólo funcionará en aquellos paneles cuyo estilo sea psOwnerDraw. Quedaría de la siguiente manera:


También puede cambiarse en cada panel propiedades tales como el marco (bevel), la alineación del texto (alignment) y el ancho (width).

Esto nos permitirá informar mejor al usuario sobre el comportamiento de nuestra aplicación en tiempo real y de una manera elegante que no interfiere con el contenido del resto del formulario.

Pruebas realizadas en Delphi 7.

24 septiembre 2007

Creando un navegador con el componente WebBrowser

Delphi incorpora dentro de la pestaña Internet el componente de la clase TWebBrowser el cual utiliza el motor de Internet Explorer para añadir un navegador en nuestras aplicaciones.


Seguro que os preguntareis, ¿que utilidad puede tener esto si ya tengo Internet Explorer, Firefox, Opera, etc.? Pues hay ocasiones en que un cliente nos pide que ciertos usuarios sólo puedan entrar a ciertas páginas web. Por ejemplo, si un usuario está en el departamento de compras, es lógico que a donde sólo tiene que entrar es a las páginas de sus proveedores y no a otras para bajarse música MP3 a todo trapo (no os podeis imaginar la pasta que pierden las empresas por este motivo).

Entonces vamos a ver como crear un pequeño navegador con las funcionalidades mínimas.

CREANDO LA VENTANA DE NAVEGACION

Vamos a crear la siguiente ventana:


Incorpora los siguientes componentes:

- Un panel en la parte superior con su propiedad Align fijada a alTop (pegado arriba).

- Dentro del panel tenemos una etiqueta para la dirección.

- Un componente ComboBox llamado URL para la barra de direcciones.

- Tres botones para ir atrás, adelante y para detener.

- Una barra de progreso para mostrar la carga de la página web.

- Un componente WebBrower ocupando el resto del formulario mediante su propiedad Align en alClient, de tal manera que si maximizamos la ventana se respete el posicionamiento de todos los componentes.

CREANDO LAS FUNCIONES DE NAVEGACION

Una vez hecha la ventana pasemos a crear cada uno de los eventos relacionados con la navegación. Comencemos con el evento de pulsar Intro dentro de la barra de direcciones llamada URL:

procedure TFormulario.URLKeyDown( Sender: TObject; var Key: Word; Shift: TShiftState );
begin
if key = VK_RETURN then
begin
WebBrowser.Navigate( URL.Text );
URL.Items.Add( URL.Text );
end;
end;

Si se pulsa la tecla Intro hacemos el objeto WebBrowser navegue a esa dirección. Además añadimos esa dirección a la lista de direcciones URL por la que hemos navegado para guardar un historial de las mismas.

Cuando se pulse el botón atrás en la barra de navegación hacemos lo siguiente:

procedure TFormulario.BAtrasClick( Sender: TObject );
begin
WebBrowser.GoBack;
end;

Lo mismo cuando se pulse el botón adelante:

procedure TFormulario.BAdelanteClick( Sender: TObject );
begin
WebBrowser.GoForward;
end;

Y también si queremos detener la navegación:

procedure TFormulario.BDetenerClick( Sender: TObject );
begin
WebBrowser.Stop;
end;

Hasta aquí tenemos la parte básica de la navegación. Pasemos ahora a controlar el progreso de la navegación así como que el usuario sólo pueda entrar a una página en concreto.

A la barra de progreso situada a la derecha del botón BDetener la llamaremos Progreso y por defecto estará invisible. Sólo la vamos a hacer aparecer cuando comience la navegación. Eso se hace en el evento OnBeforeNavigate2:

procedure TFormulario.WebBrowserBeforeNavigate2( Sender: TObject;
const pDisp: IDispatch; var URL, Flags, TargetFrameName, PostData,
Headers: OleVariant; var Cancel: WordBool );
begin
if Pos( 'terra', URL ) = 0 then
Cancel := True;

Progreso.Show;
end;

Aquí le hemos dicho al evento que antes de navegar si la URL de la página web no contiene la palabra terra que cancele la navegación. Así evitamos que el usuario se distraiga en otras páginas web.

En el caso de que si pueda navegar entonces mostramos la barra de progreso de la carga de la página web. Para controlar el progreso de la navegación se utiliza el evento OnProgressChange:

procedure TFormulario.WebBrowserProgressChange( Sender: TObject; Progress, ProgressMax: Integer );
begin
Progreso.Max := ProgressMax;
Progreso.Position := Progress;
end;

Cuando termine la navegación debemos ocultar de nuevo la barra de progreso. Eso lo hará en el evento OnDocumentComplete:

procedure TFormulario.WebBrowserDocumentComplete( Sender: TObject;
const pDisp: IDispatch; var URL: OleVariant );
begin
Progreso.Hide;
end;

Con esto ya tenemos un sencillo navegador que sólo accede a donde nosotros le digamos. A partir de aquí podemos ampliarle muchas más características tales como memorizar las URL favoritas, permitir visualizar a pantalla completa (FullScreen) e incluso permitir múltiples ventanas de navegación a través de pestañas (con el componente PageControl).

También se pueden bloquear ventanas emergentes utilizando el evento OnNewWindow2 evitando así las asquerosas ventanas de publicidad:

procedure TFormulario.WebBrowserNewWindow2(Sender: TObject;
var ppDisp: IDispatch; var Cancel: WordBool);
begin
Cancel := True;
end;

Aunque últimamente los publicistas muestran la publicidad en capas mediante hojas de estilo en cascada, teniendo que utilizar otros programas más avanzados para eliminar publicidad.

Pruebas realizadas en Delphi 7.

21 septiembre 2007

Mostrando datos en el componente StringGrid

Anteriormente vimos como mostrar información en un componente ListView llegando incluso a cambiar el color de filas y columnas a nuestro antojo. El único inconveniente estaba en que no se podían cambiar los títulos de las columnas, ya que venían predeterminadas por los colores de Windows.

Pues bien, el componente de la clase TStringGrid es algo más cutre que el ListView, pero permite cambiar al 100% el formato de todas las celdas. Veamos primero como meter información en el mismo. Al igual que ocurría con el ListView, todos las celdas de un componente StringGrid son de tipo string, siendo nosotros los que le tenemos que dar formato a mano.

AÑADIENDO DATOS A LA REJILLA

Vamos a crear una rejilla de datos con las siguiente columnas:

NOMBRE, APELLIDO1, APELLIDO2, NIF, IMPORTE PTE.

Cuando insertamos un componente StringGrid en el formulario nos va a poner por defecto la primera columna con celdas fijas (fixed). Vamos a fijar las siguientes propiedades:

Propiedad Valor Descripción
--------- ----- -----------
ColCount 5 5 columnas
RowCount 4 4 filas
FixedCols 0 0 columnas fijas
FixedRows 1 1 fila fija
DefaultRowHeight 20 altura de las filas a 20 pixels

Ahora creamos un procedimiento para completar de datos la rejilla:

procedure TFormulario.RellenarTabla;
begin
with StringGrid do
begin
// Título de las columnas
Cells[0, 0] := 'NOMBRE';
Cells[1, 0] := 'APELLIDO1';
Cells[2, 0] := 'APELLIDO2';
Cells[3, 0] := 'NIF';
Cells[4, 0] := 'IMPORTE PTE.';

// Datos
Cells[0, 1] := 'PABLO';
Cells[1, 1] := 'GARCIA';
Cells[2, 1] := 'MARTINEZ';
Cells[3, 1] := '67348321D';
Cells[4, 1] := '1500,36';

// Datos
Cells[0, 2] := 'MARIA';
Cells[1, 2] := 'SANCHEZ';
Cells[2, 2] := 'PALAZON';
Cells[3, 2] := '44878234A';
Cells[4, 2] := '635,21';

// Datos
Cells[0, 3] := 'CARMEN';
Cells[1, 3] := 'PEREZ';
Cells[2, 3] := 'GUILLEN';
Cells[3, 3] := '76892693L';
Cells[4, 3] := '211,66';
end;
end;

Al ejecutar el programa puede apreciarse lo mal que quedan los datos en pantalla, sobre todo la columna del importe pendiente:


DANDO FORMATO A LAS CELDAS DE UN COMPONENTE STRINGGRIND

Lo que vamos a hacer a continuación es lo siguiente:

- La primera fila fija va a ser de color de fondo azul oscuro con fuente blanca y además el texto va a ir centrado.

- La columna del importe pendiente va a tener la fuente de color verde y va a ir alineada a la derecha.

- El resto de columnas tendrán el color de fondo blanco y el texto en negro.

Todo esto hay que hacerlo en el evento OnDrawCell del componente StringGrid:

procedure TFormulario.StringGridDrawCell( Sender: TObject; ACol,
ARow: Integer; Rect: TRect; State: TGridDrawState );
var
sTexto: String; // Texto que va a imprimir en la celda actual
Alineacion: TAlignment; // Alineación que le vamos a dar al texto
iAnchoTexto: Integer; // Ancho del texto a imprimir en pixels
begin
with StringGrid.Canvas do
begin
// Lo primero es coger la fuente por defecto que le hemos asignado al componente
Font.Name := StringGrid.Font.Name;
Font.Size := StringGrid.Font.Size;

if ARow = 0 then
Alineacion := taCenter
else
// Si es la columna del importe pendiente alineamos el texto a la derecha
if ACol = 4 then
Alineacion := taRightJustify
else
Alineacion := taLeftJustify;

// ¿Es una celda fija de sólo lectura?
if gdFixed in State then
begin
Brush.Color := clNavy; // le ponemos azul de fondo
Font.Color := clWhite; // fuente blanca
Font.Style := [fsBold]; // y negrita
end
else
begin
// ¿Esta enfocada la celda?
if gdFocused in State then
begin
Brush.Color := clRed; // fondo rojo
Font.Color := clWhite; // fuente blanca
Font.Style := [fsBold]; // y negrita
end
else
begin
// Para el resto de celdas el fondo lo ponemos blanco
Brush.Color := clWindow;

// ¿Es la columna del importe pendiente?
if ACol = 4 then
begin
Font.Color := clGreen; // la pintamos de azul
Font.Style := [fsBold]; // y negrita
Alineacion := taRightJustify;
end
else
begin
Font.Color := clBlack;
Font.Style := [];
end;
end;
end;

sTexto := StringGrid.Cells[ACol,ARow];
FillRect( Rect );
iAnchoTexto := TextWidth( sTexto );

case Alineacion of
taLeftJustify: TextOut( Rect.Left + 5, Rect.Top + 2, sTexto );
taCenter: TextOut( Rect.Left + ( ( Rect.Right - Rect.Left ) - iAnchoTexto ) div 2, Rect.Top + 2, sTexto );
taRightJustify: TextOut( Rect.Right - iAnchoTexto - 2, Rect.Top + 2, sTexto );
end;
end;
end;

Así quedaría al ejecutarlo:


Sólo hay un pequeño inconveniente y es que la rejilla primero se pinta de manera normal y luego nosotros volvemos a pintarla encima con el evento OnDrawCell con lo cual hace el proceso dos veces. Si queremos que sólo se haga una vez hay que poner a False la propiedad DefaultDrawing. Quedaría de la siguiente manera:


Por lo demás creo que este componente que puede sernos muy útil para mostrar datos por pantalla en formato de sólo lectura. En formato de escritura es algo flojo porque habría que controlar que tipos de datos puede escribir el usuario según en que columnas esté.

Pruebas realizadas en Delphi 7.

20 septiembre 2007

Creando aplicaciones MDI

Una aplicación MDI en Windows es aquella que contiene una ventana principal la cual que contiene a su vez ventanas hijas (al estilo Word o Excel). Cada ventana hija no puede salirse del marco de trabajo de la ventana padre aunque si puede minimizarse, maximizarse, etc.


A su vez no todas las ventanas creadas tienen que ser hijas de la ventana padre. Pueden crearse ventanas independientes ya sean normales o modales (una ventana modal es aquella que no permite acceder a ninguna otra hasta que se cierre, como los típicas ventanas de diálogo).

COMO CREAR UNA APLICACION MDI

Aunque el asistente de creación de proyectos de Delphi permite crear aplicaciones MDI, realmente no tiene mucha ciencia. Sólo hay que coger el formulario principal de la aplicación y cambiar su propiedad FormStyle a fsMDIForm. Con sólo hacer eso ya tenemos la ventana padre contenedora.

Para crear una ventana hija hay que hacer lo siguiente:

procedure TFormulario.MDIChildClick( Sender: TObject );
var
Form: TForm;
begin
Form := TForm.Create( Self );
Form.FormStyle := fsMDIChild;
Form.Caption := 'Ventana hija MDI';
Form.OnClose := AlCerrar;
Form.Show;
end;

Como puede apreciarse hemos creado una ventana y le hemos dicho que su estilo es fsMDIChild. También hemos asignado el evento OnClose para cerrar la ventana:

procedure TFormulario.AlCerrar( Sender: TObject; var Action: TCloseAction );
begin
Action := caFree;
end;

Hay que hacer esto porque por defecto cuando se crea una ventana hija y se cierra, realmente no se cierra si no que se minimiza. Por ello he creado el evento AlCerrar en la ventana padre para asignarlo a todas las ventanas hijas.

Si queremos hacer un formulario normal que no esté relacionado con el formulario padre MDI, entonces el procedimiento sería el siguiente:

procedure TFormulario.NormalClick( Sender: TObject );
var
Form: TForm;
begin
Form := TForm.Create( Self );
Form.Caption := 'Ventana normal';
Form.Show;
end;

Cuando se crea un formulario nuevo su propiedad FormStyle es por defecto fsNormal. El crear una ventana modal es prácticamente lo mismo:

procedure TFormulario.ModalClick( Sender: TObject );
var
Form: TForm;
begin
Form := TForm.Create( Self );
Form.Caption := 'Ventana modal';
Form.ShowModal;
end;

Cuando se crea una ventana hija MDI hay que tener presentes un par de cosas:

- Nunca pueden ser invisibles, con lo cual si utilizamos los métodos Show o Hide provocará un error. Igual para la propiedad Visible.

- Tampoco pueden ser modales, para ello utiliza una ventana modal como hemos visto anteriormente.

ORGANIZANDO LAS VENTANAS HIJAS

Una vez creadas las ventanas hijas, la ventana padre puede organizarlas en cascada, mostrarlas todas a la vez, colocarlas horizontalmente, verticalmente e incluso cerrarlas todas.

Por ejemplo, para poner todas las ventanas hijas en cascada hacemos lo siguiente:

procedure TFormulario.CascadaClick( Sender: TObject );
begin
VentanasNormales;
Cascade;
end;


El procedimiento VentanasNormales se encarga de dejar cada una de las ventanas hijas de manera normal por si acaso estaban minimizadas:

procedure TFormulario.VentanasNormales;
var i: Integer;
begin
for i := 0 to MDIChildCount - 1 do
MDIChildren[i].WindowState := wsNormal;
end;

La clase TForm dispone de las propiedades MDIChildCount y MDIChildren para controlar el número de ventanas hijas abiertas así como cada una de ellas.

Aparte de ponerlas en cascada podemos hacer que todas se repartan en pantalla horizontamente:

procedure TFormulario.HorizontalClick( Sender: TObject );
begin
VentanasNormales;
TileMode := tbHorizontal;
Tile;
end;



Y también verticalmente:

procedure TFormulario.VerticalClick( Sender: TObject );
begin
VentanasNormales;
TileMode := tbVertical;
Tile;
end;


Si queremos minimizarlas todas hay que decírselo a cada una de ellas:

procedure TFormulario.MinimizarClick( Sender: TObject );
var i: Integer;
begin
for i := MDIChildCount - 1 downto 0 do
MDIChildren[i].WindowState := wsMinimized;
end;


También ocurre lo mismo si queremos cerrarlas todas:

procedure TFormulario.CerrarClick( Sender: TObject );
var i: Integer;
begin
for i := 0 to MDIChildCount - 1 do
MDIChildren[i].Close;
end;

Las aplicaciones MDI son muy útiles para realizar programas de gestión, procesadores de texto, visualizadores de imágenes, etc.

Pruebas realizadas en Delphi 7.

19 septiembre 2007

Creando un cliente de chat IRC con Indy (y III)

Después de haber visto la parte básica del componente IdIRC pasemos a ver algunas cosas que hay que controlar en un cliente de chat.

QUIEN SALE Y QUIEN ENTRA A UN CANAL

Mientras permanezcamos en un canal entrarán y saldrán usuarios constantemente, lo cual nos obliga a refrescar la lista de usuarios y notificar los cambios en la misma ventana del canal:

Ha entrado el usuario lola33
Ha salido el usuario carlos21

Para controlar los usuarios que entran a un canal utilizaremos el evento OnJoin:

procedure TFormulario.IRCJoin( Sender: TObject; AUser: TIdIRCUser; AChannel: TIdIRCChannel );
begin
if Assigned( AChannel ) and Assigned( AUser ) then
if AChannel.Name = CanalActual.Text then
begin
Canal.Lines.Add( 'Ha entrado el usuario ' + AUser.Nick );

if Usuarios.Items.IndexOf( AUser.Nick ) = -1 then
Usuarios.Items.Add( AUser.Nick );
end;
end;

Después de informar en el canal actual que ha aparecido un nuevo usuario también lo hemos dado de alta en la lista de usuarios, controlando que no se repitan, ya que a veces suele coincidir que se ejecuta el comando NAMES del servidor y a la vez entra un usuario nuevo.

Y cuando un usuario abandona el canal entonces utilizamos el evento OnPart:

procedure TFormulario.IRCPart( Sender: TObject; AUser: TIdIRCUser; AChannel: TIdIRCChannel );
var iUsuario: Integer;
begin
if Assigned( AChannel ) and Assigned( AUser ) then
if AChannel.Name = CanalActual.Text then
begin
Canal.Lines.Add( 'Ha salido el usuario ' + AUser.Nick );
iUsuario := Usuarios.Items.IndexOf( AUser.Nick );

if iUsuario > -1 then
Usuarios.Items.Delete( iUsuario );
end;
end;

La única dificultad que hay que controlar es que si tenemos varias ventanas (una por canal) hay que llevar las notificaciones a la ventana correspondiente.

Los canales de chat tienen dos clases de usuarios: operadores y usuarios normales. Los operadores son usuarios que tienen permisos especiales: pueden hacer operadores a usuarios normales o echar a un usuario de un canal si esta incumpliendo las normas del mismo.

Cuando un operador echa fuera a un usuario entonces nuestro componente IdIRC provoca el evento OnKick:

procedure TFormulario.IRCKick( Sender: TObject; AUser, AVictim: TIdIRCUser; AChannel: TIdIRCChannel );
var iUsuario: Integer;
begin
if Assigned( AChannel ) and Assigned( AUser ) then
if AChannel.Name = CanalActual.Text then
begin
Canal.Lines.Add( 'El usuario ' + Auser.Nick + ' ha expulsado a usuario ' + AVictim.Nick );
iUsuario := Usuarios.Items.IndexOf( AUser.Nick );

if iUsuario > -1 then
Usuarios.Items.Delete( iUsuario );
end;
end;

Este evento nos informa del nombre del operador, el de la victima y del canal de donde ha sido expulsado.

Y por último tenemos el problema de que un usuario puede cambiarse el apodo (nick) en cualquier momento. Para ello utilizaremos el evento OnNickChange para notificarlo:

procedure TFormulario.IRCNickChange( Sender: TObject; AUser: TIdIRCUser; ANewNick: String );
var iUsuario: Integer;
begin
Canal.Lines.Add( 'El usuario ' + AUser.Nick + ' ahora se llama ' + ANewNick );
iUsuario := Usuarios.Items.IndexOf( AUser.Nick );

if iUsuario > -1 then
Usuarios.Items[iUsuario] := ANewNick;
end;

Aparte de notificarlo le hemos cambiado el nombre en nuestra lista de usuarios.

INTERCEPTANDO LOS MENSAJES DEL SISTEMA

A pesar de todos los eventos de los que dispone el componente de la clase TIdIRC también podemos interceptar a pelo los mensajes del sistema a través del evento OnSystem, el cual nos dice el usuario, el código de comando y el contenido del mensaje del sistema. Por ejemplo estos son algunos códigos de comando del servidor:

353 -> Comienzo del comando NAMES
366 -> Fin del comando NAMES
376 -> Mensaje del día
etc.

Para más información hay que ver el documento RFC perteneciente al protocolo IRC que mencione en el artículo anterior. Por ejemplo, para averiguar los datos sobre un usuario en concreto hay que mandarle al servidor el comando:

WHOIS maria

Y el servidor devolverá lo siguiente:

319 - WHOIS - maria is on #amistades
312 - WHOIS - maria is using jupiter2.irc-hispano.org Servidor IRC de LLeida Networks
318 - WHOIS - maria :End of /WHOIS list

Nos esta diciendo que maria está en el canal #amistades y que está conectada utilizando el servidor jupiter2. Si estuviera conectada a más canales también lo diría (así sabemos también sus otras aficiones).

¿Cómo nos comemos todo eso? Pues supongamos que si yo hago doble clic sobre un usario quiero que me devuelva información sobre el mismo en un campo Memo cuyo nombre es DatosUsuario. Lo primero sería procesar el evento OnDblClick en la lista de usuarios:

procedure TFormulario.UsuariosDblClick( Sender: TObject );
begin
if Usuarios.ItemIndex > -1 then
IRC.Raw( 'WHOIS ' + Usuarios.Items[Usuarios.ItemIndex] );
end;

Y ahora en el evento OnSystem del componente IdIRC esperamos a que nos llegue la información:

procedure TFormulario.IRCSystem( Sender: TObject; AUser: TIdIRCUser; ACmdCode: Integer; ACommand, AContent: string );
begin
case ACmdCode of
312, 318, 319: DatosUsuario.Lines.Add( AContent );
end;
end;

Así, estudiando un poco el protocolo IRC y sabiendo algunos comandos podemos hacer más cosas de las que permite el componente IdIRC.

ENVIO Y RECEPCION DE ARCHIVOS POR ACCESO DIRECTO

El protocolo IRC permite establecer el envío y recepción de archivos con conexión punto a punto entre la IP del remitente y la IP del receptor. Esto ya no se suele utilizar hoy en día por las siguiente razones:

- Debido a que casi todas las redes locales están detrás de un router se hace necesario abrir y redireccionar puertos en el mismo, cosa que no todo el mundo sabe hacer.

- En cuanto intentemos establecer conexión por puertos que no sean los estandar los cortafuegos saltarán como liebres pidiendo permiso para conectar. Los novatos siempre dirán NO por si acaso.

- Hay robots automáticos programados en MIRC mediante scripts que no paran de realizar ataques continuamente a conexiones DCC.

Por ello no voy a comentar aquí como utilizarlo (para ello tenemos rapidshare, megaupload y demás hierbas que superan con creces este tipo de conexiones y además son más seguros).

Con esto finalizamos lo más importante del componente IdIRC de la paleta de componentes Indy.

Pruebas realizadas en Delphi 7.

18 septiembre 2007

Creando un cliente de chat IRC con Indy (II)

Una vez establecida la conexión con el servidor y sabiendo de que salas de chat dispone, vamos a entrar a un canal a chatear.

ENTRANDO A UN CANAL

Para entrar a un canal se utiliza el método Join:

IRC.Join( '#amistades' );

Esto viene a ser lo mismo que hacer:

IRC.Raw( 'join #amistades' );

Podemos entrar a tantos canales como deseemos pero para hacer un buen cliente de chat hay que crear un formulario por canal, ya que el protocolo IRC nos permite chatear en múltiples canales simultaneamente.

Una vez se ha enviado el comando JOIN el servidor nos devuelve la lista de usuarios que hay en ese canal. Para recoger dicha lista hay que utilizar el evento OnNames del componente IdIRC, donde volcaremos el contenido de la misma en un ListBox que vamos a llamar Usuarios:

procedure TFormulario.IRCNames( Sender: TObject; AUsers: TIdIRCUsers; AChannel: TIdIRCChannel );
var i: Integer;
begin
if Assigned( AUsers ) then
begin
Usuarios.Clear;

for i := 0 to AUsers.Count - 1 do
Usuarios.Items.Add( AUsers.Items[i].Nick );
end;
end;

Es conveniente que el ListBox este ordenado alfabéticamente activando la propiedad Sorted para no volvernos locos buscando los usuarios por su nombre.

LEYENDO LOS MENSAJES DEL SERVIDOR

Una vez conectados a un canal, el servidor nos mandará dos tipos de mensajes: lo que hablan todos los usuarios en el canal y lo que un usuario en concreto nos esta diciendo. ¿Cómo distinguimos cada cual? Pues para ello el evento OnMessaje nos dice el usuario y canal que nos manda el mensaje:

procedure TFormulario.IRCMessage( Sender: TObject; AUser: TIdIRCUser;
AChannel: TIdIRCChannel; Content: string );
begin
if Assigned( AUser ) then
begin
if Assigned( AChannel ) then
Canal.Lines.Add( AUser.Nick + '> ' + Content )
else
Privado.Lines.Add( AUser.Nick + '> ' + Content );
end;
end;

Como se puede apreciar primero comprobamos si existe el usuario (por si acaso es un mensaje del sistema) y luego si tiene canal metemos el mensaje por el mismo (poniendo el nick delante como MIRC) y si no es asi lo mandamos a una conversación privada. En este caso he enviado el texto de la conversación a un componente RichEdit.

Sería interesante que nuestro cliente de chat dispusiera de un formulario por cada conversación privada. Por ello, sería recomendable que nuestro programa de chat utilizara ventanas MDI para permitir el control absoluto cada una de las salas en las que estamos así como cada una de las conversaciones privadas. En otro artículo escribiré como crear aplicaciones MDI.

Otra característica que lo haría más profesional sería dibujar el nick y el mensaje del usuario con colores distintos como vimos anteriormente en el artículo dedicado al componente RichEdit. Y si además damos al usuario la posibilidad de seleccionar los colores de fondo, texto, nicks y tipo de fuente, el programa se aproximaría al famoso cliente MIRC.

ENVIANDO MENSAJES AL SERVIDOR

Se pueden enviar dos tipos de mensajes: a un canal donde estamos (para el público en general ) o a un usuario en concreto. El método say del componente IdIRC cumple dicho cometido.

Si quiero enviar un mensaje al canal amistades:

IRC.Say( '#amistades', 'hola a todos' );

Y si es para un usuario con el que estamos chateando:

IRC.Say( '#maria', 'hola guapa, que tal?' );

Al fin y al cabo para el servidor todo son canales, tanto canales públicos como usuarios particulares. Tenemos que ser nosotros los que debemos procesar de donde viene el mensaje y a donde deseamos enviarlo. De ahí la necesidad de crear un formulario por canal y otro por conversación privada.

ABANDONANDO UN CANAL Y UN SERVIDOR

Para abandonar un canal se utiliza el método Part:

IRC.Part( '#amistades' );

Si se nos olvida hacer esto y seguimos abriendo canales al final gastaremos mucho ancho de banda, ya que el servidor nos manda todos los mensajes publicos de cada uno de los canales abiertos.

Si queremos desconectar del servidor se utiliza el comando:

IRC.Disconnect;

Esto cancelará la conexión con el servidor cerrando todos los mensajes públicos y privados del mismo. En nuestro programa deberemos cerrar todas las ventanas de conversación abiertas.

Todavía quedan algunos puntos importantes para terminar de crear un cliente de chat, tales como saber si un usuario entra o abandona el canal donde estamos.

Pruebas realizadas en Delphi 7.

Publicidad