17 abril 2009

Crea tu propio editor de informes (y V)

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

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

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

GUARDANDO TODO EL INFORME EN XML

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


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

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

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

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

XML.ChildNodes.Clear;

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

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

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

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

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

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

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

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

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

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

Así quedaría después de guardarlo:


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

CARGARDO EL INFORME DESDE UN ARCHIVO XML

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

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

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

XML.LoadFromFile(AbrirXML.FileName);

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

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

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

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

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

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

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

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

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

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

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

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

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

Por último, seleccionamos la banda actual:

FNuevoInforme.SeleccionarBandaActual;

Al final tiene que quedarse igual que el informe original:


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

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

DESCARGAR EL PROYECTO

Rapidshare:

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

Megaupload:

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



Pruebas realizadas en RAD Studio 2007.

Publicidad