Mostrando entradas con la etiqueta interfaz. Mostrar todas las entradas
Mostrando entradas con la etiqueta interfaz. Mostrar todas las entradas

04 abril 2008

El componente ValueListEditor

Hace bastante tiempo mostré como guardar valores dobles (nombre=valor) dentro de una lista (StringList), concrétamente fue en este artículo:

El Objeto StringList (I)

Lo que vamos a ver ahora es cómo almacenar esos mismos valores pero utilizando el componente visual de la clase TValueListEditor, el cual está alojado en la pestaña Additional:

Al insertar este componente en un formulario se muestra de este modo:


La columna Key contendrá el nombre de los valores que deseamos insertar y la columna Value el valor asociado al nombre. Supongamos que quiero que el usuario pueda modificar los datos principales de un cliente. Lo primero que vamos a hacer el cambiar el título de las columnas a través de la propiedad TitleCaptions. Vamos a sustituir Key y Value por Campo y Valor, de manera que así quedaría:

Lo siguiente que toca es introducir los campos del cliente. Para ello utilizamos la propiedad Strings del componente e introducimos los nombres de los campos en la columna Key:

Para crear más de una línea tenemos que pulsar en el teclado la tecla del cursor hacia abajo (la verdad es que este editor de valores es algo chapucero). Una vez introducidos los valores y pulsado el botón Ok dejaría este resultado:

Al ejecutar el programa se puede apreciar que este componente funciona de manera similar al inspector de objetos de Delphi (Object inspector). La primera columna se halla en modo lectura dejando que el usuario sólo pueda modificar los valores. Pero tenemos que ir un paso más allá: hay que conseguir que dependiendo del campo, el usuario sólo pueda meter valores de cierto tipo.

ESTABLECIENDO MASCARAS PARA CADA VALOR

El problema radica en que no se puede establecer el tipo de valor que puede introducirse en un campo en tiempo de diseño utilizando el inspector de objetos. Hay que hacerlo en el evento OnCreate del formulario mediante código. Por ejemplo, vamos a hacer que en el campo ID sólo se puedan introducir valores numéricos:

procedure TFPrincipal.FormCreate( Sender: TObject );
var
PropNumerica: TItemProp;
begin
PropNumerica := TItemProp.Create( ValueListEditor );
PropNumerica.EditMask := '###0';
ValueListEditor.ItemProps[0] := PropNumerica;
end;

Lo que hemos hecho es crear el objeto PropNumerica de la clase TItemProp y posteriormente hemos establecido la regla de edición asignándola al primer elemento de la lista (el campo ID).

También podemos crear otro tipo de propiedad que permita escoger el valor sólo entre una lista predeterminada. Por ejemplo, voy a hacer que el campo ESTADO sólo pueda contener los estados PENDIENTE o PAGADO:

procedure TFPrincipal.FormCreate( Sender: TObject );
var
PropNumerica, PropEstado: TItemProp;
begin
PropNumerica := TItemProp.Create( ValueListEditor );
PropNumerica.EditMask := '###0';
ValueListEditor.ItemProps[0] := PropNumerica;

PropEstado := TItemProp.Create( ValueListEditor );
PropEstado.PickList.Add( 'PENDIENTE' );
PropEstado.PickList.Add( 'PAGADO' );
ValueListEditor.ItemProps[5] := PropEstado;
end;

Al ejecutar el programa el campo ESTADO quedaría del siguiente modo:

También podemos modificar la forma en cómo se guardan los valores dentro de la rejilla a través de la propiedad KeyOptions en el inspector de objetos:

Aunque todas estas opciones hacen que este componente sea bastante útil, se echan en falta algunas características tales como valores para selección de color, máscaras para valores numéricos en coma flotante y la selección de fuentes de texto.

Pruebas realizadas en Delphi 7.

21 marzo 2008

El componente ActionMaganer

Uno de los problemas que suele presentarse a un programador de aplicaciones es cuando tiene que rediseñar de nuevo toda la interfaz del programa. Si tenemos el código vinculado directamente a botones, menús genéricos o menús contextuales nos toca de nuevo trasladar el código que había en los eventos de los componentes viejos a los componentes nuevos (haciendo copy-paste a base de bien).

Supongamos hemos programado un pequeño bloc de notas:




Para cada opción de menú tenemos definido su código correspondiente:

procedure TForm1.NuevoClick( Sender: TObject );
begin
Memo.Lines.Clear;
end;

procedure TForm1.AbrirClick( Sender: TObject );
begin
if OpenDialog.Execute then
Memo.Lines.LoadFromFile( OpenDialog.FileName );
end;

procedure TForm1.GuardarComoClick( Sender: TObject );
begin
if SaveDialog.Execute then
Memo.Lines.SaveToFile( SaveDialog.FileName );
end;

.....

Supongamos que quiero eliminar el componente de la clase TMainMenu y crear el su lugar un componente nuevo. El código que hemos visto anteriormente quedaría abandonado y habría que cortar el código de cada opción para llevárselo al nuevo componente.

Para evitar estos problemas tenemos el componente ActionMaganer que esta situado dentro de la paleta de componentes Aditional. Este componente esta diseñado específicamente para contener acciones de menús o botones pero sin estar vinculados a ningún componentes visual en concreto.

De manera que vamos a centralizar todas las acciones de nuestro formulario dentro de este componente para desvincularlo de la interfaz. Sería algo así como una programación visual dividida en dos capas: capa lógica (la cual contiene el código fuente relacionado con las acciones del usuario) y la capa visual (los componentes con los que va a interactuar el usuario).

Veamos cuales son los pasos para realizar esta tarea:

1. Insertamos un componente ActionManager en nuestro formulario:

2. Hacemos doble clic sobre dicho componente y nos aparecerá esta ventana:

Es esta ventana vamos a definir las acciones genéricas del formulario.

3. Pulsamos el botón New Action:
4. Nos aparecera en la lista Action1. Hacemos doble clic sobre el mismo y guardamos el código asociado al menú (Archivo -> Nuevo):

procedure TForm1.Action1Execute( Sender: TObject );
begin
Memo.Lines.Clear;
end;

Igualmente vamos a crear dos acciones más para guardar los eventos Archivo -> Abrir y Archivo -> Guardar como...

procedure TForm1.Action2Execute( Sender: TObject );
begin
if OpenDialog.Execute then
Memo.Lines.LoadFromFile( OpenDialog.FileName );
end;

procedure TForm1.Action3Execute( Sender: TObject );
begin
if SaveDialog.Execute then
Memo.Lines.SaveToFile( SaveDialog.FileName );
end;

5. Ahora vamos a añadir a nuestro formulario una barra de menús de la clase TActionMainMenuBar situada en la pestaña de componentes Additional:


Esto creará una barra vacía en la parte superior de nuestro formulario:


Para añadir opciones a esta barra vamos a hacer lo siguiente:

6. Hacemos doble clic sobre el componente ActionMaganer.

7. Arrastramos el objeto de la lista Action1 hacia la barra vacía que hemos insertado en el formulario:


8. Copiamos también las acciones 2 y 3.

9. Si queremos cambiar el nombre de las acciones hacemos doble clic en el objeto ActionMaganer, pulsamos Action1 y en el inspector de objetos modificamos la propiedad Caption y ponemos Nuevo. Igual hacemos para las acciones 2 y 3 que vamos a llamarlas Abrir y Guardar como. Como podemos apreciar no sólo cambian los nombres de las acciones en la lista del objeto ActionManager sino que también las situadas en la barra superior del formulario:


10. Por último y no menos importante, sería conveniente cambiar también la propiedad Name de cada una de las acciones, de modo que en el código fuente los procedimientos pasen a llamarse:

procedure TForm1.NuevoExecute( Sender: TObject );
procedure TForm1.AbrirExecute( Sender: TObject );
procedure TForm1.GuardarComoExecute( Sender: TObject );

en vez de:

procedure TForm1.Action1Execute( Sender: TObject );
procedure TForm1.Action2Execute( Sender: TObject );
procedure TForm1.Action3Execute( Sender: TObject );

De este modo tenemos separadas las acciones internas del formulario y su parte visual. Si eliminamos por error la barra superior del formulario seguimos teniendo todos nuestros eventos vinculados al objeto ActionMaganer. Así nuestro programa queda más flexible y elegante.

La mayor parte del código fuente de este artículo puede sustituirse mediante acciones predefinidas que lleva el componente ActionMaganer. Cuando creamos una acción, en vez de pulsar botón New Action tenemos que pulsar la flecha hacia abajo que hay a su derecha y seleccionar New Standar Action. En la ventana que aparece tenemos todas las típicas acciones que suelen realizarse en un bloc de notas, tales como copiar, cortar, abrir archivos, etc. También incorpora algunas funciones para recorrer los registros de las bases de datos, acelerando de este modo la implementación del programa.

Otra ventaja de utilizar este método es que mientras un programador puede dedicarse a la parte visual del formulario (creando menús, botones, iconos, etc), otro programador puede dedicarse a crear el código asociado al formulario distribuyendo de esa manera el trabajo en equipo.

Pruebas realizadas en Delphi 7.

03 octubre 2007

Como poner una imagen de fondo en una aplicación MDI

En un artículo anterior vimos como crear aplicaciones MDI gestionando múltiples ventanas hijas dentro de la ventana padre.

Una de las cosas que más dan vistosidad a una aplicación comercial es tener un fondo con nuestra marca de fondo de la aplicación (al estilo Contaplus o Facturaplus).

Para introducir una imagen de fondo en la ventana padre MDI hay que hacer lo siguiente:

- Introducir en la ventana padre (la que tiene la propiedad FormStyle a MDIForm) un componente de la clase TImage situado en la pestaña Additional. Al componenente lo vamos a llamar Fondo.

- En dicha imagen vamos a cambiar la propidedad Align a alClient para que ocupe todo el fondo del formulario padre.

- Ahora sólo falta cargar la imagen directamente:

Fondo.Picture.LoadFromFile( 'c:\imagenes\fondo.bmp' );

El único inconveniente que tiene esto es que no podemos utilizar los eventos del formulario al estar la imagen encima (Drag and Drop, etc).

UTILIZANDO EL CANVAS

Otra forma de hacerlo sería poniendo el objeto TImage en medio del formulario pero de manera invisible (sin alClient). Después en el evento OnPaint del formulario copiamos el contenido de la imagen TImage al fondo del formulario:

procedure TFormulario.FormPaint( Sender: TObject );
var R: TRect;
begin
R.Left := 0;
R.Top := 0;
R.Right := Fondo.Width;
R.Bottom := Fondo.Height;
Canvas.CopyRect( R, Fondo.Canvas, R );
end;

Así podemos tener igualmente una imagen de fondo sin renunciar a los eventos del formulario (OnMouseMove, OnClick, etc.).

Pruebas realizadas en Dephi 7.

25 septiembre 2007

La barra de estado

Es raro encontrar una aplicación que no lleve en alguno de sus formularios la barra de estado. Hasta ahora he utilizado ejemplos sencillos de meter texto en la barra de estado de manera normal, pero el componente StatusBar permite meter múltiples paneles dentro de la barra de estado e incluso podemos cambiar el formato de la fuente en cada uno de ellos.

ESCRIBIENDO TEXTO SIMPLE

El componente de la clase TStatusBar tiene dos estados: uno para escribir texto simple en un sólo panel y otro para escribir en múltiples paneles. Supongamos que el componente de la barra de estado dentro de nuestro formulario de llama BarraEstado. Para escribir en un sólo panel se hace de la siguiente manera:

BarraEstado.SimplePanel := True;
BarraEstado.SimpleText := 'Texto de prueba';

Se puede escribir tanto texto como longitud tenga la barra de estado, o mejor dicho, tanto como sea la longitud del formulario.

ESCRIBIENDO TEXTO EN MULTIPLES PANELES

Para escribir en múltiples paneles dentro de la misma barra de estado hay que crear un panel por cada apartado. En este ejemplo voy a crear en la barra de estado tres paneles y en cada uno de ellos voy a poner un formato diferente.

begin
BarraEstado.SimplePanel := False;
BarraEstado.Panels.Clear;

with BarraEstado.Panels.Add do
begin
Text := 'x=10';
Width := 50;
Style := psOwnerDraw;
Alignment := taRightJustify;
end;

with BarraEstado.Panels.Add do
begin
Text := 'y=50';
Width := 50;
Style := psOwnerDraw;
Alignment := taRightJustify;
end;

with BarraEstado.Panels.Add do
begin
Text := 'Texto seleccionado';
Style := psText;
Width := 50;
end;
end;

La propiedad Style de cada panel determina si es psText o psOwnerDraw. Por defecto todos los paneles que se crean tiene el estilo psText (texto normal). Si elegimos el estilo psOwnerDraw significa que vamos a ser nosotros los encargados de dibujar el contenido del mismo. Ello se hace en el evento OnDrawPanel de la barra de estado:

procedure TFormulario.BarraEstadoDrawPanel( StatusBar: TStatusBar; Panel: TStatusPanel; const Rect: TRect );
begin
case Panel.ID of
0: with BarraEstado.Canvas do
begin
Font.Name := 'Tahoma';
Font.Size := 10;
Font.Style := [fsBold];
Font.Color := clNavy;
TextOut( Rect.Left + 2, Rect.Top, Panel.Text );
end;

1: with BarraEstado.Canvas do
begin
Font.Name := 'Tahoma';
Font.Size := 10;
Font.Style := [fsBold];
Font.Color := clRed;
TextOut( Rect.Left + 2, Rect.Top, Panel.Text );
end;
end;
end;

Cuando se van creando paneles dentro de una barra de estado, a cada uno de ellos se le va asignado la propiedad ID a 0, 1, 2, etc, la cual es de sólo lectura. Como puede verse en el evento OnDrawPanel si el ID es 0 lo pinto de azul y si es 1 de rojo. Pero sólo funcionará en aquellos paneles cuyo estilo sea psOwnerDraw. Quedaría de la siguiente manera:


También puede cambiarse en cada panel propiedades tales como el marco (bevel), la alineación del texto (alignment) y el ancho (width).

Esto nos permitirá informar mejor al usuario sobre el comportamiento de nuestra aplicación en tiempo real y de una manera elegante que no interfiere con el contenido del resto del formulario.

Pruebas realizadas en Delphi 7.

21 septiembre 2007

Mostrando datos en el componente StringGrid

Anteriormente vimos como mostrar información en un componente ListView llegando incluso a cambiar el color de filas y columnas a nuestro antojo. El único inconveniente estaba en que no se podían cambiar los títulos de las columnas, ya que venían predeterminadas por los colores de Windows.

Pues bien, el componente de la clase TStringGrid es algo más cutre que el ListView, pero permite cambiar al 100% el formato de todas las celdas. Veamos primero como meter información en el mismo. Al igual que ocurría con el ListView, todos las celdas de un componente StringGrid son de tipo string, siendo nosotros los que le tenemos que dar formato a mano.

AÑADIENDO DATOS A LA REJILLA

Vamos a crear una rejilla de datos con las siguiente columnas:

NOMBRE, APELLIDO1, APELLIDO2, NIF, IMPORTE PTE.

Cuando insertamos un componente StringGrid en el formulario nos va a poner por defecto la primera columna con celdas fijas (fixed). Vamos a fijar las siguientes propiedades:

Propiedad Valor Descripción
--------- ----- -----------
ColCount 5 5 columnas
RowCount 4 4 filas
FixedCols 0 0 columnas fijas
FixedRows 1 1 fila fija
DefaultRowHeight 20 altura de las filas a 20 pixels

Ahora creamos un procedimiento para completar de datos la rejilla:

procedure TFormulario.RellenarTabla;
begin
with StringGrid do
begin
// Título de las columnas
Cells[0, 0] := 'NOMBRE';
Cells[1, 0] := 'APELLIDO1';
Cells[2, 0] := 'APELLIDO2';
Cells[3, 0] := 'NIF';
Cells[4, 0] := 'IMPORTE PTE.';

// Datos
Cells[0, 1] := 'PABLO';
Cells[1, 1] := 'GARCIA';
Cells[2, 1] := 'MARTINEZ';
Cells[3, 1] := '67348321D';
Cells[4, 1] := '1500,36';

// Datos
Cells[0, 2] := 'MARIA';
Cells[1, 2] := 'SANCHEZ';
Cells[2, 2] := 'PALAZON';
Cells[3, 2] := '44878234A';
Cells[4, 2] := '635,21';

// Datos
Cells[0, 3] := 'CARMEN';
Cells[1, 3] := 'PEREZ';
Cells[2, 3] := 'GUILLEN';
Cells[3, 3] := '76892693L';
Cells[4, 3] := '211,66';
end;
end;

Al ejecutar el programa puede apreciarse lo mal que quedan los datos en pantalla, sobre todo la columna del importe pendiente:


DANDO FORMATO A LAS CELDAS DE UN COMPONENTE STRINGGRIND

Lo que vamos a hacer a continuación es lo siguiente:

- La primera fila fija va a ser de color de fondo azul oscuro con fuente blanca y además el texto va a ir centrado.

- La columna del importe pendiente va a tener la fuente de color verde y va a ir alineada a la derecha.

- El resto de columnas tendrán el color de fondo blanco y el texto en negro.

Todo esto hay que hacerlo en el evento OnDrawCell del componente StringGrid:

procedure TFormulario.StringGridDrawCell( Sender: TObject; ACol,
ARow: Integer; Rect: TRect; State: TGridDrawState );
var
sTexto: String; // Texto que va a imprimir en la celda actual
Alineacion: TAlignment; // Alineación que le vamos a dar al texto
iAnchoTexto: Integer; // Ancho del texto a imprimir en pixels
begin
with StringGrid.Canvas do
begin
// Lo primero es coger la fuente por defecto que le hemos asignado al componente
Font.Name := StringGrid.Font.Name;
Font.Size := StringGrid.Font.Size;

if ARow = 0 then
Alineacion := taCenter
else
// Si es la columna del importe pendiente alineamos el texto a la derecha
if ACol = 4 then
Alineacion := taRightJustify
else
Alineacion := taLeftJustify;

// ¿Es una celda fija de sólo lectura?
if gdFixed in State then
begin
Brush.Color := clNavy; // le ponemos azul de fondo
Font.Color := clWhite; // fuente blanca
Font.Style := [fsBold]; // y negrita
end
else
begin
// ¿Esta enfocada la celda?
if gdFocused in State then
begin
Brush.Color := clRed; // fondo rojo
Font.Color := clWhite; // fuente blanca
Font.Style := [fsBold]; // y negrita
end
else
begin
// Para el resto de celdas el fondo lo ponemos blanco
Brush.Color := clWindow;

// ¿Es la columna del importe pendiente?
if ACol = 4 then
begin
Font.Color := clGreen; // la pintamos de azul
Font.Style := [fsBold]; // y negrita
Alineacion := taRightJustify;
end
else
begin
Font.Color := clBlack;
Font.Style := [];
end;
end;
end;

sTexto := StringGrid.Cells[ACol,ARow];
FillRect( Rect );
iAnchoTexto := TextWidth( sTexto );

case Alineacion of
taLeftJustify: TextOut( Rect.Left + 5, Rect.Top + 2, sTexto );
taCenter: TextOut( Rect.Left + ( ( Rect.Right - Rect.Left ) - iAnchoTexto ) div 2, Rect.Top + 2, sTexto );
taRightJustify: TextOut( Rect.Right - iAnchoTexto - 2, Rect.Top + 2, sTexto );
end;
end;
end;

Así quedaría al ejecutarlo:


Sólo hay un pequeño inconveniente y es que la rejilla primero se pinta de manera normal y luego nosotros volvemos a pintarla encima con el evento OnDrawCell con lo cual hace el proceso dos veces. Si queremos que sólo se haga una vez hay que poner a False la propiedad DefaultDrawing. Quedaría de la siguiente manera:


Por lo demás creo que este componente que puede sernos muy útil para mostrar datos por pantalla en formato de sólo lectura. En formato de escritura es algo flojo porque habría que controlar que tipos de datos puede escribir el usuario según en que columnas esté.

Pruebas realizadas en Delphi 7.

20 septiembre 2007

Creando aplicaciones MDI

Una aplicación MDI en Windows es aquella que contiene una ventana principal la cual que contiene a su vez ventanas hijas (al estilo Word o Excel). Cada ventana hija no puede salirse del marco de trabajo de la ventana padre aunque si puede minimizarse, maximizarse, etc.


A su vez no todas las ventanas creadas tienen que ser hijas de la ventana padre. Pueden crearse ventanas independientes ya sean normales o modales (una ventana modal es aquella que no permite acceder a ninguna otra hasta que se cierre, como los típicas ventanas de diálogo).

COMO CREAR UNA APLICACION MDI

Aunque el asistente de creación de proyectos de Delphi permite crear aplicaciones MDI, realmente no tiene mucha ciencia. Sólo hay que coger el formulario principal de la aplicación y cambiar su propiedad FormStyle a fsMDIForm. Con sólo hacer eso ya tenemos la ventana padre contenedora.

Para crear una ventana hija hay que hacer lo siguiente:

procedure TFormulario.MDIChildClick( Sender: TObject );
var
Form: TForm;
begin
Form := TForm.Create( Self );
Form.FormStyle := fsMDIChild;
Form.Caption := 'Ventana hija MDI';
Form.OnClose := AlCerrar;
Form.Show;
end;

Como puede apreciarse hemos creado una ventana y le hemos dicho que su estilo es fsMDIChild. También hemos asignado el evento OnClose para cerrar la ventana:

procedure TFormulario.AlCerrar( Sender: TObject; var Action: TCloseAction );
begin
Action := caFree;
end;

Hay que hacer esto porque por defecto cuando se crea una ventana hija y se cierra, realmente no se cierra si no que se minimiza. Por ello he creado el evento AlCerrar en la ventana padre para asignarlo a todas las ventanas hijas.

Si queremos hacer un formulario normal que no esté relacionado con el formulario padre MDI, entonces el procedimiento sería el siguiente:

procedure TFormulario.NormalClick( Sender: TObject );
var
Form: TForm;
begin
Form := TForm.Create( Self );
Form.Caption := 'Ventana normal';
Form.Show;
end;

Cuando se crea un formulario nuevo su propiedad FormStyle es por defecto fsNormal. El crear una ventana modal es prácticamente lo mismo:

procedure TFormulario.ModalClick( Sender: TObject );
var
Form: TForm;
begin
Form := TForm.Create( Self );
Form.Caption := 'Ventana modal';
Form.ShowModal;
end;

Cuando se crea una ventana hija MDI hay que tener presentes un par de cosas:

- Nunca pueden ser invisibles, con lo cual si utilizamos los métodos Show o Hide provocará un error. Igual para la propiedad Visible.

- Tampoco pueden ser modales, para ello utiliza una ventana modal como hemos visto anteriormente.

ORGANIZANDO LAS VENTANAS HIJAS

Una vez creadas las ventanas hijas, la ventana padre puede organizarlas en cascada, mostrarlas todas a la vez, colocarlas horizontalmente, verticalmente e incluso cerrarlas todas.

Por ejemplo, para poner todas las ventanas hijas en cascada hacemos lo siguiente:

procedure TFormulario.CascadaClick( Sender: TObject );
begin
VentanasNormales;
Cascade;
end;


El procedimiento VentanasNormales se encarga de dejar cada una de las ventanas hijas de manera normal por si acaso estaban minimizadas:

procedure TFormulario.VentanasNormales;
var i: Integer;
begin
for i := 0 to MDIChildCount - 1 do
MDIChildren[i].WindowState := wsNormal;
end;

La clase TForm dispone de las propiedades MDIChildCount y MDIChildren para controlar el número de ventanas hijas abiertas así como cada una de ellas.

Aparte de ponerlas en cascada podemos hacer que todas se repartan en pantalla horizontamente:

procedure TFormulario.HorizontalClick( Sender: TObject );
begin
VentanasNormales;
TileMode := tbHorizontal;
Tile;
end;



Y también verticalmente:

procedure TFormulario.VerticalClick( Sender: TObject );
begin
VentanasNormales;
TileMode := tbVertical;
Tile;
end;


Si queremos minimizarlas todas hay que decírselo a cada una de ellas:

procedure TFormulario.MinimizarClick( Sender: TObject );
var i: Integer;
begin
for i := MDIChildCount - 1 downto 0 do
MDIChildren[i].WindowState := wsMinimized;
end;


También ocurre lo mismo si queremos cerrarlas todas:

procedure TFormulario.CerrarClick( Sender: TObject );
var i: Integer;
begin
for i := 0 to MDIChildCount - 1 do
MDIChildren[i].Close;
end;

Las aplicaciones MDI son muy útiles para realizar programas de gestión, procesadores de texto, visualizadores de imágenes, etc.

Pruebas realizadas en Delphi 7.

14 septiembre 2007

Creando un procesador de textos con RichEdit (y II)

Sigamos añadiendo características a nuestro pequeño editor de textos:


NEGRITA, CURSIVA Y SUBRAYADO

Vamos a definir la función de los típicos botones de negrita, cursiva y subrayado que llevan los procesadores de texto. Comencemos con el botón de negrita:

procedure TFormulario.BNegritaClick( Sender: TObject );
begin
with RichEdit.SelAttributes do
if not ( fsBold in Style ) then
Style := Style + [fsBold]
else
Style := Style - [fsBold];

RichEdit.SetFocus;
end;

Hay que tener en cuenta que si el usuario pulsa una vez negrita y el texto no estaba en negrita entonces se aplica dicho estilo, pero si ya lo estaba entonces hay que quitarlo.

Lo mismo sería para el botón de cursiva:

procedure TFormulario.BCursivaClick( Sender: TObject );
begin
with RichEdit.SelAttributes do
if not ( fsItalic in Style ) then
Style := Style + [fsItalic]
else
Style := Style - [fsItalic];

RichEdit.SetFocus;
end;

Y para el botón de subrayado:

procedure TFormulario.BSubrayadoClick( Sender: TObject );
begin
with RichEdit.SelAttributes do
if not ( fsUnderline in Style ) then
Style := Style + [fsUnderline]
else
Style := Style - [fsUnderline];

RichEdit.SetFocus;
end;

MOSTRANDO LA POSICION ACTUAL DEL CURSOR

Una de las cosas que se agradece en todo procesador de textos es que muestre en que fila y columna estoy situado. Para ello vamos a insertar en la parte inferior del formulario una barra de estado. El componente se llama StatusBar y se encuentra en la pestaña Win32. Le vamos a poner el nombre de Estado y ponemos a True su propiedad SimplePanel.

Lo que haremos a continuación es un procedimiento que muestre la posición actual del cursor dentro del RichEdit en la barra de estado:

procedure TFormulario.MostrarPosicion;
begin
Estado.SimpleText := Format( 'Fila: %d Columna %d',
[RichEdit.CaretPos.y+1, RichEdit.CaretPos.x+1] );
end;

Este procedimiento hay que llamarlo cuando creamos el formulario:

procedure TFormulario.FormCreate( Sender: TObject );
begin
MostrarPosicion;
end;

y en el evento OnSelectionChange del RichEdit:

procedure TFormulario.RichEditSelectionChange( Sender: TObject );
begin
MostrarPosicion;
end;

Al ejecutar el programa veremos que el resultado queda más profesional (ya podían los de Microsoft ponerle esto al cutre Bloc de Notas).

DANDO FORMATO A LOS PARRAFOS DEL DOCUMENTO

Otra característica que vamos a implementar es la de alinear el párrafo actual a la izquierda, al centro y a la derecha. Para ello vamos a poner arriba tres botones llamados BIzquierda, BDerecha y BCentro, cuyos eventos serían:

procedure TFormulario.BIzquierdaClick( Sender: TObject );
begin
RichEdit.Paragraph.Alignment := taLeftJustify;
end;

procedure TFormulario.BCentroClick( Sender: TObject );
begin
RichEdit.Paragraph.Alignment := taCenter;
end;

procedure TFormulario.BDerechaClick( Sender: TObject );
begin
RichEdit.Paragraph.Alignment := taRightJustify;
end;

Otra característica que se le puede añadir a un párrafo es la de añadir un punto por la izquierda como hace Microsoft Word. Para ello vamos a añadir el botón de nombre BPunto cuyo procedimiento es:

procedure TFormulario.BPuntoClick( Sender: TObject );
begin
with RichEdit.Paragraph do
if Numbering = nsNone then
Numbering := nsBullet
else
Numbering := nsNone;

RichEdit.SetFocus;
end;

Si se pulsa una vez este botón añade un punto al párrafo y si se pulsa de nuevo lo quita.

Al párrafo actual se le pueden modificar también las propiedades:

FirstIndent -> Es el espacio en pixels por la izquierda que se le da a la primera línea
LeftIndent -> Es el espacio en pixels por la izquierda que se le da a todas las líneas
RightIndent -> Es el espacio en pixels por la derecha que se le da a todas las líneas

LAS OTRAS PROPIEDADES DEL COMPONENTE RICHEDIT

Hay otras propiedades que nos permiten personalizar nuestro editor de texto como son:

PlainTex: Si se activa esta propiedad al guardar el archivo de texto a disco no almacena las propiedades de color, fuente, etc. Se comporta como el Bloc de Notas.

SelLength: Es el número de caracteres seleccionados por el usuario.

SelStart: Es la posición en el texto donde comienza la selección.

SelText: Es el texto seleccionado.

Con esto finalizamos las características más importantes del componente RichEdit.

Pruebas realizadas en Delphi 7.

13 septiembre 2007

Creando un procesador de textos con RichEdit (I)

Para crear un procesador de textos vamos a utilizar el componente RichEdit que se encuentra en la pestaña Win32. Este componente tiene la particularidad de poder definir distintos estilos de texto al contrario de un componente Memo cuya fuente es estática para todo el documento.

El componente de la clase TRichEdit hereda de TCustomMemo añadiendo características tan interesantes como la de modificar el estilo de la fuente, colorear palabras o frases, etc.

Veamos como se podría crear un mini procesador de textos utilizando este componente. Como procesador de textos lo primero que debemos definir son las funciones para la creación, carga y grabación de documentos en disco.

La programación la voy a realizar sobre este formulario:


Las opciones del menú son:

Archivo -> Nuevo, Abrir, Guardar, Guardar como y Salir.
Edición -> Cortar, Copiar y Pegar.
Formato -> Fuente
Ayuda -> Acerca de...

GUARDANDO EL TEXTO EN UN ARCHIVO

Lo primero que vamos a contemplar es la grabación en un archivo de texto. Para ello vamos a insertar en el formulario un componente de clase TSaveDialog que se encuentra en la pestaña Dialogs y lo vamos a llamar GuadarTexto.

Entonces al pulsar la opción Archivo -> Guardar como del ménu ejecutamos lo siguiente:

procedure TFormulario.GuardarComoClick( Sender: TObject );
begin
if GuardarTexto.Execute then
begin
RichEdit.Lines.SaveToFile( GuardarTexto.FileName );
sArchivo := GuardarTexto.FileName;
end;
end;

La variable sArchivo se va a encargar de guardar la ruta y el nombre archivo que se guardó por primera vez, para que cuando seleccionemos la opción guardar no haya que volver a decirle de nuevo el nombre. Esta variable la vamos a declarar en la sección privada de nuestro formulario:

private
{ Private declarations }
sArchivo: String;

Ahora tenemos que hacer que si el usuario pulsa la opción Guardar se guarde el archivo sin preguntarnos el nombre si ya lo tiene:

procedure TFormulario.GuardarClick( Sender: TObject );
begin
// ¿No tiene nombre?
if sArchivo = '' then
GuardarComoClick( Self )
else
RichEdit.Lines.SaveToFile( sArchivo );
end;

Si no tuviera nombre es que es un archivo nuevo, con lo cual lo desviamos a la opción Guardar como.

CARGADO EL TEXTO DESDE UN ARCHIVO

Para cargar el texto tenemos que añadir al formulario el componente TOpenDialog y lo llamamos CargarTexto. Y al pulsar en el menú la opción Archivo -> Abrir se ejecutaría:

procedure TFormulario.AbrirClick( Sender: TObject );
begin
if CargarTexto.Execute then
begin
RichEdit.Lines.LoadFromFile( CargarTexto.FileName );
sArchivo := CargarTexto.FileName;
end;
end;

También guardamos en la variable sArchivo el nombre del archivo cargado para su posterior utilización en la opción Guardar.

CREANDO UN NUEVO TEXTO

En nuestro programa vamos a hacer que si el usuario selecciona la opción del menú Archivo -> Nuevo se elimine el texto del componente RichEdit. Lo que si hay que controlar es que si había un texto anterior le pregunte al usuario si desea guardarlo.

procedure TFormulario.NuevoClick( Sender: TObject );
begin
// ¿Hay algo introducido?
if RichEdit.Text <> '' then
if Application.MessageBox( '¿Desea guardar el texto actual?', 'Atención',
MB_ICONQUESTION OR MB_YESNO ) = ID_YES then
GuardarClick( Self );

RichEdit.Clear;
end;

CONTROLANDO LA EDICION DEL TEXTO

Otra de las cosas básicas que debe llevar todo editor de texto son las funciones de cortar, copiar y pegar. Para ello vamos a implemantar primero la opción del menú Edición -> Cortar:

procedure TFormulario.CortarClick( Sender: TObject );
begin
RichEdit.CutToClipboard;
end;

Después hacemos la opción de Edición -> Copiar:

procedure TFormulario.CopiarClick( Sender: TObject );
begin
RichEdit.CopyToClipboard;
end;

Y por último la opción de Edición -> Pegar:

procedure TFormulario.PegarClick( Sender: TObject );
begin
RichEdit.PasteFromClipboard;
end;

CAMBIANDO EL ESTILO DEL TEXTO

Una vez tenemos implementada la parte básica del editor de texto vamos a darle la posibilidad al usuario de que pueda cambiar la fuente, el estilo, el color, etc.

Para que el usuario pueda elegir la fuente tenemos que introducir en el formulario el componente de la clase TFontDialog situado en la pestaña Dialogs. Le vamos a dar el nombre de ElegirFuente.

Ahora en la opción del menú Formato -> Fuente ejecutamos:

procedure TFormulario.FuenteClick( Sender: TObject );
begin
if ElegirFuente.Execute then
with RichEdit, ElegirFuente do
begin
SelAttributes.Name := Font.Name;
SelAttributes.Size := Font.Size;
SelAttributes.Color := Font.Color;
SelAttributes.Pitch := Font.Pitch;
SelAttributes.Style := Font.Style;
SelAttributes.Height := Font.Height;
end;
end;

¿Por qué no hemos hecho lo siguiente?

RichEdit.Font := ElegirFuente.Font;

Si hago esto me cambia la fuente de todo el texto, incluso la que he escrito anteriormente y a mi lo que me interesa es modificar la fuente de lo que se vaya a escribir a partir de ahora. Para ello se utilizan las propiedades de SelAttributes las cuales se encargan de establecer el estilo del texto seleccionado, y en el caso de que no haya texto seleccionado se aplica a donde esté el cursor.

En el próximo artículo seguiremos ampliando nuestro pequeño procesador de textos.

Pruebas realizadas en Delphi 7.

11 septiembre 2007

El componente TTreeView (y II)

Después de ver el manejo básico de un árbol TreeView pasemos a ver otras características menos conocidas pero también de suma importancia.

OBTENIENDO EL NIVEL DE CADA NODO

Cuando se van insertando elementos dentro de un árbol tenemos la dificultad de saber en que nivel de profundidad se encuentra cada uno de los nodos. Cada uno de los Items del componente TTreeView es de la clase TTreeNode.

Esta clase dispone de la propiedad Level la cual nos da el nivel de profundidad dentro del árbol. Veamos como obtener el nivel de cada uno de los nodos del árbol. Este procedimiento recorre todos los nodos del árbol y les añade a su nombre el nivel:

procedure TFTreeView.BNivelClick( Sender: TObject );
var i: Integer;
begin
// Averiguamos el nivel de cada nodo
for i := 0 to TreeView.Items.Count - 1 do
TreeView.Items[i].Text := TreeView.Items[i].Text + '_' +
IntToStr( ( TreeView.Items[i] as TTreeNode ).Level );
end.

Los elementos situados en la raiz tienen un nivel 0, los hijos un nivel 1, los nietos un 2...

LEYENDO LOS NODOS SELECCIONADOS

El nodo actualmente seleccionado se obtiene mediante:

TreeView.Selected

donde valdrá nil si no hay ninguno seleccionado. Pero si activamos la propiedad MultiSelect la forma de leer aquellos nodos seleccionados sería la siguiente:

procedure TFTreeView.BSeleccionadosClick( Sender: TObject );
var
i: Integer;
Seleccionados: TStringList;
begin
Seleccionados := TStringList.Create;

for i := 0 to TreeView.Items.Count - 1 do
if TreeView.Items[i].Selected then
Seleccionados.Add( TreeView.Items[i].Text );

ShowMessage( Seleccionados.Text );

Seleccionados.Free;
end;

Al igual que hicimos con el componente ListView volcamos el nombre de los nodos seleccionados en un StringList y lo sacamos por pantalla.

ASOCIANDO UNA IMAGEN A CADA NODO

Si añadimos a nuestro formulario el componente TImageList se pueden asocionar iconos distintos a cada uno de los elementos del árbol TTreeView. Para ello asociamos este componente a la propiedad Images del TTreeView. Una vez hecho esto todos los nodos tendrán a su izquierda la primera imagen de la lista de imágenes TImageList.

Ya será cuestión de cada cual asociar la imagen correspondiente cada nodo. Vamos a ver un ejemplo que recorre los elementos del árbol y va a poner la segunda imagen de la lista de imágenes a aquellos nodos hijos (de nivel 1):

procedure TFTreeView.CambiarNodosHijos;
var
i: Integer;
begin
for i := 0 to TreeView.Items.Count - 1 do
if ( TreeView.Items[i] as TTreeNode ).Level = 1 then
( TreeView.Items[i] as TTreeNode ).ImageIndex := 1;
end;

CAMBIANDO LA FORMA EN QUE SE MUESTRAN LOS NODOS

El componente TreeView dispone la propiedad Ident la cual determina la identación de entre los nodos padres y sus hijos que por defecto es de 19 pixels. Se puede cambiar en cualquier momento, pero afectará a todos los nodos del árbol.

Otra cosa que se puede hacer al cargar un árbol desde un archivo de texto es expandir todos sus nodos, ya que cuando se carga el árbol esta compactado. Para solucionarlo se hace:

TreeView.FullExpand;

Equivale a pulsar el botón + de cada uno de los nodos padres.

CAMBIANDO LA FORMA DE DIBUJAR LOS NODOS

Al igual que vimos con el componente ListView también se puede modificar en tiempo real la forma de dibujar cada nodo. Para ello lo que hacemos es reprogramar el evento OnCustomDrawItem. Veamos un ejemplo de como hacer que los nodos hijos aparenzan de color de fuente azul y negrita:

procedure TFTreeView.TreeViewCustomDrawItem( Sender: TCustomTreeView;
Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean );
begin
if Node.Level = 1 then
begin
Sender.Canvas.Font.Color := clBlue;
Sender.Canvas.Font.Style := [fsBold];
end
else
begin
Sender.Canvas.Font.Color := clBlack;
Sender.Canvas.Font.Style := [];
end;

if cdsFocused in State then
Sender.Canvas.Font.Color := clWhite;
end;

En las dos últimas líneas del procedimiento también nos hemos asegurado de que si un elemento esta seleccionado la fuente salga de color blanca. Hemos utilizado para ello el parámetro State del evento. Los posibles valores de esta variable son:

cdsSelected -> La columna o fila ha sido seleccionada
cdsGrayed -> La columna o fila esta grisacea
cdsDisabled -> La columna o fila esta deshabilitada
cdsChecked -> La fila aparece con el CheckBox activado
cdsFocused -> La columna o fila esta enfocada
cdsDefault -> Por defecto
cdsHot -> Se ha activado el HotTrack y esta enfocado
cdsMarked -> La fila esta marcada
cdsIndeterminate -> La fila no esta seleccionada ni deseleccionada

Con esto se resumen las propiedades más importantes del componente TreeView.

Pruebas realizadas en Delphi 7.

10 septiembre 2007

El componente TTreeView (I)

Cuando queremos representar una información en forma de árbol incluyendo nodos padres e hijos el componente ideal para ello es el TreeView, el cual funciona exactamente igual que la parte izquierda del Explorador de Windows.

Veamos las peculiaridades que aporta este componente. Todas las funciones las voy a aplicar sobre esta pantalla:


AÑADIENDO ELEMENTOS AL ARBOL

Cuando se añaden elementos a un árbol pueden pertenecer al elemento raiz del mismo (sin padre) o pertenecer a un elemento ya creado. Los elementos del árbol se llaman nodos (Node) pudiendose crear tantos niveles de nodos como se deseen.

Vamos a crear un procedimiento asociado al botón Nuevo que va a añadir un nuevo nodo al árbol. El elemento insertado será un nodo raiz, a menos que el usuario seleccione un nodo antes de ello, que hará que sea su hijo:

procedure TFTreeView.BNuevoClick( Sender: TObject );
var
sNombre: string;
begin
sNombre := InputBox( 'Crear un nodo', 'Nombre:', '' );

if sNombre <> '' then
begin
// ¿Hay un nodo seleccionado?
if TreeView.Selected <> nil then
begin
TreeView.Items.AddChild( TreeView.Selected, sNombre );
TreeView.Selected.Expanded := True;
end
else
TreeView.Items.Add( nil, sNombre );
end;
end;

Lo que hace este procedimiento es preguntarnos el nombre del nodo y si el usuario ha seleccionado uno anteriormente lo mete como hijo (AddChild) expandiendo además el nodo padre (Expanded) como si el usuario hubiese pulsado el botón + del elemento padre.

MODIFICANDO LOS ELEMENTOS DEL ARBOL

Una vez creado un árbol de elementos se puede modificar el texto de cada uno de ellos mediante la propiedad Text. Vamos a hacer que si el usuario pulsa el botón Modificar y hay un nodo seleccionado el programa nos pregunte el nuevo nombre del nodo y lo cambie:

procedure TFTreeView.BModificarClick( Sender: TObject );
var
sNombre: string;
begin
if TreeView.Selected <> nil then
begin
sNombre := InputBox( 'Crear un nodo', 'Nombre:', TreeView.Selected.Text );

if sNombre <> '' then
TreeView.Selected.Text := sNombre;
end;
end;

Cuando se van a hacer operaciones con los nodos hay que asegurarse siempre que el nodo al que vayamos a acceder no sea nil, para evitar Access Violations.

ELIMINANDO LOS ELEMENTOS DEL ARBOL

Para eliminar un nodo del árbol se utiliza el método Delete:

procedure TFTreeView.BEliminarClick( Sender: TObject );
begin
if TreeView.Selected <> nil then
TreeView.Selected.Delete;
end;

Y si queremos eliminar todos los nodos se utiliza el método Clear de la propiedad Items:

procedure TFTreeView.BBorrarTodosClick( Sender: TObject );
begin
TreeView.Items.Clear;
end;

ORDENANDO LOS NODOS ALFABETICAMENTE

El modo de ordenar los elementos de un componente TreeView es igual al de ordenar los elementos de un componente ListView. Hay dos métodos: CustomSort y AlphaSort. El método CustomSort no voy a explicarlo ya que lo mencioné anteriormente en el artículo dedicado al componente ListView y es un poco más primitivo que utilizar AlphaSort.

Para ordenar todos los elementos de un árbol TreeView se utiliza el método:

procedure TFTreeView.BOrdenarClick( Sender: TObject );
begin
TreeView.AlphaSort( True );
end;

El parámetro booleano especifica si queremos que ordene tanto los elementos padres como los hijos (True) o sólo los elementos padres. Si no se especifica nada se asume que ordene todos los elementos.

Ahora hay que programar el evento OnCompare del TreeView para que ordene alfabéticamente los elementos:

procedure TFTreeView.TreeViewCompare( Sender: TObject; Node1,
Node2: TTreeNode; Data: Integer; var Compare: Integer );
begin
Compare := CompareText( Node1.Text, Node2.Text );
end;

Si queremos que la ordenación sea descendente entonces sólo hay que cambiar el signo:

Compare := -CompareText( Node1.Text, Node2.Text );

GUARDANDO LA INFORMACION DEL ARBOL EN UN ARCHIVO DE TEXTO

La clase TTreeView dispone del método SaveToFile para volcar todos los nodos en un archivo de texto para su posterior utilización. Voy a hacer un procedimiento que pregunte al usuario que nombre deseamos dar al archivo y lo guardará en el mismo directorio del programa con la extensión .TXT:

procedure TFTreeView.BGuardarClick( Sender: TObject );
var sNombre: string;
begin
sNombre := InputBox( 'Crear un nodo', 'Nombre:', '' );

if sNombre <> '' then
TreeView.SaveToFile( ExtractFilePath( Application.ExeName ) + sNombre + '.txt' );
end;

La información la guarda por líneas donde cada elemento hijo va separado por tabuladores:

documentos
ventas
contactos
Pablo
Ana
Maria
claves
Terra
Hotmail
GMail

CARGANDO LA INFORMACION DEL ARBOL GUARDADA ANTERIORMENTE

Para cargar el archivo de texto vamos a crear en tiempo real el componente TOpenDialog para que le pregunte al usuario el archivo de texto a cargar:

procedure TFTreeView.BCargarClick( Sender: TObject );
var
Abrir: TOpenDialog;
begin
Abrir := TOpenDialog.Create( Self );
Abrir.InitialDir := ExtractFilePath( Application.ExeName );
Abrir.Filter := 'TreeView|*.txt';

if Abrir.Execute then
TreeView.LoadFromFile( Abrir.FileName );

Abrir.Free;
end;

Hemos modificado el filtro de la carga para que sólo se vean archivos TXT.

En el próximo artículo seguiremos viendo más propiedades interesantes del componente TreeView.

Pruebas realizadas en Delphi 7.

07 septiembre 2007

Mostrando información en un ListView (y III)

En esta última parte referida al componente ListView vamos a ver como modificar los colores de las filas y las columnas según nuestro propio criterio. Para ello vamos a utilizar el evento OnCustomDrawSubItem.

En el primer ejemplo voy a mostrar como cambiar la columna 2 (la del nombre del artículo) poniendo la fuente azul oscuro y negrita:

procedure TFormulario.ListViewCustomDrawSubItem( Sender: TCustomListView;
Item: TListItem; SubItem: Integer; State: TCustomDrawState;
var DefaultDraw: Boolean );
begin
// ¿Va pintar la segunda columna?
if SubItem = 2 then
begin
Sender.Canvas.Font.Color := clNavy;
Sender.Canvas.Font.Style := [fsBold];
end
else
Sender.Canvas.Font.Color := clBlack;
end;

Por el contrario, si en vez de modificar las características de una columna queremos modificar las de una fila entonces sería de la siguiente manera:

procedure TFormulario.ListViewCustomDrawSubItem( Sender: TCustomListView;
Item: TListItem; SubItem: Integer; State: TCustomDrawState;
var DefaultDraw: Boolean );
begin
// ¿Es la primera fila?
if Item.Index = 1 then
Sender.Canvas.Font.Color := clRed
else
Sender.Canvas.Font.Color := clBlack;
end;

En este ejemplo mostrado hemos puesto la segunda fila con la fuente de color rojo. También se podría dibujar una fila según los datos contenidos en ella. Supongamos que deseo que se ponga la fuente de color rojo con un fondo amarillo en aquellos artículos cuyas unidades sean superiores a 3:

procedure TFormulario.ListViewCustomDrawSubItem( Sender: TCustomListView;
Item: TListItem; SubItem: Integer; State: TCustomDrawState;
var DefaultDraw: Boolean );
begin
// Si el número de unidades es superior a tres iluminamos la fila de rojo con fondo amarillo
if StrToInt( Item.SubItems[2] ) > 3 then
begin
Sender.Canvas.Brush.Color := clYellow;
Sender.Canvas.Font.Color := clRed;
end
else
begin
Sender.Canvas.Brush.Color := clWhite;
Sender.Canvas.Font.Color := clBlack;
end;
end;

Las posibilidades para realizar combinaciones de este tipo son enormes tanto para columnas como para filas.

PINTANDO FILAS Y COLUMNAS SEGÚN SU ESTADO

Hasta ahora lo que hemos dibujado ha sido para todas las filas y columnas pero se podría modificar sólo la fila que tiene el foco en azul:

procedure TFormulario.ListViewCustomDrawSubItem( Sender: TCustomListView;
Item: TListItem; SubItem: Integer; State: TCustomDrawState;
var DefaultDraw: Boolean );
begin
// La fuente de la fila seleccionada en negrita
if cdsFocused in State then
Sender.Canvas.Font.Style := [fsBold]
else
Sender.Canvas.Font.Style := [];
end;

Las posibilidades de la variable State son:

cdsSelected -> La columna o fila ha sido seleccionada
cdsGrayed -> La columna o fila esta grisacea
cdsDisabled -> La columna o fila esta deshabilitada
cdsChecked -> La fila aparece con el CheckBox activado
cdsFocused -> La columna o fila esta enfocada
cdsDefault -> Por defecto
cdsHot -> Se ha activado el HotTrack y esta enfocado
cdsMarked -> La fila esta marcada
cdsIndeterminate -> La fila no esta seleccionada ni deseleccionada

Con lo que hemos visto ya podemos utilizar el componente ListView como una rejilla de datos sin tener que utilizar tablas temporales ni un DBGrid.

Pruebas realizadas en Delphi 7.

06 septiembre 2007

Mostrando información en un ListView (II)

Veamos que más le podemos hacer con el componente TListView.

ORDENANDO LOS ELEMENTOS DE LA LISTA

El objeto ListView dispone de dos tipos de ordenación parecidos. Por un lado tenemos la ordenación mediante la función CustomSort:

function CustomSort( SortProc: TLVCompare; lParam: Longint ): Boolean;

Esta función toma como primer parámetro la dirección de una función CALLBACK encargada de establecer los parámetros de la ordenación. Veamos un ejemplo de ordenación ascendente por la columna ARTÍCULO (la segunda):

ListView.CustomSort( @Ordenacion, 0 );

Donde la función de ordenación sería la siguiente:

function Ordenacion( Item1, Item2: TListItem; ParamSort: Integer ): integer; stdcall;
begin
Result := CompareText( Item1.SubItems[1], Item2.SubItems[1] );
end;

Esta función compara los items 1 y 2 y le devuelve el resultado a CustomSort. Si queremos ordenarla descendentemente sería así:

function Ordenacion( Item1, Item2: TListItem; ParamSort: Integer ): integer; stdcall;
begin
Result := -CompareText( Item1.SubItems[1], Item2.SubItems[1] );
end;

Por otro lado tenemos la función AlphaSort la cual no necesita una función CALLBACK porque para eso tenemos el evento OnCompare. Se haría de la siguiente manera:

ListView.AlphaSort;

y en el evento OnCompare del ListView:

procedure TFormulario.ListViewCompare( Sender: TObject; Item1, Item2: TListItem;
Data: Integer; var Compare: Integer );
begin
Compare := CompareText( Item1.SubItems[1], Item2.SubItems[1] );
end;

Prácticamente es parecido a CustomSort (ya que un ListView desciende del componente CustomListView).

UTILIZANDO LA PRIMERA COLUMNA PARA CASOS ESPECIALES

Una de las cosas que se pueden meter en primera columna es un CheckBox para que el usuario marque filas. Antes de eso vamos a volver a mostrar la primera columna (Nada) a 70 de ancho y le cambiamos el nombre a Seleccionado. Por último activamos en las propiedades del ListView el campo CheckBoxes.

Al ejecutar el programa y dar de alta filas en el listado veremos que por cada elemento aparece un CheckBox a la izquierda para poder marcarlo. ¿Cómo sabemos metiante código los elementos que han sido marcados? Con la propiedad Checked de cada Item.

Veamos un ejemplo que comprueba los artículos marcados con CheckBox, los vuelca a un StringList y los saca a pantalla:

var
Seleccionados: TStringList;
i: Integer;
begin
Seleccionados := TStringList.Create;

for i := 0 to ListView.Items.Count - 1 do
if ListView.Items[i].Checked then
Seleccionados.Add( ListView.Items[i].SubItems[1] );

ShowMessage( Seleccionados.Text );

Seleccionados.Free;
end;

Otra utilidad muy interesante que se le puede dar a la primera columna es asociarle una imagen a cada fila. Para ello hay que añadir al formulario el componente ImageList que encuentra en la pestaña Win32.

Después añadimos imágenes a la lista y asociamos las propiedades LargeImages, StateImages y SmallImages con el componente ImageList. Con sólo hacer eso todas las filas tendrán a la izquierda la primera imagen de la lista de imágenes. Para cambiar la imagen en una fila determinada se hace:

ListView.Items[2].ImageIndex := 2;

o bien cambiamos sólo la fila seleccionada por el usuario:

if ListView.Selected <> nil then
ListView.Selected.ImageIndex := 1;

ACTIVANDO LA SELECCIÓN AUTOMÁTICA

Si activamos en el componente ListView la propiedad HotTrack y pasamos el puntero del ratón por las filas veremos como se iluminan en otro color, quedando seleccionadas fijamente si permanecemos con el ratón sobre las mismas.

Esto puede ser de utilidad para crear programas visualizadores de imágenes que muestren fotografías con sólo posicionarse con el ratón encima del nombre de la foto JPG. O para crear un listado de URL para nuestras páginas web preferidas.

También se pueden activar las propiedades:

htHandPoint -> Cambia el puntero del ratón a una mano cuando se para por encima de las filas.
htUnderlineCold -> Subraya la fila que se va a seleccionar
htUnderlineHold -> Subraya la fila que hay seleccionada

En el próximo artículo vamos a ver como reprogramar el dibujado de las filas en tiempo real.

Pruebas realizadas en Delphi 7.

05 septiembre 2007

Mostrando información en un ListView (I)

Cuando nos conectamos a una base de datos estamos acostumbrados a mostrar la información en un componente TDBGrid, pero hay ocasiones en las que hay que mostrar información temporal que no va conectada a la base de datos. Un ejemplo sería el listar ciertos campos de los albaranes antes de facturar o procesar la información de varias tablas en una sola.

DEFINIENDO COMO SE MUESTRA LA INFORMACIÓN EN UN LISTVIEW

El componente TListView permite mostrar la información de varias maneras diferentes lo cual se especifica en su propiedad ViewStyle cuyas posibilidades son:

vsIcon -> Muestra sus elementos como iconos de tamaño normal
vsList -> Muestra sus elementos como una lista a varias columnas
vsReport -> Muestra sus elementos según las filas y columnas al estilo DBGrid.
vsSmallIcon -> Muestra sus elementos como iconos pequeños

En esta ocasión vamos a poner la propiedad ViewStyle a vsReport para que tenga un comportamiento similar a una rejilla de datos al estilo TDBGrid.

A continuación vamos a ver como definir las columnas a mostrar dentro de un ListView. Para ello lo que hacemos es hacer doble clic sobre el ListView y se abrirá una ventana para añadir columnas. Pulsando el botón Add New vamos a añadir las siguientes columnas:

Nº columna Caption Width Alignment
---------- ------------- ------ ----------------
0 Nada 0 taLeft
1 ID 50 taRightJustify
2 Artículo 150 taLeftJustify
3 Unidades 50 taRightJustify
4 Precio 50 taRightJustify
5 Total 50 taRightJustify

La primera columna de un ListView es especialmente utilizada para guardar el título de una fila o una imagen asociada como veremos más adelante. Como la primera columna que quiero mostrar es el ID y la quiero alinear a la derecha entonces no me sirve. Lo que hago es llamar a la primera columna Nada y le doy un ancho de 0 para que no se vea. Mas adelante le daremos utilidad a la misma.

Una vez definidas las columnas vamos a ver como añadir filas.

AÑADIENDO ELEMENTOS AL LISTADO

Todas las columnas que se dan de alta en un objeto ListView son de tipo String, con lo cual si deseamos mostrar otro tipo de información debemos utilizar las típicas rutinas de conversión FloatToStr, BoolToStr, etc.

Veamos un ejemplo de como dar de alta tres artículos:

procedure TFormulario.NuevoElemento;
begin
with ListView.Items.Add do
begin
SubItems.Add( '1' );
SubItems.Add( 'MONITOR LG' );
SubItems.Add( '3' );
SubItems.Add( '230,45' );
SubItems.Add( '691,35' );
end;

with ListView.Items.Add do
begin
SubItems.Add( '2' );
SubItems.Add( 'TECLADO LOGITECH' );
SubItems.Add( '2' );
SubItems.Add( '49,99' );
SubItems.Add( '99,98' );
end;

with ListView.Items.Add do
begin
SubItems.Add( '3' );
SubItems.Add( 'RATÓN OPTICO DELL' );
SubItems.Add( '5' );
SubItems.Add( '15,99' );
SubItems.Add( '79,95' );
end;
end;

Como se puede apreciar primero se crea un Item por cada fila y un SubItem para cada columna, ignorando la columna 0 donde interviene la propiedad Caption como veremos más adelante. De hecho, SubItem[0] es la segunda columna y no la primera.

MODIFICANDO LAS FILAS DEL LISTADO

El primer elemento introducido en un ListView tiene un índice de 0. Lo que vamos a hacer es cambiar el TECLADO LOGITECH por un GENIUS:

procedure TFormulario.ModificarElemento;
begin
// Modificamos el segundo elemento de la lista
with ListView.Items[1] do
begin
SubItems[1] := 'TECLADO GENIUS';
SubItems[2] := '7';
SubItems[3] := '31,99';
SubItems[4] := '223,93';
end;
end;

Hemos cambiado todos las columnas menos la primera (el ID). Aquí hay que andarse con ojo y no acceder a un elemento de que no exista porque si no provocaría el error que tanto nos gusta:

Access violation at address ....


ELIMINANDO FILAS DE LA LISTA

Eliminar un registro de la lista es algo tan secillo de hacer como:

ListView.Items[0].Delete;

Eso elimina la primera fila. Y si deseamos eliminar todos los elementos de la lista:

ListView.Items.Clear;

CAMBIANDO EL ASPECTO Y EL COMPORTAMIENTO DEL LISTADO

En la lista que hemos creado no podemos seleccionar ninguna fila, esta algo así como bloqueado. Para poder hacerlo hay que activar la propiedad RowSelect a True.

Otra característica interesante es la de mostrar un separador de filas y columnas al estilo de la rejilla DBGrid. Para ello activa la propiedad GridLines a True.

También ocurre que si tenemos una fila seleccionada y pasamos el foco a otro control parece que se pierde la selección de dicha fila, aunque realmente sigue seleccionada si volvemos al mismo. Para evitar esto lo que debemos hacer es desactivar la propiedad HideSelection de tal manera que cuando pasemos a otro control en vez de estar la fila seleccionada en azul lo hace en color gris pero sigue estando seleccionada para el usuario.

Y si queremos seleccionar más de una fila activamos la propiedad Multiselect, de tal manera que si hay una fila seleccionada puede saberse de la siguiente manera:

if ListView.Selected <> nil then
...

Siendo Selected el Item de la lista que ha sido seleccionado.

Si hay muchas seleccionadas la propiedad SelCount nos lo dirá el número de filas seleccionadas en todo momento. Para averiguar que filas están seleccionadas lo que hacemos es recorrer la lista y ver aquellos elementos que tienen a True la propiedad Selected:

procedure TFormulario.VerSeleccionados;
var
Seleccionados: TStringList;
i: Integer;
begin
Seleccionados := TStringList.Create;

for i := 0 to ListView.Items.Count - 1 do
if ListView.Items[i].Selected then
Seleccionados.Add( ListView.Items[i].SubItems[1] );

ShowMessage( Seleccionados.Text );

Seleccionados.Free;
end;

Lo que hace este procedimiento es crear un objeto StringList y volcar dentro del mismo el nombre de los artículos seleccionados para después sacarlo por pantalla.

En el próximo artículo seguiremos viendo que más cosas se pueden hacer con un ListView.

Pruebas realizadas en Delphi 7.

23 julio 2007

Dibujar varias columnas en un ComboBox

Vamos a ver un ejemplo de dibujar tres columnas al desplegar un ComboBox. El truco está en guardar el valor de las tres columnas en el mismo item pero separado por un punto y coma.

Después implementamos nuestra propia función de dibujado de columnas para que muestre las tres columnas. Para ello metemos en el evento OnDrawItem del ComboBox:

procedure TFormulario.ComboBoxDrawItem( Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState );
var
sValor, sTodo: string;
i, iPos: Integer;
rc: TRect;
AnchoColumna: array[0..3] of Integer;
begin
ComboBox.Canvas.Brush.Style := bsSolid;
ComboBox.Canvas.FillRect( Rect );

// Las columnas deben ir separadas por un ;
sTodo := ComboBox.Items[Index];

// Establecemos el ancho de las columnas
AnchoColumna[0] := 0;
AnchoColumna[1] := 100; // Ancho de la columna 1
AnchoColumna[2] := 200; // Ancho de la columna 2
AnchoColumna[3] := 300; // Ancho de la columna 3

// Leemos el texto de la primera columna
iPos := Pos( ';', sTodo );
sValor := Copy( sTodo, 1, iPos - 1 );

for i := 0 to 3 do
begin
// Dibujamos la primera columna
rc.Left := Rect.Left + AnchoColumna[i] + 2;
rc.Right := Rect.Left + AnchoColumna[i+1] - 2;
rc.Top := Rect.Top;
rc.Bottom := Rect.Bottom;

// Escribimos el texto
Combobox.Canvas.TextRect( rc, rc.Left, rc.Top, sValor );

// Dibujamos las líneas que separan las columnas
if i < 3 then
begin
Combobox.Canvas.MoveTo( rc.Right, rc.Top );
Combobox.Canvas.LineTo( rc.Right, rc.Bottom );
end;

// Leemos el texto de la segunda columna
sTodo := Copy( sTodo, iPos + 1, Length( sTodo ) - iPos );
iPos := Pos( ';', sTodo );
sValor := Copy( sTodo, 1, iPos - 1 );
end;
end;

Modificando el bucle y el array de enteros AnchoColumna podemos crear el número de columnas que queramos. Ahora sólo hay que meter los items en el ComboBox separados por punto y coma:

with Combobox.Items do
begin
Add( 'JOSE;SANCHEZ;GARCIA;' );
Add( 'MARIA;PEREZ;GOMEZ;' );
Add( 'ANDRES;MARTINEZ;RUIZ;' );
end;

Por último hay que decirle al ComboBox que la rutina de pintar los items corre por nuestra cuenta:

procedure TFormulario.FormCreate(Sender: TObject);
begin
// Le decimos al ComboBox que lo vamos a pintar nosotros
Combobox.Style := csOwnerDrawFixed;
end;

Pruebas realizadas en Delphi 7.

Publicidad