Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Almacenamiento estructurado

No hay nada tan molesto como trabajar día a día con una aplicación que insiste en olvidar nuestros ajustes de formato. La columna de salarios es demasiado estrecha, y cada lunes, martes o miércoles tenemos que ampliarla antes de imprimir... pero al día siguiente hay que repetir la misma operación. ¿Le parece exagerado? Se ve entonces que Ud. no ha utilizado demasiado la ventana de búsqueda de ficheros de Windows 98...

ALMACENANDO DESCENDIENTES DE TPERSISTENT

En concreto, ¿existe alguna forma sencilla para que una rejilla recuerde el ancho de sus columnas? ¡Por supuesto que sí! Podemos utilizar el registro de Windows para ello. Nos basta con seleccionar una clave adecuada, es decir, una ruta en el árbol del registro, y crear dentro de esa clave tantos "valores" como columnas tenga la rejilla. En cada valor, que podemos identificar con la ayuda del nombre del campo asociado a la columna, almacenaremos el ancho de la misma.

Pero recuerde que el usuario puede mover columnas dentro de una rejilla. ¿Podemos almacenar también el orden de las columnas? Tendríamos que crear un valor adicional, para almacenar ese orden y restaurarlo posteriormente. ¿Y qué tal si permitimos que el usuario esconda o muestre columnas? Pues que el formato de grabación en el registro podría complicarse considerablemente...

Afortunadamente, existe una técnica en la VCL que vuelve a simplificarlo todo. Todos los componentes de Delphi tienen ya programada la posibilidad de guardar su estado en un fichero o, de forma más general, en un stream. Esto se debe a la necesidad de utilizar ficheros DFM para configurar componentes durante la carga de una aplicación. Es cierto que sólo es posible guardar directamente objetos descendientes de TComponent, y que en este ejemplo lo que queremos guardar es una colección de la clase TDBGridColumns, que desciende de TCollection e, indirectamente, de TPersistent.

Pero existe un truco sencillo para superar este obstáculo: crear un componente auxiliar que almacene, como única propiedad publicada, una referencia al objeto que deseamos guardar. De hecho, el escritor de la unidad DBGrids ya se tomó esta molestia en su momento. Si busca en el código fuente de dicha unidad, encontrará la siguiente declaración de clase:

type
  TColumnsWrapper = class(TComponent)
  private
    FColumns: TDBGridColumns;
  published
    property Columns: TDBGridColumns
      read FColumns write FColumns;
  end;

Con la ayuda de esta clase, el método siguiente de la colección de columnas puede guardar la configuración de columnas de una rejilla en un flujo de datos:

procedure TDBGridColumns.SaveToStream(S: TStream);
var
  Wrapper: TColumnsWrapper;
begin
  Wrapper := TColumnsWrapper.Create(nil);
  try
    Wrapper.Columns := Self;
    S.WriteComponent(Wrapper);
  finally
    Wrapper.Free;
  end;
end;

En pocas palabras: tenemos la suerte de poder aplicar directamente los métodos SaveToStream y LoadFromStream (y sus equivalentes sobre ficheros en disco) sobre la propiedad Columns de cualquier objeto TDBGrid.

Ahora bien, esta técnica no se compagina bien con el uso del registro de Windows. Por mencionar sólo lo obvio: el volumen de datos que se guardaría sería demasiado grande, en comparación con el volumen que requiere la técnica mencionada al principio del truco, y podría comprometer desfavorablemente la rapidez de acceso al registro. Recuerde que debe guardar la configuración de las columnas de todas las rejillas de datos que pueda tener su aplicación.

BIENVENIDO SEA EL ALMACENAMIENTO ESTRUCTURADO

¿Y qué tal si guardásemos esas configuraciones de columnas en ficheros? No sería buena idea, porque el número de ficheros creado sería considerable, y el usuario se quejaría. Hay otro problema: es posible que varios usuarios utilicen el mismo ordenador, y hay que tener esto en cuenta si queremos que cada uno conserve su propia configuración. Usando el registro, la solución consistía en basar la clave en el nodo HKEY_CURRENT_USER. Con ficheros, lo más "limpio" sería tener un subdirectorio diferente para cada usuario, pero tropezaríamos nuevamente con los problemas de fragmentación del disco...

Nuestra salvación será una técnica aparentemente exótica, pero que acompaña a cualquier versión de Windows: el almacenamiento estructurado (structured storage), una de las técnicas ofrecidas por COM. El almacenamiento estructurado tiene varias aplicaciones dentro del propio COM, pero aquí nos limitaremos a una de sus facetas: la posibilidad de simular todo un sistema virtual de ficheros y carpetas sobre un único fichero físico del sistema operativo.

Para este propósito, en la unidad ActiveX se definen dos tipos de interfaz: IStorage, que corresponde aproximadamente al concepto de carpeta o directorio, y IStream, que correspondería a los ficheros. Cuando abrimos o creamos un fichero para que actúe como almacenamiento estructurado, recibimos a cambio un puntero de tipo IStorage, como si se tratase de la carpeta raíz del sistema virtual.

Si desea abrir un fichero de este tipo, o crearlo si aún no existe, puede utilizar la siguiente rutina:

function OpenStorage(const AFileName: WideString): IStorage;
const
  StgFlags = STGM_READWRITE or STGM_SHARE_EXCLUSIVE;
begin
  if not Succeeded(StgOpenStorage(PWideChar(AFileName), nil,
    StgFlags, nil, 0, Result)) then
    OleCheck(StgCreateDocfile(PWideChar(AFileName),
      StgFlags or STGM_FAILIFTHERE, 0, Result));
end;

El parámetro AFileName se refiere a un nombre de fichero "tradicional". Para compilar la anterior función, deberá añadir las unidades ActiveX, ComObj a la cláusula uses de la unidad donde defina la función. De paso, incluya también la unidad AxCtrls, que necesitaremos más adelante.

Una vez que tenemos en nuestras manos un puntero de tipo IStorage, podemos abrir flujos de datos en su interior, o crearlos, si es que todavía no existen:

function OpenStream(S: IStorage;
  const AName: WideString): IStream;
const
  StgFlags = STGM_READWRITE or STGM_SHARE_EXCLUSIVE;
begin
  if not Succeeded(S.OpenStream(PWideChar(AName), nil,
    StgFlags, 0, Result)) then
    OleCheck(S.CreateStream(PWideChar(AName),
      STGM_CREATE or StgFlags, 0, 0, Result));
end;

En este caso, AName representa un nombre "lógico" de fichero dentro del almacenamiento estructurado, no un fichero físico.

¿Y qué hacemos ahora con el puntero de tipo IStream? Pues muy fácil: en vez de llamar directamente los métodos de la interfaz, podemos utilizar el puntero para construir un objeto de la clase TOLEStream de Delphi. Esta clase es compatible con los métodos LoadFromStream y SaveToStream de TDBGridColumns, que es adonde queríamos llegar...

Con todos estos elementos en la mano, podemos abrir o crear un fichero de almacenamiento estructurado al abrir la aplicación, para intentar leer la configuración de una rejilla:

procedure TMain.FormCreate(Sender: TObject);
var
  S: TOleStream;
begin
  Stg := OpenStorage(ChangeFileExt(Application.ExeName, '.dat'));
  S := TOleStream.Create(OpenCreateStream(Stg, 'Customers'));
  try
    if S.Size > 0 then
      gdCustomers.Columns.LoadFromStream(S);
  finally
    FreeAndNil(S);
  end;
end;

La variable Stg, de tipo IStorage, debe estar declarada dentro del formulario, o dentro de un módulo de datos global. Simétricamente, al cerrar la aplicación deberíamos ejecutar el siguiente método:

procedure TMain.FormClose(Sender: TObject;
  var Action: TCloseAction);
var
  S: TOleStream;
begin
  S := TOleStream.Create(OpenStream(Stg, 'Customers'));
  try
    gdCustomers.Columns.SaveToStream(S);
  finally
    FreeAndNil(S);
  end;
end;

UN PAR DE CONSEJOS

En primer lugar, tendrá que echar un vistazo a las funciones CreateStorage y OpenStorage, para crear subcarpetas dentro del fichero principal. Recuerde que ésta es la forma más sencilla de organizar los datos de configuración cuando hay más de un usuario por ordenador. El nombre del usuario activo puede obtenerse mediante la función GetUserName, del API de Windows.

En segundo lugar, no olvide proporcionar al usuario algún sistema sencillo para que descarte los cambios de configuración realizados. Puede ser algo tan simple como que borre el fichero de configuración; en tal caso, asegúrese de que su aplicación pueda funcionar correctamente sin él. Para explicarle el porqué de mi advertencia, le contaré un bug bastante molesto con el que han tropezado varios programadores, incluido un servidor, al trabajar con el SQL Explorer. Resulta que el tamaño del panel donde se teclean las instrucciones SQL se puede variar con la ayuda de un splitter. En particular, se puede hacer cero. A partir de ese desdichado momento, no podrá recuperar el panel: la única solución es entrar en el registro, adivinar cuál es la clave involucrada en el fallo, y modificarla manualmente. Claro, esto puede ser imposible de hacer si almacenamos la configuración en un fichero de formato privado como son los ficheros del almacenamiento estructurado.

Por último: la técnica de guardar componentes utilizando la persistencia implementada por Delphi tampoco es la panacea. Para empezar, el formato utilizado tiene muchas redundancias, y los ficheros obtenidos no son todo lo pequeño que podrían ser. Además, a pesar de que son difíciles de leer por una persona "inocente", un hacker con ganas de hacer daño no tendría mayores problemas para descifrar su contenido. Por esto, quizás sea conveniente para algunos casos, el comprimir o cifrar el contenido del stream antes de guardarlo en disco.