0% encontró este documento útil (0 votos)
98 vistas70 páginas

Analisis de Sistema en Delphi

El documento describe el uso del componente BackgroundWorker en Delphi para ejecutar tareas en segundo plano de forma concurrente. El componente permite crear un hilo separado para realizar un proceso mientras la interfaz de usuario permanece respondiendo, y proporciona eventos para informar del progreso y resultados. Se muestra un ejemplo completo de su uso para calcular y mostrar los múltiplos de números del 1 al 100.

Cargado por

elias
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
98 vistas70 páginas

Analisis de Sistema en Delphi

El documento describe el uso del componente BackgroundWorker en Delphi para ejecutar tareas en segundo plano de forma concurrente. El componente permite crear un hilo separado para realizar un proceso mientras la interfaz de usuario permanece respondiendo, y proporciona eventos para informar del progreso y resultados. Se muestra un ejemplo completo de su uso para calcular y mostrar los múltiplos de números del 1 al 100.

Cargado por

elias
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 70

El componente BackwroundWorker (y 2)

Vamos a ver un ejemplo de cómo utilizar este componente con un hilo de ejecución que recorra todos los números del 1 al
100 y nos calcule los múltiplos de 5, 7 y 9. Este sería el formulario principal de nuestro proyecto:

El formulario se compone de los siguientes componentes:

- 2 botones de la clase TButton llamados BComenzar y BCancelar para iniciar o detener el hilo de ejecución.

- 1 componente TBackgroundWorker llamado BackgroundWorker.

- 1 barra de progreso de la clase TProgressBar llamada Progreso y con su propiedad Smooth a True.

- 3 etiquetas (TLabel) y 3 casillas (TEdit) llamadas Multiplos5, Multiplos7 y Multiplos9 donde se irán alojando los
múltiplos encontrados de cada uno.

- 1 barra de estado de la clase TStatusBar llamada Estado con su propiedad SimplePanel a True.

Comencemos a meter código:

1º Al pulsar el botón Comenzar ponemos en marcha el hilo:


procedure TForm1.BComenzarClick(Sender: TObject);
begin
BackgroundWorker.Execute;
end;

2º Al pulsar el botón Cancelar le pedimos al hilo que se detenga:


procedure TForm1.BDetenerClick(Sender: TObject);
begin
BackgroundWorker.Cancel;
end;

3º En el evento OnWork del componente BackgroundWorker recorremos los 100 números:


procedure TForm1.BackgroundWorkerWork(Worker: TBackgroundWorker);
var
i: Integer;
begin
for i := 1 to 100 do
begin
// ¿Hay que cancelar el proceso?
if Worker.CancellationPending then
begin
// Le indicamos al hilo que lo cancelamos
Worker.AcceptCancellation;
Exit;
end;
// ¿Es múltiplo de 5?
if i mod 5 = 0 then
Worker.ReportFeedback(5, i);
// ¿Es múltiplo de 7?
if i mod 7 = 0 then
Worker.ReportFeedback(7, i);
// ¿Es múltiplo de 9?
if i mod 9 = 0 then
Worker.ReportFeedback(9, i);
// Esperamos 50 milisegundos
Sleep(50);
// Incrementamos la barra de progreso
Worker.ReportProgress(i);
end;
end;

Al principio del bucle controlamos si nos piden abandonar el hilo de ejecución:


// ¿Hay que cancelar el proceso?
if Worker.CancellationPending then
begin
// Le indicamos al hilo que lo cancelamos
Worker.AcceptCancellation;
Exit;
end;

Después comprobamos los múltiplos de cada número y en el caso de que así sea envío provoco un evento
OnWorkFeedBack para notificar el número que he encontrado:
// ¿Es múltiplo de 5?
if i mod 5 = 0 then
Worker.ReportFeedback(5, i);
// ¿Es múltiplo de 7?
if i mod 7 = 0 then
Worker.ReportFeedback(7, i);
// ¿Es múltiplo de 9?
if i mod 9 = 0 then
Worker.ReportFeedback(9, i);

Y por último provoco un pequeño retardo de 50 milisegundos (para ir viendo el progreso) y provoco un evento
OnWorkProgress para notificar el incremento en la barra de progreso:
// Esperamos 50 milisegundos
Sleep(50);
// Incrementamos la barra de progreso
Worker.ReportProgress(i);

Sigamos...

4º En el evento OnWorkFeedBack recogemos el mensaje que nos manda el hilo para ir guardando en cada casilla los
múltiplos de cada número:
procedure TForm1.BackgroundWorkerWorkFeedback(Worker: TBackgroundWorker;
FeedbackID, FeedbackValue: Integer);
begin
case FeedbackID of
5: Multiplos5.Text := Multiplos5.Text + IntToStr(FeedbackValue) + ',';
7: Multiplos7.Text := Multiplos7.Text + IntToStr(FeedbackValue) + ',';
9: Multiplos9.Text := Multiplos9.Text + IntToStr(FeedbackValue) + ',';
end;
end;

5º En el evento OnWorkProgress actualizamos la barra de progreso y la barra de estado en pantalla:


procedure TForm1.BackgroundWorkerWorkProgress(Worker: TBackgroundWorker;
PercentDone: Integer);
begin
Progreso.Position := PercentDone;
Estado.SimpleText := 'Procesando... ' + IntToStr(PercentDone) + '%';
end;

6º En el evento OnWorkComplete le decimos al usuario en la barra de progreso que hemos terminado:


procedure TForm1.BackgroundWorkerWorkComplete(Worker: TBackgroundWorker;
Cancelled: Boolean);
begin
Estado.SimpleText := 'Proceso finalizado';
end;

7º Por si acaso al usuario le da por cerrar el formulario mientras está el hilo en marcha tenemos que cancelarlo y esperar a
que termine (al igual que hacen los programas como Emule o BitTorrent cuando lo cerramos y espera a liberar memoria y
cerrar las conexiones):
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// ¿Esta el hilo en marcha?
if BackgroundWorker.IsWorking then
begin
// Le indicamos que se detenga
BackgroundWorker.Cancel;
// Esperamos a que se detenga
BackgroundWorker.WaitFor;
end;
end;

Al ejecutar el programa irá rellenando las casillas correspondientes:

Hasta finalizar el proceso:

Como podemos apreciar, este componente nos facilita mucho la labor con los hilos de ejecución respecto a la clase
TThread. Por las pruebas que he realizado en otros programas más complejos (facturación) es bastante sencillo de
manejar y muy estable.
Pruebas realizadas en RAD Studio 2007.
Publicado por Administrador en 09:25 3 comentarios
Etiquetas: componentes, sistema

03 julio 2009
El componente BackgroundWorker (1)
Aunque haya finalizado el tema referente a los hilos de ejecución con la clase TThread no está demás hablar de otros
componentes no oficiales de Delphi que también están relacionados con los hijos de ejecución.

En este caso el componente me lo recomendó un lector de este blog (gracias a Jorge Abel) y me puse a echarle un vistazo
a ver que tal. Este componente puede descargarse gratuitamente de esta página web (licencia Freeware):

http://www.delphiarea.com/products/

Concretamente en la sección de componentes que viene más abajo:

Al igual que la clase TThread, este componente permite crear un hilo de ejecución paralelo al hilo primario de nuestra
aplicación pero de una manera sencilla y sin tener que heredad de clases.

INSTALAR EL COMPONENTE

Como es un componente que va suelto (sin paquete) vamos a proceder a crear un paquete para el sólo. Para ello creamos
una carpeta llamada DephiArea donde vamos a descomprimir los archivos del componente:

Ahora nos vamos a Delphi y seleccionamos File -> New -> Package – Delphi for Win32. Aparecerá esto a la derecha en
la ventana del proyecto:

Ahora seleccionamos File -> Save all y nos vamos a la carpeta donde hemos descomprimido el componente, por ejemplo:

D:\CodeGear\RAD Studio\5.0\Componentes\DelphiArea\

Y guardamos el proyecto con el nombre DelphiArea.dproj. Volvemos a la ventana del proyecto y pinchamos el nombre
del mismo con el botón derecho del ratón y seleccionamos Add:
En esta ventana que aparece pulsamos el botón Browse y elegimos el archivo BackgroundWorker.pas:

Ahora ya sólo tenemos que seleccionar Compile:

Y después Install:
Si todo ha ido bien mostrará este mensaje:

Si creamos un nuevo proyecto y nos fijamos en la paleta de componentes veremos el nuevo componente que hemos
instalado:

Ya estamos listos para comenzar a trabajar con el mismo.

LAS CARACTERÍSTICAS DEL COMPONENTE BACKGROUNDWORKER

Insertamos el componente en el formulario donde queremos lanzar el hilo de ejecución:

Si nos fijamos en el inspector de objetos veremos que no se han estresado añadiendo propiedades:

Aunque mediante código tenemos acceso a estas variables:

CancellationPending: esta bandera booleana es utilizada para indicarnos que debemos abandonar el bucle principal del
hilo (el evento OnWork que veremos más adelante). Por defecto está a False. Se pondrá a True cuando llamemos al
método Cancel.

IsCancelled: variable booleana que nos dice si el hilo ha sido cancelado.


IsWorking: indica si el hilo se sigue ejecutando con normalidad.

ThreadID: es un número entero con el identificador que Windows ha asignado a nuestro hilo. Podemos utilizar esta
propiedad para llamar a otras funciones de la API de Windows relacionadas con los hilos y procesos.

Y para modificar su comportamiento tenemos estos métodos:

procedure Execute;

Comienza la ejecución del hilo.

procedure Cancel;

Solicita al hilo que detenga la ejecución, es decir, pone la variable CancellationPending a True para que seamos nosotros
los que abandonemos el bucle cerrado del evento OnWork.

procedure WaitFor;

Espera a que termine la ejecución del hilo.

procedure ReportProgress(PercentDone: Integer);

A este método sólo podemos llamarlo dentro del evento OnWork. Lo que hace es realizar una llamada al evento
OnWorkProgress para que situemos en el mismo lo que vamos a hacer para informar al hilo primario del porcentaje de
trabajo que hemos realizado.

procedure ReportProgressWait(PercentDone: Integer);

Igual que el procedimiento anterior pero espera a que termine el código que hemos colocado dentro de OnWorkProgress.

procedure ReportFeedback(FeedbackID, FeedbackValue: Integer);

Al igual que los dos procedimientos anteriores, sólo podemos llamar al mismo dentro del evento OnWork. Realiza una
llamada al evento OnWorkFeedBack para que podamos enviar mensajes al hilo primario.

procedure ReportFeedbackWait(FeedbackID, FeedbackValue: Integer);

Lo mismo que el procedimiento anterior pero espera a que termine la ejecución del código que hemos puesto en el evento
OnWorkFeedBack.

procedure Synchronice(Method: ThreadMethod);

Sólo podemos llamar a este procedimiento dentro del evento OnWork. Le pasamos como parámetro el método del hilo
principal (VCL) para que lo ejecute por nosotros, por ejemplo, para mostrar información en pantalla o incrementar una
barra de progreso.

procedure AcceptCancellation;

Sólo podemos llamar a este procedimiento dentro del evento OnWork. Debemos llamar a este procedimiento cuando
dentro del evento OnWork hemos visto que la variable CancellationPending es True y entonces abandonamos el bucle
principal y llamamos a este procedimiento para informar a hilo que hemos aceptado salir del mismo.

Estos son los eventos que incorpora:


Veamos para que sirve cada evento:

OnWork: Este método equivale al procedimiento Execute de la clase TThread, de hecho, se ejecuta cuando llamamos al
procedimiento Execute. Es aquí donde debemos introducir el bucle continuo que va a hacer el trabajo pesado. A cada ciclo
del bucle debemos comprobar el valor de la variable CancellationPending para saber si debemos terminarlo. Una vez
abandonemos el bucle debemos llamar al método AcceptCancellation que pondrá la variable IsCancelled a True.

OnWorkComplete: este evento se ejecutará cuando termine la ejecución del código que hemos puesto en el evento
OnWork ya sea porque ha terminado su ejecución o porque lo hemos cancelado. Aquí podemos poner el código
encargado de liberar los objetos creados o cerrar archivos abiertos.

OnWorkProgress: se ejecutará el código que introducimos a este evento cuando desde el evento OnWork hemos hecho
una llamada a los procedimientos ReportProgress o ReportProgressWait. Desde aquí podemos informar al hilo primario
de sobre cómo vamos.

OnWorkFeedBack: se ejecuta cuando nuestro hilo envía mensajes al hilo primario mediante los procedimientos
ReportFeedBack o ReportFeedBackWait.

COMO INSTALARLO EN DELPHI 7

Para instalar este componente en Delphi 7 hay que seguir estos pasos:

1º Seleccionamos en el menú superior Install -> Component.

2º En la ventana que aparece seleccionamos la pestaña Into new Package.

3º Pulsamos el botón Browse del campo Unit file name y seleccionamos el archivo BackgroundWorker.pas.

4º Pulsamos el botón Browse del campo Package file name y escribimos DelphiArea.dpk.

5º En el campo Package Description escribimos Delphi Area.

Suponiendo que el componente lo tengo descomprimido en esta carpeta:

D:\Borland\Delphi7\Componentes\BackgroundWorker\

Quedaría la ventana de este modo:

Pulsamos el botón Ok y aparecerá esta mensaje:


Pulsamos el botón Yes y si todo ha ido bien aparecerá este mensaje:

Con esto ya tiene que aparecer arriba en la paleta de componentes:

En el próximo artículo veremos un ejemplo de cómo crear un hilo de ejecución utilizando este componente.
Pruebas realizadas en RAD Studio 2007.
Publicado por Administrador en 09:52 3 comentarios
Etiquetas: componentes, sistema

19 junio 2009
Los Hilos de Ejecución (y 4)
Hoy voy a terminar de hablar de otras cuestiones relacionadas con los hilos de ejecución como pueden ser las variables de
tipo threadvar y las secciones críticas.

ACCESO A LA MISMA VARIABLE POR MULTIPLES HILOS

Antes de ver como utilizar una variable threadvar veamos un problema que se puede plantear cuando varios hilos
intentan acceder a una variable global sin utilizar el método synchronize.

Siguiendo con nuestro ejemplo de los tres hilos que incrementan una barra de progreso cada uno, supongamos que quiero
que cada barra llegue de 0 a 100 y que cuando termine voy a hacer que termine el hilo.

Después voy a crear una variable global llamada iContador:


var
iContador: Integer;

Cuando un hilo incremente la barra de progreso entonces incrementará también esta variable global iContador:
procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Inc(iContador);
Synchronize(ActualizarProgreso);
Sleep(100);
end;
end;

También he añadido al formulario una etiqueta llamada ETotal que mostrará por pantalla el contenido de la variable
iContador:
El procedimiento de ActualizarProgreso incrementará la barra de progreso y mostrará el contador del formulario:
procedure THilo.ActualizarProgreso;
begin
Progreso.StepIt;
FTresHilos.ETotal.Caption := IntToStr(iContador);
if Progreso.Position = 100 then
Terminate;
end;

Supuestamente, si tres hilos de ejecución incrementan cada barra de 0 a 100 entonces cuando terminen de ejecutarse el
contador tendrá el valor 300. Pero no es así:

Me ha salido 277 pero lo mismo puede dar 289 que 291. Como cada hilo accede a la variable global iContador
simultáneamente lo mismo la incrementa después de otro hilo que machaca el incremento del hilo anterior.

Ya vimos que esto puede solucionarse incluyendo la sentencia Inc(iContador) dentro del procedimiento
ActualizarProgreso, de modo que mediante la sentencia Synchronize sólo el hilo primario podrá incrementar esta
variable. Esto tiene un inconveniente y es que se forma un cuello de botella en los hilos de ejecución porque cada hilo
tiene que esperar a que el hilo primario incremente la variable.

Veamos si se puede solucionar mediante variables threadvar.

LAS VARIABLES THREADVAR

Las variables de tipo threadvar son declaradas globalmente en nuestro programa para que puedan ser leídas por uno o
más hilos simultáneamente pero no pueden ser modificadas por los mismos. Me explico.

Cuando un hilo lee de una variable global threadvar, si intenta modificarla sólo modificará una copia de la misma, no la
original, ya que solo puede ser modificada por el hilo primario. Delphi creará automáticamente una copia de la variable
threadvar para cada hilo (como si fuera una variable privada dentro del objeto que hereda de Thread).

Una variable threadvar se declara igual que una variable global:


implementation
threadvar
iContador: Integer;
Si volvemos a ejecutar el programa veremos que iContador nunca de mueve:

Entonces, ¿de que nos sirve la variable threadvar? Su cometido es crear una variable donde sólo el hilo primario la pueda
incrementar pero que a la hora de ser leía por un hilo secundario siempre tenga el mismo valor.

Para solucionar este problema lo mejor es que cada clase tenga su propio contador y que luego en el formulario principal
creemos un temporizador que muestre la suma de los contadores de cada hilo y de este modo no se crean cuellos de
botella ni es necesario utilizar synchronize.

Una utilidad que se le puede dar a este tipo de variables es cuando el hilo primario debe suministrar información crítica en
tiempo real a los hijos secundarios y sobre todo cuando queremos que ningún hilo lea un valor distinto de otro por el
simple hecho que lo ha ejecutado después.

SECCIONES CRÍTICAS

Anteriormente vimos como los objetos mutex podían controlar que cuando se ejecute cierto código dentro de un hilo de
ejecución, los demás hilos tienen que esperarse a que termine.

Esto también puede crearse mediante secciones críticas (CrititalSection). Una sección crítica puede ser útil para evitar
que varios hilos intenten enviar o recibir simultáneamente información de un dispositivo (monotarea). Naturalmente esto
solo tiene sentido si hay dos o más instancias del mismo hilo, sino es absurdo.

Aprovechando el caso anterior de los tres hilos incrementando la barra de progreso, imaginemos que cada vez que un hilo
intenta incrementa su barra de progreso, tiene que enviar su progreso por un puerto serie a un dispositivo. Aquí vamos a
suponer que ese puerto serie a un objeto TStringList (que bien podía ser por ejemplo un TFileStream).

Primero creamos una variable global llamada Lista:


var
Lista: TStringList;

Después la creo cuando pulso el botón Comenzar:


procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Lista := TStringList.Create;
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
...

Y en el procedimiento Execute envío la posición de su barra de progreso al StringList:


procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
Lista.Add(IntToStr(Progreso.Position));
Sleep(100);
end;
end;

Pero al ejecutar el programa puede ocurrir esto:

Eso ocurre porque los tres hilos intentan acceder a la vez al mismo objeto StringList. Si bien podíamos solucionar esto
utilizando Synchronize volvemos al problema de que cada hilo pierde su independencia respecto al hilo principal.

Lo que vamos a hacer es que si un hilo está enviando algo al objeto StringList los otros hilos no pueden enviarlo, pero si
seguir su normal ejecución. Esto se soluciona creando lo que se llama una sección crítica que aísla el momento en que un
hilo hace esto:
Lista.Add(IntToStr(Progreso.Position));

Para crear una sección crítica primero tenemos que declarar esta variable global:
var
Lista: TStringList;
SeccionCritica: TRTLCriticalSection;

Al pulsar el botón Comenzar inicializamos la sección crítica:


procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Lista := TStringList.Create;
InitializeCriticalSection(SeccionCritica);
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
...

Acordándonos que hay que liberarla al pulsar el botón Detener:


procedure TFTresHilos.BDetenerClick(Sender: TObject);
begin
Hilo1.Terminate;
Hilo2.Terminate;
Hilo3.Terminate;
DeleteCriticalSection(SeccionCritica);
end;

Después hay que modificar el procedimiento Execute para introducir nuestra instrucción peligrosa dentro de la sección
crítica:
procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
EnterCriticalSection(SeccionCritica);
Lista.Add(IntToStr(Progreso.Position));
LeaveCriticalSection(SeccionCritica);
Sleep(100);
end;
end;
Todo lo que se ejecute dentro de EnterCritialSection y LeaveCriticalSection solo será ejecutado a la vez por hilo,
evitando así la concurrencia. Con esto solucionamos el problema sin tener que recurrir al hilo primario.

Aunque he abarcando bastantes temas respecto a los hilos de ejecución todavía quedan muchas cosas que entran en la
zona de la API de Windows y que se salen de los objetivos de este artículo. Si encuentro algo más interesante lo publicaré
en artículos independientes.
Pruebas realizadas en RAD Studio 2007.
Publicado por Administrador en 12:18 2 comentarios
Etiquetas: sistema

05 junio 2009
Los Hilos de Ejecución (3)
En la tercera parte de este artículo vamos a ver como establecer la prioridad en los hilos de ejecución así como controlar
su comportamiento mediante objetos Event y Mutex.

LA PRIORIDAD EN LA EJECUCIÓN DE LOS HILOS

Aparte de poder controlar nosotros mismos la velocidad de hilo dentro del procedimiento Execute utilizando la función
Sleep, GetTickCount o TimeGetTime también podemos establecer la prioridad que Windows le va a dar a nuestro hilo
respecto al resto de aplicaciones.

Esto se hace utilizando la propiedad Priority que puede contener estos valores:
type
TThreadPriority = (
tpIdle, // El hilo sólo se ejecuta cuando el procesador está desocupado
tpLowest, // Prioridad más baja
tpLower, // Prioridad baja.
tpNormal, // Prioridad normal
tpHigher, // Prioridad alta
tpHighest, // Prioridad muy alta
tpTimeCritical // Obliga a ejecutarlo en tiempo real
);

No deberíamos abusar de las prioridades tpHigher, tpHighest y tpTimeCritical a menos que sea estrictamente necesario,
ya que podían restar velocidad a otras tareas críticas de Windows (como nuestro querido Emule).

Vamos a ver un ejemplo en el que voy a crear tres hilos de ejecución que actualizarán cada uno una barra de progreso cada
100 milisegundos:

Esta sería la definición del hilo:


THilo = class(TThread)
Progreso: TProgressBar;
procedure Execute; override;
procedure ActualizarProgreso;
end;
Y su implementación:
{ THilo }
procedure THilo.ActualizarProgreso;
begin
Progreso.StepIt;
end;
procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
Sleep(100);
end;
end;

Su única misión es incrementar la barra de progreso que le toca y esperar 100 milisegundos. Al pulsar el botón Comenzar
hacemos esto:
procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
Hilo1 := THilo.Create(True);
Hilo2 := THilo.Create(True);
Hilo3 := THilo.Create(True);
Hilo1.Progreso := Progreso1;
Hilo2.Progreso := Progreso2;
Hilo3.Progreso := Progreso3;
Hilo1.Resume;
Hilo2.Resume;
Hilo3.Resume;
end;

Al ejecutarlo los tres hilos van exactamente iguales:

Pero ahora supongamos que hago esto antes de ejecutarlos:


Hilo1.Priority := tpTimeCritical;
Hilo2.Priority := tpLowest;
Hilo3.Priority := tpHighest;

Al ejecutarlo nos ponemos a navegar con Firefox o nuestro navegador preferido en páginas que den caña al procesador y
al observar nuestros hilos veremos que se han desincronizado según su prioridad:
No es que sea una diferencia muy significativa pero a largo plazo se nota como Windows para repartiendo el trabajo. Esto
puede venir muy bien para llamar a rutinas que procesen datos en segundo plano (tpIdle) o bien enviar comandos a
máquinas conectadas al PC por puerto serie, paralelo o USB que necesiten ir como reloj suizo (tpTimeCritical).

CONTROLANDO LOS HILOS CON EVENTOS DE WINDOWS

Los hilos de ejecución también permiten comenzar su ejecución o detenerse dependiendo de un evento externo que o bien
puede ser controlado por el hilo primario de nuestra aplicación o bien por otro hilo.

Los eventos son objetos definidos en la API de Windows que permiten tener dos estados: señalizados y no señalizados.

Para crear un evento se utiliza esta función de la API de Windows:


CreateEvent(
lpEventAttributes, // atributos de seguridad
bManualReset, // si está a True significa que nosotros nos encargamos de señalizarlo
bInitialState, // Señalizado o no señalizado
lpName // Nombre el evento
): THandle;

Los atributos de seguridad vienen definidos en la API de Windows (en C) del siguiente modo:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;

Si le ponemos nil le asignará los atributos por defecto que tenemos como usuario en el sistema operativo Windows. Este
parámetro sólo sirve para los sistemas operativos Windows NT, XP y Vista. En los Windows 9X no hará nada.

¿Para que podemos utilizar los eventos? Pues podemos crear uno que le diga a los hilos cuando deben comenzar, detenerse
o esperar cierto tiempo. Hay que procurar que el nombre del evento (lpName) sea original para que no colisione con otro
nombre de otra aplicación. Para averiguar si un evento ha producido un error podemos llamar a la función:
function GetLastError: DWord;

Si devuelve un cero es que todo ha ido bien. Los objetos Event también pueden ser creados sin nombre, enviando como
último parámetro el valor nil.

Para crear un evento voy a crear una variable global en la implementación con el Handle del evento que vamos a crear:
implementation
var
Evento: THandle;

Antes de crear los hilos y ejecutarlos creo el evento señalizado:


procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Evento := CreateEvent(nil, True, True, 'MiEvento');
...
Entonces modifico el procedimiento Execute para que haga cada ciclo si el evento está señalizado y si no es así, que
espere indefinidamente:
procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(ActualizarProgreso);
Sleep(100);
WaitForSingleObject(Evento, INFINITE);
end;
end;

La función WaitForSingleObject espera a que el evento que le pasamos como primer parámetro esté señalizado para
seguir, en caso contrario seguirá esperando indefinidadamente (INFINITE). También podíamos haber hecho que esperara
un segundo:
WaitForSingleObject(Evento, 1000);

Para señalizar o no los eventos he añadido al formulario dos componentes RadioButton:

Para señalizarlo llamo a la función SetEvent:


procedure TFTresHilos.SenalizadoClick(Sender: TObject);
begin
SetEvent(Evento);
end;

Y para no señalizarlo utilizo PulseEvent:


procedure TFTresHilos.NoSenalizadoClick(Sender: TObject);
begin
PulseEvent(Evento);
end;

Tenemos dos funciones para no señalizar un evento:

PulseEvent: no señaliza el evento inmediatamente.

ResetEvent: no señaliza el evento la próxima vez que pase por WaitForSingleObject.

Y lo mejor de todo esto que no sólo podemos activar y desactivar hilos de ejecución dentro de la misma aplicación sino
que además podemos hacerlo entre distintas aplicaciones que se están ejecutando a la vez.

En mi ejemplo he abierto dos instancias de la misma aplicación y no he señalizado en la primera un evento, con lo que se
han detenido las dos:
Esto nos da mucha potencia para sincronizar distintas aplicaciones simultáneamente.

LOS OBJETOS MUTEX

La API de Windows también nos trae otro tipo de objetos llamados Mutex, también conocidos como semáforos binarios
que funcionan de manera similar a los eventos pero que además permite asignarles un dueño. La misión de los objetos
Mutex es evitar que varios hilos accedan a la vez a los mismos recursos, al estilo del procedimiento Synchronize.

Al contrario de los objetos Event donde todos los hilos podían esperar o no al evento en cuestión, con los objetos Mutex
podemos hacer que solo uno de los hilos pueda trabajar a la vez y que los otros se esperen hasta nuevo aviso. Lo veremos
más claro con un ejemplo.

Para crear un objeto Mutex utilizamos esta función:


function CreateMutex(lpMutexAttributes, bInitialOwer, lpName);

Al igual que con los eventos, el parámetro lpMutexAttributes sirve para establecer los atributos de seguridad. El
parámetro bInitialOwer que se encarga de decir si el hilo que llama a CreateMutex es el propietario de este Mutex o por
el contrario si queremos que el primer hilo que espere al Mutex es el dueño del mismo.

En el ejemplo que vimos anteriormente con las tres barras de progreso, supongamos que cada vez que se incrementa la
barra no queremos que las otras barran lo hagan también. Esto puede ser muy útil cuando hay varios hilos que intentan
acceder al mismo recurso (grabar en CD-ROM, enviar señales por un puerto, etc.).

Al contrario del ejemplo de los eventos que me interesaba que se activaran o desactivaran todos a la vez, aquí con los
Mutex me interesa que mientras a un hilo le toca trabajar, los otros hilos tienen que esperarse a que termine (100 ms).

Al igual que hice con el objeto Event voy a crear una variable global con el handle del Mutex:
implementation
var
Mutex: THandle;

Ahora creo el Mutex al pulsar el botón Comenzar:


procedure TFTresHilos.BComenzarClick(Sender: TObject);
begin
Mutex := CreateMutex(nil, True, 'Mutex1');
Progreso1.Position := 0;
Progreso2.Position := 0;
Progreso3.Position := 0;
Hilo1 := THilo.Create(True);
Hilo2 := THilo.Create(True);
Hilo3 := THilo.Create(True);
Hilo1.Progreso := Progreso1;
Hilo2.Progreso := Progreso2;
Hilo3.Progreso := Progreso3;
Hilo1.Resume;
Hilo2.Resume;
Hilo3.Resume;
ReleaseMutex(Mutex);
end;

Para que un hilo no se lance antes que otro cuando ejecuto Resume, lo que hago es que no suelto el Mutex hasta que los
tres hilos están en ejecución (como en una carrera).

Después solo tengo que ir ejecutando cada uno haciendo esperar a los demás hasta que termine:
procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
WaitForSingleObject(Mutex, INFINITE); // captura el mutex (los demás a esperar)
Synchronize(ActualizarProgreso);
Sleep(100);
ReleaseMutex(Mutex); // mutex liberado hasta que lo capture otro hilo
end;
end;

Entre las líneas WaitForSingleObject y ReleaseMutex, lo demás hilos quedan parados hasta que termine. De este modo
creamos un cuello de botella que impide que varios hilos accedan simultáneamente a los mismos recursos.

Al ejecutarlo este es el resultado:

En la foto no se aprecia pero cuando se ve en movimiento vemos que van escalonados, de trozo en trozo en vez de píxel a
píxel.

También podíamos haber creado dos objetos Mutex que hagan que un hilo no comience a ejecutarse hasta que otro hilo le
active su Mutex correspondiente. Las combinaciones que se pueden hacer con Mutex y Event son infinitas.

En el próximo artículo veremos como crear variables de tipo ThreadVar y otros asuntos interesantes.
Pruebas realizadas en RAD Studio 2007.
Publicado por Administrador en 18:54 4 comentarios
Etiquetas: sistema
22 mayo 2009
Los Hilos de Ejecución (2)
En el artículo anterior vimos un sencillo ejemplo de cómo crear un contador utilizando un hilo de ejecución, pero hay un
pequeño inconveniente que tenemos que arreglar.

Dentro de un hilo de ejecución podemos actualizar la pantalla (los formularios y componentes) siempre y cuando la
actualización sea muy esporádica y el trabajo a realizar sea muy rápido (entrar y salir). Pero si durante el evento Execute
de un hilo intentamos dibujar o actualizar los componentes VCL más de lo normal, puede provocar una excepción de este
tipo:

También suele ocurrir si dos hilos ejecutándose en paralelo intentan actualizar el mismo componente VCL.

Esto se debe a que la librería VCL no ha sido diseñada para trabajar con problemas de concurrencia en múltiples hilos de
ejecución.

SINCRONIZANDO CÓDIGO ENTRE HILOS

Para evitar este problema vamos a utilizar el procedimiento Synchronice que permite que un hilo llame a un
procedimiento definido dentro del mismo pero en el contexto del hilo primario, es decir, el hilo principal de nuestro
programa ejecuta el procedimiento que el hilo le manda como parámetro utilizando Synchronice. Es como si el hilo le
dijera al hilo principal: ejecútame esto que a mi me da la risa.

De este modo ya podemos manipular los componentes VCL sin preocupación.

Modificamos la definición de la clase:


type
TContador = class(TThread)
dwTiempo: DWord;
iSegundos: Integer;
Etiqueta: TLabel;
constructor Create; reintroduce; overload;
procedure Execute; override;
procedure ActualizarPantalla;
end;

Y la implementación de ActualizarPantalla es la siguiente:


procedure TContador.ActualizarPantalla;
begin
Etiqueta.Caption := IntToStr(iSegundos);
end;

Entonces sólo hay que sincronizar el hilo con la VCL en el procedimiento Execute:
procedure TContador.Execute;
begin
inherited;
OnTerminate := Terminar;
// Contamos hasta 10 segundos
while (iSegundos < 10) and not Terminated do
begin
// ¿Han pasado 1000 milisegundos?
if GetTickCount - dwTiempo > 1000 then
begin
// Incrementamos el contador de segundos y
// actualizamos la etiqueta
Inc(iSegundos);
Synchronize(ActualizarPantalla);
dwTiempo := GetTickCount;
end;
end;
end;

Llamamos al procedimiento ActualizarPantalla utilizando Synchronize. Y no sólo podemos utilizar este método para
sincronizarnos con los componentes VCL sino que también con variables globales.

Ahora vamos otro caso de dos hilos incrementando paralelamente una misma variable que hace de contador. Tenemos este
formulario:

Tenemos dos listas (TListBox) que nos van a mostrar el valor de la variable global i
que van a incrementar dos hilos a la vez cuya clase es la siguiente:
type
THilo = class(TThread)
Lista: TListBox;
procedure Execute; override;
procedure MostrarContador;
end;

Y esta sería su implementación:


{ THilo }
procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
i := i + 1;
Synchronize(MostrarContador);
Sleep(1000);
end;
end;

El procedimiento Execute incrementa la variable entera global i, la muestra por pantalla en su lista correspondiente y
espera un segundo. Luego tenemos el procedimiento que muestra el valor de la variable i según el hilo:
procedure THilo.MostrarContador;
begin
Lista.Items.Add('i='+IntToStr(i));
end;
Cuando pulsemos el botón Comenzar, ponemos el contador i a cero, creamos los dos hilos, les asignamos su lista
correspondiente y los ponemos en marcha:
procedure TFDosHilos.BComenzarClick(Sender: TObject);
begin
i := 0;
Hilo1 := THilo.Create(False);
Hilo2 := THilo.Create(False);
Hilo1.Lista := Lista1;
Hilo2.Lista := Lista2;
Hilo1.Resume;
Hilo2.Resume;
end;

En cualquier momento podemos detener la ejecución de ambos hilos pulsando el botón Detener:
procedure TFDosHilos.BDetenerClick(Sender: TObject);
begin
Hilo1.Terminate;
Hilo2.Terminate;
end;

Al ejecutarlo conforme está ahora mismo este sería el resultado:

Aquí hay algo que no encaja. Si los dos hilos incrementan una vez la variable i sólo deberían verse números pares o todo
caso el valor de un hilo debería ser distinto al del otro, pero eso es lo que pasa cuando no van sincronizados los
incrementos de la variable i.

Sin tocar el código fuente, vuelvo a ejecutar el programa y me da este otro resultado:
¿Qué está pasando? Pues que el hilo 2 puede incrementar la variable i justo después de que la incremente el hilo 1 o bien
pueden hacerlo los dos a la vez, provocando un retraso en el contador.

Esto podemos solucionarlo sincronizando el incremento de la variable i dentro de un nuevo procedimiento que podemos
añadir a la clase THilo:
THilo = class(TThread)
Lista: TListBox;
procedure Execute; override;
procedure MostrarContador;
procedure IncrementarContador;
end;

El procedimiento IncrementarContador sólo hace esto:


procedure THilo.IncrementarContador;
begin
i := i + 1;
end;

Luego hacemos que el procedimiento Execute sincronice el incremento de la variable i:


procedure THilo.Execute;
begin
inherited;
FreeOnTerminate := True;
while not Terminated do
begin
Synchronize(IncrementarContador);
Synchronize(MostrarContador);
Sleep(1000);
end;
end;

Este sería el resultado al ejecutarlo:


Vemos ahora que los incrementos de la variable i son de dos en dos y evitamos que un hilo estropee el incremento de otro.
Aunque esto no soluciona a la perfección la sincronización ya que si por ejemplo tenemos otra aplicación (como el
navegador Firefox) que ralentiza el funcionamiento de Windows, podría provocar que se relentice uno de los hilos
respecto al otro y provoque de nuevo la descoordinación en los incrementos. Eso veremos como solucionarlo en el
siguiente artículo mediante semáforos y eventos.

PARAR, REANUDAR, DETENER O ESPERAR LA EJECUCIÓN DEL HILO

Siguiendo con el primer ejemplo (la clase TContador), una vez el hilo está en marcha podemos suspenderlo
(manteniendo su estado) utilizando el método Suspend:
Contador.Suspend;

Para que continúe sólo hay que volver a llamar al procedimiento Resume:
Contador.Resume;

En cualquier momento podemos saber si está suspendido el hilo mediante la propiedad:


if contador.Suspended then
...

Si queremos detener definitivamente la ejecución del hilo entonces deberíamos añadirle al bucle principal de nuestro
procedimiento Execute esta comprobación:
// Contamos hasta 10 segundos
while (iSegundos < 10) and not Terminated do
begin
...

Entonces podíamos añadir al formulario un botón llamado Detener que al pulsarlo haga esto:
Contador.Terminate;

Lo que hace realmente el procedimiento Terminate no es terminar el hilo de ejecución, sino poner la propiedad
Terminated a True para que nosotros salgamos lo antes posible del bucle infinito en el que está sumergido nuestro hilo.
Lo que nunca hay que intentar es hacer esto para terminar un hilo:
Contador.Free;

Lo que es terminar, seguro que termina, pero la explosión la tenemos asegurada. Para cerrar el hilo inmediatamente
podemos llamar a una función de la API de Windows llamada TerminateThread:
TerminateThread(Contador.Handle, 0);

El primer parámetro es el Handle del hilo que queremos detener y el según parámetro es el código de salida que puede ser
leído posteriormente por la función GetExitCodeThread. Recomiendo no utilizar la función TerminateThread a menos
que sea estrictamente necesario. Es mejor utilizar el procedimiento Terminate y procurar salir limpiamente del bucle del
hilo cerrando los asuntos que tengamos a medio (cerrando ficheros abiertos, liberando memoria de objetos creados, etc.).

Por otro lado tenemos el procedimiento DoTerminate que no termina el hilo de ejecución, sino que provoca el evento
OnTerminate en el cual podemos colocar todo lo que necesitamos para terminar nuestro hilo. Por ejemplo, podemos
poner al comienzo de nuestro procedimiento Execute:
procedure TContador.Execute;
begin
inherited;
OnTerminate := Terminar;
...

Y luego definimos dentro de nuestra clase TContador el evento Terminar:


procedure TContador.Terminar(Sender: TObject);
begin
// Escribimos aquí el código que necesitamos
// para liberar los recursos del hilo y luego
// le mandamos una señal para que termine el bucle principal
Terminate;
end;

También podemos hacer que el hilo principal de la aplicación espere a que termine la ejecución de un hilo antes de seguir
ejecutando código. Esto se consigue con el procedimiento WaitFor:
Contador.WaitFor;

Aunque esto puede ser algo peligroso porque no permite que Windows procese los mensajes de nuestra ventana. Podría
utilizarse por ejemplo para ejecutar un programa externo (de MSDOS) y esperar a que termine su ejecución.

En el siguiente artículo veremos como podemos controlar las esperas entre el hilo principal y los hilos secundarios para
que se coordinen en sus trabajos.
Pruebas realizadas en RAD Studio 2007.
Publicado por Administrador en 12:53 2 comentarios
Etiquetas: sistema

08 mayo 2009
Los Hilos de Ejecución (1)
Hace tiempo escribí un pequeño post sobre como crear hilos de ejecución:

Cómo crear un hilo de ejecución

Pero debido a que últimamente los procesadores multinúcleo van tomando más relevancia y que algunos lectores de este
blog han pedido que me extienda en el tema, voy a meterme de lleno en el apasionante y peligroso mundo de las
aplicaciones multihilo.

Cuando manejamos Windows creemos que es un sistema operativo robusto que puede ejecutar gran cantidad de tareas a la
vez. Eso es lo que parece externamente, pero realmente sólo tiene dos hilos de ejecución en los últimos procesadores de
doble núcleo. Y en procesadores Pentium 4 e inferiores sólo contiene un solo hilo de ejecución (dos mediante el
disimulado HyperThreading que tantos problemas nos dio con Interbase 6).
Entonces, ¿Cómo puede ejecutar varias tareas a la vez? (Emule, Bittorrent, MSN, reproductor multimedia, etc.). Primero
tenemos que ver lo que significa un proceso.

LOS PROCESOS

Un proceso (un programa EXE o un servicio de Windows) contiene como mínimo un hilo, llamado hilo primario. Si
queremos que nuestro proceso tenga más hilos, se lo tenemos que pedir al sistema operativo. Estos nuevos hilos pueden
ejecutarse paralelamente al hilo primario y ser independientes.

Un proceso se compone principalmente de estas tres áreas:

AREA DE CÓDIGO: Es una zona de memoria de sólo lectura donde está compilado nuestro programa en código objeto
(binario).

HEAP (MONTÓN): Es en esta zona de memoria de lectura/escritura donde se suelen guardar las variables globales que
creamos así como las instancias de los objetos que se van creando en memoria.

PILA: Aquí se guardan temporalmente los parámetros que se pasan entre funciones así como las direcciones de llamada y
retorno de procedimientos y funciones. Todos los datos que se introducen en la pila tienen que salir mediante el método
LIFO (lo último que entra es lo primero que tiene que salir) porque si no provoca un desbordamiento del puntero del
procesador que hace que retorne a otra zona de memoria provocando el error Access Violation 000000 o FFFFFF, es decir,
intenta salirse del segmento de memoria de nuestra aplicación.

Estas tres zonas se encuentran herméticamente separadas las unas de las otras y si por error un comando de nuestro
programa intenta acceder fuera del heap o de la pila provocará el conocido mensaje que tanto nos gusta: Access Violation.
Suele ocurrir al intentar acceder a un objeto que no ha sido creado en el heap (dirección 000000 = nil), etc.

El conjunto de estas tres zonas de memoria es el proceso. Ahora bien, si creamos un nuevo hilo de ejecución dentro del
proceso, éste tendrá su propia pila, aunque compartirá la misma zona de datos (heap).

EL PROCESO MULTITAREA

Windows realiza la multitarea cediendo un pequeño tiempo de procesador a cada proceso, de modo que en un solo
segundo pueden ejecutarse ciertos de procesos simultáneos. Para los viejos roqueros que estudiamos ensamblador vimos
que antes de cambiar de tarea Windows guarda el estado de todos los registros en la pila:
PUSH EAX
PUSH EBX
...

Para luego restaurarlos y seguir su marcha:


...
POP EBX
POP EAX

No está de más adquirir unos pequeños conocimientos de ensamblados de x86 para conocer a fondo las tripas de la
máquina.

Cuando Windows para el control de un proceso, memoriza el estado de los registros del procesador y la pila y pasa al
siguiente proceso hasta que finalice su tiempo. Realmente, este cambio de procesos no lo realiza Windows, sino el mismo
procesador que trae esta característica por hardware (desde el 80386. Hasta el procesador 80286 tenía este modo
multitarea aunque sólo en 16 bits).

Pero lo que vamos a ver es este artículo es un cambio de ejecución entre hilos del mismo proceso, algo que consume
muchos menos recursos que el cambio entre procesos.

Por ejemplo, los navegadores webs modernos actuales como Firefox o Chrome ejecutan cada web dentro del mismo
proceso pero en hilos diferentes (incluso Chrome lo hace en distintos procesos), de modo que si una página web se queda
colgada no afecta a otra situada en la siguiente pestaña (lo que no se es si llamarán al sistema de hilos de Windows y
tendrán su propio núcleo duro de ejecución, su propio sistema multitarea).

Pero programar hilos de ejecución no está exento de problemas. ¿Qué pasaría si dos hilos acceden a la vez a la misma
variable de memoria? O peor aún, ¿Y si intentan escribir a la vez en una unidad de CD-ROM? Las excepciones que
pueden ocurrir pueden ser catastróficas, aunque afortunadamente a partir de Windows NT y XP se controlan muy bien
todos estos problemas de concurrencia.

Lo que si es realmente difícil es intentar depurar dos hilos de ejecución que utilizan los mismos recursos del proceso. De
ahí a que sólo hay que recurrir a los hilos en casos estrictamente necesarios.

LA CLASE TTHREAD

Toda la complejidad de los hilos de ejecución que se programan en la API de Windows quedan encapsulados en Delphi en
esta sencilla clase. Para crear un hilo de ejecución basta heredar de la clase TThread y sobrecargar el método Execute:
type
THilo = class(TThread)
procedure Execute; override;
end;

En la implementación del procedimiento Execute es donde hay que introducir el código que queremos ejecutar cuando
arranque nuestro hilo de ejecución:
{ THilo }
procedure THilo.Execute;
begin
inherited;
end;

Veamos un ejemplo sencillo de un hilo de ejecución con un contador de 10 segundos.

CREANDO UN CONTADOR DE SEGUNDOS

Voy a crear un nuevo proyecto con un formulario en el que sólo va a haber una etiqueta (TLabel) llamada EContador con
el contador de segundos:

A este formulario lo he llamado FPrincipal. En la interfaz del mismo voy a definir la clase TContador que va a heredar
de un hilo TThread:
type
TContador = class(TThread)
dwTiempo: DWord;
iSegundos: Integer;
Etiqueta: TLabel;
constructor Create; reintroduce; overload;
procedure Execute; override;
end;

He sobrecargado el constructor para que inicialice mis contadores de tiempo y segundos:


constructor TContador.Create;
begin
inherited Create(True); // llamamos al constructor del padre (TThread)
dwTiempo := GetTickCount;
iSegundos := 0;
end;

La función GetTickCount nos devuelve un número entero que representa el número de milisegundos que han pasado
desde que encendimos nuestro PC. Si queréis más precisión podéis utilizar la función TimeGetTime declarada en la
unidad MMSystem (sobre todo si vais a programar videojuegos).

Siguiendo con la implementación de nuestra clase TContador, la variable dwTiempo la voy a utilizar para controlar el
número de milisegundos que van pasando desde que ejecutamos el hilo. Y la variable iSegundos es un contador de
segundos de 0 a 10. También he añadido una referencia a una Etiqueta de tipo TLabel que se la daremos al crear una
instancia del hilo en el evento OnCreate del formulario principal:
procedure TFPrincipal.FormCreate(Sender: TObject);
begin
Contador := TContador.Create(True);
Contador.Etiqueta := EContador;
Contador.FreeOnTerminate := True;
Contador.Resume;
end;

Después de crear el hilo le pasamos la etiqueta que tiene que actualizar y le decimos mediante la propiedad
FreeOnTerminate que se libere de memoria automáticamente al terminar la ejecución del hilo.

Después llamamos al método Resume que lo que hace internamente es ejecutar el procedimiento Execute de la clase
TThread que tendrá este código:
procedure TContador.Execute;
begin
inherited;
// Contamos hasta 10 segundos
while iSegundos < 10 do
// ¿Han pasado 1000 milisegundos?
if GetTickCount - dwTiempo > 1000 then
begin
// Incrementamos el contador de segundos y actualizamos la etiqueta
Inc(iSegundos);
Etiqueta.Caption := IntToStr(iSegundos);
dwTiempo := GetTickCount;
end;
end;

Creamos un bucle cerrado que va contando de 1000 en 1000 milisegundos e incrementando el contador de segundos.
Cuando se salga de este bucle se terminará la ejecución del hilo automáticamente. También se podía haber utilizado
procedimiento Sleep, pero nunca me ha gustado mucho esta función porque miestras se está ejecutando no podemos hacer
absolutamente nada.

Eso ocurrirá al ejecutar el programa y cuando llegue el contador a 10:

Podemos ver como va la ejecución del hilo en la ventana inferior de Delphi si estamos ejecutando el programa en modo
depuración:
Como puede verse en las líneas en rojo, el hilo que hemos creado tiene el ID 3736 que no tiene nada que ver con el hilo
primario:

En el siguiente artículo seguiremos profundizando un poco más en los hilos de ejecución a través de otros ejemplos.
Pruebas realizadas en RAD Studio 2007.
Publicado por Administrador en 10:00 9 comentarios
Etiquetas: sistema

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

Veamos que tenemos Delphi para estos menesteres:

function CreateDir( const Dir: string ): Boolean;

Esta función crea un nuevo directorio en la ruta indicada por Dir. Devuelve True o False dependiendo si ha podido
crearlo o no. El único inconveniente que tiene esta función es que deben existir los directorios padres. Por ejemplo:
CreateDir( 'C:\prueba' ) devuelve True
CreateDir( 'C:\prueba\documentos' ) devuelve True
CreateDir( 'C:\otraprueba\documentos' ) devuelve False (y no lo crea)

function ForceDirectories( Dir: string ): Boolean;

Esta función es similar a CreateDir salvo que también crea toda la ruta de directorios padres.
ForceDirectories( 'C:\prueba' ) devuelve True
ForceDirectories( 'C:\prueba\documentos' ) devuelve True
ForceDirectories( 'C:\otraprueba\documentos' ) devuelve True

procedure ChDir( const S: string ); overload;

Este procedimiento cambia el directorio actual al indicado por el parámetro S. Por ejemplo:
ChDir( 'C:\Windows\Fonts' );

function GetCurrentDir: string;

Nos devuelve el nombre del directorio actual donde estamos posicionados. Por ejemplo:
GetCurrentDir devuelve C:\Windows\Fonts

function SetCurrentDir( const Dir: string ): Boolean;


Establece el directorio actual devolviendo True si lo ha conseguido. Por ejemplo:
SetCurrentDir( 'C:\Windows\Java' );

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

Devuelve el directorio actual de una unidad y lo mete en la variable S. El parámetro D es el número de la unidad siendo:
D Unidad
--- ------------------
0 Unidad por defecto
1 A:
2 B:
3 C:
...

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


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

function RemoveDir( const Dir: string ): Boolean;

Elimina un directorio en el caso de que este vacío, devolviendo False si no ha podido hacerlo.
RemoveDir( 'C:\prueba\documentos' ) devuelve True
RemoveDir( 'C:\prueba' ) devuelve True
RemoveDir( 'C:\otraprueba' ) devuelve False porque no esta vacío

function DirectoryExists( const Directory: string ): Boolean;

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


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

function DiskFree( Drive: Byte ): Int64;

Devuelve el número de bytes libres de una unidad de dico indicada por la letra Drive:
Drive Unidad
----------- ------
0 Unidad por defecto
1 A:
2 B:
3 C:
...

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


DiskFree( 3 ) devuelve 5579714560

function DiskSize( Drive: Byte ): Int64;

Nos dice el tamaño total en bytes de una unidad de disco. Por ejemplo:
DiskSize( 3 ) devuelve 20974428160
BUSCANDO ARCHIVOS DENTRO DE UN DIRECTORIO

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

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

Busca el primer archivo, directorio o unidad que se encuentre dentro de una ruta en concreto. Devuelve un cero si ha
encontrado algo. El parámetro TSearchRec es una estructura de datos donde se almacena lo encontrado:
type
TSearchRec = record
Time: Integer;
Size: Integer;
Attr: Integer;
Name: TFileName;
ExcludeAttr: Integer;
FindHandle: THandle;
FindData: TWin32FindData;
end;

function FindNext( var F: TSearchRec ): Integer;

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

procedure FindClose( var F: TSearchRec );

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

Veamos un ejemplo donde se utilizan estas funciones. Vamos a hacer un procedimiento que lista sólo los archivos de un
directorio que le pasemos y vuelca su contenido en un StringList:
procedure TFPrincipal.Listar( sDirectorio: string; var Resultado: TStringList );
var
Busqueda: TSearchRec;
iResultado: Integer;
begin
// Nos aseguramos que termine en contrabarra
sDirectorio := IncludeTrailingBackslash( sDirectorio );
iResultado := FindFirst( sDirectorio + '*.*', faAnyFile, Busqueda );
while iResultado = 0 do
begin
// ¿Ha encontrado un archivo y no es un directorio?
if ( Busqueda.Attr and faArchive = faArchive ) and
( Busqueda.Attr and faDirectory <> faDirectory ) then
Resultado.Add( Busqueda.Name );
iResultado := FindNext( Busqueda );
end;
FindClose( Busqueda );
end;

Si listamos el raiz de la unidad C:


var
Directorio: TStringList;
begin
Directorio := TStringList.Create;
Listar( 'C:', Directorio );
ShowMessage( Directorio.Text );
Directorio.Free;
end;
El resultado sería:
AUTOEXEC.BAT
Bootfont.bin
CONFIG.SYS
INSTALL.LOG
IO.SYS
MSDOS.SYS
NTDETECT.COM

Con estas tres funciones se pueden hacer cosas tan importantes como eliminar directorios, realizar búsquedas de archivos,
calcular lo que ocupa un directorio en bytes, etc.
Pruebas realizadas en Delphi 7.
Publicado por Administrador en 12:24 0 comentarios
Etiquetas: lenguaje, sistema

03 septiembre 2007
Trabajando con archivos de texto y binarios (y V)
Vamos a terminar de ver los recursos de los que dispone Delphi para el tratamiento de archivos.

PARTIENDO UN ARCHIVO EN DOS

En este ejemplo vamos a coger el archivo prueba.dat de 100 Kb y dejamos sólo las primeras 30 Kb eliminando el resto:
procedure TFormulario.PartirArchivo;
var F: File of byte;
Buffer: array[0..1023] of Byte;
i: Integer;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.dat' );
Reset( F );
// Leemos 30 Kb utilizando un buffer de 1 Kb
for i := 1 to 30 do
BlockRead( F, Buffer, 1024 );

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

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

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

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

Para evitar esto disponemos de la función Flush:

function Flush( var t: TextFile ): Integer;

Esta función lo que hace es vaciar el buffer del archivo de texto en el disco duro. Es algo así como ejecutar CloseFile pero
sin cerrar el archivo. Por ejemplo:
var
F: TextFile;
sArchivo: String;
begin
sArchivo := ExtractFilePath( Application.ExeName ) + 'prueba.txt';
AssignFile( F, sArchivo );
Rewrite( F );
WriteLn( F, 'Esto es el contenido del archivo de texto.' );
Flush( F );
// aquí podemos seguir escribiendo en el mismo
CloseFile( F );
end;

GUARDANDO OTROS TIPOS DE DATOS EN LOS ARCHIVOS

Hasta ahora sólo hemos guardado información de tipo texto y binaria en archivos pero se puede guardar cualquier tipo de
información utilizando cualquier tipo de dato. Por ejemplo para guardar una serie de números reales se haría de la
siguiente manera:
procedure TFormulario.GuardarRecibos;
var
F: File of Real;
r: Real;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'ImporteRecibos.dat' );
Rewrite( F );
r := 120.45;
Write( F, r );
r := 1800.05;
Write( F, r );
r := 66.31;
Write( F, r );
CloseFile( F );
end;

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

También se pueden guardar estructuras de datos almacenadas en registros. En este ejemplo que voy a mostrar vamos a ver
como guardar los datos de un cliente en un archivo:
type
TCliente = record
ID: Integer;
sNombre: String[50];
rSaldo: Real;
bPagado: Boolean;
end;
procedure TFPrincipal.CrearArchivoRegistro;
var
Cliente: TCliente;
F: File of TCliente;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'clientes.dat' );
Rewrite( F );
with Cliente do
begin
ID := 1;
sNombre := 'FRANCISCO MARTINEZ LÓPEZ';
rSaldo := 1200.54;
bPagado := False;
end;
Write( F, Cliente );
with Cliente do
begin
ID := 2;
sNombre := 'MARIA ROJO PALAZÓN';
rSaldo := 622.32;
bPagado := True;
end;
Write( F, Cliente );
CloseFile( F );
end;

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

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


Pruebas realizadas en Delphi 7.
Publicado por Administrador en 13:18 0 comentarios
Etiquetas: lenguaje, sistema

31 agosto 2007
Trabajando con archivos de texto y binarios (IV)
En el artículo anterior vimos como movernos por un archivo binario utilizando la propiedad Position de la clase
TFileStream. Ahora vamos a ver lo mismo utilizando AssignFile.

RECORRIENDO UN ARCHIVO BINARIO EN MODO LECTURA

Si utilizamos AssignFile para leer un archivo en vez de TFileStream podemos mover el puntero en cualquier dirección
utilizando el procedimiento Seek:

procedure Seek( var F; N: Longint );

Este procedimiento toma como primer parámetro el puntero al archivo (F: File of Byte) y como segundo parámetro la
posición donde queremos movernos, siendo cero la primera posición. Para irse al final del archivo se hace:
Seek( F, FileSize(F) )

Vamos a abrir el archivo prueba.dat de 100 KB creado anteriormente y nos vamos a ir a la posición 50 para leer 10 bytes
volcando la información en un campo memo en formato hexadecimal:
var F: file of byte;
i: Integer;
Buffer: array[0..9] of Byte;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.dat' );
Reset( F );
Seek( F, 50 );
BlockRead( F, Buffer, 10 );
for i := 0 to 9 do
Memo.Text := Memo.Text + IntToHex( Buffer[i], 2 ) + ' ';
CloseFile( F );
end;

El resultado sería:
32 33 34 35 36 37 38 39 3A 3B

El único inconveniente que tiene la función Seek es que sólo funciona en modo lectura (Reset). No se puede utilizar en
modo escritura (Rewrite) para irse al final del archivo y seguir añadiendo datos.

Si no sabemos donde estamos se puede utilizar la función FilePos para averiguarlo:


ShowMessage( 'posición: ' + IntToStr( FilePos( F ) ) );
LEYENDO LAS PROPIEDADES DE UN ARCHIVO

Veamos de que funciones dispone Delphi para leer los atributos de un archivo (fecha, modo de acceso. etc.):

function FileAge( const FileName: string ): Integer;

Esta función devuelve la fecha y hora de última modificación de un archivo en formato TTimeStamp. Para pasar de
formato TTimeStamp a formato TDateTime utilizamos la función FileDateToDateTime. Por ejemplo:
FileDateToDateTime( FileAge( 'prueba.txt' ) ) -> devuelve la fecha y hora de modificación del
archivo prueba.txt

También disponemos de una función para obtener los atributos de un archivo:

function FileGetAttr( const FileName: string ): Integer;

Devuelve un valor entero conteniendo los posibles atributos de un archivo (puede tener varios a la vez). Por ejemplo, para
averiguar si el archivo esta oculto se haría lo siguiente:
var
iAtributos: Integer;
begin
if ( iAtributos and faHidden <> 0 ) and ( iAtributos and faArchive <> 0 ) then
...
end;

Esta función no sólo comprueba archivos, sino también directorios y unidades de disco. Todos los posibles valores se
encuentran en binario activando el bit correspondiente. Los valores posibles son:
Constante Valor Tipo de archivo
-------------------------------------------------------------------------------------------
faReadOnly 1 Sólo lectura
faHidden 2 Oculto
faSysFile 4 Archivo del sistema
faVolumeID 8 Unidad de disco
faDirectory 16 Directorio
faArchive 32 Archivo
faSymLink 64 Enlace simbólico
faAnyFile 71 Cualquier archivo

Otra función interesante es FileSize la cual devuelve en bytes el tamaño de un archivo. El único inconveniente es que hay
que abrir el archivo para averiguarlo:
var
F: File of byte;
begin
AssignFile( F, 'c:\prueba.txt' );
Reset( F );
ShowMessage( IntToStr( FileSize( F ) ) + ' bytes' );
CloseFile( F );
end;

MODIFICANDO LAS PROPIEDADES DE UN ARCHIVO

Las función para modificar las propiedad de un archivo es:

function FileSetAttr( const FileName: string; Attr: Integer ): Integer;

Si se pudo modificar el atributo del archivo devuelve un 0 o en caso de error el código del mismo. Por ejemplo para hacer
un archivo de sólo lectura y oculto:
FileSetAttr( 'c:\prueba.txt', faReadOnly or faHidden );

En el próximo artículo terminaremos de ver las funciones más importates para el manejo de archivos.
Pruebas realizadas en Delphi 7.

Trabajando con archivos de texto y binarios (III)


Vamos a ver las distintas maneras que tenemos de manejar archivos binarios.

CREANDO UN ARCHIVO BINARIO

El crear un archivo binario utilizando AssignFile no es muy diferente de crear un archivo de texto:
procedure TFPrincipal.CrearArchivoBinario;
var
F: File of byte;
i: Integer;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.dat' );
Rewrite( F );
for i := 1 to 10 do
Write( F, i );
CloseFile( F );
end;

Como se puede apreciar, la variable F representa un puntero a un tipo de archivo de bytes. Después utilizamos la función
Write para ir guardando byte a byte dentro del archivo.

Cuando los archivos binarios son pequeños no hay problema pero cuando son grandes la lentitud puede ser insoportable
(parece como si se hubiera colgado el programa). Cuando ocurre esto entonces hay que crear un buffer temporal para ir
guardando los bytes en bloques de 1 Kb, 10 Kb, 20 Kb, etc. Hoy en día todo lo que entra y sale de un disco duro para por
la memoria caché del mismo (a parte de la memoria virtual de Windows).

Vamos a ver un ejemplo de como crear un archivo binario grande (100 Kb) utilizando un buffer. Vamos a guardar en el
archivo bytes consecutivos (0, 1, 2, .. 255, 0, 1, 2, ...):
procedure TFPrincipal.CrearArchivoBinarioConBuffer;
var
F: File of byte;
i, j: Integer;
b: Byte;
Buffer: array[1..1024] of byte;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.dat' );
Rewrite( F );
b := 0;
// Guardamos 100 veces el buffer de 1 KB (100 KB)
for j := 1 to 100 do
begin
for i := 1 to 1024 do
begin
Buffer[i] := b;
Inc( b );
end;
BlockWrite( F, Buffer, 1024 );
end;
CloseFile( F );
end;

Hemos creado un buffer de 1024 bytes para guardar la información mediante el procedimiento BlockWrite que toma
como primer parámetro el puntero F, como segundo parámetro el buffer del cual va a guardar la información y como
tercer parámetro la longitud del buffer. Cuando más grande sea nuestro buffer menos accesos a disco se necesita y más
suelto va nuestro programa.
Ahora veremos como hacer lo mismo utilizando la clase TFileStream.

CREANDO UN ARCHIVO BINARIO CON LA CLASE TFILESTREAM

El parecido con la rutina anterior es casi idéntico salvo que hemos sustituido un fichero de tipo File of byte por la clase
TFileStream. El método Write toma como parámetros el buffer y su longitud:
procedure TFPrincipal.CrearStreamBinario;
var F: TFileStream;
Buffer: array[0..1023] of byte;
i, j: Integer;
b: Byte;
begin
F := TFileStream.Create( ExtractFilePath( Application.ExeName ) + 'prueba.dat', fmCreate );
b := 0;
// Guardamos 100 veces el buffer de 1 KB (100 KB)
for j := 1 to 100 do
begin
for i := 0 to 1023 do
begin
Buffer[i] := b;
Inc( b );
end;
F.Write( Buffer, 1024 );
end;
F.Free;
end;

MODIFICANDO UN ARCHIVO BINARIO

En un archivo que no sea de tipo TextFile no se puede utilizar el procedimiento Append para añadir datos al final del
mismo. Usando AssignFile se pueden utilizar dos métodos:

- Leer la información de todo el archivo en un buffer, añadir información al final del mismo y posteriormente guardarlo
todo sobrescribiendo el archivo con Rewrite.

- Otro método sería crear una copia del archivo y añadirle datos al final del mismo. Luego habría que borrar el original y
sustituirlo por este nuevo.

Ambos métodos no los voy a mostrar ya que sería algo primitivo en los tiempos que estamos. Para ello nada mejor que la
clase TFileStream para tratamiento de archivos binarios.

MODIFICANDO UN ARCHIVO BINARIO UTILIZANDO LA CLASE TFILESTREAM

Añadir datos a un archivo binario utilizando la clase TFileStream es tan fácil como irse al final del archivo y ponerse a
escribir. De hecho sólo hemos añadido una línea de código al archivo anterior y hemos cambiado el método de apertura:
procedure TFPrincipal.AnadirStreamBinario;
var F: TFileStream;
Buffer: array[0..1023] of byte;
i, j: Integer;
b: Byte;
begin
F := TFileStream.Create( ExtractFilePath( Application.ExeName ) + 'prueba.dat', fmOpenWrite );
F.Position := F.Size;
b := 0;
// Guardamos 100 veces el buffer de 1 KB (100 KB)
for j := 1 to 100 do
begin
for i := 0 to 1023 do
begin
Buffer[i] := b;
Inc( b );
end;
F.Write( Buffer, 1024 );
end;
F.Free;
end;

LEYENDO UN ARCHIVO BINARIO

Para la lectura de un archivo binario también utilizaremos un buffer para acelerar el proceso:
procedure TFPrincipal.CargarStreamBinario;
var F: TFileStream;
Buffer: array[0..1023] of byte;
begin
F := TFileStream.Create( ExtractFilePath( Application.ExeName ) + 'prueba.dat', fmOpenRead );
// ¿No ha llegado al final de archivo?
while F.Position < F.Size do
begin
// Leemos un bloque de 1024 bytes
F.Read( Buffer, 1024 );
// ya tenemos un bloque de información en el buffer
// podemos hacer lo que queramos con el antes de cargar el siguiente bloque
end;
F.Free;
end;

¿Cómo sabemos cuando se termina el archivo? Lo sabemos por la variable Position que se va moviendo automáticamente
a través del método Read. Cuando llegue al final (F.Size) cerramos el archivo.

Con esto llegamos a la conclusión de que para el manejo de archivos de texto lo ideal es utilizar AssignFile, StringList o
campos Memo, pero para archivos binarios TFileStream cumple mejor su función.

En el próximo artículo seguiremos viendo más cosas sobre el tratamiento de archivos.


Pruebas realizadas en Delphi 7.
Publicado por Administrador en 13:06 1 comentarios
Etiquetas: lenguaje, sistema

29 agosto 2007
Trabajando con archivos de texto y binarios (II)
Vamos a ver otra manera de manejar ficheros de texto utilizando algunas clases que lleva Delphi para hacer nuestra labor
mucho más fácil.

Para ello utilizaremos la clase TFileStream que hereda de la clase TStream para manejar flujos de datos, en este caso
archivos. Una de las ventanas de utilizar FileStream en vez de los clásicos métodos de pascal tales como Rewrite,
WriteLn, etc. es que controla automáticamente los buffer en disco según el tamaño de los mismos en Windows.

CREANDO UN ARCHIVO DE TEXTO USANDO LA CLASE TFILESTREAM

La clase TFileStream proporciona un método rápido y flexible para el manejo de archivos. Veamos como crear un
archivo detexto:
procedure TFormulario.CrearArchivoStream;
var
F: TFileStream;
s: String;
begin
F := TFileStream.Create( ExtractFilePath( Application.ExeName ) + 'prueba.txt', fmCreate );
s := 'Añadiendo información al archivo de texto.' + #13 + #10;
F.Write( s[1], Length( s ) );
F.Free;
end;
El constructor Create de la clase TFileStream toma como primer parámetro la ruta del archivo y como segundo
parámetro el tipo de acceso. Los tipos de acceso son:
fmCreate -> Crea un nuevo archivo. Si el archivo ya existe lo sobrescribe.
fmOpenRead -> Abre el archivo en modo de solo lectura.
fmOpenWrite -> Abre el archivo en modo de escritura.
fmOpenReadWrite -> Abre el archivo en modo lectura/escritura.

Después se graba la información mediante el método Write el cual lee el contenido de un buffer (puede ser texto o
binario) pasando como segundo parámetro su longitud (Length). Finalmente liberamos la clase con Free y el mismo
objeto cierra automáticamente el archivo.

Le hemos pasado como primer parámetro s[1] porque es la dirección de memoria donde comienza la variable s (los string
empiezan por 1). Al final de la cadena le hemos metido cambién los caracteres de retorno de carro para que pase a la
siguiente línea, ya que un objeto FileStream lo trata todo como texto continuo.

Cuando la cantidad de información a mover es muy grande, éste metodo es mucho más rápido que utilizar el clasico
Rewrite, WriteLn, etc. (a menos claro que creemos nuestro buffer).

AÑADIENDO TEXTO A UN ARCHIVO CON FILESTREAM

Para añadir líneas a nuestro archivo creado anteriormente abrimos el archivo en modo fmOpenWrite, nos vamos al final
del archivo utilizando la propiedad Position y añadimos el texto:
procedure TFPrincipal.AnadirArchivoStream;
var
F: TFileStream;
s: String;
begin
F := TFileStream.Create( ExtractFilePath( Application.ExeName ) + 'prueba.txt', fmOpenWrite );
F.Position := F.Size;
s := 'Añadiendo información al archivo de texto.' + #13 + #10;
F.Write( s[1], Length( s ) );
F.Free;
end;

Si no se hubiese utilizado la propiedad Position habría machadado la información introducida anteriormente. Podemos
movernos a nuestro antojo con la propiedad Position para guardar información en un fichero en cualquier sitio. Para irse
al principio de un archivo sólo hay que hacer:
F.Position := 0;

LEYENDO LOS DATOS MEDIANTE FILESTREAM

El siguiente procedimiento lee el contenido del archivo en modo fmOpenRead en la variable s que hace de buffer y
posteriormente lo manda al memo:
procedure TFPrincipal.CargarArchivoStream;
var
F: TFileStream;
s: String;
begin
F := TFileStream.Create( ExtractFilePath( Application.ExeName ) + 'prueba.txt', fmOpenRead );
SetLength( s, F.Size ); // *** Me faltaba esto, gracias por la corrección ***
F.Read( s[1], F.Size );
Memo.Text := s;
F.Free;
end;

Aunque en este caso no tenemos la flexibilidad que nos aportaban las funciones ReadLn y WriteLn para archivos de
texto.
BLOQUEANDO EL ARCHIVO A OTROS USUARIOS

Una de las cualidades de las que goza el objeto FileStream es la de bloquear el acceso a otros usuarios mientras estamos
trabajando con un archivo. La protección se realiza cuando se abre el archivo. Por ejemplo, si queremos abrir el archivo en
modo lectura exclusivamente para nosotros hacemos:
F := TFileStream.Create( ExtractFilePath( Application.ExeName ) + 'prueba.txt', fmReadOnly or
fmShareExclusive );

Mediante el operador OR mezclamos las opciones de apertura con las opciones de bloqueo. Las posibles opciones de
bloqueo son:
fmShareCompat -> Permite compatibilizar la apertura con otro usuario o programa
fmShareExclusive -> Sólo nosotros tenemos derecho acceso de lectura/escritura mientras este
abierto
fmShareDenyWrite -> Bloqueamos el archivo para que nadie pueda escribir salvo nosotros
fmShareDenyRead -> Bloqueamos el archivo para que nadie pueda leerlo salvo nosotros
fmShareDenyNone -> Permite la lectura/escritura por parte de otros usuarios.

También se podrían manipular archivos de texto facilmente mediante objetos TStringList como vimos con anterioridad.
Yo personalmente utilizo TStringList, objetos Memo o RitchEdit ya que permiten una manipulación del texto en
memoria bastante flexible antes de guardarla en disco.

En el próximo artículo veremos el tratamiento de archivos binarios.


Pruebas realizadas en Delphi 7.
Publicado por Administrador en 16:57 4 comentarios
Etiquetas: lenguaje, sistema

28 agosto 2007
Trabajando con archivos de texto y binarios (I)
Voy a mostrar a continuación las distintas maneras que tenemos para crear, editar o eliminar archivos de texto o binarios.
También aprenderemos a movermos por los directorios y por dentro de los archivos obteniendo la información del sistema
cuando sea necesario.

CREANDO UN ARCHIVO DE TEXTO

Los pasos para crear un archivo son los siguientes:

1º Se crea una variable de tipo TextFile, la cual es un puntero a un archivo de texto.

2º Se utiliza la función AssignFile para asignar el puntero F al archivo de texto.

3º A continuación abrimos el archivo en modo escritura mediante el procedimiento Rewrite.

4º Escrimimos en el archivo mediante la función WriteLn.

5º Cerramos el archivo creado con el procedimiento CloseFile.

Vamos a crear un procedimiento para crear el archivo prueba.txt al lado de nuestro programa:
procedure TFormulario.CrearArchivoTexto;
var
F: TextFile;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.txt' );
Rewrite( F );
WriteLn( F, 'Esto es el contenido del archivo de texto.' );
CloseFile( F );
end;

MODIFICANDO UN ARCHIVO DE TEXTO

Cuando se utiliza la función Rewrite no comprueba si el archivo ya existía anteriormente, lo que puede provocar que
elimine la información anterior. Para prevenir esto lo que hacemos es preguntar si ya existe el archivo y si es así entonces
añadimos al contenido del archivo mediante el procedimiento Append:
procedure TFormulario.AnadirArchivoTexto;
var
F: TextFile;
sArchivo: string;
begin
sArchivo := ExtractFilePath( Application.ExeName ) + 'prueba.txt';
AssignFile( F, sArchivo );
if FileExists( sArchivo ) then
Append( F )
else
Rewrite( F );
WriteLn( F, 'Añadiendo información al archivo de texto.' );
CloseFile( F );
end;

ABRIENDO UN ARCHIVO DE TEXTO

Vamos a abrir el archivo de texto en modo lectura para volcar su contenido en un campo memo del formulario. Para ello
abrimos el archivo con el procedimiento Reset y leemos cada línea de texto mediante ReadLn:
procedure TFormulario.CargarArchivoTexto;
var F: TextFile;
sLinea: String;
begin
AssignFile( F, ExtractFilePath( Application.ExeName ) + 'prueba.txt' );
Reset( F );
while not Eof( F ) do
begin
ReadLn( F, sLinea );
Memo.Lines.Add( sLinea );
end;
CloseFile( F );
end;

El procedimiento ReadLn obliga a leer la línea de texto dentro de una variable. Después incrementa automáticamente el
puntero F hasta la siguiente línea de texto, siendo la función Eof la que nos dice si ha llegado al final del archivo. Aunque
en este caso lo más fácil sería utilizar la propiedad LoadFromFile del objeto memo:
Memo.Lines.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'prueba.txt' );

ELIMINANDO UN ARCHIVO

La eliminación de un archivo se hace con la función DeleteFile la cual está dentro de la unidad SysUtils:
procedure TFormulario.EliminarArchivoTexto;
var sArchivo: String;
begin
sArchivo := ExtractFilePath( Application.ExeName ) + 'prueba.txt';
if FileExists( sArchivo ) then
DeleteFile( sArchivo );
end;

Todas estas funciones (Rewrite, Append, etc.) vienen del lenguaje estándar de Pascal, ya que en Delphi hay maneras
mucho más sencillas de realizar estas tareas, como veremos más adelante.
Pruebas ralizadas en Delphi 7.
Publicado por Administrador en 12:24 14 comentarios
Etiquetas: lenguaje, sistema

27 agosto 2007
Funciones y procedimientos para fecha y hora (y III)
Sigamos con las funciones para el tratamiento de TTime, TDate y TDateTime:

function IsLeapYear( Year: Word ): Boolean;

Esta función nos dice si un año es bisiesto. Por ejemplo:


IsLeapYear( 2004 ) devuelve True
IsLeapYear( 2006 ) devuelve False
IsLeapYear( 2007 ) devuelve False
IsLeapYear( 2008 ) devuelve True
IsLeapYear( 2009 ) devuelve False

function MonthOfTheYear( const AValue: TDateTime ): Word;

Devuelve el número de mes de un año a partir de un valor TDateTime (evita el tener que utilizar DecodeTime). Por
ejemplo:
MonthOfTheYear( StrToDate( '20/01/2007' ) ) devuelve 1
MonthOfTheYear( StrToDate( '27/08/2007' ) ) devuelve 8
MonthOfTheYear( StrToDate( '31/12/2007' ) ) devuelve 12

function Now: TDateTime;

Esta función dos devuelve la fecha y hora actual del sistema (reloj de Windows). Por ejemplo:
Now devuelve 27/08/2007 10:05:01

function RecodeDate( const AValue: TDateTime; const AYear, AMonth, ADay: Word ): TDateTime;

Esta función modifica la fecha de un valor TDateTime sin afectar a la hora. Por ejemplo:
var
dt: TDateTime;
begin
dt := StrToDateTime( '27/08/2007 15:21:43' );
ShowMessage( 'Fecha y hora antes de modificar: ' + DateTimeToStr( dt ) );
dt := RecodeDate( dt, 2005, 7, 26 );
ShowMessage( 'Fecha y hora después de modificadar: ' + DateTimeToStr( dt ) );
end;

Al ejecutarlo muestra:
Fecha y hora antes de modificar: 27/08/2007 15:21:43
Fecha y hora después de modificar: 26/07/2005 15:21:43

function RecodeTime( const AValue: TDateTime; const AHour, AMinute, ASecond, AMilliSecond: Word ):
TDateTime;

Modifica la hora de un valor TDateTime sin afectar a la fecha. Por ejemplo:


var
dt: TDateTime;
begin
dt := StrToDateTime( '27/08/2007 15:21:43' );
ShowMessage( 'Fecha y hora antes de modificar: ' + DateTimeToStr( dt ) );
dt := RecodeTime( dt, 16, 33, 14, 0 );
ShowMessage( 'Fecha y hora después de modificar: ' + DateTimeToStr( dt ) );
end;

El resultado sería:
Fecha y hora antes de modificar: 27/08/2007 15:21:43
Fecha y hora después de modificar: 27/08/2007 16:33:14

procedure ReplaceDate( var DateTime: TDateTime; const NewDate: TDateTime );

Este procedimiento modifica la fecha de una variable TDateTime sin afectar a la hora. Por ejemplo:
var
dt: TDateTime;
begin
dt := StrToDateTime( '27/08/2007 18:15:31' );
ShowMessage( 'Fecha y hora antes de modificar: ' + DateTimeToStr( dt ) );
ReplaceDate( dt, StrToDate( '01/09/2007' ) );
ShowMessage( 'Fecha y hora después de modificar: ' + DateTimeToStr( dt ) );
end;

El resultado sería:
Fecha y hora antes de modificar: 27/08/2007 18:15:31
Fecha y hora después de modificar: 01/09/2007 18:15:31

procedure ReplaceTime( var DateTime: TDateTime; const NewTime: TDateTime );

Modifica la hora de una variable TDateTime sin afectar a la fecha. Por ejemplo:
var
dt: TDateTime;
begin
dt := StrToDateTime( '27/08/2007 19:33:22' );
ShowMessage( 'Fecha y hora antes de modificar: ' + DateTimeToStr( dt ) );
ReplaceTime( dt, StrToTime( '14:21:05' ) );
ShowMessage( 'Fecha y hora después de modificar: ' + DateTimeToStr( dt ) );
end;

El resultado sería:
Fecha y hora antes de modificar: 27/08/2007 19:33:22
Fecha y hora después de modificar: 27/08/2007 14:21:05

function Time: TDateTime;


function GetTime: TDateTime;

Estas dos funciones devuelven la hora actual en formato TDateTime. Por ejemplo:
ShowMessage( 'Time devuelve ' + TimeToStr( Time ) );
ShowMessage( 'GetTime devuelve ' + TimeToStr( GetTime ) );
ShowMessage( 'Time devuelve ' + DateTimeToStr( Time ) );
ShowMessage( 'GetTime devuelve ' + DateTimeToStr( GetTime ) );
Al ejecutarlo muestra:
Time devuelve 10:36:10
GetTime devuelve 10:36:10
Time devuelve 30/12/1899 10:36:10
GetTime devuelve 30/12/1899 10:36:10

Si os fijais bien cuando mostramos la hora en formato TDateTime vemos que la fecha esta nula (30/12/1899). Mucho
cuidado con eso si no quereis que el usuario se quede con cara de flipado, sobre todo al guardar o cargar el valor de un
campo de la base de datos.

function Tomorrow: TDateTime;

Nos devuelve la fecha de mañana. Por ejemplo:


ShowMessage( 'DateToStr( Tomorrow ) devuelve ' + DateToStr( Tomorrow ) );
ShowMessage( 'DateToStr( Tomorrow ) devuelve ' + DateTimeToStr( Tomorrow ) );
ShowMessage( 'DateToStr( Tomorrow ) devuelve ' + TimeToStr( Tomorrow ) );

Nos mostraría:
DateToStr( Tomorrow ) devuelve 28/08/2007
DateToStr( Tomorrow ) devuelve 28/08/2007
DateToStr( Tomorrow ) devuelve 0:00:00

Como vemos en el ejemplo sólo nos da el día de mañana, pero no la hora (0:00:00).

function Yesterday: TDateTime;

Nos devuelve el día de ayer en formato TDateTime. Por ejemplo:


DateToStr( Yesterday ) devuelve 26/08/2007

Las siguientes funciones ya las hemos visto anteriormente:

function DateToStr( Date: TDateTime ): string; overload;


function TimeToStr( Time: TDateTime ): string; overload;
function DateTimeToStr( DateTime: TDateTime ): string; overload;
function StrToDate( const S: string ): TDateTime; overload;
function StrToTime( const S: string ): TDateTime; overload;
function StrToDateTime( const S: string ): TDateTime; overload;

Pruebas realizadas en Delphi 7.


Publicado por Administrador en 10:59 0 comentarios
Etiquetas: lenguaje, sistema

24 agosto 2007
Funciones y procedimientos para fecha y hora (II)
Vamos a seguir viendo funciones para fecha y hora:

procedure DecodeDateTime( const AValue: TDateTime; out AYear, AMonth, ADay, AHour, AMinute, ASecond,
AMilliSecond: Word );

Este procedimiento decodifica un valor TDateTime en valores enteros para el año, mes, día, hora, minutos, segundos y
milisegundos. Por ejemplo:
var
wAnyo, wMes, wDia: Word;
wHora, wMinutos, wSegundos, wMilisegundos: Word;
begin
DecodeDateTime( StrToDateTime( '24/08/2007 12:34:53' ), wAnyo, wMes, wDia, wHora, wMinutos,
wSegundos, wMilisegundos );
Memo.Lines.Add( 'Día: ' + IntToStr( wDia ) );
Memo.Lines.Add( 'Mes: ' + IntToStr( wMes ) );
Memo.Lines.Add( 'Año: ' + IntToStr( wAnyo ) );
Memo.Lines.Add( IntToStr( wHora ) + ' horas' );
Memo.Lines.Add( IntToStr( wMinutos ) + ' minutos' );
Memo.Lines.Add( IntToStr( wSegundos ) + ' segundos' );
Memo.Lines.Add( IntToStr( wMilisegundos ) + ' milisegundos' );
end;

Al ejecutarlo mostraría:
Día: 24
Mes: 8
Año: 2007
12 horas
34 minutos
53 segundos
0 milisegundos

function EncodeDate( Year, Month, Day: Word ): TDateTime;

Convierte los valores enteros de año, mes y día a un valor TDateTime o TDate. Por ejemplo:
EncodeDate( 2007, 8, 24 ) devuelve 24/08/2007

function EncodeTime( Hour, Min, Sec, MSec: Word ): TDateTime;

Convierte los valores enteros de hora, minutos, segundos y milisegundos en un valor TDateTime o TTime. Por ejemplo:
EncodeTime( 12, 34, 47, 0 ) devuelve 12:34:47

function EncodeDateTime( const AYear, AMonth, ADay, AHour, AMinute, ASecond, AMilliSecond:
Word ):TDateTime;

Convierte los valores enteros de año, mes, día, hora, minutos, segundos y milisegundos en un valor TDateTime. Por
ejemplo:
EncodeDateTime( 2007, 8, 24, 12, 34, 47, 0 ) devuelve 24/08/2007 12:34:47

function EndOfADay( const AYear, ADayOfYear: Word ): TDateTime; overload;


function EndOfADay( const AYear, AMonth, ADay: Word ): TDateTime; overload;

Devuelve un valor TDateTime que representa la última fecha y hora de un día en concreto. Por ejemplo:
EndOfADay( 2007, 8, 13 ) devuelve 13/08/2007 23:59:59
EndOfADay( 2007, 1, 1 ) devuelve 01/01/2007 23:59:59
EndOfADay( 2007, 2, 1 ) devuelve 01/02/2007 23:59:59
EndOfADay( 2007, 8, 23 ) devuelve 08/23/2007 23:59:59
EndOfADay( 2007, 1 ) devuelve 01/01/2007 23:59:59
EndOfADay( 2007, 32 ) devuelve 01/02/2007 23:59:59

NOTA IMPORTANTE: La función con tres parámetros es erronea en la versión de Delphi 7 Build 4.453. Da los
siguientes valores:
EndOfADay( 2007, 8, 13 ) devuelve 12/09/2007 23:59:59 (mal)
EndOfADay( 2007, 1, 1 ) devuelve 31/01/2007 23:59:59 (mal)
EndOfADay( 2007, 2, 1 ) devuelve 28/02/2007 23:59:59 (mal)
EndOfADay( 2007, 8, 23 ) devuelve 12/09/2007 23:59:59 (mal)
EndOfADay( 2007, 1 ) devuelve 01/01/2007 23:59:59 (bien)
EndOfADay( 2007, 32 ) devuelve 01/02/2007 23:59:59 (bien)

Con lo cual es recomendable actualizarse a una versión superior (yo me he actualizado a la versión Delphi 7.0 Build 8.1 y
sigue sin hacerme ni puto caso).

function EndOfAMonth( const AYear, AMonth: Word ): TDateTime;

Devuelve un valor TDateTime que representa la última fecha y hora de un mes. Por ejemplo:
EndOfAMonth( 2007, 1 ) devuelve 31/01/2007 23:59:59
EndOfAMonth( 2007, 2 ) devuelve 28/02/2007 23:59:59
EndOfAMonth( 2007, 8 ) devuelve 31/08/2007 23:59:59

function EndOfAWeek( const AYear, AWeekOfYear: Word; const ADayOfWeek: Word = 7 ): TDateTime;

Devuelve un valor TDateTime que representa la última fecha y hora de una semana. El parámetro AWeekOfYear
representa el número de semana a lo largo del año. Por ejemplo:
EndOfAWeek( 2007, 1 ) devuelve 07/01/2007 23:59:59
EndOfAWeek( 2007, 2 ) devuelve 14/01/2007 23:59:59
EndOfAWeek( 2007, 3 ) devuelve 21/01/2007 23:59:59

function EndOfAYear( const AYear ): TDateTime;

Devuelve un valor TDateTime que representa la última fecha y hora del año que le pasemos. Por ejemplo:
EndOfAYear( 2007 ) devuelve 31/12/2007 23:59:59
EndOfAYear( 2008 ) devuelve 31/12/2008 23:59:59
EndOfAYear( 2009 ) devuelve 31/12/2009 23:59:59

function IncDay( const AValue: TDateTime; const ANumberOfDays: Integer = 1 ): TDateTime;

Devuelve el valor fecha y hora AValue incrementando el número de días especificado en ANumberOfDays (por defecto
1). Por ejemplo:
IncDay( StrToDate( '24/08/2007' ) ) devuelve 25/08/2007
IncDay( StrToDate( '24/08/2007' ), 10 ) devuelve 03/09/2007

function IncMonth( const Date: TDateTime; NumberOfMonths: Integer = 1 ): TDateTime;

Devuelve el valor fecha y hora AValue incrementando el número de meses especificado en NumberOfMonths (por
defecto 1). Por ejemplo:
IncMonth( StrToDate( '24/08/2007' ) ) devuelve 24/09/2007
IncMonth( StrToDate( '24/08/2007' ), 3 ) devuelve 24/11/2007

procedure IncAMonth( var Year, Month, Day: Word; NumberOfMonths: Integer = 1 );

Este procedimiento es similar a la función IncMonth pero en vez de pasarle la fecha en formato TDateTime se le pasa en
formato año, mes y día. Por ejemplo:
var
wAnyo, wMes, wDia: Word;
begin
wDia := 24;
wMes := 8;
wAnyo := 2007;
IncAMonth( wAnyo, wMes, wDia );
Memo.Lines.Add( 'Día: ' + IntToStr( wDia ) );
Memo.Lines.Add( 'Mes: ' + IntToStr( wMes ) );
Memo.Lines.Add( 'Año: ' + IntToStr( wAnyo ) );
end;

Al ejecutarlo obtenemos:
Día: 24
Mes: 9
Año: 2007
Día: 1
Mes: 4
Año: 2007

function IncWeek( const AValue: TDateTime; const ANumberOfWeeks: Integer = 1 ): TDateTime;

Devuelve el valor fecha y hora AValue incrementando el número de semanas especificado en ANumberOfWeeks (por
defecto 1). Por ejemplo:
IncWeek( StrToDate( '24/08/2007' ) ) devuelve 31/08/2007
IncWeek( StrToDate( '24/08/2007' ), 2 ) devuelve 07/09/2007

function IncYear( const AValue: TDateTime; const ANumberOfYears: Integer = 1 ): TDateTime;

Devuelve el valor fecha y hora AValue incrementando el número de años especificado en ANumberOfYears (por defecto
1). Por ejemplo:
IncYear( StrToDate( '24/08/2007' ) ) devuelve 24/08/2008
IncYear( StrToDate( '24/08/2007' ), 2 ) devuelve 24/08/2009

En el próximo artículo veremos más funciones interesantes.


Pruebas realizadas en Delphi 7.
Publicado por Administrador en 11:56 0 comentarios
Etiquetas: lenguaje, sistema

23 agosto 2007
Funciones y procedimientos para fecha y hora (I)
Si hay algo de lo que no podemos escapar los programadores de gestión es el tener que lidiar con campos de fecha y hora
tales como cálculo de días entre fechas, averiguar en que fecha caen los días festivos, cronometrar el tiempo que tardan
nuestras rutinas, etc.

Para la mayoría de funciones tenemos que añadir la unidad DateUtils:


uses
Windows, Messages, ..., DateUtils;

Veamos entonces que funciones nos pueden ser útiles para ello:

function DayOfTheMonth( const AValue: TDateTime ): Word;

Esta función extrae el número de día de una fecha en concreto sin tener que utilizar la función DecodeDate. Por ejemplo:
var Fecha: TDate;
begin
Fecha := StrToDate( '23/08/2007' );
ShowMessage( IntToStr( DayOfTheMonth( Fecha ) ); // muestra 23
end;
function DayOfTheWeek( const AValue: TDateTime ): Word;

Nos díce en que día de la semana (1-7) cae una fecha en concreto (el primer día es el Lunes, al contrario de otras
funciones de fecha y hora donde el primer día es el Domingo). Por ejemplo:
Fecha := StrToDate( '5/08/2007' );
ShowMessage( IntToStr( DayOfTheWeek( Fecha ) ) ); // devuelve 7 (Domingo)
Fecha := StrToDate( '10/08/2007' );
ShowMessage( IntToStr( DayOfTheWeek( Fecha ) ) ); // devuelve 5 (Viernes)
Fecha := StrToDate( '23/08/2007' );
ShowMessage( IntToStr( DayOfTheWeek( Fecha ) ) ); // devuelve 4 (Jueves)

function DayOfWeek( Date: TDateTime ): Integer;

Esta función es igual a la función DayOfTheWeek con la diferencia de que el primer día de la semena es el Domingo. Por
ejemplo:
Fecha := StrToDate( '5/08/2007' );
ShowMessage( IntToStr( DayOfWeek( Fecha ) ) ); // devuelve 1 (Domingo)
Fecha := StrToDate( '10/08/2007' );
ShowMessage( IntToStr( DayOfWeek( Fecha ) ) ); // devuelve 6 (Viernes)
Fecha := StrToDate( '23/08/2007' );
ShowMessage( IntToStr( DayOfWeek( Fecha ) ) ); // devuelve 5 (Jueves)

function DayOfTheYear( const AValue: TDateTime ): Word;

Al pasarle una fecha nos devuelve el número de día que corresponde a lo largo del año. Por ejemplo:
Fecha := StrToDate( '5/08/2007' );
ShowMessage( IntToStr( DayOfTheYear( Fecha ) ) ); // devuelve 217
Fecha := StrToDate( '10/08/2007' );
ShowMessage( IntToStr( DayOfTheYear( Fecha ) ) ); // devuelve 222
Fecha := StrToDate( '23/08/2007' );
ShowMessage( IntToStr( DayOfTheYear( Fecha ) ) ); // devuelve 235

function DaysBetween( const ANow, AThen: TDateTime ): Integer;

Devuelve el número de días que hay entre las fechas ANow y AThen. Por ejemplo:
var
Fecha1, Fecha2: TDate;
begin
Fecha1 := StrToDate( '10/08/2007' );
Fecha2 := StrToDate( '23/08/2007' );
ShowMessage( IntToStr( DaysBetween( Fecha1, Fecha2 ) ) ) ; // devuelve 13
end;

function DaysInMonth( const AValue: TDateTime ): Word;

Nos dice cuantos días tiene el mes que le pasamos como fecha. Por ejemplo:
DaysInMonth( StrToDate( '01/01/2007' ) ) devuelve 31
DaysInMonth( StrToDate( '01/02/2007' ) ) devuelve 28
DaysInMonth( StrToDate( '01/04/2007' ) ) devuelve 30

function DaysInYear( const AValue: TDateTime ): Word;

Nos dice el número de días que tiene un año según la fecha que le pasamos. Por ejemplo:
DaysInYear( StrToDate( '01/01/2007' ) ) devuelve 365
function DaySpan( const ANow, AThen: TDateTime): Double;

Devuelve el número de días de diferencia entre dos fechas y horas incluyendo la parte fraccional. Por ejemplo:
var
FechaHora1, FechaHora2: TDateTime;
begin
FechaHora1 := StrToDateTime( '10/08/2007 13:00' );
FechaHora2 := StrToDateTime( '23/08/2007 19:00' );
ShowMessage( FloatToStr( DaySpan( FechaHora1, FechaHora2 ) ) ); // devuelve 13,25
end;

procedure DecodeDate( Date: TDateTime; var Year, Month, Day: Word );

Este procedimiento convierte un valor de fecha TDate en valores enteros para el año, mes y día. Por ejemplo:
var
wAnyo, wMes, wDia: Word;
begin
DecodeDate( StrToDate( '23/08/2007' ), wAnyo, wMes, wDia );
Memo.Lines.Add( 'Día: ' + IntToStr( wDia ) );
Memo.Lines.Add( 'Mes: ' + IntToStr( wMes ) );
Memo.Lines.Add( 'Año: ' + IntToStr( wAnyo ) );
end;

Al ejecutarlo mostraría en el campo memo:


Día: 23
Mes: 8
Año: 2007

procedure DecodeTime( Time: TDateTime; var Hour, Min, Sec, MSec: Word );

Este procedimiento convierte un valor de hora TTime en valores enteros para la hora, minutos, segundos y milisegundos.
Por ejemplo:
var
wHora, wMinutos, wSegundos, wMilisegundos: Word;
begin
DecodeTime( StrToTime( '14:25:37' ), wHora, wMinutos, wSegundos, wMilisegundos );
Memo.Lines.Add( IntToStr( wHora ) + ' horas' );
Memo.Lines.Add( IntToStr( wMinutos ) + ' minutos' );
Memo.Lines.Add( IntToStr( wSegundos ) + ' segundos' );
Memo.Lines.Add( IntToStr( wMilisegundos ) + ' milisegundos' );
end;

Al ejecutar este código mostraría:


14 horas
25 minutos
37 segundos
0 milisegundos

En el siguiente artículo seguiremos con más funciones para fecha y hora.


Pruebas realizadas en Delphi 7.
Publicado por Administrador en 13:18 0 comentarios
Etiquetas: lenguaje, sistema
17 julio 2007
Trocear y unir archivos
Una de las utilidades más famosas que se han asociado a la descarga de archivos es el programa Hacha, el cual trocea
archivos a un cierto tamaño para luego poder unirlos de nuevo.

El siguiente procedimiento parte un archivo a la longitud en bytes que le pasemos:


procedure TrocearArchivo( sArchivo: TFileName; iLongitudTrozo: Integer );
var
i: Word;
FS, Stream: TFileStream;
sArchivoPartido: String;
begin
FS := TFileStream.Create( sArchivo, fmOpenRead or fmShareDenyWrite );
try
for i := 1 to Trunc( FS.Size / iLongitudTrozo ) + 1 do
begin
sArchivoPartido := ChangeFileExt( sArchivo, '.' + FormatFloat( '000', i ) );
Stream := TFileStream.Create( sArchivoPartido, fmCreate or fmShareExclusive );
try
if fs.Size - fs.Position < iLongitudTrozo then
iLongitudTrozo := FS.Size - FS.Position;
Stream.CopyFrom( FS, iLongitudTrozo );
finally
Stream.Free;
end;
end;
finally
FS.Free;
end;
end;

Si por ejemplo le pasamos el archivo Documentos.zip creará los archivos:

Documentos.001
Documentos.002
....

Para volver a unirlo tenemos otro procedimiento donde le pasamos el primer trozo y el nombre del archivo original:
procedure UnirArchivo( sTrozo, sArchivoOriginal: TFileName );
var
i: integer;
FS, Stream: TFileStream;
begin
i := 1;
FS := TFileStream.Create( sArchivoOriginal, fmCreate or fmShareExclusive );
try
while FileExists( sTrozo ) do
begin
Stream := TFileStream.Create( sTrozo, fmOpenRead or fmShareDenyWrite );
try
FS.CopyFrom( Stream, 0 );
finally
Stream.Free;
end;
Inc(i);
sTrozo := ChangeFileExt( sTrozo, '.' + FormatFloat( '000', i ) );
end;
finally
FS.Free;
end;
end;

Una ampliación interesante a estos procedimientos sería meter el nombre del archivo original en el primer o último trozo,
así como un hash (MD4, MD5, SHA, etc.) para saber si algún trozo está defectuoso.
Pruebas realizadas en Delphi 7.
Publicado por Administrador en 11:52 0 comentarios
Etiquetas: lenguaje, sistema

13 julio 2007
Averiguar los datos del usuario de Windows
Una de las mejores cosas que se pueden hacer en un programa cuando da un error es que nos envíe automáticamente los
datos por correo electrónico. Pero es importante saber que usuario ha enviado el error dentro de la red local.

A continuación vamos a ver cuatro procedimientos que nos van a dar el nombre del usuario de Windows, el nombre de su
PC en la red, su IP local y su IP pública.

Lo primero como siempre es añadir las unidades:


uses
Windows, Messages, ..., WinSock, IdHttp, WinInet;

Esta función nos devuelve el nombre del usuario:


function LeerUsuarioWindows: string;
var
sNombreUsuario: String;
dwLongitudNombre: DWord;
begin
dwLongitudNombre := 255;
SetLength( sNombreUsuario, dwLongitudNombre );
if GetUserName( PChar( sNombreUsuario ), dwLongitudNombre ) Then
Result := Copy( sNombreUsuario, 1, dwLongitudNombre - 1 )
else
Result := 'Desconocido';
end;

Y esta otra nos da el nombre del PC en la red:


function LeerNombrePC: string;
var
Buffer: array[0..255] of char;
dwLongitud: DWord;
begin
dwLongitud := 256;
if GetComputerName( Buffer, dwLongitud ) then
Result := Buffer
else
Result := ''
end;

La siguiente nos da la IP Local en la red:


function IPLocal: String;
var
p: PHostEnt;
s: array[0..128] of char;
p2: pchar;
wVersionRequested: WORD;
wsaData: TWSAData;
begin
// Arranca la librería WinSock
wVersionRequested := MAKEWORD( 1, 1 );
WSAStartup( wVersionRequested, wsaData );
// Obtiene el nombre del PC
GetHostName( @s, 128 );
p := GetHostByName( @s );
// Obtiene la dirección IP y libera la librería WinSock
p2 := iNet_ntoa( PInAddr( p^.h_addr_list^ )^ );
Result := Result + p2;
WSACleanup;
end;

Y esta última lo que hace es decirnos nuestra IP pública conectando con el servidor dyndns.org y utiliza el componente
Indy HTTP el cual leer el contenido del HTML:
function IP_Publica: string;
function EsNumerico( S: string ): Boolean;
begin
Result := false;
if ( Length( S ) > 0 ) then
case S[1] of
'0'..'9': Result := True;
end;
end;
var
HTMLBody: string;
i: Integer;
IdHTTP: TIdHTTP;
begin
Result := '';
// ¿Estamos conectados a Internet?
if WinInet.InternetGetConnectedState( nil, 0 ) then
begin
IdHTTP := TIdHTTP.Create( Application );
try
HTMLBody := IdHTTP.Get( 'http://checkip.dyndns.org/' );
for i := 0 to Length( HTMLBody ) - 1 do
begin
if EsNumerico( HTMLBody[i] ) or ( HTMLBody[i] = '.' ) then
Result := Result + HTMLBody[i];
end;
finally
IdHTTP.Free;
end;
end;
end;

Pruebas realizadas en Delphi 7.


Publicado por Administrador en 13:23 0 comentarios
Etiquetas: sistema

12 julio 2007
Leer la cabecera PE de un programa
¿Queréis verle las tripas a un archivo EXE? El siguiente procedimiento que voy a mostrar lee la cabecera PE de los
archivos ejecutables y nos informa del punto de entrada del programa, el estado de los registros, la pila, etc.

Un archivo ejecutable se compone de distintas cabeceras dentro del mismo, ya sea si se va a ejecutar dentro del antiguo
sistema operativo MS-DOS o en cualquier versión de Windows.

El siguiente procedimiento toma como parámetro un archivo ejecutable y lo guarda en un supuesto campo memo llamado
INFORMACION que se encuentra en el formulario FPrincipal:
procedure TFPrincipal.ExaminarEXE( sArchivo: String );
var
FS: TFilestream;
Firma: DWORD;
Cabecera_dos: IMAGE_DOS_HEADER;
Cabecera_pe: IMAGE_FILE_HEADER;
Cabecera_opc: IMAGE_OPTIONAL_HEADER;
begin
INFORMACION.Clear;
FS := TFilestream.Create( sArchivo, fmOpenread or fmShareDenyNone );
try
FS.Read( Cabecera_dos, SizeOf( Cabecera_dos ) );
if Cabecera_dos.e_magic <> IMAGE_DOS_SIGNATURE then
begin
INFORMACION.Lines.Add( 'Cabecera DOS inválida' );
Exit;
end;

LeerCabeceraDOS( Cabecera_dos, INFORMACION.Lines );


FS.Seek( Cabecera_dos._lfanew, soFromBeginning );
FS.Read( Firma, SizeOf( Firma ) );
if Firma <> IMAGE_NT_SIGNATURE then
begin
INFORMACION.Lines.Add( 'Cabecera PE inválida' );
Exit;
end;
FS.Read( Cabecera_pe, SizeOf( Cabecera_pe ) );
LeerCabeceraPE( Cabecera_pe, INFORMACION.Lines );
if Cabecera_pe.SizeOfOptionalHeader > 0 then
begin
FS.Read( Cabecera_opc, SizeOf( Cabecera_opc ) );
LeerCabeceraOpcional( Cabecera_opc, INFORMACION.Lines );
end;
finally
FS.Free;
end;
end;

Éste a su vez llama a cada uno de los procedimientos que leen las cabeceras DOS, PE y opcional dentro del mismo EXE:
procedure LeerCabeceraDOS( const h: IMAGE_DOS_HEADER; Memo: TStrings );
begin
Memo.Add( 'Cabecera DOS del archivo' );
Memo.Add( Format( 'Número mágico: %d', [h.e_magic] ) );
Memo.Add( Format( 'Byes de la última página del archivo: %d', [h.e_cblp] ) );
Memo.Add( Format( 'Páginas en archivo: %d', [h.e_cp] ) );
Memo.Add( Format( 'Relocalizaciones: %d', [h.e_crlc] ) );
Memo.Add( Format( 'Tamaño de la cabecera en párrafos: %d', [h.e_cparhdr] ) );
Memo.Add( Format( 'Mínimo número de párrafos que necesita: %d', [h.e_minalloc] ) );
Memo.Add( Format( 'Máximo número de párrafos que necesita: %d', [h.e_maxalloc] ) );
Memo.Add( Format( 'Valor inicial (relativo) SS: %d', [h.e_ss] ) );
Memo.Add( Format( 'Valor inicial SP: %d', [h.e_sp] ) );
Memo.Add( Format( 'Checksum: %d', [h.e_csum]));
Memo.Add( Format( 'Valor inicial IP: %d', [h.e_ip] ) );
Memo.Add( Format( 'Valor inicial (relativo) CS: %d', [h.e_cs] ) );
Memo.Add( Format( 'Dirección del archivo de la tabla de relocalización: %d', [h.e_lfarlc] ) );
Memo.Add( Format( 'Número overlay: %d', [h.e_ovno]));
Memo.Add( Format( 'Identificador OEM (para e_oeminfo): %d', [h.e_oemid] ) );
Memo.Add( Format( 'Información OEM; específico e_oemid: %d', [h.e_oeminfo] ) );
Memo.Add( Format( 'Dirección de la nueva cabecera exe: %d', [h._lfanew] ) );
Memo.Add( '' );
end;
procedure LeerCabeceraPE( const h: IMAGE_FILE_HEADER; Memo: TStrings );
var
Fecha: TDateTime;
begin
Memo.Add( 'Cabecera PE del archivo' );
Memo.Add( Format( 'Máquina: %4x', [h.Machine]));
case h.Machine of
IMAGE_FILE_MACHINE_UNKNOWN : Memo.Add(' Máquina desconocida ' );
IMAGE_FILE_MACHINE_I386: Memo.Add( ' Intel 386. ' );
IMAGE_FILE_MACHINE_R3000: Memo.Add( ' MIPS little-endian, 0x160 big-endian ' );
IMAGE_FILE_MACHINE_R4000: Memo.Add( ' MIPS little-endian ' );
IMAGE_FILE_MACHINE_R10000: Memo.Add( ' MIPS little-endian ' );
IMAGE_FILE_MACHINE_ALPHA: Memo.Add( ' Alpha_AXP ' );
IMAGE_FILE_MACHINE_POWERPC: Memo.Add( ' IBM PowerPC Little-Endian ' );
$14D: Memo.Add( ' Intel i860' );
$268: Memo.Add( ' Motorola 68000' );
$290: Memo.Add( ' PA RISC' );
else
Memo.Add( ' tipo de máquina desconocida' );
end;
Memo.Add( Format( 'Número de secciones: %d', [h.NumberOfSections] ) );
Memo.Add( Format( 'Fecha y hora: %d', [h.TimeDateStamp] ) );
Fecha := EncodeDate( 1970, 1, 1 ) + h.Timedatestamp / SecsPerDay;
Memo.Add( FormatDateTime( ' c', Fecha ) );
Memo.Add( Format( 'Puntero a la tabla de símbolos: %d', [h.PointerToSymbolTable] ) );
Memo.Add( Format( 'Número de símbolos: %d', [h.NumberOfSymbols] ) );
Memo.Add( Format( 'Tamaño de la cabecera opcional: %d', [h.SizeOfOptionalHeader] ) );
Memo.Add( Format( 'Características: %d', [h.Characteristics] ) );
if ( IMAGE_FILE_DLL and h.Characteristics ) <> 0 then
Memo.Add(' el archivo es una' )
else
if (IMAGE_FILE_EXECUTABLE_IMAGE and h.Characteristics) <> 0 then
Memo.Add(' el archivo es un programa' );
Memo.Add('');
end;
procedure LeerCabeceraOpcional( const h: IMAGE_OPTIONAL_HEADER; Memo: TStrings );
begin
Memo.Add( 'Información sobre la cabecera PE de un archivo ejecutable EXE' );
Memo.Add( Format( 'Magic: %d', [h.Magic] ) );
case h.Magic of
$107: Memo.Add( ' Imagen de ROM' );
$10b: Memo.Add( ' Imagen de ejecutable' );
else
Memo.Add( ' Tipo de imagen desconocido' );
end;
Memo.Add( Format( 'Versión mayor del enlazador: %d', [h.MajorLinkerVersion] ) );
Memo.Add( Format( 'Versión menor del enlazador: %d', [h.MinorLinkerVersion]));
Memo.Add( Format( 'Tamaño del código: %d', [h.SizeOfCode]));
Memo.Add( Format( 'Tamaño de los datos inicializados: %d', [h.SizeOfInitializedData]));
Memo.Add( Format( 'Tamaño de los datos sin inicializar: %d', [h.SizeOfUninitializedData]));
Memo.Add( Format( 'Dirección del punto de entrada: %d', [h.AddressOfEntryPoint]));
Memo.Add( Format( 'Base de código: %d', [h.BaseOfCode]));
Memo.Add( Format( 'Base de datos: %d', [h.BaseOfData]));
Memo.Add( Format( 'Imagen base: %d', [h.ImageBase]));
Memo.Add( Format( 'Alineamiento de la sección: %d', [h.SectionAlignment]));
Memo.Add( Format( 'Alineamiento del archivo: %d', [h.FileAlignment]));
Memo.Add( Format( 'Versión mayor del sistema operativo: %d', [h.MajorOperatingSystemVersion]));
Memo.Add( Format( 'Versión mayor del sistema operativo: %d', [h.MinorOperatingSystemVersion]));
Memo.Add( Format( 'Versión mayor de la imagen: %d', [h.MajorImageVersion]));
Memo.Add( Format( 'Versión menor de la imagen: %d', [h.MinorImageVersion]));
Memo.Add( Format( 'Versión mayor del subsistema: %d', [h.MajorSubsystemVersion]));
Memo.Add( Format( 'Versión menor del subsistema: %d', [h.MinorSubsystemVersion]));
Memo.Add( Format( 'Valor de la versión Win32: %d', [h.Win32VersionValue]));
Memo.Add( Format( 'Tamaño de la imagen: %d', [h.SizeOfImage]));
Memo.Add( Format( 'Tamaño de las cabeceras: %d', [h.SizeOfHeaders]));
Memo.Add( Format( 'CheckSum: %d', [h.CheckSum]));
Memo.Add( Format( 'Subsistema: %d', [h.Subsystem]));
case h.Subsystem of
IMAGE_SUBSYSTEM_NATIVE:
Memo.Add( ' La imagen no requiere un subsistema. ' );
IMAGE_SUBSYSTEM_WINDOWS_GUI:
Memo.Add( ' La imagen se corre en un subsistema GUI de Windows. ' );
IMAGE_SUBSYSTEM_WINDOWS_CUI:
Memo.Add( ' La imagen corre en un subsistema terminal de Windows. ' );
IMAGE_SUBSYSTEM_OS2_CUI:
Memo.Add( ' La imagen corre sobre un subsistema terminal de OS/2. ' );
IMAGE_SUBSYSTEM_POSIX_CUI:
Memo.Add( ' La imagen corre sobre un subsistema terminal Posix. ' );
else
Memo.Add( ' Subsistema desconocido.' )
end;
Memo.Add( Format( 'Características DLL: %d', [h.DllCharacteristics]) );
Memo.Add( Format( 'Tamaño de reserva de la pila: %d', [h.SizeOfStackReserve]) );
Memo.Add( Format( 'Tamaño de trabajo de la pila: %d', [h.SizeOfStackCommit]) );
Memo.Add( Format( 'Tamaño del Heap de reserva: %d', [h.SizeOfHeapReserve]) );
Memo.Add( Format( 'Tamaño de trabajo del Heap: %d', [h.SizeOfHeapCommit]) );
Memo.Add( Format( 'Banderas de carga: %d', [h.LoaderFlags] ) );
Memo.Add( Format( 'Numeros RVA y tamaño: %d', [h.NumberOfRvaAndSizes] ) );
end;
Espero que os sea de utilidad si os gusta programar herramientas de administración de sistemas operativos Windows.
Pruebas realizadas en Delphi 7.
Publicado por Administrador en 13:03 0 comentarios
Etiquetas: sistema

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.


Publicado por Administrador en 12:13 0 comentarios
Etiquetas: sistema

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.
Publicado por Administrador en 10:25 0 comentarios
Etiquetas: internet, sistema

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.
Publicado por Administrador en 11:15 2 comentarios
Etiquetas: internet, sistema

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.


Publicado por Administrador en 11:47 3 comentarios
Etiquetas: sistema

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.
Publicado por Administrador en 10:31 0 comentarios
Etiquetas: sistema

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.
Publicado por Administrador en 11:16 0 comentarios
Etiquetas: sistema

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.


Publicado por Administrador en 10:45 0 comentarios
Etiquetas: aplicación, sistema

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.
Publicado por Administrador en 11:18 0 comentarios
Etiquetas: gráficos, sistema
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.
Publicado por Administrador en 11:05 0 comentarios
Etiquetas: interfaz, sistema

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.


Publicado por Administrador en 12:51 0 comentarios
Etiquetas: internet, sistema

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.

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.
Publicado por Administrador en 12:10 0 comentarios
Etiquetas: sistema

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.
Publicado por Administrador en 10:38 0 comentarios
Etiquetas: sistema

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.
Publicado por Administrador en 12:58 2 comentarios
Etiquetas: sistema

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.
Publicado por Administrador en 12:25 2 comentarios
Etiquetas: interfaz, sistema

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.
Publicado por Administrador en 19:55 2 comentarios
Etiquetas: interfaz, sistema

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.
Publicado por Administrador en 18:24 2 comentarios
Etiquetas: gráficos, interfaz, sistema

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

También podría gustarte