![]() |
![]() |
| Volver al índice | Por Luis Roche
|
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.
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:
Definiendo la alineación de un modo global. Si nos decidiesemos por esta implementación, bastaría con definir una propiedad (denominada, logicamente, Alignment) que controlaría la alineación de todas las celdas del grid. Esto sería muy sencillo, pero muy poco práctico, ya que lo normal es que queramos tener, por ejemplo, las celdas de cabecera centradas, las de texto alineadas a la izquierda, las númericas a la derecha, etc.
Una solución un poco mejor: definir la alineación a nivel de columnas. De este modo cada columna puede estar alineada con independencia de las demás, pero persiste el tema de que las filas de una misma columna deben tener la misma alineación (cabecera y datos). Un problema añadido es que para implementar esta elección deberíamos crearnos un editor de propiedades, y aún no sabemos como hacerlo (pero paciencia, que en próximas unidades se verá). Este es el caso del editor de columnas que incorpora Delphi 2.
La tercera posibilidad nos ofrece un control total: definir la alineación de cada celda a nivel individual. De este modo cada celda tendrá su propia alineación con independencia de las demás. La pega es que, como más adelante veremos, requiere un poco más de esfuerzo por parte del usuario del componente.
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.
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...)
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.
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 ;)
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.
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;
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í