08 mayo 2009

Los Hilos de Ejecución (1)

Hace tiempo escribí un pequeño post sobre como crear hilos de ejecución:

Cómo crear un hilo de ejecución

Pero debido a que últimamente los procesadores multinúcleo van tomando más relevancia y que algunos lectores de este blog han pedido que me extienda en el tema, voy a meterme de lleno en el apasionante y peligroso mundo de las aplicaciones multihilo.

Cuando manejamos Windows creemos que es un sistema operativo robusto que puede ejecutar gran cantidad de tareas a la vez. Eso es lo que parece externamente, pero realmente sólo tiene dos hilos de ejecución en los últimos procesadores de doble núcleo. Y en procesadores Pentium 4 e inferiores sólo contiene un solo hilo de ejecución (dos mediante el disimulado HyperThreading que tantos problemas nos dio con Interbase 6).

Entonces, ¿Cómo puede ejecutar varias tareas a la vez? (Emule, Bittorrent, MSN, reproductor multimedia, etc.). Primero tenemos que ver lo que significa un proceso.

LOS PROCESOS

Un proceso (un programa EXE o un servicio de Windows) contiene como mínimo un hilo, llamado hilo primario. Si queremos que nuestro proceso tenga más hilos, se lo tenemos que pedir al sistema operativo. Estos nuevos hilos pueden ejecutarse paralelamente al hilo primario y ser independientes.

Un proceso se compone principalmente de estas tres áreas:

AREA DE CÓDIGO: Es una zona de memoria de sólo lectura donde está compilado nuestro programa en código objeto (binario).

HEAP (MONTÓN): Es en esta zona de memoria de lectura/escritura donde se suelen guardar las variables globales que creamos así como las instancias de los objetos que se van creando en memoria.

PILA: Aquí se guardan temporalmente los parámetros que se pasan entre funciones así como las direcciones de llamada y retorno de procedimientos y funciones. Todos los datos que se introducen en la pila tienen que salir mediante el método LIFO (lo último que entra es lo primero que tiene que salir) porque si no provoca un desbordamiento del puntero del procesador que hace que retorne a otra zona de memoria provocando el error Access Violation 000000 o FFFFFF, es decir, intenta salirse del segmento de memoria de nuestra aplicación.

Estas tres zonas se encuentran herméticamente separadas las unas de las otras y si por error un comando de nuestro programa intenta acceder fuera del heap o de la pila provocará el conocido mensaje que tanto nos gusta: Access Violation. Suele ocurrir al intentar acceder a un objeto que no ha sido creado en el heap (dirección 000000 = nil), etc.

El conjunto de estas tres zonas de memoria es el proceso. Ahora bien, si creamos un nuevo hilo de ejecución dentro del proceso, éste tendrá su propia pila, aunque compartirá la misma zona de datos (heap).

EL PROCESO MULTITAREA

Windows realiza la multitarea cediendo un pequeño tiempo de procesador a cada proceso, de modo que en un solo segundo pueden ejecutarse ciertos de procesos simultáneos. Para los viejos roqueros que estudiamos ensamblador vimos que antes de cambiar de tarea Windows guarda el estado de todos los registros en la pila:

PUSH EAX
PUSH EBX
...

Para luego restaurarlos y seguir su marcha:

...
POP EBX
POP EAX

No está de más adquirir unos pequeños conocimientos de ensamblados de x86 para conocer a fondo las tripas de la máquina.

Cuando Windows para el control de un proceso, memoriza el estado de los registros del procesador y la pila y pasa al siguiente proceso hasta que finalice su tiempo. Realmente, este cambio de procesos no lo realiza Windows, sino el mismo procesador que trae esta característica por hardware (desde el 80386. Hasta el procesador 80286 tenía este modo multitarea aunque sólo en 16 bits).

Pero lo que vamos a ver es este artículo es un cambio de ejecución entre hilos del mismo proceso, algo que consume muchos menos recursos que el cambio entre procesos.

Por ejemplo, los navegadores webs modernos actuales como Firefox o Chrome ejecutan cada web dentro del mismo proceso pero en hilos diferentes (incluso Chrome lo hace en distintos procesos), de modo que si una página web se queda colgada no afecta a otra situada en la siguiente pestaña (lo que no se es si llamarán al sistema de hilos de Windows y tendrán su propio núcleo duro de ejecución, su propio sistema multitarea).

Pero programar hilos de ejecución no está exento de problemas. ¿Qué pasaría si dos hilos acceden a la vez a la misma variable de memoria? O peor aún, ¿Y si intentan escribir a la vez en una unidad de CD-ROM? Las excepciones que pueden ocurrir pueden ser catastróficas, aunque afortunadamente a partir de Windows NT y XP se controlan muy bien todos estos problemas de concurrencia.

Lo que si es realmente difícil es intentar depurar dos hilos de ejecución que utilizan los mismos recursos del proceso. De ahí a que sólo hay que recurrir a los hilos en casos estrictamente necesarios.

LA CLASE TTHREAD

Toda la complejidad de los hilos de ejecución que se programan en la API de Windows quedan encapsulados en Delphi en esta sencilla clase. Para crear un hilo de ejecución basta heredar de la clase TThread y sobrecargar el método Execute:

type
THilo = class(TThread)
procedure Execute; override;
end;

En la implementación del procedimiento Execute es donde hay que introducir el código que queremos ejecutar cuando arranque nuestro hilo de ejecución:

{ THilo }

procedure THilo.Execute;
begin
inherited;

end;

Veamos un ejemplo sencillo de un hilo de ejecución con un contador de 10 segundos.

CREANDO UN CONTADOR DE SEGUNDOS

Voy a crear un nuevo proyecto con un formulario en el que sólo va a haber una etiqueta (TLabel) llamada EContador con el contador de segundos:


A este formulario lo he llamado FPrincipal. En la interfaz del mismo voy a definir la clase TContador que va a heredar de un hilo TThread:

type
TContador = class(TThread)
dwTiempo: DWord;
iSegundos: Integer;
Etiqueta: TLabel;
constructor Create; reintroduce; overload;
procedure Execute; override;
end;

He sobrecargado el constructor para que inicialice mis contadores de tiempo y segundos:

constructor TContador.Create;
begin
inherited Create(True); // llamamos al constructor del padre (TThread)
dwTiempo := GetTickCount;
iSegundos := 0;
end;

La función GetTickCount nos devuelve un número entero que representa el número de milisegundos que han pasado desde que encendimos nuestro PC. Si queréis más precisión podéis utilizar la función TimeGetTime declarada en la unidad MMSystem (sobre todo si vais a programar videojuegos).

Siguiendo con la implementación de nuestra clase TContador, la variable dwTiempo la voy a utilizar para controlar el número de milisegundos que van pasando desde que ejecutamos el hilo. Y la variable iSegundos es un contador de segundos de 0 a 10. También he añadido una referencia a una Etiqueta de tipo TLabel que se la daremos al crear una instancia del hilo en el evento OnCreate del formulario principal:

procedure TFPrincipal.FormCreate(Sender: TObject);
begin
Contador := TContador.Create(True);
Contador.Etiqueta := EContador;
Contador.FreeOnTerminate := True;
Contador.Resume;
end;

Después de crear el hilo le pasamos la etiqueta que tiene que actualizar y le decimos mediante la propiedad FreeOnTerminate que se libere de memoria automáticamente al terminar la ejecución del hilo.

Después llamamos al método Resume que lo que hace internamente es ejecutar el procedimiento Execute de la clase TThread que tendrá este código:

procedure TContador.Execute;
begin
inherited;

// Contamos hasta 10 segundos
while iSegundos < 10 do
// ¿Han pasado 1000 milisegundos?
if GetTickCount - dwTiempo > 1000 then
begin
// Incrementamos el contador de segundos y actualizamos la etiqueta
Inc(iSegundos);
Etiqueta.Caption := IntToStr(iSegundos);
dwTiempo := GetTickCount;
end;
end;

Creamos un bucle cerrado que va contando de 1000 en 1000 milisegundos e incrementando el contador de segundos. Cuando se salga de este bucle se terminará la ejecución del hilo automáticamente. También se podía haber utilizado procedimiento Sleep, pero nunca me ha gustado mucho esta función porque miestras se está ejecutando no podemos hacer absolutamente nada.

Eso ocurrirá al ejecutar el programa y cuando llegue el contador a 10:


Podemos ver como va la ejecución del hilo en la ventana inferior de Delphi si estamos ejecutando el programa en modo depuración:


Como puede verse en las líneas en rojo, el hilo que hemos creado tiene el ID 3736 que no tiene nada que ver con el hilo primario:


En el siguiente artículo seguiremos profundizando un poco más en los hilos de ejecución a través de otros ejemplos.

Pruebas realizadas en RAD Studio 2007.

17 abril 2009

Crea tu propio editor de informes (y V)

Voy a cerrar esta serie de artículos dedicada a la creación de nuestro propio editor de informes añadiendo la posibilidad de guardar y cargar los informes que hemos diseñado.

Hay diversas maneras de poder hacerlo: en bases de datos, en archivos de texto, en archivos binarios (con records) y también en XML.

Voy a elegir el lenguaje XML porque es más flexible para futuras ampliaciones.

GUARDANDO TODO EL INFORME EN XML

Para poder guardar y cargar informes necesitamos añadir en el formulario principal el componente de la clase TXMLDocument que se encuentra en la sección Internet:


Le vamos a poner de nombre XML para simplificar y ponemos su propiedad Active a True. También vamos a necesitar el componente TSaveDialog que se encuentra en la sección Dialogs. A este componente lo he llamado GuardarXML.

Vamos a ver en partes como sería guardar todo el documento en XML. Lo primero que hago es comprobar si hay algún informe abierto (una ventana hija MDI de la clase TFInforme) y si no es así saco una advertencia:

procedure TFPrincipal.GuardarComoClick(Sender: TObject);
var
NInforme, NConfig, NBanda, NComponente: IXMLNode; // nodos del XML
FBuscaInforme: TFInforme;
i, j: Integer;
Banda: TQRBand;
Etiqueta: TQRLabel;
Figura: TQRShape;
Campo: TQRDBText;
begin
if ActiveMDIChild is TFInforme then
FBuscaInforme := ActiveMDIChild as TFInforme
else
begin
Application.MessageBox('No hay ningún informe para guardar.',
'Proceso cancelado', MB_ICONSTOP);
Exit;
end;

Después limpio todo el componente XML (por si grabamos por segunda vez) y creo un primer nodo llamado Informe del que van a colgar todos los demás. También guardo un nodo de configuración donde meto la conexión a la base de datos y la tabla:

XML.ChildNodes.Clear;

// Creamos el nodo principal
NInforme := XML.AddChild('Informe');

// Añadimos el nodo de configuración
NConfig := NInforme.AddChild('Configuracion');
NConfig.Attributes['BaseDatos'] := FBuscaInforme.BaseDatos.DatabaseName;
NConfig.Attributes['Tabla'] := FBuscaInforme.Tabla.TableName;

Ahora viene la parte más pesada de este procedimiento. Tengo que recorrer todos los componentes del informe en busca de una banda y cuando la encuentra la grabo en el XML:

// Recorremos todos los componentes del informe y los vamos guardando
for i := 0 to FBuscaInforme.Informe.ComponentCount-1 do
begin
// ¿Hemos encontrado una banda?
if FBuscaInforme.Informe.Components[i] is TQRBand then
begin
Banda := FBuscaInforme.Informe.Components[i] as TQRBand;
NBanda := NInforme.AddChild('Banda');
NBanda.Text := Banda.Name;
NBanda.Attributes['Alto'] := Banda.Height;
NBanda.Attributes['Tipo'] := Ord(Banda.BandType);

Después vuelvo a recorrer con otro bucle todos los componentes pertenecientes a esa banda y los voy guardando:

// Recorremos todos los componentes de la banda
for j := 0 to Banda.ComponentCount-1 do
begin
// ¿Es una etiqueta?
if Banda.Components[j] is TQRLabel then
begin
Etiqueta := Banda.Components[j] as TQRLabel;
NComponente := NBanda.AddChild('Etiqueta');
NComponente.Attributes['Nombre'] := Etiqueta.Name;
NComponente.Attributes['ID'] := Etiqueta.Tag;
NComponente.Attributes['x'] := Etiqueta.Left;
NComponente.Attributes['y'] := Etiqueta.Top;
NComponente.Attributes['Ancho'] := Etiqueta.Width;
NComponente.Attributes['Alto'] := Etiqueta.Height;
NComponente.Attributes['Texto'] := Etiqueta.Caption;
end;

// ¿Es una figura?
if Banda.Components[j] is TQRShape then
begin
Figura := Banda.Components[j] as TQRShape;
NComponente := NBanda.AddChild('Figura');
NComponente.Attributes['Nombre'] := Figura.Name;
NComponente.Attributes['ID'] := Figura.Tag;
NComponente.Attributes['x'] := Figura.Left;
NComponente.Attributes['y'] := Figura.Top;
NComponente.Attributes['Ancho'] := Figura.Width;
NComponente.Attributes['Alto'] := Figura.Height;
end;

// ¿Es un campo?
if Banda.Components[j] is TQRDBText then
begin
Campo := Banda.Components[j] as TQRDBText;
NComponente := NBanda.AddChild('Campo');
NComponente.Attributes['Nombre'] := Campo.Name;
NComponente.Attributes['ID'] := Campo.Tag;
NComponente.Attributes['x'] := Campo.Left;
NComponente.Attributes['y'] := Campo.Top;
NComponente.Attributes['Ancho'] := Campo.Width;
NComponente.Attributes['Alto'] := Campo.Height;
NComponente.Attributes['Campo'] := Campo.DataField;
end;
end;
end;
end;

Cuando termine vuelco todo el XML a un archivo preguntándole el nombre al usuario:

if GuardarXML.Execute then
XML.SaveToFile(GuardarXML.FileName);

Así quedaría después de guardarlo:


Como puede apreciarse en la imagen (hacer clic para ampliar), la libertad que nos da un archivo XML para guardar una información jerárquica es mucho más flexible que con cualquier otro tipo de archivo.

CARGARDO EL INFORME DESDE UN ARCHIVO XML

Para cargar el informe vamos a necesitar el componente TOpenDialog en el formulario principal y que llamaremos AbrirXML. Veamos paso a paso el proceso de carga.

Abrimos el documento XML y comenzamos a recorrer todos los nodos en busca del archivo de configuración:

procedure TFPrincipal.AbrirClick(Sender: TObject);
var
FNuevoInforme: TFInforme;
i, j: Integer;
NBanda, NComponente: IXMLNode; // nodos del XML
Etiqueta: TQRLabel;
Figura: TQRShape;
Campo: TQRDBText;
begin
if not AbrirXML.Execute then
Exit;

XML.LoadFromFile(AbrirXML.FileName);

// Creamos un nuevo informe
FNuevoInforme := TFInforme.Create(Self);

// Recorremos todos los nodos del XML
for i := 0 to XML.ChildNodes[0].ChildNodes.Count-1 do
begin
// ¿Es el archivo de configuración?
if XML.ChildNodes[0].ChildNodes[i].NodeName = 'Configuracion' then
begin
FNuevoInforme.BaseDatos.DatabaseName :=
XML.ChildNodes[0].ChildNodes[i].Attributes['BaseDatos'];

FNuevoInforme.Tabla.TableName :=
XML.ChildNodes[0].ChildNodes[i].Attributes['Tabla'];
end;

He creado el formulario hijo FNuevoInforme de la clase TFInforme para ir creando en tiempo real los componentes que vamos leyendo del XML. Como podéis ver en el código, parto directamente del primer nodo XML.ChildNodes[0] ya que se con seguridad que el primero nodo se llama Informe y que todo lo demás cuelga del mismo.

Después vamos en busca de la banda y la creamos:

// ¿Es una banda?
if XML.ChildNodes[0].ChildNodes[i].NodeName = 'Banda' then
begin
NBanda := XML.ChildNodes[0].ChildNodes[i];
FNuevoInforme.BandaActual := TQRBand.Create(FNuevoInforme.Informe);

with FNuevoInforme.BandaActual do
begin
Parent := FNuevoInforme.Informe;
Height := NBanda.Attributes['Alto'];
BandType := NBanda.Attributes['Tipo'];
end;

Luego voy recorriendo todos los componentes de la banda y los voy metiendo en el informe con sus respectivas propiedades:

// Recorremos todos sus componentes
for j := 0 to NBanda.ChildNodes.Count-1 do
begin
// ¿Es una etiqueta?
if NBanda.ChildNodes[j].NodeName = 'Etiqueta' then
begin
NComponente := NBanda.ChildNodes[j];
Etiqueta := TQRLabel.Create(FNuevoInforme.BandaActual);
Etiqueta.Parent := FNuevoInforme.BandaActual;
Etiqueta.Name := NComponente.Attributes['Nombre'];
Etiqueta.Tag := NComponente.Attributes['ID'];
Etiqueta.Left := NComponente.Attributes['x'];
Etiqueta.Top := NComponente.Attributes['y'];
Etiqueta.Width := NComponente.Attributes['Ancho'];
Etiqueta.Height := NComponente.Attributes['Alto'];
Etiqueta.Caption := NComponente.Attributes['Texto'];
end;

// ¿Es una figura?
if NBanda.ChildNodes[j].NodeName = 'Figura' then
begin
NComponente := NBanda.ChildNodes[j];
Figura := TQRShape.Create(FNuevoInforme.BandaActual);
Figura.Parent := FNuevoInforme.BandaActual;
Figura.Name := NComponente.Attributes['Nombre'];
Figura.Tag := NComponente.Attributes['ID'];
Figura.Left := NComponente.Attributes['x'];
Figura.Top := NComponente.Attributes['y'];
Figura.Width := NComponente.Attributes['Ancho'];
Figura.Height := NComponente.Attributes['Alto'];
end;

// ¿Es una campo?
if NBanda.ChildNodes[j].NodeName = 'Campo' then
begin
NComponente := NBanda.ChildNodes[j];
Campo := TQRDBText.Create(FNuevoInforme.BandaActual);
Campo.Autosize := False;
Campo.Parent := FNuevoInforme.BandaActual;
Campo.Tag := NComponente.Attributes['ID'];
Campo.DataField := NComponente.Attributes['Campo'];
Campo.Left := NComponente.Attributes['x'];
Campo.Top := NComponente.Attributes['y'];
Campo.Width := NComponente.Attributes['Ancho'];
Campo.Height := NComponente.Attributes['Alto'];
Campo.Name := NComponente.Attributes['Nombre'];

if Campo.DataField <> '' then
Campo.Caption := Campo.DataField
else
Campo.Caption := NComponente.Attributes['Nombre'];

Campo.DataSet := FNuevoInforme.Tabla;
end;
end;
end;
end;

Por último, seleccionamos la banda actual:

FNuevoInforme.SeleccionarBandaActual;

Al final tiene que quedarse igual que el informe original:


Me gustaría haber dejado el editor algo más pulido con características tales como la opción de Guardar (creando un procedimiento común para Guardar y Guardar como), poder seleccionar el tipo de banda, o añadir otros componentes de QuickReport. Pero por falta de tiempo no ha podido ser.

De todas formas, creo que con este material podéis ver lo duro que tiene que ser crear un editor de informes profesional y si alguien se anima a continuar a partir de aquí pues mucha suerte y paciencia. Lo que más se aprende con estos temas es a manipular componentes VCL en tiempo real y a crear nuestro propio editor visual.

DESCARGAR EL PROYECTO

Rapidshare:

http://rapidshare.com/files/223134561/EditorInformes5Final_DelphiAlLimite.zip.html

Megaupload:

http://www.megaupload.com/?d=3MUEMUUD



Pruebas realizadas en RAD Studio 2007.

03 abril 2009

Crea tu propio editor de informes (IV)

En el artículo de hoy voy a añadir al editor la funcionalidad de añadir conectarse a una base de datos Interbase/Firebird para añadir los campos de las tablas.

AÑADIENDO MÁS COMPONENTES AL INFORME

Para poder conectar con la base de datos necesitamos añadir tres componentes al formulario FInforme:

1º Un componente TIBDatabase que se encuentra en la sección Interbase y que vamos a llamar BaseDatos:


Modificamos su propiedad LoginPrompt a False y no hay que olvidarse de darle el usuario SYSDBA y la contraseña masterkey al hacer doble clic sobre el mismo.

2º Un componente TIBTransaction que también se encuentra en Interbase y le ponemos el nombre Transaccion:


También debemos vincular la transacción a la base de datos mediante la propiedad DefaultTransaction.

3º Y un componente de la clase TIBTable para traernos los campos de la tabla al hacer la vista previa.

CREANDO EL FORMULARIO DE CONFIGURACION

Creamos un nuevo formulario llamado FConfiguracion con lo siguiente:


Es un formulario de tipo diálogo que contiene una etiqueta, un componente TEdit llamado Ruta y un botón llamado BExaminar que al pulsarlo utiliza al componente TOpenDialog para elegir una base de datos local:

procedure TFConfiguracion.BExaminarClick(Sender: TObject);
begin
Abrir.Filter := 'Interbase/Firebird|*.gdb;*.fdb';

if Abrir.Execute then
Ruta.Text := Abrir.FileName;
end;

Si la base de datos fuera remota el botón Examinar no sirve de nada ya que la ruta sería una unidad real del servidor.

Para poder realizar un test de la conexión lo que hacemos es pasarle a este formulario la base de datos con la que vamos a conectar. Para ello creamos esta variable pública:

public
{ Public declarations }
BaseDatos: TIBDatabase;

Para poder compilar este componente debemos añadir a la sección uses la unidad IBDatabase. Entonces en el botón de comprobar la conexión hacemos esto:

procedure TFConfiguracion.BConectarClick(Sender: TObject);
begin
if BaseDatos.Connected then
BaseDatos.Close;

BaseDatos.DatabaseName := IP.Text + ':' + Ruta.Text;

try
BaseDatos.Open;
except
raise;
end;

Application.MessageBox( 'Conexión realizada correctamente.',
'Conectado', MB_ICONINFORMATION );
end;

Ahora nos vamos al formulario FInforme y añadimos una nueva opción al menú contextual:


Al pinchar sobre esta opción abrimos el formulario de configuración:

procedure TFInforme.ConfiguracionClick(Sender: TObject);
begin
Application.CreateForm( TFConfiguracion, FConfiguracion );
FConfiguracion.BaseDatos := BaseDatos;
FConfiguracion.ShowModal;
end;

Una vez conectados a la base de datos ya podemos crear añadir campos de bases de datos TQRDBText.

AÑADIENDO LOS COMPONENTES TDBTEXT

Aquí no voy a enrollarme mucho ya que el modo insertar los campos TQRDBText va a ser el mismo que he utilizado para los componentes TQRLabel o TQRShape.

Lo primero que he hecho es añadir la opción Campo a nuestro menú contextual:


Cuya implementación es la siguiente:

procedure TFInforme.CampoClick(Sender: TObject);
begin
if BandaActual = nil then
begin
Application.MessageBox( 'Debe crear una banda.',
'Acceso denegado', MB_ICONSTOP );
Exit;
end;

QuitarSeleccionados;
with TQRDBText.Create(BandaActual) do
begin
Parent := BandaActual;
Left := 10;
Top := 10;
Inc(UltimoID);
Tag := UltimoID;
Caption := 'Campo' + IntToStr(UltimoID);
Name := Caption;
end;
end;

Después he modificado el procedimiento SeleccionarComponente:

procedure TFInforme.SeleccionarComponente;

...

// ¿Ha pinchado una etiqueta, una figura o un campo?
if (Seleccionado is TQRLabel) or (Seleccionado is TQRShape) or
(Seleccionado is TQRDBText) then

...


Luego he modificado el procedimiento MoverRatonPulsado:

procedure TFInforme.MoverRatonPulsado;

...

// ¿Estamos moviendo una etiqueta, una figura o un campo?
if (Seleccionado is TQRLabel) or (Seleccionado is TQRShape) or
(Seleccionado is TQRDBText) then
begin

...

También he ampliado los procedimientos ComprobarSeleccionComponentes, MoverOtrosSeleccionados, AmpliarComponenes, etc., es decir, en todo lo referente a la selección y movimiento de componentes (ya lo veréis en el proyecto).

CONFIGURANDO LA TABLA Y EL CAMPO

Una vez insertados los campos en el informe debemos dar la posibilidad al usuario de especificar el nombre del campo y de la tabla al que pertenece. Para ello voy a crear un nuevo formulario llamado FCampo con lo siguiente:


Para poder traernos los campos y tablas de la base de datos le he creado estas tres variables públicas:

public
{ Public declarations }
Campo: TQRDBText;
Tabla: TIBTable;
BaseDatos: TIBDatabase;

Estas variables nos las va a pasar el formulario FInforme cuando el usuario haga clic sobre un campo con la tecla SHIFT pulsada. He intentado asignarle un menú contextual a los objeto TQRDBText pero como no tienen la propiedad Popup no hay manera.

Así que en el procedimiento SeleccionarComponente compruebo si el usuario mantienen el botón SHIFT pulsado para abrir el formulario FCampo:

if (Seleccionado is TQRDBText) and
(HiWord(GetAsyncKeyState(VK_LSHIFT)) <> 0) then
ConfigurarCampo(Seleccionado as TQRDBText);

El procedimiento ConfigurarCampo le envía al formulario FCampo lo que necesita para trabajar:

procedure TFInforme.ConfigurarCampo(Campo: TQRDBText);
begin
// si no está conectado con la base de datos no hacemos nada
if not BaseDatos.Connected then
begin
Application.MessageBox('Conecte primero con la base de datos.',
'Acceso denegado', MB_ICONSTOP);
Exit;
end;

Application.CreateForm(TFCampo, FCampo);
FCampo.BaseDatos := BaseDatos;
FCampo.Campo := Campo;
FCampo.Tabla := Tabla;
FCampo.ShowModal;
end;

También comprueba si previamente hemos conectado con la base de datos. Dentro del formulario FCampo utilizo el evento OnShow para pedirle a la base de datos el nombre de las tablas:

procedure TFCampo.FormShow(Sender: TObject);
begin
// Nos traemos el nombre de todas las tablas
BaseDatos.GetTableNames(SelecTabla.Items, False);
end;

Cuando el usuario seleccione una tabla activando con el ComboBox SelecTabla el evento OnChange entonces vuelo a pedirle a la base de datos el nombre de los campos de la tabla seleccionada:

procedure TFCampo.SelecTablaChange(Sender: TObject);
begin
// Si no ha seleccionado ninguna tabla no hacemos nada
if SelecTabla.ItemIndex = -1 then
Exit;

// Le pedimos a la base de datos que nos de los campos de la tabla
BaseDatos.GetFieldNames(SelecTabla.Text, SelecCampo.Items);
end;

Una vez pulsamos el botón Aceptar le envío al objeto TQRDBText el campo y la tabla a la que pertenece:

procedure TFCampo.BAceptarClick(Sender: TObject);
begin
if SelecTabla.ItemIndex = -1 then
begin
Application.MessageBox('Debe seleccionar una tabla.',
'Atención', MB_ICONSTOP);
Exit;
end;

if SelecCampo.ItemIndex = -1 then
begin
Application.MessageBox('Debe seleccionar un campo.',
'Atención', MB_ICONSTOP);
Exit;
end;

Campo.DataField := SelecCampo.Text;
Tabla.TableName := SelecTabla.Text;
Campo.DataSet := Tabla;
Close;
end;

MODIFICANDO LA VISTA PREVIA

Cuando hagamos una vista previa del documento entonces debemos abrir la tabla de la base de datos antes de imprimir:

procedure TFInforme.VistaPreviaClick(Sender: TObject);
begin
// ¿Ha elegido el usuario una tabla?
if Tabla.TableName <> '' then
begin
// Entonces se la asignamos al informe
Informe.DataSet := Tabla;
Tabla.Close;
Tabla.Open;
end;

Informe.Preview;
end;

CAMBIAR EL NOMBRE DE LAS ETIQUETAS

Vamos a hacer una pequeña modificación más para poder cambiar el nombre de las etiquetas. Para ello volvemos a modificar el procedimiento SeleccionarComponente para que nos pida el nombre de la etiqueta al pincharla con el botón SHIFT pulsado:

if (Seleccionado is TQRLabel) and (HiWord(GetAsyncKeyState(VK_LSHIFT)) <> 0) then
begin
EtiSel := Seleccionado as TQRLabel;
sNombre := InputBox('Cambiar nombre etiqueta', 'Nombre:', '');
if sNombre <> '' then
EtiSel.Caption := sNombre;
end;

Con todo esto que he montado se puede hacer un pequeño informe a una banda con la ficha de un cliente:


Esta sería su vista previa:



EL PROYECTO

Aquí va el proyecto comprimido en RapidShare y Megaupload:

http://rapidshare.com/files/216858597/EditorInformes4_DelphiAlLimite.rar.html

http://www.megaupload.com/?d=AKDT45NN

También he incluido una base de datos creada con Firebird 2.0 con la tabla de CLIENTES. En el próximo artículo voy a finalizar esta serie dedicada a nuestro propio editor de informes añadiendo la posibilidad de grabar y cargar plantillas de informes.

Pruebas realizadas en RAD Studio 2007.

13 marzo 2009

Crea tu propio editor de informes (III)

Hoy vamos a perfeccionar la función de mover varios componentes seleccionados a la vez. También veremos como añadir figuras gráficas al informe y como cambiar sus dimensiones.

Al igual que en artículos anteriores daré al final del mismo el enlace para descargar todo el proyecto.

MOVIENDO VARIOS COMPONENTES CON EL RATÓN

Una vez hemos conseguido mover dos o más componentes a la vez mediante el teclado tenemos que hacer lo mismo con el ratón, ya que si tenemos tres etiquetas seleccionadas e intentamos moverlas (cogiendo una) veremos que sólo se mueve esta última.

Para ello hay que modificar la primera parte del procedimiento MoverRatonPulsado:

procedure TFInforme.MoverRatonPulsado;
var
dx, dy, xAnterior, yAnterior: Integer;
Seleccion: TShape;
begin
// ¿Ha movido el ratón estándo el botón izquierdo pulsado?
if ( ( x <> Cursor.X ) or ( y <> Cursor.Y ) ) and ( Seleccionado <> nil ) then
begin
// ¿Estamos moviendo una etiqueta o una banda?
if (Seleccionado is TQRLabel) or (Seleccionado is TQRShape) then
begin
// Movemos el componente seleccionado
dx := Cursor.X - x;
dy := Cursor.Y - y;
xAnterior := Seleccionado.Left;
yAnterior := Seleccionado.Top;
Seleccionado.Left := xInicial + dx;
Seleccionado.Top := yInicial + dy;

// Movemos la selección
Seleccion := BuscarSeleccion(Seleccionado.Tag);
if Seleccion <> nil then
begin
Seleccion.Left := xInicial+dx-1;
Seleccion.Top := yInicial+dy-1;
end;

MoverOtrosSeleccionados(Seleccionado.Tag, Seleccionado.Left,
Seleccionado.Top, xAnterior, yAnterior);
end;

...


Sólo he añadido tres cosas:

1º He añadido al principio del procedimiento las variables xAnterior e yAnterior que van a encargarse de guardar la coordenada original de la etiqueta que estoy movimendo.

2º Antes de mover la etiqueta guardo sus coordenadas originales:

xAnterior := Seleccionado.Left;
yAnterior := Seleccionado.Top;
Seleccionado.Left := xInicial + dx;
Seleccionado.Top := yInicial + dy;

3º Llamo al procedimiento MoverOtrosSeleccionados que se encargará de arrastrar el resto de etiquetas seleccionadas:

MoverOtrosSeleccionados(Seleccionado.Tag, Seleccionado.Left,
Seleccionado.Top, xAnterior, yAnterior);

Le paso como parámetro el ID de la etiqueta actual (para excluirla), la nueva posición y la anterior. Este sería el procedimiento al que llamamos:

procedure TFInforme.MoverOtrosSeleccionados( ID, x, y, xAnterior, yAnterior: Integer );
var
i, dx2, dy2: Integer;
Etiqueta: TQRLabel;
Seleccion: TShape;
begin
for i := 0 to BandaActual.ComponentCount - 1 do
begin
// ¿Es una etiqueta?
if (BandaActual.Components[i] is TQRLabel) then
begin
Etiqueta := BandaActual.Components[i] as TQRLabel;

// ¿Es distinta a la etiqueta original y está seleccionada?
if (Etiqueta.Tag <> ID) and (Etiqueta.Hint <> '' ) then
begin
dx2 := x - xAnterior;
dy2 := y - yAnterior;
Etiqueta.Left := Etiqueta.Left + dx2;
Etiqueta.Top := Etiqueta.Top + dy2;

// Movemos la selección
Seleccion := BuscarSeleccion(Etiqueta.Tag);
if Seleccion <> nil then
begin
Seleccion.Left := Etiqueta.Left-1;
Seleccion.Top := Etiqueta.Top-1;
end;
end;
end;
end;
end;

Lo que hace es recorrer todas las etiquetas de la banda actual (menos la seleccionada) y las arrastra según lo que se ha movido la etiqueta original:


Si movemos la etiqueta 3 entonces tienen que venirse detrás las etiquetas 1 y 2.

CREANDO FIGURAS GRÁFICAS

Otro elemento que se hace imprescindible en todo editor de informes son las figuras gráficas (líneas y rectángulos). Vamos a seguir la misma filosofía que para crear etiquetas.

Primero añadimos la opción Figura a nuestro menú contextual:


Su implementación sería esta:

procedure TFInforme.FiguraClick(Sender: TObject);
begin
if BandaActual = nil then
begin
Application.MessageBox( 'Debe crear una banda.',
'Acceso denegado', MB_ICONSTOP );
Exit;
end;

QuitarSeleccionados;
with TQRShape.Create(BandaActual) do
begin
Parent := BandaActual;
Left := 10;
Top := 10;
Inc(UltimoID);
Tag := UltimoID;
Name := 'Figura' + IntToStr(UltimoID);
end;
end;

Al igual que con las etiquetas, creamos un objeto TQRShape y le damos el siguiente ID que le corresponda.

Si ejecutamos el programa y creamos una nueva figura, aparecerá en la parte superior izquierda de la banda con borde negro y fondo blanco:


SELECCIONANDO LAS FIGURAS

Ahora tenemos que modificar las rutinas de selección para que se adapten a nuestro nuevo objeto TQRShape. Empezamos por el procedimiento SeleccionarComponente:

procedure TFInforme.SeleccionarComponente;
var
Punto: TPoint;
begin
bPulsado := True;
x := Cursor.X;
y := Cursor.Y;

// ¿Ha pinchado una etiqueta o una figura?
if (Seleccionado is TQRLabel) or (Seleccionado is TQRShape) then
begin
xInicial := Seleccionado.Left;
yInicial := Seleccionado.Top;

if Seleccionado.Hint = '' then
begin
// ¿No esta pulsado la tacla control?
if HiWord(GetAsyncKeyState(VK_LCONTROL)) = 0 then
QuitarSeleccionados;

Seleccionado.Hint := 'X';
end;
end;
...

Realmente sólo he cambiado esta línea:

if (Seleccionado is TQRLabel) or (Seleccionado is TQRShape) then

ya que no afecta a lo demás. Lo que si hay que ampliar bien es el procedimiento encargado de dibujar los componentes seleccionados:

procedure TFInforme.DibujarSeleccionados;
var
i: Integer;
Seleccion: TShape;
Etiqueta: TQRLabel;
Figura: TQRShape;
begin
if BandaActual = nil then
Exit;

for i := 0 to BandaActual.ComponentCount-1 do
begin
// ¿Es una etiqueta?
if BandaActual.Components[i] is TQRLabel then
begin
Etiqueta := BandaActual.Components[i] as TQRLabel;

if Etiqueta.Hint <> '' then
begin
// Antes de crearla comprobamos si ya tiene selección
Seleccion := BuscarSeleccion(Etiqueta.Tag);

if Seleccion = nil then
begin
Seleccion := TShape.Create(BandaActual);
Seleccion.Parent := BandaActual;
Seleccion.Pen.Color := clRed;
Seleccion.Width := Etiqueta.Width+2;
Seleccion.Height := Etiqueta.Height+2;
Seleccion.Tag := Etiqueta.Tag;
Seleccion.Name := 'Seleccion' + IntToStr(Etiqueta.Tag);
end;

Seleccion.Left := Etiqueta.Left-1;
Seleccion.Top := Etiqueta.Top-1;
end;
end;

// ¿Es una figura?
if BandaActual.Components[i] is TQRShape then
begin
Figura := BandaActual.Components[i] as TQRShape;

if Figura.Hint <> '' then
begin
// Antes de crearla comprobamos si ya tiene selección
Seleccion := BuscarSeleccion(Figura.Tag);

if Seleccion = nil then
begin
Seleccion := TShape.Create(BandaActual);
Seleccion.Parent := BandaActual;
Seleccion.Pen.Color := clRed;
Seleccion.Width := Figura.Width+2;
Seleccion.Height := Figura.Height+2;
Seleccion.Tag := Figura.Tag;
Seleccion.Name := 'Seleccion' + IntToStr(Figura.Tag);
end;

Seleccion.Left := Figura.Left-1;
Seleccion.Top := Figura.Top-1;
end;
end;
end;
end;

Comprobamos que si es una figura creamos una selección para la misma también de color rojo. Aunque estos dos componentes podíamos haberlos metido en una sólo bloque de código, conviene dejarlos separados para futuras ampliaciones en cada uno de ellos.

Igualmente tenemos que contemplar también las figuras a la hora de quitar los seleccionados:

procedure TFInforme.QuitarSeleccionados;
var
i: Integer;
begin
if BandaActual = nil then
Exit;

// Le quitamos la selección a los componentes
for i := 0 to BandaActual.ComponentCount-1 do
begin
if BandaActual.Components[i] is TQRLabel then
(BandaActual.Components[i] as TQRLabel).Hint := '';

if BandaActual.Components[i] is TQRShape then
(BandaActual.Components[i] as TQRShape).Hint := '';
end;

...


MOVIENDO LAS FIGURAS

Otra cosa que también tenemos hacer es cambiar el procedimiento encargado de arrastrar los componentes para que pueda hacer lo mismo con las figuras:

procedure TFInforme.MoverRatonPulsado;
var
dx, dy, xAnterior, yAnterior: Integer;
Seleccion: TShape;
begin
// ¿Ha movido el ratón estándo el botón izquierdo pulsado?
if ( ( x <> Cursor.X ) or ( y <> Cursor.Y ) ) and ( Seleccionado <> nil ) then
begin
// ¿Estamos moviendo una etiqueta o una figura?
if (Seleccionado is TQRLabel) or (Seleccionado is TQRShape) then
begin
...


Igualmente tenemos que ampliar el procedimiento encargado de seleccionar varios componentes:

procedure TFInforme.MoverOtrosSeleccionados(ID, x, y, xAnterior, yAnterior: Integer);
var
i, dx2, dy2: Integer;
Etiqueta: TQRLabel;
Figura: TQRShape;
Seleccion: TShape;
begin
for i := 0 to BandaActual.ComponentCount - 1 do
begin
// ¿Es una etiqueta?
if (BandaActual.Components[i] is TQRLabel) then
begin
Etiqueta := BandaActual.Components[i] as TQRLabel;

// ¿Es distinta a la etiqueta original y está seleccionada?
if (Etiqueta.Tag <> ID) and (Etiqueta.Hint <> '') then
begin
dx2 := x - xAnterior;
dy2 := y - yAnterior;
Etiqueta.Left := Etiqueta.Left + dx2;
Etiqueta.Top := Etiqueta.Top + dy2;

// Movemos la selección
Seleccion := BuscarSeleccion(Etiqueta.Tag);
if Seleccion <> nil then
begin
Seleccion.Left := Etiqueta.Left-1;
Seleccion.Top := Etiqueta.Top-1;
end;
end;
end;

// ¿Es una figura?
if (BandaActual.Components[i] is TQRShape) then
begin
Figura := BandaActual.Components[i] as TQRShape;

// ¿Es distinta a la Figura original y está seleccionada?
if (Figura.Tag <> ID) and (Figura.Hint <> '' ) then
begin
dx2 := x - xAnterior;
dy2 := y - yAnterior;
Figura.Left := Figura.Left + dx2;
Figura.Top := Figura.Top + dy2;

// Movemos la selección
Seleccion := BuscarSeleccion(Figura.Tag);
if Seleccion <> nil then
begin
Seleccion.Left := Figura.Left-1;
Seleccion.Top := Figura.Top-1;
end;
end;
end;
end;
end;

Y como no, hay que modificar la selección de varios componentes con el rectángulo azul. Eso estaba en el procedimiento ComprobarSeleccionComponetes:

procedure TFInforme.ComprobarSeleccionComponentes;
var
Sel: TShape; // Rectángulo azul de selección
i: Integer;
Eti: TQRLabel;
Fig: TQRShape;
begin
if BandaActual = nil then
Exit;

// ¿Hemos abierto una selección azul?
Sel := BandaActual.FindComponent('Seleccionar') as TShape;

if Sel <> nil then
begin
// Recorremos todas las etiquetas de esta banda en busca
// las cuales están dentro de nuestro rectángulo de selección
for i := 0 to BandaActual.ComponentCount-1 do
begin
// ¿Es una etiqueta?
if BandaActual.Components[i] is TQRLabel then
begin
Eti := BandaActual.Components[i] as TQRLabel;

// ¿Está dentro de nuestro rectángulo azul de selección?
if ( Eti.Left >= Sel.Left ) and
( Eti.Top >= Sel.Top ) and
( Eti.Left+Eti.Width <= Sel.Left+Sel.Width ) and
( Eti.Top+Eti.Height <= Sel.Top+Sel.Height ) then
Eti.Hint := 'X';
end;

// ¿Es una figura?
if BandaActual.Components[i] is TQRShape then
begin
Fig := BandaActual.Components[i] as TQRShape;

// ¿Está dentro de nuestro rectángulo azul de selección?
if ( Fig.Left >= Sel.Left ) and
( Fig.Top >= Sel.Top ) and
( Fig.Left+Fig.Width <= Sel.Left+Sel.Width ) and
( Fig.Top+Fig.Height <= Sel.Top+Sel.Height ) then
Fig.Hint := 'X';
end;
end;

DibujarSeleccionados;
FreeAndNil(Sel); // Eliminamos la selección
end;
end;


El mismo rollo he tenido que hacer en el procedimiento MoverComponentes (el que movía los componentes con el teclado). De este modo podemos seleccionar y arrastrar a la vez tanto etiquetas como figuras:


REDIMENSIONANDO EL TAMAÑO DE LOS COMPONENTES

Ya tenemos hecho que si el usuario tiene seleccionado uno o más componentes y pulsa los cursores entonces mueve los componentes. Ahora vamos a hacer que si pulsa también la tecla SHIFT entonces lo que hace es redimensionarlos. Esto los controlamos en el evento FormKeyDown:

procedure TFInforme.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
// ¿Está pulsada la tecla SHIFT?
if ssShift in Shift then
begin
// Redimensionamos el tamaño de los componentes
case key of
VK_RIGHT: AmpliarComponentes( 1, 0 );
VK_LEFT: AmpliarComponentes( -1, 0 );
VK_UP: AmpliarComponentes( 0, -1 );
VK_DOWN: AmpliarComponentes( 0, 1 );
end;
end
else
begin
// Los movemos
case key of
VK_RIGHT: MoverComponentes( 1, 0 );
VK_LEFT: MoverComponentes( -1, 0 );
VK_UP: MoverComponentes( 0, -1 );
VK_DOWN: MoverComponentes( 0, 1 );
end;
end;
end;

El procedimiento AmpliarComponentes es similar al de moverlos:

procedure TFInforme.AmpliarComponentes(x, y: Integer);
var
i: Integer;
Etiqueta: TQRLabel;
Figura: TQRShape;
Seleccion: TShape;
begin
if BandaActual = nil then
Exit;

// Recorremos todos los componentes
for i := 0 to BandaActual.ComponentCount-1 do
begin
// ¿Es una etiqueta?
if BandaActual.Components[i] is TQRLabel then
begin
Etiqueta := BandaActual.Components[i] as TQRLabel;

// ¿Está seleccionada?
if Etiqueta.Hint <> '' then
begin
// Movemos la etiqueta
Etiqueta.Width := Etiqueta.Width + x;
Etiqueta.Height := Etiqueta.Height + y;

// Movemos su selección
Seleccion := BandaActual.FindComponent('Seleccion'+IntToStr(Etiqueta.Tag)) as TShape;
Seleccion.Width := Etiqueta.Width+2;
Seleccion.Height := Etiqueta.Height+2;
end;
end;

// ¿Es una figura?
if BandaActual.Components[i] is TQRShape then
begin
Figura := BandaActual.Components[i] as TQRShape;

// ¿Está seleccionada?
if Figura.Hint <> '' then
begin
// Movemos la Figura
Figura.Width := Figura.Width + x;
Figura.Height := Figura.Height + y;

// Movemos su selección
Seleccion := BandaActual.FindComponent('Seleccion'+IntToStr(Figura.Tag)) as TShape;
Seleccion.Width := Figura.Width+2;
Seleccion.Height := Figura.Height+2;
end;
end;
end;
end;

También se podría haber hecho con el ratón cogiendo las esquinas del componente y estirándolas, pero me llevaría un artículo sólo para eso (y la vaca no da para más).

Ya veis la movida que hay que hacer para hacer un simple editor de informes. En el próximo artículo vamos a añadir el componente TQRDBText para conectarnos con bases de datos Internase/Firebird.

DESCARGA DEL PROYECTO

Aquí van los enlaces para bajarse todo el proyecto en RapidShare y Megaupload:

http://rapidshare.com/files/208654755/EditorInformes3_DelphiAlLimite.rar.html

http://www.megaupload.com/?d=3BPPEI7H


Pruebas realizadas en RAD Studio 2007.

27 febrero 2009

Crea tu propio editor de informes (II)

En el artículo de hoy vamos a conseguir seleccionar varios componentes a la vez pulsando la tecla control así como poder seleccionarlos con un rectángulo. También vamos a crear una selección lateral para saber con que banda estamos trabajando.

Una vez seleccionados los componentes vamos a dar la posibilidad de arrastrarlos todos a la vez utilizando los cursores del teclado:


Pero antes de nada, vamos a comenzar a pegar hachazos en el código fuente para dejarlo todo bien organizado. Al igual que en el artículo anterior daré al final del mismo los enlaces a RapidShare y Megaupload para que no tengáis que picar código, ya que veréis como se va complicando el asunto.

MODIFICANDO EL NUCLEO DE LA APLICACIÓN

Vamos a comenzar modificando el evento OnTimer de nuestro Temporizador simplificando el código fuente de este modo:

procedure TFInforme.TemporizadorTimer(Sender: TObject);
begin
GetCursorPos( Cursor );

// ¿Está pulsado el botón izquierdo del ratón?
if HiWord( GetAsyncKeyState( VK_LBUTTON ) ) <> 0 then
begin
if Seleccionado = nil then
Seleccionado := FindControl( WindowFromPoint( Cursor ) );

// ¿No estaba pulsado el botón izquierdo del ratón anteriormente?
if not bPulsado then
SeleccionarComponente
else
MoverRatonPulsado;
end
else
begin
Seleccionado := nil;
bPulsado := False;
ComprobarSeleccionComponentes;
end;
end;

Primero comprobamos si el usuario ha pulsado el botón izquierdo del ratón. En ese caso guardamos en la variable Seleccionado el componente donde ha pinchado y llamamos al procedimiento SeleccionarComponente que se encarga de comprobar que componente hemos pulsado (banda o etiqueta).

Si el ratón ya estaba pulsado de antes significa que vamos a arrastrar un componente o que vamos a abrir una selección. Esto se hace con el procedimiento MoverRatonPulsado.

En el caso de que el usuario suelte el botón izquierdo del ratón entonces anulamos tanto la selección individual de componentes como la colectiva mediante el procedimiento ComprobarSeleccionComponentes.

Veamos la implementación de cada procedimiento.

SELECCIONANDO COMPONENTES

El procedimiento SeleccionarComponente comprobará si hemos pulsado una etiqueta o la banda para efectuar una acción u otra:

procedure TFInforme.SeleccionarComponente;
var
Punto: TPoint;
begin
bPulsado := True;
x := Cursor.X;
y := Cursor.Y;

// ¿Ha pinchado sobre una etiqueta?
if Seleccionado is TQRLabel then
begin
xInicial := Seleccionado.Left;
yInicial := Seleccionado.Top;

if Seleccionado.Hint = '' then
begin
// ¿No esta pulsado la tacla control?
if HiWord(GetAsyncKeyState(VK_LCONTROL)) = 0 then
QuitarSeleccionados;

Seleccionado.Hint := 'X';
end;
end;

// ¿Ha pinchado sobre una banda?
if Seleccionado is TQRBand then
begin
BandaActual := Seleccionado as TQRBand;
SeleccionarBandaActual;

// Transformamos las coordenadas del cursor del formulario a la banda
Punto.X := x;
Punto.Y := y;
Punto := ScreenToClient( Punto );
xInicial := Punto.X-BandaActual.Left;
yInicial := Punto.Y-BandaActual.Top;
QuitarSeleccionados;
end;

DibujarSeleccionados;
end;

Primero lee las coordenadas del ratón. Si hemos pinchado sobre una etiqueta entonces la seleccionamos y deseleccionamos las demás, pero aquí hemos añadido una novedad. Si el usuario mantiene pulsado la tecla CONTROL entonces seguimos seleccionando sin quitar la selección a las otras etiquetas.

En el caso de que pulsemos sobre una banda significa que vamos a deseleccionar todas las etiquetas y abrir un rectángulo de selección. Por eso guardo en las variables xInicial e yInicial donde comienza la selección. Aquí he tenido que convertir las coordenadas del cursor del ratón a coordenadas del formulario para que no se desplacen las coordenadas mediante la función ScreenToClient.

El procedimiento DibujarSeleccionados no ha cambiado.

SELECCIONANDO LA BANDA SOBRE LA QUE ESTAMOS

El procedimiento SeleccionarBandaActual crea un rectángulo a la izquierda de la banda actual de color verde:

procedure TFInforme.SeleccionarBandaActual;
var
SeleccionBanda: TShape;
begin
// Si ya había seleccionada una banda la eliminamos (la selección)
SeleccionBanda := Informe.FindComponent('SeleccionBanda') as TShape;
if SeleccionBanda <> nil then
FreeAndNil(SeleccionBanda);

// Seleccionamos de nuevo la banda
SeleccionBanda := TShape.Create(Informe);
with SeleccionBanda do
begin
Name := 'SeleccionBanda';
Parent := Informe;
Left := 20;
Top := BandaActual.Top;
Width := 4;
Pen.Width := 2;
Pen.Color := clGreen;
Height := BandaActual.Height;
end;
end;

A este procedimiento lo vamos a llamar también al crear una banda:

procedure TFInforme.BandaClick(Sender: TObject);
begin
BandaActual := TQRBand.Create(Informe);

with BandaActual do
begin
Parent := Informe;
Height := 200;
end;

SeleccionarBandaActual;
end;

Cuando creamos una nueva banda la damos por la banda de trabajo actual.

ARRASTRANDO UN COMPONENTE O LA SELECCIÓN

El procedimiento MoverRatonPulsado se ejecutará cuando dejamos pulsado el botón izquierdo del ratón sobre un componente o si estamos abriendo un rectángulo de selección:

procedure TFInforme.MoverRatonPulsado;
var
dx, dy: Integer;
Seleccion: TShape;
begin
// ¿Ha movido el ratón estándo el botón izquierdo pulsado?
if ( ( x <> Cursor.X ) or ( y <> Cursor.Y ) ) and ( Seleccionado <> nil ) then
begin
// ¿Estamos moviendo una etiqueta?
if Seleccionado is TQRLabel then
begin
// Movemos el componente seleccionado
dx := Cursor.X - x;
dy := Cursor.Y - y;
Seleccionado.Left := xInicial + dx;
Seleccionado.Top := yInicial + dy;

// Movemos la selección
Seleccion := BuscarSeleccion(Seleccionado.Tag);
if Seleccion <> nil then
begin
Seleccion.Left := xInicial+dx-1;
Seleccion.Top := yInicial+dy-1;
end;
end;

// ¿Estamos sobre una banda?
if Seleccionado is TQRBand then
begin
// si no existe un rectángulo de selección lo creamos
Seleccion := BandaActual.FindComponent('Seleccionar') as TShape;

if Seleccion = nil then
begin
Seleccion := TShape.Create(BandaActual);
Seleccion.Parent := BandaActual;
Seleccion.Pen.Color := clBlue;
Seleccion.Name := 'Seleccionar';
end;

dx := Cursor.X - x;
dy := Cursor.Y - y;
Seleccion.Left := xInicial;
Seleccion.Top := yInicial;
Seleccion.Width := dx;
Seleccion.Height := dy;
Seleccion.Brush.Style := bsClear;
end;
end;
end;

Si lo que arrastramos es una etiqueta, el código fuente no cambia mucho respecto al artículo anterior. Pero si lo hacemos sobre el fondo de una banda entonces comprobamos si existe un rectángulo de selección. Si no lo hay abrimos uno mediante un componente TShape de color azul.

La selección tendrá lugar cuando el usuario suelte el botón izquierdo del ratón.

SELECCIONANDO UNO O MAS COMPONENTES

El procedimiento ComprobarSeleccionComponentes se ejecuta inmediatamente después de soltar el botón izquierdo del ratón:

procedure TFInforme.ComprobarSeleccionComponentes;
var
Sel: TShape; // Rectángulo azul de selección
i: Integer;
Eti: TQRLabel;
begin
if BandaActual = nil then
Exit;

// ¿Hemos abierto una selección azul?
Sel := BandaActual.FindComponent('Seleccionar') as TShape;

if Sel <> nil then
begin
// Recorremos todas las etiquetas de esta banda en busca
// las cuales están dentro de nuestro rectángulo de selección
for i := 0 to BandaActual.ComponentCount-1 do
// ¿Es una etiqueta?
if BandaActual.Components[i] is TQRLabel then
begin
Eti := BandaActual.Components[i] as TQRLabel;

// ¿Está dentro de nuestro rectángulo azul de selección?
if ( Eti.Left >= Sel.Left ) and
( Eti.Top >= Sel.Top ) and
( Eti.Left+Eti.Width <= Sel.Left+Sel.Width ) and
( Eti.Top+Eti.Height <= Sel.Top+Sel.Height ) then
Eti.Hint := 'X';
end;

DibujarSeleccionados;
FreeAndNil(Sel); // Eliminamos la selección
end;
end;

Comprueba si hay un rectángulo azul llamado Seleccionar y en ese caso recorre todas las etiquetas de la banda y comprueba si alguna está dentro del rectángulo de selección. Si es así, le metemos una X en el Hint. Luego llamamos al procedimiento DibujarSeleccionados para que se encargue del resto y eliminamos la selección.

MOVIENDO LOS COMPONENTES CON LOS CURSORES

Para poder mover los componentes seleccionados con el teclado podemos aprovechar el evento OnKeyDown del formulario:

procedure TFInforme.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case key of
VK_RIGHT: MoverComponentes( 1, 0 );
VK_LEFT: MoverComponentes( -1, 0 );
VK_UP: MoverComponentes( 0, -1 );
VK_DOWN: MoverComponentes( 0, 1 );
end;
end;


El procedimiento MoverComponentes desplaza todos las etiquetas seleccionadas horizontal o verticalmente según los parámetros:

procedure TFInforme.MoverComponentes(x, y: Integer);
var
i: Integer;
Etiqueta: TQRLabel;
Seleccion: TShape;
begin
if BandaActual = nil then
Exit;

// Recorremos todos los componentes
for i := 0 to BandaActual.ComponentCount-1 do
// ¿Es una etiqueta?
if BandaActual.Components[i] is TQRLabel then
begin
Etiqueta := BandaActual.Components[i] as TQRLabel;

// ¿Está seleccionada?
if Etiqueta.Hint <> '' then
begin
// Movemos la etiqueta
Etiqueta.Left := Etiqueta.Left + x;
Etiqueta.Top := Etiqueta.Top + y;

// Movemos su selección
Seleccion := BandaActual.FindComponent('Seleccion'+IntToStr(Etiqueta.Tag)) as TShape;
Seleccion.Left := Etiqueta.Left-1;
Seleccion.Top := Etiqueta.Top-1;
end;
end;
end;

Aparte de mover la etiqueta también tenemos que arrastrar detrás su selección (TShape).

MODIFICANDO LA DESELECCION DE COMPONENTES

Tenemos que modificar también la deselección de componentes ya que si no también eliminaría la selección verde de la banda:

procedure TFInforme.QuitarSeleccionados;
var
i: Integer;
begin
if BandaActual = nil then
Exit;

// Quitamos las etiquetas seleccionadas
for i := 0 to BandaActual.ComponentCount-1 do
if BandaActual.Components[i] is TQRLabel then
(BandaActual.Components[i] as TQRLabel).Hint := '';

// Eliminamos las selecciones
for i := BandaActual.ComponentCount-1 downto 0 do
if BandaActual.Components[i] is TShape then
if BandaActual.Components[i].Name <> 'SeleccionBanda' then
BandaActual.Components[i].Free;
end;

Lo hacemos sólo para ese caso en especial.

EL PROYECTO EN INTERNET

Aquí va el proyecto comprimido con rar y subido a RapidShare y Megaupload:

http://rapidshare.com/files/203119248/EditorInformes2_DelphiAlLimite.rar.html

http://www.megaupload.com/?d=2FCA8P9C

CONTINUARA

Como podéis ver con estos ejemplos, crear un editor de informes no es cosa fácil (hacer algo como Microsoft Word tiene que doler). Aunque no me meteré a fondo con todo lo que engloba un editor de informes, todavía nos quedan muchas características básicas que implementar. Eso será en el próximo artículo.

Pruebas realizadas en RAD Studio 2007.

13 febrero 2009

Crea tu propio editor de informes (I)

Después de haber visto todas las características de QuickReport a uno se le queda un sabor agridulce por dos razones: es uno de los editores de informes más completos que hay pero no permite exportar plantillas de informes, es decir, por cada informe hay que hacer un formulario.

Eso sin contar que los clientes de nuestros programas no pueden personalizar los informes a su gusto. Todo tiene que pasar por nosotros mediante Delphi.

Para solucionar esto podemos crear un pequeño editor de informes que permita a cualquier usuario diseñar o modificar cualquier informe aunque internamente funcione con QuickReport.

Veamos todos los pasos que se necesitan para crear un editor.

CREANDO UN NUEVO PROYECTO

Para iniciar el proyecto vamos a hacer lo siguiente:

1º Creamos un nuevo proyecto y guardamos el formulario principal con el nombre FPrincipal. Luego guardamos este formulario con el nombre UPrincipal.pas y el proyecto con el nombre EditorInformes.dproj.

2º Como nos interesa hacer una aplicación que permita manejar varios informes a la vez, vamos a modificar la propiedad FormStyle del formulario con el valor fsMDIForm.

Para los que no lo sepan, una aplicación MDI se compone de una ventana padre (fsMDIForm) y varias ventanas hijas (fsMDIChild). Por ejemplo, los programas de Microsoft Office (Word, Excel y Access) son aplicaciones MDI.

3º También vamos a hacer que cuando se ejecute el programa aparezca la ventana maximizada. Eso se hace poniendo el valor wsMaximized en la propiedad WindowState del formulario.

Una vez tenemos el formulario principal, vamos a ir añadiendo las opciones generales del programa.

AÑADIENDO EL MENU DE OPCIONES

Insertamos en el formulario el componente de la clase TMainMenu (que se encuentra en la sección Standard) y lo vamos a llamar MenuPrincipal:


Hacemos doble clic sobre dicho componente y añadimos el siguiente menú Archivo:


La opción de salir ya la podemos implementar cerrando el formulario:

procedure TFPrincipal.SalirClick(Sender: TObject);
begin
Close;
end;

CREANDO UN NUEVO INFORME

Vamos a crear un nuevo formulario llamado FInforme que va a ser el alma del proyecto. Es el que vamos a utilizar para diseñar el informe. Guardamos el informe con el nombre UInforme.pas.

En la propiedad Caption del formulario le ponemos Nuevo informe. También tenemos que poner su propiedad FormStyle a fsMDIChild, para indicarle que es un formulario hijo de FPrincipal.

Ahora añadimos al formulario el componente TQuickRep que se encuentra en la sección de componentes QReport y lo colocamos en la esquina superior izquierda del formulario (Left=0, Top=0):


Al componente TQuickRep lo vamos a llamar Informe. Ahora volvemos al formulario principal y vamos a implementar la opción Archivo -> Nuevo para crear nuestro informe:

procedure TFPrincipal.NuevoClick(Sender: TObject);
begin
Application.CreateForm(TFInforme,FInforme);
FInforme.Show;
end;

Al compilar el proyecto os dará un error quejándose de que no encuentra la unidad QuickRpt. Tenéis que añadir la ruta de búsqueda del proyecto con el directorio donde se encuentra instalado QuickReport:


Ejecutamos el programa y seleccionamos Archivo -> Nuevo y aparecerá esta ventana hija:


Si intentáis cerrar el formulario hijo veréis que no se cierra, se minimiza. Esto es normal en las aplicaciones MDI. Para solucionar esto tenemos que forzar a que se libere el formulario utilizando el evento OnClose del formulario FInforme:

procedure TFInforme.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;

Ahora pasemos a insertar componentes visuales en nuestro informe.

AÑADIENDO COMPONENTES AL INFORME

Vamos a hacer que el usuario pueda añadir componentes utilizando un menú contextual. Para ello añadimos un componente de la clase TPopupMenu que se encuentra en la pestaña Standard:


A este componente lo vamos a llamar MenuInforme y va tener estas opciones:


Como el componente TQuickRep no es un componente visual para manejar en tiempo de ejecución, no podemos vincular nuestro menú popup directamente a este componente sino que hay que hacerlo con el formulario FInforme.

Para arrastrar los componentes tampoco podemos utilizar las propiedades de Drag and Drop que suelen llevar los componentes visuales de Delphi, ya que ningún componente de QuickReport tiene estas propiedades.

Vamos a necesitar estas variables en la sección private de nuestro formulario:

private
{ Private declarations }
bPulsado: Boolean;
Seleccionado: TControl;
x, y, xInicial, yInicial, UltimoID: Integer;
Cursor: TPoint;

Veamos para que sirve cada variable:

bPulsado: Nos va a indicar cuando el usuario ha dejado pulsado el botón izquierdo del ratón.

Seleccionado: Esta variable va a puntar al control que tenemos seleccionado (por ejemplo TQRLabel).

x, y: Utilizamos estas variables para guardar la posición actual del puntero del ratón.

xInicial, yInicial: Cuando arrastramos un componente utilizamos estas variables para conservar la posición inicial del mismo.

UltimoID: Con cada componente que creamos le asignamos un identificador (un número único) que nos va a servir para asignar un rectángulo de selección.

Cursor: Lo utilizamos para leer las coordenadas del ratón.

También tenemos que añadir la unidad QRCtrls en el apartado uses de nuestra unidad.

CREANDO UNA NUEVA BANDA

Ya sabemos que para que se muestre algo en QuickReport lo primero que tenemos que hacer es crear una banda. Este es el código fuente que vamos a introducir en la opción Nuevo -> Banda:

procedure TFInforme.BandaClick(Sender: TObject);
begin
BandaActual := TQRBand.Create(Informe);

with BandaActual do
begin
Parent := Informe;
Height := 200;
end;
end;

Aparte de crear el objeto le hemos asignado como padre el componente TQuickRep y le hemos dado un tamaño vertical de 200. Aprovechamos para guardar en la variable BandaActual el objeto TQRBand con el que estamos trabajando.

Ahora pasemos a la creación de una etiqueta.

CREANDO UNA ETIQUETA

Aquí va el código para la opción Nuevo -> Etiqueta:

procedure TFInforme.EtiquetaClick(Sender: TObject);
begin
if BandaActual = nil then
begin
Application.MessageBox( 'Debe crear una banda.',
'Acceso denegado', MB_ICONSTOP );
Exit;
end;

QuitarSeleccionados;
with TQRLabel.Create(BandaActual) do
begin
Parent := BandaActual;
Left := 10;
Top := 10;
Inc(UltimoID);
Tag := UltimoID;
Caption := 'Etiqueta' + IntToStr(UltimoID);
Name := Caption;
end;
end;

Lo primero que hacemos es comprobar si primero hemos creado una banda. Si es así, entonces llamamos a otro procedimiento llamado QuitarSeleccionados. Eso lo veremos más adelante.

Después creo un componente de la clase TQRLabel, le asigno el padre (TQuickRep) y le doy un tamaño predeterminado. También lo coloco en la esquina superior izquierda de la banda actual. Le asigno un nuevo entero identificador que vamos a utilizar posteriormente para poder seleccionarlo y por último le doy el nombre Etiqueta + UltimoID.

LA SELECCION DE COMPONENTES

Al igual que el editor de formularios de Delphi vamos a poder seleccionar componentes si hacemos clic sobre los mismos. Esta selección la vamos a hacer utilizando componentes de la clase TShape con la ventaja de que los podemos ver en el editor pero no se imprimen cuando se lanza el informe a la impresora o se hace la vista previa.

Como el usuario va a poder seleccionar uno o más componentes, lo que hago es asociar cada etiqueta TQRLabel a cada componente TShape mediante un identificador (UltimoID) y lo guardamos en la propiedad Tag de cada componente.

Por ejemplo, si creo tres etiquetas, este sería su Tag:

TQRLabel TShape Tag
--------- ---------- ---
Etiqueta1 Seleccion1 1
Etiqueta2 Seleccion2 2
Etiqueta3 Seleccion3 3

Este sería el procedimiento para eliminar todos los seleccionados:

procedure TFInforme.QuitarSeleccionados;
var
i: Integer;
begin
if BandaActual = nil then
Exit;

// Quitamos las etiquetas seleccionadas
for i := 0 to BandaActual.ComponentCount-1 do
if BandaActual.Components[i] is TQRLabel then
(BandaActual.Components[i] as TQRLabel).Hint := '';

// Eliminamos las selecciones
for i := BandaActual.ComponentCount-1 downto 0 do
if BandaActual.Components[i] is TShape then
BandaActual.Components[i].Free;
end;

Quitar los seleccionados implica eliminar todos los componentes TShape y decirle a cada etiqueta TQRLabel que no está seleccionada. ¿Cómo sabemos si una etiqueta está seleccionada o no? Pues como el Tag ya lo tenemos ocupado con su identificador, lo que hacemos es utilizar su propiedad Hint. Si el Hint tiene una X lo damos por seleccionado, en caso contrario estará vacío.

MOVIENDO LOS COMPONENTES POR LA PANTALLA

Esto es lo que más me ha costado. ¿Cómo mover componentes que no soportan la propiedad Drag and Drop?. ¿Cómo averiguar a que componente hemos pinchado en pantalla?

Gracias a Internet y un poco de paciencia pude encontrar piezas de código que me solucionaron todos estos problemas. Vayamos por partes. Como no podemos aprovechar los eventos MouseDown, MouseMove, etc. del formulario (por que tenemos el objeto TQuickRep encima que no tiene estos eventos) entonces opté por utilizar un temporizador (TTimer) para controlar el todo momento las acciones del usuario:


A este componente lo he llamado Temporizador. Le he puesto su propiedad Interval a 1 milisegundo y este sería su evento OnTimer:

procedure TFInforme.TemporizadorTimer(Sender: TObject);
var
dx, dy: Integer;
Seleccion: TShape;
begin
GetCursorPos( Cursor );

// ¿Está pulsado el botón izquierdo del ratón?
if HiWord( GetAsyncKeyState( VK_LBUTTON ) ) <> 0 then
begin
if Seleccionado = nil then
Seleccionado := FindControl( WindowFromPoint( Cursor ) );

// ¿No estaba pulsado el botón izquierdo del ratón anteriormente?
if not bPulsado then
begin
bPulsado := True;
x := Cursor.X;
y := Cursor.Y;

// ¿Ha seleccionado una etiqueta?
if Seleccionado is TQRLabel then
begin
xInicial := Seleccionado.Left;
yInicial := Seleccionado.Top;

if Seleccionado.Hint = '' then
begin
QuitarSeleccionados;
Seleccionado.Hint := 'X';
end;
end;

if Seleccionado is TQRBand then
QuitarSeleccionados;

DibujarSeleccionados;
end
else
begin
// ¿Ha movido el ratón estándo el botón izquierdo pulsado?
if ( ( x <> Cursor.X ) or ( y <> Cursor.Y ) ) and ( Seleccionado <> nil ) then
if Seleccionado is TQRLabel then
begin
// Movemos el componente seleccionado
dx := Cursor.X - x;
dy := Cursor.Y - y;
Seleccionado.Left := xInicial + dx;
Seleccionado.Top := yInicial + dy;

// Movemos la selección
Seleccion := BuscarSeleccion(Seleccionado.Tag);
if Seleccion <> nil then
begin
Seleccion.Left := xInicial+dx-1;
Seleccion.Top := yInicial+dy-1;
end;
end;
end;
end
else
begin
bPulsado := False;
Seleccionado := nil;
end;
end;

¿Vaya ladrillo, no? No es tan difícil como parece. Vamos a ver este procedimiento por partes:

1º Leemos la posición del cursor en pantalla:

GetCursorPos( Cursor );

2º Para averiguar si está pulsado el botón izquierdo del ratón utilizo esta función:

if HiWord( GetAsyncKeyState( VK_LBUTTON ) ) <> 0 then

GetAsyncKeyState es una función asíncrona de la API de Windows que nos devuelve el estado de una tecla o el botón del ratón.

3º Averiguamos sobre que componente esta el puntero del ratón con esta función:

if Seleccionado = nil then
Seleccionado := FindControl( WindowFromPoint( Cursor ) );

4º Si no estaba pulsado el botón izquierdo del ratón entonces compruebo si lo que ha pulsado es una etiqueta o la banda. Si es la etiqueta guardamos su posición original (por si no da por moverla) y la selecciono guardando una X en su propieda Hint:

if Seleccionado is TQRLabel then
begin
xInicial := Seleccionado.Left;
yInicial := Seleccionado.Top;

if Seleccionado.Hint = '' then
begin
QuitarSeleccionados;
Seleccionado.Hint := 'X';
end;
end;

5º Si lo que hemos pulsado es una banda deseleccionamos todos las etiquetas:

if Seleccionado is TQRBand then
QuitarSeleccionados;

6º Llamamos al procedimiento encargado de dibujar las etiquetas seleccionadas:

DibujarSeleccionados;

La implementación de este procedimiento la veremos más adelante.

7º La otra parte del procedimiento se ejecuta si mantenemos pulsando el botón izquierdo del ratón y arrastramos el componente. Pero por ahora sólo dejamos arrastrar una etiqueta:

// ¿Ha movido el ratón estándo el botón izquierdo pulsado?
if ( ( x <> Cursor.X ) or ( y <> Cursor.Y ) ) and ( Seleccionado <> nil ) then
if Seleccionado is TQRLabel then
begin
// Movemos el componente seleccionado
dx := Cursor.X - x;
dy := Cursor.Y - y;
Seleccionado.Left := xInicial + dx;
Seleccionado.Top := yInicial + dy;

// Movemos la selección
Seleccion := BuscarSeleccion(Seleccionado.Tag);
if Seleccion <> nil then
begin
Seleccion.Left := xInicial+dx-1;
Seleccion.Top := yInicial+dy-1;
end;
end;

Para moverla calculo la diferencia entre la posición original y le sumo el desplazamiento del ratón (variales dx y dy). Para evitar que una etiqueta tenga varias selecciones he creado la fución BuscarSeleccion a la cual le pasamos el ID de la etiqueta y nos devuelve el objeto TShape para poder moverlo. Si no fuera así, cada vez que movemos una etiqueta se queda su selección abandonada.

Este sería el procedimiento de buscar una selección:

function TFInforme.BuscarSeleccion( ID: Integer ): TShape;
var
i: Integer;
begin
Result := nil;

if BandaActual = nil then
Exit;

for i := 0 to BandaActual.ComponentCount-1 do
if BandaActual.Components[i] is TShape then
if BandaActual.Components[i].Tag = ID then
begin
Result := BandaActual.Components[i] as TShape;
Exit;
end;
end;

CREANDO LA SELECCIÓN PARA CADA ETIQUETA

El procedimiento DibujarSeleccionados comprueba primero si una etiqueta ya tiene selección (por su ID) y si no es así la creamos:

procedure TFInforme.DibujarSeleccionados;
var
i: Integer;
Seleccion: TShape;
Etiqueta: TQRLabel;
begin
if BandaActual = nil then
Exit;

for i := 0 to BandaActual.ComponentCount-1 do
if BandaActual.Components[i] is TQRLabel then
begin
Etiqueta := BandaActual.Components[i] as TQRLabel;

if Etiqueta.Hint <> '' then
begin
// Antes de crearla comprobamos si ya tiene selección
Seleccion := BuscarSeleccion(Etiqueta.Tag);

if Seleccion = nil then
begin
Seleccion := TShape.Create(BandaActual);
Seleccion.Parent := BandaActual;
Seleccion.Pen.Color := clRed;
Seleccion.Width := Etiqueta.Width+2;
Seleccion.Height := Etiqueta.Height+2;
Seleccion.Tag := Etiqueta.Tag;
Seleccion.Name := 'Seleccion' + IntToStr(Etiqueta.Tag);
end;

Seleccion.Left := Etiqueta.Left-1;
Seleccion.Top := Etiqueta.Top-1;
end;
end;
end;

Para que un componente tenga selección tienen que cumplirse dos condiciones:

1º Que sea una etiqueta.
2º Que su propiedad Hint = ‘X’

Si la selección (TShape) ya estaba creada de antes sólo tenemos que actualizar su posición.

Por último implementamos la opción de vista previa:

procedure TFInforme.VistaPreviaClick(Sender: TObject);
begin
Informe.Preview;
end;

Y aquí tenemos el resultado:


Aquí os dejo el proyecto comprimido con RAR y subido a RapidShape y Megaupload:

http://rapidshare.com/files/197571065/EditorInformes_DelphiAlLimite.rar.html

http://www.megaupload.com/?d=UO2IU2MV

En la siguiente parte de este artículo seguiremos mejorando nuestro editor.

Pruebas realizadas en RAD Studio 2007.

Publicidad