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.

30 enero 2009

Creación de informes con QuickReport (y V)

En artículos anteriores vimos que mediante etiquetas podemos llevar al informe cualquier tipo de dato (fecha, hora, totales, etc.) generado por nuestro código fuente, pero hay funciones que lleva QuickReport que evitan tener que hacer muchas de estas cosas a mano.

Esto se hace utilizando campos del tipo TQRExpr y TQRSysData. Veamos los tipos de funciones.

FUNCIONES DE FECHA Y HORA

Las funciones de fecha y hora que tenemos con TQRExpr son TIME y DATE:


Estas funciones hay que introducirlas en la propiedad Expression.

Si utilizamos el campo TQRSysData podemos obtener la fecha y la hora también:


En este tipo de campos seleccionamos los valores predeterminados mediante su propiedad Data.

FUNCIONES MATEMATICAS

Tenemos estas cuatro funciones matemáticas:

INT( x ) -> Devuelve la parte entera de un número real x

FRAC( x ) -> Devuelve la parte fraccionaria de un número real x

SQRT( x ) -> Calcula la raíz cuadrada de x

DIV( x, y ) -> Realiza la división entera de x entre y.

Este sería un ejemplo al ejecutarlas:


FUNCIONES ESTADISTICAS

Las funciones estadísticas van aplicadas a campos del DataSet que estén dentro de una banda tipo detalle o subdetalle (la banda padre):

SUM( campo ) -> Calcula la suma de toda la columna del campo. Hace suma y sigue.

COUNT -> Cuenta el número de registros aparecidos.

MAX( campo ) -> Muestra el valor máximo del campo de entre todos los registros aparecidos.

MIN( campo) -> Muestra el valor mínimo.

AVERAGE( campo ) -> Calcula el valor medio del campo respecto a todos los registros aparecidos.

Este ejemplo lo he realizado en una banda de tipo Summary y debajo de una banda de tipo Detail:


Y este sería el resultado:


FUNCION PARA AVERIGUAR EL TIPO DE CAMPO

La función TYPEOF nos dice el tipo de campo que le pasamos como parámetro, aunque por las pruebas que he realizado no trabaja muy fino que digamos:


El tipo fecha me lo ha dado como STRING. De todas formas no le encuentro mucho sentido a esta función, ya que rara vez se trabaja con tipos Variant en los informes.

FUNCIONES LOGICAS

Podemos condicionar resultados mediante la función:

IF ( condición, valor si es verdadero, valor si es falso )

Por ejemplo:


Esta función evalúa el primer parámetro y si es verdadero entonces devuelve el segundo parámetro y en caso contrario el tercero.

OTRAS FUNCIONES

En el apartado Other del asistente de la propiedad Expression tenemos estas funciones:

STR( x ) -> Convierte un número entero o real a tipo cadena de texto (STRING).

UPPER( s ) -> Pasa el contenido de una cadena de texto a mayúsculas.

LOWER( s ) -> Convierte todo el texto a minúsculas.

PRETTY( s ) -> Pasa a mayúsculas la primera letra del texto (capitalización).

COPY( s, inicio, longitud ) -> Copia un trozo de cadena de texto que comience en la posición inicio y con una longitud predeterminada.

FORMATNUMERIC( formato, numeroreal ) -> Convierte un número entero o real a STRING con el formato que le pasemos como parámetro.

Veamos un ejemplo de todas estas funciones:


OTROS USOS DEL CAMPO TQRSYSDATA

Aparte de mostrar los valores de fecha y hora mediante la propiedad Data, el campo de la clase TQRSysData permite mostrar esta otra información:


Las propiedades qrsDetailCount y qrsDetailNo devuelven el número de registros encontrados (la verdad es que no se donde está la diferencia entre ambas). Luego tenemos qrsPageNumber que devuelve el número de página actual y qrsReportTitle que imprime el título del informe (TQuickRep.ReportTitle).

En el caso de que estas funciones sean insuficientes siempre podemos echar mano a una etiqueta TQRLabel enviando a su propiedad Caption el formato o información que nos interese en tiempo real con los eventos BefotePrint de las bandas.

INFORMES COMPUESTOS

Otra de las ventajas que incorpora QuickReport es la posibilidad de imprimir varios informes juntos (uno detrás de otro) como si fueran el mismo (divide y vencerás).

Para ello añado al formulario principal de la aplicación el componente TQRCompositeReport:


A este componente lo voy a llamar InformeCompuesto y voy a juntar en un solo informe los listados de facturas y de ventas:

Application.CreateForm( TFListadoFacturas, FListadoFacturas );
Application.CreateForm( TFListadoVentas, FListadoVentas );
InformeCompuesto.Preview;

En el evento OnAddReports de este componente añadimos todos los informes que queremos imprimir:

procedure TForm1.InformeCompuestoAddReports(Sender: TObject);
begin
InformeCompuesto.Reports.Add( FListadoFacturas.Informe );
InformeCompuesto.Reports.Add( FListadoVentas.Informe );
end;

De modo que al ejecutarlo mostraría este aspecto:


Esto es algo ideal cuando hay que volcar todos los informes en un archivo PDF.

INFORMES ABSTRACTOS

Un informe abstracto es un componentes de la clase TQuickAbstractRep que viene a ser el equivalente a TQuickRep. Este componente no va vinculado con bases de datos ni con campos. Los informes abstractos sólo son útiles para imprimir rápidamente información fija que podemos cargar de un archivo de texto.

Para poder utilizar este tipo de informes tenemos que añadir una banda de tipo TQRStringBand y dentro de esta última metemos expresiones TQRExpr. También permite añadir etiquetas TQRLabel o campos TQRSysData.

En resumen, los informes abstractos son una versión “light” de TQuickRep cuando necesitamos imprimir textos fijos rápidamente y sin complicaciones de bases de datos.

CONCLUSIONES

Con estos puntos finalizo la iniciación a la creación de Informes con QuickReport. Creo que con esto cubro más o menos las partes más importantes. Podía haber realizado cientos de ejemplos pero eso lleva un tiempo no tengo.

De todas formas, si alguien tiene alguna duda sobre como realizar un cierto tipo informe que lo escriba en el blog o me mande un correo y veré que puedo hacer. O si hay algún usuario avanzado que controle a tope QuickReport y se le ocurra algún ejemplo que me lo mande y lo publicaré encantado.

Pruebas realizadas en RAD Studio 2007.

23 enero 2009

Creación de informes con QuickReport (IV)

Si creéis que con los informes que hemos hecho hemos cubierto casi todas las posibilidades estáis equivocados. La mente retorcida de nuestros clientes siempre hace dar otra vuelta de tuerca a nuestros informes hasta límites insospechados. Simplemente quieren verlo todo en un folio.

MAS DIFICIL TODAVIA

A partir de las tablas de CLIENTES y FACTURAS vamos a hacer un informe de ventas por cliente que nos va a mostrar lo que ha facturado cada uno (desglosado), con el total y un suma y sigue. Además vamos a totalizar toda la facturación:


Podéis verlo mejor si hacéis clic sobre la imagen. El diseño del informe sería este:


A la primera banda la vamos a llamar Cabecera y va a ser un objeto TQRBand cuya propiedad BandType será rbTitle. Sólo va a tener el título del listado y el encabezado:


La segunda banda se va a llamar Detalle y también será de tipo TQRBand. La banda va a ser de tipo rbDetail y va a mostrar los nombres de los clientes y las cabeceras de la factura mediante campos TQRDBText:


La tercera banda va a ser un objeto TQRSubDetail que vamos a llamar Subdetalle. En esta banda vamos a mostrar los importes de cada factura (también con TQRBText):


La cuarta banda también va a ser un objeto TQRSubDetalle que llamaremos Subdetalle2. Esta banda va a encargarse de totalizar las facturas de cada cliente y hacer un suma y sigue:


En esta banda, la primera fila de campos son etiquetas TQRLabel que vamos a reprogramar para que nos sumen el total de facturación de cada cliente. Para cumplir este cometido creamos en la sección privada del formulario de este informe estas variables:

private
{ Private declarations }
dSumaBase, dSumaIva, dSumaTotal: Double;

Ahora programamos el evento OnBeforePrint de la banda Detalle (rbDetail) para que inicialice estas variables y además nos filtre las facturas del cliente actual:

procedure TFListadoVentas.DetalleBeforePrint(Sender: TQRCustomBand;
var PrintBand: Boolean);
begin
with Form1 do
begin
Facturas.Filter := 'IDCLIENTE=' + #39 +
Clientes.FieldByName('ID').AsString + #39;
Facturas.Filtered := True;
end;

dSumaBase := 0;
dSumaIva := 0;
dSumaTotal := 0;
end;

Ahora le decimos antes de imprimir la banda Subdetalle2 que cambie las etiquetas por lo que llevamos aculumado:

procedure TFListadoVentas.Subdetalle2BeforePrint(Sender: TQRCustomBand;
var PrintBand: Boolean);
begin
ESUMABASE.Caption := FormatFloat( '###,###,#0.00', dSumaBase );
ESUMAIVA.Caption := FormatFloat( '###,###,#0.00', dSumaIva );
ESUMATOTAL.Caption := FormatFloat( '###,###,#0.00', dSumaTotal );
end;

Y para que haga la suma le decimos en el evento AfterPrint (después de imprimir) de la banda Detalle que vaya sumando los totales de cada registro:

procedure TFListadoVentas.SubdetalleAfterPrint(Sender: TQRCustomBand;
BandPrinted: Boolean);
begin
dSumaBase := dSumaBase + Form1.Facturas.FieldByName('BASEIMPONIBLE').AsFloat;
dSumaIva := dSumaIva + Form1.Facturas.FieldByName('IVA').AsFloat;
dSumaTotal := dSumaTotal + Form1.Facturas.FieldByName('TOTAL').AsFloat;
end;

Esta banda de totales también contiene campos de tipo TQRExpr (expresiones) que no van vinculados a bases de datos pero que permiten realizar operaciones con los campos del formulario. Estos campos los utilizamos para hacer el suma y sigue de todos los clientes. Para acumular la suma del campo BASEIMPONIBLE le decimos a la propiedad Expression del objeto TQRExpr que haga esto:


Igual tenemos que hacer para sumar el IVA y el TOTAL: SUM(IVA) y SUM(TOTAL), cada uno en su campo correspondiente. Como puede verse hay otras funciones para calcular la media, el máximo, pasar a mayúsculas, etc.

Y la quinta y última banda va a ser un objeto TQRBand cuyo tipo va a ser rbSummary. Aquí vamos a meter campos de tipo TQRExpr para sumar Base Imponible, Importe IVA y Total.

Aunque todo esto parezca una paranoia mental os puedo asegurar que se hace rápidamente respecto a otros editores como Rave (el que tenga valor que intente hacer este ejemplo).

Hay que procurar utilizar las funciones que trae el objeto TQRExpr para que nos resuelva los problemas, pero en casos como el que hemos visto de totales los podemos solucionar metiendo etiquetas y haciendo el trabajo a mano por programación.

EXPORTANDO EL INFORME A PDF

Aunque hoy en día se suelen utilizar programas como Primo PDF para convertir los documentos impresos a PDF, a los usuarios de los programas de gestión no les suele hacer mucha gracia porque cada vez que van a imprimir tiene que seleccionar la impresora Primo PDF y elegir la ubicación.

Por fortuna, las últimas versiones de QuickReport permiten exportar el formato a PDF. Esto podemos hacerlo directamente mediante código cuando vamos a imprimir el documento:

Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.ExportToFilter(
TQRPDFDocumentFilter.Create( 'C:\ListadoVentas.pdf' ) );

Para poder compilar esto tenemos que añadir la unidad QRPDFFilt en nuestro formulario. Con una sola línea de código hemos solucionado el problema.

EXPORTANDO A OTROS FORMATOS

Igualmente podemos pasarlo a HTML de este modo:

Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.ExportToFilter(
TQRGHTMLDocumentFilter.Create( 'C:\ListadoVentas.html' ) );

Este filtro necesita la unidad QRWebFilt.

Para exportar el informe a un documento RTF compatible con Microsoft Word sería así:

Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.ExportToFilter( TQRRTFExportFilter.Create( 'C:\ListadoVentas.rtf' ) );

Ese filtro necesita la unidad QRExport.

También podemos exportar a Microsoft Excel de este modo:

Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.ExportToFilter( TQRXLSFilter.Create( 'C:\ListadoVentas.xls' ) );

Utiliza también la unidad QRExport.

Si nos interesa extraer tanto los datos como los metadatos se puede exportar todo a XML:

Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.ExportToFilter( TQRXDocumentFilter.Create( 'C:\ListadoVentas.xml' ) );

Este filtro está en la unidad QRXMLSFilt.

Se puede exportar a un formato gráfico de 16 bits de gráficos vectoriales (aunque también soporta bitmaps) con extensión WMF que era antiguamente utilizado por Windows 3.0. Este formato puede verse con programas de diseño gráfico como GIMP. Se exporta de este modo:

var
FiltroWMF: TQRWMFExportFilter;
begin
FiltroWMF := TQRWMFExportFilter.Create( 'C:\ListadoVentas.wmf' );
Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.Prepare;
FListadoVentas.Informe.ExportToFilter( FiltroWMF );
end;

Un formato que considero muy interesante es pasarlo a texto plano para impresoras matriciales de tickets:

Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.ExportToFilter( TQRASCIIExportFilter.Create( 'C:\ListadoVentas.txt' ) );

Este formato de texto también viene muy bien para impresoras matriciales de papel continuo que tango nos castigan cuando se desajustan verticalmente.

Por último y no menos importante, tenemos el formato CSV que permite exportar los datos en texto plano entre comillas y separados por comas (ideal para exportar datos a Excel, Access, MySQL, etc.):

Application.CreateForm( TFListadoVentas, FListadoVentas );
FListadoVentas.Informe.ExportToFilter( TQRCommaSeparatedFilter.Create( 'C:\ListadoVentas.csv' ) );

Con esto cubrimos todas las necesidades de exportación de nuestros informes a otros programas ofimáticos y de terceros. Estos últimos filtros que hemos visto también tiran de la unidad QRExport.

No os podéis ni imaginar la cantidad de horas que he tenido que echar para averiguar como se exporta a todos estos formatos (mirando el código fuente). Todavía no entiendo por que narices los que fabrican componentes tan buenos como estos no les sale de los ... de escribir una documentación y ejemplos decentes (y eso también va por los Indy). No me extraña de que algunos programadores abandonen Delphi y elijan otros lenguajes por esta dificultad. No entiendo que a estas alturas (Delphi 2009) los programadores de Delphi tengamos que sufrir tanto para averiguar cuatro tonterías. A ver si "patera" technlogies se pone las pilas de una vez.

La semana que viene seguiré investigando para sacarle todo el partido posible a QuickRerport.

Pruebas realizadas en RAD Studio 2007.

16 enero 2009

Creación de informes con QuickReport (III)

Hoy vamos a ver como modificar la impresión de un informe en tiempo real para poder cambiar su aspecto. También veremos otro ejemplo de un informe con detalle y subdetalle.

MODIFICANDO LA IMPRESIÓN EN TIEMPO REAL


Supongamos que en el listado de clientes que vimos en ejemplos anteriores hemos añadido la columna SALDO INICIAL y nos interesa que salgan todos los importes en negro menos aquellos con saldo negativo que aparecerán en rojo:

Cuando se insertan campos en un informe, Delphi suele darles el nombre QRDBText1, QRDBText2, etc. Debemos acostumbrarnos a poner en la propiedad Name de estos componentes el mismo nombre que tiene el campo en la tabla, en nuestro caso SALDOINICIAL. Sólo para los campos de la base de datos, no es necesario para etiquetas y figuras gráficas.

También sería recomendable ponerle nombre a la banda donde se imprimen los campos (Detalle).

Luego programamos el evento BeforePrint de la banda Detalle del siguiente modo:

procedure TFListadoClientes.DetalleBeforePrint(Sender: TQRCustomBand;
var PrintBand: Boolean);
begin
if Form1.Clientes['SALDOINICIAL'] >= 0 then
SALDOINICIAL.Font.Color := clBlack
else
SALDOINICIAL.Font.Color := clRed;
end;

El evento BeforePrint (antes de imprimir) se va a ejecutar antes de dibujar cada registro en la banda. De este modo podemos cambiar en tiempo real la visualización de cada campo antes de que se plasme en el folio.

Otro caso especial que nos puede solucionar este método es el de modificar los campos booleanos para que no imprima True o False. Por ejemplo, si quiero imprimir el campo activo pasaría esto:


Para solucionar esto en vez de imprimir directamente el campo ACTIVO utilizando un objeto TQRBDText lo que vamos a hacer es meter una etiqueta llamada EActivo (TQRLabel):


Ahora modificamos el método BeforePrint de la banda Detalle:

procedure TFListadoClientes.DetalleBeforePrint(Sender: TQRCustomBand;
var PrintBand: Boolean);
begin
if Form1.Clientes['SALDOINICIAL'] >= 0 then
SALDOINICIAL.Font.Color := clBlack
else
SALDOINICIAL.Font.Color := clRed;

if Form1.Clientes['ACTIVO'] then
EActivo.Caption := 'SI'
else
EActivo.Caption := 'NO';
end;

Si el campo ACTIVO está a True imprimimos SI, NO en canso contrario. Estos truquillos nos ahorran mucho tiempo y evitan tener que modificar los datos de la tabla original.

Hasta modemos meter en la banda figuras gráficas o iconos según los datos del registro actual. Pongamos por ejemplo que a los clientes cuyo saldo inicial sea superor a 800 euros le ponemos el icono de un lápiz a lado como advertencia para su edición:

Lo que he hecho es insertar el objeto imagen TQRImage con el icono del lápiz (a través de su propiedad Picture) y luego vuelvo a ampliar el evento BeforePrint de este modo:

procedure TFListadoClientes.DetalleBeforePrint(Sender: TQRCustomBand;
var PrintBand: Boolean);
begin
if Form1.Clientes['SALDOINICIAL'] >= 0 then
SALDOINICIAL.Font.Color := clBlack
else
SALDOINICIAL.Font.Color := clRed;

if Form1.Clientes['ACTIVO'] then
EActivo.Caption := 'SI'
else
EActivo.Caption := 'NO';

if Form1.Clientes['SALDOINICIAL'] >= 800 then
Icono.Enabled := True
else
Icono.Enabled := False;
end;

Como puede verse en el código he utilizado la propiedad Enabled en vez de la propiedad Visible para ocultar el icono. La propiedad Visible no tiene ningún efecto en QuickReport (por lo menos a mi no me ha funcionado en ningún caso).

Con estos pequeños retoques nuestros informes parecerán mucho más profesionales (y a nuestros clientes les sacamos más pasta diciendo que nos ha costado mucho hacerlo :)

CREANDO UN INFORME CON DETALLE Y SUBDETALLE

Ahora vamos a complicar un poco más el asunto y vamos hacer un listado desglosado de facturas con dos bandas de tipo detalle. Más bien con un detalle y un subdetalle:


Este listado muestra las cabeceras de las facturas (los totales) y sus líneas de detalle. Este sería su diseño en QuickReport:


El documento se compone de tres bandas:

Cabecera: esta banda es un objeto TRQBand y va a ser de tipo rbTitle. En esta banda sólo vamos a meter el título del listado:


Detalle: también va a ser un objeto TRQBand y su tipo será rbDetail. En esta banda necesitamos los títulos de la factura así como los campos de la misma:


El único campo que no hay que vincular con la tabla Factura es el NOMBRE que va vinculado a la tabla CLIENTES.

SubDetalle: aquí introducimos una banda de tipo TQRSubDetail. No es lo mismo una banda TRQBand de tipo rbSubDetail que una banda TQRSubDetail. La primera sólo suele utilizarse para totalizar datos (sin vincularlo a tablas) pero esta última tiene la propiedad DataSet que permite vincular sus campos a otra tabla:


Ahora viene la vinculación con las tablas. Primero hay que seleccionar el objeto TQuickRep y vincular a su propiedad DataSet la tabla Facturas (la cabecera). En la banda de Detalle vinculamos todos los campos a la tabla Facturas.

En la banda SubDetail tenemos que vincular los campos a la tabla Detalle (el contenido de la factura).

Si ejecutáramos el listado como está, nos pasaría esto:


Han ocurrido dos cosas:

1º Al no filtrar la tabla de Clientes por cada factura resulta que aparece el mismo cliente en ambas facturas. Debería ser: CLIENTES.ID = FACTURAS.IDCLIENTE.

2º Al no filtrar la tabla Detalle por cada factura nos aparece el detalle de ambas facturas debajo de cada cabecera. Debería ser así: DETALLE.IDFACTURA = FACTURAS.ID.

Para solucionar esto tenemos que escribir este código en el evento BeforePrint (antes de imprimir) de la banda Detalle:

procedure TFListadoFacturas.DetalleBeforePrint(Sender: TQRCustomBand;
var PrintBand: Boolean);
begin
with Form1 do
begin
Clientes.Filter := 'ID=' + IntToStr(Facturas['IDCLIENTE']);
Clientes.Filtered := True;

Detalle.Filter := 'IDFACTURA=' + IntToStr(Facturas['ID']);
Detalle.Filtered := True;
end;
end;

Lo que hacemos es filtrar el cliente y el detalle de la factura respecto a la factura que estamos imprimiendo en cada momento. De este modo no hay que recurrir a sentencias SQL para filtrar la tablas (aunque también se podría hacer así).

En la siguiente parte de este artículo veremos más ejemplos de informes así como la forma de exportar a PDF y otros formatos.

Pruebas realizadas en RAD Studio 2007.

Publicidad