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

Controles de listas y grupos

La versión de la Common Control Library que acompañó a Windows XP introdujo muchas mejoras en el aspecto de los controles. El cambio más notable fue el soporte para temas, pero hay una larga lista de mejoras adicionales, como el soporte para grupos en controles de listas (list views). Uno de los primeros beneficiados por esta característica fue el Explorador de Windows:

Windows Explorer para Windows XP

Es probable que ésta sea también la técnica que utiliza Outlook para agrupar mensajes:

Outlook 2003

En este caso, los grupos pueden colapsarse por medio de un botón añadido a la cabecera. El control de listas no ofrece esta funcionalidad "de serie", pero lo más probable es que los programadores de Outlook hayan añadido un botón dibujando sobre el control. En cualquier caso, el API de los grupos en el control de listas soporta también grupos "colapsados".

No obstante, es difícil asegurar que Outlook utilice este recurso... porque se necesita la versión de la Common Controls Library que viene con WinXP o una versión posterior. Además, en teoría, no se puede instalar esta versión en un sistema operativo anterior.

ACTIVACION DE LOS GRUPOS

Escribo este artículo con la ayuda de un sencillo editor de textos programado "en casa", con un soporte muy básico para HTML (HTML Pad). No me gustan la mayoría de los editores "profesionales" de páginas Web, porque suelen generar mucha porquería innecesaria. Tratándose de una página técnica como la mía, me importa un pepino si el aspecto visual puede mejorarse o no. Mi objetivo: que las páginas se puedan cargar con rapidez y que sean legibles.

HTML Pad - Links paneHace un par de semanas, en uno de esos extraños y cada vez menos frecuentes momentos de libertad, se me ocurrió que podía mantener una lista con los enlaces de la página en edición dentro de un panel del editor, como la que muestro en la imagen de la derecha. Gracias a este panel, me resulta muy sencillo comprobar si tengo algún enlace roto dentro de la página que estoy editando. Puedo también cargar páginas dentro del editor, cuando se trata de páginas locales, o mostrar páginas externas mediante el Internet Explorer, o incluso abrir con la ayuda del shell cualquier otro tipo de documento local, como las imágenes, los ficheros PDF, etc.

Al haber tanta diferencia en la acción disparada por un clic del ratón sobre un enlace, es casi obligatorio dividir los enlaces de acuerdo a su tipo: no es muy cómodo que un clic determinado abra Internet Explorer, y que un error de un par de píxeles con el ratón provoque la carga de una página local. Para separar los enlaces de acuerdo a su tipo tenía dos posibilidades. La primera: usar un control de árboles. Sin embargo, es un desperdicio usar un árbol para representar sólo un par de niveles. Además, la posibilidad de colapsar nodos me pareció "peligrosa", o como mínimo, incómoda para el usuario de la aplicación (¡yo!). Por lo tanto, sólo me quedaba la segunda opción: utilizar el soporte para grupos de los controles de listas, tal y como muestra la imagen.

Por desgracia, y como podrá imaginar, Borland no actualiza las unidades necesarias desde finales del pasado siglo (o casi). No se trata ya de incorporar la nueva funcionalidad en el control TListView. Al fin y al cabo, estamos hablando de recursos que no se pueden utilizar en todas las versiones existentes de Windows. Eso sería justificable: el problema real es que ni siquiera se han molestado en actualizar las unidades de bajo nivel que dan acceso directo al API de la librería. La clase TListView se declara en una gigantesca unidad llamada ComCtrls, mientras que el API de bajo nivel corresponde a la unidad CommCtrl.

La técnica que voy a emplear aquí es la de clases interpuestas. Esto nos permitirá evitar el registro de componentes en el IDE de Delphi. Creamos una unidad vacía (en mi caso, la llamé UGroups), y declaramos una clase TListView en su interfaz:

type
  TListView = class(ComCtrls.TListView)
  protected
    procedure CreateWnd; override;
  public
    // ...
  end;

Note cómo nuestra clase se llama igual que la clase de componentes que ofrece Delphi. Nuestra clase hereda directamente de la clase délfica, para la cual necesitamos cualificar la referencia a esta última. Doy por descontado que ha añadido la unidad ComCtrls a la cláusula uses de la interfaz de nuestra unidad.

El primer paso es redefinir el método virtual protegido CreateWnd. Dentro de este método debe incluirse la inicialización del objeto de ventanas del sistema operativo. Para un control más profesional, tendríamos que incluir una propiedad para activar y desactivar el soporte de grupos, pero por simplificar, vamos a activar siempre esta característica:

procedure TListView.CreateWnd;
begin
  inherited CreateWnd;
  SendMessage(Handle, LVM_ENABLEGROUPVIEW, 1, 0);
end;

Primer problema: la constante LVM_ENABLEGROUPVIEW, que no viene declarada en ningún lugar. Tenemos que añadir esta declaración de constante en nuestra unidad, preferiblemente dentro de la sección de implementación:

const
  LVM_ENABLEGROUPVIEW = LVM_FIRST + 157;

¿De dónde he sacado este valor? Si usted tiene instalado Visual Studio con soporte para C++, encontrará esta declaración dentro del fichero commctrl.h del directorio include de la instalación. Si no tiene Visual Studio, puede comprar el CD con el Platform SDK por un precio simbólico, directamente en las páginas de Microsoft. En el SDK también se incluyen los ficheros necesarios. Créame: más temprano que tarde, los va a necesitar.

CREACION DE GRUPOS

El siguiente paso: añadir un método público para crear grupos:

type
  TListView = class(ComCtrls.TListView)
    // ...
  public
    procedure AddGroup(AGroupID: Integer; ACaption: PWideChar);
    // ...
  end;

Por suerte, la metodología de programación de la Common Controls Library es muy predecible. Para crear un grupo, tenemos que rellenar un formulario impreso... perdón, tenemos que inicializar un record con la descripción del grupo, para enviarlo a continuación, como parámetro de un mensaje, al control de listas:

procedure TListView.AddGroup(AGroupID: Integer; ACaption: PWideChar);
var
  Group: TLvGroup;
begin
  FillChar(Group, SizeOf(Group), 0);
  Group.cbSize := SizeOf(Group);
  Group.mask := LVGF_HEADER or LVGF_ALIGN or LVGF_GROUPID;
  Group.pszHeader := ACaption;
  Group.cchHeader := Length(ACaption);
  Group.iGroupIdL := AGroupID;
  Group.uAlign := LVGA_HEADER_LEFT;
  SendMessage(Handle, LVM_INSERTGROUP, 0, LongInt(@Group));
end;

Nuevamente tropezamos con la necesidad de importar declaraciones a Delphi. Esta es la declaración del tipo TLvGroup:

type
  TLvGroup = record
    cbSize: UINT;
    mask: UINT;
    pszHeader: LPWSTR;
    cchHeader: Integer;
    pszFooter: LPWSTR;
    cchFooter: Integer;
    iGroupIdL: Integer;
    stateMask: UINT;
    state: UINT;
    uAlign: UINT;
  end;

Hay varios detalles típicos. Los primeros cuatro bytes, por ejemplo, indican el tamaño del registro; se trata de una técnica muy útil para evitar problemas con los cambios en el API. El campo mask, por su parte, indica al control cuáles de los restantes campos han sido rellenados explícitamente. En nuestro ejemplo, hemos indicado la descripción literal del grupo (LVGF_HEADER), su alineación (LVGF_ALIGN) y el identificador numérico del grupo (LVGF_GROUPID).

Por último, tenemos esas molestas constantes que tenemos que importar desde el SDK:

const
  LVM_INSERTGROUP = LVM_FIRST + 145;

  LVGF_HEADER  = $00000001;
  LVGF_ALIGN   = $00000008;
  LVGF_GROUPID = $00000010;

  LVGA_HEADER_LEFT   = $00000001;
  LVGA_HEADER_CENTER = $00000002;
  LVGA_HEADER_RIGHT  = $00000004;
Al parecer, el control ordena los grupos de acuerdo a sus descripciones, alfabéticamente. Otro mensaje aceptado por el control (LVM_INSERTGROUPSORTED) permite crear grupos ordenados de acuerdo a una función que debemos indicar durante la creación de los mismos.

ASIGNANDO ELEMENTOS A GRUPOS

Sólo nos queda declarar métodos para asociar un elemento dado del control a un grupo particular. Tenemos la clase TListItem de Borland, a alto nivel, para identificar los elementos de la lista. No tenemos una clase separada para los grupos, por lo que tendremos que usar sus identificadores numéricos. Bajo estas suposiciones, estos son los métodos que necesitamos:

type
  TListView = class(ComCtrls.TListView)
    // ...
  public
    // ...
    function GetGroup(AItem: TListItem): Integer;
    procedure SetGroup(AItem: TListItem; AGroupID: Integer);
  end;

Como verá, podemos agrupar estos dos métodos por medio de una propiedad vectorial:

type
  TListView = class(ComCtrls.TListView)
    // ...
  public
    // ...
    property Group[AItem: TListItem]: AGroupID
      read GetGroup write SetGroup;
    // ...
  end;

No es muy elegante, desde el punto de vista metodológico, pero le aseguro que es efectivo.

A primera vista, hay un mensaje (LVM_MOVEITEMTOGROUP) que parece servir para asociar un elemento a un grupo. Pero he tenido problemas con este mensaje. Más eficaz es utilizar directamente los mensajes generales LVM_GETITEM y LVM_SETITEM para leer y escribir el identificador de grupo asociado a un elemento. Borland necesita usar este mensaje y el tipo asociado para su propio TListView, pero una breve inspección a la CommCtrl nos enfría los ánimos: efectivamente, Borland declara el tipo necesario... pero no incluye todos los campos soportados por la versión de la biblioteca en Windows XP. Otra vez tenemos que declarar el tipo de datos:

type
  TLvItemA = record
    mask: UINT;
    iItem: Integer;
    iSubItem: Integer;
    state: UINT;
    stateMask: UINT;
    pszText: PAnsiChar;
    cchTextMax: Integer;
    iImage: Integer;
    lParam: lParam;
    iIndent: Integer;
    iGroupId: Integer;
    cColumns: UINT;
    puColumns: PUINT;
  end;

También necesitaremos un indicador binario para avisar que estamos usando el campo iGroupId del tipo anterior:

const
  LVIF_GROUPID = $0100;

Ahora podemos implementar los dos métodos de esta sección:

function TListView.GetGroup(AItem: TListItem): Integer;
var
  Item: TLvItemA;
begin
  FillChar(Item, SizeOf(Item), 0);
  Item.mask := LVIF_GROUPID;
  Item.iItem := AItem.Index;
  SendMessage(Handle, LVM_GETITEM, 0, LongInt(@Item));
  Result := Item.iGroupId;
end;

procedure TListView.SetGroup(AItem: TListItem; AGroupID: Integer);
var
  Item: TLvItemA;
begin
  FillChar(Item, SizeOf(Item), 0);
  Item.mask := LVIF_GROUPID;
  Item.iItem := AItem.Index;
  Item.iGroupId := AGroupID;
  SendMessage(Handle, LVM_SETITEM, 0, LongInt(@Item));
end;

Y en principio, ya tenemos todo lo que necesitamos.

PONIENDO A PRUEBA EL INVENTO

¿Probamos la nueva clase? En un formulario vacío, deje caer un control TListView. Compile el proyecto (para que el IDE añada las unidades necesarias en la cláusula uses) y luego modifique manualmente dicha cláusula:

uses
  Windows, Messages, SysUtils, Variants, Classes,
  Graphics, Controls, Forms, Dialogs, ComCtrls,
  ToolWin, ActnList, ImgList, StdCtrls, ShellAPI,
  ExtCtrls, Menus, UGroups;

Es indispensable que nuestra unidad, UGroups, aparezca mencionada después de la unidad que contiene el TListView original de Borland, ComCtrls. Recuerde que aquí está la clave de truco de las clases interpuestas.

Para crear los grupos, le recomiendo usar un método auxiliar parecido al siguiente:

procedure TEnlaces.DefinirGrupos;
begin
  ListView.AddGroup(GID_RELATIVE, 'Relative URLS');
  ListView.AddGroup(GID_DOCUMENT, 'Documents');
  ListView.AddGroup(GID_LOCAL, 'Local relative URLS');
  ListView.AddGroup(GID_ABSOLUTE, 'Absolute HTTP URLS');
  ListView.AddGroup(GID_OTHER, 'Other URLS');
end;

Las constantes GID_xxx pertenecen al código fuente del HTMLPad. Puede sustituirlas directamente por los valores numéricos que se le antojen.

No quiero presentar un ejemplo complicado, pero lo que queda es muy sencillo. Debe seguir este algoritmo:

  // Crear grupos...
  DefinirGrupos;
  // Crear elementos...
  // ...
  // Asignar elementos de acuerdo a su grupo
  for I := 0 to ListView.Items.Count - 1 do
  begin
    Item := ListView.Items[I];
    ListView.Group[Item] := DecidirGrupo(Item);
  end;

Tenga presente que los grupos a los que no se asocien elementos, serán ocultados automáticamente por el control.