15 junio 2007

Minimizar en la bandeja del sistema

Una de las características mas utilizadas en los programas P2P es la de minimizar nuestra aplicación en la bandeja del sistema (al lado del reloj de Windows en la barra de tareas).

Voy a mostraros como modificar el formulario principal de vuestra aplicación para que se minimize en la bandeja del sistema y una vez minimizado cuando se pulse sobre el icono se restaure. También vamos a añadir la posibilidad de pulsar dicho icono con el botón derecho del ratón y que muestre un menu contextual (popup) con la opción Mostrar.

Lo primero de todo es añadir un menu contextual a nuestro formulario principal (PopupMenu) con el nombre MenuBandeja. Añadimos una sola opción llamada Mostrar. A continuación añadimos en la sección uses del formulario principal la unidad ShellAPI:

uses
Windows, Messages, ...., ShellAPI;

Después en la sección private insertamos la variable:

IconData: TNotifyIconData;

En la misma sección private añadimos los procedimientos:

procedure WMSysCommand( var Msg: TWMSysCommand ); message WM_SYSCOMMAND;
procedure Restaurar( var Msg: TMessage ); message WM_USER+1;

Cuya implementación sería la siguiente:

procedure TFPrincipal.WMSysCommand( var Msg: TWMSysCommand );
begin
if Msg.CmdType = SC_MINIMIZE then
Minimizar
else
DefaultHandler( Msg );
end;

procedure TFPrincipal.Restaurar( var Msg: TMessage );
var p: TPoint;
begin
// ¿Ha pulsado el botón izquierdo del ratón?
if Msg.lParam = WM_LBUTTONDOWN then
MostrarClick( Self );

// ¿Ha pulsado en la bandeja del sistema con el botón derecho del ratón?
if Msg.lParam = WM_RBUTTONDOWN then
begin
SetForegroundWindow( Handle );
GetCursorPos( p );
MenuBandeja.Popup( p.x, p.y );
PostMessage( Handle, WM_NULL, 0, 0 );
end;
end;

El procedimiento WMSysCommand es el encargado de interceptar los mensajes del sistema que manda Windows a nuestra aplicación. En el caso de que el mensaje enviado sea SC_MINIMIZE minimizamos la ventana en la bandeja del sistema. Si es otro mensaje dejamos que Windows lo maneje (DefaultHandler).

El procedimiento Restaurar comprueba si ha pulsado el botón izquierdo del ratón sobre el icono de la bandeja del sistema para volver a mostrar nuestra ventana. Si pulsa el botón derecho llamará a nuestro menu contextual MenuBandeja.

Ahora creamos el procedimiento encargado de minimizar la ventana:

procedure TFPrincipal.Minimizar;
begin
with IconData do
begin
cbSize := sizeof( IconData );
Wnd := Handle;
uID := 100;
uFlags := NIF_MESSAGE + NIF_ICON + NIF_TIP;
uCallbackMessage := WM_USER + 1;

// Usamos de icono el mismo de la aplicación
hIcon := Application.Icon.Handle;

// Como Hint del icono, el nombre de la aplicación
StrPCopy( szTip, Application.Title );
end;

// Ponemos el icono al lado del reloj
Shell_NotifyIcon( NIM_ADD, @IconData );

// Ocultamos el formulario
Hide;
end;

Y por último el evento al pulsar la opción Mostrar en el menú contextual:

procedure TFPrincipal.MostrarClick( Sender: TObject );
begin
// Volvemos a mostrar de nuevo el formulario
FPrincipal.Show;
ShowWindow( Application.Handle, SW_SHOW );

// Eliminamos el icono de la bandeja del sistema
Shell_NotifyIcon( NIM_DELETE, @IconData );
IconData.Wnd := 0;
end;

Aunque pueda parecer algo engorroso creo que es mas limpio que tener que instalar componentes para que realicen esto. Al fin y al cabo sólo hay que hacerlo sólo en el formulario principal.

Pruebas realizadas en Delphi 7.

14 junio 2007

Cómo ocultar una aplicación

Vamos a ver como hacer que una aplicación cualquiera hecha en Delphi quede oculta de la barra de tareas de Windows y del escritorio. Sólo podrá verse ejecutando el administrador de tareas en la pestaña Procesos.

Para ello vamos a añadir en el evento OnCreate del formulario principal de nuestra aplicación lo siguiente:

procedure TFPrincipal.FormCreate(Sender: TObject);
begin
// Hacemos que el formulario sea invisible poniendolo en la
// esquina superior izquierda, tamaño cero y aplicación invisible
BorderStyle := bsNone;
Left := 0;
Top := 0;
Width := 0;
Height := 0;
Visible := False;
Application.Title := '';
Application.ShowMainForm := False;

// Lo ocultamos de la barra de tareas
ShowWindow( Application.Handle, SW_HIDE );
SetWindowLong( Application.Handle, GWL_EXSTYLE,
GetWindowLong(Application.Handle, GWL_EXSTYLE) or
WS_EX_TOOLWINDOW and not WS_EX_APPWINDOW);
end;

Esto nos puede ser util para crear programas residentes ocultos al usuario para administración de copias de seguridad, reparación automática de bases de datos y envío de mailing automátizado.

Pruebas realizadas en Delphi 7.

13 junio 2007

Capturar el teclado en Windows

Hay ocasiones en las cuales nos interesa saber si una tecla de Windows ha sido pulsada aunque estemos en otra aplicación que no sea la nuestra.

Por ejemplo en el artículo anterior mostré como capturar la pantalla. Sería interesante que si pulsamos F8 estando en cualquier aplicación nos capture la pantalla (incluso si nuestra aplicación esta minimizada).

Para ello vamos a utilizar la función de la API de Windows GetAsyncKeyState la cual acepta como parámetro la tecla pulsada (VK_RETURN, VK_ESCAPE, VK_F8, etc) y nos devuelve -32767 si la tecla ha sido pulsada.

Como el teclado hay que leerlo constantemente y no conviene dejar un bucle cerrado consumiendo mucho procesador, lo que vamos a hacer es meter a nuestro formulario un temporizador TTimer activado cada 10 milisegundos (Inverval) y con el evento OnTimer definido de la siguiente manera:

procedure TFormulario.TemporizadorTimer( Sender: TObject );
begin
// ¿Ha pulsado una tecla?
if GetAsyncKeyState( VK_F8 ) = -32767 then
CapturarPantalla;
end;

Para capturar números o letras se hace con la función ord:

if GetAsyncKeyState( Ord( 'A' ) ) then ...
if GetAsyncKeyState( Ord( '5' ) ) then ...

Si es una letra hay que pasarla a mayúsculas.

Sólo con esto podemos interceptar cualquier tecla del buffer de Windows. Por ejemplo se podría hacer una aplicación que al pulsar F10 minimize todas las ventanas de Windows.

Pruebas realizadas en Delphi 7.

12 junio 2007

Capturar la pantalla de Windows

Vamos a crear un procedimiento que captura un trozo de la pantalla de Windows y la guarda en un bitmap:

procedure CapturarPantalla( x, y, iAncho, iAlto: Integer; Imagen: TBitmap );
var
DC: HDC;
lpPal : PLOGPALETTE;
begin
if ( iAncho = 0 ) OR ( iAlto = 0 ) then
Exit;

Imagen.Width := iAncho;
Imagen.Height := iAlto;
DC := GetDc( 0 );

if ( DC = 0 ) then
Exit;

if ( GetDeviceCaps( dc, RASTERCAPS) and RC_PALETTE = RC_PALETTE ) then
begin
GetMem( lpPal, SizeOf( TLOGPALETTE ) + ( 255 * SizeOf( TPALETTEENTRY ) ) );
FillChar( lpPal^, SizeOf( TLOGPALETTE ) + ( 255 * SizeOf( TPALETTEENTRY ) ), #0 );
lpPal^.palVersion := $300;
lpPal^.palNumEntries := GetSystemPaletteEntries( DC, 0, 256, lpPal^.palPalEntry );

if (lpPal^.PalNumEntries <> 0) then
Imagen.Palette := CreatePalette( lpPal^ );

FreeMem( lpPal, SizeOf( TLOGPALETTE ) + ( 255 * SizeOf( TPALETTEENTRY ) ) );
end;

BitBlt( Imagen.Canvas.Handle, 0, 0, iAncho, iAlto, DC, x, y, SRCCOPY );
ReleaseDc( 0, DC );
end;

Resumiendo a grandes rasgos lo que hace el procedimiento es crear un dispositivo de contexto donde según el número de bits por pixel reserva una zona de memoria para capturar el escritorio. Después mediante la función BitBlt vuelca la imagen capturada al Canvas de la imagen que le pasamos.

Para capturar toda la pantalla de Windows utilizando este procedimiento hacemos lo siguiente:

var Imagen: TBitmap;
begin
Imagen := TBitmap.Create;
CapturarPantalla( 0, 0, Screen.Width, Screen.Height, Imagen );
Imagen.SaveToFile( ExtractFilePath( Application.ExeName ) + 'captura.bmp' );
Imagen.Free;
end;

La pantalla capturada la guarda en el archivo captura.bmp al lado de nuestro ejecutable. Sólo faltaría el poder capturar una tecla de Windows desde cualquier aplicación para activar nuestro capturador de pantalla (para que no se capture a si mismo).

Pruebas realizadas en Delphi 7.

11 junio 2007

Descargando archivos por FTP con INDY

El compononente IdFTP que utilizamos en el artículo anterior para subir archivos es el mismo que vamos a utilizar para descargarlos. Añadimos a la sección interface:

uses
Windows, Messages, ......, IdFTP, IdComponent;

Y creamos el procedimiento para descargar el archivo:

procedure DescargarArchivo( sArchivo: String );
var
FTP: TIdFTP;
begin
FTP := TIdFTP.Create( nil );
FTP.OnWork := FTPWork;
FTP.Username := 'usuario';
FTP.Password := 'miclave';
FTP.Host := 'miftp.midominio.com';

try
FTP.Connect;
except
raise Exception.Create( 'No se ha podido conectar con el servidor ' + FTP.Host );
end;

FTP.ChangeDir( '/misarchivos/copiaseguridad/' );

Antes de comenzar la descarga hay que averiguar el tamaño del archivo en el servidor:

Barra.Max := FTP.Size( ExtractFileName( sArchivo ) ) div 1024;

Donde Barra es un objeto TProgressBar que colocamos para mostrar al usuario el progreso de la descarga. Ahora nos aseguramos de que el archivo a descargar no haya sido descargado anteriormente, ya que podría producir un error:

if FileExists( sArchivo ) then
DeleteFile( sArchivo );

Para descargar el archivo utilizaremos el método Get, el cual toma como primer parámetro la ruta y nombre del archivo a descargar en local, como segundo parámetro el nombre que va a tener el archivo en el servidor, como tercer parámetro si deseamos añadir a un archivo ya existente y como último parámetro si deseamos la opción RESUME (en caso de interrumpirse la conexión si queremos que continue por donde iba, siempre y cuando el servidor soporte dicho modo).

FTP.Get( ExtractFileName( sArchivo ), sArchivo, False, False );

Para finalizar nos desconectamos del servidor y eliminamos el objeto.

FTP.Disconnect;
FTP.Free;
end;

También creamos el evento OnWork para controlar el progreso de la descarga:

procedure TFVentana.FTPWork( Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Integer );
begin
Barra.Position := AWorkCount div 1024;
end;

Para terminar recomiendo meter el método Get en un hilo de ejecución por si el componente se queda bloqueado en la descarga.

Pruebas realizadas en Delphi 7.

05 junio 2007

Subiendo archivos por FTP con INDY

Para subir archivos por FTP utilizaremos el objeto TIdFTP de la paleta de componentes Indy Clients. Para poder utilizar dicho objeto debemos añadirlo en la seccion interface:

uses
Windows, Messages, ......, IdFTP, IdComponent;

La unidad IdComponent la utilizaremos luego para controlar eventos del componente FTP. El objeto lo creamos en tiempo de ejecución:

procedure SubirArchivo( sArchivo: String );
var
FTP: TIdFTP;
begin
FTP := TIdFTP.Create( nil );

Antes de subir un archivo hay que conectar con el servidor dando usuario y password:

FTP.Username := 'usuario';
FTP.Password := 'miclave';
FTP.Host := 'miftp.midominio.com';

try
FTP.Connect;
except
raise Exception.Create( 'No se ha podido conectar con el servidor ' + FTP.Host );
end;

Ahora ya estamos listos para enviar el archivo, pero antes debemos ir al directorio del servidor donde deseamos subir el archivo:

FTP.ChangeDir( '/misarchivos/copiaseguridad/' );

Para subir un archivo tenemos el método Put, el cual toma como primer parámetro la ruta y nombre del archivo a subir, como segundo parámetro el nombre que va a tener el archivo en el servidor (se supone que el mismo) y como tercer parámetro si deseamos añadir a un archivo ya existente o crear el archivo de nuevo:

FTP.Put( sArchivo, ExtractFileName( sArchivo ), False );

En nuestro caso hemos subido el archivo con el mismo nombre y si hubiera otro igual lo sobrescribe. Por último nos desconectamos del servidor y eliminamos el objeto.

FTP.Disconnect;
FTP.Free;
end;

Como suelo comentar en artículos anteriores los componentes Indy no controlan bien la multitarea, por lo tanto si vamos a subir archivos relativamente grandes recomiendo meter el método Put dentro de un hilo de ejecución, ya que si no es así, mientras que el componente TIdFTP no termine de subir el archivo, nuestra aplicación da el aspecto de estar colgada (no se puede ni mover la ventana).

Tampoco estaría mal mostrar al usuario mediante una barra de progreso el estado de la subida del archivo. Para ello el objeto TIdFtp posee el evento OnWork el cual nos informa de los bytes subidos al servidor. El cambio es sencillo.

Primero creamos el evento OnWork:

procedure TFVentana.FTPWork( Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Integer );
begin
Barra.Position := AWorkCount div 1024;
end;

Se supone que Barra es un componenete TProgressBar donde vamos acumulando el tamaño del archivo enviado. En este caso he dividido los bytes subidos entre 1024 para que me devuelva la información en kilobytes.

Y ahora asociamos el evento al componente después de crearlo:

FTP := TIdFTP.Create( nil );
FTP.OnWork := FTPWork;

¿Cómo averiguamos el tamaño del archivo para medir el progreso? Muy fácil:

procedure SubirArchivo( sArchivo: String );
var
FTP: TIdFTP;
F: File of byte;
begin
AssignFile( F, sArchivo );
Reset( F );
Barra.Max := FileSize( F ) div 1024;
CloseFile( F );
....

Con esto le decimos a la barra de progreso que la longitud máxima de la misma es la longitud del archivo en kilobytes. Una vez comience a subir el archivo la barra de progreso se va incrementando sola según el evento OnWork.

Y por supuesto nunca se os olvide controlar en todo momento el estado de la conexión, tanto para conectar, subir el archivo o desconectarse del servidor capturando las excepciones oportunas e informando al usuario de lo que ocurre (por ejemplo con una barra de estado en la parte inferior de la ventana).

En el próximo artículo mostraré como descargar un archivo por FTP.

Y lo se, a veces los componentes Indy pueden sacar de quicio a cualquiera.

Pruebas realizadas en Delphi 7.

11 agosto 2006

Guardando y cargando opciones

Hay ocasiones en que nos interesa que las opciones de nuestro programa permanezcan en el mismo después de terminar su ejecución. Principalmente se suelen utilizar cuatro maneras de guardar las opciones:

1º En un archivo de texto plano.

2º En un archivo binario.

3º En un archivo INI.

4º En el registro del sistema de Windows.

Vamos a suponer que tenemos un formulario de opciones con campos de tipo string, integer, boolean, date, time y real.

Los archivos de opciones se crearán en el mismo directorio en el que se ejecuta nuestra aplicación.

GUARDANDO OPCIONES EN TEXTO PLANO

Para ello utilizamos un archivo de tipo TextFile para guardar la información:

procedure TFPrincipal.GuardarTexto;
var F: TextFile;
begin
// Asignamos el archivo de opciones al puntero F
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'opciones.txt' );

// Abrimos el archivo en modo creación/escritura
Rewrite( F );

// Guardamos las opciones
WriteLn( F, IMPRESORA.Text );
WriteLn( F, IntToStr( COPIAS.Value ) );

if VISTAPREVIA.Checked then
WriteLn( F, 'CON VISTA PREVIA' )
else
WriteLn( F, 'SIN VISTA PREVIA' );

WriteLn( F, DateToStr( FECHA.Date ) );
WriteLn( F, HORA.Text );
WriteLn( F, FormatFloat( '###0.00', MARGEN.Value ) );

CloseFile( F );
end;

CARGANDO OPCIONES DESDE TEXTO PLANO

Antes de abrir el archivo comprobamos si existe:

procedure TFPrincipal.CargarTexto;
var F: TextFile;
sLinea: String;
begin
// Si no existe el archivo de opciones no hacemos nada
if not FileExists( ExtractFilePath( Application.ExeName ) + 'opciones.txt' ) then
Exit;

// Asignamos el archivo de opciones al puntero F
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'opciones.txt' );

// Abrimos el archivo en modo lectura
Reset( F );

// Cargamos las opciones
ReadLn( F, sLinea );
IMPRESORA.Text := sLinea;

ReadLn( F, sLinea );
COPIAS.Value := StrToInt( sLinea );

ReadLn( F, sLinea );
VISTAPREVIA.Checked := sLinea = 'CON VISTA PREVIA';

ReadLn( F, sLinea );
FECHA.Date := StrToDate( sLinea );

ReadLn( F, sLinea );
HORA.Text := sLinea;

ReadLn( F, sLinea );
MARGEN.Value := StrToFloat( sLinea );

CloseFile( F );
end;

GUARDANDO OPCIONES EN UN ARCHIVO BINARIO

Lo que hacemos en esta ocación es crear un registro (record) que contenga las opciones de nuestro programa. Después volcamos todo el contenido del registro en un archivo binario del mismo tipo.

En la interfaz de nuestra unidad definimos:

type
TOpciones = record
sImpresora: String[100];
iCopias: Integer;
bVistaPrevia: Boolean;
dFecha: TDate;
tHora: TTime;
rMargen: Real;
end;

Y creamos el procemimiento que lo graba:

procedure TFPrincipal.GuardarBinario;
var
// Creamos un registro y un fichero para el mismo
Opciones: TOpciones;
F: file of TOpciones;
begin
// Metemos las opciones del formulario en el registro
Opciones.sImpresora := IMPRESORA.Text;
Opciones.iCopias := COPIAS.Value;
Opciones.bVistaPrevia := VISTAPREVIA.Checked;
Opciones.dFecha := FECHA.Date;
Opciones.tHora := StrToTime( HORA.Text );
Opciones.rMargen := MARGEN.Value;

// Asignamos el archivo de opciones al puntero F
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'opciones.dat' );

// Abrimos el archivo en modo creación/escritura
Rewrite( F );

// Guardamos de golpe todas las opciones
Write( F, Opciones );

// Cerramos el fichero
CloseFile( F );
end;

CARGANDO OPCIONES DESDE UN ARCHIVO BINARIO

Utilizamos el registro creado anteriormente:

procedure TFPrincipal.CargarBinario;
var
// Creamos un registro y un fichero para el mismo
Opciones: TOpciones;
F: file of TOpciones;
begin
// Asignamos el archivo de opciones al puntero F
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'opciones.dat' );

// Abrimos el archivo en modo creación/escritura
Reset( F );

// Guardamos de golpe todas las opciones
Read( F, Opciones );

// Cerramos el fichero
CloseFile( F );

// Copiamos las opciones del registro en el formulario de opciones
IMPRESORA.Text := Opciones.sImpresora;
COPIAS.Value := Opciones.iCopias;
VISTAPREVIA.Checked := Opciones.bVistaPrevia;
FECHA.Date := Opciones.dFecha;
HORA.Text := TimeToStr( Opciones.tHora );
MARGEN.Value := Opciones.rMargen;
end;

GUARDANDO OPCIONES EN UN ARCHIVO INI

Los dos casos anteriores tienen un defecto: si ampliamos el número de opciones e intentamos cargar las opciones con el formato antiguo se puede provocar un error de E/S debido a que los formatos de texto o binario han cambiado.

Lo más flexible en este claso es utilizar un archivo INI, el cual permite agrupar opciones y asignar un nombre a cada una:

procedure TFPrincipal.GuardarINI;
var INI: TIniFile;
begin
// Creamos el archivo INI
INI := TINIFile.Create( ExtractFilePath( Application.ExeName ) + 'opciones.ini' );

// Guardamos las opciones
INI.WriteString( 'OPCIONES', 'IMPRESORA', IMPRESORA.Text );
INI.WriteInteger( 'OPCIONES', 'COPIAS', COPIAS.Value );
INI.WriteBool( 'OPCIONES', 'VISTAPREVIA', VISTAPREVIA.Checked );
INI.WriteDate( 'OPCIONES', 'FECHA', FECHA.Date );
INI.WriteTime( 'OPCIONES', 'HORA', StrToTime( HORA.Text ) );
INI.WriteFloat( 'OPCIONES', 'MARGEN', MARGEN.Value );

// Al liberar el archivo INI se cierra el archivo opciones.ini
INI.Free;
end;

CARGANDO OPCIONES DE UN ARCHIVO INI

Aunque aquí comprobamos si existe el archivo, no es necesario, ya que cargaría las opciones por defecto:

procedure TFPrincipal.CargarINI;
var INI: TIniFile;
begin
// Si no existe el archivo no hacemos nada
if not FileExists( ExtractFilePath( Application.ExeName ) + 'opciones.ini' ) then
Exit;

// Creamos el archivo INI
INI := TINIFile.Create( ExtractFilePath( Application.ExeName ) + 'opciones.ini' );

// Guardamos las opciones
IMPRESORA.Text := INI.ReadString( 'OPCIONES', 'IMPRESORA', '' );
COPIAS.Value := INI.ReadInteger( 'OPCIONES', 'COPIAS', 0 );
VISTAPREVIA.Checked := INI.ReadBool( 'OPCIONES', 'VISTAPREVIA', False );
FECHA.Date := INI.ReadDate( 'OPCIONES', 'FECHA', Date );
HORA.Text := TimeToStr( INI.ReadTime( 'OPCIONES', 'HORA', Time ) );
MARGEN.Value := INI.ReadFloat( 'OPCIONES', 'MARGEN', 0.00 );

// Al liberar el archivo INI se cierra el archivo opciones.ini
INI.Free;
end;

GUARDANDO OPCIONES EN EL REGISTRO DEL SISTEMA

Si queremos que nadie nos toque las opciones del programa podemos guardarlo todo en el registro de Windows:

procedure TFPrincipal.GuardarRegistroSistema;
var Reg: TRegistry;
begin
// Creamos un objeto para manejar el registro
Reg := TRegistry.Create;

// Guardamos las opciones
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey( '\Software\MiPrograma', True ) then
begin
Reg.WriteString( 'IMPRESORA', IMPRESORA.Text );
Reg.WriteInteger( 'COPIAS', COPIAS.Value );
Reg.WriteBool( 'VISTAPREVIA', VISTAPREVIA.Checked );
Reg.WriteDate( 'FECHA', FECHA.Date );
Reg.WriteTime( 'HORA', StrToTime( HORA.Text ) );
Reg.WriteFloat( 'MARGEN', MARGEN.Value );
Reg.CloseKey;
end;
finally
Reg.Free;
end;
end;

Para probar si se ha guardado la información pulsa el botón INICIO y opción EJECUTAR: REGEDIT. Las opciones se habrán guardado en la carpeta:

\HKEY_LOCAL_MACHINE\SOFTWARE\MiPrograma


CARGANDO OPCIONES DESDE EL REGISTRO DEL SISTEMA

Antes de cargar las opciones comprueba si existe la clave:

procedure TFPrincipal.CargarRegistroSistema;
var Reg: TRegistry;
begin
// Creamos un objeto para manejar el registro
Reg := TRegistry.Create;

// Guardamos las opciones
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey( '\Software\MiPrograma', True ) then
begin
IMPRESORA.Text := Reg.ReadString( 'IMPRESORA' );
COPIAS.Value := Reg.ReadInteger( 'COPIAS' );
VISTAPREVIA.Checked := Reg.ReadBool( 'VISTAPREVIA' );
FECHA.Date := Reg.ReadDate( 'FECHA' );
HORA.Text := TimeToStr( Reg.ReadTime( 'HORA' ) );
MARGEN.Value := Reg.ReadFloat( 'MARGEN' );
Reg.CloseKey;
end;
finally
Reg.Free;
end;
end;

Pruebas realizadas en Delphi 7

01 agosto 2006

Mejorar el aspecto de las ventanas

Voy a explicar el buen uso de un interfaz, desde el punto de vista de la distribución y alineamiento de los controles en las ventanas.

Muchos creerán que un interfaz atractiva es aquella que rediseña completamente la ventana con nuevos gráficos y miles de iconos. Y personalmente no es así. Cualquier aspecto gráfico que nos presente el sistema operativo es bueno, siempre y cuando respetemos unas reglas, que con la experiencia, he ido aprendiendo. Sin más preámbulos, comencemos la lección:

La regla de oro de un interfaz es la siguiente: EL ESPACIO.

Actualmente la resolución de pantalla se ha incrementado notablemente, sobre todo en ordenadores portátiles. Por ello, la resolución mínima indiscutible es a partir de 1024 x 768. De esta forma, no hay que tener reparo en crear ventanas con generosos espacios en blanco entre los controles. Ello da sensación de ventanas más limpias, menos agobiantes. Por supuesto, siempre y cuando la o las ventanas contengan pocos controles o no sean ventanas que ocupen toda la pantalla, cómo por ejemplo "Microsoft Outlook 2003".

Pueden aplicar las reglas que más creen oportunas, pero a modo de guía, pueden utilizar las medidas que utilizo yo para alinear y distrubir adecuadamente los controles, y presentar ventanas claras y elegantes. Las reglas son:

1. Al crear una ventana, siempre creo un rectángulo con un margen de 14 píxeles, a partir de los bordes de la ventana. De esta forma, todos los controles respetan ese marco, y mejora la sensación de alineación. Por supuesto, este marco de guía o referencia sólo tendrá utilidad en modo diseño, luego, al iniciar la aplicación, no debe aparecer.


2. A partir de aquí comienzo a colocar componentes en la ventana, siempre de arriba hacia abajo (según la importancia), y de izquierda a derecha. Y aquí entran en juego las medidas en píxeles de espacios en blanco que hay que respetar para un interfaz limpia:

* Si se muestra un comentario en la parte superior de la ventana para informar sobre la tarea que realiza, mantener 20 píxeles de espacio entre el comentario y el comienzo de los controles. También dejar 20 píxeles entre el último control mostrado y los botones de aceptar, cancelar, etc...

* Al mostrar controles no relacionados entre sí, mantener 14 píxeles de espacio.

* Al mostrar controles relacionados, mantener 8 píxeles de margen.

Imágenes de ejemplo:







Lo mostrado hasta ahora son reglas básicas para casí la mayoría de las ventanas. Pero, cuando hay páginas con pestañas, los márgenes de espacios en blanco varian. Lo mejor es dejaros estas capturas, y estudiaís las medidas:






Espero que el artículo sea de utilidad, y mejore el aspecto de vuestras aplicaciones.


Pruebas realizadas en Delphi 7.

27 julio 2006

Efectos de animación en las ventanas

En este artículo explicaré de forma detallada cómo crear animaciones para las ventanas de delphi con los mismos efectos que disponen los sistemas operativos Windows, y aclararé cuándo aplicarlos y los problemas que tienen.

La función encargada de animar ventanas es la siguiente (api de windows):

AnimateWindow

Y los parámetros que la definen son los siguientes:

hWnd - Manejador o Handle de la ventana, a la cuál se aplica el efecto.
dwTime - Velocidad para reproducir el efecto. A más tiempo, más suave y con más lentitud es el efecto.
dwFlags - Parámetros que definen el tipo de efecto, la orientación y la activación de la ventana.
Se pueden combinar varios parámetros para conseguir efectos personalizados.

Dentro del parámetro dwFlags, se pueden realizar los efectos de animación que detallo:

Tipos de efectos

AW_SLIDE

Esta es una animación de deslizamiento. Este parámetro es ignorado si se utiliza la bandera AW_CENTER. De forma predeterminada, y si no se indica este parámetro, todas las ventanas utilizan el efecto de persiana, o enrollamiento.

AW_BLEND

Aplica un efecto de aparición gradual. Recuerde utilizar este parámetro si la ventana tiene prioridad sobre las demás. Este efecto sólo funciona con Windows 2000 y Windows XP.

AW_HIDE

Oculta la ventana, sin animación. Hay que combinar con otro parámetro para que la ocultación muestre animación. Por ejemplo con AW_SLIDE o AW_BLEND.

AW_CENTER

Este efecto provoca que la ventana aparezca desde el centro de la pantalla o escritorio. Para que funcione, debe ser combinado con el parámetro AW_HIDE para mostrar la ventana, o no utilizar AW_HIDE para ocultarla.

Orientación al mostrar u ocultar

AW_HOR_POSITIVE

Animar la ventana de izquierda a derecha. Este parámetro puede ser combinado con las animaciones de deslizamiento o persiana. Si utiliza AW_CENTER o AW_BLEND, no tendrá efecto.

AW_HOR_NEGATIVE

Animar la ventana de derecha a izquierda. Este parámetro puede ser combinado con las animaciones de deslizamiento o persiana. Si utiliza AW_CENTER o AW_BLEND, no tendrá efecto.

AW_VER_POSITIVE

Animar la ventana de arriba hacia abajo. Este parámetro puede ser combinado con las animaciones de deslizamiento o persiana. Si utiliza AW_CENTER o AW_BLEND, no tendrá efecto.

AW_VER_NEGATIVE

Animar la ventana de abajo hacia arriba. Este parámetro puede ser combinado con las animaciones de deslizamiento o persiana. Si utiliza AW_CENTER o AW_BLEND, no tendrá efecto.


Otros parámetros

AW_ACTIVATE

Este parámetro traspasa el foco de activación a la ventana antes de aplicar el efecto. Recomiendo utilizarlo, sobre todo cuando las ventanas contiene algún tema de Windows XP. No utilizar con la bandera AW_HIDE.


Utilizando la función en Delphi

¿En qué evento utilizar esta función?

Normalmente, y a nivel personal y por experiencias negativas, siempre la utilizo en el evento FormShow de la ventana a la cuál aplicar el efecto. Un ejemplo sería el siguiente:

procedure TFForm1.FormShow(Sender: TObject);
begin
AnimateWindow( Handle, 400, AW_ACTIVATE or AW_SLIDE or AW_VER_POSITIVE );
end;

(Este efecto va mostrando la ventana de arriba hacia abajo con deslizamiento).


Problemas con los temas de Windows XP y las ventanas de Delphi

Naturalmente, no todo es una maravilla, y entre los problemas que pueden surgir al crear estos efectos, están los siguientes:

- Temas visuales de Windows XP:

Cuando un efecto de animación es mostrado, a veces ciertos controles de la ventana, cómo los TEdit, ComboBox, etc, no terminan de actualizarse, quedando con el aspecto antiguo de Windows 98. Para solucionar este problema, hay que escribir la función "RedrawWindow" a continuación de AnimateWindow:

procedure TFForm1.FormShow(Sender: TObject);
begin
AnimateWindow( Handle, 400, AW_ACTIVATE or AW_SLIDE or AW_VER_POSITIVE );
RedrawWindow( Handle, nil, 0, RDW_ERASE or RDW_FRAME or RDW_INVALIDATE or RDW_ALLCHILDREN );
end;

- Ocultando ventanas entre ventanas de Delphi:

Por un problema desconocido de delphi (por lo menos desconozco si Delphi 2006 lo hace), ocultar una ventana con animación, teniendo otras ventanas de delphi (de tu aplicación) detrás, produce un efecto de redibujado fatal, que desmerece totalmente el efecto realizado. Esto no pasa si las ventanas que aparecen detrás no son de Delphi, o de tu propia aplicación. Por ese motivo, personalmente nunca utilizo este efecto de ocultación de ventanas.

Últimos consejos

Por último, permitidme daros un consejo. Estos efectos también son válidos en Windows 2000, pero los efectos pueden no ser tan fluidos como en Windows XP. Por ello, no estaría mal que estos efectos sean una opción de configuración de vuestra aplicación, es decir, permitir al usuario activarlos o desactivarlos.

También recomiendo no abusar de estos efectos, al final terminan siendo un poco molestos. Realizarlos en las ventanas principales es mejor que en todas las ventanas.

Espero que el artículo sea de utilidad, y dé un toque de elegancia a vuestras aplicaciones.

Pruebas realizadas en Delphi 7.

26 julio 2006

Leyendo el correo con INDY

Vamos a dividir el proceso de descargar el correo en dos partes. Primero leemos las cabeceras de nuestros mensajes y después descargamos los mensajes que nos interesen. Esto puede ser útil para descartar el correo spam desde el mismo servidor.

Vamos a crear dos componentes de la paleta INDY en tiempo real, con lo cual hay que añadir al comienzo de nuestra unidad:

uses
IdPOP3, IdMessage;

A continuación vamos a crear un procedimiento donde le pasamos los datos de nuestra cuenta de correo y vuelca el contenido en un ListView (el cual se supone que contiene tres columnas: Dirección, Asunto y Fecha/hora).

procedure LeerCorreo( sServidor, sUsuario, sClave: String; Mensajes: TListView );
var
POP3: TIdPOP3;
Mensaje: TIdMessage;
i: Integer;
begin
// creamos el objeto POP3
POP3 := TIdPOP3.Create( nil );
POP3.Host := sServidor;
POP3.Username := sUsuario;
POP3.Password := sClave;
POP3.Port := 110;

// conectamos con el servidor
try
POP3.Connect;
except
raise Exception.Create( 'Error al conectar con el servidor.' );
end;

Mensaje := TIdMessage.Create( nil );
for i := 1 to POP3.CheckMessages do
begin
// Leemos la cabecera del mensaje
Mensaje.Clear;
POP3.RetrieveHeader( i, Mensaje );

Mensajes.Items.Add;
Mensajes.Items[i-1].SubItems.Add( Mensaje.From.Address ); // dirección
Mensajes.Items[i-1].SubItems.Add( Mensaje.Subject ); // asunto
Mensajes.Items[i-1].SubItems.Add( DateTimeToStr( Mensaje.Date ) ); // Fecha-hora
end;

FreeAndNil( Mensaje );
FreeAndNil( POP3 );
end;

Una vez tenemos las cabeceras del mensaje en nuestra lista ListView supongamos que haciendo doble clic se descarga el mensaje del servidor a nuestro disco duro. Antes de eso vamos a crear una clase llamada TMensaje que contenga el mensaje descargado del servidor. La implementación de la misma sería:

type
TMensaje = class
iNumero: Integer; // Nº de mensaje dentro de nuestro buzón de correo
sServidor, sUsuario, sClave: String;
sAsunto, sMensaje: String;
Adjuntos: TStringList;
sRutaAdjuntos: String; // Ruta donde se guardaran los archivos adjuntos

constructor Create;
destructor Destroy; override;
function Descargar: Boolean;
end;

Y aquí viene la implementación de sus métodos:

constructor TMensaje.Create;
begin
Adjuntos := TStringList.Create;
end;

destructor TMensaje.Destroy;
begin
FreeAndNil( Adjuntos );
inherited;
end;

function TMensaje.Descargar: Boolean;
var
POP3: TIdPOP3;
Mensaje: TIdMessage;
i: Integer;
sAdjunto: String; // Nombre del archivo adjunto
begin
// creamos el objeto POP3
POP3 := TIdPOP3.Create( nil );
POP3.Host := sServidor;
POP3.Username := sUsuario;
POP3.Password := sClave;
POP3.Port := 110;

// Conectamos con el servidor
try
POP3.Connect;
except
raise Exception.Create( 'Error al conectar con el servidor.' );
end;

// Leemos todo el mensaje
Mensaje := TIdMessage.Create( nil );
try
POP3.Retrieve( iNumero, Mensaje );
except
raise Exception.Create( 'Error al leer el mensaje.' );
end;

// y desconectamos del servidor
POP3.Disconnect;

// Mostramos el mensaje en otro formulario
sAsunto := Mensaje.Subject;

// ¿Tiene mensajes algún mensaje adjunto?
if Mensaje.MessageParts.Count > 0 then
begin
// Leemos todas las partes del mensaje
for i := 0 to Mensaje.MessageParts.Count - 1 do
begin
// ¿Esta parte es de texto?
if ( Mensaje.MessageParts.Items[i] is TIdText ) then
sMensaje := TIdText( Mensaje.MessageParts.Items[i] ).Body.Text
else
// ¿Esta parte es un archivo binaro adjunto?
if ( Mensaje.MessageParts.Items[i] is TIdAttachment ) then
begin
// Guardamos el nombre del archivo adjunto en una variable para hacerlo más legible
sAdjunto := TIdAttachment( Mensaje.MessageParts.Items[i] ).FileName;

// Si ya existe el archivo adjunto lo borramos para que no de error
if FileExists( sRutaAdjuntos + sAdjunto ) then
DeleteFile( sRutaAdjuntos + sAdjunto );

// Guardamos el archivo adjunto y lo añadimos a la lista de adjuntos
TIdAttachment( Mensaje.MessageParts.Items[i] ).SaveToFile( sRutaAdjuntos + sAdjunto );
Adjuntos.Add( sAdjunto );
end
end
end
else
// Tomamos todo el mensaje como un mensaje de texto
sMensaje := Mensaje.Body.Text;

FreeAndNil( Mensaje );
FreeAndNil( POP3 );

Result := True;
end;

Si nos fijamos en el código fuente vemos que el método Descargar controla si el mensaje lleva archivos adjuntos o no. Aunque los mensajes de correo electrónico suelen codificarse de cuarenta leches distintas, generalmente hay dos tipos:

1º Texto plano o página web sin contenido.
2º Multiparte, donde cada parte puede ser texto plano, página web, archivo binario, etc.

En ambos casos la codificación utilizada es MIME.

Si el archivo que nos envían tiene una plantilla de página web (casi todos hoy en día) hay que complicarse un poco la vida y sacarlo mediante un navegador (browser). Eso lo dejaré para otra ocasión.

Pues bien, por último creamos el método que hace doble clic en nuestra lista de mensajes y muestra el contenido del mensaje en otro formulario:

procedure TFFormPrincipal.MensajesDblClick( Sender: TObject );
var i: Integer;
Mensaje: TMensaje;
begin
// ¿Ha selecionado un mensaje de la lista?
if Mensajes.Selected <> nil then
begin
Mensaje := TMensaje.Create;
Mensaje.iNumero := Mensajes.Selected.Index+1;
Mensaje.sServidor := Servidor.Text;
Mensaje.sUsuario := Usuario.Text;
Mensaje.sClave := Clave.Text;
Mensaje.sRutaAdjuntos := ExtractFilePath( Application.ExeName ) + 'Adjuntos\';

if Mensaje.Descargar then
begin
Application.CreateForm( TFMensaje, FMensaje );
FMensaje.Asunto.Text := Mensaje.sAsunto;
FMensaje.Mensaje.Text := Mensaje.sMensaje;

for i := 0 to Mensaje.Adjuntos.Count-1 do
FMensaje.Adjuntos.Items.Add( Mensaje.Adjuntos[i] );

FMensaje.ShowModal;
end;

FreeAndNil( Mensaje );
end;
end;

Esta es la base de un programa lector de correo POP3 aunque realmente hay que controlar muchas más cosas, como por ejemplo el borrar del servidor los mensajes ya descargados (Ahora no lo hace).


Pruebas realizadas en Delphi 7.

19 julio 2006

Enviando un correo con INDY

Vamos a crear un procedimiento para mandar correos electrónicos utilizando el componente TIdSMTP de la paleta de componentes INDY CLIENTS.

El componente no hace falta ponerlo en el formulario, ya que lo creo en tiempo real dentro del procedimiento. Sólo hace falta añadir en el apartado USES de nuestro formulario lo siguiente:

uses
IdSMTP, IdMessage;

Vamos con el procedimiento que envía un mensaje de correo electrónico:

procedure EnviarMensaje( sUsuario, sClave, sHost, sAdjunto, sAsunto, sDestino, sMensaje: String );
var SMTP: TIdSMTP;
Mensaje: TIdMessage;
Adjunto: TIdAttachment;
begin
// Creamos el componente de conexión con el servidor
SMTP := TIdSMTP.Create( nil );
SMTP.Username := sUsuario;
SMTP.Password := sClave;
SMTP.Host := sHost;
SMTP.Port := 25;
SMTP.AuthenticationType := atLogin;

// Creamos el contenido del mensaje
Mensaje := TIdMessage.Create( nil );
Mensaje.Clear;
Mensaje.From.Name := sDestino;
Mensaje.From.Address := sDestino;
Mensaje.Subject := sAsunto;
Mensaje.Body.Text := sMensaje;
Mensaje.Recipients.Add;
Mensaje.Recipients.Items[0].Address := sDestino;

// Si hay que meter un archivo adjunto lo creamos y lo asignamos al mensaje
if sAdjunto <> '' then
begin
if FileExists( sAdjunto ) then
Adjunto := TIdAttachment.Create( Mensaje.MessageParts, sAdjunto );
end
else
Adjunto := nil;

// Conectamos con el servidor SMTP
try
SMTP.Connect;
except
raise Exception.Create( 'Error al conectar con el servidor.' );
end;

// Si ha conectado enviamos el mensaje y desconectamos
if SMTP.Connected then
begin
try
SMTP.Send( Mensaje );
except
raise Exception.Create( 'Error al enviar el mensaje.' );
end;

try
SMTP.Disconnect;
except
raise Exception.Create( 'Error al desconectar del servidor.' );
end;
end;

// Liberamos los objetos creados
if Adjunto <> nil then
FreeAndNil( Adjunto );

FreeAndNil( Mensaje );
FreeAndNil( SMTP );

Application.MessageBox( 'Mensaje enviado correctamente.',
'Fin de proceso',MB_ICONINFORMATION );
end;
Y este es un ejemplo de envío de mensajes:

EnviarMensaje( 'juanito33', 'djeuE21', 'smtp.terra.es',
'c:\documento.zip', 'Te envio mi documento',
'felipe8843@terra.es', 'Adjunto archivo: documento.zip' );

Con un poco de imaginación se puede hacer que muestre el estado de la conexión en la barra de estado e incluso una barra de progreso para ver cuanto queda por terminar de enviar.

Pruebas realizadas en Delphi 7.

17 julio 2006

Conectando a pelo con INTERBASE o FIREBIRD

Aunque Delphi contiene componentes para mostrar directamente datos de una tabla, en ocasiones nos obligan a mostrar el contenido de una base de datos en una página web o en una presentación multimedia con SDL, OPENGL ó DIRECTX. En este caso, los componentes de la pestaña DATA CONTROLS no nos sirven de nada. Nos los tenemos que currar a mano.

Voy a mostraros un ejemplo de conexión con una base de datos de INTERBASE o FIREBIRD mostrando el resultado directamente dentro de un componente ListView, aunque con unas modificaciones se puede lanzar el resultado a un archivo de texto, página web, XML o lo que sea.

Lo primero es conectar con la base de datos:

function ConectarBaseDatos( sBaseDatos: String ): TIBDatabase;
var DB: TIBDatabase;
begin  DB := TIBDatabase.Create( nil );
  DB.Name := 'IB';
  DB.DatabaseName := '127.0.0.1:' + sBaseDatos;
  DB.Params.Add( 'user_name=SYSDBA' );
  DB.Params.Add( 'password=masterkey' );
  DB.SQLDialect := 3;
  DB.LoginPrompt := False;
  try
   DB.Open;
  except
   raise Exception.Create( 'No puedo conectar con INTERBASE/FIREBIRD.' + #13 + #13 + 'Consulte con el administrador del programa.' );
  end;

  Result := DB;
end;

Si nos fijamos en el procedimiento, primero se crea en tiempo real un componente de conexión a bases de datos TIBDatabase. Después le decimos con que IP va a conectar (en principio en nuestra misma máquina) y la ruta de la base de datos que es la que se le pasa al procedimiento.

Más adelante le damos el usuario y password por defecto y desactivamos en Login. Finalmente contectamos con la base de datos controlando la excepción si casca.

Un ejemplo de conexión sería:

var DB: TIBDatabase;

DB := ConectarBaseDatos( 'c:\bases\bases.gdb' ); // PARA INTERBASE Ó
DB := ConectarBaseDatos( 'c:\bases\bases.fdb' ); // PARA FIREBIRD

if DB = nil then
  Exit;

Una vez conectados a la base de datos vamos a ver como listar los registros de una tabla dentro de un ListView:

procedure ListarTabla( DB: TIBDatabase; sTabla: String; Listado: TListView );
var Campos: TStringList;
  i: Integer;
  Consulta: TIBSQL;
  Transaccion: TIBTransaction;
begin
  if DB = nil then Exit;

  // Creamos un stringlist para meter los campos de la tabla
  Campos := TStringList.Create;
  DB.GetFieldNames( sTabla, Campos );

  // Creamos una transacción para la consulta
  Transaccion := TIBTransaction.Create( nil );
  Transaccion.DefaultDatabase := DB;

  // Creamos una consulta
  Consulta := TIBSQL.Create( nil );
  Consulta.Transaction := Transaccion;
  Consulta.SQL.Add( 'SELECT * FROM ' + sTabla );
  Transaccion.StartTransaction;
  try
   Consulta.ExecQuery;
  except
   Transaccion.Rollback;
   raise;
  end;

  // Creamos en el listview una columna por cada campo
  Listado.Columns.Clear;
  Listado.Columns.Add;
  Listado.Columns[0].Width := 0;
  for i := 0 to Campos.Count - 1 do
  begin
   Listado.Columns.Add;
   Listado.Columns[i+1].Caption := Campos[i];
   Listado.Columns[i+1].Width := 100;
  end;

  // Listamos los registros
  Listado.Clear;
  while not Consulta.Eof do
  begin
   Listado.Items.Add;

   for i := 0 to Campos.Count - 1 do
    Listado.Items[Listado.Items.Count-1].SubItems.Add( Consulta.FieldByName(
Campos[i] ).AsString );

   Consulta.Next;
  end;

  // Una vez hemos terminado liberamos los objetos creados
  FreeAndNil( Campos );
  FreeAndNil( Consulta );
  FreeAndNil( Transaccion );
end;

Por supuesto, todo este proceso se puede mejorar refactorizando código y dividiendo las partes más importantes en clases más pequeñas. Haciendo muchas pruebas con objetos TIBSQL y TIBQuery me he dado cuenta que para operaciones donde se requiere velocidad los objetos TIBSQL con mucho más rápidos que los TIBQuery, aunque estos últimos son mucho más completos.

Pruebas realizadas en Delphi 7

13 julio 2006

Cómo crear un hilo de ejecución

Hay ocasiones en que necesitamos que nuestro programa realize paralelamente algún proceso secundario que no interfiera en la aplicación principal, ya que si nos metemos en bucles cerrados o procesos pesados (traspaso de ficheros, datos, etc.) nuestra aplicación se queda medio muerta (no se puede ni mover la ventana, minimizarla y menos cerrarla).

Para ello lo que hacemos es crear un hilo de ejecución heredando de la clase TThread del siguiente modo:

THilo = class( TThread )
  Ejecutar: procedure of object;
  procedure Execute; override;
end;


La definición anterior hay que colocarla dentro del apartado Type de nuestra unidad (en la sección interface). Le he añadido el procedimiento Ejecutar para poder mandarle que procedimiento queremos que se ejecute paralelamente.

En el apartado implementation de nuestra unidad redifinimos el procedimiento de la clase TThread para que llame a nuestro procedimiento Ejecutar:

procedure THilo.Execute;
begin
  Ejecutar;
  Terminate;
end;


Con esto ya tenemos nuestra clase THilo para crear todos los hilos de ejecución que nos de la gana. Ahora vamos a ver como se crea un hilo y se pone en marcha:

var
Hilo: THilo; // variable global o pública

procedure CrearHilo;
begin
  Hilo.Ejecutar := ProcesarDatos;
  Hilo.Priority := tpNormal;
  Hilo.Resume;
end;

procedure ProcesarDatos;
begin
  // Este es el procedimiento que ejecutará nuestro hilo
  // Cuidado con hacer procesos críticos aquí
  // El procesamiento paralelo de XP no es el de Linux
  // Se puede ir por las patas abajo...
end;


Si en cualquier momento queremos detener la ejecución del hilo:

Hilo.Terminate;
FreeAndNil( Hilo );


Los hilos de ejecución sólo conviene utilizarlos en procesos críticos e importantes. No es conveniente utilizarlos así como así ya que se pueden comer al procesador por los piés.

Pruebas realizadas en Delphi 7

12 julio 2006

Bienvenidos a Delphi al Límite

Esta página nace como necesidad de reunir todos los trucos de Delphi y llegar mucho más allá de lo que se pretende con este lenguaje.

Todos los trucos y piezas de código colocadas aquí los habré probado personalmente y el código fuente estará en castellano, es decir, no me limitaré a hacer Copy-Paste.

También comentaré con que versión se ha probado el código fuente incluido.

Vamos a ver hasta donde puede llegar Delphi...

Publicidad