API de Windows y Delphi

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 65

La biblioteca VCL, que hace que la creación de aplicaciones Delphi sea tan rápida y sencilla, no permite que

el desarrollador use todas las funciones del sistema operativo. El acceso completo a todas las funciones
documentadas del sistema lo proporciona la API (Interfaz de Programación de Aplicaciones).
Existen muchos libros sobre programación API de Windows, así como material en Internet. Sin embargo, si
se hace todo usando sólo la API, entonces incluso para crear una ventana vacía tendrá que escribir algunas
decenas de líneas de código y tendrá que olvidarse del diseño visual de la ventana. Por lo tanto, es deseable
combinar de alguna forma la potencia de la API y la comodidad de la VCL. Este capítulo trata de cómo
hacerlo.
La primera parte del capítulo se centra en los principios generales del uso de la API y la integración de esta
interfaz con la VCL. La segunda parte trata ejemplos sencillos que ilustran la teoría. La tercera parte
presenta algunos ejemplos generalizados de cómo usar la API: pequeñas aplicaciones completas que usan
varias funciones API para resolver problemas complejos.

Esta sección describirá cómo combinar la API de Windows con los componentes de la VCL. Suponemos que
está familiarizado con las técnicas básicas de la VCL y la sintaxis de Delphi, por lo que nos saltamos esta
parte. Dado que la ayuda y los ejemplos “oficiales” de la API están escritas en C o C++ y pueden resultar
difíciles para alguien nuevo en Delphi, también nos centraremos en la lectura del Manual de Referencia y en
su traducción de C/C++ a Delphi.

La API de Windows es un conjunto de funciones que el sistema operativo proporciona a cada programa.
Estas funciones están contenidas en bibliotecas de vínculos dinámicos (DLL) estándar como kernel32.dll,
user32.dll, gdi32.dll. Estos archivos se encuentran en los directorios del sistema de Windows. En general,
cada programa debe encargarse de enlazar estas bibliotecas por su cuenta. Las DLL se pueden enlazar a un
programa de forma estática o dinámica. En el primer caso, el enlace a la biblioteca se escribe en el
ejecutable del programa y cuando el sistema inicia el programa, también carga inmediatamente la
biblioteca en su espacio de direcciones. Si la biblioteca no se encuentra en el disco, el programa no se
iniciará. En el caso de un enlace dinámico, el programa carga la biblioteca en cualquier momento que le
resulte conveniente mediante la función LoadLibrary. Si se produce un error porque la biblioteca no se
encuentra en el disco, el programa puede decidir cómo manejarlo.
La carga estática es más simple que la dinámica, pero la carga dinámica es más flexible. Con la carga
dinámica, el programador puede descargar la biblioteca sin tener que esperar el final del programa. En
segundo lugar, el programa puede seguir funcionando incluso si la biblioteca no se encuentra presente. En
tercer lugar, es posible cargar aquellas DLL cuyos nombres se desconocen al momento de la compilación.
Esto permite ampliar la funcionalidad de la aplicación después de transferirla al usuario con la ayuda de
bibliotecas adicionales (plug-ins).

Las bibliotecas estándar son requeridas por el sistema y por todos los programas, siempre están en
memoria y por lo tanto se suelen cargar estáticamente.

1
Para cargar estáticamente una función de la API de Windows en Delphi, por ejemplo, la función
GetWindowDC del módulo user32.dll, debe escribir lo siguiente:
function GetWindowDC(Wnd: HWnd); HDC; stdcall; external 'user32.dll' name
'GetWindowDC';

Como resultado, en una sección especial del archivo ejecutable, llamada tabla de importación, aparecerá
una entrada que indica que el programa importa la función GetWindowDC desde la biblioteca user32.dll.
Con esta declaración, el compilador sabrá cómo llamar a esta función, aunque su dirección real se
introducirá en la tabla de importación sólo cuando se inicie el programa. Tenga en cuenta que la función
GetWindowDC, como todas las funciones API de Windows, está escrita de acuerdo al modelo de llamada
stdcall, mientras que el modelo de llamada por defecto de Delphi es register (el modelo de llamada
determina cómo se pasan los parámetros a la función). Por lo tanto, al importar funciones de bibliotecas
estándar, es necesario especificar explícitamente este modelo (hacemos hincapié en que esto se aplica a las
bibliotecas estándar; otras bibliotecas pueden utilizar cualquier otro modelo de llamada, el desarrollador de
la biblioteca es libre de elegir).
Luego se especifica de qué biblioteca se importa la función y qué nombre tiene en la biblioteca. El hecho es
que el nombre de la función en la biblioteca puede ser diferente del nombre con el cual se le da a conocer al
compilador. Esto puede ayudar a resolver conflictos de nombres al importar funciones con el mismo
nombre de diferentes bibliotecas que se dan también en otras situaciones que veremos más adelante. La
principal desventaja de las DLL es que almacenan información sobre los nombres de las funciones, pero no
sobre sus parámetros.
Por lo tanto, si se especifican parámetros erróneos al importar una función, el programa no funcionará
correctamente (incluso se colgará) y ni el compilador ni el sistema operativo podrán identificar el error.
Normalmente, un programa necesita muchas de las funciones API de Windows. Declararlas todas es
bastante tedioso. Afortunadamente, Delphi le ahorra este trabajo: muchas de estas funciones ya están
definidas en ciertos módulos, por lo que basta con escribir el nombre de los módulos en la sección use. Por
ejemplo, las funciones más utilizadas están definidas en los módulos Windows y Messages.

Las funciones API que no están presentes en todas las versiones de Windows se cargan mejor
dinámicamente. Por ejemplo, si un programa importa estáticamente la función
SetLayeredWindowsAttributes, no se ejecutará en Windows 9x, donde esta función no existe y el
sistema abortará el programa si la función esta mencionada en la tabla de importación. Por lo tanto, si
desea que el programa funcione también en Windows 9x, esta función debe importarse dinámicamente.
Tenga en cuenta que el enlazador de Delphi pone en la tabla de importación solo aquellas funciones que
realmente llama el programa. Por lo tanto, tener una declaración SetLayeredWindowsAttributes en el
módulo Windows no impide que el programa se ejecute en Windows 9x si no se llama a esta función.

Para los que decidan trabajar con la API de Windows, es imprescindible una documentación de las
funciones. Hay tantas que es difícil recordarlas todas, y trabajar sin un libro de referencia es imposible.
La principal fuente de información sobre las tecnologías de Microsoft para los desarrolladores es Microsoft
Developer Network (MSDN). Se trata de un sistema de referencia independiente que no se incluye en Delphi.
MSDN se puede adquirir por separado o se puede acceder a él en línea en http://msdn.microsoft.com

2
(acceso gratuito, sin necesidad de registro). MSDN no sólo contiene información sobre la API, también
contiene todo lo que puede necesitar un programador que utilice diversas herramientas de desarrollo de
Microsoft. Además del material de referencia, MSDN incluye la especificación de normas y tecnologías
relacionadas con Windows, artículos de revistas de programación y capítulos de varios libros. Toda esta
información es extremadamente útil para el desarrollador. MSDN se actualiza regularmente y la
información que contiene está siempre al día. En la Figura 1.1 se muestra un ejemplo de ayuda de MSDN.

Figura 1.1. Versión en línea de MSDN (muestra la ayuda de la función DeleteObject).

NOTA:
Tenga en cuenta que MSDN también describe las funciones del sistema operativo Windows CE. La API de
Windows CE es, a primera vista, muy similar a la de Windows, pero existen diferencias entre ambas a veces muy
significativas. Por lo tanto, al utilizar MSDN, no debe seleccionar la sección de Referencia de la API, ya que está
completamente dedicada a la API de WinCE.

Delphi viene con un sistema de ayuda que contiene descripciones de las funciones de la API de Windows. El
sistema de ayuda de Delphi hasta la versión 7 se basaba en archivos hlp. En lo que respecta a la ayuda de
la API de Windows, esto presentaba dos problemas. En primer lugar, los archivos hlp tienen un límite en el
número de secciones del sistema de ayuda, de modo que un archivo de ayuda no puede cubrir tanto la
información de Delphi como la de la API de Windows: los dos archivos de ayuda deben leerse
alternativamente. Para abrir un archivo de ayuda de la API de Windows, sólo tendría que hacer clic en <F1>
en el editor de código y pulsar la tecla <F1>; en este caso la ayuda de la API de Windows estaría presente
en lugar de la ayuda de Delphi. La segunda opción es localizar la carpeta Delphi en el menú Programas y en
ella la carpeta Help\MS SDK Files y seleccionar la sección deseada. También puede abrir manualmente
el archivo MSTools.hlp. En versiones anteriores de Delphi, se encuentra en el directorio
$(Delphi)\NAyuda, en versiones posteriores, debería encontrarse en $(Archivos de programa)\N-
Archivos comunes. La antigua ventana de ayuda se muestra en la Figura 1.2.

El segundo problema con la ayuda basada en el archivo hlp es el hecho de que los desarrolladores de
Delphi, por supuesto, no escribieron la ayuda ellos mismos, sino que utilizaron la proporcionada por
Microsoft. Microsoft publicó la última versión de Ayuda en formato HLP durante un periodo en el que
Windows 95 estaba fuera y Windows NT 4 aún no estaba disponible, por lo que muchas características que
funcionan bien en NT 4 no son compatibles en Windows NT, ya que no lo eran en versiones anteriores. La
ayuda que viene con Delphi 7 (y posiblemente con algunas versiones anteriores) tiene esta información
corregida, pero incluso aquí faltan funciones que sólo están disponibles en Windows NT 4 (como
CoCreateInstanceEx). Y por supuesto, es inútil buscar en esta ayuda funciones que aparecieron en
Windows 98, 2000, XP. En consecuencia, en estas versiones de Delphi ni siquiera se puede discutir si se
debe utilizar la ayuda que acompaña a Delphi o MSND para obtener información sobre la API de Windows.
Por supuesto, hay que seleccionar MSDN. En comparación con MSDN, la única ventaja de la ayuda
suministrada por Delphi es que se puede acceder a ella a través del entorno mediante <F1>. Sin embargo, el
riesgo de obtener información errónea es demasiado grande para ser un argumento de peso. La única
situación en la que es preferible la ayuda proporcionada por Delphi es si no tiene acceso a Internet lo

3
suficientemente rápido como para trabajar con la versión en línea de MSDN y no puede comprar e instalar
una versión sin conexión.

Figura 1.2. Ayuda antigua (basada en archivos hlp) de la API de Windows (Se muestra la función
DeleteObject).

Desde BDS 2006, Borland/CodeGear ha implementado el nuevo sistema de ayuda de Borland (Figura 1.3). Su
interfaz se asemeja mucho a la versión sin conexión de MSDN, y también utiliza archivos del mismo formato,
por lo que ya no existen problemas técnicos de integración de los sistemas de ayuda de Delphi y de la API de
Windows. BDS 2006 integra la ayuda de la API de Windows de 2002-2003 (las distintas secciones tienen
fechas diferentes).
La ayuda de Delphi 2007 contiene información de la API de Windows de 2006, es decir esta es la más
reciente API de Windows. Esto significa que por fin se puede prescindir de MSDN offline para Delphi 2007 y
optar por utilizar la versión online sólo ocasionalmente cuando se quiera conocer los últimos cambios en la
API de Windows (por ejemplo, los cambios introducidos por Windows Vista).

NOTA:
A pesar de la gran calidad de las secciones de la API de Windows de MSDN, a veces también se producen
errores ocasionales que se corrigen con el tiempo. Por lo tanto, si se encuentra con una situación en la que
sospecha que una función de la API de Windows no se comporta como se describe en la ayuda offline, vale la
pena consultar la ayuda online para ver si hay información adicional sobre esa función.

Figura 1.3. Ventana de ayuda de Delphi 2007 (función DeleteObject).

El sistema Windows está escrito en C++, por lo que todas las descripciones de las funciones API de
Windows, así como los ejemplos de su uso, están en este lenguaje (esto se aplica tanto a MSDN como a la
ayuda que viene con Delphi). Pero, sobre todo, hay que entender los tipos de datos. La mayoría de los tipos
que aparecen en la API de Windows se definen en Delphi. La correspondencia entre ellos se muestra en el
Tabla 1.1.
Tabla 1.1. Correspondencia de tipos Delphi con tipos del sistema.

Tipo API de Windows Tipo Delphi


INT INT
UINT LongWord
WORD Word
SHORT SmallInt
USHORT Word
CHAR La mayoría de veces corresponde a Char, pero también puede
interpretarse como ShortInt, ya que no hay diferencia entre los tipos
carácter y entero en C++
UCHAR La mayoría de veces corresponde a Byte, pero también puede
interpretarse como Char
DWORD LongWord

4
BYTE Byte
WCHAR WideChar
BOOL LongBool
int Integer
long LongInt
short SmallInt
unsigned int Cardinal

Los tipos de punteros llevan el prefijo P o LP (puntero o puntero largo; las versiones de 16 bits de Windows
tenían punteros cortos y largos. En las versiones de 32 bits, todos los punteros son largos, por lo que ambos
prefijos tienen el mismo significado). Por ejemplo, LPDWORD es equivalente a ^DWORD, PUCHAR es
equivalente a ^Byte.

A veces el prefijo P o LP va seguido de C, lo que significa que es un puntero a una constante. En C++ es
posible declarar punteros que apuntan a un contenido constante, es decir, el compilador le permite leer el
contenido, pero no modificarlo. En Delphi estos punteros no existen, y estos tipos se sustituyen por punteros
normales, es decir, se ignora el prefijo C.

Los tipos PVOID y LPVOID corresponden a punteros no tipados (Pointer).

El tipo TCHAR es la forma más común de pasar caracteres. Windows admite dos codificaciones: ANSI (1 byte
por carácter) y Unicode (2 bytes por carácter; más adelante hablaremos de la compatibilidad con Windows
Unicode). CHAR corresponde a la codificación de caracteres ANSI, WCHAR a Unicode. Para los programas que
utilizan ANSI, TCHAR es equivalente a CHAR, y WCHAR para Unicode. En Delphi, no hay un equivalente directo
al tipo TCHAR, el propio desarrollador debe averiguar qué tipo de carácter es necesario en una determinada
situación.
Las cadenas en la API de Windows se pasan como punteros a una cadena de caracteres que termina en
cero. Por lo tanto, un puntero a TCHAR puede apuntar a un solo carácter, así como a una cadena. Para
ayudar a determinar qué puntero es cada uno, la API de Windows incluye los tipos LPTCHAR y LPTSTR. Son
equivalentes, pero el primero se usa cuando se requiere un puntero a un solo carácter y el segundo cuando
se requiere una cadena. Cuando se pasa una cadena a una función de sólo lectura, se suele utilizar un
puntero constante, es decir, de tipo LPCTSTR. En Delphi, esto corresponde a PChar para ANSI y PWideChar
para Unicode.
En este punto hay que destacar la peculiaridad de escribir los literales de cadena en C/C++. El carácter \ en
un literal tiene un significado especial: va seguido de uno o más caracteres de control. Por ejemplo, \n
significa salto de línea, \t significa carácter de tabulación, etc. En Delphi, no existe tal secuencia, por lo que,
al traducir los ejemplos de MSDN, se deben escribir explícitamente los códigos de los respectivos
caracteres. Por ejemplo, el literal 'a\nb' en Delphi se convierte en 'a'#13'b'. El símbolo \ puede ir seguido de
un número, en cuyo caso se tratará como un código de carácter, es decir, el literal C/C++ 'a'0b\9' equivale a
'a'#0'b'#9 en Delphi. Si se quiere que el literal de la cadena incluya al propio símbolo \, se duplica, es decir, '\'
en C++ equivale a '\' en Delphi. Además, en los ejemplos de código proporcionados en MSDN, a menudo se
puede ver que los literales de cadena son manejados por las macros TEXT o _T, que se utilizan para unificar
los literales de cadena en las codificaciones ANSI y Unicode. Al traducir dicho código a Delphi, estas macros
pueden simplemente omitirse. Teniendo esto en cuenta, el siguiente código (tomado del ejemplo de Named
pipes):

5
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");

en Delphi se vería así:


var
lpszPipeName: PChar;
...
lpszPipeName := '\\.\pipe\mynamedpipe';

La mayoría de los nombres de tipos de la parte izquierda de la Tabla 1.1 están definidos en el módulo
Windows por motivos de compatibilidad, por lo que son válidos como tipos normales de Delphi. Además de
estos tipos de uso general, existen tipos especiales. Por ejemplo, el descriptor de una ventana es de tipo
HWND, y el primer parámetro del mensaje es de tipo WPARAM (que equivale a Word en los antiguos Windows
de 16 bits y a LongInt en los de 32 bits). Estos tipos especiales también se describen en el módulo
Windows.
Un (record) en C/C++ se llama estructura y se declara mediante la palabra struct. Debido a la forma en
que se describen las estructuras en C, las estructuras en la API de Windows reciben dos nombres: un
nombre principal, formado por las letras principales, usadas a continuación y un nombre auxiliar, que deriva
del nombre principal agregando el prefijo tag. Desde la cuarta versión de Delphi, la convención de
nomenclaturas para estos tipos es la siguiente: el nombre principal y el nombre auxiliar permanecen sin
cambios y se agrega un nuevo nombre que deriva del nombre principal anteponiéndole el popular prefijo T
de Delphi. En la función CreatePenIndirect, por ejemplo, uno de los parámetros es de tipo LOGPEN. Este
es el nombre principal de este tipo y el nombre auxiliar es tagLOGPEN. En consecuencia, la etiqueta
tagLOGPEN y sus sinónimos, LOGPEN y TLogPen, se definen en el módulo Windows. Estos tres
identificadores son intercambiables entre sí en Delphi. El nombre auxiliar se usa raramente y los
desarrolladores, dependiendo de su preferencia personal, pueden elegir el nombre principal o el nombre
prefijado con una T.

Las reglas de denominación de tipos descritas aquí pueden causar cierta confusión al utilizar la VCL. Por
ejemplo, para describir una imagen, la API de Windows define el tipo BITMAP (también conocido como
tagBITMAP). En Delphi, el tipo correspondiente tiene otro nombre, TBitmap. Pero la clase TBitmap,
descrita en el módulo Graphics, tiene el mismo nombre. En el código que Delphi crea automáticamente, el
módulo Graphics en el listado uses está después del módulo Windows, por lo que el identificador
TBitmap es tratado por el compilador como Graphics.TBitmap y no como Windows.TBitmap. Para
usar Windows.TBitmap, debe especificar explícitamente el nombre del módulo o utilizar uno de los
nombres alternativos.
Las versiones anteriores de Delphi tenían diferentes convenciones de nomenclaturas. Por ejemplo, en
Delphi 2 había un tipo BITMAP, pero no había TBitmap y tagBITMAP, y en Delphi 3, de estos tres tipos sólo
había TBitmap.

En la API de Windows las estructuras se describen sin alineación, es decir, el compilador no inserta bytes
sin usar entre los campos, de modo que los límites de los campos están al principio de una palabra doble o
cuádruple.

6
Al describir las estructuras de la API de Windows, es posible que se encuentre con la palabra reservada
union (véase la estructura in_addr, por ejemplo). La combinación de varios campos con esta palabra
significa que todos estos comparten la misma dirección. En Delphi, esto corresponde a entradas variantes
(es decir, el uso de case y record). Las combinaciones de C/C++ son más flexibles que las entradas
variantes de Delphi porque permiten colocar la parte variante en cualquier parte de la estructura, no sólo al
final. Al migrar estas estructuras a Delphi, a veces tenemos que introducir tipos adicionales.
Veamos ahora la sintaxis de la misma descripción de la función en C++ (Listado 1.1).
Listado 1.1. Sintaxis de descripción de funciones en C++.
< Tipo de función > < Nombre de la función > '('
[ < Tipo de parámetro > {< Nombre del parámetro >}
{',' < Tipo de parámetro > {< Nombre del parámetro >} }
]
')';

Como puede ver en el Listado 1.1, al declarar una función, es posible especificar sólo los tipos de parámetros
y no especificar los nombres de los mismos. Sin embargo, esto se considera obsoleto y se utiliza raramente
(excepto para los "parámetros" VOID que se describen a continuación).

Pero recuerda que las mayúsculas y las minúsculas son diferentes en C/C++, así que HDC, hdc, hDC, etc. -
son identificadores diferentes (al autor de C le gustaba mucho la brevedad y quería poder hacer 52 variables
con un nombre de una letra, no 26). Por lo tanto, a menudo se puede encontrar que el nombre de un
parámetro y su tipo distinguen entre mayúsculas y minúsculas. Afortunadamente, al definir una función en
Delphi, no tenemos que almacenar los nombres de los parámetros; lo que cuenta es el tipo y el orden de los
parámetros. Teniendo en cuenta esto, la función descrita en la ayuda como
HMETAFILE CopyMetaFile(HMETAFILE hmfSrc, LPCTSTR lpszFile);

en Delphi tiene la forma


function CopyMetaFile(hmfSrc: HMETAFILE; lpszFile: LPCTSTR): HMETAFILE;

o, lo que es lo mismo,
function CopyMetaFile(hmfSrc: HMETAFILE; lpszFile: PChar): METAFILE;

NOTA:
El compilador de Delphi insiste en que el nombre de un procedimiento o parámetro de función sea el mismo que
su tipo, por lo que veremos más adelante que a veces el nombre del parámetro y su tipo son el mismo, pero se
escriben en registros diferentes, de forma que el prototipo Delphi de una función coincida lo más posible con el
prototipo original de C/C++. Por ejemplo, una variable local de un tipo determinado debe declararse con el
nombre explícito del módulo en el que se declara el tipo.

Un tipo algo diferente es VOID (o vacío, que es lo mismo, pero en la API de Windows este identificador
ocurre con mucha menos frecuencia). Si una función es de este tipo, se describe en Pascal como un

7
procedimiento. Si una función tiene VOID entre paréntesis en lugar de parámetros, significa que la función
no tiene parámetros. Por ejemplo, la función
VOID CloseLogFile(VOID);

en Delphi se describe como


procedure CloseLogFile;

NOTA:
El lenguaje C++, a diferencia de C, permite la declaración de funciones sin parámetros, es decir, la función
CloseLogFile puede declararse de la siguiente forma VOID CloseLogFile();. En C++, estas variantes son
equivalentes, pero en la API de Windows, la variante sin parámetro explícito es mucho más rara debido a la
incompatibilidad con C.

Cuando un tipo de parámetro es un puntero a otro tipo (que suele empezar por LP), se puede utilizar un
parámetro variable cuando se describe esta función en Delphi, porque en este caso se le pasa un puntero a
la función. Por ejemplo, la función
int GetRgnBox(HRGN hrgn, LPRECT lprc);

en el módulo de Windows se describe como


function GetRgnBox(RGN: HRGN; var p2: TRect): Integer;

Esto es útil si el valor del parámetro no puede ser un puntero nulo, porque no es posible pasar tal puntero
cuando se utiliza var. Un puntero nulo en C/C++ es NULL. NULL y 0 son intercambiables en estos lenguajes,
por lo que puedes encontrar una indicación en la ayuda de que puede ser NULL incluso para un parámetro
entero.
Finalmente, si no puede entender cómo la función descrita en la Ayuda, debe ser traducida a Pascal, puede
intentar encontrar una descripción de la función en los módulos de código fuente que vienen con Delphi.
Estos módulos se encuentran en el directorio $(DELPHI){Source\RTL\Win (antes de Delphi 7) o
$(BDS){Source\Win32\RTL\Win (BDS 2006 y superior). También puede utilizar el tooltip que aparece en el
editor de Delphi después de escribir el nombre de la función.
Si miras la ayuda de la función GetSystemMetrics, por ejemplo, puedes ver que esta función debe tener
un único parámetro entero. Sin embargo, al llamar a la función, la ayuda sugiere que el parámetro debe ser
SM_ARRANGE, SM_CLEANBOOT, etc. en lugar de números. La situación es similar para muchas otras
funciones de la API de Windows. Estos SM_ARRANGE, SM_CLEANBOOT, etc. son todos nombres de constantes
numéricas. Estas constantes se describen en el mismo módulo en el que se describe la función que las
utiliza, por lo que no es necesario averiguar los valores numéricos de estas constantes, sino especificar sus
nombres al llamar a la función, por ejemplo, GetSystemMetrics(SM_ARRANGE);.

Si, por alguna razón, aún necesita averiguar los valores numéricos, no los busque en el sistema de ayuda:
no están allí. Se pueden encontrar en el código fuente de los módulos Delphi en los que se describen estas
constantes. Por ejemplo, puede ver en Windows.pas que SM_ARRANGE = 56.

En el archivo de ayuda que acompaña a Delphi hasta la versión 7 inclusive, verá tres enlaces en la parte
superior de muchas funciones de la API de Windows: QuickInfo, Overview y Group. QuickInfo da una

8
breve información sobre la función: en qué biblioteca está implementada, en qué versiones de Windows
funciona, etc. (le recuerdo que la información de la versión en esta ayuda debe ser tratada con mucha
cautela). Overview presenta una descripción general de algún tema importante. Por ejemplo, para
cualquier función que trabaje con mapas de bits, en Overview se explicará por qué estos mismos mapas de
bits son necesarios en principio y cómo funcionan. La página vinculada a Overview generalmente contiene
información muy concisa, pero al hacer clic en el botón >> en la parte superior de la ventana puede continuar
con Overview. Por último, Group. Este enlace le lleva a un listado de todas las funciones relacionadas.

Por ejemplo, para la función CreateRectRgn, el grupo será todas las funciones relacionadas con regiones.
Si ahora pulsa el botón << aparecerá una página con una breve descripción de las posibles aplicaciones de
los objetos con los que operan las funciones (en el ejemplo anterior, una descripción de las posibilidades de
regiones). Para leerlos en una secuencia normal, lo mejor es pulsar el botón << tantas veces como sea
posible y luego ir en la dirección opuesta con el botón >>.
MSDN (así como la Ayuda de BDS 2006 y superior) ofrece aún más información útil. En la parte inferior de la
descripción de cada función hay una sección Requirements, que le indica qué biblioteca y versión de
Windows es necesaria para utilizarla. En la parte inferior de la descripción de la función se encuentran el
enlace See also. El primer enlace es un resumen del tema correspondiente (por ejemplo, para la función
CreateRectRgn ya mencionada, se llama Regions Overview). El segundo enlace es un listado de
funciones relacionadas (Region Functions en este caso). Esto le conduce a una página donde se listan
todas las funciones relacionadas con la función seleccionada. Después de estos dos enlaces obligatorios
hay enlaces a descripciones de funciones y tipos que se usan comúnmente junto con esta función.
Los tipos, constantes y funciones básicas de la API de Windows se declaran en los módulos Windows y
Messages. Sin embargo, muchas funciones se declaran en otros módulos que no están incluidos en el
programa por defecto, por lo que el desarrollador debe averiguar por sí mismo en qué módulo se encuentra
el identificador requerido, y añadirlo. Por supuesto, ni la ayuda proporcionada con Delphi ni MSDN pueden
ofrecerle la información necesaria. Para saber en qué módulo se declara el identificador requerido, puede
buscar en todos los archivos con extensión pas en la carpeta de código fuente de los módulos estándar. Por
ejemplo, este método podría utilizarse para averiguar que la popular función ShellExecute se encuentra
en el módulo ShellAPI y que CoCreateInstance está en un módulo ActiveX (y también en el módulo
Ole2, que está diseñado para ser compatible con versiones anteriores de Delphi).

Unas palabras más sobre las constantes numéricas. Puede ver números como 0xC56F o 0x3341 en la ayuda.
El prefijo 0x en C/C++ representa un número hexadecimal. En Delphi, sustitúyalo por $, es decir, estos
números deben escribirse como $C56F y $3341 respectivamente.

Cuando programamos en Delphi nos acostumbramos rápidamente al hecho de que cada objeto esta
implementado por una instancia de una determinada clase. Por ejemplo, una instancia de la clase TButton
implementa un botón y la clase TCanvas implementa un dispositivo de contexto de Windows. Cuando se
crearon las primeras versiones de Windows, la programación orientada a objetos aún no estaba
generalmente aceptada, por lo que no se implementó. Las versiones modernas de Windows han heredado
parte de esta deficiencia, por lo que en la mayoría de casos hay que trabajar "a la antigua" sobre todo
porque las DLL sólo pueden exportar funciones, no clases. Cuando hablamos de objetos creados a través de

9
la API de Windows, no nos referiremos a objetos en términos de POO, sino a una entidad cuya estructura
interna se nos oculta, por lo que sólo podemos tratar con esta entidad como una entidad única e indivisible.
A cada objeto creado mediante la API de Windows se le asigna un número único (un descriptor). Su valor
específico no tiene ninguna información útil para el desarrollador y solo se usa para identificar al objeto con
el cual se realiza la operación cuando se llama a una función API de Windows. En la mayoría de casos los
descriptores son números de 32 dígitos, lo que significa que se pueden transferir a donde sea se requieran.
Como veremos más adelante la API de Windows manipula con cierta libertad tipos de datos, es decir, el
mismo parámetro en diferentes situaciones puede ser un número, un puntero o un descriptor, por lo que
conocer la representación binaria del descriptor sigue siendo útil para el desarrollador (aunque si Windows
estuviera "diseñado según las reglas" el tipo del descriptor no habría sido en absoluto interés del
desarrollador). Así, la principal diferencia entre los métodos de clase y las funciones API de Windows es que
los primeros están asociados a una instancia de clase a través de la cual son llamados y, por lo tanto, no
requieren de una referencia explícita al objeto, mientras que las funciones API requieren que se apunte a un
objeto a través de un descriptor, ya que no están asociados a ningún objeto (en términos del sistema).
NOTA:
Normalmente una rutina de creación de objetos en Windows devuelve el descriptor (handle) del objeto. Un
descriptor se define como un valor que identifica de forma única a un objeto, o una referencia indirecta a un
objeto. Para ser más precisos, un descriptor es un valor que tiene una correspondencia uno a uno con un objeto.
Un objeto puede ser asignado a un único descriptor y un descriptor puede ser asignado a un único objeto.

Los componentes de la VCL suelen ser objetos envoltorios. En este caso tienen una propiedad
(normalmente llamada Handle) que contiene al descriptor del objeto correspondiente. A veces, una clase
Delphi encapsula múltiples objetos Windows. Por ejemplo, la clase TBitmap incluye a HBITMAP y
HPALETTE - la imagen y su paleta. En consecuencia, almacena dos descriptores en las propiedades Handle
y Palette.

Hay que tener en cuenta que los mecanismos internos de la VCL no se activan si un objeto se modifica a
través de la API de Windows. Por ejemplo, si una ventana se oculta no a través del método Hide sino a
través de una llamada a la función API de Windows ShowWindow(Handle, SW_HIDE), no se producirá el
evento OnHide porque este se desencadena por los mecanismos internos de la VCL. Pero tales
malentendidos generalmente ocurren solo cuando las funciones API de Windows duplican lo que también se
puede hacer con la VCL.
Todas las instancias de clase creadas en Delphi deben eliminarse. En algunos casos, esto se hace de forma
automática, y a veces el desarrollador tiene que encargarse él mismo de "la recolección de basura". La
situación es similar para los objetos creados en la API de Windows. Si mira la ayuda de cualquier función
que crea un objeto, le indicará qué función puede utilizar para eliminar el objeto, y si tiene que hacerlo
manualmente o el sistema lo hará automáticamente. En muchos casos, se pueden eliminar objetos
completamente diferentes con la misma función. Por ejemplo, la función DeleteObject elimina plumas,
brochas, fuentes, regiones, mapas de bits y paletas. Cuidado con las excepciones. Por ejemplo, el sistema no
elimina automáticamente las regiones, pero si llama a la función SetWindowRgn para una región, esta pasa
a ser propiedad del sistema operativo y a partir de entonces no podrá realizar más operaciones, incluida la
eliminación de la región.

10
Si el objeto del sistema sólo lo utiliza una aplicación, se borrará cuando la aplicación termine. Sin embargo,
un buen estilo de programación requiere que el programa elimine los objetos explícitamente y no dependa
del sistema para hacerlo.

La palabra "ventana" se suele utilizar para describir un formulario, similar a los creados con la clase TForm.
Sin embargo, el término es un concepto mucho más amplio. En general, una ventana es cualquier objeto que
tenga coordenadas de pantalla y responda al ratón y al teclado. Por ejemplo, un botón, que se crea con la
clase TButton, también es una ventana.

La VCL introduce cierta confusión en esta noción. Algunos componentes visuales de la VCL no son ventanas;
sólo las imitan, como TImage, por ejemplo. Esto ahorra recursos del sistema y aumenta la velocidad del
programa. El mecanismo de esta simulación lo discutiremos más adelante; por ahora, conviene recordar
que sólo son ventanas aquellos componentes visuales que tienen de ancestro a la clase TWinControl. Los
desarrolladores de la VCL se aseguraron de que la diferencia entre los componentes visuales con y sin
ventana fuera mínima. De hecho, a primera vista, un TLabel sin ventana y un TStaticText con ventana
parecen ser casi gemelos. La diferencia se hace evidente cuando se utiliza la API de Windows. Los
componentes que no son de ventana sólo pueden ser manipulados por la VCL y ni siquiera tienen una
propiedad Handle, mientras que los componentes de ventana pueden ser manipulados por la API de
Windows.
Observe una diferencia más entre los componentes que son de ventana y aquellos que no lo son, estos
últimos se dibujan directamente sobre la superficie del componente padre, mientras que los componentes
con ventana se "colocan" encima del padre. Esto significa, por ejemplo, que el componente que no es de
ventana TLabel sobre un formulario no puede cubrir parte de un TButton, porque TLabel se dibuja
encima del formulario y un botón es un objeto independiente que se encuentra encima del formulario y tiene
su propia superficie. TStaticText puede aparecer encima del botón porque también está encima del
formulario.

NOTA:
Para colocar un componente visual que no es de ventana sobre un componente de ventana, si es necesario,
puede hacer lo siguiente. Colocar un panel (TPanel) en el formulario - es un componente de ventana y puede
colocarse encima de otros componentes de ventana. El panel puede utilizarse para colocar cualquier
componente visual que no sea de ventana; no se dibujará sobre el formulario, sino sobre el panel. Si ahora
elimina el marco del panel y lo reduce al tamaño del componente que no es de ventana, el panel se volverá
invisible y en conjunto parecerá que el componente que no es de ventana está por encima del componente de
ventana.

Cada ventana pertenece a un tipo de ventana. La clase de la ventana no debe confundirse con las clases de
Delphi. La clase de la ventana es una plantilla que define las propiedades básicas de una ventana. Cada una
de estas plantillas recibe un nombre único en su ámbito. Antes de poder utilizar una de estas clases, hay
que registrarlas (función RegisterClassEx). Esta función toma como parámetro un registro de tipo
TWndClassEx, cuyos campos contienen los parámetros de la clase.

11
Cada ventana está asociada a una función especial llamada procedimiento de ventana (lo discutiremos con
más detalle más adelante). No es un parámetro de una sola ventana, sino de toda una clase de ventanas, es
decir, todas las ventanas pertenecientes a una clase determinada utilizarán el mismo procedimiento de
ventana. Este procedimiento puede estar ubicado en el propio módulo ejecutable o en una de las DLL
cargadas. Cuando se crea una clase, se especifica el descriptor del módulo en el que se encuentra el
procedimiento de ventana.

NOTA:
Aquí hay cierta confusión de términos. En la ayuda en inglés, aparece la palabra module, que se refiere a un
archivo asignado al espacio de direcciones de un proceso, es decir, básicamente el archivo exe que generó el
proceso y las DLL cargadas por éste. Y está la palabra unit, que significa módulo en Delphi, que también se
traduce como módulo. Anteriormente, hemos hablado de los módulos como archivos mapeados en el espacio de
direcciones es decir tienen descriptores. Los módulos Delphi no son objetos del sistema y no tienen
descriptores.

El descriptor de un módulo cargado en memoria se puede recuperar mediante la función


GetModuleHandle. La función LoadLibrary también devuelve el descriptor de la DLL cargada cuando se
retorna satisfactoriamente. Además, Delphi proporciona dos variables: MainInstance del módulo System
y HInstance del módulo SysInstance (ambas se incluyen automáticamente sin ser definidas
explícitamente en el listado uses). MainInstance contiene el descriptor del archivo exe que generó el
proceso, y HInstance contiene el descriptor del módulo actual. En un archivo ejecutable, MainInstance y
HInstance son iguales entre sí; en una DLL, HInstance contiene el descriptor de la propia biblioteca y
MainInstance contiene el descriptor del módulo principal que la cargó.

Cada ventana en Windows está asociada a un módulo (en Windows 9x/ME hay que especificar explícitamente
el descriptor del módulo, NT/2000/XP define el módulo desde dónde se llama automáticamente a la función
que crea la ventana). Las clases de ventanas se dividen en locales y globales: las ventanas de las clases
locales sólo pueden ser creadas por el módulo que alberga el procedimiento de ventana de la clase,
mientras que las ventanas de las clases globales pueden ser creadas por cualquier módulo de una
determinada aplicación. Que una clase sea local o global depende de los valores del campo TWndClassEx
al registrar la clase.
La clase de ventana a la que pertenece una ventana se especifica al crearla. Puede ser una clase
previamente registrada o una de las clases del sistema. Las clases del sistema son 'BUTTON',
'COMBOBOX', 'EDIT', 'LISTBOX', 'MDICLIENT', 'SCROLLBAR' y 'STATIC'. El propósito de estas
clases está claro por sus nombres (la clase 'STATIC' implementa elementos gráficos o de texto estáticos,
es decir, que no responden al ratón y al teclado, pero tienen un descriptor). Además de estas clases,
también están las clases de la biblioteca ComCtl32.dll, que están disponibles para todas las aplicaciones
sin necesidad de registrarlas previamente (se puede encontrar más información sobre estas clases en
MSDN, en Common Controls Reference).
No hay clases preparadas para las ventanas en el sentido convencional de la palabra; tiene que registrarlas
usted mismo. Generalmente, la VCL para los formularios registra clases de ventanas cuyos nombres son los
mismos que las clases VCL correspondientes.

12
Además del nombre, la clase incluye otros parámetros, como estilo, pincel, etc. En la ayuda se detallan
estos aspectos.
Las funciones CreateWindow y CreateWindowEx se usan para crear una ventana. Cuando se crea una
ventana, otros parámetros incluyen el módulo al que se asigna la ventana, el nombre de la clase de la
ventana, el estilo y el estilo extendido. Los dos últimos parámetros definen el comportamiento de una
ventana concreta y no tienen nada en común con el estilo de la clase. El resultado de estas funciones es el
descriptor de la ventana que se crea.
Otro parámetro importante de estas funciones es el descriptor de la ventana padre. Una ventana está a
cargo de su padre. Por ejemplo, si una ventana hija es un botón u otro control, entonces se encuentra
visualmente en otra ventana, que es su ventana padre. Si la ventana hija es una MDIChild, es padre de
MDIForm (estrictamente hablando, no es MDIForm en sí, sino una ventana especial de la clase MDICLIENT
que es hija de MDIForm; el handle de la ventana se almacena en la propiedad ClientHandle del
formulario principal). La relación padre-hijo, en otras palabras, define lo que es una ventana para otra, la
relación visual entre ambas. Para ventanas con un padre no especificado (es decir, un descriptor del padre
pasado como cero), la ventana se encuentra directamente en el escritorio. Si se especifica el estilo
WS_CHILD al crear la ventana, las coordenadas de la ventana se referencian desde la esquina superior
izquierda del área de cliente de la ventana padre y cualquier ventana hija se desplazará cuando se mueva la
ventana padre.
Una ventana que tiene el estilo WS_CHILD no puede ser colocada en el escritorio; un intento de crear tal
ventana fallará.
Los componentes visuales de la VCL tienen dos propiedades que a veces se confunden: Owner y Parent. La
propiedad Parent indica un objeto que implementa la ventana que es el objeto padre para este componente
visual (los componentes que no están relacionados con TWinControl también tienen esta propiedad, VCL
imita esta relación para ellos, pero no pueden ser padres de otros componentes visuales). La propiedad
Owner indica el propietario del componente. La relación propietario-propiedad se implementa
completamente dentro de la VCL. Todos los descendientes de TComponent tiene una propiedad Owner,
incluidos los componentes no visuales, y el propietario de otros componentes también puede ser un
componente no visual (por ejemplo, TDataModule). Cuando se destruye un componente, se destruyen
automáticamente todos los componentes de los cuales es propietario (sin embargo, hay un cierto
solapamiento de funciones aquí, porque un componente de ventana también destruye todos los
componentes visuales de los que este es el padre). El propietario también es responsable de cargar todas
las propiedades de los componentes que posee y que se establecieron durante el proceso.
La propiedad Owner es de sólo lectura. El propietario se establece una vez que se llama al constructor y
permanece sin cambios durante todo el ciclo de vida del componente (excepto en los casos raros cuando se
llaman explícitamente a los métodos InsertComponent y RemoveComponent). La propiedad Parent se
define por separado y puede cambiarse posteriormente (visualmente, esto parecerá un componente que
"salta" de una ventana a otra).
Un componente visual puede no tener dueño. Esto significa que el desarrollador que lo creó es responsable
de eliminarlo. Pero la mayoría de los componentes visuales no pueden funcionar si no se especifica la
propiedad Parent. Por ejemplo, no es posible mostrar un componente TButton que no tenga establecida la
propiedad Parent. Esto se debe a que la mayoría de los componentes de ventanas tienen el estilo

13
WS_CHILD, que, de nuevo, no permite colocar una ventana en el escritorio. Sólo los descendientes de
TCustomForm pueden ser ventanas sin padre.

Sin embargo, puede utilizar la API de Windows para crear un botón sin un padre. Por ejemplo, con estas
instrucciones (véase el Listado 1.2).
Listado 1.2. Creación de un botón que no tiene ventana padre.
CreateWindow('BUTTON', 'Test',
WS_VISIBLE or BS_PUSBUTTON or WS_POPUP, 10, 10, 100, 50, 0, 0, HInstance, nil);
Recomendamos eliminar el estilo WS_POPUP en este ejemplo y ver lo que sucede - el efecto es bastante
divertido. Tenga en cuenta que no tiene sentido crear estos botones colgantes por sí mismos, porque los
eventos que se producen en los controles estándar son recibidos por la ventana padre, y en su ausencia, el
programa no puede responder a un clic de botón, por ejemplo.
Además del constructor normal Create, la clase TWinControl tiene un constructor CreateParented
que permite crear componentes de ventana cuyos padres son ventanas creadas sin usar la VCL. A este
constructor se le pasa como parámetro el descriptor de la ventana padre. Los componentes creados de esta
forma no necesitan tener establecida la propiedad Parent.

NOTA:
Para aumentar la confusión entre padre y propietario, MSDN también utiliza los términos owner y owned
(propietario y propiedad) en relación con las ventanas, pero no tiene nada que ver con propietario en términos
de la VCL. Si una ventana tiene el estilo WS_CHILD, entonces debe tener un padre, pero no puede tener un
propietario. Si una ventana no tiene este estilo, no puede tener un padre, pero puede tener (aunque no es
necesario) un propietario. El propietario en este caso es la ventana cuyo manipulador se pasa como padre, es
decir, el padre y el propietario en términos del sistema son lo mismo, que se interpreta de forma diferente
según el estilo de la ventana. Una ventana con propietario se destruye cuando el propietario se destruye, se
oculta cuando el propietario se minimiza y siempre está por encima del propietario. Una ventana que tiene un
estilo WS_CHILD puede ser padre, pero no puede ser propietario de otra ventana; si se pasa el manipulador de
dicha ventana como propietario, el verdadero propietario será el padre de la ventana hija. Para evitar la
confusión de propietario en términos de la VCL y del sistema, siempre especificaremos en qué sentido se
referirá la palabra "propietario" en el futuro.

La creación de ventanas a través de la API de Windows requiere mucho trabajo. La VCL es muy buena para
esto, así que sólo tiene que crear las ventanas usted mismo si no quiere usar la VCL, por ejemplo, si quiere
escribir una aplicación lo más compacta posible. En todos los demás casos, sólo hay que retocar un poco la
VCL. Por ejemplo, puede utilizar la API de Windows para cambiar la forma de la ventana o para eliminar la
barra de título y dejar el marco. Tales acciones no requieren que el desarrollador cree una nueva ventana;
simplemente puede usar la que ya ha creado la VCL.
Otro caso en el que las funciones de ventana de la API de Windows pueden ser necesarias es cuando una
aplicación necesita hacer algo con las ventanas de otros desarrolladores. Por ejemplo, puede limitarse a
enumerar todas las ventanas abiertas en ese momento, como hace la utilidad WinSight32 de Delphi. Pero
en este caso, tampoco tiene que crear las ventanas usted mismo, el trabajo va con las existentes.

14
Antes de seguir adelante, hay que saber qué son las funciones callback (a veces también traducidas como
"funciones de llamadas indirectas"). Estas funciones se incluyen en el programa, pero por lo general no se
llaman directamente, aunque nada impide que lo hagan. En ese sentido, las funciones callback son similares
a los métodos de clase asociados a los eventos.
Por ejemplo, nada impide llamar directamente al método FormCreate, pero es inusual. Por otro lado,
aunque no se llame directamente a este método, se ejecuta igual, porque la VCL lo llama automáticamente
sin necesidad de una instrucción directa del desarrollador. Otra característica de este método, de llamada
indirecta, es que su nombre específico no es importante. Puede cambiarse, pero si este método sigue
vinculado al evento OnCreate, aun será llamado. La única diferencia es que estos métodos son llamados
por los mecanismos internos de la VCL, y las funciones callback son llamadas por el propio sistema de
Windows. Los requisitos para estas funciones son: en primer lugar, tienen que ser funciones y no métodos;
en segundo lugar, tienen que estar escritas mediante el modelo stdcall (MSDN sugiere utilizar el modelo
callback, que es sinónimo de stdcall en las versiones actuales de Windows). En cuanto a la forma en
que el programador le indica al sistema que ha escrito una función callback, será diferente en cada caso.

Como ejemplo, considere enumerar las ventanas actuales mediante la función EnumWindows. Su
descripción en la Ayuda es la siguiente:
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);

En consecuencia, en el modulo windows tendrá el siguiente formato:


function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;

El parámetro lpEnumFunc debe contener al puntero de la función callback. El prototipo de esta función
se describe a continuación:
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);

No existe ninguna función con este nombre en la API de Windows. Se trata del así llamado prototipo de la
función, según el cual se describe a la función callback. De hecho, este prototipo ofrece más libertad del
que parece a primera vista. Primero, el nombre puede ser cualquier nombre. En segundo lugar, el sistema
no impone restricciones estrictas en cuanto a los nombres y tipos de los parámetros; pueden ser cualquier
nombre y tipo de parámetro, siempre que los nuevos tipos coincidan en tamaño con los especificados (el
tipo TFNWndEnumProc declarado en el módulo windows no es un tipo procedimental, sino simplemente un
puntero no tipado, así que el compilador de Delphi no controlará si la función callback pasada se
corresponde con su prototipo). En cuanto al tipo de la función y al tipo del primer parámetro, tienen cierto
significado, y cambiarlos difícilmente puede resultar útil. En cambio, el segundo parámetro está diseñado
específicamente para pasar un valor que el desarrollador puede usar a voluntad, el sistema simplemente
pasa a través de la función callBack el valor que tenía el parámetro LParam al llamar a la función
EmumWindows. Un desarrollador puede encontrar más conveniente trabajar no con el tipo LPARAM (es decir,
LongInt), sino, por ejemplo, con un puntero o una matriz de cuatro bytes. Siempre que sean cuatro bytes y
no ocho, dieciséis o cualquier otro número. Incluso puede convertir este parámetro en un parámetro
variable, ya que pasará los mismos cuatro bytes – la dirección de la variable. Sin embargo, quienes no estén
muy familiarizados con la forma en que se usa la pila para transferir parámetros en los diferentes modelos
de llamada, mejor no experimentar cambiando el tipo del parámetro, y siga estrictamente el prototipo
declarado, si es necesario, realizando las conversiones necesarias dentro de la función callBack.

15
La función EnumWindows trabaja de la siguiente forma: tras una llamada, comienza a iterar a través de
todas las ventanas de nivel superior disponibles actualmente, es decir, por aquellas que no tienen padre.
Para cada una de estas ventanas, se llama a la función callBack correspondiente, como primer parámetro
se le pasa el descriptor de la ventana (cada vez, por supuesto, uno diferente) y como segundo parámetro lo
que se le pasó a la función EnumWindows propiamente dicha en su segundo parámetro (cada vez lo mismo).
Al recibir uno por uno los descriptores de todas las ventanas de nivel superior, la función callBack puede
realizar una determinada acción en cada una de ellas (cerrar, minimizar, etc.). O alternativamente, verificar
en todas ellas el cumplimiento de una determinada condición, tratando de encontrar la correcta.
El valor devuelto por la función callback afecta al funcionamiento de EnumWindows. Si devuelve False,
entonces todo lo que hay que hacer está hecho, por lo que no es necesario pasar por el resto de ventanas.
El código final para el caso cuando el segundo parámetro es de tipo Pointer se ilustra en el listado 1.3.

Listado 1.3. Llamada a la función EnumWindows con una función callback.


function MyCallbackFunction(Wnd: HWND; P: Pointer): BOOL; stdcall;
begin
{ hace algo }
end;
..............
var
MyPointer: Pointer;
..............
EnumWindows(@MyCallbackFunction, LongInt(MyPointer));

Hagamos lo que hagamos con el tipo del segundo parámetro de la función callback, no cambiará el tipo
del parámetro correspondiente en EnumWindows. Por lo tanto, necesitamos convertir explícitamente el
parámetro pasado, al tipo LongInt. La conversión de tipo inversa al llamar a MyCallbackFunction es
automática. El uso de EnumWindows y las funciones callback se muestran en el ejemplo EnumWnd.

Tenga en cuenta que las funciones callback serán llamadas antes de que la función EnumWindows
termine. Sin embargo, no se trata de un trabajo en paralelo. Para ilustrarlo, considere la situación en la que
un programa llama a una función A, que a su vez llama a la función B. Obviamente, la función B comenzará
antes de que la función A termine. Lo mismo ocurrirá con la función callback en EnumWindows: será
llamada desde el código de EnumWindows de la misma forma que la función B es llamada desde el código
de la función A. Por lo tanto, el código de la función callback recibirá el control (y más de una vez, porque
EnumWindows llamará a esta función en un bucle) antes de que la función EnumWindows termine.

Sin embargo, esta regla no se aplica en todas las situaciones. En algunos casos, el sistema recuerda la
dirección de la función callback que se le pasó, para utilizarla posteriormente. Un ejemplo de este tipo de
función es el procedimiento de ventana: su dirección se pasa al sistema cuando se registra la clase y luego
el sistema llama repetidamente a esta función si es necesario.
En las versiones de 16bits de Windows, llamar a funciones callback se complicaba ya que requería de un
código especial llamado prólogo. El prólogo se creaba con la función MakeProcInstance y se
eliminaba tras su finalización con FreeProcInstance. Por lo tanto, una llamada a EnumWindows tenía que
ser como el Listado 1.4.
Listado 1.4. Llamada a la función EnumWindows en versiones de 16 bits de Windows.

16
var
MyProcInstnace: TFarProc;
...................
MyProcInstance := MakeProcInstance(@MyCallbackFunction, HInstance);
EnumWindows(MyProcInstance, LongInt(MyPointer));
FreeProcInstance(MyProcInstance);

En Delphi, este código funciona, porque MakeProcInstance y FreeProcInstance se mantiene por


compatibilidad. Pero hacen nada (tal como se puede comprobar mirando el archivo fuente windows.pas),
así que puede prescindir de ellas. Sin embargo, estas funciones a veces se siguen utilizando, probablemente
por costumbre. Otra forma de hacer un prólogo en versiones de 16bits es declarar una función con la
directiva export. Esta directiva se mantiene por compatibilidad en Delphi, pero también hace nada en las
versiones de32 bits (aunque en la Ayuda para, por ejemplo, Delphi 3 dice lo contrario; la Ayuda para Delphi 4
ya no comete este error).

Para alguien familiarizado con Delphi, el esquema de control de eventos debería estar claro. El
desarrollador escribe sólo métodos en respuesta a varios eventos y luego este código se hace cargo cuando
se produce el evento correspondiente. Los programas simples de Delphi consisten enteramente de métodos
para procesar eventos (como OnCreate, OnClick y OnCloseQuery). Un evento no es sólo un evento en el
sentido habitual de la palabra, es decir, cuando sucede algo externamente; sino también una situación en la
que un evento se usa simplemente para transferir el control al código escrito por el desarrollador del
programa, en los casos cuando la VCL no pueda procesar una tarea por su cuenta.
Un ejemplo de este evento es TListBox.OnDrawItem. Al establecer el estilo del listado en
lbOwnerDrawFixed o lbOwnerDrawVariable, el desarrollador indica que necesita un aspecto no
estándar para los elementos del listado, por lo que se encarga de dibujarlos. Y cada vez que surge esta
misma necesidad, la VCL trasfiere el control a un código especialmente escrito. De hecho, la diferencia entre
los dos tipos de eventos es muy arbitraria. Se puede decir que cuando el usuario pulsa una tecla, la VCL no
"sabe" qué hacer, y por lo tanto trasfiere el control al gestor OnKeyPress.

La gestión de eventos no es un invento de Delphi. Es un enfoque del sistema Windows. Sólo que aquí, los
eventos se llaman mensajes, lo que a veces es aún más reflexivo. Windows envía mensajes al software
porque se ha producido un evento externo (clic del ratón, tecla del teclado, etc.) o porque el propio sistema
necesita que el software haga algo. La acción más común es el suministro de información. Por ejemplo,
cuando se necesita conocer el texto del título de una ventana, Windows envía un mensaje especial a la
ventana y ésta tiene que decirle al sistema su título como respuesta. También hay mensajes que
simplemente notifican al programa el inicio de una acción (por ejemplo, una acción de arrastrar y soltar) y le
dan la oportunidad de intervenir. Pero esta intervención es opcional.
En Delphi, se suele crear un método para reaccionar a cada evento. En Windows, un único procedimiento
llamado procedimiento de ventana, gestiona todos los mensajes dirigidos a una ventana en particular. (En
C/C++, no existe el concepto de "procedimiento", allí el término "procedimiento de ventana" no causa
confusión, pero en Delphi, está claramente definido qué es un procedimiento. Esto puede resultar confuso: lo
que el sistema llama procedimiento de ventana no sería un procedimiento desde el punto de vista de Delphi,
sino una función. Sin embargo, utilizaremos el término convencional "procedimiento de ventana"). Cada
mensaje tiene un número único, y el procedimiento de ventana suele consistir enteramente de una
declaración case, y cada mensaje tiene una alternativa diferente a la declaración case. No es necesario que

17
conozca los números de los mensajes porque puede utilizar las constantes descritas en el módulo
Messages. Estas constantes comienzan con un prefijo que indica que el mensaje pertenece a un grupo. Por
ejemplo, los mensajes de propósito general comienzan con WM_: WM_PAINT, WM_GETTEXTLENGTH. Los
mensajes específicos de los botones, por ejemplo, comenzarán con el prefijo BM_. Los restantes grupos de
mensajes también están asociados o bien a determinados controles o bien a acciones especiales, por
ejemplo, Los otros grupos de mensajes también tienen que ver con otros elementos del control o con
acciones especiales, por ejemplo, con el intercambio dinámico de datos (DDE). Un programa típico tiene que
procesar muchos mensajes, por lo que el procedimiento de ventana tiende a ser muy largo y engorroso. El
procedimiento de ventana es definido por el desarrollador como una función callback y se especifica
cuando se crea la clase de la ventana. Así, todas las ventanas de una determinada clase tienen el mismo
procedimiento de ventana. Sin embargo, es posible crear una subclase, es decir, una nueva clase que
hereda todas las propiedades de una clase existente excepto el procedimiento de ventana. Esto se explicará
con más detalle a continuación.
Además del número, cada mensaje contiene dos parámetros: wParam y lParam.

Los prefijos w y l significan "Word" y "Long", es decir, el primer parámetro es de 16 bits y el segundo de 32
bits. Sin embargo, esto sólo ocurría en las versiones antiguas de 16 bits de Windows. En las versiones de 32
bits, ambos parámetros son de 32 bits, a pesar de sus nombres. El significado exacto de cada parámetro
depende del mensaje. En algunos mensajes uno o ambos parámetros pueden no utilizarse en absoluto, en
otros, por el contrario, incluso faltan dos parámetros. En este caso, uno de los parámetros (normalmente
lParam) contiene un puntero a datos adicionales. El procedimiento de ventana tiene que devolver un valor
después de procesar el mensaje. Por lo general, este valor simplemente indica que el mensaje no necesita
procesamiento adicional, pero en algunos casos es más significativo, por ejemplo, WM_SETICON debe
devolver un identificador a un icono que se estableció anteriormente. El procedimiento de ventana prototipo
es el siguiente:
LRESULT CALLBACK WindowProc(
HWND hwnd, // descriptor de la ventana
UINT uMsg, // número del mensaje
WPARAM wParam, // primer parámetro del mensaje
LPARAM lParam // segundo parámetro del mensaje
);
En Delphi, un procedimiento de ventana se declara como:
function WindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM):
LRESULT; stdcall;
Todo lo que una ventana "puede hacer" está determinado por cómo su procedimiento de ventana responde a
los mensajes. Para que una ventana pueda ser arrastrada, por ejemplo, su procedimiento de ventana debe
procesar una cantidad de mensajes relacionados con el mouse. Para no obligar al programador a
implementar un gestor de eventos estándar para todas las ventanas cada vez, el sistema proporciona la
función DefWindowProc. El desarrollador de la aplicación en su procedimiento de ventana debe
proporcionar sólo el procesamiento de mensajes específicos para esta ventana y transferir el
procesamiento de todos los demás mensajes a esta función. También hay análogos de la función
DefWindowProc para ventanas especializadas: DefDlgProc para ventanas de diálogo, DefFrameProc
para ventanas padre MDI, DefChildMDIProc para ventanas hijo MDI.

18
Se puede publicar (post) o enviar (send) un mensaje a una ventana. Cada proceso que llama al menos a
una función de la biblioteca user32.dll o gdi32.dll tiene su propia cola de mensajes que contiene todos
los mensajes enviados a las ventanas creadas por este proceso (por ejemplo, se puede publicar un mensaje
a una ventana mediante la función PostMensaje). Esto significa que alguien tiene que recuperar estos
mensajes de la cola y pasarlos a las ventanas de destino. Esto se hace mediante un bucle especial llamado
bucle de mensajes. En este bucle continuo, que debe implementar el desarrollador de la aplicación, los
mensajes se recuperan de la cola mediante la función GetMessage (con menos frecuencia, PeekMessage)
y pasarlos a la función DispatchMessage. Esta función determina a qué ventana va destinado el mensaje y
llama a su procedimiento de ventana. Así, el bucle de mensajes más simple se parece al Listado 1.5.
Listado 1.5. El bucle de mensajes más simple.
var
Msg: TMsg;
...
while GetMessage(Msg, 0, 0, 0) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;

El diagrama de bloques del bucle de mensajes se muestra en la Figura 1.4.

19
Figura 1.4: Diagrama de bloques del bucle de mensajes.
La función GetMessage devuelve True hasta que se recibe un mensaje WM_QUIT que indica que el
programa debe terminar. Un programa típico de Windows, después de haber completado los pasos
preliminares (registro de clases y creación de ventanas), entra en un bucle de mensajes que se ejecuta
hasta el final de su trabajo. Todas las demás acciones se realizan en el procedimiento de ventana
atendiendo a los mensajes correspondientes.
NOTA:
Si un proceso no tiene un bucle de mensajes, los mensajes que se envíen a sus ventanas no se procesarán. Esto
debe tenerse en cuenta al crear componentes como, por ejemplo, TTimer y TClientSocket. Estos
componentes crean ventanas invisibles para recibir los mensajes que necesitan para funcionar. Si el proceso
que creó estos objetos no tiene un bucle de mensajes, serán inoperables.

GetMessage coloca un mensaje recuperado de la cola de mensajes en la primera variable del parámetro de
tipo TMsg. Los tres últimos parámetros se utilizan para filtrar los mensajes, lo que permite recuperar de la
cola de mensajes solo aquellos mensajes que cumplen con ciertos criterios. Si estos parámetros son cero,
como suele ser el caso, no se realizará ningún filtrado al recuperar mensajes.

20
La función TranslateMessage, que normalmente se llama en el bucle de mensajes, se usa para traducir
los mensajes del teclado (si el bucle de mensajes se implementa sólo para procesar mensajes a las
ventanas invisibles, que por ejemplo utilizan COM/DCOM, o por alguna otra razón la entrada del teclado no se
procesa o se procesa de forma no estándar, se puede omitir la llamada a TranslateMessage). Cuando el
usuario pulsa una tecla, el sistema envía el mensaje WM_KEYDOWN a la ventana que tiene el foco. El código
virtual de la tecla pulsada se transmite a través de los parámetros del mensaje: un número de dos bytes que
está determinado solo por la posición de la tecla pulsada y no depende de la configuración actual del
teclado, ni del estado de las teclas <CapsLock>, etc. La función TranslateMessage cuando encuentra un
mensaje de este tipo, agrega a la cola de mensajes (no al final, sino al principio) el mensaje WM_CHAR, en
cuyos parámetros se pasa el código del carácter correspondiente a la tecla pulsada teniendo en cuenta la
configuración del teclado, condición de las teclas <CapsLock>, <Shift>, etc. Es la función
TranslateMessage la que determina el código del carácter a partir del código virtual de tecla pulsada. Al
pulsar cualquier tecla se genera WM_KEYDOWN, pero WM_CHAR no se genera para todas las teclas, sino sólo
para aquellas que corresponden a algún carácter (por ejemplo, no se genera WM_CHAR cuando se pulsan las
teclas <Shift>, <Ctrl>, <Insert> y teclas de función).

NOTA:
Muchos componentes de la VCL tienen eventos OnKeyDown y OnKeyPress. El primero ocurre cuando el
componente recibe el mensaje WM_KEYDOWN, el segundo ocurre cuando el componente recibe el mensaje
WM_CHAR.

Si la cola de mensajes está vacía, la función GetMessage espera hasta que haya al menos un mensaje
antes de salir. Durante esta espera el proceso no carga al procesador. El bucle de mensajes puede
recuperar y enviar el siguiente mensaje para su procesamiento solo cuando el procedimiento de ventana
haya terminado de procesar el anterior mensaje. Por lo tanto, un mensaje que tarda mucho tiempo en
procesarse bloquea el procesamiento de otros mensajes y todas las ventanas creadas por este proceso
dejan de responder a las acciones del usuario. Esto explica el bloqueo temporal de un programa que, en uno
de sus gestores de mensajes, hace cálculos matemáticos o realiza una extensa consulta a una base de
datos: los mensajes se acumulan en la cola de mensajes, pero no serán recuperados ni procesados. Tan
pronto como el mensaje actual termine de procesarse, todos los demás mensajes se recuperarán de la cola
y se procesarán.
En algunos casos, puede ayudar a evitar que el programa se cuelgue temporalmente estableciendo un bucle
de mensajes localmente. Si un gestor de mensajes que tarda mucho tiempo en ejecutarse contiene un bucle,
se pueden insertar llamadas a la función PeekMessage para comprobar si hay algún mensaje en la cola de
mensajes. Si se detectan mensajes, se debe llamar a DispatchMessage para pasarlos a la ventana
correspondiente. En este caso, los mensajes se recuperarán de la cola de mensajes y se procesarán antes
de que termine la llamada. En la Figura 1.5 se muestra un diagrama de bloques de un programa que contiene
un bucle de mensajes localmente. (para abreviar, solo se muestran las dos funciones más importantes del
bucle principal de mensajes: GetMessage y DispatchMessage, aunque en este caso todo el bucle
principal tiene el mismo aspecto que el de la Fig. 1.4).
Cuando se usa un bucle de mensajes localmente, existe el peligro de la recursividad infinita. Veamos esto
con un ejemplo sencillo: supongamos que un código complejo que contiene un bucle de mensajes
localmente se ejecuta al hacer clic en el botón de una aplicación. Mientras el gestor se ejecuta, un usuario

21
impaciente puede volver a hacer clic en el botón, iniciando la segunda activación del gestor, y así varias
veces. Por supuesto, es poco probable que de esta forma el usuario pueda profundizar en la recursividad
(no basta la paciencia), pero a menudo el hecho de que varias activaciones del gestor sean causadas
recursivamente puede conducir a consecuencias desagradables. Si el programa ejecuta un bucle de
mensajes localmente en el gestor de mensajes de un temporizador, la recursividad puede efectivamente
profundizarse hasta el desbordamiento de la pila. Por ello, hay que tomar precauciones contra la
recursividad a la hora de establecer bucles de mensajes. Por ejemplo, en el caso del botón, este se puede
deshabilitar (Enabled := False) y sólo volverse a habilitar cuando el procesamiento haya terminado, de
esta forma el usuario no podrá presionar el botón mientras se esté ejecutando el bucle de mensajes
localmente.
Es posible poner en cola un mensaje que no está unido a ninguna ventana. Esto se hace mediante la función
PostThreadMessage. Estos mensajes deben ser gestionados por el propio bucle de mensajes, ya que la
función DispatchMessage simplemente los ignora.

Figura 1.5: Diagrama de bloques de un programa con un bucle de mensajes localmente.


También existen los mensajes de difusión que se envían a varias ventanas a la vez. La forma más sencilla de
enviar un mensaje de este tipo es mediante la función PostMessage, especificando la constante
HWND_BROADCAST como destino en lugar de un manipulador de ventana específico. Este mensaje lo
recibirán todas las ventanas que se encuentren directamente en el escritorio y sin propietarios (en términos

22
del sistema). También existe la función especial BroadcastSystemMessage (desde Windows XP, su
versión extendida BroadcastSystemMessageEx) que permiten especificar qué ventanas recibirán los
mensajes de difusión.
Además de los parámetros wParam y lParam, a cada mensaje se le asigna la hora del envío y las
coordenadas del cursor al momento de producirse. Los campos correspondientes están en la estructura
TMsg utilizada por las funciones GetMessage y DispatchMessage, pero el procedimiento de ventana no
tiene parámetros para pasarlos. Se pueden utilizar las funciones GetMessageTime y GetMessagePos
para obtener la hora del envío y las coordenadas del cursor al momento de procesarse el mensaje.
También hay una serie de funciones que pueden procesar mensajes sin la participación de
DispatchMessage y de un procedimiento de ventana. Si estas funciones reconocen el mensaje recuperado
de la cola de mensajes como "suyo", realizan todas las acciones necesarias para procesarlo, y no es
necesario llamar a TranslateMessage y DispatchMessage. Estas funciones incluyen, entre otras, las
siguientes:
▪ TranslateAccelerator – procesa teclas de acceso rápido para los comandos de menú del sistema a
partir de la coincidencia en una tabla especificada de accesos rápidos y luego envía el mensaje
WM_COMMAND o WM_SYSCOMMAND directamente al procedimiento de ventana, similar a cuando el usuario
selecciona un elemento de menú;
▪ TranslateMDISysAccel - es similar a la función anterior, excepto que procesa "teclas acceso rápido"
para los comandos de menú del sistema en ventanas MDI;
▪ IsDialogMessage - procesa los mensajes que tienen un significado especial para las ventanas de
diálogo (por ejemplo, la tecla <Tab> que cambia entre controles). Se usa para diálogos no modales y
diálogos que no son diálogos (es decir, creados sin utilizar las funciones CreateDialogXXXX) pero que
requieren la misma funcionalidad.
Si es necesario, las funciones listadas se insertan en el bucle de mensajes. El Listado 1.6 muestra cómo
sería un bucle de mensajes con una llamada a TranslateAccelerator para un formulario MDI padre y
con una llamada a TranslateMDISysAccel para un formulario MDI hijo.

Listado 1.6. Bucle de mensajes con tratamiento de teclas de acceso rápido del menú principal y del menú de
sistema en ventanas MDI.
while GetMessage(Msg, 0, 0, 0) do
if not TranslateMDISysAccel(ActiveMDIChildHandle, Msg)
and not TranslateAccelerator(MDIFormHandle, AccHandle, Msg) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;

Cuando se envía un mensaje, a diferencia de un paquete, el mensaje no se pone en cola, sino que se pasa
directamente al procedimiento de ventana. Puede enviar un mensaje, por ejemplo, mediante la función
SendMessage. Si esta función se llama desde el mismo proceso al cual pertenece la ventana de destino, es
equivalente a llamar directamente al procedimiento de ventana. Si la ventana pertenece a otro proceso, el
mensaje se coloca en una cola de mensajes separada de mayor prioridad que la cola de mensajes enviados.

23
Las funciones GetMessage y PeekMessage seleccionan primero todos los mensajes de esta cola y los
envían para su procesamiento antes de analizar la cola de mensajes enviados.
NOTA:
Debido a que los mensajes enviados a una ventana se envían directamente al procedimiento de ventana o bien
se despachan dentro de GetMessage o PeekMessage, estos mensajes no entran en las funciones
TranslateMDISysAccel, TranslateAccelerator y TranslateMessage. Esto debe tenerse en cuanta al
enviar mensajes que emulen pulsaciones del teclado. Tales mensajes deben ser enviados a la ventana, en lugar
de enviarlos a pasar por todo el ciclo de procesamiento y así la ventana responda correctamente. Para emular
mensajes del teclado, puede usar la función keybd_event, pero esta función envía el mensaje a la ventana
activa, que no siempre es conveniente.

Los cuadros de diálogo procesan los mensajes de un modo especial. Estas ventanas se dividen en modales
(creadas y mostradas usando las funciones DialogBoxXXXX) y no modales (creadas usando las funciones
CreateDialogXXXX y luego mostradas usando la función ShowWindow, que también se usa para ventanas
normales que no son de diálogo). Tanto las ventanas modales como las no modales se crean a partir de una
plantilla que puede estar almacenada en los recursos de la aplicación o en memoria.
En la plantilla, se puede especificar explícitamente el nombre de la clase de ventana de diálogo que se está
creando o (como suele ser el caso) no especificarlo en absoluto para seleccionar la clase predeterminada
que el sistema proporciona para los cuadros de diálogo. El procedimiento de ventana de la clase de diálogo
debe pasar los mensajes no procesado a la función DefDlgProc.

Todos los cuadros de diálogo tienen un así llamado procedimiento de diálogo, una función cuyo puntero se
pasa como uno de los parámetros a las funciones DialogBoxXXXX y CreateDialogXXXX. Los prototipos
de los procedimientos de diálogo y de ventana son los mismos. La función DefDlgProc comienza llamando
al procedimiento de diálogo. Si el mensaje pasado no se procesa (indicado por un valor de retorno igual a
cero), la función DefDlgProc procesa el mensaje por sí misma. Por lo tanto, con una clase de ventana y un
procedimiento de ventana, se pueden implementar diferentes cuadros de diálogo utilizando diferentes
procedimientos de diálogo.
Las funciones DialogBoxXXXX crean un cuadro de diálogo y lo muestran inmediatamente en modo modal.
Estas funciones completan su ejecución solo cuando se cierra la ventana modal. Las funciones modales
tienen dentro su propio bucle de mensajes. Todas las demás ventanas se desactivan mientras se presenta el
dialogo modal (como si para ellas se llamara a la función EnableWindow con el parámetro FALSE), es
decir, dejan de responder a los mensajes del mouse y el teclado. Al mismo tiempo, conservan la capacidad
de responder a otros mensajes, lo que les permite, por ejemplo, actualizar su contenido con un
temporizador (la ayuda dice que nada impide que un desarrollador inserte en el procedimiento de diálogo
llamadas a funciones que resuelvan ventanas terminadas por el sistema, pero ello anularía el propósito de
los diálogos modales).
Si no hay mensajes en la cola, el bucle modal envía el mensaje WM_ENTERIDLE al cuadro de diálogo
principal, cuyo procesamiento permite que este cuadro realice acciones en segundo plano. Por supuesto, el
gestor de WM_ENTERIDLE no debe ejecutarse por mucho tiempo, de lo contrario, la ventana modal se
bloqueará.

24
Normalmente, una ventana utiliza el procedimiento de ventana especificado al momento de crear la clase de
ventana correspondiente. Sin embargo, se pueden crear las llamadas subclases, redefiniendo el
procedimiento de ventana después de haberse creado la ventana. Esta redefinición solo se aplica a la
ventana especificada y no afecta al resto de ventanas que pertenecen a la misma clase. Esto se realiza
mediante la función SetWindowLong con el parámetro GWL_WNDPROC (otros valores de este parámetro
permiten cambiar otras propiedades de la ventana, como el estilo y el estilo extendido). Solo se puede
cambiar el procedimiento de ventana en las ventanas creadas por el mismo proceso.
El nuevo procedimiento de ventana, que se instala al crear una subclase, debe pasar todos los mensajes sin
procesar al procedimiento de ventana establecido anteriormente, no a la función DefWindowLong.
SetWindowLong devuelve el descriptor del procedimiento de ventana anterior (el mismo descriptor se
puede obtener llamando a la función GetWindowLong con el argumento GWL_WNDPROC). Normalmente, el
valor del descriptor es numéricamente el mismo que la dirección del procedimiento de ventana anterior, por
lo que algunas fuentes recomiendan usar este descriptor directamente como un puntero de procedimiento.
Esto funcionará incluso para las clases de ventana creadas por el propio programa. Sin embargo, es más
seguro llamar al procedimiento de ventana anterior con la función CallWindowProc y dejar que el sistema
"juzgue" si el descriptor es o no un puntero.
Como ejemplo, considere crear la subclase de una ventana cuyo descriptor está contenido en la variable
Wnd. Supongamos que necesitamos procesar el mensaje WM_KILLFOCUS de una forma no estándar.
Entonces el código para el procedimiento de ventana nuevo y el código para configurarlo se verían como el
Listado 1.7.
Listado 1.7. Creación de una subclase para el manejo especial de WM_KILLFOCUS.
var
OldWndProc: TFNWndProc;
function NewWindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
if Msg = WM_KILLFOCUS then
// Procesa el mensaje
else
Result := CallWindowProc(OldWndProc, hWnd, Msg, wParam, lParam);
end;

...
// Instalación del nuevo procedimiento de ventana en la ventana Wnd
OldWndProc := TFNWndProc(SetWindowLong(Wnd, GWL_WNDPROC, Longint(@NewWindowProc)));
...

NOTA:
MSDN considera obsoletas las funciones GetWindowLong y SetWindowLong y recomienda usar en su lugar
GetWindowLongPtr y SetWindowLongPtr, que son compatibles con las versiones de Windows 64 bits. Sin
embargo, hasta Delphi 2007 inclusive, estas funciones no estaban disponibles en el módulo Windows y, si es
necesario debe importarlas usted mismo.

25
También puede redefinir un procedimiento de ventana mediante SetWindowLong en ventanas cuyo
procedimiento de ventana fue redefinido anteriormente. Esto crea cadenas de procedimientos de ventana,
cada uno llamando al anterior.

Ahora hablaremos de cómo se crean las ventanas en la VCL. Aquí no hablaremos de escribir código para
crear una ventana mediante la VCL (se supone que el lector ya lo sabe), sino de qué funciones API son
llamadas por la VCL al crear una ventana y en qué momento.
Si revisa el código de los métodos de clase para TWinControl que se llaman cuando se crea y se muestra
una ventana, no encontrará inmediatamente en que parte se crea la ventana. A primera vista, parece que
este código no tiene nada que ver con crear una ventana, como si la ventana se estaría creando en otro
lugar completamente diferente y TWinControl recibiera un descriptor ya terminado. De hecho, es el propio
TWinControl el que crea la ventana, y su creación está oculta en la propiedad Handle. El método
GetHandle, que devuelve el valor de la propiedad Handle, tiene el siguiente aspecto (Listado 1-8).

Listado 1.8. Implementación del método TWinControl.GetHandle.


procedure TWinControl.HandleNeeded;
begin
if FHandle = 0 then
begin
if Parent <> nil then Parent.HandleNeeded;
CreateHandle;
end;
end;

function TWinControl.GetHandle: HWnd;


begin
HandleNeeded;
Result := FHandle;
end;

Cada vez que se accede a la propiedad Handle, se llama al método HandleNeeded, que comprueba si ya se
ha creado una ventana, y si no, la crea y crea la ventana padre si es necesario. Así, se crea la ventana la
primera vez que se accede a la propiedad Handle.
El método CreateHandle, que se llama desde el método HandleNeed, realiza sólo algunas operaciones
auxiliares indirectamente y llama al método CreateWnd para crear la ventana (ver Figura 1.9).

Listado 1.9. Implementación del método CreateWnd.


procedure TWinControl.CreateWnd;
var
Params: TCreateParams;
TempClass: TWndClass;
ClassRegistered: Boolean;
begin
CreateParams(Params);
with Params do
begin
if (WndParent = 0) and (Style and WS_CHILD <> 0) then
if (Owner <> nil) and (csReading in Owner.ComponentState) and
(Owner is TWinControl) then
WndParent := TWinControl(Owner).Handle
else
raise EInvalidOperation.CreateFmt(SParentRequired, [Name]);
FDefWndProc := WindowClass.lpfnWndProc;

26
ClassRegistered := GetClassInfo(WindowClass.hInstance,
WinClassName, TempClass);
if not ClassRegistered or
(TempClass.lpfnWndProc <> @InitWndProc) then
begin
if ClassRegistered then Windows.UnregisterClass(WinClassName,
WindowClass.hInstance);
WindowClass.lpfnWndProc := @InitWndProc;
WindowClass.lpszClassName := WinClassName;
if Windows.RegisterClass(WindowClass) = 0 then RaiseLastOSError;
end;
CreationControl := Self;
CreateWindowHandle(Params);
if FHandle = 0 then
RaiseLastOSError;
if (GetWindowLong(FHandle, GWL_STYLE) and WS_CHILD <> 0) and
(GetWindowLong(FHandle, GWL_ID) = 0) then
SetWindowLong(FHandle, GWL_ID, FHandle);
end;
StrDispose(FText);
FText := nil;
UpdateBounds;
Perform(WM_SETFONT, FFont.Handle, 1);
if AutoSize then AdjustSize;
end;

La creación real de la ventana no ocurre de nuevo aquí, sino en el método CreateWindowHandle, que es
muy simple: consiste en una sola llamada a la función API CreateWindowEx con parámetros cuyos valores
se toman de los campos del registro Params de tipo TCreateParams (Listado 1.10).

Listado 1.10. Registro TCreateParams.


TCreateParams = record
Caption: PChar;
Style: DWORD;
ExStyle: DWORD;
X, Y: Integer;
Width, Height: Integer;
WndParent: HWnd;
Param: Pointer;
WindowClass: TWndClass;
WinClassName: array[0..63] of Char;
end;

El registro Params almacena los parámetros tanto de la ventana pasada a la función WindowCreateEx
como de la clase de la ventana (campos WindowClass y WndClassName). Todos los campos son
inicializados por el método CreateParams basado en los valores de las propiedades del componente
ventana. El método es virtual y puede redefinirse ser anulado en sus descendientes, lo que es útil cuando se
quiere cambiar el estilo de una ventana que se está creando. Por ejemplo, al agregar el estilo extendido
WS_EX_CLIENTEDGE (o, alternativamente, WS_EX_STATICEDGE), puede obtener una ventana con un borde
inusual (vea el Listado 1.11).
Listado 1.11. Redefiniendo el método CreateParams.
procedure TForm1.CreateParams(var Params: TCreateParams);
begin
// Вызов унаследованного метода для заполнения всех полей
// записи Params
inherited CreateParams(Params);
// Добавляем флаг WS_EX_CLIENTEDGE к расширенному стилю окна
Params.ExStyle := Params.ExStyle or WS_EX_CLIENTEDGE;
end;

27
NOTA:
En la Sección. 1.1.4 dijimos que el nombre de la clase de la ventana que la VCL crea para un componente ventana
es el mismo que el nombre de la clase de este componente. Aquí vemos que, de hecho, el nombre de la clase de
la ventana también puede ser diferente, simplemente cambiando los valores del campo
Params.WinClassName.

Tenga en cuenta que, a todas las clases sin excepción, el método CreateWnd asigna el mismo
procedimiento de ventana – InitWndProc. Esta es la base para el procesamiento de mensajes en la VCL,
es por ello que el procedimiento de ventana no se asigna en el método CreateParams, sino en el método
CreateWnd, de modo que no se pueda cambiar este comportamiento en los herederos (el método
CreateWnd también es virtual, pero al redefinirlo sólo tiene sentido agregar algunas acciones y no cambiar
el comportamiento del método heredado).
Para entender cómo funciona el procedimiento InitWndProc, observe otra peculiaridad del método
CreateWnd: este escribe una referencia al objeto actual en la variable global CreationControl antes de
llamar a CreateWindowHandle (es decir, justo antes de crear la ventana). Esta variable es utilizada por el
procedimiento InitWindProc (ver Listado 1.12).

Listado 1.12. Procedimiento de ventana InitWndProc


function InitWndProc(HWindow: HWnd; Message, WParam,
LParam: Longint): Longint;
begin
CreationControl.FHandle := HWindow;
SetWindowLong(HWindow, GWL_WNDPROC,
Longint(CreationControl.FObjectInstance));
if (GetWindowLong(HWindow, GWL_STYLE) and WS_CHILD <> 0) and
(GetWindowLong(HWindow, GWL_ID) = 0) then
SetWindowLong(HWindow, GWL_ID, HWindow);
SetProp(HWindow, MakeIntAtom(ControlAtom),
THandle(CreationControl));
SetProp(HWindow, MakeIntAtom(WindowAtom),
THandle(CreationControl));

asm
PUSH LParam
PUSH WParam
PUSH Message
PUSH HWindow
MOV EAX,CreationControl
MOV CreationControl,0
CALL [EAX].TWinControl.FObjectInstance
MOV Result,EAX
end;

end;

NOTA:
El código de la función InitWndProc del Listado 1.12 está tomado de Delphi 7. En versiones posteriores, el
código incluye soporte para ventanas codificadas en Unicode, por lo que se puede elegir entre las variantes
ANSI y Unicode de las funciones API (véase la Sección 1.1.12 para más información sobre las variantes ANSI y
Unicode). Este código es más difícil de entender debido a estos complementos. Además, todo lo relacionado con
el compilador LINUX se ha eliminado del Listado 1.12 para no saturarlo.

28
El Listado 1.12 muestra que el procedimiento de ventana InitWndProc no procesa ningún mensaje por sí
mismo, sino que simplemente reasigna el procedimiento de ventana a la ventana. Así, InitWndProc sólo se
llama una vez por ventana para reasignar el procedimiento de ventana. El procesamiento del mensaje que
hizo que se llamara a InitWndProc también se trasfiere a este nuevo procedimiento (la inserción de código
ensamblador al final de InitWndProc hace precisamente eso).
Al examinar este código surgen dos preguntas. En primer lugar, ¿por qué escribir un procedimiento de
ventana de esta forma, por qué no solo asignar el procedimiento de ventana necesario de la forma habitual?
La cuestión aquí es que por un tema estándar se asigna un procedimiento de ventana a toda una clase de
ventanas, mientras que la lógica interna de la VCL requiere que cada instancia de un componente tenga su
propio procedimiento de ventana. Esto sólo se logra mediante la subclasificación después crear la ventana.
Un puntero a su procedimiento de ventana único (de dónde viene este procedimiento y por qué debe ser
único lo discutiremos en la siguiente sección) es almacenado por cada instancia en el campo
FObjectInstance. El valor de la variable global CreationControl se asigna, como recordamos, justo
antes de crear la ventana y la ventana literalmente recibe su primer mensaje en el momento de su creación.
Dado que la VCL es fundamentalmente una biblioteca de un solo proceso, es imposible que otro código se
interponga entre establecer el valor de la variable CreationControl y la llamada a InitWndProc, por lo
que ésta recibe una referencia valida al objeto que se está creando.
La segunda pregunta es ¿por qué es tan difícil? ¿Por qué no se puede llamar a SetWindowLong en el
método CreateWnd inmediatamente después de crear la ventana y establecer ahí el procedimiento de
ventana deseado, en lugar de indicarle al procedimiento InitWndProc que lo haga? Aquí la respuesta es
que se hace así porque la ventana recibe sus primeros mensajes (por ejemplo, los mensajes WM_CREATE y
WM_NCCREATE) antes de que la función CreateWindowEx complete su trabajo. Para completar la creación
de una ventana, CreateWindowEx envía varios mensajes a la ventana, y solo después de que la ventana los
haya procesado correctamente se considera que el proceso de crear la ventana está completo. Por lo tanto,
asignar un procedimiento de ventana único después de que CreateWindowEx se haya completado es
demasiado tarde. Esta es la razón del por qué el procedimiento de ventana único se asigna de una forma tan
poco obvia y algo torpe.

En casos sencillos, cuando se usa la VCL, no tiene que procesar los mensajes de la ventana usted mismo, ya
que casi todo se puede hacer mediante las propiedades, métodos y eventos del componente. Sin embargo,
algunos mensajes deben procesarse manualmente. Esto es más frecuente cuando se desarrollan
componentes propios, pero también puede ser útil en aplicaciones generales.
Además de los mensajes proporcionados por el sistema, los componentes de la VCL intercambian mensajes
creados por los autores de esta biblioteca. Estos mensajes llevan el prefijo CM_ y CN_. No están
documentados en ninguna parte y sólo pueden entenderse a partir del código fuente de la VCL. Cuando
desarrolle sus propios componentes, debe procesar estos mensajes, que aquí no describiremos en su
totalidad, pero algunos de ellos se mencionarán en la descripción de cómo la VCL funciona con eventos.
En la API de Windows no existe el concepto de ventana principal; todas las ventanas que no tienen un padre
(o propietario en términos de sistema) son equivalentes y la aplicación puede seguir funcionando después
de cerrar cualquier ventana. Pero en la VCL se introduce el concepto de formulario principal: el formulario
que se crea primero se convierte en el formulario principal, y cerrarlo significa cerrar toda la aplicación.

29
Si la ventana no tiene un padre o un propietario en términos del sistema (tales ventanas se llaman ventanas
de nivel superior), aparece un botón asociado a esta ventana en la barra de tareas (una ventana propietaria
también puede tener este botón si se crea con el estilo WS_EX_APPWINDOW). Por lo general, una aplicación
tiene una ventana de nivel principal y actúa como la ventana principal de esta aplicación, aunque el sistema
no prohíbe que una aplicación cree varias ventanas de nivel superior (por ejemplo, Internet Explorer,
Microsoft Word). Los desarrolladores de la VCL tomaron un camino diferente: el objeto Application crea
la ventana de nivel superior responsable de la aparición del botón en la barra de tareas. El descriptor de
esta ventana se almacena en la propiedad Application.Handle y es invisible porque no tiene
dimensiones. Como cualquier otra ventana, esta tiene un procedimiento de ventana y puede procesar
mensajes. El formulario principal es una ventana separada que, desde un punto de vista formal, no tiene
nada que ver con el botón de la barra de tareas. La visibilidad de la conexión entre este botón y el formulario
principal está garantizada por la interacción entre el objeto Application y el objeto formulario principal
dentro de la VCL. Por lo tanto, incluso la aplicación VCL más simple crea dos ventanas: la ventana invisible
del objeto Application y la ventana del formulario principal. La ventana creada por el objeto
Application se llamará ventana invisible de la aplicación. Por defecto, la ventana invisible de la aplicación
se convierte en propietaria (en términos del sistema) de todos los formularios que no tienen establecido
explícitamente su propiedad Parent, incluido el formulario principal.

Al procesar mensajes, la VCL se encarga de dos tareas: recuperar mensajes de una cola de mensajes y
pasar el mensaje a un componente especifico. Veamos la primera tarea.
El objeto Application recupera los mensajes de la cola, y su método ProcessMessage es responsable
de recuperar y enviar el mensaje (ver Listado 1.13).
Listado 1.13. Método TApplication.ProcessMessage.
function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Unicode: Boolean;
Handled: Boolean;
MsgExists: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then
begin
Unicode := (Msg.hwnd <> 0) and IsWindowUnicode(Msg.hwnd);
if Unicode then
MsgExists := PeekMessageW(Msg, 0, 0, 0, PM_REMOVE)
else
MsgExists := PeekMessage(Msg, 0, 0, 0, PM_REMOVE);
if not MsgExists then Exit;
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False;
if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
if not IsPreProcessMessage(Msg) and not IsHintMsg(Msg) and
not Handled and not IsMDIMsg(Msg) and
not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
begin
TranslateMessage(Msg);
if Unicode then
DispatchMessageW(Msg)
else
DispatchMessage(Msg);
end;
end
else

30
FTerminate := True;
end;
end;

En este código, hace falta un comentario separado sobre cómo utilizar la función PeekMessage. En primer
lugar, esta función se llama con el parámetro PM_NOREMOVE para comprobar si hay un mensaje en la cola y
a qué ventana está destinado el primer mensaje. El mensaje en sí permanece en la cola. La función
IsWindowUnicode comprueba si la ventana de destino utiliza ANSI o Unicode, y luego, dependiendo del
resultado se recupera el mensaje ya sea con la función PeekMessage o con su homóloga Unicode
PeekMessageW (vea la Sección 1.1.12 para las homólogas Unicode). Cuando se procesa un mensaje también
se llama a la función DispatchMessage o a su análoga Unicode DispatchMessageW.

Si el método ProcessMessage recupera un mensaje WM_QUIT de la cola de mensajes mediante


PeekMessage, establece el campo FTerminate en True y sale. El procesamiento de todos los demás
mensajes recuperados de la cola consta de los siguientes pasos principales (consulte la Figura 1.6):
1. Si se asigna el gestor Application.OnMessage, se le pasa el mensaje. En este gestor, puede
establecer la variable Handled a True, lo que significa que el mensaje no necesita procesamiento
adicional.
2. El segundo paso es el procesamiento previo del mensaje (llamando al método IsPreProcessMessage).
Este paso se introdujo a partir de BDS 2006, no está presente en las versiones anteriores. Normalmente, el
mensaje es procesado previamente por la ventana de destino, pero si la ventana de destino no es una
ventana VCL, se busca la ventana VCL a través de la cadena de padres. Además, si una ventana captura la
entrada del mouse, será esta ventana la que procese previamente el mensaje. Si se encuentra la ventana
que cumpla con estos requisitos, se llama al método PreProcessMessage, que devuelve un resultado de
tipo lógico. Si devuelve True, el procesamiento del mensaje termina. Tenga en cuenta que ninguno de los
componentes estándar de la VCL utiliza esta capacidad de captura; está se implementa para componentes
de terceros.
3. A continuación, si existen sugerencias (hint) en la ventana, se comprueba si el mensaje entrante debe
ocultar esta sugerencia, y si es así, se elimina de la ventana (método IsHintMessage). El listado de
mensajes que deben ocultar sugerencias emergentes depende de la clase de ventana (aquí se refiere a la
clase de ventana de la VCL, no a la de Windows) y está definido en el método virtual
THintWindow.IsHintMsg. La implementación estándar de este método considera "ocultar" todos los
mensajes del mouse, del teclado, mensajes de activación y desactivación del programa, así como las
acciones del usuario sobre los menús o componentes visuales. Si el método IsHintMessage devuelve
False, el mensaje no se procesa más, pero la implementación estándar de este método siempre devuelve
True.

4. Seguidamente, se comprueba el valor de la variable Handled en el gestor OnMessage (si está asignado).
Si este valor es True, el método ProcessMessage sale y finaliza el procesamiento del mensaje. Por lo
tanto, el procesamiento de un mensaje por el evento OnMessage no impide el procesamiento previo del
mensaje y ocultación de una sugerencia emergente en la ventana.
5. Si el formulario principal de la aplicación tiene un estilo MDIForm y una de sus ventanas secundarias MDI
está activa, el mensaje se pasa a la función TranslateMDISysAccel. Si esta función devuelve True,
finaliza el procesamiento del mensaje (todas estas acciones se ejecutan en el método IsMDIMsg).

31
6. Luego, si se recibe un mensaje del teclado, este va al mismo procesamiento previo de ventana que en el
paso 2 (método IsKeyMsg). El procesamiento previo de un mensaje del teclado comienza con el intento de
encontrar la combinación de teclas recibidas entre las teclas de acceso directo del menú contextual y
ejecutar el comando correspondiente. Si el menú contextual no reconoce el mensaje como su propia tecla
de acceso directo, se llama al gestor de eventos OnShortCut de la ventana que realiza el procesamiento
previo (si esta ventana no es un formulario y no tiene este evento, se llama al gestor OnShortCut de su
formulario padre). Si el gestor OnShortCut no ha establecido la variable Handled a True, la combinación
de teclas de acceso directo resultante se busca primero entre las teclas de acceso rápido del menú
principal y luego entre los componentes de TActionList. Si la combinación tampoco se encuentra aquí, se
produce el evento Application.OnShortCut, que también tiene la variable Handled que le permite
especificar que el mensaje no necesita procesamiento adicional. Si el gestor no establece este parámetro, el
mensaje se pasa al formulario principal de la aplicación que busca la combinación de teclas de acceso
directo en su menú contextual, lo pasa al gestor OnShortCut y busca entre las teclas de acceso directo del
menú principal y los componentes TActionList. Si la tecla pulsada no es una tecla de acceso directo, sino
que se refiere a las teclas utilizadas para controlar las ventanas de diálogo (<Tab>, flechas, <Esc>, etc.),
el formulario recibe un mensaje al respecto y, si es necesario, se procesa. Así, en esta etapa, las
herramientas de la VCL emulan las funciones TranslateAccelerator e IsDialogMessage.

32
7. Si uno de los diálogos estándar está presente en la pantalla (en la VCL se implementan con las clases
TOpenDialog, TSaveDialog, etc.), se llama a la función IsDialogMessage para que estos diálogos
puedan funcionar normalmente (método IsDlgMsg).

8. Si el mensaje no se procesó en ninguno de los pasos anteriores, se llama a las funciones


TranslateMessage y DispatchMessage que completan el procesamiento del mensaje trasladándolo a la
función de ventana correspondiente.
NOTA:
Si analizamos detenidamente el sexto paso del procesamiento de mensajes, podemos ver que se comprueba la
correspondencia entre la combinación de las teclas pulsadas y las "teclas rápidas" del menú, primero en el
formulario activo y luego en el formulario principal. El evento OnShortCut del formulario activo ocurre

33
primero, seguido del evento Application.OnShortCut, y luego del evento OnShortCut del formulario
principal. Si el formulario principal está activo cuando se recibe el mensaje, comprobará dos veces que las
teclas correspondan a las teclas de acceso rápido de sus menús, y el evento OnShortCut también ocurrirá dos
veces (la primera vez con el campo Msg.Msg igual a CN_KEYDOWN, la segunda vez con CM_APPKEYDOWN). Esta
comprobación ocurrirá dos veces sólo si la combinación de teclas no se reconoce como teclas de acceso rápido;
en caso contrario, la cadena de comprobaciones termina en la primera comprobación.

El método ProcessMessage devuelve True si el mensaje ha sido recuperado y procesado, y False si la cola
de mensajes está vacía. Esto se realiza en el método HandleMessage, que llama a ProcessMessage y, si
retorna False, llama al método Application.Idle para acciones de baja prioridad, que sólo deben
ejecutarse cuando no haya mensajes en la cola. El método Idle primero comprueba sobre qué componente
está el cursor del mouse y almacena una referencia al mismo en el campo FMouseControl que se usa en
una comprobación posterior para ver si se debe o no ocultar una sugerencia emergente. Luego, si es
necesario, se oculta la anterior y se muestra la nueva sugerencia emergente. Después se llama al gestor
Application.OnIdle si hay uno asignado. Este gestor tiene un parámetro Done, que por defecto es
True. Si este parámetro, no se cambia a False en el código del gestor, el método Idle activa los eventos
OnUpdate para todos los objetos TAction que los tengan asignados (si Done es False después de una
llamada, HandleMessage no pierde el tiempo generando eventos OnUpdate).

NOTA:
BDS 2006 introdujo la propiedad Application.ActionUpdateDelay, que le permite retrasar la carga en el
procesador posponiendo la actualización de los objetos TAction por algún tiempo. Si el valor de esta propiedad
es diferente de cero, en lugar de llamar a OnUpdate en el método Idle, se inicia un temporizador y se llama a
OnUpdate en su señal.

Entonces, independientemente del valor de Done, el procedimiento CheckSynchronize comprueba si hay


entradas en la lista de métodos que esperan sincronización (estos métodos se colocan en la lista
especificada cuando se llama a TThread.Synchronize). Si la lista no está vacía, se ejecuta el primero de
estos métodos (por supuesto, se eliminan de la lista). Entonces, si Done sigue siendo True y la lista de
métodos a sincronizar está vacía (es decir, no se necesita ninguna otra acción), HandleMessage llama a la
función API WaitMessage. Esta función detiene la ejecución del proceso hasta que haya mensajes en su
cola.
NOTA:
Llamar a Synchronize hace que el método proporcionado sea ejecutado por el proceso principal de la
aplicación y se suspenda el proceso de Synchronize hasta que el proceso principal lo haga. Esto demuestra
que no es conveniente poner todo el código en el proceso de Synchronize. Si lo hace, el proceso de
Synchronize no hará nada, todo lo hará el proceso principal, y no obtendrá ningún beneficio por haber creado
un proceso complementario. Por lo tanto, el proceso de Synchronize debe contener solo aquellas acciones
que no se puedan ejecutar en el proceso principal (por ejemplo, las llamadas a propiedades y métodos de los
componentes de la VCL).

34
El bucle de mensajes principal en la VCL se implementa mediante el método Application.Run, cuya
llamada se inserta automáticamente en el archivo de proyecto dpr de la VCL. Application.Run llama al
método HandleMessage hasta que el campo FTerminate sea True (recuerde que el valor True se asigna a
este campo cuando ProcessMessage recupera de la cola de mensajes el mensaje WM_QUIT, y cuando se
procesa un mensaje WM_ENDSESSION y se cierra el formulario principal).

El método Application.ProcessMessages existe para establecer un bucle de mensajes local. Llama a


ProcessMessage hasta que la cola de mensajes este vacía. Se recomienda insertar este método en los
gestores de eventos que se ejecutan durante mucho tiempo, para que el programa no pierda la capacidad de
responder a las acciones del usuario.
De lo anterior puede parecer que el proceso principal comprueba la lista de métodos de sincronización sólo
en el bucle de mensajes principal cuando se llama al método Idle. En realidad, no es así. El módulo Classes
contiene la variable WakeMainThread que almacena el puntero al método que se llama cuando se agrega
un nuevo método al listado de sincronización. En el constructor de TApplication, a esta variable se le
asigna un puntero al método TApplication.WakeMainThread que envía el mensaje WM_NULL a una
ventana invisible de la aplicación. El mensaje WM_NULL es un mensaje "vacío" al que la ventana no debe
responder (se usa, por ejemplo, cuando se captura mensajes con un gancho: un gancho no puede evitar que
se envíe un mensaje a la ventana, pero puede cambiarlo a WM_NULL para que la ventana ignore el mensaje).
Sin embargo, la ventana invisible de la aplicación no ignora este mensaje, sino que llama a
CheckSynchronize cuando se recibe. Por lo tanto, la ejecución síncrona del método no se retrasa hasta
que se llama a Idle, pero se ejecuta si el proceso principal pasa a modo de espera para la recepción de
mensajes (mediante una llamada a WaitMessage), entonces la llamada a Synchronize desde otro
proceso interrumpirá esta espera, porque el mensaje WM_NULL estará en cola.

El procedimiento CheckSynchronize y la variable WakeMainThread permiten la sincronización en


aplicaciones que no utilizan completamente la VCL. El desarrollador de la aplicación debe asegurarse de
llamar a CheckSynchronize desde el proceso principal para que TThread.Synchronize pueda ser
llamado desde otros procesos. En este caso, el proceso principal puede prescindir de un bucle de mensajes.
La asignación de su propio método a la variable WakeMainThread le permite implementar una forma
específica de la aplicación para acelerar la llamada al método en el proceso principal.
NOTA:
El método de sincronización de procesos descrito aquí apareció a partir de la 6 versión de Delphi. En versiones
anteriores, no había un listado de métodos para sincronizar. En su lugar, se creaba una ventana invisible
especial en el proceso principal, y el método TThread.Synchronize utilizaba SendMessage para enviar el
mensaje CM_EXECPROC a esta ventana con la dirección del objeto cuyo método debía sincronizarse. El método
se ejecutaba se ejecutaba en el procedimiento de ventana de esa ventana mientras se procesaba el mensaje.
Esto también permitía la sincronización en aplicaciones que no eran de la VCL, pero requería de un bucle de
mensajes en el proceso principal y no permitía la sincronización mientras el proceso principal estaba en un
bucle de mensajes local. Debido al cambio en el mecanismo de sincronización, podrían surgir problemas al
migrar aplicaciones antiguas a las nuevas versiones: si antes era suficiente organizar un bucle de mensajes
para garantizar la sincronización, ahora necesita encontrar un lugar para llamar a CheckSynchronize. Por
supuesto, al portar una aplicación VCL completas, estos problemas no surgen porque todo lo que necesitas está
contenido en los métodos de la clase TApplication.

El método de sincronización adoptado en Delphi 6 se desarrolló aún más en BDS 2006. En la clase TThread
apareció el método Queue para pasar al código del proceso principal, una llamada al método para su ejecución

35
asíncrona, es decir, una en la que el proceso que llama a Queue continúa ejecutándose sin esperar a que el
proceso principal ejecute el código requerido. El proceso principal ejecuta este código en paralelo siempre que
tiene oportunidad de hacerlo (esta información se ha obtenido de un análisis al código fuente de los módulos de
la VCL, ya que la ayuda de Delphi, por desgracia, no describe este método; no se menciona en absoluto en BDS
2006 y sí en Delphi 2007, pero la descripción completa consiste en una sola frase: "Queue es un miembro de la
clase TThread"). El método Queue utiliza el mismo listado de métodos para sincronizar que el método
Synchronize, sólo que los elementos de este listado se agregan con la consigna de ejecución asíncrona, por lo
que CheckSynchronize no notifica al proceso que colocó el método en el listado. Además, el método
TThread.RemoveQueuedEvents permite eliminar llamadas asíncronas del listado de métodos para
sincronizar si ya no son necesarios.

Cuando un formulario de la VCL se muestra en modo modal, los mensajes de la cola se obtienen de una
forma especial. Las ventanas modales en la VCL no son lo mismo que los diálogos modales en términos de
la API. Un diálogo sólo puede crearse en base a una plantilla y su modalidad lo proporciona el propio
sistema operativo, mientras que en la VCL se permite la modalidad de cualquier formulario, lo que permite
que el desarrollador no este limitado por el alcance de la plantilla proporcionada por el sistema. Esto se
consigue de la siguiente forma: cuando se llama al método ShowModal, todas las ventanas son prohibidas
por la VCL, entonces la ventana se muestra normalmente como no modal, pero como todas las demás
ventanas están prohibidas, se crea el efecto modal.
Dentro de ShowModal, se crea un bucle de mensajes. En este bucle se llama al método
Application.HandleMessage hasta que se establezca la propiedad ModalResult o llegue un mensaje
WM_QUIT. Una vez terminado este bucle, todas las ventanas que estaban permitidas antes de la llamada a
ShowModal se habilitan de nuevo y el formulario "modal" se cierra. A diferencia de los diálogos modales del
sistema, un formulario modal de la VCL no envía un mensaje WM_ENTERIDLE a la ventana principal durante
su actividad, pero debido a que un bucle de mensajes "modal" utiliza HandleMessage, se llamará a Idle, lo
que significa que se producirá el evento Application.OnIdle que permitirá realizar acciones en segundo
plano.
Ahora veamos cómo la VCL gestiona los mensajes recuperados de la cola de mensajes. Como se mencionó
anteriormente, para cada clase de formulario, la VCL registra la misma clase de ventana y todas las
ventanas pertenecientes a esta clase comparten el mismo procedimiento de ventana. Por otro lado, la lógica
detrás de la VCL requiere que los eventos sean procesados por la instancia del objeto que encapsula la
ventana de destino. Por lo tanto, surge la pregunta de cómo pasar un mensaje a una determinada instancia
de clase de la VCL.
La VCL resuelve este problema de la siguiente forma. El módulo Classes contiene una función
MakeObjectInstance no documentada que se describe a continuación:
type
TWndMethod = procedure(var Message: TMessage) of object;
function MakeObjectInstance(Method: TWndMethod): Pointer;

El tipo TMessage almacena información sobre el mensaje. Todos los métodos de los componentes de la VCL
relacionados con el procesamiento de mensajes usan este tipo (veremos esto con más detalle más
adelante).

36
La función MakeObjectInstance genera dinámicamente un nuevo procedimiento de ventana y devuelve
un puntero (de aquí que, cualquier aplicación de la VCL contiene código automodificable). El propósito de
este procedimiento creado dinámicamente es transferir el control al método especificado en la llamada a
MakeObjectInstance (por tanto, los diversos procedimientos de ventana generados por esta función solo
difieren en el método MainWndProc de la instancia de clase a la cual se llama).

Cada instancia de un componente de ventana crea su propio procedimiento de ventana, que pasa el
procesamiento de mensajes a su método MainWndProc. Este puntero de procedimiento se escribe en el
campo FObjectInstance. Como se explicó en la sección anterior, cuando se registra una clase de
ventana, InitWndProc se especifica como un procedimiento de ventana que crea una subclase cuando se
recibe el primer mensaje, y el procedimiento de ventana se asigna a aquel cuyo puntero se almacenó en el
campo FObjectInstance, es decir, a la función creada con MakeObjectInstance (ver Listado 1-12). Por
lo tanto, cada instancia recibe su propio procedimiento de ventana y el método MainWndProc comienza a
procesar el mensaje.
MainWndProc es un método no virtual que gestiona problemas técnicos, como eliminar la basura que queda
del procesamiento de mensajes y el manejo de excepciones. Transfiere el procesamiento real del mensaje
al método apuntado por la propiedad WindowProc. Esta propiedad es de tipo TWndMethod y por defecto
apunta al método virtual WndProc. Por lo tanto, si el desarrollador no cambió el valor de la propiedad
WindowProc, WndProc se encarga de procesar el mensaje.

El método WndProc maneja sólo aquellos mensajes que deben procesarse de forma especial para soportar
la funcionalidad de la VCL. El método WndProc procesa los mensajes del mouse de modo especial:
monitorea en qué componente visual se encuentran las coordenadas de los mensajes del mouse y si este
componente difiere de aquel en cuya área entró el mensaje anterior, se le ordena al componente del
mensaje anterior que procese el mensaje CM_MOUSELEAVE, y al nuevo el mensaje CM_MOUSEENTER. Esto
asegura que los componentes visuales respondan a la salida y entrada del mouse (en particular, generando
los eventos OnMouseExit y OnMouseEnter). La necesidad de implementar esta forma de monitorear la
salida y llegada del mouse en lugar de utilizar los mensajes del sistema WM_MOUSEELEAVE y
WM_MOUSEHOVER se debe a que los mensajes del sistema sólo válidos para trabajar con ventanas, mientras
que la VCL también monitorea la salida y llegada del mouse en componentes visuales que no son ventanas.
Sin embargo, WM_MOUSELEAVE en WndProc también sirve como medio adicional de verificar el movimiento
del mouse.
NOTA:
El método descrito aquí para monitorear la salida y entrada del mouse se implementó a partir de SDE 2006. En
versiones anteriores de Delphi, el método Application.Idle se encargaba de esto, que, como recordamos,
se llamaba sólo cuando no había mensajes en la cola. Debido a ello, a veces (por ejemplo, al mover el mouse
rápidamente) se perdían los eventos de salida y entrada del mouse, interrumpiéndose la lógica del programa.
Por esta razón a partir de BDS 2006, se cambió la forma de monitorear la salida y llegada del mouse
asignándose la responsabilidad de esto al método TWinControl.WndProc. Esto eliminó un inconveniente (la
pérdida de eventos) pero ocasiono otro. Ahora capturar y procesar mensajes del mouse antes que lo haga el
método WndProc podría llevar a perder la capacidad de monitorear la salida y llegada del mouse. Sin embargo,
este problema sólo se manifiesta cuando el desarrollador realiza ciertas acciones significativas para
implementar código en un procedimiento de ventana, por lo que es mucho menos grave que el eliminado.

37
El método WndProc procesa los eventos del mouse por su cuenta, sin la ayuda de la función
DispatchMessage. Esto se debe a que DispatchMessage pasa el mensaje al componente ventana al que
está destinado desde el punto de vista del sistema. Sin embargo, desde el punto de vista de la VCL, este
componente puede ser el padre de componentes visuales sin ventana, y si un mensaje del mouse está
asociado a su área, debe ser procesado por el componente sin ventana correspondiente, no por su padre
con ventana. DispatchMessage no "sabe" nada sobre componentes sin ventana y no puede pasarles
mensajes, por lo que los desarrolladores de la VCL tuvieron que implementarlo su propio método.
Aquellos mensajes que el método WndProc no procesa por su cuenta (y son la gran mayoría), se pasan al
método Dispatch, que se declara e implementa en la clase TObject. A primera vista, puede parecer
extraño que la propia clase base implemente una funcionalidad que se usa solo en componentes visuales.
Esta rareza se debe al hecho de que los desarrolladores de Delphi crearon soporte para el procesamiento
de mensajes directamente en el lenguaje. Los métodos de clase descritos con la directiva message sirven
específicamente para procesar mensajes. La sintaxis para describir este método es la siguiente:
procedure <Name>(var Message: <TMsgType>); message <MsgNumber>;

<MsgNumber> es el número del mensaje que el método pretende procesar. El nombre del método puede
ser cualquiera, pero tradicionalmente coincide con el nombre de la constante del mensaje, excepto que tiene
un carácter más conveniente y no tiene el carácter "_" (por ejemplo, el método para procesar WM_SIZE
sería WMSize).

El compilador permite cualquier tipo como tipo del parámetro <TMsgType>, pero en la práctica solo tiene
sentido utilizar el tipo TMessage o un tipo "compatible" con este. El tipo TMessage se describe en el
Listado 1.14.
Listado 1.14. Descripción del tipo TMessage.
TMessage = packed record
Msg: Cardinal;
case Integer of
0: (
WParam: Longint;
LParam: Longint;
Result: Longint);
1: (
WParamLo: Word;
WParamHi: Word;
LParamLo: Word;
LParamHi: Word;
ResultLo: Word;
ResultHi: Word);
end;

El campo Msg contiene el código del mensaje y los valores en los campos WParam y LParam son los
parámetros del mensaje. El campo Result es el campo de salida – el método que realiza el procesamiento
final del mensaje introduce en este campo el valor devuelto por procedimiento de ventana. Los campos con
los sufijos Hi y Lo permiten acceder por separado a las palabras más y menos significativas de los campos
correspondientes, lo que puede ser muy útil cuando estos parámetros contienen valores de 16 bits. Por
ejemplo, en el mensaje WM_NCMOUSEMOVE, las palabras menos y más significativas del parámetro LParam
contienen las coordenadas X y Y del mouse respectivamente. Si se procesa este mensaje, LParamLo
contendrá la coordenada X y LParamHi contendrá la coordenada Y del mouse.

38
"Compatible" con TMessage son aquellas estructuras que tienen el mismo tamaño y el mismo parámetro
Msg que define el mensaje. Estas estructuras son específicas para cada mensaje. Sus nombres se forman a
partir de los nombres del mensaje descartando el carácter “_” y agregando el prefijo T. Por ejemplo, para el
mensaje anterior WM_NCMOUSEMOVE el tipo correspondiente sería como el mostrado en el Listado 1.15.

Listado 1.15. Tipo TWMNCMouseMove.


TWMNCMouseMove = packed record
Msg: Cardinal;
HitTest: Longint;
XCursor: Smallint;
YCursor: Smallint;
Result: Longint;
end;

En este caso el parámetro WParam pasa a llamarse HitTest para reflejar mejor su significado y el
parámetro LParam se divide en dos partes de 16 bits: XCursor e YCursor.

El parámetro del método que procesa el mensaje es del tipo correspondiente al mensaje que se está
procesando (puede opcionalmente describir su propio tipo), o del tipo TMessage. Por lo tanto, el gestor de
mensajes para WM_NCMOUSEMOVE tendrá el aspecto mostrado en el Listado 1.16.

Listado 1.16. Declaración e implementación de un método para procesar el mensaje WM_NCMOUSEMOVE


type
TSomeForm = class(TForm)
................
procedure WMNCMouseMove(var Message: TWMNCMouseMove);
message WM_NCMOUSEMOVE;
................
end;
procedure TSomeForm.WMNCMouseMove(var Message: TWMNCMouseMove);
begin
..............
inherited; // Probablemente esta llamada no sea necesaria
end;

Un método gestor de mensajes puede procesar un mensaje en su totalidad por sí solo, en este caso no será
necesario llamar al método gestor de mensajes heredado. Sin embargo, si la respuesta de un ancestro al
mensaje en general se adapta al desarrollador, y sólo se necesita complementar, la palabra clave
inherited permite llamar a un gestor heredado para el mensaje. De este modo, se puede formar una
cadena de llamadas a gestores heredados para el mismo mensaje, cada uno de las cuales realiza su propia
parte del procesamiento. Si los ancestros de clase no tienen un gestor para el mensaje, la directiva
inherited transfiere el control al método TObject.DefaultHandler.

Volviendo al método Dispatch. Este busca entre los gestores de mensajes de la clase (propios o
heredados) un método que procese el mensaje especificado en el campo Message.Msg y, si encuentra uno,
le trasfiere el control. Si ni la propia clase ni sus ancestros contienen un gestor para el mensaje, el
procesamiento se transfiere al método DefaultHandler.

El método DefaultHandler es virtual, no realiza ninguna acción en la clase TObject, pero sus
descendientes lo sobrescriben. Se sobrescribe primero en la clase TControl para procesar los mensajes
relacionados con la obtención y el establecimiento del título de la ventana - WM_GETTEXT,
WM_GETTEXTLENGTH y WM_SETTEXT. Recordemos que la clase TControl es el ancestro de todos los
componentes visuales, no sólo de los componentes de ventana y la presencia de un gestor de mensajes del

39
sistema en esta clase es parte de la simulación del procesamiento de mensajes por parte de los
componentes que no son de ventana que ya se discutió.
El método DefaultHandler también se sobrescribe en la clase TWinControl. Además de pasar algunos
mensajes a las ventanas hijas (hablaremos de esto más adelante) y procesar algunos mensajes internos,
llama a un procedimiento de ventana cuya dirección se almacena en la propiedad DefWndProc. Esta
propiedad contiene la dirección que se asignó al campo WindowClass.lpfnWndProc de la estructura
TCreateParams en el método CreateParams. Por defecto, este campo contiene la dirección del
procedimiento de ventana estándar DefWindowProc. Como se ha dijo antes, el procesamiento de mensajes
mediante la API suele terminar con una llamada a este procedimiento.
En la clase TCustomForm, el método DefaultHandler también se sobrescribe: si el formulario es un
formulario MDI, los mensajes que se le envían se pasan al procedimiento DefFrameProc (con la excepción
de WM_SIZE, que se pasa a DefWindowProc) independientemente del valor de la propiedad DefWndProc.
Para todos los demás tipos de formularios, se llama al DefaultHandler heredado de TWinControl.

Repitamos una vez más toda la cadena de procesamiento de mensajes de parte de los componentes de
ventana de la VCL (Figura 1.7). Para cada componente, se crea un procedimiento de ventana único que pasa
el control al método MainWndProc. MainWndProc pasa el control al método cuyo puntero se almacena en
la propiedad WindowProc. Por defecto, este es un método del componente WndProc. Procesa algunos
mensajes, pero en la mayoría de casos pasa el control al método Dispatch, que busca un gestor para el
mensaje entre los métodos del componente o de sus ancestros. Si no se encuentra ningún gestor, el método
DefaultHandler toma el control (también puede tomar el control si se encuentra el gestor, pero llama a
inherited). DefaultHandler procesa algunos mensajes de por sí, pero la mayoría se pasan a un
procedimiento de ventana cuya dirección se almacena en la propiedad DefWndProc (por defecto, esta es la
función predeterminada de la API de Windows DefWindowProc).

La clase TControl tiene al método Perform que se puede utilizar para forzar a un componente visual a
procesar un mensaje en particular, evitando al procedimiento de ventana y al mecanismo de paso de
mensajes del sistema. Perform provoca una llamada directa de un método, cuyo puntero se almacena en la
propiedad WindowProc. Además, la cadena de procesamiento de mensajes la misma que cuando se recibe
un mensaje a través de un procedimiento de ventana. Para los componentes de ventana, llamar a Perform
es casi equivalente a enviar un mensaje mediante SendMessage con dos excepciones. En primer lugar,
cuando se usa SendMessage, el sistema prevé la conmutación entre procesos y el mensaje se ejecutará en
el proceso que creó la ventana, mientras que Perform no prevé ninguna conmutación y el procesamiento
del mensaje se ejecutará en el proceso que llamó a Perform. Por ello, Perform, a diferencia de
SendMessage, sólo se puede utilizar en el proceso principal (recordemos que la VCL en principio es una
librería de un solo proceso, y crear formularios fuera del proceso principal es inadmisible). En segundo
lugar, Perform es ligeramente más rápido porque el procedimiento de ventana y el método MainWndProc
están excluidos de la cadena de procesamiento de mensajes.

40
Figura 1.7. Diagrama de bloques de un procedimiento de ventana para componentes de ventana de la VCL

41
Pero la principal ventaja de Perform sobre SendMessage es que Perform es adecuado para trabajar con
todos los componentes visuales, no sólo con componentes de ventana. Los componentes visuales que no
son de ventana pueden no tener un procedimiento de ventana, pero sí tienen una cadena de procesamiento
de mensajes. Carecen de un procedimiento de ventana y del método MainWndProc, y DefaultHandler no
llama a ningún procedimiento de ventana estándar, pero por lo demás la cadena es completamente
equivalente a la cadena de componentes de ventana. Por lo tanto, la cadena de procesamiento de mensajes
de los componentes de ventana tiene dos puntos de entrada: el procedimiento de ventana y el método
Perform, mientras que la cadena de los componentes que no son de ventana sólo tiene al método
Perform. Por lo tanto, el método Perform es universal: funciona igualmente bien con componentes de
ventana y sin ella. Se utiliza mucho en la VCL porque permite trabajar uniformemente con cualquier
componente visual.
Los componentes visuales que no son de ventana reciben mensajes de su ventana padre. Por ejemplo, como
ya se mencionó, el procesamiento de mensajes relacionados con el mouse en la clase TWinControl
implica comprobar si las coordenadas del cursor caen dentro de los límites de cualquiera de los
componentes hijos que no son de ventana. Y si lo hace, el componente de ventana no procesa este mensaje
por sí mismo, sino que lo transmite al componente apropiado que no es de ventana mediante Perform. Esta
transferencia asegura que los mensajes sean recibidos por los componentes que no son de ventana.
Los mensajes en la VCL se transmiten no solo a los componentes que no son de ventana, sino también a los
componentes de ventana. En Windows, todos los mensajes que informan sobre un cambio en el estado de
los controles estándar los recibe su ventana padre y no el control en sí. Por ejemplo, cuando se hace clic en
un botón, no se recibe un mensaje de notificación al respecto en el botón en sí, sino en la ventana que lo
contiene. El botón en sí recibe y procesa solo aquellos mensajes que normalmente no son de interés para el
desarrollador. Esto simplifica el trabajo del desarrollador, ya que no es necesario que cada control escriba
su propio procedimiento de ventana, todos los mensajes significativos son recibidos por el procedimiento de
ventana de la ventana padre.
Considere lo que sucede cuando se hace clic sobre un botón de un formulario. La ventana que contiene este
botón recibe el mensaje WM_COMMAND que notifica que se ha producido un evento entre los componentes de
la ventana. Los parámetros del mensaje permiten determinar qué evento y en qué control ocurrió el evento
(en este caso el evento sería BN_CLICKED). El gestor WM_COMMAND de la clase TWinControl busca al
componente que generó el mensaje y le envía el mensaje CN_COMMAND (como se puede ver en el prefijo, se
trata de un mensaje interno de la VCL) con los mismos parámetros. En nuestro ejemplo, será una instancia
de la clase TButton que implementa el botón que el usuario pulso. Al recibir CN_COMMAND, el componente
comienza a procesar el evento que le ocurrió (en particular, TButton inicia el evento OnClick).

NOTA:
Sobrescribir el gestor de WM_COMMAND debe ser tratado con cuidado para no interrumpir el mecanismo de
emisión de mensajes. La clase TCustomGrid es un ejemplo de sobrescribir incorrectamente. A menudo, en los
foros se puede encontrar la pregunta de por qué los controles cuyos padres son TDrawGrid o TStringGrid
no se comportan correctamente: los botones no generan eventos OnClick, las listas desplegables permanecen
vacías, etc. Esto se debe a que el gestor de WM_COMMAND en TCustomGrid solo considera la posibilidad de un
componente hijo – el editor interno que aparece cuando la opción goEditing está habilitada. El mensaje
WM_COMMAND no se trasmite a otros componentes hijos, privándoles de la capacidad de responder
correctamente a los eventos que les ocurran. La salida puede ser o bien crear un descendiente de TDrawGrid o
TStringGrid que trasmita correctamente el mensaje WM_COMMAND, o bien designar como ventana principal a

42
un componente insertado en la cuadricula, formulario, panel u otro componente de ventana que traduzca
correctamente el mensaje.

Veamos todos los métodos mediante los cuales se puede insertar código en la cadena de procesamiento de
mensajes de un componente de ventana y capturar mensajes. Hay seis formas de hacerlo en total.
1. Como con cualquier ventana, puede cambiar el procedimiento de ventana con SetWindowLong.
Mejor evitar usar este método porque el código de la VCL no "sabrá" nada sobre esta sustitución, y
los mensajes recibidos por el componente mediante Perform en lugar del procedimiento de
ventana, no serán capturados. Otra desventaja de este método es que es imposible cambiar algunas
propiedades del componente (por ejemplo, FormStyle y BorderStyle del formulario) sin destruir
la ventana y crear una nueva. Para el desarrollador, volver a crear la ventana parece transparente,
pero la nueva ventana obtendrá un nuevo procedimiento de ventana y tendrá que realizar una nueva
captura. Puede rastrear la ventana hasta el momento en que se vuelve a crear mediante el mensaje
CM_RECREATEWND, en cuyo gestor se destruye la ventana anterior y se pospone la creación de una
nueva ventana hasta la primera llamada a la propiedad Handle. Si captura este mensaje, entonces,
en principio, después de ejecutar el gestor estándar, puede volver a instalar la captura mediante
SetWindowLong, pero ya que este método no ofrece ninguna ventaja sobre los otros, más simples,
mejor no usarlo.
2. Puede crear su propio método de procesamiento de mensajes y colocar un puntero de este en la
propiedad WindowProc. Al hacerlo, se suele guardar el puntero anterior, ya que el nuevo gestor
procesa sólo algunos mensajes y pasa el resto al puntero anterior. En este enfoque, la ventaja es
que el método cuyo puntero se coloca en WindowProc no tiene que pertenecer al componente
cuyos mensajes se capturan. Esto permite, en primer lugar, crear componentes que afectan al
procesamiento de mensajes mediante los formularios padre y, en segundo lugar, implementar un
procesamiento de mensajes no estándar mediante componentes estándar sin dar lugar a la
herencia.
3. Al escribir un nuevo componente, puede sobrescribir el método virtual WndProc e implementar el
procesamiento de mensajes necesario. Esto permite que el componente intercepte mensajes al
principio de la cadena (con la excepción de los gestores externos establecidos mediante la
propiedad WindowProc – aquí el desarrollador del componente no tiene control).

4. La forma más conveniente de procesar eventos es escribiendo uno mismo sus métodos de
procesamiento. Este enfoque es el más común. Su desventaja es que los códigos de los mensajes a
procesar se deben conocer en la fase de compilación. Para los mensajes del sistema y los
mensajes internos de la VCL esta condición se cumple, pero más adelante hablaremos de los
mensajes definidos por el usuario, cuyos códigos en algunos casos se desconocen en la fase de
compilación. No es posible procesar estos mensajes mediante los métodos con la directiva
message.

5. Puede sobrescribir el método virtual DefaultHandler para capturar mensajes que no hayan sido
procesados por los métodos del controlador.
6. Finalmente, puede escribir un procedimiento de ventana y colocar un puntero de este en la
propiedad DefWndProc. Este método es prácticamente equivalente en sus capacidades al anterior,

43
pero menos conveniente. Sin embargo, el método anterior sólo es adecuado para crear su propio
componente, mientras que DefWndProc se puede modificar en instancias de clases existentes.
Recuerde que este método no es adecuado para formularios con FormStyle = fsMDIForm,
porque dichos formularios ignoran el valor de la propiedad DefWndProc.

Todos los métodos anteriores son aceptables para capturar mensajes de componentes visuales que no son
de ventana, excepto el primero y el último.
El método WndProc de un componente de ventana transmite los mensajes del mouse a los componentes
visuales que no son de ventana de los que él es el padre. Por ejemplo, si se coloca un componente TImage
en un formulario y reemplaza el método para procesar el mensaje WM_LBUTTONDOWN, entonces hacer clic
en TImage no hará que se llame a este método, porque WndProc enviará este mensaje a TImage y no se
llamará a Dispatch. Sin embargo, si remplaza WndProc o cambia el valor de la propiedad WindowProc
(es decir, utiliza el segundo o tercer método de captura), también puede recibir y procesar los mensajes "del
mouse" que también deben transmitirse a los componentes hijos que no son de ventana. Es una regla
general: cuanto antes inserte su propio código en la cadena de procesamiento de mensajes, más
posibilidades tendrá.
Como ya hemos mencionado, a partir de BDS 2006 se introdujo otro método de captura de mensajes:
sobrescribiendo el método PreProcessMessage. Este método no se puede comparar a los seis métodos
anteriores, ya que tiene dos diferencias importantes. En primer lugar, captura todos los mensajes que caen
en el bucle de mensajes, no sólo los enviados a un componente en particular, lo que puede requerir un
filtrado de mensajes adicional. En segundo lugar, el método PreProcessMessage captura los mensajes
que caen en el bucle de mensajes en lugar de en el procedimiento de ventana del componente. Por un lado,
esto permite capturar aquellos mensajes que el método Application.ProcessMessage no considera
necesario pasar al procedimiento de la ventana, pero, por otro lado, no permite capturar aquellos mensajes
que recibe la ventana omitiendo el bucle de mensajes (por ejemplo, los enviados por SendMessage o
Perform). Por estas razones, el alcance de este método es muy diferente de los métodos que implican
insertar código en un procedimiento de ventana. Sobrescribir PreProcessMessage es bastante
comparable a utilizar el evento Application.OnMessage.

Las diferentes formas de captura de mensajes se ilustran con una serie de ejemplos en el CD que acompaña
al libro: el uso de la propiedad WindowProc se muestra en los ejemplos Line, CoordLabel y PanelMsg,
el reemplazo del método WndProc en el ejemplo NumBroadcast y la creación de un método para procesar
el mensaje en el ejemplo ButtonDel.

Los mensajes son muy útiles cuando se quiere que una ventana haga algo. Para ello, Windows permite que
los desarrolladores creen sus propios mensajes. Existen tres tipos de mensajes de usuario:
▪ Mensajes para clases de ventana;
▪ Mensajes para la aplicación;
▪ Mensajes globales (cadena).
A cada grupo se le asigna un rango de numeración distinto. La numeración de los mensajes estándar va de 0
a WM_USER-1 (WM_USER es una constante, 1024 para las versiones de 32 bits de Windows).

44
Los mensajes para clases de ventana tienen números que van desde WM_USER hasta WM_APP-1 (con
WM_APP igual a 32768). El desarrollador puede elegir en este rango números arbitrarios para sus mensajes.
Cada mensaje debe tener sentido sólo para una clase de ventana específica. Es posible definir mensajes con
números idénticos para diferentes clases de ventana. El sistema no garantiza que los mensajes definidos
para una clase de ventana en particular se envíen sólo a las ventanas de esta clase – el desarrollador tiene
que encargarse de esto por sí mismo. En este mismo rango también se encuentran los mensajes específicos
para las clases de ventana estándar como 'BUTTON', 'EDIT', 'LISTBOX', 'COMBOBOX', etc. El uso de mensajes
de este rango se ilustra en el ejemplo ButtonDel.

El rango desde WM_APP hasta 49151 (para este valor no existe una constante) pertenece a los mensajes para
la aplicación. El desarrollador también puede elegir de este rango números arbitrarios para los mensajes.
En este caso el sistema si garantiza que las clases de ventana estándar no usen mensajes de este rango.
Esto permite que solo se transmitan dentro de la aplicación. Ninguna de las clases estándar responderá a
tales mensajes ni realizará acciones indeseadas.
Los mensajes internos de la VCL que se mencionaron anteriormente con los prefijos CM_ y CN_ tienen
números en el rango de 45056 a 49151, es decir, utilizan parte del rango de los mensajes para la aplicación.
Al utilizar la VCL, el rango de mensajes para la aplicación se reduce a WM_APP...45055.

Los mensajes para clases de ventana y mensajes para la aplicación también son adecuados para interactuar
con otras aplicaciones, pero el emisor debe estar seguro de que el receptor lo entenderá correctamente. En
este caso se excluye la difusión, ya que la respuesta de otras aplicaciones que también recibirán el mensaje
puede ser imprevisible. No obstante, si es necesario enviar mensajes de difusión entre aplicaciones, se
deben usar mensajes globales, para los cuales se reserva el rango de números de 49152 a 65535.
Un mensaje global debe tener un nombre (por lo que dichos mensajes también se denominan mensajes de
cadena) con el cual se registra en el sistema mediante la función RegisterWindowMessage. Esta función
devuelve un número único que pertenece al mensaje registrado. Si es la primera vez que se registra el
mensaje con este nombre, se selecciona el número entre los que aún están disponibles. Si ya se registró un
mensaje con el mismo nombre, se devuelve el mismo número asignado cuando se registró por primera vez.
De esta forma, diferentes programas que registren mensajes con el mismo nombre recibirán los mismos
números y podrán entenderse entre sí. Para otras ventanas, este mensaje no tendrá ningún sentido.
La creación y el uso de mensajes de ventana se muestran en el ejemplo NumBroadcast que llega con el
CD-ROM.
Por supuesto, es posible que dos aplicaciones diferentes elijan el mismo nombre para sus mensajes
globales y esto cause problemas al transmitir los mensajes. Pero, al darle a sus mensajes nombres
significativos en lugar de algo como WM_MYMESSAGE1, la posibilidad de que coincidan será muy pequeña. En
situaciones especialmente críticas, se puede utilizar un GUID como nombre del mensaje, cuya exclusividad
está garantizada.
La numeración de los mensajes globales solo se conoce durante la etapa de ejecución del programa. Esto
significa que no se pueden utilizar los métodos con la directiva message para procesarlos, sino que se
deben sobrescribir los métodos WndProc o DefaultHandler.

45
Algunos mensajes no se envían ni se procesan de acuerdo con las reglas generales, sino con distintas
excepciones. La siguiente lista de mensajes no pretende ser exhaustiva, pero aun así puede darle una idea
de tales excepciones.
El mensaje WM_COPYDATA se usa para transferir un bloque de datos de un proceso a otro. En las versiones
de Windows de 32 bits la memoria asignada a un proceso no está disponible para todos los demás procesos.
Por eso no se puede simplemente pasar un puntero a otro proceso, ya que no podrá acceder a esta zona de
memoria. Cuando se envía un mensaje WM_COPYDATA, el sistema copia el bloque especificado desde el
espacio de direcciones del emisor al espacio de direcciones del receptor, pasa el puntero al receptor y
libera el bloque cuando termina el procesamiento del mensaje. Todo esto requiere de una cierta
sincronización de acciones que no se puede lograr al enviar un mensaje, por lo que con WM_COPYDATA sólo
es posible enviar, pero no publicar (es decir, se puede usar SendMessage, pero no PostMessage).

El mensaje WM_PAINT está destinado a redibujar el área cliente de una ventana. Si la imagen es compleja,
este proceso lleva mucho tiempo, por lo que Windows proporciona mecanismos que minimizan el número de
redibujos. Una ventana debe redibujar su contenido cuando recibe el mensaje WM_PAINT. Cada uno de estos
mensajes está asociado a una región que debe redibujarse. Esta región puede coincidir o formar parte del
área cliente de la ventana o formar parte de ella. En este último caso, el programa puede acelerar el
redibujo al no dibujar toda la ventana, sino solo la parte que se necesita (la VCL ignora la posibilidad de
redibujar sólo una parte de la ventana, por lo que cuando se trabaja con esta biblioteca, la ventana siempre
se redibuja completamente). No es posible enviar un mensaje WM_PAINT mediante PostMessage u una
ventana, porque no está en cola. En su lugar, puede marcar la región como que requiere actualización
mediante las funciones InvalidateRect e InvalidateRgn. Si, al momento de llamar a estas funciones,
la región a actualizar no estaba vacía cuando, la nueva región se combina con la región anterior. Las
funciones GetMessage y PeekMessage devuelven el mensaje WM_PAINT si la cola de mensajes está vacía
y la región a actualizar no está vacía. Por lo tanto, el redibujo de la ventana se pospone hasta el momento en
que se procesan todos los demás mensajes. Tampoco es posible enviar WM_PAINT mediante SendMessage.
Si una ventana necesita ser redibujada inmediatamente, se debe llamar a las funciones UpdateWindow o
RedrawWindow que no sólo envían un mensaje a la ventana, sino que también realizan acciones
relacionadas con la región de actualización.
El procesamiento del mensaje WM_PAINT también tiene algunas características especiales. El gestor debe
obtener el contexto del dispositivo de la ventana (ver sección 1.1.11 de este capítulo) mediante la función
BeginPaint y liberarlo mediante EndPaint cuando se haya terminado. Estas funciones deben llamarse
sólo una vez al procesar el mensaje. En consecuencia, si un mensaje se procesa en etapas por múltiples
gestores, como es el caso de la captura de mensajes, sólo el primer gestor debe recibir y liberar el contexto
del dispositivo, y los demás deben usar el contexto recibido.
El sistema no impone requisitos obligatorios que resuelvan el problema, sino que ofrece una solución que
utilizan todas las clases de sistemas predefinidos. Cuando se recupera un mensaje WM_PAINT de la cola, su
parámetro wParam es cero. Sin embargo, si el manejador recibe un mensaje con wParam <> 0, trata el
valor de este parámetro como un manejador para el contexto del dispositivo y lo utiliza en lugar de obtener
el manejador a través de BeginPaint. El primero en la cadena de manejadores debe pasar un mensaje
hacia abajo en la cadena con el parámetro wParam cambiado. Los componentes de la VCL también se
aprovechan de esta solución. Al capturar un mensaje WM_PAINT, hay que tener en cuenta esto.

46
El sistema no impone requisitos obligatorios que podrían resolver el problema, pero ofrece una solución que
utilizan todas las clases predefinidas del sistema. Cuando se recupera un mensaje WM_PAINT de la cola, su
parámetro wParam es cero. Sin embargo, si el gestor recibe un mensaje con wParam <> 0, trata el valor
de este parámetro como el gestor para el contexto del dispositivo y lo utiliza en lugar de obtener el gestor a
través de BeginPaint. El primero en la cadena de gestores debe pasar un mensaje a la cadena con el
parámetro wParam modificado. Los componentes de la VCL también aprovechan esta solución. Al capturar
el mensaje WM_PAINT, hay que tenerlo en cuenta.

Los ejemplos PanelMsg y Line disponibles en el CD demuestran cómo capturar correctamente un mensaje
WM_PAINT. Los temporizadores estándar creados por el sistema mediante la función SetTimer informan
su vencimiento a través del mensaje WM_TIMER. La comprobación de si el intervalo ha caducado se realiza
dentro de GetMessage y PeekMessage. Por lo tanto, si estas funciones no se llaman durante mucho
tiempo, WM_TIMER no se pondrá en cola, incluso si el plazo expira. Si este intervalo expira varias veces
mientras se procesan otros mensajes, sólo se pone un mensaje WM_TIMER en la cola. Si ya hay un mensaje
WM_TIMER en la cola, no se agrega uno nuevo incluso si el intervalo expira. Por lo tanto, algunos mensajes
WM_TIMER se pierden, es decir, si el intervalo del temporizador se establece en un segundo, entonces no se
recibirán 3600 mensajes WM_TIMER por hora, y la diferencia será mayor cuanto más intensamente la
aplicación cargue al procesador.
NOTA:
La clase TTimer encapsula un temporizador que actúa por medio de WM_TIMER. Los mensajes son recibidos
por una ventana invisible creada específicamente para este propósito. Por lo tanto, el evento OnTimer también
se producirá menos de 3600 veces en una hora con un intervalo de un segundo.

Los mensajes del teclado también tienen algunos detalles. Al procesar este tipo de mensajes, puede utilizar
la función GetKeyState, que devuelve el estado de cualquier tecla (pulsada o soltada) en el momento en
que se produjo el evento. Exactamente en el momento de la ocurrencia, y no en el momento de la llamada a
la función. Si se utiliza la función GetKeyState cuando se procesa un mensaje que no es del teclado,
devolverá el estado de la tecla en el momento en que se recuperó el último mensaje del teclado de la cola.

La parte de la API de Windows que se encarga de los gráficos suele llamarse GDI (Graphic Device Interface).
El concepto clave de la GDI es el Contexto de Dispositivo (DC). Un contexto de dispositivo es un objeto
especifico que almacena información sobre las capacidades del dispositivo, la forma en que se trabaja con
él y qué está permitido modificar. En Delphi, el contexto de dispositivo está representado por la clase
TCanvas, cuya propiedad Handle contiene al descriptor de un contexto de dispositivo. TCanvas es
universal en el sentido de que hace que dibujar en una ventana, en una impresora o en un metarchivo se vea
igual. Lo mismo ocurre con el contexto de dispositivo. La única diferencia es cómo se obtiene el descriptor
del contexto en diferentes casos.
La mayoría de los métodos de la clase TCanvas son "calcados" de las funciones GDI correspondientes (en
la mayoría de casos). Pero en algunos otros (principalmente en los métodos de salida de texto y de dibujo de
polígonos), los parámetros de los métodos en TCanvas son de un tipo más conveniente que el de las
funciones GDI. Por ejemplo, el método TCanvas.Polygon requiere una matriz de elementos TPoint como

47
parámetro, mientras que la función GDI correspondiente requiere un puntero a una región de memoria que
contenga coordenadas de puntos y el número de puntos. Esto significa que es necesario asignar memoria
antes de llamar a la función y liberarla después. También se necesita código que llene este espacio de
memoria con los valores requeridos. Y en ningún caso debe equivocarse en el número de elementos de la
matriz. Si reserva memoria para un número de puntos y especifica otro al llamar a la función, el programa
no funcionará correctamente. Pero para funciones simples, trabajar con la GDI no es más difícil que a través
de TCanvas.

Existe varias funciones para obtener el descriptor de un contexto de dispositivo. Existen cuatro funciones
sólo para obtener el descriptor del contexto de una ventana estándar: BeginPaint, GetDC, GetWindowDC
y GetDCEx. La primera función devuelve el contexto del área cliente de la ventana al procesar el mensaje
WM_PAINT. La segunda devuelve el contexto del área cliente de la ventana que puede ser utilizado en
cualquier momento, no sólo cuando se procesa WM_PAINT. La tercera devuelve el contexto de toda la
ventana, incluyendo el área no cliente. La última permite obtener el contexto de un área específica del área
cliente de la ventana.
Una vez obtenido el descriptor de contexto, se puede aprovechar la clase TCanvas. Para ello, debe crear
una instancia de esta clase y asignar a su propiedad Handle al descriptor obtenido. La liberación de
recursos debe hacerse en el siguiente orden: primero, la propiedad Handle se establece en cero, luego se
destruye la instancia de la clase TCanvas y finalmente se libera el contexto de dispositivo mediante una
función GDI adecuada. Un ejemplo del uso de la clase TCanvas se muestra en el Listado 1-17.

Listado 1.17. Uso de la clase TCanvas para trabajar con un contexto de dispositivo arbitrario.
var
DC: HDC;
Canvas: TCanvas;
begin
DC := GetDC (...); // Aquí hay otras formas de obtener el DC
Canvas := TCanvas.Create;
try
Canvas.Handle := DC;
// Aquí se dibuja con Canvas
finally
Canvas.Free;
end;
// Liberar un objeto Canvas no libera el contexto de dispositivo DC
// DC debe quitarse manualmente
ReleaseDC(DC);
end;

En el ejemplo panelMsg del CD-ROM se muestra el uso de la clase TCanvas para dibujar en un contexto de
dispositivo para el cual existe un descriptor.
Por supuesto, se puede llamar a funciones GDI mientras se trabaja con TCanvas. Para ello, sólo necesita
pasar como descriptor del contexto el valor de la propiedad Canvas.Handle. Haremos una breve
descripción de las funciones GDI que, por alguna razón, los desarrolladores de la VCL no consideraron
necesario incluir en el TCanvas: trabajo con regiones y trayectorias; alineación del texto a cualquier
esquina o centro; establecer un sistema de coordenadas personalizado; obtención de información detallada
sobre el dispositivo; uso de lápices geométricos; inclinación del texto respecto a la horizontal; opciones
avanzadas de salida de texto; una serie de opciones para dibujar curvas y polígonos con una sola función;
soporte para modos de relleno. A todas estas funciones sólo se puede acceder a través de la API. Tenga en
cuenta también que Windows NT/2000/XP admite más funciones gráficas que 9x/ME. Las funciones que no

48
tienen soporte en 9x/ME tampoco tienen análogos en los métodos de TCanvas, de ser así los programas
escritos con esta clase podrían ejecutarse en estas versiones de Windows.
La GDI proporciona capacidades de conversión de coordenadas muy interesantes, pero sólo en Windows
NT/2000/XP; no son compatibles con Windows 9x/ME. Con la función SetWorldTransform, se puede
especificar una matriz de transformación de coordenadas arbitraria y todas las operaciones gráficas
adicionales funcionarán en el nuevo sistema de coordenadas. La matriz nos permite definir
transformaciones de coordenadas como rotación, desplazamiento del origen de coordenadas y escalado, es
decir, las posibilidades son muy amplias. También hay una función de transformación de coordenadas
menos flexible, pero también útil, SetMapMode, que es compatible con todas las versiones de Windows. Con
ayuda de esta función se puede configurar el sistema de coordenadas para que en todas las funciones se
especifique coordenadas, por ejemplo, en milímetros y no en píxeles. Esto permite usar el mismo código de
salida para dispositivos con diferentes resoluciones.
Algunas características de la GDI que no tienen análogos en TCanvas se muestran en el ejemplo GDIDraw.

Para especificar un color en la GDI, se proporciona el tipo COLORREF (en el módulo windows también define
su equivalente en Delphi, TColorRef). Es un entero sin signo de 4 bytes cuyo byte más significativo
especifica el formato de la representación del color. Si este byte es cero (llamaremos a este formato cero),
el primer, segundo y tercer byte representan las intensidades de color rojo, verde y azul respectivamente. Si
el byte más significativo es 1, los dos bytes menos significativos almacenan el índice de color de la paleta del
dispositivo actual, el tercer byte no se usa y debe ser cero. Si el byte más significativo es 2, los bytes
restantes, al igual que en el formato cero, muestran la intensidad de los componentes de color.
El tipo TColorRef permite cambiar la profundidad de cada canal de color de 0 a 255, de esta forma
proporciona 16,777,216 tonos diferentes (esto corresponde al modo TrueColor). Si la resolución de color
del dispositivo es baja, la GDI selecciona el color más parecido posible de la paleta. Si el byte más
significativo de TColorRef es 0, el color se elige de la paleta actual del sistema (por defecto esta paleta
contiene sólo 20 colores, por lo que los resultados están lejos de ser perfectos). Si el byte más significativo
es 2, la GDI selecciona el color más parecido de la paleta del dispositivo. En este caso los resultados son
más aceptables. Si el dispositivo tiene una mayor profundidad de color y no utiliza la paleta, no hay
diferencia entre el cero y segundo formato de COLORREF.

NOTA:
Aunque el modo HighColor (32,768 o 65,536 colores) no tiene suficiente profundidad de color para representar
a todos los valores posibles de TColorRef, el color más parecido no se elige de una paleta (no se usa paletas
en este modo), sino de todos los colores que el dispositivo puede mostrar. Por lo tanto, seleccionar en este
modo el formato cero da buenos resultados.

La API de Windows define las macros RGB, PaletteIndex y PaletteRGB (y en el módulo windows, las
funciones del mismo nombre, respectivamente). RGB toma tres parámetros – las intensidades de los
componentes de color rojo, verde y azul y construye a partir de ellos un valor de tipo TColorRef de
formato cero. PaletteIndex toma como parámetro el número del color de la paleta y basado en ello
construye un valor de formato uno. La macro PaletteRGB es equivalente a RGB, excepto que establece en
dos el valor de retorno del byte más significativo. Las funciones GetRValue, GetGValue y GetBValue se

49
utilizan para extraer las intensidades de los componentes de color individuales de un valor de tipo
TColorRef.

El sistema define dos valores de color especiales: CLR_NONE ($1FFFFFF) y CLR_DEFAULT ($20000000).
Se utilizan solo en listados de imágenes para especificar el fondo y los colores de superposición cuando se
muestra la imagen. CLR_NONE especifica que no hay ningún color de fondo o superposición (en cuyo caso no
se aplica el efecto visual correspondiente), CLR_DEFAULT establece el color especificado para todo el
listado.
La VCL proporciona el tipo TColor, definido en el módulo Graphics, para representar colores. Es un número
de 4 bytes cuyo conjunto de valores es un superconjunto de valores de tipo TColorRef. Se agregó el formato
255 a los formatos cero, uno y dos del sistema. Si el byte más significativo del valor de un TColor es 255, el
byte menos significativo se interpreta como el índice de color del sistema (en este caso el segundo y tercer
byte se ignoran). Los colores del sistema son los que utiliza el sistema para dibujar los distintos elementos
de la interfaz de usuario. Los valores de color RGB específicos dependen de la versión de Windows y del
esquema de color actual. Se puede obtener el valor de color RGB del sistema mediante la función
GetSysColor. El formato 255 de TColor elimina la necesidad de llamar explícitamente a esta función.
El tipo TColor tiene definido una serie de constantes para facilitar su uso. Algunos de ellas corresponden a
un color RGB especifico (clWhite, clBlack, clRed, etc.), otras corresponden a un color específico del sistema
(clWindow, clHighlight, clBtnFace, etc.). Los valores de color RGB se definen en el formato cero. Esto no
conducirá a una pérdida de precisión de color en los modos de paleta, ya que las constantes se definen solo
para los 16 colores primarios que necesariamente están presentes en la paleta del sistema. Los valores
CLR_NONE y CLR_DEFAULT corresponden a las constantes clNone y clDefault. Sirven (además del listado de
imágenes) para especificar el color transparente en un mapa de bits. Si este color es igual a clNone, la
imagen se considera opaca, si es clDefault, se considera como color transparente al píxel inferior izquierdo.
Siempre que se requiera un valor de tipo TColor, se puede sustituir TColorRef, es decir, todas las
propiedades y parámetros de los métodos de la clase TCanvas que son de tipo TColor se pueden asignar a
los valores TColorRef generados por las funciones API. Lo contrario no es cierto: las funciones API no saben
cómo procesar el formato 255 de TColor. La conversión de TColor a TColorRef se realiza mediante la función
ColorToRGB. Los valores de los formatos cero, uno y dos, así como clNone y clDefault se dejan sin cambios,
y los valores del formato 255 se convierten a cero mediante la función GetSysColor. Use esta función al
pasan valores TColor a funciones GDI.
El uso de plumas, brochas y fuentes en la GDI es fundamentalmente diferente de cómo se hace en la VCL. La
clase TCanvas tiene las propiedades Pen (Pluma), Brush (Brocha) y Font (Fuente), cuyo cambio de estas
propiedades conduce a la selección de una pluma, brocha o fuente. En GDI, estos objetos son
independientes, deben crearse, obtenerse su descriptor, "seleccionarse" en el contexto del dispositivo
deseado mediante la función SelectObject y destruirse después usarse. Además, solo se pueden eliminar
aquellos objetos que no están seleccionados en ningún contexto. También existen algunos objetos estándar
que no necesitan ser creados o destruidos. Sus descriptores se pueden obtener mediante la función
GetStockObject. Como ejemplo, considere el fragmento de código que dibuja dos líneas en un contexto con
un descriptor DC: azul y rojo (Listado 1.18). Este código usa la función SelectObject para devolver el
descriptor del objeto relacionado con el objeto seleccionado previamente. Por lo tanto, al seleccionar una
nueva pluma, devolverá el descriptor de la pluma que se seleccionó antes.
Listado 1.18. Dibujar con diferentes plumas usando la GDI.

50
SelectObject(DC, CreatePen(PS_SOLID, 1, RGB(255, 0, 0)));
MoveToEx(DC, 100, 100, nil);
LineTo(DC, 200, 200);
DeleteObject(SelectObject(DC,
CreatePen(PS_SOLID, 1, RGB(0, 0, 255))));
MoveToEx(DC, 200, 100, nil);
LineTo(DC, 100, 200);
DeleteObject(SelectObject(DC, GetStockObject(BLACK_PEN)));

Los descriptores de objetos GDI sólo tienen sentido dentro del proceso que los creó; no se pueden transferir
entre procesos. Sin embargo, en ocasiones se afirma que esta transferencia es posible. El origen de este
error es que los descriptores de objetos GDI se podían transferir entre procesos en la versión antigua de
16bits de Windows, por lo que todas las afirmaciones de que es posible la transferencia de descriptores
entre procesos se basan simplemente en información obsoleta.
Existen tres formatos para almacenar imágenes de mapa de bits en Windows: DDB, DIB y DIB-Section. DDB
es un formato dependiente del dispositivo, un formato definido por el dispositivo gráfico, al cual se envía la
imagen. DIB es un mapa de bits independiente del dispositivo, un formato único para todos los dispositivos.
Actualmente el formato DIB es un formato obsoleto que no permite utilizar las funciones gráficas de la GDI
para modificar la imagen. La imagen sólo se puede modificar de una forma, escribiendo su propio código. En
la versión de 32bits, apareció otro formato, DIB-Section. Es esencialmente el mismo DIB, pero con la
capacidad adicional de poder modificar la imagen utilizando las funciones graficas de la GDI. Todas las
diferencias entre estos tres formatos los puede leer en el excelente libro [1]; aquí nos limitaremos a un
breve resumen.
El formato DDB es compatible con la propia tarjeta de video (u otro dispositivo de salida), por lo que cuando
se opera sobre esta imagen interviene un acelerador de gráficos (hardware). Los raster DDB se almacenan
en grupos de memoria paginadas del sistema (Windows NT/2000/XP) o en el head de la GDI (Windows
9x/ME). El tamaño de un raster DDB no puede exceder de 16 MB en Windows 9x/ME y de 48 MB en Windows
NT/2000/XP. El formato DDB no es portátil de un dispositivo a otro, solo puede usarse dentro de un
dispositivo. El acceso directo a la imagen y su modificación por código propio no son posibles, porque el
formato de almacenamiento de la imagen de un dispositivo en particular es especifico. Solo se puede
modificar un DDB por medio de funciones graficas de la GDI. La profundidad de color de las imágenes DDB
viene determinadas por el dispositivo.
Un DIB-Section se puede almacenar en cualquier área de memoria, su tamaño solo está limitado por el
tamaño de la memoria disponible para la aplicación, para dibujar sobre esta imagen las funciones GDI sólo
utilizan algoritmos de software, no usan aceleradores de hardware de ninguna forma. DIB-Secction admite
diferentes profundidades de color y acceso directo al área de memoria donde se almacena la imagen, es
transferible de un dispositivo a otro. Los archivos BMP almacenan la imagen como un DIB.
La velocidad de trabajo con una imagen de formato DIB-Section depende solo del rendimiento del
procesador, de la memoria y de la calidad de la implementación de los algoritmos gráficos por parte del
sistema (que, debo decir, se implementan muy bien en Windows). La velocidad de trabajo con una imagen de
formato DDB también depende del controlador y del acelerador de hardware de la tarjeta de video. En
primer lugar, el controlador y el acelerador de hardware pueden soportar o no primitivas gráficas de dibujo
(en este último caso, estas primitivas son dibujadas por el sistema; puede averiguar qué operaciones
soporta el controlador mediante la función GetDeviceCaps). Hasta hace poco, era característica la situación
en la que dibujar una imagen en un ráster DDB y mostrar dicho ráster en la pantalla era notablemente (a
veces dos o tres veces) más rápido que las mismas operaciones con DIB-Section. Sin embargo, ahora la
diferencia es mucho menor, el rendimiento del sistema en su conjunto ha crecido más que el rendimiento de

51
los aceleradores de hardware 2D (aparentemente, los desarrolladores de tarjetas de video ya no consideran
que los gráficos 2D sean un cuello de botella y, por lo tanto, han centrado sus esfuerzos en el desarrollo
aceleradores de gráficos de hardware 3D). En algunas computadoras potentes puede incluso darse la
situación de que la imagen DDB vaya por detrás de una DIB.
La clase TBitmap puede almacenar imágenes DDB y DIB-Section – según lo determinado por el valor de la
propiedad PixelFormat. El valor pfDevice indica el uso de DDB, los valores restantes son DIB-Section con
diferentes profundidades de color. Por defecto, TBitmap crea una imagen con el formato pfDevice, pero el
desarrollador puede cambiarlo en cualquier momento. En este caso, se crea una nueva imagen con el
formato requerido, copia la antigua y la destruye.
Estrechamente relacionada con la propiedad PixelFormat está la propiedad HandleType, que puede tomar
los valores bmDIB y bmDDB. Al cambiar la propiedad PixelFormat, cambia la propiedad HandleType y
viceversa.
NOTA:
Si va a imprimir una imagen contenida en un TBitmap, debe asegurarse de que la imagen se almacene en
formato DIB configurando las propiedades PixelFormat o HandleType. El intento de imprimir una imagen DDB
tiene resultados imprevisibles (la mayoría de veces se imprime nada) porque el controlador de la impresora no
entiende el formato correspondiente a la tarjeta de vídeo.

Cuando se carga una imagen desde un archivo, recurso o stream, la clase TBitmap normalmente crea una
imagen en formato DIB-Section que coincide en profundidad de color con la fuente. Una excepción son los
archivos comprimidos (el formato BMP sólo soporta la compresión para imágenes de 16 y 256 colores), en
este caso se crea un DDB. El archivo Graphics define la variable global DDBsOnly que por defecto es False.
Si se cambia su valor a True, la imagen cargada siempre estará en formato DDB.
NOTA:
La ayuda dice que cuando DDBsOnly = False, las imágenes recién creadas se almacenan por defecto como DIB-
Section. De hecho, debido a un error en el módulo Graphics (al menos hasta la versión 2007 de Delphi) la imagen
recién creada siempre se almacena como DDB, independientemente del valor de DDBsOnly.

La clase TBitmap tiene una propiedad ScanLine a través de la cual se puede acceder directamente a la
matriz de píxeles que componen la imagen. La Ayuda dice que esta propiedad sólo se puede utilizar con
imágenes DIB. Pero, de hecho, las imágenes DDB también permiten usar esta propiedad, aunque con
limitaciones sustanciales. Si la imagen se almacena en formato DDB, cuando se accede a ScanLine, se crea
una copia DIB de la imagen y ScanLine devuelve un puntero a la matriz de esta copia. Por lo tanto, en primer
lugar, ScanLine trabaja con imágenes DDB muy lentamente y, en segundo lugar, no funciona con la imagen,
sino con su copia, de lo cual se derivan las siguientes limitaciones:
1. Se crea una copia en el momento del acceso a ScanLine, por lo que los cambios realizados en la imagen
utilizando funciones GDI posteriormente, no estarán disponibles.
2. Cada acceso a ScanLine crea una nueva copia de la imagen, y la antigua se destruye. No hay garantía de
que la nueva copia se encuentre en la misma área de memoria, por lo que el puntero obtenido, durante el
acceso anterior a ScanLine, ya no puede ser utilizado.

52
3. Los cambios realizados en la matriz de píxeles sólo afectan a la copia de la imagen, pero la imagen en sí
no cambia. Por lo tanto, en el caso de DDB, la propiedad ScanLine permite leer, pero no cambiar la imagen.
Tenga en cuenta que TBitmap a veces crea DIB-Section incluso si las propiedades HandleType y PixelFormat
indican explícitamente usar DDB. Esto es especialmente cierto para imágenes grandes. Esto parece ocurrir
cuando no hay espacio en el sistema para almacenar una imagen DDB de ese tamaño, y los desarrolladores
de TBitmap decidieron que, en este caso, era mejor crear una imagen DIB que crear nada.
El ejemplo BitmapSpeed del CD adjunto permite comparar la velocidad de diferentes operaciones en
imágenes DDB y DIB.

Windows soporta dos codificaciones: ANSI y Unicode. En ANSI (American National Standard Institute) cada
carácter se codifica con un código de un byte. Los códigos del 0 al 127 coinciden con los códigos ASCII; los
códigos del 128 al 255 pueden representar diferentes caracteres en diferentes idiomas dependiendo de la
página de código seleccionada. Las páginas de código permiten reunir varios caracteres de diferentes
idiomas en un código de un solo bytes, pero sólo se puede trabajar con una página de código, es decir, con
un idioma. Una página de código elegida incorrectamente da como resultado caracteres incomprensibles
(normalmente llamados "garabatos") en lugar de un texto con sentido.
Unicode usa 2 bytes por carácter, lo que permite codificar 65,536 caracteres. Esto es suficiente para los
caracteres latinos y cirílicos, alfabeto griego, caracteres chinos, letras árabes y hebreas, así como
numerosos caracteres adicionales (financieros, matemáticos, etc.). No existen páginas de código en
Unicode.
NOTA:
La página de código para el idioma ruso en ANSI tiene el número 1251. La codificación de caracteres es diferente
de la llamada codificación alternativa adoptada en DOS. Por razones de compatibilidad, Windows utiliza una
codificación alternativa para programas de DOS, así como para las aplicaciones de consola. Por esta razón
aparecen esos "cocodrilos" al mostrar texto en ruso en aplicaciones de consola. Para evitar esto, debe
recodificar los caracteres ANSI a DOS en la salida y viceversa en la entrada. Esto se puede hacer mediante las
funciones CharToOem y OemToChar.

Windows NT/2000/XP soporta ANSI y Unicode en su totalidad. Esto significa que cualquier función de cadena
se presenta en dos versiones: ANSI y Unicode. Windows 9x/ME sólo es enteramente compatible con ANSI.
Las variantes Unicode de estos sistemas tienen un número relativamente pequeño de funciones. Cada
página de MSDN dedicada a una función de cadena (o con una estructura que contiene cadenas) en la parte
inferior tiene una inscripción que indica si la variante Unicode de esta función se implementa sólo para
NT/2000/XP o para todas las plataformas.
NOTA:
Como ejemplo, veamos las funciones para mostrar texto en pantalla. Sólo dos de ellas tienen variantes Unicode
en todas las plataformas: TextOut y ExtTextOut. Las funciones DrawText y DrawTextEx tienen variantes Unicode
sólo en Windows NT/2000/XP. Si mira las funciones para trabajar con Windows, entre ellas no hay ninguna
función que tenga una variante Unicode en Windows 9x/ME.

53
Hay que tener en cuenta que desde hace relativamente poco tiempo Microsoft ha proporcionado una extensión
para Windows 9x/ME que permite agregar soporte completo de Unicode a estos sistemas. Esta extensión se
llama MSLU (Microsoft Layer for Unicode) y puede descargarse del sitio web oficial de Microsoft.

Veamos cómo coexisten las dos opciones en el ejemplo de la función RegisterWindowMessage. Segun la
ayuda, es exportado por la biblioteca user32.dll. Sin embargo, si observa el listado de funciones exportadas
por esta biblioteca (que se puede hacer, por ejemplo, con la utilidad TDump.exe, que es parte de Delphi),
entonces no estará aquí, pero estarán las Funciones RegisterWindowMessageA y
RegisterWindowMessageW. La primera de ellas es la versión ANSI de la función, la segunda es la versión
Unicode (la letra W significa Wide - Ancho; los caracteres Unicode por lo general se denominan con W
debido al hecho de que no hay uno, sino dos bytes por carácter). Primero, veamos cómo se utilizan dos
variantes de la misma función en Microsoft Visual C++. Los archivos de cabecera estándar tienen en cuenta
la presencia de la macro UNICODE. Hay dos tipos de caracteres: CHAR para ANSI y WCHAR para Unicode. Si
está definida la macro UNICODE, el tipo TCHAR coincide con el tipo WCHAR, si no está definida, coincide con
el tipo CHAR (después de esto, los tipos derivados de TCHAR, como LPCTSTR, empiezan a coincidir con la
codificación definida por la presencia o ausencia de la definición UNICODE).
En los archivos de cabecera se importan ambas funciones y se define la macro RegisterWindowMessage. Su
significado también depende de la macro UNICODE: si está definida, RegisterWindowMessage es equivalente
a RegisterWindowMessageW, si no está definida, es equivalente a RegisterWindowMessageA. Todas las
funciones que soportan las dos opciones de codificación se importan de la misma forma. Por lo tanto, al
insertar o eliminar la definición de la macro UNICODE, es posible, sin cambiar ningún carácter del programa,
compilar su versión ANSI o Unicode.
Los desarrolladores de Delphi no copiaron por completo este mecanismo, aparentemente debido a la
compilación separada de módulos de Delphi, que impide que todos los módulos sean recompilados al definir
un solo carácter (especialmente porque algunos de ellos pueden no tener código fuente). Por lo tanto, Delphi
no tiene un tipo similar a TCHAR.
Considere cómo la misma función RegisterWindowMessage es importada por un módulo windows (Listado
1.19).
Listado 1.19. Importando la función RegisterWindowMessage.
interface
...
function RegisterWindowMessage(lpString: PChar): UINT; stdcall;
function RegisterWindowMessageA(lpString: PAnsiChar): UINT; stdcall;
function RegisterWindowMessageW(lpString: PWideChar): UINT; stdcall;
...
implementation
...
function RegisterWindowMessage;
external user32 name 'RegisterWindowMessageA';
function RegisterWindowMessageA;
external user32 name 'RegisterWindowMessageA';
function RegisterWindowMessageW;
external user32 name 'RegisterWindowMessageW';

Puede ver que la función RegisterWindowMessageA se importa dos veces: una con su nombre real y la
segunda con el nombre RegisterWindowMessage. Cualquiera de estos nombres es adecuado para llamar a
la variante ANSI de esta función (recuerde que los tipos PChar y PAnsiChar son equivalentes). Para llamar a
la variante Unicode de esta función, necesitará la función RegisterWindowMessageW.

54
Las estructuras que contienen datos de cadenas también tienen variante ANSI y Unicode. Por ejemplo, la
estructura WNDCLASS en el módulo windows está representada por los tipos TWndClassA (con sinónimos
WNDCLASSA y tagWNDCLASSA) y TWndClassW (con sinónimos WNDCLASSW y tagWNDCLASSW). El tipo
TWndClass (y sus sinónimos WNDCLASS y tagWNDCLASS) es equivalente al tipo TWndClassA. En
consecuencia, cuando se llama a las funciones RegisterClassA y RegisterClassExA, se usa el tipo
TWndClassA, cuando se llama a RegisterClassW y RegisterClassExW, se usa el tipo TWndClassW.

Unicode es poco frecuente en Delphi porque los programas que utilizan esta codificación no funcionan en
Windows 9x/ME. La biblioteca VCL también ignora Unicode, limitándose a ANSI. Por lo tanto, a partir de
ahora sólo hablaremos de ANSI. Puede trabajar de forma similar con Unicode reemplazando PChar por
PWideChar y string por WideString.
Para trabajar con cadenas en Delphi, el tipo más común es AnsiString, comúnmente referido simplemente
como string (la relación entre estos tipos se discute más en detalle en el Capítulo 3). Una variable de tipo
string es un puntero a una cadena almacenada en memoria dinámica. Este puntero apunta al primer
carácter de la cadena. El primer elemento (contando hacia atrás desde el comienzo de la propia cadena) es
la longitud de la cadena y el segundo elemento es un contador de referencias que evita copiar
innecesariamente la cadena, implementando lo que se llama "copiar según necesidad". Si asigna una
variable cadena al valor de otra variable cadena, la cadena no se copia, la variable simplemente apuntará a
la misma cadena y el contador de referencias de la cadena se incrementará en uno. Cuando se modifica una
cadena, se comprueba el contador de referencias: si no es uno, entonces se copia la cadena y el contador de
referencias de la cadena copiada se reduce en uno, la nueva copia tendrá un contador de referencias de uno
y la variable modificada apuntará a la nueva copia. Por lo tanto, solo se copiará una cadena cuando una de
las variables que la referencian comienza a modificarla, de esta forma la modificación de una cadena no
afectará a las otras variables. Cualquier modificación de una cadena agregará automáticamente el carácter
nulo al final (ignorado al calcular la longitud de la cadena mediante la función Lenght). En cambio, si se
asigna una cadena vacía a una variable cadena, la variable se convierte en un puntero nulo (nil); no se
asignará memoria para almacenar solo al carácter #0. Cuando una variable cadena queda fuera de ámbito
(por ejemplo, cuando termina el procedimiento en el que la variable es local o cuando se destruye el objeto
del cual la variable es un campo), finaliza automáticamente, es decir, el contador de referencias se reduce
en uno, y si es cero, se libera la memoria asignada a la cadena (vea también la sección 3.3 sobre el
funcionamiento interno de AnsiString).
El mecanismo para asignar y liberar memoria y contar referencias es transparente al programa. Solo se
requiere que el desarrollador no interfiera con este trabajo al utilizar operaciones con punteros de bajo
nivel y así el mecanismo de memoria no se confunda.
NOTA:
A diferencia de string, el tipo WideString no tiene contador de referencias y cualquier asignación de variables de
este tipo hace que se copie la cadena. Esto se hace por compatibilidad con el tipo del sistema BSTR utilizado en
COM/DCOM y OLE.

Las funciones API de Windows no soportan el tipo string. Trabajan con cadenas terminadas en #0 (cadenas
terminadas en nulo). Esto significa que una cadena es un puntero a una cadena de caracteres. La indicación

55
del final de una cadena de este tipo es el carácter de código 0. Antes existía el término ASCIIZ para este tipo
de cadenas. ASCII es el nombre de la codificación y Z es cero. Ahora la codificación ASCII no existe en su
forma pura, por lo que este término ya no se usa, aunque se trata esencialmente de la misma cadena. Como
ya se mencionó, en Delphi, se agrega implícitamente el carácter nulo a todas las cadenas de tipo string, que
no se tiene en cuenta al contar el número de caracteres. Esto por compatibilidad con las cadenas
terminadas en nulo. Sin embargo, esta compatibilidad es limitada.
Delphi normalmente usa el tipo PChar para trabajar con cadenas terminadas en nulo. Formalmente, este es
un puntero a un solo carácter de tipo Char, pero se entiende que es el primer carácter de la cadena, seguido
del resto de caracteres. El desarrollador debe decidir por su cuenta dónde se ubicarán estos caracteres y
cómo se asignará la memoria para ellos. También debe asegurarse de que el final de la cadena de
caracteres sea el carácter #0.
La cadena a la cual apunta PChar se puede usar donde sea que se requiera un string – el compilador
realizará las conversiones necesarias. Lo contrario no es cierto. De hecho, string – es un puntero al
comienzo de una cadena que termina en nulo, el mismo puntero que se requiere cuando se trabaja con
PChar. Sin embargo, como ya se ha señalado, las manipulaciones incorrectas con este puntero pueden
conducir a efectos no deseados, por lo que el compilador requiere de una conversión explícita de las
variables y expresiones de tipo string a PChar. A su vez, el programador debe entender claramente a qué
consecuencias puede conducir esto.
Si observa en la Ayuda la descripción de las funciones API que tienen parámetros de cadena, podrá ver que
en algunos casos los parámetros de cadena son de tipo LPCTSTR (como, por ejemplo, la función
SetWindowText) y, en otros, de tipo LPTSTR (GetWindowText). Anteriormente dijimos que la presencia del
prefijo C después de LP indica que se trata de un puntero a una constante, es decir, lo que dicho puntero
apunta no puede ser cambiado. El tipo LPCTSTR tiene estos parámetros cuyo contenido sólo lee la función,
pero no modifica. Estos parámetros de cadena son los más fáciles de trabajar. Como un ejemplo, echemos
un vistazo a cómo trabajar la función SetWindowText con estos parámetros (Listado 1.20).
Listado 1.20. Llamada a una función con un parámetro de tipo LPCTSTR.
{ Método 1: }
SetWindowText(Handle, 'Сadenа');

{ Método 2: S — variable de tipo string }


SetWindowText(PChar(S));

{ Método 3: X — variable de tipo Integer }


SetWindowText(PChar('Resultado ' + IntToStr(X) + '%'));

En el primer método, el compilador coloca el literal de cadena en el segmento de código y pasa a la función
el puntero a esta ubicación de memoria. Dado que la función no modifica la cadena, sólo la lee, pasar este
puntero no causa problemas.
En el segundo método, se le pasa a la función un puntero almacenado en la variable S. Esta conversión de
string a PChar es segura, ya que la cadena a la cual hace referencia la variable S no se modificará. Pero
aquí hay una trampa: la construcción PChar(S) no es solo una conversión de tipo, cuando se usa, se llama
implícitamente a la función _LStrToPChar. Como ya hemos indicado, cuando string almacena una cadena
vacía, su puntero es simplemente nil. La función _LStrToPChar verifica si la cadena referenciada por la
variable es una cadena vacía, si no es una cadena vacía, devuelve su puntero, y si es una cadena vacía, no
devuelve nil, sino un puntero al carácter #0, especialmente colocado en el segmento de código. Por lo tanto,
incluso si S contiene una cadena vacía, se pasará un puntero distinto de nil a la función.

56
La evaluación de expresiones de cadena requiere la reasignación de memoria, que el compilador sólo hace
con expresiones de tipo string. Por lo tanto, el resultado de la expresión en el tercer método es también de
tipo string. Pero se puede convertir a PChar. La memoria para almacenar el resultado de la expresión se
asigna dinámicamente, como con las variables regulares tipo string. Para pasar a la función el puntero de
esta expresión, debe convertirlo a PChar. Al final del procedimiento que llama a la función SetWindowText o
a cualquier otra función con un argumento similar, se agrega código que libera la cadena generada
dinámicamente, por lo que no se producen pérdidas de memoria.
Por supuesto, hay otras formas de obtener un parámetro LPCTSTR además de las sugeridas aquí. Puede,
por ejemplo, asignar memoria para una cadena terminada en cero utilizando StrNew o una función
relacionada del módulo SysUtils. Se puede utilizar una matriz de tipo Char. Se puede asignar memoria de
cualquier otra forma. Pero las tres opciones propuestas aquí son, en la mayoría de casos, las más
convenientes.
Los parámetros LPTSTR se utilizan cuando una función no sólo puede leer sino también modificar el valor
que se le pasa. En la mayoría de casos, estos parámetros son puramente de salida, es decir, la función no
está interesada en el valor que tenía el parámetro cuando fue llamado, ya que se utiliza sólo para devolver
un valor. Cuando se devuelve un valor de cadena, siempre existe el problema de dónde, quién y cómo se
asignará la memoria en la que se escribirá la cadena. Las funciones API de Windows, con muy pocas
excepciones, resuelven este problema de la siguiente forma: la memoria debe ser asignada por el programa
que llama, y un puntero a este bloque preasignado se pasa a la función. La función en sí sólo copia la cadena
en ese bloque.
Los parámetros LPTSTR se utilizan cuando una función no sólo puede leer sino también modificar el valor
que se le pasa. En la mayoría de casos, estos parámetros son puramente de salida, es decir, la función no
pregunta qué valor tenía el parámetro cuando se llamó, ya que se utiliza sólo para devolver un valor.
Cuando se devuelve un valor de cadena, siempre existen problemas: ¿dónde? ¿quién y cómo se asignará la
memoria en el cual se escribirá la cadena? Las funciones API de Windows, con muy pocas excepciones,
resuelven este problema de la siguiente forma: el programa que llama debe asignar memoria, pasándole a
la función un puntero a este bloque preasignado. La función en sí sólo copia la cadena en ese bloque.
Por lo tanto, el programa se enfrenta a la tarea de saber cuánta memoria debe asignarse para la cadena
devuelta. Aquí, la API no ofrece una solución universal, diferentes funciones resuelven este problema de
diferentes formas. Por ejemplo, si recupera el título de una ventana mediante GetWindowText, el tamaño del
título se puede obtener llamando previamente a GetWindowTextLength. Las funciones del tipo
GetCurrentDirectory devuelven la longitud de la cadena. Si no se asigna suficiente memoria cuando se llama
a esta función por primera vez, puede aumentar el búfer y llamar a la función una vez más. Finalmente, hay
funciones como SHGetSpecialFolderPath describen el tamaño mínimo de búfer necesario para garantizar
que esta función pase la cadena completa (por supuesto, esto solo es posible cuando el tamaño de la cadena
devuelta tiene algún límite natural). También debe tenerse en cuenta que la mayoría de las funciones API
que devuelven cadenas toman el tamaño del búfer como uno de los parámetros para no copiar más bytes de
los que el búfer puede aceptar.
Hay muchas formas de asignar un buffer para recuperar una cadena. En la práctica, lo más conveniente son
las matrices estáticas, el tipo de cadena o la asignación de memoria dinámica para cadenas terminadas en
cero.

57
Las matrices estáticas se pueden utilizar si se conoce el tamaño del buffer en la fase de compilación. Las
matrices de tipo Char con un índice inicial de 0 son tratadas por el compilador como cadenas terminadas en
cero, por lo que es conveniente realizar más operaciones con ellas. Este método es conveniente porque no
necesita preocuparse por asignar y liberar memoria, por lo que a menudo se usa donde la longitud de la
cadena en la etapa se desconoce formalmente, pero "basándonos en el sentido común" podemos concluir
que en la gran mayoría de casos esta longitud no excederá cierto valor, que se toma como el tamaño de la
matriz.
Las cadenas de tipo string también pueden servir como buffer para recibir valores de cadenas del sistema.
Para ello, primero debemos establecer la longitud de la cadena necesaria con SetLength y luego pasar a la
función API un puntero al principio de la cadena. Aquí debemos tener cuidado: si la longitud de la cadena es
cero, la variable de tipo string tendrá el valor de nil, y el sistema intentará escribir una cadena vacía que
consiste en el único carácter #0. Esto dará lugar a un error de violación de acceso.
La tercera forma es asignar memoria para el búfer mediante StrAlloc o una función similar. La memoria
asignada de esta forma debe liberarse mediante StrDispose. En este caso, es muy recomendable utilizar la
construcción try/finally para que la aparición de excepciones no genere pérdidas de memoria.
Las tres formas de obtener datos de cadenas de las funciones de la API de Windows se muestran en el
ejemplo EnumWnd del CD incluido.

En esta sección se explican los ejemplos sencillos del CD-ROM. Todos estos ejemplos se han mencionado
antes, y cada uno de ellos ilustra una característica específica de la API. Los ejemplos de generalización
más complejos, en los cuales intervienen más de una función API y VCL a la vez, se tratan en la siguiente,
tercera sección de este capítulo.

El programa EnumWnd es un ejemplo sencillo del uso de las funciones EnumWindows y


EnumChildWindows, así como a las funciones callback necesarias para que estas dos funciones trabajen.
El programa busca todas las ventanas creadas actualmente en el sistema y las muestra en forma de árbol,
donde cada nodo corresponde a una ventana y los nodos secundarios corresponden a las ventanas
secundarias de esta ventana (Fig. 1.8).
EnumWnd también es un ejemplo de cómo trabajar con parámetros de tipo LPTSTR a través de los cuales
las funciones API de Windows devuelven valores de cadena al programa. En la sección 1.1.13, se enumeraron
tres métodos de crear un búfer para trabajar con parámetros de tipo LPTSTR: asignando memoria en forma
de una matriz de elementos de tipo Char, usando cadenas de tipo string y usando cadenas de tipo PChar.
Los tres métodos están implementados en el ejemplo EnumWnd.

En el formulario principal y único del programa EnumWnd hay dos componentes: TreeWindow de tipo
TTreeView y un botón BtnBuild. El gestor del evento clic del botón parece muy conciso (Listado 1.21).

Listado 1.21. Gestor del evento clic de botón BtnBuild.

procedure TFormWindows.BtnBuildClick(Sender: TObject);

58
begin
Screen.Cursor := crHourGlass;
try
TreeWindows.Items.Clear;
EnumWindows(@EnumWindowsProc, 0);
finally
Screen.Cursor := crDefault;
end;
end;

Figura 1.8. Ventana del programa EnumWnd.

Todo lo que hace este gestor es limpiar el componente TreeWindows y llamar a EnumWindows, pasándole
la función callback EnumWindowsProc, en la cual se realiza la mayor parte del trabajo. Solo tenga en
cuenta que en este ejemplo usaremos la misma función callback para EnumWindows y
EnumWindowsProc. La función callback en sí tiene el siguiente aspecto (Listado 1.22).

Listado 1.22. Función callback EnumWindowsProc (Primera visión).


{ Esta es la función callback que se usará cuando se llame a EnumWindows y
EnumChildWindows. El tipo del segundo parámetro no coincide con el tipo
especificado en MSDN. Sin embargo, TTreeNode, como cualquier otra clase,
es un puntero, por lo que se puede utilizar siempre que se requiera un
puntero no tipado – a nivel binario no hay diferencia entre ambos. En el
módulo windows, el puntero a una función callback en EnumWindows y
EnumChildWindows se declara como un puntero no tipado, por lo que el
compilador no controla el cumplimiento del prototipo real con el
declarado}
function EnumWindowsProc(Wnd: HWND; ParentNode: TTreeNode): Bool; stdcall;

{ El sistema no tiene forma de saber cuan largo es el nombre de una clase,


por lo que, al obtener el nombre, debe asignar un búfer largo con la
esperanza de que el nombre de la clase no sea aún más largo. En este
ejemplo, el tamaño del buffer está determinado por la constante
ClassNameLen. Es muy poco probable que el nombre de la clase tenga más
de 511 caracteres (el 512 se reserva para el carácter nulo al final)}
const
ClassNameLen = 512;

Var
// Aquí es donde se almacenará el título de la ventana
Text: string;
TextLen: Integer;

// Este es el búfer para el nombre de la clase


ClassName: array[0..ClassNameLen — 1] of Char;
Node: TTreeNode;
NodeName: string;

begin

59
Result := True;

{ La función EnumChildWindows no solo enumera directamente a las


ventanas secundarias de una determinada ventana, sino también a las
ventanas secundarias de sus ventanas secundarias, etc. Pero al
construir el árbol en cada paso, solo necesitamos a los descendientes
directos, por lo que se ignorará a todas las ventanas que no sean
descendientes directos}
if Assigned(ParentNode) and (GetParent(Wnd) <> HWND(ParentNode.Data)) then Exit;

{ Obtenemos la longitud del título. En lugar de las funciones


GetWindowText y GetWindowTextLength, usamos los mensajes WM_GETTEXT y
WM_GETTEXTLENGTH, porque las funciones, a diferencia de los mensajes,
no saben cómo trabajar con controles que pertenecen a ventanas de
otros procesos}
TextLen := SendMessage(Wnd, WM_GETTEXTLENGTH, 0, 0);

{ Establece la longitud de la variable Text que servirá como buffer


para el título de la ventana. El uso de SetLength asegura que se
asigne un área especial de memoria a la cual no se harán otras
referencias}
SetLength(Text, TextLen);

{ Si el título de la ventana es una cadena vacía, TextLen tendrá el


valor de 0, y el puntero Text recibirá un valor nulo cuando se
ejecute SetLength. Pero al procesar el mensaje WM_GETTEXT, el
procedimiento de ventana intentará de todos modos escribir una cadena
en el buffer, incluso si el título de la ventana está vacío - en este
caso se escribirá un solo carácter – el carácter nulo. Pero si se
pasa nil, un intento de escribir algo en el buffer resultará en una
violación de acceso, por lo que sólo se puede enviar WM_GETTEXT a la
ventana si TextLen > 0}
if TextLen > 0 then
SendMessage(Wnd, WM_GETTEXT, TextLen + 1, LParam(Text));

{ El título de la ventana puede ser muy largo: en Memo, por ejemplo, el


título es todo el texto que hay allí. La práctica muestra que hay
problemas al agregar nodos con nombres muy largos a TTreeView: cuando
se intenta abrir un nodo con estas características desde un programa
ejecutado en Delphi falla en el depurador (cuando se ejecuta fuera de
Delphi no se notan los problemas). Para evitar que suceda, se
recortan las líneas de texto que son demasiado largas}
if TextLen > 100 then
Text := Copy(Text, 1, 100) + ' ...';
GetClassName(Wnd, ClassName, ClassNameLen);
ClassName[ClassNameLen — 1] := #0;
if Text = '' then
NodeName := 'Sin título (' + ClassName + ')'
else
NodeName := Text + ' (' + ClassName + ')';
Node := FormWindows.TreeWindows.Items.AddChild(ParentNode, NodeName);

{ Se escribe en Data el descriptor de la ventana correspondiente para


poder descartar a los descendientes que no sean directos}
Node.Data := Pointer(Wnd);

60
{ Se llama a EnumChildWindows, con la función EnumWindowsProc como
parámetro y un puntero al nodo creado como parámetro de esta función.
En este caso, se llamará a EnumWindowsProc desde EnumChildWindows, es
decir, se consigue la recursividad}
EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node));
end;

Como recordamos, el primer parámetro de la función callback para EnumWindows contiene al descriptor
de la ventana encontrada, y el segundo parámetro puede ser un valor arbitrario de 4 bytes, que el sistema
ignora aquí simplemente copiando el valor que se pasa al llamar a EnumWindows o EnumChildWindows.
Usaremos este parámetro para pasar una referencia al nodo del árbol correspondiente a la ventana
principal. También acordamos que en la propiedad Data de cada nodo almacenaremos al descriptor de la
ventana asociada. Para las ventanas de nivel superior, esta referencia será nil, lo que se garantiza por el
hecho de que cuando se llama a EnumWindows, su segundo parámetro es cero (consulte el Listado 1.21).

La función comienza verificando que la ventana principal de una determinada ventana sea efectivamente la
ventana cuyo descriptor está asociado al nodo de la ventana principal. Esta verificación es necesaria porque
la función EnumChildWindows enumera no sólo a las ventanas hijas, sino también a las 'nietas', 'bisnietas',
etc. Aquí esto no es necesario, en cada paso sólo estamos interesados en los "hijos" inmediatos de una
ventana; llegaremos a los "nietos" cuando llamemos a EnumChildWindows para las ventanas hijas, así que
filtremos el exceso.
El siguiente paso es obtener el título de la ventana. Para ello utilizaremos el mensaje WM_GETTEXT (la
diferencia entre este mensaje y la función GetWindiowText se explica en la Sección 1.3.1). El búfer es la
variable Texto de tipo string. Primero con el mensaje WM_GETTEXTLENGTH obtendremos la longitud del
título y luego asignaremos la cantidad necesaria de memoria a la cadena Texto mediante SetLength.
Finalmente recuperaremos la cadena con WM_GETTEXT. El segundo parámetro de este mensaje es la
dirección del buffer donde se almacenará la cadena. Dado que una variable de tipo string es un puntero a
un buffer de cadena (esto se explicará en detalle en la sección 3.3), es suficiente con convertir la variable
Text al tipo LParam y pasar el resultado.

NOTA:
Estrictamente hablando, aquí no se tiene un parámetro de tipo LPTSTR, sin embargo, cuando se trabaje con
parámetros de este tipo, se puede hacer exactamente lo mismo: asignar la cantidad de memoria correcta para
una variable de tipo string y pasar esta variable convertida en LPTSTR como parámetro.

A continuación, obtendremos el nombre de la clase de ventana. Para ello, usaremos una matriz estática
ClassName, es decir, el tamaño del búfer se determina en la etapa de compilación. En primer lugar, esto es
incorrecto porque no hay restricciones de longitud para el nombre de la clase (al menos, no se mencionan
en la documentación) y como ya se dijo, este método sólo debe utilizarse en la etapa de compilación cuando
existan restricciones de longitud. En segundo lugar, cuando se trata del nombre de una clase, no hay nada
como el mensaje WM_GETTEXTLENGTH, es decir, la API no proporciona una forma de obtener la longitud del
nombre de una clase, lo que hace que todas las manipulaciones con el tamaño del buffer no sirvan en la
etapa de ejecución del programa. Por lo tanto, el tamaño del búfer se determina en la etapa de compilación,

61
basándonos en el hecho de que los nombres demasiado largos de una clase son raros. Cuando llamemos a
una función con un parámetro de tipo LPTSTR, podemos simplemente pasar una matriz sin conversión de
tipo, dado que un LPTSTR es un PChar, y el compilador considera que las matrices de tipo Char indexadas
desde cero son compatibles con este tipo y realiza todas las conversiones necesarias implícitamente.
Y, aunque consideremos un tamaño de búfer con un buen margen, no podemos descartar situaciones en las
que el nombre de la clase resulte ser más largo que el búfer. En nuestro caso, no sucederá nada, porque le
pasamos a la función un tamaño de búfer específicamente para no intentar escribir algo fuera de él. Sin
embargo, el carácter #0 de terminación de cadena no entrará en el búfer, y cuando se intente seguir
trabajando con la cadena, puede que alguna otra función, al no encontrar este carácter dentro del búfer,
intente buscar fuera de ella, dando lugar a resultados impredecibles. Por lo tanto, por si acaso, escribimos
#0 en el último carácter del búfer. Si el nombre de la clase resulta ser más largo que el búfer, se recortará
la cadena al tamaño del búfer, y si es más corto, no hará daño, porque el carácter de terminación estará
presente en el búfer y todos los caracteres posteriores se ignoraran de todos modos.
Después de esto, todo lo que queda es crear un nuevo elemento del árbol y poblar sus elementos
secundarios llamando a EnumChildWindows para obtener un listado de ventanas secundarias. Como
EnumChildWindows pasa la misma función callback, se obtiene la recursividad que se detiene cuando la
función llega a una ventana sin ventanas secundarias.
Anteriormente dijimos que el programa EnumWind se implementan tres métodos para obtener una cadena a
través de un parámetro de tipo LPTSTR, pero hasta ahora sólo hemos visto dos (de hecho, es difícil mostrar
tres métodos diferentes en el ejemplo de obtener dos cadenas). Para mostrar el tercer método, organizar el
buffer mediante cadenas de tipo PChar, se ha reescrito la función EnumWindowsProc (Listado 1.23). En el
código fuente del programa EnumWnd, este método está presente como un comentario. Puede quitar este
comentario y dejar de lado el primer método para probar cómo funciona obtener una cadena con PChar.

Listado 1.23. Función callback EnumWindowsProc (Segunda versión).


{ A continuación se muestra otra versión de la función EnumWindowsProc,
que se diferencia del anterior en que el buffer para obtener el título
de la ventana esta organizado manualmente mediante una variable de tipo
PChar, en lugar de un string. En cuanto a su funcionalidad ambas
versiones son iguales}
function EnumWindowsProc(Wnd: HWND; ParentNode: TTreeNode): Bool; stdcall;
const
ClassNameLen = 512;
var
TextLen: Integer;
Text: PChar;
ClassName: array[0..ClassNameLen — 1] of Char;
Node: TTreeNode;
NodeName: string;
begin
Result := True;
if Assigned(ParentNode) and
(GetParent(Wnd) <> HWND(ParentNode.Data)) then Exit;

{ Aquí, a diferencia de la visión anterior, se suma uno a la longitud


obtenida mediante WM_GETTEXTLENGTH porque hay que tener en cuenta
manualmente un byte extra para el cero final}

62
TextLen := SendMessage(Wnd, WM_GETTEXTLENGTH, 0, 0) + 1;

{ Se asigna la cantidad de memoria necesaria. Dado que el compilador no


liberará automáticamente esta memoria, debemos usar un bloque
try/finally, de lo contrario tendremos pérdidas de memoria en caso de
la ocurrencia de excepciones}
Text := StrAlloc(TextLen);
Try
{ Dado que se asignará al menos un byte al búfer incluso con una
cabecera vacía, se puede enviar WM_GETTEXT sin comprobar la longitud
de la cadena como en el caso anterior - el búfer siempre será el
correcto}
SendMessage(Wnd, WM_GETTEXT, TextLen, LParam(Text));

{ Recorta una cadena demasiado larga. Modificar un PChar es más difícil


que un string. Insertar un cero en medio de la cadena hace que todas
las funciones API ignoren la "cola", pero no afectará a StrDispose
porque la función StrAlloc (así como otras funciones de asignación de
memoria para cadenas terminadas en cero del módulo SysUtils) almacena
el tamaño de la memoria asignada en la propia cadena, y StrDispose se
centra en este tamaño y no en el cero final}
if TextLen > 104 then
begin
(Text + 104)^ := #0;
(Text + 103)^ := '.';
(Text + 102)^ := '.';
(Text + 101)^ := '.';
(Text + 100)^ := ' ';
end;
GetClassName(Wnd, ClassName, ClassNameLen);
if Text^ = #0 then
NodeName := 'Без названия (' + ClassName + ')'
else
NodeName := Text + ' (' + ClassName + ')';
Node := FormWindows.TreeWindows.Items.AddChild(ParentNode, NodeName);
Node.Data := Pointer(Wnd);
EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node));
finally
// Libera manualmente la memoria asignada al buffer
StrDispose(Text);
end;
end;

La segunda versión de la función EnumWindowsProc difiere de la primera únicamente en que se usa una
variable de tipo PChar en lugar de una variable de tipo string para organizar el buffer y obtener el nombre
de la ventana. En consecuencia, todas las manipulaciones de memoria dinámica ahora se realizan
manualmente, y no podemos simplemente cortar una cadena demasiado larga y agregar al resultado otra
cadena (...), sino que tenemos que modificar la cadena carácter por carácter. Sin embargo, vemos que
incluso con el tipo PChar, la tarea de crear un buffer para la cadena devuelta por la función API se resuelve
con bastante facilidad.

63
El ejemplo Line es un componente no visual TLine que intercepta los mensajes de su ventana propietaria (el
propietario en términos de la VCL, por supuesto, ya que estamos hablando de un componente no ventana). El
componente TLine dibuja una línea en su propietario desde el punto (StartX, StartY) hasta el punto (EndX,
EndY) en Color. El usuario puede mover los extremos de la línea con el mouse. Basta con colocar el
componente TLine en el formulario, y tendrá una línea que el usuario puede arrastrar tanto en tiempo de
diseño como en tiempo de ejecución. También puede colocar, por ejemplo, un panel en el formulario y
convertirlo en el propietario del componente TLine - entonces la línea se dibujará en el panel. Pero esto sólo
se puede hacer en tiempo de ejecución, porque el formulario se convierte en el propietario de todos los
componentes creados en tiempo de diseño.
Para instalar el componente, haga lo siguiente:
1. Copie desde el CD los archivos Line.pas y Line.dcr a la carpeta donde almacena componentes. Si aún no
tiene una, es hora de crearla. No importa dónde se ubique exactamente, elija cualquier lugar conveniente
para usted. Lo importante luego es especificar esta carpeta en las rutas donde Delphi buscará
componentes. Para hacerlo en Delphi 7 y versiones anteriores, abra el menú Tools\Environment Options,
en el cuadro de diálogo que aparece, seleccione la opción Library y agregue la ruta de su carpeta en el
campo Library path. En BDS 2006 y versiones posteriores, abra el menú Tools\Options, y en el cuadro de
diálogo del árbol que aparece, al lado izquierdo seleccione Environment Options\Delphi Options\Library
— Win32 y agregue la ruta de su carpeta en el campo Library path.
2. Cree un nuevo paquete (en menú File\New\Other, en la ventana que se abre seleccione Package). Delphi
7 y versiones anteriores abrirán una pequeña ventana de paquetes. En BDS 2006 y versiones posteriores,
no se abrirá una ventana, sin embargo, aparecerá un paquete en el grupo de proyectos (por defecto, esta
es la ventana Project Manager en la esquina superior derecha de la ventana principal). Guarde el paquete
en la misma carpeta que Line.pas con un nombre distinto de Line (de lo contrario, se generará un
conflicto de nombres).
3. Agregue el archivo Line.pas al paquete. En BDS 2006, para hacer esto, debe usar el botón derecho del
mouse para llamar al menú contextual del paquete en la ventana Project Manager y seleccionar Add. En
Delphi 7 y versiones anteriores, haga clic en Add en la ventana del paquete.
4. Instale el componente. En BDS 2006 y versiones posteriores, seleccione Install en el menú contextual del
proyecto, y en Delphi 7 y versiones anteriores, haga clic en el botón Install en la ventana del paquete. A
continuación, verá la pestaña Delphi Kingdom Samples en la paleta de componentes y en ella el
componente TLine.
Si no quiere poner el componente TLine en la paleta de componentes (o tiene Turbo Delphi Explorer y
simplemente no tiene esa opción), puede recurrir al proyecto LineSample, que crea dos instancias de TLine
en tiempo de ejecución, una perteneciente a un formulario y la otra a un panel.
La interceptación de los mensajes del propietario se hace modificando su propiedad WindowProc –
escribiendo en ella un puntero a su gestor de mensajes. Aquí se puede aplicar un truco. El componente
TLine no tiene su propio procedimiento de ventana, porque siendo un descendiente directo de la clase
TComponent, no es una ventana. Pero tiene un método Dispatch, declarado en la clase TObject. En la clase
TComponent y sus ancestros, nunca se llama al método Dispatch. Si escribimos un gestor de mensajes de
tal forma que pase los mensajes al método Dispatch, entonces podemos crear nuestro propio método en

64
nuestro componente para procesar los mensajes, al cual el método Dispatch pasará los mensajes para su
procesamiento cuando sea necesario. En este caso, los mensajes sin procesar se pasarán al método
DefaultHandler, que en la clase TComponent no hace nada. Si sobrescribimos DefaultHandler para llamar al
gestor de mensajes original del padre, entonces todos los mensajes sin procesar llegaran aquí. Además,
llamar a los métodos heredados del gestor de mensajes también llamará al gestor original del padre,
porque en este caso lo heredado en ausencia de un gestor heredado llamará a DefaultHandler. El listado
1.24 muestra la declaración de la clase TLine y el código de sus métodos relacionados con la interceptación
de mensajes.

65

También podría gustarte

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy