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