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.

04 septiembre 2007

Explorar unidades y directorios

Si importante es controlar el manejo de archivos no menos importante es el saber moverse por las unidades de disco y los directorios.

Veamos que tenemos Delphi para estos menesteres:

function CreateDir( const Dir: string ): Boolean;

Esta función crea un nuevo directorio en la ruta indicada por Dir. Devuelve True o False dependiendo si ha podido crearlo o no. El único inconveniente que tiene esta función es que deben existir los directorios padres. Por ejemplo:

CreateDir( 'C:\prueba' ) devuelve True
CreateDir( 'C:\prueba\documentos' ) devuelve True
CreateDir( 'C:\otraprueba\documentos' ) devuelve False (y no lo crea)

function ForceDirectories( Dir: string ): Boolean;

Esta función es similar a CreateDir salvo que también crea toda la ruta de directorios padres.

ForceDirectories( 'C:\prueba' ) devuelve True
ForceDirectories( 'C:\prueba\documentos' ) devuelve True
ForceDirectories( 'C:\otraprueba\documentos' ) devuelve True


procedure ChDir( const S: string ); overload;

Este procedimiento cambia el directorio actual al indicado por el parámetro S. Por ejemplo:

ChDir( 'C:\Windows\Fonts' );

function GetCurrentDir: string;

Nos devuelve el nombre del directorio actual donde estamos posicionados. Por ejemplo:

GetCurrentDir devuelve C:\Windows\Fonts

function SetCurrentDir( const Dir: string ): Boolean;

Establece el directorio actual devolviendo True si lo ha conseguido. Por ejemplo:

SetCurrentDir( 'C:\Windows\Java' );

procedure GetDir( D: Byte; var S: string );

Devuelve el directorio actual de una unidad y lo mete en la variable S. El parámetro D es el número de la unidad siendo:

D Unidad
--- ------------------
0 Unidad por defecto
1 A:
2 B:
3 C:
...

Por ejemplo para leer el directorio actual de la unidad C:

var
sDirectorio: String;
begin
GetDir( 3, sDirectorio );
ShowMessage( 'El directorio actual de la unidad C: es ' + sDirectorio );
end;

function RemoveDir( const Dir: string ): Boolean;

Elimina un directorio en el caso de que este vacío, devolviendo False si no ha podido hacerlo.

RemoveDir( 'C:\prueba\documentos' ) devuelve True
RemoveDir( 'C:\prueba' ) devuelve True
RemoveDir( 'C:\otraprueba' ) devuelve False porque no esta vacío

function DirectoryExists( const Directory: string ): Boolean;

Comprueba si existe el directorio indicado por el parámetro Directory. Por ejemplo:

DirectoryExists( 'C:\Windows\System32\' ) devuelve True
DirectoryExists( 'C:\Windows\MisDocumentos\' ) devuelve False

function DiskFree( Drive: Byte ): Int64;

Devuelve el número de bytes libres de una unidad de dico indicada por la letra Drive:

Drive Unidad
----------- ------
0 Unidad por defecto
1 A:
2 B:
3 C:
...

Por ejemplo vamos a ver el número de bytes libres de la unidad C:

DiskFree( 3 ) devuelve 5579714560


function DiskSize( Drive: Byte ): Int64;

Nos dice el tamaño total en bytes de una unidad de disco. Por ejemplo:

DiskSize( 3 ) devuelve 20974428160


BUSCANDO ARCHIVOS DENTRO DE UN DIRECTORIO

Para buscar archivos dentro de un directorio disponemos de las funciones:

function FindFirst( const Path: string; Attr: Integer; var F: TSearchRec ): Integer;

Busca el primer archivo, directorio o unidad que se encuentre dentro de una ruta en concreto. Devuelve un cero si ha encontrado algo. El parámetro TSearchRec es una estructura de datos donde se almacena lo encontrado:

type
TSearchRec = record
Time: Integer;
Size: Integer;
Attr: Integer;
Name: TFileName;
ExcludeAttr: Integer;
FindHandle: THandle;
FindData: TWin32FindData;
end;


function FindNext( var F: TSearchRec ): Integer;

Busca el siguiente archivo, directorio o unidad especificado anteriormente por la función FindFirst. Devuelve un cero si ha encontrado algo.

procedure FindClose( var F: TSearchRec );

Este procedimiento cierra la búsqueda comenzada por FindFirst y FindNext.

Veamos un ejemplo donde se utilizan estas funciones. Vamos a hacer un procedimiento que lista sólo los archivos de un directorio que le pasemos y vuelca su contenido en un StringList:

procedure TFPrincipal.Listar( sDirectorio: string; var Resultado: TStringList );
var
Busqueda: TSearchRec;
iResultado: Integer;
begin
// Nos aseguramos que termine en contrabarra
sDirectorio := IncludeTrailingBackslash( sDirectorio );

iResultado := FindFirst( sDirectorio + '*.*', faAnyFile, Busqueda );

while iResultado = 0 do
begin
// ¿Ha encontrado un archivo y no es un directorio?
if ( Busqueda.Attr and faArchive = faArchive ) and
( Busqueda.Attr and faDirectory <> faDirectory ) then
Resultado.Add( Busqueda.Name );

iResultado := FindNext( Busqueda );
end;

FindClose( Busqueda );
end;

Si listamos el raiz de la unidad C:

var
Directorio: TStringList;
begin
Directorio := TStringList.Create;
Listar( 'C:', Directorio );
ShowMessage( Directorio.Text );
Directorio.Free;
end;

El resultado sería:

AUTOEXEC.BAT
Bootfont.bin
CONFIG.SYS
INSTALL.LOG
IO.SYS
MSDOS.SYS
NTDETECT.COM

Con estas tres funciones se pueden hacer cosas tan importantes como eliminar directorios, realizar búsquedas de archivos, calcular lo que ocupa un directorio en bytes, etc.

Pruebas realizadas en Delphi 7.

03 septiembre 2007

Trabajando con archivos de texto y binarios (y V)

Vamos a terminar de ver los recursos de los que dispone Delphi para el tratamiento de archivos.

PARTIENDO UN ARCHIVO EN DOS

En este ejemplo vamos a coger el archivo prueba.dat de 100 Kb y dejamos sólo las primeras 30 Kb eliminando el resto:

procedure TFormulario.PartirArchivo;
var F: File of byte;
Buffer: array[0..1023] of Byte;
i: Integer;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.dat' );
Reset( F );

// Leemos 30 Kb utilizando un buffer de 1 Kb
for i := 1 to 30 do
BlockRead( F, Buffer, 1024 );

Truncate( F );
CloseFile( F );
end;

Hemos utilizado para ello la función Truncate, la cual parte el archivo que estamos leyendo según donde este el puntero F.


ASEGURANDO QUE SE GUARDE LA INFORMACIÓN EN ARCHIVOS DE TEXTO

Cuando abrimos un archivo de texto para escribir en él no se guarda completamente toda la información hasta que se cierra con el procedimiento CloseFile. Esto puede dar problemas si en algún momento el procedimiento WriteLn provoca un error dejando el archivo abierto. Se perdería la mayor parte de la información que supuestamente debería haberse guardado en el mismo.

Para evitar esto disponemos de la función Flush:

function Flush( var t: TextFile ): Integer;

Esta función lo que hace es vaciar el buffer del archivo de texto en el disco duro. Es algo así como ejecutar CloseFile pero sin cerrar el archivo. Por ejemplo:

var
F: TextFile;
sArchivo: String;
begin
sArchivo := ExtractFilePath( Application.ExeName ) + 'prueba.txt';
AssignFile( F, sArchivo );
Rewrite( F );
WriteLn( F, 'Esto es el contenido del archivo de texto.' );
Flush( F );
// aquí podemos seguir escribiendo en el mismo
CloseFile( F );
end;

GUARDANDO OTROS TIPOS DE DATOS EN LOS ARCHIVOS

Hasta ahora sólo hemos guardado información de tipo texto y binaria en archivos pero se puede guardar cualquier tipo de información utilizando cualquier tipo de dato. Por ejemplo para guardar una serie de números reales se haría de la siguiente manera:

procedure TFormulario.GuardarRecibos;
var
F: File of Real;
r: Real;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'ImporteRecibos.dat' );
Rewrite( F );
r := 120.45;
Write( F, r );
r := 1800.05;
Write( F, r );
r := 66.31;
Write( F, r );
CloseFile( F );
end;

Si os fijais bien en el archivo resultante vemos que ocupa 24 bytes. Esto es así porque el tipo real ocupa 8 bytes en memoria y como son 3 números reales lo que hemos guardado entonces hace un total de 24 bytes. Hay que asegurarse de que tanto al leer como al guardar información en este tipo de archivos se haga con el mismo tipo de variable. Si guardamos información con File of Real y la leemos con File of Single los resultados podrían ser catastróficos (a parte de quedarse el archivo a medio leer).

También se pueden guardar estructuras de datos almacenadas en registros. En este ejemplo que voy a mostrar vamos a ver como guardar los datos de un cliente en un archivo:

type
TCliente = record
ID: Integer;
sNombre: String[50];
rSaldo: Real;
bPagado: Boolean;
end;

procedure TFPrincipal.CrearArchivoRegistro;
var
Cliente: TCliente;
F: File of TCliente;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'clientes.dat' );
Rewrite( F );

with Cliente do
begin
ID := 1;
sNombre := 'FRANCISCO MARTINEZ LÓPEZ';
rSaldo := 1200.54;
bPagado := False;
end;

Write( F, Cliente );

with Cliente do
begin
ID := 2;
sNombre := 'MARIA ROJO PALAZÓN';
rSaldo := 622.32;
bPagado := True;
end;

Write( F, Cliente );

CloseFile( F );
end;

Unas de las cosas a tener en cuenta es que cuando se define una estructura de datos no se puede definir una variable de tipo String sin dar su tamaño, ya que cada registro debe tener una longitud fija. Si ponemos String nos da un error al compilar, por eso hemos puesto en el nombre un String[50].

Con esto finalizamos la parte básica de tratamiento de archivos en Delphi.

Pruebas realizadas en Delphi 7.

Publicidad