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.

09 enero 2009

Creación de informes con QuickReport (II)

Después de crear nuestro primer informe con QuickReport vamos a hacer otro a dos bandas el cual va a ser un listado de clientes. En un listado cualquiera necesitamos que la banda superior se mantenga fija en todas las hojas que se impriman y que la banda que va a imprimir lo datos se adapte al folio según los mismos.

CREANDO UN LISTADO SIMPLE A DOS BANDAS

Para cumplir nuestro cometido vamos a crear un nuevo formulario llamado FListadoClientes y dentro del mismo volvemos a añadir el objeto de la clase TQuickRep que llamaremos Informe.

Como necesito imprimir columnas bien anchas entonces debemos imprimir el folio de manera apaisado. Esto se hace pinchando el objeto QuickRep con el botón derecho del ratón y seleccionando Report Settings:


Y aparecerá este cuadro de diálogo:


Como puede verse en la imagen no sólo podemos seleccionar el tipo de folio (A4, A5, B4, etc.) sino que además se puede configurar el folio a un ancho y alto determinado en milímetros que viene muy bien para formatos de documentos que se imprimen en impresoras matriciales con papel continuo (lo más asqueroso para un programador).

En la parte derecha de esta ventana tenemos un ComboBox que por defecto tiene seleccionada la opción Portrait. Esto significa que va a imprimir el folio verticalmente. Como lo vamos a imprimir apaisado seleccionamos LandScape.

Ahora añadimos al objeto TQuickRep un par de bandas (TQRBand) y a la segunda banda le podemos mediante el inspector de objetos a su propiedad BandType el valor rbDetail:


También es buena costumbre ponerle nombre a las bandas para luego poder modificar sus acciones mediante código. A la primera banda le ponemos en su propiedad Name el nombre Cabecera y a la segunda banda la llamamos Detalle.

Insertamos etiquetas en la cabecera mediante objetos TQRLabel y los campos en el detalle mediante TQRDBText:


Al igual que hicimos en el artículo anterior debemos vincular este formulario a la unidad donde están los dados (uses Unit1) y añadir en la propiedad DataSet del objeto TQuickReport la tabla Clientes.

Hay que procurar que la banda de Detalle tenga la altura ajustada a los campos a imprimir, para que la separación de las líneas no sea muy grande y no se desperdicien folios (algunos jefes de empresas son muy tacaños).

Creamos el formulario del informe y lo ejecutamos desde la ventana principal:

Application.CreateForm( TFListadoClientes, FListadoClientes );
FListadoClientes.Informe.Preview;

Al ejecutar nuestro informe ya lo tendremos apaisado:


INFORMES MAESTRO/DETALLE A TRES BANDAS

El siguiente ejemplo que vamos a ver es la impresión de una factura que tiene cabecera, detalle y pie así como los datos de un cliente que viene de otra tabla, es decir, vamos a imprimir datos de tres tablas a la vez.

Como en los casos anteriores creamos un nuevo formulario llamado FImprimirFactura e insertamos el objeto TQuickRep y lo llamamos Informe.

Para este ejemplo voy a añadir al formulario principal un nuevo objeto TClientDataSet llamado Facturas con los siguientes campos:


Esta sería la cabecera de la factura. Ahora creamos otro ClientDataSet para el Detalle de la factura con estos campos:


Introducimos una factura en la tabla con un par de líneas de detalle:

Facturas.Append;
Facturas['ID'] := 1;
Facturas['FECHA'] := Date;
Facturas['IDCLIENTE'] := 1;
Facturas['BASEIMPONIBLE'] := 300;
Facturas['IVA'] := 48;
Facturas['TOTAL'] := 348;
Facturas.Post;

Detalle.Append;
Detalle['IDFACTURA'] := 1;
Detalle['UNIDADES'] := 1;
Detalle['ARTICULO'] := 'MOVIL SAMSUNG';
Detalle['PRECIO'] := 100;
Detalle['TOTALLINEA'] := 100;
Detalle.Post;

Detalle.Append;
Detalle['IDFACTURA'] := 1;
Detalle['UNIDADES'] := 1;
Detalle['ARTICULO'] := 'MOVIL NOKIA';
Detalle['PRECIO'] := 200;
Detalle['TOTALLINEA'] := 200;
Detalle.Post;

A continuación creamos un nuevo formulario llamado FImprimirFactura e insertamos un objeto TQuickRep con tres bandas:


A las bandas las vamos a llamar Cabecera, Detalle, Pie y serán del tipo (BandType) rbTitle, rbDetail y rbPageFooter respectivamente.

Como hicimos en ejemplos anteriores, vinculamos a formulario con el formulario principal para poder traernos los datos de las tablas (uses Unit1). Seleccionamos el objeto TQuickRep y vinculamos mediante la propiedad DataSet la tabla Detalle.

Aunque pueda parecer extraño, cuando vamos a imprimir un documento maestro/detalle debemos vincular a la propiedad DataSet del objeto TQuickRep la tabla Detalle y no la del maestro. Esto se debe a que es el detalle la banda que se va a repetir en la factura, es decir, el centro de la misma. La cabecera y el pie son sólo datos adicionales. Eso no quita que luego cada campo se vincule con una tabla distinta. No hay limitaciones a la hora de vincular campos con tablas.

Pero vayamos por partes. Primero implementamos la cabecera de la factura en la primera banda:


Los campos ID y FECHA están vinculados con la tabla FACTURAS. En cambio todos los campos encerrados en el rectángulo están vinculados con la tabla CLIENTES (sólo hay que procurar tener filtrado que FACTURA.IDCLIENTE = CLIENTES.ID).

Lo demás son todo etiquetas TQRLabel. El rectángulo y las líneas que he puesto debajo de los títulos de los campos del detalle lo he hecho con un objeto TQRShape que en su propiedad Shape soporta estos formatos:

qrsRectangle: Un rectángulo.
qrsCircle: Un círculo.
qrsHorLine: Línea horizontal.
qrsVerLine: Línea vertical.
qrsRoundRect: Rectángulo con las esquinas redondeadas.
qrsRightAndLeft: dos líneas verticales (un rectángulo sin techo ni suelo).
qrsTopAndBottom: dos líneas horizontales (un rectángulo sin paredes).

En mi formato de factura sólo he utilizado líneas horizontales y un rectángulo.

Vamos ahora con el detalle de la factura en la segunda banda:


Esta banda sólo va a contener campos TQRDBText vinculados a la tabla DETALLE. Es importante quitar en este tipo de campos el valor Autosize y ajustar el ancho manualmente para que por ejemplo el nombre del artículo no chafe el precio en el caso de que el nombre sea demasiado largo.

También es importante que todos los campos de tipo float estén alineados a la derecha mediante su propiedad Alingment = taRightJustify.

Hay que procurar que la altura de la banda de detalle sea un poco más grande que la altura de los campos para que el espacio entre líneas no sea ni muy ancho ni muy estrecho.

Vamos con la última banda, el pie:


Esta banda sólo contiene los tres campos que totalizan la factura y que están vinculados con la tabla FACTURAS. Un detalle que hay que tener presente es que la posición vertical a la que van a salir estos totales es inversamente proporcional a la altura de la banda de tipo rbPageFooter, es decir, si queremos que los totales salgan más arriba hay que ampliar verticalmente la altura de esta banda. Y si queremos que los totales salgan pegados en la parte inferior del folio entonces hay que procurar que esta banda sea lo más estrecha posible (verticalmente).

Este seria nuestro diseño de la factura:



Llamamos a la vista previa como siempre:

Application.CreateForm( TFImprimirFactura, FImprimirFactura );
FImprimirFactura.Informe.Preview;

Y esta sería la vista previa al imprimirla:


Esta sería la parte superior:


Y la parte inferior:


También sería recomendable que los campos que vamos a imprimir de tipo moneda tengan definida su máscara a dos decimales para evitar problemas de redondeo. Esto se hace en el diseño de la tabla original a través de la propiedad DisplayFormat:


De modo que al imprimir aparezcan los dos decimales:

Estos tres ejemplos que hemos visto son los tres típicos informes que nos suelen pedir: un formulario, un listado o un documento maestro/detalle (y pie). Aunque aquí no se acaban nuestras pesadillas ya que esto solo son juguetes comparado con lo que los clientes suelen pedir de verdad.

En el próximo artículo veremos casos más complejos y algunos truquillos para modificar el aspecto del informe en tiempo real.

Pruebas realizadas en RAD Studio 2007.

Publicidad