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

Alineación a la medida

Delphi 6 introdujo un nuevo valor para la propiedad Align que, a pesar de las posibilidades que ofrece, se utiliza muy poco... por culpa de la mala documentación sobre la misma.

LA TECNICA

Si hace memoria, recordará que todos los controles heredan una propiedad Align, de tipo TAlign, para ajustar dinámicamente el tamaño y posición de los controles cuando el control que los contiene cambia de tamaño. TAlign definía cinco valores, desde la época de Delphi 1, y Delphi 6 ha añadido una nueva constante, alCustom, a su dominio.

Sin embargo, hay problemas con alCustom: si asignamos este valor en la propiedad de un control arbitrario en tiempo de diseño, aparentemente no ocurrirá nada. Además, ni la documentación aclara gran cosa, ni vienen demostraciones sobre cómo utilizar la nueva constante.

En parte, esto sucede porque lo más importante al asignar alCustom a un control no es el propio control, ¡sino el comportamiento del control que lo contiene! Supongamos que colocamos cuatro botones sobre un formulario, y que cambiamos sus propiedades Align a alCustom. Es entonces el formulario quien tiene plena libertad para elegir cómo distribuye estos botones cuando cambie su tamaño. Por omisión, nada debe ocurrir: los controles mantendrán su posición y tamaño. Si queremos que algo suceda, tendremos que redefinir dos métodos virtuales en el formulario:

function CustomAlignInsertBefore(
    C1, C2: TControl): Boolean; override;
procedure CustomAlignPosition(Control: TControl;
    var NewLeft, NewTop, NewWidth, NewHeight: Integer;
    var AlignRect: TRect; AlignInfo: TAlignInfo); override;

El primero de los métodos se utiliza durante la primera fase de la alineación, para crear una lista de los controles con alCustom y ordenarlos. En la segunda fase, se ejecuta el segundo método para cada control de la lista construida en la fase anterior. El primer parámetro es, como podrá imaginar, el control que se va a alinear, y los cuatro siguientes parámetros sirven para que especifiquemos su nueva posición y tamaño. AlignRect indica cuánto espacio tenemos a nuestra disposición. No olvide que los controles con alineación "tradicional" roban espacio una vez alineados.

El último parámetro es sumamente importante... y sin embargo, la ayuda en línea ni siquiera lo menciona. Esta es la declaración de su tipo de datos:

type
  TAlignInfo = record
    AlignList: TList;
    ControlIndex: Integer;
    Align: TAlign;
    Scratch: Integer;
  end;

En AlignList tenemos la lista de los controles a alinear. ControlIndex se va actualizando automáticamente para indicar en cada momento cuál es el índice dentro de la lista anterior del control que debemos realinear. Align contiene la alineación del control (¡alCustom, por supuesto!) y Scratch está ahí para que hagamos con él lo que nos plazca (observe, no obstante, que el parámetro se pasa por valor)... y de paso para que TAlignInfo tenga 16 bytes de tamaño.

UN EJEMPLO CON CUADROS DE LISTAS

Veamos primero un ejemplo muy sencillo. Traiga dos componentes TListBox a un formulario vacío, y ordénelos más o menos de esta manera:

Alineación a la medida

Cambie la propiedad Align de ambos controles a alCustom. Vaya entonces a la declaración de la clase del formulario, y añada las declaraciones de los dos métodos virtuales mencionados anteriormente en una sección protected. Pulse Ctrl+May+C para que Code Completion cree los esqueletos de las implementaciones. Esta será la de CustomAlignInsertBefore:

function TForm1.CustomAlignInsertBefore(C1, C2: TControl): Boolean;
begin
  Result := (C1 is TWinControl) and (C2 = TWinControl) and
    (TWinControl(C1).TabOrder < TWinControl(C2).TabOrder);
end;

Aquí estamos haciendo una suposición bastante razonable: que el orden de alineación debe coincidir con el orden de tabulación. Como sólo los controles nativos Windows (en contraste con los descendientes de TGraphicControl) pueden recibir el foco del teclado y tener un orden de tabulación, es natural preguntar primero si ambos controles descienden de TWinControl.

El verdadero código de alineación es el siguiente:

procedure TForm1.CustomAlignPosition(Control: TControl;
  var NewLeft, NewTop, NewWidth, NewHeight: Integer;
  var AlignRect: TRect; AlignInfo: TAlignInfo);
begin
  if Control = ListBox1 then
    NewLeft := 8
  else
    NewLeft := ClientWidth div 2 + 8;
  NewTop := 8;
  NewWidth := ClientWidth div 2 - 16;
  NewHeight := ClientHeight - 16;
end;

He aprovechado que dentro del formulario sólo se encuentran los dos cuadros de lista. Con este método logramos que ambos se repartan el espacio anterior del formulario de manera proporcional. Observe que este ejemplo no puede lograrse simplemente con Align e incluso con Anchors.

UN EJEMPLO CON CUADROS DE EDICION

Otra situación en la que podemos aprovechar la alineación a la medida es la distribución automática de controles TEdit y TDBEdit. Observe la siguiente imagen:

Alineación a la medida

¿Qué pasará si reducimos el ancho del formulario en tiempo de ejecución? Si queremos que al hacerlo se redistribuyan los controles manteniendo el formato "horizontal", podemos crear una clase interpuesta para el panel que los contiene, de modo que sepa manejar controles con alineación a la medida.

Para empezar, vamos a interceptar el mensaje CM_CONTROLLISTCHANGE en la nueva clase de panel:

procedure CMControlListChange(var M: TMessage);
    message CM_CONTROLLISTCHANGE;

Este mensaje interno de la VCL se envía al panel cada vez que se inserta o elimina un control de su superficie. Necesitamos enterarnos de esta situación para cambiar la alineación de todos los controles dentro del panel a alCustom. Y es que hay un problema con TEdit: los diseñadores de la VCL decidieron, hace ya mucho tiempo, que su propiedad Align no estuviese disponible en tiempo de diseño.

procedure TPanel.CMControlListChange(var M: TMessage);
begin
  if Boolean(M.LParam) and (TControl(M.WParam) is TWinControl) then
    TControl(M.WParam).Align := alCustom;
end;

La implementación de CustomAlignInsertBefore puede ser similar a la mostrada en el ejemplo anterior. Y la del otro método puede ser como la siguiente:

procedure TPanel.CustomAlignPosition(Control: TControl;
  var NewLeft, NewTop, NewWidth, NewHeight: Integer;
  var AlignRect: TRect; AlignInfo: TAlignInfo);
var
  Prev: TControl;
begin
  if AlignInfo.ControlIndex = 0 then
  begin
    NewLeft := 8;
    NewTop := 8;
  end
  else
  begin
    Prev := TControl(AlignInfo.AlignList[AlignInfo.ControlIndex-1]);
    NewLeft := Prev.Left + Prev.Width + 8;
    if NewLeft + NewWidth < ClientWidth then
      NewTop := Prev.Top
    else
    begin
      NewLeft := 8;
      NewTop := Prev.Top + 12;
    end;
  end;
end;

Para su uso en aplicaciones reales, tendríamos que tener en cuenta la más que probable presencia de etiquetas TLabel vinculadas a algunos de estos controles. Podemos, por ejemplo, añadir éstas al final de la lista de alineación para reubicarlas de acuerdo al valor de sus propiedades FocusControl.