Analisis de Sistema en Delphi
Analisis de Sistema en Delphi
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:
- 2 botones de la clase TButton llamados BComenzar y BCancelar para iniciar o detener el hilo de ejecución.
- 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.
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;
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;
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/
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:
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:
Si nos fijamos en el inspector de objetos veremos que no se han estresado añadiendo propiedades:
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.
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.
procedure Execute;
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;
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.
Igual que el procedimiento anterior pero espera a que termine el código que hemos colocado dentro de OnWorkProgress.
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.
Lo mismo que el procedimiento anterior pero espera a que termine la ejecución del código que hemos puesto en el evento
OnWorkFeedBack.
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.
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.
Para instalar este componente en Delphi 7 hay que seguir estos pasos:
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.
D:\Borland\Delphi7\Componentes\BackgroundWorker\
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.
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.
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.
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).
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).
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;
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.
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:
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 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).
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.
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;
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);
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.
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.
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;
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.
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.
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.
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;
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;
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;
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;
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;
...
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:
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.
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
...
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;
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;
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.
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.
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)
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
Este procedimiento cambia el directorio actual al indicado por el parámetro S. Por ejemplo:
ChDir( 'C:\Windows\Fonts' );
Nos devuelve el nombre del directorio actual donde estamos posicionados. Por ejemplo:
GetCurrentDir devuelve C:\Windows\Fonts
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:
...
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
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:
...
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
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;
Busca el siguiente archivo, directorio o unidad especificado anteriormente por la función FindFirst. Devuelve un cero si
ha encontrado algo.
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;
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.
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.
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.
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;
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].
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.
Si utilizamos AssignFile para leer un archivo en vez de TFileStream podemos mover el puntero en cualquier dirección
utilizando el procedimiento Seek:
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.
Veamos de que funciones dispone Delphi para leer los atributos de un archivo (fecha, modo de acceso. etc.):
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
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;
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.
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.
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;
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.
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;
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.
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.
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).
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;
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.
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.
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;
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;
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:
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
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;
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
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
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
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.
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).
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
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
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
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).
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
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
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
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
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
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
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
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.
Veamos entonces que funciones nos pueden ser útiles para ello:
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)
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)
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
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;
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
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;
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;
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;
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.
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;
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;
É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;
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] ) );
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;
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;
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.
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;
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.
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;
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.
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;
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.
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;
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;
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;
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;
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.
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;
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:
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;
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