11 julio 2007

Leer las dimensiones de imágenes JPG, PNG y GIF

Si estáis pensando en crear un visor de fotografías aquí os traigo tres procedimientos que leen al ancho y alto de imagenes con extensión JPG, PNG y GIF leyendo los bytes de su cabecera. No hay para BMP ya que se puede hacer con un componente TImage.

Antes de nada hay que incluir una función que lee los enteros almacenados en formato del procesador motorola, que guarda los formatos enteros en memoria al contrario de los procesadores Intel/AMD:

function LeerPalabraMotorola( F: TFileStream ): Word;
type
TPalabraMotorola = record
case Byte of
0: ( Value: Word );
1: ( Byte1, Byte2: Byte );
end;
var
MW: TPalabraMotorola;
begin
F.Read( MW.Byte2, SizeOf( Byte ) );
F.Read( MW.Byte1, SizeOf( Byte ) );
Result := MW.Value;
end;

El siguiente procedimiento toma como parámetros la ruta y nombre de una imagen JPG, y dos variales enteras donde se almacenará el ancho y alto de la imagen:

procedure DimensionJPG( sArchivo: string; var wAncho, wAlto: Word );
const
ValidSig: array[0..1] of Byte = ($FF, $D8);
Parameterless = [$01, $D0, $D1, $D2, $D3, $D4, $D5, $D6, $D7];
var
Sig: array[0..1] of byte;
F: TFileStream;
x: integer;
Seg: byte;
Dummy: array[0..15] of byte;
Len: word;
iLongitudLinea: LongInt;
begin
FillChar( Sig, SizeOf( Sig ), #0 );

F := TFileStream.Create( sArchivo, fmOpenRead );
try
iLongitudLinea := F.Read( Sig[0], SizeOf( Sig ) );

for x := Low( Sig ) to High( Sig ) do
if Sig[x] <> ValidSig[x] then
iLongitudLinea := 0;

if iLongitudLinea > 0 then
begin
iLongitudLinea := F.Read( Seg, 1 );

while ( Seg = $FF ) and ( iLongitudLinea > 0 ) do
begin
iLongitudLinea := F.Read( Seg, 1 );

if Seg <> $FF then
begin
if ( Seg = $C0 ) or ( Seg = $C1 ) then
begin
iLongitudLinea := F.Read( Dummy[0], 3 ); // Nos saltamos estos bytes
wAlto := LeerPalabraMotorola( F );
wAncho := LeerPalabraMotorola( F );
end
else
begin
if not ( Seg in Parameterless ) then
begin
Len := LeerPalabraMotorola( F );
F.Seek( Len - 2, 1 );
F.read( Seg, 1 );
end
else
Seg := $FF; { Fake it to keep looping. }
end;
end;
end;
end;
finally
F.Free;
end;
end;

Lo mismo para una imagen PNG:

procedure DimensionPNG( sArchivo: string; var wAncho, wAlto: Word );
type
TPNGSig = array[0..7] of Byte;
const
ValidSig: TPNGSig = (137,80,78,71,13,10,26,10);
var
Sig: TPNGSig;
F: TFileStream;
x: Integer;
begin
FillChar( Sig, SizeOf( Sig ), #0 );
F := TFileStream.Create( sArchivo, fmOpenRead );
try
F.read( Sig[0], SizeOf( Sig ) );

for x := Low( Sig ) to High( Sig ) do
if Sig[x] <> ValidSig[x] then
Exit;

F.Seek( 18, 0 );
wAncho := LeerPalabraMotorola( F );
F.Seek( 22, 0 );
wAlto := LeerPalabraMotorola( F );
finally
F.Free;
end;
end;

Y para una imagen GIF:

procedure DimensionGIF( sArchivo: string; var wAncho, wAlto: Word );
type
TCabeceraGIF = record
Sig: array[0..5] of char;
ScreenWidth, ScreenHeight: Word;
Flags, Background, Aspect: Byte;
end;

TBloqueImagenGIF = record
Left, Top, Width, Height: Word;
Flags: Byte;
end;
var
F: file;
Cabecera: TCabeceraGIF;
BloqueImagen: TBloqueImagenGIF;
iResultado: Integer;
x: Integer;
c: char;
bEncontradasDimensiones: Boolean;
begin
wAncho := 0;
wAlto := 0;

if sArchivo = '' then
Exit;

{$I-}
FileMode := 0; // Sólo lectura
AssignFile( F, sArchivo );
Reset( F, 1);
if IOResult <> 0 then
Exit;

// Lee la cabecera y se asegura de que sea un archivo válido
BlockRead( F, Cabecera, SizeOf( TCabeceraGIF ), iResultado );

if ( iResultado <> SizeOf( TCabeceraGIF ) ) or ( IOResult <> 0 ) or
( StrLComp( 'GIF', Cabecera.Sig, 3 ) <> 0 ) then
begin
Close( F );
Exit;
end;

{ Skip color map, if there is one }
if ( Cabecera.Flags and $80 ) > 0 then
begin
x := 3 * ( 1 shl ( ( Cabecera.Flags and 7 ) + 1 ) );
Seek( F, x );
if IOResult <> 0 then
begin
Close( F );
Exit;
end;
end;

bEncontradasDimensiones := False;
FillChar( BloqueImagen, SizeOf( TBloqueImagenGIF ), #0 );

BlockRead( F, c, 1, iResultado );
while ( not EOF( F ) ) and ( not bEncontradasDimensiones ) do
begin
case c of
',': // Encontrada imagen
begin
BlockRead( F, BloqueImagen, SizeOf( TBloqueImagenGIF ), iResultado );
if iResultado <> SizeOf( TBloqueImagenGIF ) then
begin
Close( F );
Exit;
end;

wAncho := BloqueImagen.Width;
wAlto := BloqueImagen.Height;
bEncontradasDimensiones := True;
end;

'ÿ': // esquivar esto
begin
// Nada
end;

// No hacer nada, ignorar
end;

BlockRead( F, c, 1, iResultado );
end;

Close( F );
{$I+}
end;

Así no será necesario cargar toda la imagen para averiguar sus dimensiones.

Pruebas realizadas en Delphi 7.

10 julio 2007

Descargar un archivo de Internet sin utilizar componentes

Añadiendo a nuestro formulario la librería WinINet se pueden descargar archivos por HTTP con la siguiente función:

function DescargarArchivo( sURL, sArchivoLocal: String ): boolean;
const BufferSize = 1024;
var
hSession, hURL: HInternet;
Buffer: array[1..BufferSize] of Byte;
LongitudBuffer: DWORD;
F: File;
sMiPrograma: String;
begin
sMiPrograma := ExtractFileName( Application.ExeName );
hSession := InternetOpen( PChar( sMiPrograma ), INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0 );

try
hURL := InternetOpenURL( hSession, PChar( sURL ), nil, 0, 0, 0 );

try
AssignFile( F, sArchivoLocal );
Rewrite( F, 1 );

repeat
InternetReadFile( hURL, @Buffer, SizeOf( Buffer ), LongitudBuffer );
BlockWrite( F, Buffer, LongitudBuffer );
until LongitudBuffer = 0;

CloseFile( F );
Result := True;
finally
InternetCloseHandle( hURL );
end
finally
InternetCloseHandle( hSession );
end
end;

El primer parámetro es la URL completa del archivo a descargar y el segundo la ruta y nombre del archivo donde se va a guardar en nuestro disco duro. Un ejemplo de llamada a la función sería:

DescargarArchivo( 'http:\\miweb.com\imagen.jpg', 'C:\Mis documentos\imagen.jpg' );

Pruebas realizadas en Delphi 7.

09 julio 2007

Averiguar el nombre del procesador y su velocidad

El registro de Windows suele almacenar gran cantidad de información no sólo de la configuración de los programas instalados, sino también el estado real del hardware de nuestro PC.

En esta ocasión vamos a leer el nombre del procesador y su velocidad desde nuestro programa. Antes de nada añadimos a uses:

uses
Windows, Messages, ..., Registry;

La siguiente función nos devuelve el nombre del procesador:

function NombreProcesador: string;
var
Registro: TRegistry;
begin
Result := '';
Registro := TRegistry.Create;

try
Registro.RootKey := HKEY_LOCAL_MACHINE;

if Registro.OpenKey( '\Hardware\Description\System\CentralProcessor\0', False ) then
Result := Registro.ReadString( 'Identifier' );
finally
Registro.Free;
end;
end;

Y esta otra nos da su velocidad (según la BIOS y el fabricante):

function VelocidadProcesador: string;
var
Registro: TRegistry;
begin
Registro := TRegistry.Create;
try
Registro.RootKey := HKEY_LOCAL_MACHINE;

if Registro.OpenKey( 'Hardware\Description\System\CentralProcessor\0', False ) then
begin
Result := IntToStr( Registro.ReadInteger( '~MHz' ) ) + ' MHz';
Registro.CloseKey;
end;
finally
Registro.Free;
end;
end;

Hay veces que dependiendo del procesador y del multiplicador de la BIOS casi nunca coincide la velocidad real que nos da Windows con la de verdad (sobre todo en procesadores AMD). Aquí tenemos otra función que calcula en un segundo la velocidad real del procesador con una pequeña rutina en ensamblador:

function CalcularVelocidadProcesador: Double;
const
Retardo = 500;
var
TimerHi, TimerLo: DWORD;
ClasePrioridad, Prioridad: Integer;
begin
ClasePrioridad := GetPriorityClass( GetCurrentProcess );
Prioridad := GetThreadPriority( GetCurrentThread );

SetPriorityClass( GetCurrentProcess, REALTIME_PRIORITY_CLASS );
SetThreadPriority( GetCurrentThread, THREAD_PRIORITY_TIME_CRITICAL );

Sleep( 10 );

asm
dw 310Fh
mov TimerLo, eax
mov TimerHi, edx
end;

Sleep( Retardo );

asm
dw 310Fh
sub eax, TimerLo
sbb edx, TimerHi
mov TimerLo, eax
mov TimerHi, edx
end;

SetThreadPriority( GetCurrentThread, Prioridad );
SetPriorityClass( GetCurrentProcess, ClasePrioridad );

Result := TimerLo / ( 1000 * Retardo );
end;

Nos devuelve el resultado en una variable double, donde que podríamos sacar la información en pantalla de la siguiente manera:

ShowMessage( Format( 'Velocidad calculada: %f MHz', [CalcularVelocidadProcesador] ) );

Pruebas realizadas en Delphi 7.

06 julio 2007

Convertir un icono en imagen BMP

Aunque hay cientos de librerías de iconos por la red que suele utilizar todo el mundo para sus programas, lo ideal sería diseñar nuestros propios iconos utilizando como plantilla los que hay por Internet.

Se que hay decenas de programas de diseño que convierten entre distintos formatos, pero lo ideal sería tener nuestro propio conversor. Para ello tenemos el siguiente procedimiento que convierte un archivo ICO en una imagen BMP para que luego podamos utilizarla en nuestras aplicaciones:

procedure ConvertirImagen( sIcono, sBMP: String );
var
Bitmap: TBitmap;
Imagen: TImage;
begin
Imagen := TImage.Create( nil );
Imagen.Picture.LoadFromFile( sIcono );
Bitmap := TBitMap.Create;

with Bitmap do
begin
PixelFormat := pf24bit;
Height := Application.Icon.Height;
Width := Application.Icon.Width;
Canvas.Draw( 0, 0, Imagen.Picture.Graphic );
end;

Bitmap.SavetoFile( sBMP );

Imagen.Free;
end;

El primer parámetro es el icono y el segundo la imagen BMP resultante. Lo que hace el procedimiento es cargar el icono en un objeto TImage, para después copiar su contenido en un bitmap antes de guardarlo en un archivo.

Pruebas realizadas en Delphi 7.

05 julio 2007

Borrar archivos temporales de Internet

Uno de los componentes más útiles de Delphi hoy en día es WebBrowser el cual nos permite crear dentro de nuestros programas un navedador web utilizando el motor de Internet Explorer.

El único inconveniente es que al finalizar nuestro programa tenemos que ir a Internet Explorer y vaciar la caché, evitando que se llene el disco duro de basura.

Pues vamos a ver un procedimiento que elimina los archivos temporales de Internet Explorer, no sin antes añadir la unidad WinINet:

uses
Windows, Messages, ..., WinInet;

procedure BorrarCacheIE;
var
lpEntryInfo: PInternetCacheEntryInfo;
hCacheDir: LongWord;
dwEntrySize: LongWord;
begin
dwEntrySize := 0;
FindFirstUrlCacheEntry( nil, TInternetCacheEntryInfo( nil^ ), dwEntrySize );
GetMem( lpEntryInfo, dwEntrySize );

if dwEntrySize > 0 then
lpEntryInfo^.dwStructSize := dwEntrySize;

hCacheDir := FindFirstUrlCacheEntry( nil, lpEntryInfo^, dwEntrySize );

if hCacheDir <> 0 then
begin
repeat
DeleteUrlCacheEntry( lpEntryInfo^.lpszSourceUrlName );
FreeMem( lpEntryInfo, dwEntrySize );
dwEntrySize := 0;
FindNextUrlCacheEntry( hCacheDir, TInternetCacheEntryInfo( nil^ ), dwEntrySize );
GetMem( lpEntryInfo, dwEntrySize );

if dwEntrySize > 0 then
lpEntryInfo^.dwStructSize := dwEntrySize;

until not FindNextUrlCacheEntry( hCacheDir, lpEntryInfo^, dwEntrySize );
end;

FreeMem( lpEntryInfo, dwEntrySize );
FindCloseUrlCache( hCacheDir );
end;

Este procedimiento habría que ejecutarlo al cerrar nuestro programa dejando el sistema limpio.

Pruebas realizadas en Delphi 7.

04 julio 2007

Deshabilitar el cortafuegos de Windows XP

Una de las tareas más frecuentes a las que se enfrenta un programador es la de crear aplicaciones que automaticen procesos de nuestra aplicación tales como subir datos por FTP, conectar con otro motor de bases de datos para enviar, etc.

Y si hay algún programa que pueda interrumpir el proceso de cara a las comunicaciones TCP/IP es el cortafuegos de Windows. Primero añadimos a uses:

uses
Windows, Messages, ..., WinSvc, ShellApi;

Este sería un el procedimiento que detiene el servicio:

procedure DeshabilitarCortafuegosXP;
var
SCM, hService: LongWord;
sStatus: TServiceStatus;
begin
SCM := OpenSCManager( nil, nil, SC_MANAGER_ALL_ACCESS );
hService := OpenService( SCM, PChar( 'SharedAccess' ), SERVICE_ALL_ACCESS );
ControlService( hService, SERVICE_CONTROL_STOP, sStatus );
CloseServiceHandle( hService );
end;

Para volver a activarlo sólo hay que ir al panel de control y ponerlo en marcha de nuevo. Esto no vale para otros cortafuegos (Panda, Norton, etc.)

Pruebas realizadas en Delphi 7.

03 julio 2007

Leer el número de serie de una unidad

Cuando se vende un programa generalmente se suele poner el precio según el número de equipos donde se va a instalar (licencia). Proteger nuestra aplicación contra copias implica leer algo característico en el PC que lo haga único.

Pues bien, cuando se formatea una unidad de disco Windows le asigna un número de serie que no cambiará hasta que vuelva a ser formateada. Lo que vamos a hacer es una función que toma como parámetro la unidad de disco que le pasemos (C:, D:, ...) y nos devolverá su número de serie:

function LeerSerieDisco( cUnidad: Char ): String;
var
dwLongitudMaxima, VolFlags, dwSerie: DWord;
begin
if GetVolumeInformation( PChar( cUnidad + ':\' ), nil, 0,
@dwSerie, dwLongitudMaxima, VolFlags, nil, 0) then
begin
// devolvemos el número de serie en hexadecimal
Result := IntToHex( dwSerie, 8 );
Insert( '-', Result, 5 );
end
else
Result := '';
end;

Nos devolverá algo como esto:

D4BD-0EC7

Con ese número ya podemos crear nuestro propio keygen alterando las letras, el orden o utilizando el algoritmo de encriptación que nos apetezca.

El único inconveniente es que si el usuario vuelve a formatear esa unidad entonces nos tiene que volver a pedir el número de serie. Hay otros programadores que prefieren leer el número de la BIOS o de la tarjeta de video, ya depende del nivel de protección que se desee.

Pruebas realizadas en Delphi 7.

02 julio 2007

Leer los archivos del portapapeles

El siguiente procedimiento lee el nombre de archivos o directorios del portapapales capturados por el usuario con CTRL + C ó CTRL + X y los muestra en un ListBox que le pasamos como parámetro:

procedure LeerArchivosPortapapeles( Lista: TListBox );
var
HPortapapeles: THandle; // Handle del portapapeles
iNumArc, i: Integer; // Nº de archivos
Archivo: array [0..MAX_PATH - 1] of char;
begin
if ClipBoard.HasFormat( CF_HDROP ) then
begin
HPortapapeles := ClipBoard.GetAsHandle( CF_HDROP );
iNumArc := DragQueryFile( HPortapapeles, $FFFFFFFF, nil, 0);

for i := 0 to iNumArc - 1 do
begin
DragQueryFile( HPortapapeles, i, @Archivo, MAX_PATH );
Lista.Items.Add( Archivo );
end;
end;
end;

Para poder compilarlo hay que añadir las unidades externas:

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

Sólo mostrará archivos o directorios y no imágenes o cualquier otro archivo capturado dentro de un programa. Puede sernos de utilidad para realizar programas de copia de seguridad, conversiones de archivo, etc.

Pruebas realizadas en Delphi 7.

29 junio 2007

Listar los programas instalados en Windows

Windows almacena la lista de programas instalados (Agregar/Quitar programas) en la clave de registro:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\

En esa clave hay tantas subclaves como programas instalados. Pero lo que nos interesa a nosotros no es el nombre de la clave del programa instalado sino el nombre del programa que muestra Windows en Agregar/Quitar programas. Para ello entramos en cada clave y leemos el valor DisplayName.

Lo primero añadimos la unidad:

uses
Windows, Messages, ..., Registry;

Y aquí tenemos un procedimiento al cual le pasamos un ListBox y nos lo rellena con la lista de programas instalados en Windows:

procedure ListarAplicaciones( Lista: TListBox );
const
INSTALADOS = '\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall';
var
Registro: TRegistry;
Lista1 : TStringList;
Lista2 : TStringList;
j, n : integer;
begin
Registro := TRegistry.Create;
Lista1 := TStringList.Create;
Lista2 := TStringList.Create;

// Guardamos todas las claves en la lista 1
with Registro do
begin
RootKey := HKEY_LOCAL_MACHINE;
OpenKey( INSTALADOS, False );
GetKeyNames( Lista1 );
end;

// Recorremos la lista 1 y leemos el nombre del programa instalado
for j := 0 to Lista1.Count-1 do
begin
Registro.OpenKey( INSTALADOS + '\' + Lista1.Strings[j], False );
Registro.GetValueNames( Lista2 );

// Mostramos el programa instalado sólo si tiene DisplayName
n := Lista2.IndexOf( 'DisplayName' );
if ( n <> -1 ) and ( Lista2.IndexOf('UninstallString') <> -1 ) then
Lista.Items.Add( ( Registro.ReadString( Lista2.Strings[n] ) ) );
end;

Lista.Sorted := True; // Ordenamos la lista alfabéticamente
Lista1.Free;
Lista2.Free;
Registro.CloseKey;
Registro.Destroy;
end;

Con esto se podría hacer un programa que eliminara de Agregar/Quitar programas aquellas claves de programas mal desinstalados.

Pruebas realizadas en Delphi 7.

28 junio 2007

Ejecutar un programa y esperar a que termine

Uno de los problemas habituales con los que se enfrenta un programador es que su cliente le pida algo que o bien no sabe como programarlo o no dispone del componente o librería necesaria para llevar tu tarea a cabo.

Un ejemplo puede ser realizar una copia de seguridad en formatos ZIP, RAR, 7Z, etc., convertir de un formato de video o sonido a otro e incluso llamar a comandos del sistema para realizar procesos criticos en un servidor. Entonces sólo se nos ocurre llamar a un programa externo que realice la tarea por nosostros (y que soporte parámetros).

Sé lo que estáis pensando (la función WinExec), pero en este caso no me vale ya que el programa tiene que esperar a que termine de ejecutarse antes de pasar al siguiente proceso.

Aquí os muestro un procedimiento que ejecuta un programa y se queda esperando a que termine:

function EjecutarYEsperar( sPrograma: String; Visibilidad: Integer ): Integer;
var
sAplicacion: array[0..512] of char;
DirectorioActual: array[0..255] of char;
DirectorioTrabajo: String;
InformacionInicial: TStartupInfo;
InformacionProceso: TProcessInformation;
iResultado, iCodigoSalida: DWord;
begin
StrPCopy( sAplicacion, sPrograma );
GetDir( 0, DirectorioTrabajo );
StrPCopy( DirectorioActual, DirectorioTrabajo );
FillChar( InformacionInicial, Sizeof( InformacionInicial ), #0 );
InformacionInicial.cb := Sizeof( InformacionInicial );

InformacionInicial.dwFlags := STARTF_USESHOWWINDOW;
InformacionInicial.wShowWindow := Visibilidad;
CreateProcess( nil, sAplicacion, nil, nil, False,
CREATE_NEW_CONSOLE or NORMAL_PRIORITY_CLASS,
nil, nil, InformacionInicial, InformacionProceso );

// Espera hasta que termina la ejecución
repeat
iCodigoSalida := WaitForSingleObject( InformacionProceso.hProcess, 1000 );
Application.ProcessMessages;
until ( iCodigoSalida <> WAIT_TIMEOUT );

GetExitCodeProcess( InformacionProceso.hProcess, iResultado );
MessageBeep( 0 );
CloseHandle( InformacionProceso.hProcess );
Result := iResultado;
end;

El parámetro iVisibilidad puede ser:

SW_SHOWNORMAL -> Lo normal
SW_SHOWMINIMIZED -> Minimizado (ventanas MS-DOS o ventanas no modales)
SW_HIDE -> Oculto (ventanas MS-DOS o ventanas no modales)

La función devuelve un cero si la ejecución terminó correctamente.

Por ejemplo para ejecutar la calculadora de Windows y esperar a que termine:

procedure EjecutarCalculadora;
begin
if EjecutarYEsperar( 'C:\Windows\System32\Calc.exe', SW_SHOWNORMAL ) = 0 then
ShowMessage( 'Ejecución terminada con éxito.' )
else
ShowMessage( 'Ejecución no terminada correctamente.' );
end;

Pruebas realizadas en Delphi 7.

27 junio 2007

Obtener modos de video

A la hora de realizar un programa hay que tener muy presente la resolución de la pantalla y el área de trabajo donde se pueden colocar las ventanas.

Para ello tenemos el objeto TScreen que nos devuelve no sólo la resolución actual de video sino que además nos da el tamaño del escritorio y el área de trabajo del mismo (si esta fija la barra de tareas hay que respetar su espacio y procurar que nuestra ventana no se superponga a la misma).

Si utilizamos las propiedades Position o WindowsState del formulario no hay que preocuparse por esto, pero si hemos creado nuestra propia piel y pasamos de las ventanas normales de Windows hay que andarse con ojo y no dejar que el usuario se crea que ha desaparecido la barra de tareas.

El siguiente procedimiento vuelca la información de la pantalla actual en un objeto Memo llamado PANTALLA:

procedure TFInformacion.InfoPantalla;
begin
PANTALLA.Lines.Clear;
PANTALLA.Lines.Add( Format( 'Resolución: %dx%d ', [Screen.Width, Screen.Height] ) );
PANTALLA.Lines.Add( Format( 'Escritorio: x: %d y: %d Ancho: %d Alto: %d',
[Screen.DesktopLeft, Screen.DesktopTop, Screen.DesktopWidth, Screen.DesktopHeight] ) );
PANTALLA.Lines.Add( Format( 'Area de trabajo: x: %d y: %d Ancho: %d Alto: %d',
[Screen.WorkAreaLeft, Screen.WorkAreaTop, Screen.WorkAreaWidth, Screen.WorkAreaHeight] ) );
end;

Otra información interesante sería saber que resoluciones posibles tiene nuestra tarjeta de video. Vamos a mostrar todos los modos de video posible en un ListBox llamado MODOS:

procedure TFInformacion.InfoModosVideo;
var i: Integer;
ModoVideo: TDevMode;
begin
i := 0;
MODOS.Clear;
while EnumDisplaySettings( nil, i, ModoVideo ) do
begin
with ModoVideo do
MODOS.Items.Add(Format( '%dx%d %d Colores', [dmPelsWidth, dmPelsHeight, Int64(1) shl dmBitsperPel] ) );

Inc( i );
end;
end;

Esta es la típica información que suelen mostrar los programas tipo TuneUp.

Pruebas realizadas en Delphi 7.

26 junio 2007

Formularios transparentes

Si estais hartos de que vuestros formularios tengan el mismo aspecto soso de siempre podeis crear nuevos temas y skins tales como los reproductores BSPlayer, WinDVD, PowerDVD, etc.

Para ello tenemos aquí un procedimiento que toma como parámetro un formulario y que hará que todo su fondo sea transparente. Si queremos que algo no sea transparente lo ponemos dentro de un bitmap, shape o panel.

procedure TransparentarFormulario( Form: TForm );
var
Region, RegionTemp: HRGN;
i: Integer;
Rect: TRect;
begin
Region := 0;

for i := 0 to Form.ControlCount - 1 do
begin
Rect := Form.Controls[i].BoundsRect;
OffsetRect( Rect, Form.ClientOrigin.x - Form.Left, Form.ClientOrigin.y - Form.Top );
RegionTemp := CreateRectRgnIndirect( Rect );

if Region = 0 then
Region := RegionTemp
else
begin
CombineRgn( Region, Region, RegionTemp, RGN_OR );
DeleteObject( RegionTemp );
end;
end;

RegionTemp := CreateRectRgn( 0, 0, Form.Width,
GetSystemMetrics( SM_CYCAPTION )+
GetSystemMetrics( SM_CYSIZEFRAME )+
GetSystemMetrics( SM_CYMENU ) * Ord( Form.Menu <> nil ) );

CombineRgn( Region, Region, RegionTemp, RGN_OR );
DeleteObject( RegionTemp );
SetWindowRgn( Form.Handle, Region, True );
end;

Quitando los margenes de la ventana con BorderStyle = bsNone ya tenemos un formulario completamente transparente donde sólo hay que insertarle los objetos que van a ser visibles.

Pruebas realizadas en Delphi 7.

25 junio 2007

Utilizar una fuente TTF sin instalarla

Uno de los primeros inconvenientes al distribuir nuestras aplicaciones es ver que en otros Windows aparecen nuestras etiquetas y campos desplazados debido a que la fuente que utilizan no es la que tenemos nosotros en nuestro equipo.

O bien utilizamos fuentes estandar tales como Tahoma, Arial, etc. o podemos utilizar el siguiente truco que consiste en añadir la fuente utilizada al lado de nuestro ejecutable y cargarla al arrancar nuestra aplicación.

El procedimiento para cargar una fuente es:

procedure CargarFuente( sFuente: String );
begin
AddFontResource( PChar( ExtractFilePath( Application.ExeName ) + sFuente ) );
SendMessage( HWND_BROADCAST, WM_FONTCHANGE, 0, 0 );
end;

Y en el procedimiento OnCreate de nuestro formulario cargamos la fuente:

procedure TFPrincipal.FormCreate( Sender: TObject );
begin
CargarFuente( 'Diner.ttf' );
Etiqueta.Font.Name := 'Diner';
end;

Donde se supone que el archivo Diner.ttf está al lado de nuestro ejecutable.

Antes de cerrar nuestra aplicación debemos liberar de memoria la fuente utilizada con el procedimiento:

procedure EliminarFuente( sFuente: String );
begin
RemoveFontResource( PChar( ExtractFilePath( Application.ExeName ) + sFuente ) );
SendMessage( HWND_BROADCAST, WM_FONTCHANGE, 0, 0 );
end;

Este prodecimiento sería llamado en el evento OnDestroy del formulario:

procedure TFPrincipal.FormDestroy( Sender: TObject );
begin
EliminarFuente( 'Diner.ttf' );
end;

Es recomendable hacer esto una sola vez en el formulario principal de la aplicación y no en cada formulario del programa, a menos que tengamos un formulario que utiliza exclusivamente una fuente en concreto.

Pruebas realizadas en Delphi 7.

22 junio 2007

Obtener los favoritos de Internet Explorer

El siguiente procedimiento recursivo obtiene la lista de los enlaces favoritos de Internet Explorer. Puede ser de mucha utilidad en el caso de formatear el equipo y salvar la lista de favoritos a un archivo de texto o a una base de datos.

Lo primero es añadir en uses:

uses
Windows, Dialogs, ..., ShlObj;

Y este sería el procedimiento:

function ObtenerFavoritosIE( sRutaFavoritos: String ): TStrings;
var
Busqueda: TSearchrec;
ListaFavoritos: TStrings;
sRuta, sDirectorio, sArchivo: String;
Buffer: array[0..2047] of Char;
iEncontrado: Integer;
begin
ListaFavoritos := TStringList.Create;
try
sRuta := sRutaFavoritos + '\*.url';
sDirectorio := ExtractFilepath( sRuta );
iEncontrado := FindFirst( sRuta, faAnyFile, Busqueda );

while iEncontrado = 0 do
begin
SetString( sArchivo, Buffer,
GetPrivateProfileString( 'InternetShortcut',
PChar( 'URL' ), nil, Buffer, SizeOf( Buffer ),
PChar( sDirectorio + Busqueda.Name ) ) );
ListaFavoritos.Add( sArchivo );
iEncontrado := FindNext( Busqueda );
end;

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

while iEncontrado = 0 do
begin
if ( ( Busqueda.Attr and faDirectory ) > 0 ) and ( Busqueda.Name[1] <> '.' ) then
ListaFavoritos.AddStrings( ObtenerFavoritosIE( sDirectorio + '\' + Busqueda.name ) );

iEncontrado := FindNext( Busqueda );
end;

FindClose( Busqueda );
finally
Result := ListaFavoritos;
end;
end;

Para utilizar el procedimiento supongamos que en el formulario tenemos un componente ListBox (FAVORITOS) y un botón (BFavoritos) que al pulsarlo nos trae todos los favoritos a dicha lista:

procedure TFPrincipal.BFavoritosClick( Sender: TObject );
var
pidl: PItemIDList;
sRutaFavoritos: array[0..MAX_PATH] of Char;
begin
SHGetSpecialFolderLocation( Handle, CSIDL_FAVORITES, pidl );
SHGetPathFromIDList( pidl, sRutaFavoritos );
FAVORITOS.Items := ObtenerFavoritosIE( StrPas( sRutaFavoritos ) );
end;

Pruebas realizadas en Delphi 7.

21 junio 2007

Crear un acceso directo

Aquí tenemos un pequeño pero interesante procedimiento para crear accesos directos en Windows. Antes de implementarlo hay que añadir en uses una serie de unidades externas:

uses
Windows, Dialogs, ..., ShlObj, ActiveX, StdCtrls, Registry, ComObj;

Este sería el procedimiento:

procedure CrearAccesoDirecto( sExe, sArgumentos, sDirTrabajo, sNombreLnk, sDirDestino: string );
var
Objeto: IUnknown;
UnSlink: IShellLink;
FicheroP: IPersistFile;
WFichero: WideString;
begin
Objeto := CreateComObject( CLSID_ShellLink );
UnSlink := Objeto as IShellLink;
FicheroP := Objeto as IPersistFile;

with UnSlink do
begin
SetArguments( PChar( sArgumentos ) );
SetPath( PChar( sExe ) );
SetWorkingDirectory( PChar( sDirTrabajo ) );
end;

WFichero := sDirDestino + '\' + sNombreLnk;
FicheroP.Save( PWChar( WFichero ), False );
end;

Y estos son sus parámetros:

sExe -> Ruta que apunta al ejecutable o archivo a crear el acceso directo
sArgumentos -> Parámetros que le mandamos al EXE
sDirTrabajo -> Ruta al directorio de trabajo del ejecutable
sNombreLnk -> Nombre del acceso directo
sDirDestino -> Ruta destino donde se creará el acceso directo

Aquí os muestro un ejemplo de cómo crear un acceso directo de la calculadora de Windows al escritorio:

procedure CrearAccesoCalculadora;
var
sEscritorio: String;
Registro: TRegistry;
begin
Registro := TRegistry.Create;

// Leemos la ruta del escritorio
try
Registro.RootKey := HKEY_CURRENT_USER;

if Registro.OpenKey( '\Software\Microsoft\Windows\CurrentVersion\explorer\Shell Folders', True ) then
sEscritorio := Registro.ReadString( 'Desktop' );
finally
Registro.CloseKey;
Registro.Free;
inherited;
end;

CrearAccesoDirecto( 'C:\Windows\System32\calc.exe', '',
'C:\Windows\System32\', 'Calculadora.lnk', sEscritorio );
end;

Pruebas realizadas en Delphi 7.

20 junio 2007

Averiguar la versión de Windows

La siguiente función nos devuelve la versión de Windows donde se está ejecutando nuestro programa:

function ObtenerVersion: String;
var
osVerInfo: TOSVersionInfo;
VersionMayor, VersionMenor: Integer;
begin
Result := 'Desconocida';
osVerInfo.dwOSVersionInfoSize := SizeOf( TOSVersionInfo );

if GetVersionEx( osVerInfo ) then
begin
VersionMenor := osVerInfo.dwMinorVersion;
VersionMayor := osVerInfo.dwMajorVersion;

case osVerInfo.dwPlatformId of
VER_PLATFORM_WIN32_NT:
begin
if VersionMayor <= 4 then
Result := 'Windows NT'
else
if ( VersionMayor = 5 ) and ( VersionMenor = 0 ) then
Result := 'Windows 2000'
else
if ( VersionMayor = 5 ) and ( VersionMenor = 1 ) then
Result := 'Windows XP'
else
if ( VersionMayor = 6 ) then
Result := 'Windows Vista';
end;

VER_PLATFORM_WIN32_WINDOWS:
begin
if ( VersionMayor = 4 ) and ( VersionMenor = 0 ) then
Result := 'Windows 95'
else
if ( VersionMayor = 4 ) and ( VersionMenor = 10 ) then
begin
if osVerInfo.szCSDVersion[1] = 'A' then
Result := 'Windows 98 Second Edition'
else
Result := 'Windows 98';
end
else
if ( VersionMayor = 4 ) and ( VersionMenor = 90 ) then
Result := 'Windows Millenium'
else
Result := 'Desconocida';
end;
end;
end;
end;

Primero averigua de que plataforma se trata. Si es Win32 los sistemas operativos pueden ser: Windows 95, Windows 98, Windows 98 Second Edition y Windows Millenium. Por otro lado, si se trata de la plataforma NT entonces las versiones son: Windows NT, Windows 2000, Windows XP y Windows Vista.

Esta función puede ser de utilidad para saber que librerias DLL tiene instaladas, que comandos del sistema podemos ejecutar o para saber si tenemos que habilitar o deshabilitar ciertas funciones de nuestra aplicación.

Pruebas realizadas en Delphi 7.

19 junio 2007

Recorrer un árbol de directorios

El procedimiento que voy a mostrar a continuación recorre el contenido de directorios, subdirectorios y archivos volcando la información en un campo memo (TMemo).

Modificando su comportamiento puede ser utilizado para realizar copias de seguridad, calcular el tamaño de un directorio o borrar el contenido de los mismos.

El procedimiento RecorrerDirectorio toma como primer parámetro la ruta que desea recorrer y como segundo parámetro si deseamos que busque también en subdirectorios:

procedure TFBuscar.RecorrerDirectorio( sRuta: String; bIncluirSubdirectorios: Boolean );
var
Directorio: TSearchRec;
iResultado: Integer;
begin
// Si la ruta no termina en contrabarra se la ponemos
if sRuta[Length(sRuta)] <> '\' then
sRuta := sRuta + '\';

// ¿No existe el directorio que vamos a recorrer?
if not DirectoryExists( sRuta ) then
begin
Application.MessageBox( PChar( 'No existe el directorio:' + #13 + #13 + sRuta ), 'Error', MB_ICONSTOP );
Exit;
end;

iResultado := FindFirst( sRuta + '*.*', FaAnyfile, Directorio );
while iResultado = 0 do
begin
// ¿Es un directorio y hay que entrar en él?
if ( Directorio.Attr and faDirectory = faDirectory ) and bIncluirSubdirectorios then
begin
if ( Directorio.Name <> '.' ) and ( Directorio.Name <> '..' ) then
RecorrerDirectorio( sRuta + Directorio.Name, True );
end
else
// ¿No es el nombre de una unidad ni un directorio?
if ( Directorio.Attr and faVolumeId <> faVolumeID ) then
Archivos.Lines.Add( sRuta + Directorio.Name );

iResultado := FindNext( Directorio );
end;

SysUtils.FindClose( Directorio );
end;

Antes de comenzar a buscar directorios se asegura de que la ruta que le pasemos termine en contrabarra y en el caso de que no sea así se la pone al final.

Para recorrer un directorio utiliza la estructura de datos TSearchRec la cual se utiliza para depositar en ella la información del contenido de un directorio mediante las funciones FindFirst y FindNext.

TSearchRec no contiene la información de todo el directorio sino que es un puntero al directorio o archivo actual. Sólo mirando los atributos mediante la propiedad Attr podemos saber si lo que estamos leyendo es un directorio, archivo o unidad.

También se cuida de saltarse los directorios '.' y '..' ya que sino el procedimiento recursivo RecorrerDirectorio se volvería loco hasta reventar la pila.

Realizar modificaciones para cambiar su comportamiento puede ser peligroso si no lleváis cuidado ya que la recursividad puede de dejar sin memoria la aplicación. Al realizar tareas como borrar subdirectorios mucho cuidado no darle la ruta C:\. Mejor hacer ensayos volcando el contenido en un Memo hasta tener el resultado deseado.

Pruebas realizadas en Delphi 7.

18 junio 2007

Ejecutar un programa al arrancar Windows

Para ejecutar automáticamente nuestra aplicación al arrancar Windows vamos a utilizar la siguiente clave del registro del sistema:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run\

Todos los programas que se introduzcan dentro de esa clave (antivirus, monitores del sistema, etc.) arrancarán al iniciar Windows.

Para ello vamos a utilizar el objeto TRegistry. Para ello hay que añadir su unidad correspondiente en uses:

uses Windows, Messages, ..., Registry;

Ahora vamos con el procedimiento encargado de poner nuestro programa
al inicio de Windows:

procedure TFPrincipal.PonerProgramaInicio;
var Registro: TRegistry;
begin
Registro := TRegistry.Create;
Registro.RootKey := HKEY_LOCAL_MACHINE;

if Registro.OpenKey( 'Software\Microsoft\Windows\CurrentVersion\Run', FALSE ) then
begin
Registro.WriteString( ExtractFileName( Application.ExeName ), Application.ExeName );
Registro.CloseKey;
end;

Registro.Free;
end;

El método WriteString toma como primer parámetro la el nombre del valor en el registro y como segúndo parámetro la ruta donde se encuentra el programa a ejecutar. En nuestro caso como nombre del valor le he dado el nombre de nuestro ejecutable y como segundo la ruta desde donde estamos ejecutando el programa en este mismo instante.

Si en un futuro deseamos quitar el programa entonces sólo hay que eliminar la clave:

procedure TFPrincipal.QuitarProgramaInicio;
var Registro: TRegistry;
begin
Registro := TRegistry.Create;
Registro.RootKey := HKEY_LOCAL_MACHINE;

if Registro.OpenKey( 'Software\Microsoft\Windows\CurrentVersion\Run', FALSE ) then
begin
// ¿Existe el valor que vamos a borrar?
if Registro.ValueExists( ExtractFileName( Application.ExeName ) ) then
Registro.DeleteValue( ExtractFileName( Application.ExeName ) );

Registro.CloseKey;
end;

Registro.Free;
end;

Hay ciertos antivirus como el NOD32 que saltan nada más compilar nuestro programa por el simple hecho de tocar la clave Run. Habrá que decirle a nuestro antivirus que nuestro programa no es maligno.

Pruebas realizadas en Delphi 7.

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

Publicidad