Boletín Delphi #1 (ahora Boletín Pascal)
Los ejemplos completos de código fuente de este número están disponibles para descargar.
![]() |
![]() |
Boletín Delphi #1 INDICE 1. UNAS PALABRAS DEL EDITOR 2. HILOS DE EJECUCION (THREADS) - Introducción - La clase TThread - Ejemplo de TThread 3. EJECUCION DE UNA APLICACION EXTERNA 4. ¿QUE SIGUE? ________________________________________________________________________ 1. UNAS PALABRAS DEL EDITOR Tengo el placer de anunciar que la semana pasada tuvimos un gran éxito con el lanzamiento de nuestro Kylix Newsletter, y ahora esperamos que este Delphi Newsletter siga el mismo camino. Este boletín es una publicación no oficial destina a programadores en Delphi con un nivel intermedio. Asumimos que el lector ya tiene cierta familiaridad con la VCL, el lenguaje Object Pascal y el IDE de Delphi, de modo tal que iremos directamente a aquellas tareas que un programador frecuentemente necesita realizar y para las cuales la información contenida en la documentación que acompaña Delphi es escasa (o incluso inexistente). La primera edición estaba programada para mediados de Junio, pero ha generado tanta expectativa que hemos optado por hacer un esfuerzo y acceder así a las demandas de nuestros suscriptores de lanzarla mucho antes de lo planeado. Esperamos que la disfruten. Este boletín está protegido por la propiedad intelectual, pero es muy importante para nosotros mantenerlo creciendo en audiencia y contenido para continuar este proyecto, así que por favor siéntase libre de reenviarlo a amigos, conocidos y colegas, que Usted crea que pueden estar interesados en esta publicación, siempre y cuando lo haga en forma completa y sin modificaciones. En este número le mostraremos cómo escribir una aplicación multi-hilos o multi-hebras (del inglés multi-threaded). Este artículo responde a una tarea muy común en programación, aunque estamos conscientes que quizás es demasiado técnico, por lo que hemos tratado de escribirlo tan expli- cativamente como sea posible, esperando que los programadores más principiantes puedan al menos entender los conceptos básicos del multi- hilado. Si tiene dudas, problemas o preguntas acerca del ejemplo que aquí se presenta, por favor envíenos un email. Estaremos complacidos de ayudarlo en todo lo que esté a nuestro alcance. Para compensar, también trataremos en este número un tema de menor difi- cultad, como lo es la ejecución de otras aplicaciones desde la nuestra. Estamos muy contentos con esta primera edición y esperamos ansiosamente sus comentarios. Nos gustaría mucho conocer sus opiniones acerca de este boletín para poder mejorarlo. También nos gustaría conocer cuales son sus necesidades de programación para evaluar si podemos cubrirlas en las futuras ediciones. ¡Manténgase en contacto! Atentamente, Ernesto De Spirito eds2004 @ latiumsoftware.com ________________________________________________________________________ JfControls Lib. Multilenguaje. Multiapariencia. Skins. Privilegios. Más de 40 componentes integrados y personalizables. Múltiples problemas de programación resueltos. Administración centralizada de recursos. Para Delphi 3-6 y C++ Builder 3-5. http://www.jfactivesoft.com/spindex.htm ________________________________________________________________________ 2. HILOS DE EJECUCION (THREADS) Introducción ============ En Windows de 32 bits Ud. puede tener varios programas corriendo al mismo tiempo. Por ejemplo, puede estar imprimiendo un documento y simultáneamente revisar su disco duro en busca de virus con un programa antivirus y mientras tanto, para matar el tiempo, puede jugar al solitario, recibir su correo o realizar cualquier otra tarea. Sin embargo, dado que el microprocesador (UCP) sólo es capaz de realizar una sola operación en un momento dado, ¿cómo es posible ejecutar varios programas al mismo tiempo? Bueno, primero, cuando un programa está cargado en memoria para ser ejecutado se lo denomina "proceso", y segundo, si su computadora tiene una sola UCP, entonces sólo un proceso puede estar ejecutándose en un determinado momento. ¿Cómo es entonces que parece como si hubieran varios procesos ejecután- dose simultáneamente? Esta es la pregunta correcta. "Parece como si" varios procesos se ejecutaran al mismo tiempo gracias a las capacidades multi-tarea de Windows. ¿Cómo funciona esta multitarea? Bueno, supongamos que tenemos cinco procesos (A, B, C, D y E). El SO (Sistema Operativo) le asigna un período muy corto de tiempo a un proceso (por ej. el A) y luego cuando se acaba ese tiempo, el proceso es detenido temporalmente y el SO le da un período muy corto de tiempo a otro proceso (por ej. el B), y así sucesivamente con el resto de los procesos (por ej. el C, D y E), hasta que vuelve a ser el turno entonces del primer proceso (el A, en nuestro ejemplo), y así el ciclo se repite una y otra vez, de modo tal que en definitiva los procesos se ejecutan un brevísimo lapso de tiempo, se detienen (para permitir que los otros se ejecuten un brevísmio lapso de tiempo también), se vuelven a ejecutar otro poco, se detienen, y así sucesivamente hasta que terminan, dando la sensación de una ejecución continua sin interrupciones, del mismo modo en que una película no es más que una serie de fotogramas estáticos que cambian tan rápidamente que percibimos la ilusión de un movimiento continuo. De esta forma, parece como si varios procesos estuvieran corriendo al mismo tiempo. Ahora bien, las capacidades multitarea de Windows no se limitan a procesos, sino que dentro de un mismo proceso también es posible tener partes del mismo ejecutándose "al mismo tiempo", es decir, en distintos "hilos de ejecución" (threads). Normalmente usamos uno solo (el hilo principal de la VCL), pero podemos crear y ejecutar otros hilos dentro una aplicación. En este caso, se dice que la aplicación es multi-hilos. ¿Cuándo necesitaríamos crear otros hilos? Cuando queramos que nuestra aplicación realice varias cosas al mismo tiempo, por supuesto, siendo el caso típico cuando necesitamos realizar alguna tarea que pueda demorar mucho (como realizar un gran apareo de archivos o buscar un fichero en todo el disco duro, por citar algunos ejemplos), y deseamos que mientras se realice esta tarea nuestra aplicación responda a eventos, de modo que el usuario pueda detener prematuramente la tarea antes que ésta finalice y/o que pueda hacer otras cosas con nuestra aplicación. La clase TThread ================ La clase TThread (declarada en la unidad Classes) encapsula las llamadas a la API de Windows necesarias para manejar distintos hilos de ejecu- ción. Esta clase tiene un método abstracto (llamado Execute), y por ende es una clase abstracta, por lo que no puede ser instanciada (es decir, crear objetos de esa clase). Lo que se debe hacer es crear una clase descendiente de TThread (por ejemplo TThread1) y redefinir (override) el método Execute, que es el método que se ejecutará en un hilo de ejecu- ción diferente. Para realmente ejecutar este hilo, primero debemos declarar un objeto de esta nueva clase (por ejemplo Thread1) y luego construirlo: Thread1 := TThread1.Create(False); El parámetro para el constructor es el valor que tendrá la propiedad Suspended (suspendido). Si éste es False, se llamará a Execute inmedia- tamente después que el objeto sea creado. Esta llamada se realizará de modo asíncrono, es decir, el método Execute se ejecutará en un nuevo hilo de ejecución, y el hilo principal de la VCL también seguirá ejecu- tándose, es decir que la ejecución de la aplicación no se detiene a esperar que Execute termine, sino que continúa ejecutándose "al mismo tiempo". Normalmente el hilo se crea "suspendido" (pasando True como parámetro en la llamada al constructor), de modo que primero podamos establecer los valores de otras propiedades del hilo de ejecución como Priority (prio- ridad) y FreeOnTerminate (liberar al terminar) antes de asignar False a Suspended para comenzar la ejecución del hilo (lo que hará que se llame al método Execute asíncronamente). Los hilos con mayor prioridad obtienen más tiempo de la UCP que los otros y aunque eso hará que se ejecuten "más rápido", puede que ralentice otros hilos de la aplicación, por lo que se debe ser crite- rioso al establecer el valor de la propiedad Priority. La propiedad FreeOnTerminate determina si el objeto se destruye una vez que el método Execute finalice su ejecución (liberándonos de esta tarea), y en tal caso, el método Destroy será llamado (y podemos utili- zarlo por ejemplo para notificar al hilo principal que este hilo ha terminado). Para detener prematuramente la ejecución del hilo se debe poner en True la propiedad Terminated. Es importante hacer notar que eso no termina el hilo inmediatamente, sino que supuestamente el método Execute debería estar constantemente chequeando el valor de la propiedad Terminated para ver si se le requiere que finalice, y en tal caso limpiar (liberar recursos, restablecer de variables, etc.) y salir del procedimiento. Ejemplo de TThread ================== En este ejemplo vamos a utilizar un hilo como una propiedad de un formulario. El formulario tendrá un botón de inicio (para comenzar la ejecución del hilo), un botón de detención (para cancelar la ejecución del hilo) y una barra de progreso para visualizar el progreso de la ejecución del hilo, que consistirá simplemente de un bucle "para" (for). Para probar este ejemplo, primero cree una aplicación nueva. En el formulario (Form1) añada una barra de progreso (ProgressBar, que debería estar en la paleta Win32) y dos botones (Button, en la paleta Standard). En el Inspector de Objetos, establezca las propiedades de esos objetos: ProgressBar1 Max := 100000 Step := 1 Smooth := True Button1 Caption := '&Iniciar' Button2 Cancel := True Caption := '&Detener' Enabled := False Al botón de detención lo hemos inhabilitado porque el hilo no estará inicialmente en ejecución cuando se cree el formulario. Cuando lo hagamos ejecutar (en el evento OnClick del botón de inicio) entonces habilitaremos el botón de cancelación (para que el usuario pueda detener el hilo) e inhabilitaremos el botón de inicio (porque el hilo ya se habría iniciado). Cuando el hilo finalice su ejecución, deberíamos volver a habilitar el botón de inicio (para que el usuario pueda volver a ejecutar el hilo si así lo desea) e inhabilitar el botón de cancelación. Pero, ¿cómo sabremos que el hilo ha finalizado? Una forma de saber es establecer la propiedad FreeOnTerminate en True antes de ejecutar el hilo, de modo que cuando termine, el objeto que representa al hilo sea destruido y en su método Destroy podamos enviar un mensaje al formulario "dueño" del hilo y capturar este mensaje en el formulario. Ahora vayamos al código. Antes de las declaraciones type --en donde está declarado el formulario-- declare una constante para identificar el mensaje que será enviado del hilo al formulario. const WM_ThreadDoneMsg = WM_User + 8; WM_User es una constante predefinida que indica el valor mínimo de los mensajes definidos por el usuario. Los valores por debajo de WM_User están reservados para los mensajes de Windows. El ocho (8) que le sumamos es un valor arbitrario. Dentro de las declaraciones de tipos (type), antes de la declaración del formulario, declare la clase del hilo como sigue: TThread1 = class(TThread) private OwnerHandle: HWND; ProgressBar1: TProgressBar; procedure UpdateProgressBar; protected procedure Execute; override; published constructor Create(Owner: TForm; ProgressBar: TProgressBar); destructor Destroy; override; end; Esto amerita algunas explicaciones: 1) Necesitamos el manejador (window handle) del formulario que contiene el hilo para poder enviarle el mensaje WM_ThreadDoneMsg cuando el hilo termine, por lo que declaramos una propiedad OwnerHandle que contenga este manejador (de tipo HWND) y también redefinimos el método Destroy (porque allí incluiremos el código para enviar el mensaje). 2) Debido a que usaremos la barra de progreso del formulario desde el hilo, creamos la propiedad ProgressBar1 para tener una referencia a ese componente. Otra posibilidad sería por ejemplo tener directamente una referencia al formulario, pero debería ser de tipo type TForm1 (lo que implica que hay que declarar la clase como forward) o de tipo TForm (que implicaría que deberemos castar esa referencia como TForm1 antes de acceder a la barra de progreso), así que consideramos la solución adoptada como la más simple. 3) El método UpdateProgressBar será llamado desde Execute para actua- lizar la barra de progreso (simplemente la hará avanzar). ¿Por qué no avanzar la barra de progreso directamente desde el método Execute? Se hace así para evitar conflictos multi-hilos (dos hilos accediendo la misma memoria o recursos pueden potencialmente introducir errores en la aplicación). Ese método UpdateProgressBar será llamado usando el método Synchronize de la clase TThread, que hará que se ejecute en el hilo principal de la VCL de modo que no conflictúe con él. 4) Redefinimos el método Execute. Una obligación, si queremos poder instanciar la clase TThread1. 5) Declaramos nuestro propio constructor, que recibe como parámetros una referencia al formulario propietario del hilo y otra a la barra de progreso. Una vez que hemos declarado la clase TThread1 class, en la sección privada de la clase TForm1 agregue las siguientes declaraciones: private Thread1: TThread1; procedure Thread1Done(var AMessage: TMessage); message WM_ThreadDoneMsg; Aquí, Thread1 representa al hilo y es un objeto de la clase TThread1 declarada más arriba, mientras que Thread1Done es una especie de evento que se ejecutará cuando el formulario reciba el mensaje WM_ThreadDoneMsg. Los métodos que responden a mensajes siempre reciben una referencia a un objeto TMessage como parámetro. En nuestro ejemplo no lo usaremos, pues la simple recepción del mensaje es todo lo que nos importa. En el Inspector de Objetos genere los eventos OnClick de los dos botones y el evento OnClose del formulario, insertando el código que sigue en dichos manejadores de evento: procedure TForm1.Button1Click(Sender: TObject); begin Button1.Enabled := False; // Inhabilita el botón de inicio Button2.Enabled := True; // Habilita el botón de detención // Crea e inicia el nuevo hilo de ejecución Thread1 := TThread1.Create(Self, ProgressBar1); // La ejecución de la aplicación continúa aquí y "al mismo // tiempo" se ejecuta el método Execute del objeto Thread1 end; procedure TForm1.Button2Click(Sender: TObject); begin Thread1.Terminate; // Detiene el hilo. No ocurre inmediatamente. end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin if Button2.Enabled then begin // Si el hilo está ejecutándose Thread1.Terminate; // Lo detiene y Thread1.WaitFor; // espera a que termine end;//if Action := caFree; // Libera el formulario // después que se cierre end; Ahora agregue el siguiente código en la sección Implementation de la unidad. procedure TForm1.Thread1Done(var AMessage: TMessage); // Captura el mensaje enviado por el hilo señalando que ha // terminado su ejecución begin Button1.Enabled := True; // Habilita el botón de inicio Button2.Enabled := False; // Inhabilita el botón de detención end; constructor TThread1.Create(Owner: TForm; ProgressBar: TProgressBar); begin inherited Create(True); // Crea el hilo suspendido OwnerHandle := Owner.Handle; // Manejador del formulario ProgressBar1 := ProgressBar; // Ref. a la barra de progreso ProgressBar1.Position := 0; // Inicializa la barra de progreso Priority := tpNormal; // Establece la prioridad del hilo FreeOnTerminate := True; // El hilo se liberará solo Suspended := False; // Inicia la ejecución del hilo // ==> La ejecución de la aplicación continúa aquí y "al mismo // tiempo" se ejecuta el método Execute (modo asíncrono) end; destructor TThread1.Destroy; // Se llamará cuando Execute termine porque FreeOnTerminate fue // puesto en True begin // Envía un mensaje al formulario para notificarle que el hilo // ha terminado. PostMessage(OwnerHandle, WM_ThreadDoneMsg, Self.ThreadID, 0); inherited Destroy; // Llama al destructor del ancestro end; procedure TThread1.Execute; // Ejecución del hilo var i: integer; begin for i := 1 to 100000 do begin Synchronize(UpdateProgressBar); // Actualiza la barra de // progreso en el hilo ppal. if Terminated then break; // Finaliza el bucle si el // hilo es detenido end;//for end; procedure TThread1.UpdateProgressBar; // Este método es llamado desde el método Execute en modo // sincronizado para que se ejecute en el hilo principal de la // VCL, evitando posibles conflictos multi-hilos. begin ProgressBar1.StepIt; // Hace avanzar la barra de progreso end; Bueno, ¡eso es todo! El código fuente completo de este ejemplo está disponible en el archivo adjunto. ________________________________________________________________________ 3. EJECUCION DE UNA APLICACION EXTERNA Muchas veces necesitamos ejecutar otro programa desde el nuestro. Para hacer esto, podemos utilizar la función ShellExecute declarada en la unidad ShellAPI. La sintaxis es: ShellExecute(Manejador, Operación, NombreFichero, Parámetros, Carpeta, Mostrar) Manejador (HWND) es el manejador (window handle) de la ventana madre, por ejemplo el manejador del formulario principal de nuestra aplicación. Operación (PChar) es un puntero a una cadena terminada en nulo conte- niendo el nombre de la operación a realizar, pudiendo ser "open" (abrir), "print" (imprimir) o "explore" (explorar carpeta). Este parámetro puede ser Nil y en tal caso se asumirá la operación "open" (abrir). NombreFichero (PChar) es un puntero a una cadena terminada en nulo conteniendo el camino y el nombre de la aplicación a ejecutar, el documento abrir o imprimir con la aplicación asociada o la carpeta a abrir o explorar. Parámetros (PChar) es un puntero a una cadena terminada en nulo conte- niendo los parámetros que se pasan a la aplicación indicada en NombreFichero. Si NombreFichero no especificaba un ejecutable sino un documento, entonces Parámetros debe ser Nil. Carpeta (PChar) es un puntero a una cadena terminada en nulo conteniendo el camino de la carpeta que se tomará como directorio por omisión de la aplicación. Se corresponde con el cuadro de texto "Iniciar en:" de las propiedades de los accesos directos. Este parámetro puede ser Nil. Mostrar (Integer) especifica la forma en que se mostrará la aplicación especificada en NombreFichero. Hay varios valores posibles: SW_HIDE SW_SHOWMAXIMIZED SW_MAXIMIZE SW_SHOWMINIMIZED SW_MINIMIZE SW_SHOWMINNOACTIVE SW_RESTORE SW_SHOWNA SW_SHOW SW_SHOWNOACTIVATE SW_SHOWDEFAULT SW_SHOWNORMAL La documentación indica que si NombreFichero se refiere a un documento, Mostrar debería ser 0, sin embargo si prueban otros valores verán que sí funcionan. Valor devuelto: Si la función tiene éxito, ShellExecute devuelve un valor tipo HINST (definido como LongWord) con el manejador (handle) de la aplicación que se ejecutó o el manejador de la aplicación servidora DDE. Si la función fracasa (o sea, si la aplicación no pudo iniciarse), el valor devuelto es un código de error entre 0 y 32. Veamos ahora algunos ejemplos: if ShellExecute(Form1.Handle, nil, 'c:\windows\general.txt', nil, nil, SW_SHOWNORMAL) <= 32 then Application.MessageBox('No se pudo ejecutar la aplicación', 'Error', MB_ICONEXCLAMATION); ShellExecute(Form1.Handle, nil, 'c:\windows\notepad.exe', 'c:\windows\general.txt', nil, SW_SHOWMAXIMIZE); ShellExecute(Form1.Handle, 'open', 'c:\windows\notepad.exe', 'general.txt', 'c:\windows', SW_SHOWNORMAL); ShellExecute(Form1.Handle, nil, PChar(fname + '.txt'), nil, nil, SW_MAXIMIZE); ShellExecute(Form1.Handle, nil, 'c:\windows\notepad.exe', nil, nil, SW_SHOWNORMAL); Nota: Las aplicaciones llamadas por ShellExecute se ejecutan en modo asíncrono, es decir, la ejecución de nuestra aplicación continúa sin esperar a que la aplicación llamada por ShellExecute finalice. Si deseamos que un proceso se ejecute en modo síncrono, no debemos usar ShellExecute sino un código como el siguiente: procedure TForm1.Button1Click(Sender: TObject); var proc_info: TProcessInformation; startinfo: TStartupInfo; ExitCode: longword; begin //Inicializar las estructuras FillChar(proc_info, sizeof(TProcessInformation), 0); FillChar(startinfo, sizeof(TStartupInfo), 0); startinfo.cb := sizeof(TStartupInfo); // Intenta crear el proceso if CreateProcess('c:\windows\notepad.exe', nil, nil, nil, false, NORMAL_PRIORITY_CLASS, nil, nil, startinfo, proc_info) <> False then begin // El proceso se creó con éxito // Ahora esperar hasta que el proceso termine... WaitForSingleObject(proc_info.hProcess, INFINITE); // El proceso ha terminado. Ahora lo cerramos. GetExitCodeProcess(proc_info.hProcess, ExitCode); // Opcional CloseHandle(proc_info.hThread); CloseHandle(proc_info.hProcess); Application.MessageBox((PChar(Format( '¡Bloc de notas finalizado! (Código de retorno=%d)', [ExitCode])), 'Aviso', MB_ICONINFORMATION); end else begin // No se puedo crear el proceso Application.MessageBox('No se pudo ejecutar la aplicación', 'Error', MB_ICONEXCLAMATION); end;//if end; Para abrir un fichero con la aplicación, emplee un código como el que se muestra a continuación: if CreateProcess(nil, '"c:\windows\notepad.exe" "c:\windows\general.txt"', ... Las comillas dobles sólo son necesarias cuando el camino o el nombre de la aplicación o del documento contienen espacios. La función CreateProcess sólo funciona con achivos ejecutables, es decir, no ejecuta documentos con la aplicación asociada. La solución a esta cuestión la veremos en el próximo número. ________________________________________________________________________ 4. ¿QUE SIGUE? En el próximo número trataremos de responder todas las preguntas de nuestros lectores respecto de los temas tratados en éste, y veremos cómo hacer algo más útil en el método Execute, por ejemplo que revise ficheros en una carpeta en búsqueda de una palabra clave y también veremos un ejemplo de uso del Registro de Windows para abrir un documento con su aplicación asociada y esperar a que ésta termine. ¡Nos vemos! ________________________________________________________________________ Si no has recibido el archivo con el código fuente completo de los ejemplos que se presentan en este boletín, puedes descargarlo de la siguiente dirección: http://www.latiumsoftware.com/descarga/delphi-1.zip ________________________________________________________________________ Página principal: http://www.latiumsoftware.com/es/pascal/index.php Página del grupo: http://espanol.groups.yahoo.com/group/boletin-pascal/ Para suscribirse / apuntarse: boletin-pascal-subscribe@gruposyahoo.com Para cancelar / removerse: boletin-pascal-unsubscribe@gruposyahoo.com Para reportar problemas con la suscripción: eds2004 @ latiumsoftware.com ________________________________________________________________________ Este boletín se provee "TAL Y COMO ESTA", sin garantía de ninguna clase. Su uso implica la aceptación de nuestros términos de licencia y de la ausencia de garantía que puedes leer en nuestro sitio web. Allí también encontrarás una nota sobre marcas registradas. Te animamos a que redis- tribuyas este boletín, siempre y cuando lo hagas en forma completa (incluyendo la información de copyright), sin modificaciones y de manera gratuita. Los artículos son copyright de sus respectivos autores y se reproducen aquí con el permiso de los mismos. ________________________________________________________________________ Latium Software http://www.latiumsoftware.com/es/index.php Copyright (c) 2000 por Ernesto De Spirito. Todos los derechos reservados ________________________________________________________________________ |
Los ejemplos completos de código fuente de este número están disponibles para descargar.
![]() |
¿Errores? ¿Omisiones? ¿Comentarios? Por favor contáctanos!






