delphi.gif - 582,0 K
Curso de creación de componentes en Delphi

Unidad 6. TMultiGrid: color, alineación y multiples líneas en un TStringGrid.

delphi.gif - 582,0 K
Volver al índice Por Luis Roche emailed.gif - 15503,0 K

blinklbl.gif - .966 K En esta unidad crearemos un componente de tipo StringGrid mejorado. Aprenderemos a crear eventos, los cuales nos permitirán dotar de nuevas y potentes posibilidades a nuestros componentes. Además, estudiaremos el método OnDrawCell y profundizaremos en los temas ya tratados en unidades anteriores referentes a los objetos Canvas y Brush.


Bverde.gif - .325 KObjetivo del componente

Como acabamos de mencionar, nuestro propósito es crear un componente de tipo StringGrid pero con nuevas funcionalidades. De modo que lo primero que deberíamos tener presente es que nos permite y que no nos permite hacer el componente TStringGrid estándard. Así que si aún no conoces dicho objeto, haremos una pequeña pausa para darte tiempo a ir Delphi y mirar la ayuda en línea ;) ¿Ya esta? Bien, pues ahora las nuevas características que implementaremos a nuestro componente: el color y la alineación de las celdas al nivel que deseemos (columna, filas e incluso celdas individuales, incluyendo alineación vertical) así como una nueva propiedad denominada multilinea que nos permitirá mostrar más de una línea de texto en cada celda del grid (rejilla).

La figura que sigue es un ejemplo del componente en funcionamiento:

mgej1.gif - 4679,0 K

Bverde.gif - .325 KImplementando la alineación de celdas. Creando nuestros propios eventos.

Vamos a comenzar con la propiedad alineación. Lo primero que debemos decidir es cómo vamos a implantarla. Fundamentalmente, hay tres posibilidades:

Como se ve, cada una de las tres implementaciones tiene sus ventajas e inconvenientes. En nuestro componente vamos a implementar una combinación del primer y tercer método. De este modo, definiremos una propiedad Alignment que especificará la alineación por defecto de las celdas del grid y al mismo tiempo, mediante un nuevo evento, se podrá determinar la alineación de celdas a nivel individual.

La implementación de la alineación horizontal a nivel global no tiene ningún misterio: basta definir la propiedad Alignment de tipo TAlignment (tipo ya incluido en Delphi). El campo que guardará el valor de esta propiedad se denominará FAlignment. Para escribir el valor de la propiedad utilizaremos el método SetAlignment, mientras que la lectura de la propiedad se hará directamente del campo FAlignment. De este modo tenemos perfectamente definido la interfaz de la propiedad Alignment. Respecto a cómo dibujaremos el contenido de la celda con la alineación apropiada lo dejaremos para más tarde, cuando estudiemos el evento OnDrawCell.

La alineación vertical se desarrolla de una forma similar. El único aspecto que conviene hacer notar es que Delphi no incorpora un tipo TVerticalAlignment, de modo que debemos crearlo nosotros:

   TVerticalAlignment = (vaTop, vaCenter, vaBottom);

Nos queda ver como implementamos la interfaz de la alineación de las celdas a nivel individual. Para ello vamos a crearnos un nuevo evento, que se disparará cada vez que necesitemos saber el estado de alineación de una celda en concreto.

Cómo ya se vió en la unidad 2, un evento (también denominado suceso) es un mecanismo que víncula una acción a cierto código. Más concretamente, un evento es un puntero a método, un puntero que apunta a un método específico de una instancia de objeto específica ¡toma ya que impresionante!;)

La forma de implementar un evento se hace mediante propiedades, es decir, los eventos son propiedades. A diferencia de las propiedades estándard, los eventos no utilizan métodos para implementar las partes read y write. En su lugar, las propiedades de eventos utilizan un campo privado del mismo tipo que la propiedad.

Pero basta de teoría y manos a la obra. Como ya se ha mencionado, vamos a crear un nuevo evento que se debe disparar cada vez que necesitemos obtener el valor de la alineación de una celda específica. Lo primero que debemos hacer, por tanto, es definir nuestro tipo de suceso. Esto lo hacemos mediante la siguiente sentencia:

  TMultiGridAlignmentEvent=procedure(Sender:TObject; ARow,ACol:LongInt;
    var HorAlignment: TAlignment; var VerAlignment: TVerticalAlignment) of object;
Los parámetros del evento TMultiGridAlignmentEvent son los siguientes:

Conviene hacer notar las palabras of object al final de la declaración del tipo de evento.

Ya hemos definido el tipo de evento. Ahora debemos crear un campo que guarde el estado de la propiedad OnGetCellAlignment. Esto lo realizamos en la parte private:
  private
     ...
     FOnGetCellAlignment : TMultiGridAlignmentEvent;
Por último, nos queda definir la propiedad en la parte published:
    property OnGetCellAlignment : TMultiGridAlignmentEvent read FOnGetCellAlignment write   FOnGetCellAlignment;
Sólo nos queda disparar el vento cuando lo necesitemos. Aunque un poco más adelante veremos con más detalle cuando queremos hacerlo en nuestro componente, el fragmento que sigue nos permite ver el mecanismo general:
  if Assigned(FOnGetCellAlignment) then
    FOnGetCellAlignment(Self,ARow,ACol,HorAlineacion,VerAlineacion);

Importante: Antes de activar un evento, conviene mirar primero si dicho evento tiene un manejador de evento asignado, ya que el usuario del componente no tiene porque haber escrito dicho manejador. De ahí la comparación if Assigned: si hay un manejedor de evento, se le llama, pero si no lo hay no se hace nada.

Bverde.gif - .325 KImplementando la fuente, estilo y color de celdas.

Si se ha entendido la sección anterior, no habrá ningún problema en entender esta, ya que la forma de implementar los atributos de color y fuente de una celda determinada se hará de una forma similar.

Defininimos un nuevo evento que se activará cuando sea necesario determinar los atributos de la celda. Para ello, crearemos el tipo del evento, el campo privado que lo almacenará y la propiedad asociada a dicho evento:

  TMultiGridColorEvent=procedure(Sender: TObject; ARow,Acol: LongInt;
     AState: TGridDrawState; Abrush: TBrush; AFont:TFont) of object;
  ...
  FOnGetCellColor : TMultiGridColorEvent;
  ...
  property OnGetCellColor : TMultiGridColorEvent read FOnGetCellColor write FOnGetCellColor;

La diferencia principal entre este evento y el correspondiente a la alineación viene dada por los parámetros ABruh y AFont. El usuario del componente debe devolver el brush y font correspondientes a la celda en concreto referenciada por ARow y ACol. El parámetro AState nos informa del estado de la celda (seleccionada, enfocada, fija...)

Bverde.gif - .325 KCeldas Multilínea.

Pasemos ahora a la implemantación de celdas multilínea. Esta es una característica que se echa mucho de menos en el StringGrid estándard y que, como vamos a ver, no nos va a costar casi nada implementar.

En primer lugar definiremos la interfaz. Para ello, vamos a crear una nueva propiedad denominada MultiLinea (original, ¿verdad? ;) ). Dicha propiedad se almacenará en el campo FMultiLinea que será de tipo boolean. Si FMultiLinea está a false, nuestro componente se comportará como el StringGrid normal, mientras que si está a true se permitirán las celdas multilínea.

  private
    FMultiLinea : Boolean;
  ...
    property MultiLinea : Boolean read FMultiLinea write SetMultiLinea default False;

Conviene hacer notar que en el método SetMultiLinea, si se asigna un nuevo valor a FMultiLinea se provoca el repintado del componente mediante la instrucción Invalidate. Esta misma técnica se utiliza en los métodos SetAlignment, SetVerticalAlignment y SetColor.

Nos queda por ver cómo dibujaremos las celdas multilínea. Este aspecto lo trataremos en la siguiente sección.

Bverde.gif - .325 KEl corazón de la bestia: el método DrawCell.

Hasta ahora, nos hemos centrado en la interfaz de nuestro componente; ha llegado el momento de definir la implementación. Todo el proceso de dibujar una celda se realiza en el método DrawCell. Este método es el verdadero corazón de nuestro componente, ya que en él se debe escribir todo el código encargado de calcular y pintar el texto correspondiente a una determinada celda.

El evento OnDrawCell pasa los siguientes parámetros al método DrawCell:

Ya sabemos donde. Ahora falta por ver cómo. En principio podría asustarnos todo lo que tenemos que hacer: calcular la alineación horizontal y vertical, activar los eventos de alineación y color, fragmentar el contenido de la celda en varias líneas... Pero no hay motivo: Delphi y el Api de windows vienen en nuestra ayuda y toda esta codificación se reduce a 20 o 30 líneas fácilmente entendibles. A continuación se muestra el código correspondiente al método DrawCell, el cúal paso a comentar:

procedure TMultiGrid.DrawCell(ACol,ARow : LongInt; ARect : TRect; AState : TGridDrawState);
Const
  TextAlignments : Array[TAlignment] of Word = (dt_Left, dt_Right, dt_Center);
Var
  HorAlineacion : TAlignment;
  VerAlineacion : TVerticalAlignment;
  Texto : string;
  Altura : integer;
  CRect : TRect;
  opciones : integer;
begin
  Texto:=Cells[ARow,Acol];
  HorAlineacion:=FAlignment;
  VerAlineacion:=FVerticalAlignment;
  if Assigned(FOnGetCellAlignment) then FOnGetCellAlignment(Self,ARow,ACol,HorAlineacion,VerAlineacion);
  if Assigned(FOnGetCellColor) then
    FOnGetCellColor(Self,ARow,ACol,AState,Canvas.Brush,Canvas.Font);
  Canvas.FillRect(ARect);

  Inc(ARect.Left,2);
  Dec(ARect.Right,2);
  CRect:=Arect;
  opciones:=TextAlignments[HorAlineacion] or dt_VCenter;
  if Multilinea then opciones:=opciones or dt_WordBreak;
  if not DefaultDrawing then
    inherited DrawCell(ACol,ARow,ARect,AState)
  else
    with ARect,Canvas do
    begin
      Altura:=DrawText(Handle,PChar(Texto),-1,CRect,opciones or dt_CalcRect);
      if FVerticalAlignment = vaCenter then
      begin
        if Altura < Bottom-Top+1 then
        begin
          Top:=(Bottom+Top-Altura) shr 1;
          Bottom:=Top+Altura;
        end;
      end
      else if FVerticalAlignment = vaBottom then
        if Altura < Bottom-Top+1 then Top:=Bottom-Altura;
      DrawText(Handle,PChar(Texto),-1,ARect,opciones)
    end;
end;

Lo primero que hacemos es guardar el contenido de la celda a dibujar en la variable Texto (de tipo string). A continuación se dan los valores por defecto a las variables HorAlineacion y VerAlineacion debido a que si el usuario no ha introducido una alineación particular para la celda en cuestión se aplicarán estas alineaciones.

Ahora viene una de las claves: la llamada a los eventos. Si el usuario ha escrito un manejador para el evento Alineación, se llama. Lo mismo ocurre para el evento Color. De este modo ya tenemos el tratamiento específico de la celda.

Lo siguiente es dibujar el fondo de la celda mediante el método FillRect al que pasamos el recuadro de dibujo de dicha celda (ARect).

Necesitamos ahora hacer una pausa para explicar cómo vamos a dibujar el texto.

A primera vista, lo lógico sería utilizar el método TextOut del objeto Canvas. Este método necesita como parámetros las coordenadas (x ,y) donde dibujar el texto y una cadena con el texto a dibujar. Pero para nuestros propósitos se queda corto, ya que tendríamos que calcular a mano la posición de dibujo para que la alineación fuera la correcta. Además tendríamos que calcular las divisiones de palabras necesarias para las celdas multilíneas, etc. En fin, un rollo. Y en mi opinión si podemos evitarnos todo este trabajo, pues mejor, ¿no? ;) Y lo bueno es que... ¡¡¡podemos!!!. El secreto: una función del Api de windows: DrawText

DrawText necesita los siguientes parámetros:

Hay más opciones para DrawText, de modo que si quieres más información sólo tienes que mirar en la ayuda en línea.

En la práctica necesitamos hacer dos llamadas a DrawText, una primera en la que al incluir la opción dt_CalcRect no se dibuja nada en pantallas, sino que sólo se calcula la altura requerida del rectángulo para el troceo de palabras (multilínea). Esta altura la guardamos en la variable Altura. Es importante hacer notar que el parámetro Rect pasado cómo parámetro se modifica, por lo que necesitaremos previamente guardar su estado. Posteriormente, una vez reajustado el rectángulo se hace una segunda llamada sin la opción dt_Calcrect que es la que de verdad dibuja el texto en el canvas.

Volvemos ahora al flujo del programa. Despues de rellenar el fondo de la celda con FillRect, copiamos en la variable CRect el rectángulo original (ARect) y se preparan las opciones con que vamos a llamar a DrawText. Aquí surge un pequeño problema, ya que HorAlineación es del tipo TAlignment (ta_LeftJustify...) y DrawText no entiende este tipo, por lo que es necesario una conversión entre dicho tipo y el que DrawText entiende. Esta conversión la llevamos a cabo mediente una matriz constante denominada TextAlignments. A continuación, si la propiedad Multilinea está a true, se añade dt_WordBreak a las opciones de DrawText.

Lo que se hace a continuación es verificar si el usuario ha tocado el valor de la propiedad DefaultDrawing. Si el valor de esta propiedad es false indica que es el usuario el que se encarga de todo el proceso, en caso contrario, el componente se encarga del dibujo.

Si es el componente el que se encarga de todo (para eso lo queremos, ¿no?) hacemos la primera llamada a DrawText para obtener la altura requerida del rectángulo de celda. Con esta altura y, siempre en cuando sea posible (todo el texto multilínea quepa en la celda), se pasa a centrar el texto en la celda (o a poner a top o bottom según corresponda). Una vez hecho esto, volvemos a llamar a DrawText para que se produzca, ahora si, el dibujado del texto en pantalla. Ojo a que en esta ocación pasamos ARect como rectángulo. Esto es lógico, ya que nos nos podemos salir del rectángulo que tiene la celda. Y ¡¡¡voilà!!! hemos terminado.

Este es a grandes rasgos el funcionamiento del método DrawCell. Conviene que mires y remires el mismo hasta que lo llegues a entender ya que es extremadamente potente y puede servirte para otras ocasiones. Y si tienes dudas, ya sabes, me escribes ;)

Bverde.gif - .325 KOtros detalles.
Por último quiero mencionar algunos pequeños detalles:
Bverde.gif - .325 KCódigo fuente del componente.
unit MultiGrid;       { (c) 1997 by Luis Roche }

interface

uses
  SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
  Forms, Dialogs, Grids;

type
  TVerticalAlignment = (vaTop, vaCenter, vaBottom);
  TMultiGridAlignmentEvent=procedure(Sender:TObject;ARow,ACol:LongInt;var HorAlignment:TAlignment;var VerAlignment:TVerticalAlignment) of object;
  TMultiGridColorEvent=procedure(Sender:TObject;ARow,Acol:LongInt;AState:TGridDrawState;ABrush:TBrush;AFont:TFont) of object;
  TMultiGrid = class(TStringGrid)
  private
    FAlignment : TAlignment;
    FVerticalAlignment : TVerticalAlignment;
    FMultiLinea : Boolean;
    FOnGetCellAlignment : TMultiGridAlignmentEvent;
    FOnGetCellColor : TMultiGridColorEvent;
    procedure SetAlignment(Valor : TAlignment);
    procedure SetVerticalAlignment(Valor : TVerticalAlignment);
    procedure SetMultiLinea(Valor : Boolean);
  protected
    procedure DrawCell(ACol,ARow : LongInt; ARect : TRect; AState : TGridDrawState); override;
  public
    constructor Create(AOwner : TComponent); override;
  published
    property Alignment : TAlignment read FAlignment write SetAlignment default taLeftJustify;
    property VerticalAlignment : TVerticalAlignment read FVerticalAlignment write SetVerticalAlignment default vaCenter;
    property MultiLinea : Boolean read FMultiLinea write SetMultiLinea default False;
    property OnGetCellAlignment : TMultiGridAlignmentEvent read FOnGetCellAlignment write FOnGetCellAlignment;
    property OnGetCellColor : TMultiGridColorEvent read FOnGetCellColor write FOnGetCellColor;
    property Options default [goFixedVertLine,goFixedHorzLine,goVertLine,goHorzLine,goRangeSelect,goRowSizing,goColSizing];
  end;

procedure Register;

implementation

constructor TMultiGrid.Create(AOwner : TComponent);
begin
  inherited Create(AOwner);
  FAlignment:=taLeftJustify;
  FVerticalAlignment:=vaCenter;
  {FColor:=clWindowText;}
  FMultiLinea:=False;
  Options:=[goFixedVertLine,goFixedHorzLine,goVertLine,goHorzLine,goRangeSelect,goRowSizing,goColSizing];
end;

procedure TMultiGrid.SetAlignment(Valor : TAlignment);
begin
  if valor <> FAlignment then
  begin
    FAlignment:=Valor;
    Invalidate;
  end;
end;

procedure TMultiGrid.SetVerticalAlignment(Valor : TVerticalAlignment);
begin
  if valor <> FVerticalAlignment then
  begin
    FVerticalAlignment:=Valor;
    Invalidate;
  end;
end;

procedure TMultiGrid.SetMultiLinea(Valor : Boolean);
begin
  if valor <> FMultiLinea then
  begin
    FMultiLinea:=Valor;
    Invalidate;
  end;
end;

procedure TMultiGrid.DrawCell(ACol,ARow : LongInt; ARect : TRect; AState : TGridDrawState);
Const
  TextAlignments : Array[TAlignment] of Word = (dt_Left, dt_Right, dt_Center);
Var
  HorAlineacion : TAlignment;
  VerAlineacion : TVerticalAlignment;
  Texto : string;
  Altura : integer;
  CRect : TRect;
  opciones : integer;
begin
  Texto:=Cells[ARow,Acol];
  HorAlineacion:=FAlignment;
  VerAlineacion:=FVerticalAlignment;
  if Assigned(FOnGetCellAlignment) then FOnGetCellAlignment(Self,ARow,ACol,HorAlineacion,VerAlineacion);
  if Assigned(FOnGetCellColor) then
    FOnGetCellColor(Self,ARow,ACol,AState,Canvas.Brush,Canvas.Font);
  Canvas.FillRect(ARect);
  Inc(ARect.Left,2);
  Dec(ARect.Right,2);
  CRect:=Arect;
  opciones:=TextAlignments[HorAlineacion] or dt_VCenter;
  if Multilinea then opciones:=opciones or dt_WordBreak;
  if not DefaultDrawing then
    inherited DrawCell(ACol,ARow,ARect,AState)
  else
    with ARect,Canvas do
    begin
      Altura:=DrawText(Handle,PChar(Texto),-1,CRect,opciones or dt_CalcRect);
      if FVerticalAlignment = vaCenter then
      begin
        if Altura < Bottom-Top+1 then
        begin
          Top:=(Bottom+Top-Altura) shr 1;
          Bottom:=Top+Altura;
        end;
      end
      else if FVerticalAlignment = vaBottom then
        if Altura < Bottom-Top+1 then Top:=Bottom-Altura;
      DrawText(Handle,PChar(Texto),-1,ARect,opciones)
    end;
end;

procedure Register;
begin
  RegisterComponents('Curso', [TMultiGrid]);
end;

end.

Bverde.gif - .325 KEjemplo de utilización.

A continuación se muestra un ejemplo de utilización de nuestro nuevo componente. Por brevedad, sólo se muestra el código correspondiente a los eventos FormCreate y GetCellCOlor y GetCellAlignment. Por supuesto, este no es un jemplo real, ya que por simplicidad, el contenido de las celdas se determina en el FormCreate cuando lo normal es que dicho contenido venga de otra parte (de cálculos, bases de datos, etc.). Pero como muestra de uso es suficiente.

procedure TForm1.FormCreate(Sender: TObject);
begin
  MultiGrid1.Cells[0,1]:='Enero';
  MultiGrid1.Cells[0,2]:='Febrero';
  MultiGrid1.Cells[0,3]:='Total Año';
  MultiGrid1.Cells[0,4]:='Notas';
  MultiGrid1.Cells[1,0]:='Zona A';
  MultiGrid1.Cells[2,0]:='Zona B';
  MultiGrid1.Cells[3,0]:='Resto de Zonas';
  MultiGrid1.Cells[4,0]:='TOTAL';
  MultiGrid1.Cells[1,1]:='1.000.000';
  MultiGrid1.Cells[1,2]:='1.250.000';
  MultiGrid1.Cells[1,3]:='9.150.000';
  MultiGrid1.Cells[1,4]:='Incremento sobre año anterior';
  MultiGrid1.Cells[2,1]:='1.450.000';
  MultiGrid1.Cells[2,2]:='  950.000';
  MultiGrid1.Cells[2,3]:='4.150.000';
  MultiGrid1.Cells[2,4]:='Decremento';
  MultiGrid1.Cells[3,1]:='4.000.000';
  MultiGrid1.Cells[3,2]:='3.250.000';
  MultiGrid1.Cells[3,3]:='17.250.000';
  MultiGrid1.Cells[3,4]:='Incremento sobre año anterior';
  MultiGrid1.Cells[4,1]:='6.450.000';
  MultiGrid1.Cells[4,2]:='5.450.000';
  MultiGrid1.Cells[4,3]:='30.550.000';
  MultiGrid1.Cells[4,4]:='';
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  MultiGrid1.Color:=clRed;
end;

procedure TForm1.MultiGrid1GetCellAlignment(Sender: TObject; ARow,
  ACol: Longint; var HorAlignment: TAlignment;
  var VerAlignment: TVerticalAlignment);
begin
  if (ACol in [1..3]) and (ARow in [1..4]) then
    HorAlignment:=taRightJustify
  else HorAlignment:=taCenter;
end;

procedure TForm1.MultiGrid1GetCellColor(Sender: TObject; ARow,
  Acol: Longint; AState: TGridDrawState; Abrush: TBrush; AFont: TFont);
begin
  if (ARow=0) then
  begin
    ABrush.Color:=clMaroon;
    AFont.Color:=clWhite;
    AFont.Style:=[fsBold];
  end
  else if (ACol=0) then
  begin
    AFont.Color:=clBlack;
    AFont.Style:=[fsBold];
    ABrush.Color:=clYellow;
  end
  else
  begin
    AFont.Color:=clBlack;
    ABrush.Color:=clYellow;
  end;
end;

Luis Roche revueltaroche@redestb.es
Ultima modificación 20.12.1996

Otros Links de Interés:

Cocina  -  Videos  -  Juegos Gratis  - Postales cachondas  - Cine  - Programas Gratis  -  Letras de Canciones

Listas de todos los Tutoriales Gratis. 1998- 2007 - -

Los tutoriales y cursos aquí reunidos son una recopilación de los mejores encontrados en Internet.
El crédito y copyright de los mismos si lo hubiere corresponde al autor de cada uno de ellos.
Si tu tutorial o curso está aquí, y deseas darlo de baja de esta recopilación o quieres añadir el tuyo, envíanos un mensaje desde
aquí

Publispain - Fun