Está en la página 1de 192

Tutorial 1: Lo básico

En este tutorial asumo que el lector sabe como usar MASM. Si no estás
familiarizado con MASM, descarga win32asm.exe y estudia el texto dentro del
paquete antes de seguir con este tutorial. Bueno. Ahora estás listo. Ok vamos !

Teoría:
Los programas de Win32 corren en modo protegido, disponible desde el 80286.
Pero ahora el 80286 es historia. Asi que ahora debemos interesarnos en el 80386 y
sus descendientes. Windows corre cada programa de 32 bits en espacios virtuales
de memoria separados. Eso significa que cada programa de Win32 tiene sus
propios 4 GB de memoria en el espacio de direcciones. Como sea, esto no significa
que cada programa de Win32 tenga 4GB de memora física, sino que el programa
puede direccionar cualquier dirección en ese rango.

Windows hará cualquier cosa que sea necesaria para hacer que la memoria y las
referencias del programas sean válidas. Por supuesto, el programa debe adherirse
a las reglas impuestas por Windows, si no, causará un error de protección general.
Cada programa está solo en su espacio de direcciones. Esto contrasta con la
situación en Win16. Todos los programas de Win16 podían *verse* unos a otros.
No es lo mismo en Win32. Esto reduce la posibilidad de que un programa escriba
sobre el código/datos de otros programas.

El modelo de la memoria es también drásticamente diferente al de los antiguos días


del mundo de 16-bits. Bajo Win32, ya no necesitamos meternos nunca más con el
modelo o los segmentos de memoria! Hay un solo tipo de modelo de memoria: El
modelo plano (flat).

Ahora, no hay solamente segmentos de 64k. La memoria es un largo y continuo


espacio de 4GB. Eso también significa que no tendrás que jugar mas con los
registros de los segmentos. Ahora puedes usar cualquier registro de segmento para
direccionar a cualquier punto en el espacio de la memoría. Eso es una GENIAL
ayuda para los programadores. Esto hace la programación de ensamblador para
Win32 tan fácil como C.

Cuando programas bajo Win32, debes tener en cuenta unas cuantas reglas
importantes. Una es que Windows usa esi ,edi ebp y ebx internamente y no
espera que los valores en esos registros también. Asi que recuerda esta regla
primero: si usas cualquiera de estos cuatro registros en tu función callback,
nunca olvides restaurarlos antes de regresar el control a Windows. Una
función callback es una función escrita por tí que Windows llama cuando algún
evento específico se produce. El ejemplo mas obvio es el procedimiento de ventana.
Esto no significa que no puedas usar estos cuatro registros; sí puedes. Solo
asegúrate de restaurarlos antes de pasarle el control a Windows.

Contenido:
Aquí hay un esqueleto de un programa. Si no entiendes algo de los códigos, que no
cunda el pánico. Los explicaré cada uno de ellos mas abajo.

.386
.MODEL Flat, STDCALL
.DATA
<Tu data (información) inicializada>
......
.DATA?
<Tu data NO inicializada>
......
.CONST
<Tus constantes>
......
.CODE
<Etiqueta>
<Tu código>
.....
end <Etiqueta>

Sip, eso es todo, vamos a analizar el programa esqueleto.

.386
Esto es una directiva para el ensamblador, que le dice que vamos a usar el
conjunto de instrucciones del 80386. También puedes usar .486,.586 pero
es mas seguro ponerle .386. Hay actualmente dos formas casi idénticas
para cada modelo de CPU. .386/.386p, .486/.486p. Esas versiones de "p"
son necesarias cuando tu programa usa instrucciones privilegiadas. Las
instrucciones privilegiadas son las instrucciones reservadas por la
CPU/sistema operativo cuando están en modo protegido. Solamente
pueden ser usadas por un código privilegiado, asi como los virtual device
drivers (controladores de dispositivos virtuales = VXD).

.MODEL FLAT, STDCALL


.MODEL es una directiva para el ensamblador que especifíca el modelo de
memoria de tu programa. Bajo Win32, hay un solo tipo de memoria, la
PLANA(FLAT).
STDCALL Indica al ensamblador la convención de paso de los parámetros.
La convención de paso de parámetros especifíca la orden que debe
seguirse para pasar parámetros, izquierda-a-derecha o derecha-a-
izquierda, y también equilibrará la pila después de una llamada (call)

Bajo Win16, hay dos tipos de convenciones para las llamadas a funciones:
C y PASCAL
La convención para pasar parámetros de derecha a izquierda en cada
llamada (call), es decir, el parámetro de más a la derecha es empujado
primero a la pila. La rutina que hace la llamada es la responsable de
equilibrar el marco de la pila después de la llamada (call). Por ejemplo, si
vamos a llamar a una función con nombre foo(int primera_parte, int
segunda_parte, int tercera_parte) en lenguaje C la convención sería así en
asm:
push [tercera_parte] ; Empuja el tercer parámetro
push [segunda_parte] ; Seguido por el segundo
push [primera_parte] ; Y aqui se pone de primero
call foo
add sp, 12 ; La llamada equilibra el marco de la pila
La convención de llamadas en PASCAL es totalmente al revés de la
convención de C. Pasa los parámetros de izquierda a derecha y la rutina
que llama es responsable de equilibrarse después de la llamada.
El sistema Win16 adopta la convención de PASCAL porque produce códigos
más pequeños. La convención de C es eficiente cuando no sabes cuántos
parámetros serán pasados a la función como es el caso de wsprintf(). En el
caso de wsprintf(), no hay manera de determinar cuántos parámetros
serán empujados por esta función a la pila, así que no se puede hacer el
balanceo de la pila.

STDCALL es la convención híbrida entre C y PASCAL. Pasa parámetros de


derecha a izquierda, pero la llamada es la responsable por el balance de la
pila después de la llamada. La plataforma de Win32 usa el modelo STDCALL
exclusivamente. Excepto en un caso: wsprintf(). Debes usar la convención
de llamada C con la función wsprintf().

.DATA
.DATA?
.CONST
.CODE
Estas 4 directivas son las llamadas 'secciones'. No tienes segmentos en
Win32, ¿recuerdas?, pero puedes dividir todo el espacio de direcciones en
secciones lógicas. El comienzo de una sección demuestra el fin de la otra
sección previa. Hay dos grupos de secciones: data y code. Las secciones
Data están divididas en tres categorías:

• .DATA Esta sección contiene la información inicializada de tu


programa.
• .DATA? Esta sección contiene la información no inicializada de tu
programa. A veces quieres solamente prelocalizar alguna memoria
pero no quieres iniciarla. Ese es el propósito de esta sección. La
ventaja de la información no inicializada es que no toma espacio en
el ejecutable. Por ejemplo, si tu localizas 10,000 bytes en tu
sección .DATA?, tu ejecutable no se infla 10,000 bytes. Su tamaño se
mantiene muy homogéneo. Tu sólo le dices al ensamblador cuánto
espacio necesitas cuando el programa se carga en la memoria, eso
es todo.
• .CONST Esta sección contiene declaraciones de constantes usadas
por tu programa. Las constantes nunca pueden ser modificadas en
tu programa. Sólo son *constantes*

No tienes que usar las tres secciones en tu programa. Declara solo la(s)
sección(es) que quieres usar.

Existe solo una sección para el código (code):.CODE. Aquí es donde tu


código reside.
<etiqueta>
end <etiqueta>
Donde está la <etiqueta> se puede usar cualquier nombre como etiqueta
para especificar la extensión de tu código.. Ambas etiquetas deben ser
idénticas. Todos tus códigos deben residir entre <etiqueta> y end
<etiqueta>

En este tutorial, crearemos un programa de Windows completamente funcional que despliega


un cuadro con el mensaje "Win32 assembly is great!".
Baja el ejemplo aquí.

Teoría:
Windows tiene preparado una gran cantidad de recursos para sus programas. En el centro de
esta concepción se ubica la API (Application Programming Interface = Interface de
Programación de Aplicaciones) de Windows. La API de Windows es una enorme colección de
funciones muy útiles que residen en el propio sistema Windows, listas para ser usadas por
cualquier programa de Windows. Estas funciones están almacenadas en varias librerías de
enlace dinámico [dynamic-linked libraries (DLL)] tales como kernel32.dll, user32.dll y gdi32.dll.
Kernel32.dll contiene las funciones de la API que tienen que ver con el manejo de memoria y
de los procesos. User32.dll controla los aspectos de la interface de usuario de tu programa.
Gdi32.dll es la responsable de las operaciones gráficas. Además de estas "tres funcones
principales", hay otras DLLs que nuestros programas pueden emplear, siempre y cuando
tengas la suficiente información sobre las funciones de la API que te interesan.

Los programas de Windows se enlazan dinámicamente a estas DLLs, es decir, las rutinas de
las funciones de la API no están incluidas en el archivo ejecutable del programa de Windows.
Con el fin de que tu programa pueda encontrar en tiempo de ejecución las funciones de la API
deseadas, tienes que meter esa información dentro del archivo ejecutable. Esta información se
encuentra dentro de archivos .LIB. Debes enlazar tus programas con las librerías de
importación correctas o no serán capaces de localizar las funciones de la API.

Cuando un programa de Windows es cargado en la memoria, Windows lee la información


almacenada en el programa. Esa información incluye el nombre de las funciones que el
programa usa y las DLLs donde residen esas funciones. Cuando Windows encuentra esa
información en el programa, cargará las DLLs y ejecutará direcciones de funciones fijadas en el
programa de manera que las llamadas transfieran el control a la función correcta.

Hay dos categorías de funciones de la API: Una para ANSI y la otra para Unicode. Los
nombres de las funciones de la API para ANSI terminan con "A", por ejemplo, MessageBoxA.
Los de Unicode terminan con "W" [para Wide Char (caracter ancho), pienso]. Windows 95
soporta ANSI y Windows NT Unicode.

Generalmente estamos familiarizados con las cadenas ANSI, que son arreglos de caracteres
terminados en NULL. Un caracter ANSI tiene un tamaño de 1 byte. Si bien el código ANSI es
suficiente para los lenguajes europeos, en cambio no puede manejar algunos lenguajes
orientales que tienen millares de caracteres únicos. Esa es la razón por la cual apareció
UNICODE. Un caracter UNICODE tiene un tamaño de 2 bytes, haciendo posible tener 65536
caracteres únicos en las cadenas.

Sin embargo, la mayoría de las veces, usarás un archivo include que puede determinar y
seleccionar las funciones de la API apropiadas para tu plataforma. Sólo referencia los nombres
de las funciones de la API sin su sufijo.

Ejemplo:
Presentaré abajo el esqueleto del programa. Luego lo completaremos.

.386.model flat, stdcall


.data
.code
start:
end start

Lsa ejecución se inicia inmediatamente después de la etiqueta especificada después de la


directiva end. En el esqueleto de arriba, la ejecución iniciará en la primera instrucción
inmediatamante debajo de la etiqueta start. Le ejecución procederá instrucción por instrucción
hasta encontrar algunas instrucciones de control de flujo tales como jmp, jne, je, ret etc. Esas
instrucciones redirijen el fujo de ejecución a algunas otras instrucciones. Cuando el programa
necesite salir a Windows, deberá llamar a una fucnión de la API, ExitProcess.

ExitProcess proto uExitCode:DWORD

A la línea anterior la llamamos prototipo de la función. Un prototipo de función define los


atributos de una función para que el ensamblador/enlazador pueda tipear y chequear para tí. El
formato de un prototipo de función es:

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

En pocas palabras, el nombre de la función seguido por la palabra clave PROTO y luego por la
lista de tipos de datos de los parámetros, separados por comas. En el ejemplo de arriba de
ExitProcess, se define ExitProcess como una función que toma sólo un parámetro del tipo
DWORD. Los prototipos de funciones son muy útiles cuando usas sintaxis de llamadas de alto
nivel, como invoke. Puedes pensar en invoke como una llamada simple con chequeo-tipeo. Por
ejemplo, si haces:

call ExitProcess

sin meter en la pila un valor dword, MASM no será capaz de cachar ese error para tí. Sólo lo
notarás luego cuando tu programa se guinde o se quiebre. Pero si usas:

invoke ExitProcess

El enlazador te informará que olvidaste meter a la pila un valor dword y gracias a esto evitarás
el error. Recomiendo que uses invoke en vez de call. La sintaxis de invoke es como sigue:

INVOKE expresión [,argumentos]

expresión puede ser el nombre de una función o el nombre de un puntero de función. Los
parámetros de la función están separados por comas.

Muchos de los prototipos de funciones para las funciones de la API se conservan en archivos
include. Si usas el MASM32 de hutch, la encontrarás en la carpeta MASM32/include. Los
archivos include tienen extensión .inc y los prototipos de función para las funciones en una DLL
se almacenan en archivos .inc con el mismo nombre que la DLL. Por ejemplo, ExitProcess es
exportado por kernel32.lib de manera que el prototipo de función para ExitProcess está
almacenado en kernel32.inc.

También puedes crear prototipos para tus propias funciones.

A través de mis ejemplos, usaré windows.inc de hutch que puedes bajar desde
http://win32asm.cjb.net

Ahora regresemos a ExitProcess, el parámetro uExitCode es el valor que quieres que el


programa regrese a Windows después de que el programa termina. Puedes llamara
ExitProcess de esta manera:

invoke ExitProcess, 0

Pon esa línea inmediatamente abajo de la etiqueta de inicio, y obtendrás un programa win32
que saldrá inmediatamente a Windows, pero no obstante será un programa válido.
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
invoke ExitProcess,0
start:
end start

la opción casemap:none dice a MASM que haga a las etiquetas sensibles a mayusculas-
minusculas, así que ExitProcess y exitprocess son diferentes.

Nota una nueva directiva, include. Esta directiva es seguida por el nombre de un archivo que
quieres insertar en el lugar donde se encuentra la directiva. En el ejemplo de arriba, cuando
MASM procese la línea include \masm32\include\windows.inc, abrirá windows.inc que está en
el directorio \MASM32\include y procesará el contenido de windows.inc como si pegaras ahí el
contenido de windows.inc.

windows.inc de hutch contiene definiciones de constantes y estructuras que necesitas en la


programación de win32. No contiene ningún prototipo de función. windows.inc es totalmente
incomprensible. hutch y yo tratamos de poner tantas constantes y estructuras como sea posible
pero quedan todavía muchas por incluir. Será constantemente actualizada. Chequea el
homepage de hutch o el mío por actualizaciones.

De windows.inc, tus programas obtendrán las definiciones de constantes y estructuras. Pero


para los prototipos de la funciones, necesitarás incluir otros archivos include. Puedes generar a
partir de librerías de importación los archivos include que contienen sólo prototipos de
funciones. Esbozaré ahora los pasos para generar los archivos include:

1. Baja las librerías para el paquete MASM32 del homepage de hutch o del mío. Contiene
una colección de librerías de importación que necesitas para programar para win32.
también baja la utilidad l2inc.

2. Desempaca (unzip) ambos paquetes dentro del mismo archivo. Si instalaste MASM32,
desempácalos dentro del directorio MASM32\Lib

3. Corre l2inca.exe con los siguientes conmutadores (switches):


l2inca /M *.lib

l2inca.exe extraerá información de las librerías de importación y creará archivos include


llenos de prototipos de funciones.

4. Mueve tus archivos include a tu carpeta de archivos de este tipo. Los usuarios de
MASM32 deberán moverlos a la carpeta MASM32\include.

En nuestro ejemplo, llamamos la función exportada por kernel32.dll, así que necesitamos incluir
los prototipos de las funciones de kernel32.dll. Ese archivo es kernel32.inc. Si lo abres con un
editor de texto, verás que está lleno de prototipos de funciones de kernel32.dll. Si no incluyes
kernel32.inc, puedes llamar todavía a call ExitProcess pero sólo con una sintaxis simple de
llamada. No podrás usar invoke para "invocar" la función. El punto aquí es: para invocar una
función, tienes que poner el prototipo de la función en alguna parte del código fuente. En el
ejemplo anterior, si incluyes kernel32.inc, puedes definir los prototipos de funciones para
ExitProcess en cualquier parte del código fuente antes del comando invoke para que trabaje.
Los archivos include están ahí para liberarte del trabajo que significa escribirlas a cada
momento que necesites llamar funciones con el comando invoke.

Ahora encontramos una nueva directiva, includelib. includelib no trabaja como include. Es la
única menera que tiene tu programa de decirle al ensamblador que importe las librerías que
necesita. Cuando el ensamblador ve la directiva includelib, pone un comando del enlazador
dentro del mismo archivo objeto que genera, de manera que el enlazador sepa cuáles librerías
necesita importar tu programa que deben ser enlazadas. Sin embargo, no estás obligado a usar
includelib. Puedes especificar los nombres de las librerías de importación en la línea de
comando del enlazador pero, créeme, es tedioso y la línea de comando sólo soporta 128
caracteres.

Ahora salvamos el ejemplo bajo el nombre msgbox.asm. Asumiendo que ml.exe está un tu
entorno ("path"), ensamblamos msgbox.asm con:

ml /c /coff /Cp msgbox.asm

• /c dice a MASM que sólo ensamble, que no invoque a link.exe. Muchas veces, no
querrás llamar automáticamente a link.exe ya que quizás tengas que ejecutar algunas
otras tareas antes de llamar a link.exe.

/coff dice a MASM que cree un archivo objeto en formato COFF. MASM usa una
variación de COFF (Common Object File Format = Formato de Archivo Objeto Común)
que es usado bajo Unix como su propio formato de archivo objeto y ejecutable.
/Cp dice a MASM que preserve la sensibilidad a la diferencia entre mayúsculas y
minúsculas de los identificadores usados. Si usas el paquete MASM32 de hutch,
puedes poner "option casemap:none" en la cabeza del código fuente, justo debajo de la
directiva .model para alcanzar el mismo efecto.

Después de haber ensamblado satisfactoriamente msgbox.asm, obtendrás msgbox.obj.


msgbox.obj es un archivo objeto. Un archivo objeto está a sólo un paso del archivo ejecutable.
Contiene las instrucciones/datos en forma binaria. Sólo necesitas que el enlazador fije algunas
direcciones en él.

Entonces enlacemos:

link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj

/SUBSYSTEM:WINDOWS informa a Link qué tipo de ejecutable es este programa


/LIBPATH:<path to import library> dice a Link dónde se encuentran las librerías de importación.
Si usas MASM32, estarán en el archivo MASM32\lib.

Link lee en el archivo objeto y lo fija con las direcciones de las librerías de importación. Cuando
el proceso termina obtienes msgbox.exe.

Obtienes msgbox.exe. Vamos, córrelo. Encontrarás que no hace nada. Bien, todavía no hemos
puesto nada interesante en él. Sin embargo, es un programa de Windows. ¡Y mira su tamaño!
En mi PC, es de 1,536 bytes.

Vamos a ponerle ahora una caja de mensaje [Dialog Box]. Su prototipo de función es:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd es el manejador de la ventana padre. Puedes pensar en el manejador como un número


que representa la ventana a la cual te refieres. Su valor no es tan importante para tí. Sólo
recuerda que representa la ventana. Cuando quieres hacer algo con la ventana, debes referirte
a ella por su manjador.
lpText es el puntero al texto que quieres desplegar en el área cliente de la caja de mensaje. En
realidad, un puntero es la dirección de lago: Puntero a cadena de texto==Dirección de esa
cadena.
lpCaption es un puntero al encabezado de la caja de mensaje
uType especifica el icono, el número y el tipo de botones de la caja de mensajes

Modifiquemos msgbox.asm para que incluya la caja de mensaje.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0

.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

Ensámblalo y córrelo. Verás un cuadro de mensaje desplegando el texto "Win32 Assembly is


Great!".

Veamos de nuevo el código fuente.

Definimos dos cadenas terminadas en cero en la sección .data. Recuerda que toda cadena
ANSI en Windows debe terminar en NULL (0 hexadecimal).

Usamos dos constantes, NULL y MB_OK. Esas constantes están documentadas en


windows.inc. Así que nos referiremos a ellas por nombres y no por valores. Esto facilita la
lectura de nuestro código fuente.

El operador addr es usado para pasar la dirección de una etiqueta a la función. Es válido sólo
en el contexto de la directiva invoke. No puedes usarla para asignar la dirección de un
registro/variable, por ejemplo. En vez de esto, puedes usar offset en el ejemplo anterior. Sin
embargo hay algunas diferencias entre los dos:

1. addr no puede manejar referencias delante de ella mientras que offset si puede. Por
ejemplo, si la etiqueta está definida en una parte más adelante del código fuente que la
directiva invoke, entonces addr no trabajará.

invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK


......
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0

MASM reportará error.Si usas offset en vez de addr en el recorte de código de arriba,
MASM lo ensamblará felizmente.
2. addr puede manejar variables locales mientras que offset no puede. Una variable local
es un espacio reservado en algún lugar de la pila. No conocerás su dirección durante el
tiempo de ejecución. offset es interpretado por el ensamblador durante el tiempo de
ensamblaje. Así que es natural que offset no trabaje para variables locales. addr puede
manejar variables locales debido a que el ensamblador chequea primero si la variable
referida por addr es global o local. Si es una variable global, pone la dirección de la
variable dentro del archivo objeto. En este aspecto, trabaja como offset. Si la variable
es local, genera uina secuencia de instrucciones como la siguiente antes de llamar a la
función:

lea eax, LocalVar


push eax

Puesto que "lea" puede determinar la dirección de una etiqueta en tiempo de ejecución,
esto trabaja bien.

En este tutorial, construiremos un programa para Windows que despliegue una ventana
completamente funcional sobre el escritorio.

Puedes bajar un archivo de ejemplo aquí

Teoría:

Los programas de Windows realizan la parte pesada del trabajo de programación a


través funciones API para sus GUI (Graphic User Interface = Interface de Usuario
Gráfica). Esto beneficia a los usuarios y a los programadores. A los ususarios, porque
no tienen que aprender cómo navegar por la GUI de cada nuevo programa, ya que las
GUIs de los programas Windows son semejantes. A los programadores, porque tienen
ya a su disposición las rutinas GUI, probadas y listas para ser usadas. El lado negativo
para los programadores es la creciente complejidad involucrada. Con el fin de crear o de
manipular cualquiera de los objetos GUI, tales como ventanas, menúes o iconos, los
programadores deben seguir un "récipe" estricto. Pero esto puede ser superado a través
de programación modular o siguiendo el paradigma de Progranación orientada a
Objetos (OOP = Object Oriented Programming).

Esbozaré los pasos requeridos para crear una ventana sobre el escritorio:

1. Obtener el manejador [handle] de instancia del programa (requerido)


2. Obtener la línea de comando (no se requiere a menos que el programa vaya a
procesar la línea de comando)
3. Registrar la clase de ventana (requerido, al menos que vayan a usarse tipos de
ventana predefinidos, eg. MessageBox o una caja o de diálogo)
4. Crear la ventana (requerido)
5. Mostrar la ventana en el escritorio (requerido al menos que se quiera mostrar la
ventana inmediatamente)
6. Refrescar el área cliente de la ventana
7. Introducir un bucle (loop) infinito, que chequée los mensajes de Windows
8. Si llega un mensaje, es procesado por una función especial, que es responsable
por la ventana
9. Quitar el programa si el usuario cierra la ventana
Como puedes ver, la estructura de un programa de Windows es más compleja que la de
un programa de DOS, ya que el mundo de Windows es totalmente diferente al mundo
de DOS. Los programas de Windows deben ser capaces de coexistir pacíficamente uno
junto a otro. Por eso deben seguir reglas estrictas. Tú (o Usted), como programador,
debes ser más estricto con tus estilos y hábitos de programación.

Contenido:

Abajo está el código fuente de nuestro programa de ventana simple. Antes de entrar en
los sangrientos detalles de la programación Win32 ASM, adelantaré algunos puntos
delicados que facilitarán la programación.

• Se deberían poner todas las constantes, estructuras, y prototipos de funciones de


Windows en un archivo include e incluirlo al comienzo de nuestro archivo .asm.
Esto nos ahorrará esfuerzos y gran cantidad de tipeo. Generalmente, el archivo
include más completo para MASM es windows.inc de hutch, el cual puedes
bajar desde su página o mi página. Puedes definir tus propias constantes y
estructuras pero deberías ponerlas en un archivo include separado.
• Usa la directiva includelib para especificar la librería de importación usada en tu
programa. Por ejemplo, si tu programa llama a MessageBox, deberías poner la
línea:

• includelib user32.lib

al comienzo de tu archivo .asm. Esta directiva dice a MASM que tu programa


hará uso de funciones es esa librería de importación. Si tu programa llama
funciones en más de una librería, entonces hay que agregar una línea includelib
para cada librería que se vaya a usar. Usando la directiva includelib no tendrás
que preocuparte de las librerías de importación en el momento de enlazar.
Puedes usar el conmutador del enlazador /LIBPATH para decirle a Link donde
están todas las librerías.

Cuando declares prototipos de funciones de la API, estructuras, o constantes en


un archivo include, trata de emplear los nombres originales usados en archivos
include de Windows, cuidando siempre la diferencia entre mayúsculas y
minúsculas. Esto te liberará de dolores de cabeza cuando necesites buscar
información sobre algún elemento en la referencia de la API de Win32.

• Usa un archivo makefile para automatizar el proceso de ensamblaje. Esto te


liberará de tener que tipear más de la cuenta.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib ; llamadas a las funciones en user32.lib y
kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.DATA ; data inicializada


ClassName db "SimpleWinClass",0 ; el nombre de nuestra clase de ventana
AppName db "Our First Window",0 ; el nombre de nuestra ventana

.DATA? ; data no inicializada


hInstance HINSTANCE ? ; manejador de instancia de nuestro programa
CommandLine LPSTR ?
.CODE ; Aquí comienza nuestro código
start:
invoke GetModuleHandle, NULL ; obtener el manejador de instancia del
programa.
; En Win32, hmodule==hinstance

mov hInstance,eax
invoke GetCommandLine ; Obtener la línea de comando. No hay que llamar esta función
; si el programa no procesa la línea de comando
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; llamar la función
principal
invoke ExitProcess ; quitar nuestro programa. El código de salida es devuelto en eax desde
WinMain.

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWOR
D
LOCAL wc:WNDCLASSEX ; crear variables locales en la pila (stack)

LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX ; Llenar los valores de los


miembros de wc
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc ; registrar nuestra clase de
ventana
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; desplegar nuestra ventana en el
escritorio
invoke UpdateWindow, hwnd ; refrescar el area cliente

.WHILE TRUE ; Introducir en bucle (loop) de mensajes


invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ; Regresar el código de salida en eax
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM,


lParam:LPARAM
.IF uMsg==WM_DESTROY ; si el usuario cierra nuestra ventana
invoke PostQuitMessage,NULL ; quitar nuestra aplicación
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Procesar el mensaje por defecto
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Análisis:

Puede parecer desconcertante que un simple programa de Windows requiera tanto


código. Pero muchas de estas rutinas son verdaderamente un código *plantilla* que
puede copiarse de un archivo de código fuente a otro. O si prefieres, podrías ensamblar
algunas de estas rutinas en una librería para ser usadas como rutinas de prólogo o de
epílogo. Puedes escribir solamente las rutinas en la función WinMain. En realidad, esto
es lo que hacen los compiladores en C: permiten escribir las rutinas en WinMain sin que
tengas que preocuparte de otros asuntos ruitinarios y domésticos. Todo lo que hay que
hacer es tener una función llamada WinMain, sino los compiladores C no serán capaces
de combinar tus rutinas con el prólogo y el epílogo. No exiten estas restricciones con
lenguaje ensamblador. Puedes usar otros nombres de funciones en vez de WinMain o no
emplear esta función en ninguna parte.

Bueno, ahora prepárate. Esto va a ser largo, un largo tutorial ¡Vamos a analizar este
programa hasta la muerte!!

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

Las primeras tres líneas son "necesarias". .386 dice a MASM que intentamos usar en
nuestro programa el conjunto de instrucciones para los porcesadores 80386 o
superiores. .model flat,stdcall a MASM que nuestro programa usa el modelo de
direccionamiento de memoria plana (flat). También usaremos la convención de paso de
parámetros stdcall como la convención por defecto del programa.

Lo siguiente constituye el prototipo para la función WinMain. Ya que llamaremos más


tarde a WinMain, debemos definir su prototipo de función primero para que podamos
invocarle.
Debemos incluir windows.inc al comienzo del codigo fuente. Contiene estructuras y
constantes importantes que son usadas por nuestro programa. El archivo include,
windows.inc, es un archivo de texto. Puedes abrirlo con cualquier editor de texto. Por
favor, nota que windows.inc no contiene todas las estructuras y constantes (todavía).
hutch y yo estamos trabajando en ello. Puedes agregarle elementos nuevos si ellos no
están en el archivo.

Nuestro programa llama las funciones API que residen en user32.dll (CreateWindowEx,
RegisterWindowClassEx, por ejemplo) y kernel32.dll (ExitProcess), así que debemos
enlazar nuestro programa a esas librerías de importación. La próxima cuestión es:
¿cómo podemos saber cuál librería debe ser enlazada con nuestro programa? La
respuesta es : debes saber donde residen las funciones API llamadas por el programa.
Por ejemplo, si llmas una función API en gdi32.dll, debes enlazarla con gdi32.lib.

Esta es la manera como lo hace MASM. El método de TASM para importar librerías a
través del enlace es mucho más simple: sólo hay que enlazar un archivo : import32.lib.

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

Siguen las secciones "DATA".

En .DATA, declaramos dos cadenas de caracteres terminadas en cero (cadenas


ASCIIZ): ClassName, que es el nombre de nuestra clase de ventana, y AppName, el
nombre de nuestra ventana. Nota que las dos variables están inicializadas.

En .DATA?, son declaradas tres variables: hInstance (manejador [handle] de instancia


de nuestro programa), CommandLine (linea de comando de nuestro programa), y
CommandShow (estado de nuestro programa en su primera aparición). Los tipos no
familiares de datos, HINSTANCE y LPSTR, realmente son nombres nuevos para
DWORD. Puedes verificarlo en windows.inc. Nota que todas las variables en la sección
.DATA? no están inicializadas, es decir, no tienen que tener ningún valor especíifico al
inicio, pero queremos reservarle un espacio para su usarlas en el futuro.

.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine,
SW_SHOWDEFAULT
invoke ExitProcess,eax
.....
end start

.CODE contiene todas las instrucciones. Tus instrucciones deben residir entre <etiqueta
inicio> (start) y terminar en <etiqueta inicio> (end start). El nombre de las etiquetas es
importante. Puedes llamarla de la forma que quieras siempre y cuando no violes las
convenciones para los nombres de MASM.

Nuestra primera instrucción llama a GetModuleHandle para recuperar el manejador de


instancia de nuestro programa. Bajo Win32, el manejador de la instancia y el manejador
del módulo son una y la misma cosa. Se puede pensar en el manejador de instancia
como el ID de nuestro programa. Es usado como parámetro en algunas funciones API
que nuestro programa debe llamar, así que generalmente es una buena idea obtenerlo al
comienzo del programa.

Nota: Realmente bajo win32, el manejador de instancia es la dirección lineal de


nuestro programa en la memoria.

Al regresar a una función de Win32, el valor regresado, si hay alguno, puede


encontrarse en el registro eax. Todos los demás valores son regresados a través de
variables pasadas en la lista de parámetros de la función que va a ser llamada.
Cuando se llama a una función Win32, casi siempre preservará los registros de
segmento y los registros ebx, edi, esi y ebp. Al contrario, los registros eax, ecx y edx
son considerados como registros dinámicos y siempre sus valores son indeterminados e
impredecibles cuando retorna una función Win32.

Nota: No esperes que los valores de eax, ecx, edx sean preservados durante las
llamadas a una función API.

La línea inferior establece que: cuando se llama a una fucnión API, se espera que
regrese el valor en eax. Si cualquiera de las funciones que creamos es llamada por
Windows, también debe seguir la siguiente regla: preservar y restablecer los valores de
los registros de segmentos ebx, edi, esi y ebp cuando la función regrese, sino el
programa se quebrará (crash) de inmediato, esto incluye el procedimiento de ventana y
las funciones callback de ventanas.

La llamada a GetCommandLine es inecesaria si el programa no procesa la línea de


comando. En este ejemplo muestro como llamarla en caso que sea necesario en un
programa.

Lo siguiente es la llamada a WinMain. Aquí recibe cuatro parámetros: el manejador de


instancia de nuestro programa, el manejador de instancia de la instancia previa del
programa, la línea de comando y el estado de la ventana en su primera aparición. Bajo
Win32, no hay instancia previa. Cada programa está aislado en su espacio de
direcciones, así que el valor de hPrevInst siempre es 0. Esto es uno de los restos de los
días de Win16 cuando todas las instancias de un programa corrían en el mismo espacio
de direcciones y una instancia necesitaba saber si era la primera. En win16, si hPrevInst
es NULL entonces es la primera instancia.

Nota: Esta función no tiene que ser declarada como WinMain. En realidad, hay
completa libertad a este respecto. Ni siquiera hay que usar siempre una función
equivalente a WinMain. Se puede pegar el código dentro de la función WinMain
inmediatamente después de GetCommandLine y el programa funcionará perfectamente.

Al regresar de WinMain, eax tiene el código de salida. Pasamos el código de salida


como parámetro de ExitProcess, que terminará nuestra aplicación.

WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

La línea de arriba forma la declaración de la función WinMain. Nota que los pares
parámetro:tipo que siguen a la directiva PROC. Son parámetros queWinMain recibe
desde la instrucción que hace la llamada [caller]. Puedes referirte a estos parámetros por
nombre en vez de a través de la manipulación de la pila. Además, MASM generará los
códigos de prólogo y epílogo para la función. Así que no tenemos que preocuparnos del
marco de la pila cuando la función entre (enter) y salga (exit).

LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
La directiva LOCAL localiza memoria de la pila para las variables locales usadas en la
función. El conjunto de directivas LOCAL debe estar ubicado inmediatamente abajo de
la directiva PROC.

La directiva LOCAL es seguida inmediatamente por <nombre de la variable


local>:<tipo de variable>. Así que LOCAL wc:WNDCLASSEX le dice a MASM que
localize memoria de la pila con un espacio equivalente al tamaño de la estructura
WNDCLASSEX para la variable llamada wc. Podemos hacer referencia a wc en nuestro
código sin ninguna dificultad en la manipulación de la pila. Creo que esto es realmente
una bendición. El aspecto negativo de esto es que las variables locales no pueden ser
usadas fuera de la función porque ellas son creadas para ser destruidas inmediatamante
cuando la función retorna a la rutina desde la cual fue llamada. Otra contrapartida es que
no se pueden inicializar variables locales automáticamente porque ellas son localizadas
dinámicamente en la memoria de la pila cuando la función es introducida (entered). Hay
que asignarlas manualmente con los valores deseados después de las directivas LOCAL.

mov wc.cbSize,SIZEOF WNDCLASSEX


mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc

Las líneas intimidantes de arriba son realmente comprensibles en cuanto concepto.


Toma varias líneas de instrucciones realizar la operación ahí implicada. El concepto
detrás de todas estas líneas es el de clase de ventana (window class). Una clase de
ventana no es más que un anteproyecto o especificación de una ventana. Define algunas
de las características importantes de una ventana tales como un icono, su cursor, la
función que se resposabiliza de ella, su color etc. Una ventana se crea a partir de una
clase de ventana. Este es una especie de concepto orientado a objeto. Si se quiere crear
más de una ventana con las mismas características, lo razonable es almacenar todas
estas características en un solo lugar y referirse a ellas cuando sea necesario. Este
esquema salva gran cantidad de memoria evitando duplicación de código. Hay que
recordar que Windows fue diseñado cuando los chips de memoria eran prohibitivos ya
que una computadora tenía apenas 1 MB de memoria. Windows debía ser muy eficiente
al usar recursos de memorias escasos. El punto es: si defines tu propia ventana, debes
llenar las características de tu ventana en una estructura WNDCLASS o
WNDCLASSEX y llamar a RegisterClass o RegisterClassEx antes de crear la ventana.
Sólo hay que registrar la clase de ventana una vez para cada tipo de ventana que se
quiera crear desde una clase de ventana.
Windows tiene varias clases de ventanas predefinidas, tales como botón (button) y caja
de edición (edit box). Para estas ventanas (o controles), no tienes que registrar una clase
de venana, sólo hay que llamara a CreateWindowEx con el nombre de la clase
predefinido.

El miembro más importante en WNDCLASSEX es lpfnWndProc. lpfn se concibe como


un puntero largo a una función. Bajo Win32, no hay puntero "cercano" o "lejano"
pointer, sino sólo puntero, debido al nuevo modelo de memoria FLAT. Pero esto
también es otro de los restos de los días de Win16. Cada clase de ventana debe estar
asociada con la función llmada procedimiento de ventana. El procedimiento de ventana
es la función responsable por el manejo de mensajes de todas las ventanas creadas a
partir de la clase de ventana asociada. Windows enviará mensajes al procedimiento de
ventana para notificarle sobre eventos importantes concernientes a la ventana de la cual
el procedimiento es responsable, tal como el uso del teclado o la entrada del ratón. Le
toca al procedimiento de ventana responder inteligentemante a cada evento que recibe la
ventana. Seguro que pasarás bastante tiempo escribiendo manejadores de evento en el
procedimiento de ventana.

Describo abajo los miembros de WNDCLASSEX:

WNDCLASSEX STRUCT DWORD


cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
hIconSm DWORD ?
WNDCLASSEX ENDS

cbSize: Tamaño de la estructura WNDCLASSEX en bytes. Podemos usar el operador


SIZEOF para obtener este valor.
style: El estilo para las ventanas creadas a partir de esta clase. Se pueden combinar
varios tipos de estilo combinando el operador "or".
lpfnWndProc: La dirección del procedimiento de ventana responsable para las
ventanas creadas a partir de esta clase.
cbClsExtra: Especifica el número de bytes extra para localizar la siguiente estructura
de clase de ventana. El sistema operativo inicializa los bytes poniéndolos en cero.
Puedes almacenar aquí datos específcos de la clase de ventana.
cbWndExtra: : Especifica el número de bytes extra para localizar the window instance.
El sistema operativo inicializa los bytes poniéndolos en cero. Si una aplicación usa la
estructura WNDCLASS para registrar un cuadro de diálogo creado al usar la directiva
CLASS en el archivo de recurso, debe poner este miembro en DLGWINDOWEXTRA.
hInstance: Manejador de instancia del módulo.
hIcon: Manejador [handle] del icono. Se obtiene llamando a LoadIcon.
hCursor: Manejador del cursor. Se obtiene llamando a LoadCursor.
hbrBackground: Color de fondo de la ventana creada a partir de esta clase.
lpszMenuName: Manejador del menú por defecto para la ventana creada a partir de
esta clase.
lpszClassName: Nombre para esta clase de ventana.
hIconSm: Manejador del icono pequeño asociado con la clse de ventana. Si este
miembro es NULL, el sistema busca el recurso de icono especificado por el miembro
hIcon para un icono de tamaño apropiado para ser usado como icono pequeño.

invoke CreateWindowEx, NULL,\


ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL

Después de registrar la clase de ventana, podemos llamar a CreateWindowEx para crear


nuestra ventana basada en la clase de ventana propuesta. Nota que hay 12 parámetros
para esta función.

CreateWindowExA proto dwExStyle:DWORD,\


lpClassName:DWORD,\
lpWindowName:DWORD,\
dwStyle:DWORD,\
X:DWORD,\
Y:DWORD,\
nWidth:DWORD,\
nHeight:DWORD,\
hWndParent:DWORD ,\
hMenu:DWORD,\
hInstance:DWORD,\
lpParam:DWORD

Veamos la descripción detallada de cada parámetro:

dwExStyle: Estilos extra de ventana. Es el nuevo parámetro agregado a la antigua


función CreateWindow. Aquí puedes poner estilos nuevos para Windows 95 y NT.
Puedes especificar tu estilo de ventana ordinario en dwStyle pero si quieres algunos
estilos especiales tales como "topmost window" (nventana en el tope), debes
especificarlos aquí. Puedes usar NULL si no quieres usar estilos de ventana extra.

lpClassName: (Requerido). Dirección de la cadena ASCIIZ que contiene el nombre de


la clase de ventana que quieres usar como plantilla para esta ventana. La Clase puede
ser una clase registrada por tí mismo o una clase de ventana predefinida. Como se
estableció arriba, todas las ventanas que creas deben estar basadas en una clase de
ventana.

lpWindowName: Dirección de la cadena ASCIIZ que contiene el nombre de la


ventana. Será mostrada en la barra de título de la ventana. Si este parámetro es NULL,
la barra de título de la ventana aparecería en blanco.

dwStyle: Estilos de la ventana. Aquí puedes especificar la apariencia de la ventana.


Pasar NULL pero la ventana no tendrá el menú de sistema, ni botones minimizar-
maximizar, y tampoco el botón cerrar-ventana. La ventana no sería de mucha utilidad.
Necesitarás presionarAlt+F4 para cerrarla. El estilo de ventana más común es
WS_OVERLAPPEDWINDOW. UN estilo de ventana sólo es una bandera de un bit.
Así que puedes combinar varios estilos usando el operador "or" para alcanzar la
apariencia deseada de la ventana. El estilo WS_OVERLAPPEDWINDOW es realmante
la combinación de muchos estilos de ventana comunes empleando este método.

X,Y: La coordenada de la esquina izquierda superior de la vetana. Noramlamente este


valor debería ser CW_USEDEFAULT, es decir, deseas que Windows decida por tí
dónde poner la ventana en el escritorio.

nWidth, nHeight: El ancho y el alto de la ventana en pixeles. También puedes usar


CW_USEDEFAULT para dejar que Windows elija por tí el ancho y la altura apropiada.

hWndParent: El manejador [handle] de la ventana padre de la ventana (si existe). Este


parámetro dice a Windows si esta ventana es una hija (subordinada) de alguna otra
ventana y, si lo es, cual ventana es el padre. Nota que no se trata del tipo de
interrelación padre-hija de la MDI (Multiple Document Interface = Interface de
Documento Múltiple). Las ventanas hijas no están restringidas a ocupar el área cliente
de la ventana padre. Esta interrelación es únicamente para uso interno de Windows. Si
la ventana padre es destruida, todas las ventanas hijas serán destruidas automáticamente.
Es realmente simple. Como en nuestro ejemplo sólo hay una ventana, especificamos
este parámetro como NULL.

hMenu: Manejador del menú de la ventana. NULL si la clase menú va a ser usada.
Observa el miembro de la estructura WNDCLASSEX, lpszMenuName. lpszMenuName
especifica el menú *por defecto* para la clase de ventana. Toda ventana creada a partir
de esta clase de ventana tendrá por defecto el mismo menú, a menos que especifiques un
nuevo menú *imponiéndolo* a una ventana específica a través de su parámetro hMenu.
hMenu es realmente un parámetro de doble propósito. En caso de que la ventana que
quieras crear sea de un tipo predefinido (como un control), tal control no puede ser
propietario de un menú. hMenu es usado entonces más bien como un ID de control.
Windows puede decidir si hMenu es realmente un manejador de menú o un ID de
control revisando el parámetro lpClassName. Si es el nombre de una clase de ventana
predefinida, hMenu es un ID de control, sino entonces es el manejador del menú de la
ventana.

hInstance: El manejador de instancia para el módulo del programa que crea la ventana.

lpParam: Puntero opcional a la estructura pasada a la ventana. Es usada por la ventana


MDI para pasar los datos CLIENTCREATESTRUCT. Normalmente, este valor es
puesto en NULL, que significa que ningún dato es pasado vía CreateWindow(). La
ventana puede recibir el valor de este parámetro llamando a la función
GetWindowLong.

mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd

El regreso satisfactorio de CreateWindowEx, devuelve el manejador de ventana en eax.


Debemos conservar este valor para usarlo luego. La ventana que creamos no es
desplegada inmediatamente. Debes llamar a ShowWindow con el manejador de ventana
y el *estado de despliegue* deseado para la ventana y desplegarla sobre el monitor.
Luego puedes llamara a UpdateWindow con el fin de que tu ventana vuelva a dibujarse
sobre el area cliente. Esta función es útil cuando se desea actualizar el contenido del
area cliente. Aunque puedes omitir esta llamada.

.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW

En este momento nuestra ventana está desplegada en la pantalla. Pero no puede recibir
entrada del mundo exterior. Así que tendremos que *informarle* de los eventos
relevantes. Hacemos esto con un bucle de mensajes. Sólo hay un bucle de mensaje para
cada módulo. Este bucle de mensaje chequea continuamente los mensajes de Windows
llamando a GetMessage. GetMessage pasa a Windows un puntero a una estructura
MSG. Esta estructura MSG será llenada con información sobre el mensaje que
Windows quiere enviar a una ventana en el módulo. La función GetMessage no
regresará hasta que haya un mensaje para una ventana en el módulo. Durante ese
tiempo, Windows puede darle el control a otros programas. Esto es lo que forma el
esquema de multitareas cooperativas de la plataforma Win16. GetMessage regresa
FALSE si el mensaje WM_QUIT es recibido en el bucle de mensaje, lo cual terminará
el programa y cerrará la aplicación.

TranslateMessage es una útil función que toma la entrada desde el teclado y genera un
nuevo mensaje (WM_CHAR) que es colocado en la cola de mensajes. El mensaje
WM_CHAR viene acompañado del valor ASCII de la tecla presionada, el cual es más
fácil de manipular que los códigos brutos de lectura de teclado. Se puede omitir esta
llamada si el programa no procesa los golpes de tecla.

DispatchMessage envía los datos del mensaje al procedimiento de ventana responsable


por la ventana específica a la cual va dirigido el mensaje.

mov eax,msg.wParam
ret
WinMain endp
Si termina el bucle de mensaje, el código de salida es almacenado en el miembro
wParam de la estructura MSG. Puedes almacenar el código de salida en eax para
regresarlo a Windows. Para estos momentos, Windows no usa el valor regresado, pero
es mejor hacerlo por si acaso y para jugar con las reglas.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM,


lParam:LPARAM

Este es nuestro procedimiento de ventana. No tienes que llamarlo necesariamente


WndProc. El primer parámetro, hWnd, es el manejador de la ventana hacia el cual el
mensaje está destinado. uMsg es el mensaje. Nota que uMsg no es una estructura MSG.
Realmente sólo es un número. Windows define cientos de mensajes, muchos de los
cuales carecen de interés para nuestro programa. Windows enviará un mensaje
apropiado a la ventana en caso de que ocurra algo relevante a la ventana. El
procedimiento de ventana recibe el mensage y recciona a él inteligentemente. wParam y
lParam sólo son parámetros extra a ser utiizados por algunos mensajes. Algunos
mensajes envían datos junto con ellos en adición al mensaje propiamente dicho. Estos
datos son pasados al procedimiento de ventana por medio de lParam y wParam.

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

Aquí viene la parte crucial. Es donde reside gran parte de la inteligencia de los
programas. El código que responde a cada mensaje de Windows está en el
procedimiento de ventana. El código debe chequear el mensaje de Windows para ver si
hay un mensaje que sea de interés. Si lo es, se hace algo que se desee en respuesta a ese
mensaje y luego se regresa cero en eax. Si no es así, debe llamarse a DefWindowProc,
pasando todos los parámetros recibidos para su procesamiento por defecto.
DefWindowProc es una función de la API que procesa los mensajes en los que tu
programa no está interesado.

El único mensaje a que DEBES responder es WM_DESTROY. Este mensaje es enviado


a tu procedimiento de ventana cada vez que la ventana se va a cerrar. En el momento
que tu procedimiento de diálogo reciba este mensaje, tu ventana estará ya removida del
escritorio. Este mensaje sólo es una notificación de que tu ventana ha sido destruida y
de que debes prepararte para regresar a Windows. En respuesta a esto, puedes ejecutar
alguna tarea doméstica antes de regresar a Windows. No queda más opción que quitar
cuando la ventana llega a este estado. Si quieres tener la oportunidad de detener el
usuario cuando éste intente cerrar la ventana, debes procesar el mensaje WM_CLOSE.

Ahora, regresando a WM_DESTROY, después de ejecutar la tareas domésticas, debes


llamar al PostQuitMessage que enviará el mensaje WM_QUIT de vuelta a tu módulo.
WM_QUIT hará que GetMessage regrese el valor NULL en eax, el cual terminará el
bucle de mensajes y devolverá el control a Windows. Puedes enviar el mensaje
WM_DESTROY a tu propio procedimiento de ventana llamando a la función
DestroyWindow.

En este tutorial, aprenderemos como "pintar" texto en el área cliente de una ventana. También
aprenderemos sobre el contexto del dispositivo.

Puedes bajar el código fuente desde aquí.

Teoría:

El texto en Windows es un tipo de objeto GUI. Cada carácter está compuesto por numerosos
pixeles o puntos (dots) que están amontonados dentro de un patrones distintos. Por eso
hablamos de "pintar" en vez de "escribir". Normalmente, pintas texto en tu propia area cliente
(relamente, puedes también pintar fuera del area cliente, pero eso es otra historia). En
Windows, poner texto en la pantalla es algo radicalmente distinto a DOS. En DOS, puedes
pensar en la pantalla como una dimensión de 80x25. Pero en Windows, la pantalla es
compartida por varios programas. Algunas reglas deben ser reforzadas para evitar que los
programas escriban sobre la pantalla de otros. Windows asegura esto limitando el área para
pintar de cada ventana a su área cliente solamente. El tamaño del area cliente de una ventana
tampoco es constante. El usuario puede cambiarla en cualquier momento. Así que hay que
determinar dinámicamente las dimensiones del área cliente de las ventanas.

Antes de que puedas pintar algo sobre el área cliente, debes pedir permiso a Windows. Eso es
correcto, ya no tienes el control total sobre el monitor como lo tenías con DOS. Debes pedir
permiso a Windows para pintar tu propia area cliente. Windows determinará el tamaño de tu
área cliente, de la fuente, los colores y otros atributos GDI y regresará un manejador del
contexto de dispositivo a tu programa.

Luego puedes emplear tu contexto de dispositivo como un pasaporte para pintar tu área cliente.

¿Qué es un contexto de dispositivo? Es sólo una estructura de datos que Windows mantiene
en su interior. Un contexto de dispositivo está asociado con un dispositivo en particular, tal
como una impresora o un monitor de video. Para un monitor de video, un contexto de
dispositivo está normalmente asociado con una ventana particular en el monitor.

Algunos valores en el contexto de dispositivo son atributos gráficos como colores, fuentes etc.
Estos son valores por defecto que se pueden cambiar a voluntad. Existen para ayudar a reducir
la carga de tener que especificar estos atributos en todas las llamadas a funciones GDI.

Puedes pensar en un contexto de dispositivo como un ambiente por defecto preparado para tí
por Windows. Luego puedes anular algunos de los elementos establecidos por defecto si
quieres.

Cuando un programa necesita pintar, debe obtener un manejador al contexto de dispositivo.


Normalmente, hay varias maneras de realizar esto.

llamar a BeginPaint en respuesta al mensaje WM_PAINT.


llamar a GetDC en respuesta a otros mensajes.
llamar a CreateDC para crear tu propio contexto de dispositivo

Hay algo que debes recordar para después de que tengas el manejador [handle] del contexto
de dispositivo, y que debes realizar para el procesamiento de cualquier mensaje: no obtener el
manejador en respuesta a un mensaje y emplearlo como respuesta a otro.

Windows envía mensajes WM_PAINT a la ventana para notificar que es ahora el momento de
volver a pintar su área cliente. Windows no salva el contenido del área cliente de una
ventana. En vez de eso, cuando ocurre una situación que garantiza que se va a volver a pintar
el área cliente (tal como cuando una ventana ha sido cubierta por otra y luego descubierta),
Windows pone el mensaje WM_PAINT en la cola de mensajes de ese programa. Es
responsabilidad de Windows volver a pintar su propia área cliente. Debes reúnir toda la
información sobre cómo volver a pintar el área cliente en la sección WM_PAINT de tu
procedimiento de ventana, así que tu procedimiento de ventana puede volver a pintar tu area
cliente cuando llega el mensaje WM_PAINT.

Otro concepto que debes tener en consideración es el de rectángulo inválido. Windows define
un rectángulo inválido como el área rectangular más pequeña que el área cliente necesita para
volver a ser pintada. Cuando Windows detecta un rectángulo inválido en el área cliente de una
ventana, envía un mensaje WM_PAINT a esa ventana. En respuesta al mensaje WM_PAINT,
la ventana puede obtener una estructura paintstruct que contiene, entre otras cosas, la
coordenada del rectángulo inválido. Puedes llamar a BeginPaint en respuesta al mensaje
WM_PAINT para validar el rectángulo inválido. Si no procesas el mensaje WM_PAINT, al
menos debes llamar a DefWindowProc o a ValidateRect para validar el rectángulo inválido, sino
Windows te enviará repetidamente el mensaje WM_PAINT.

Estos son los pasos que deberías realizar en respuesta a un mensaje WM_PAINT:

Obtener un manejador al contexto de dispositivo con BeginPaint.


Pintar el área cliente.
Liberar el manejador del contexto de dispositivo con EndPaint

Nota que no tienes que validar explícitamente el rectángulo inválido. Esto es realizado
automáticamente por la llamada a BeginPaint. Entre las llamadas a BeginPaint y EndPaint,
puedes llamar cualquiera de las funciones GDI para pintar tu área. Casi todas ellas requieren el
manejador del contexto de dispositivo como parámetro.

Contenido:

Escribiremos un programa que despliega una cadena con el texto "Win32 assembly is great
and easy!" en el centro del área cliente.

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
OurText db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR,


CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax, eax
ret
WndProc endp
end start

Análisis:

La mayoría del código es el mismo que el del ejemplo del tutorial 3. Sólo explicaré los cambios
importantes.

LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT

Estas variables locales son usadas por las funciones GDI en tu sección WM_PAINT. hdc es
usado para almacenar el manejador al contexto de dispositivo regresado por la llamada a
BeginPaint. ps es una estructura PAINTSTRUCT. Normalmente no tienes que usar los valores
en ps. Es pasado a la función BeginPaint y Windows la llena con valores apropiados. Luego
pasa ps a la función EndPaint cuando terminas de pintar el área cliente. rect es una estructura
RECT definida así:

RECT Struct
left LONG ?
top LONG ?
right LONG ?
bottom LONG ?
RECT ends

left y top son las coordenadas de la esquina izquierda superior de un rectángulo. right y bottom
son las coordenadas de la esquina derecha inferior. Debe recordarse que: El origen de los ejes
x-y está en la esquina superior izquierda. Entonces el punto y=10 está DEBAJO del punto y=0.

invoke BeginPaint,hWnd, ADDR ps


mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps

En respuesta al mensaje WM_PAINT, llamas a BeginPaint pasando como parámetros al


manejador de la ventana que quieres pintar y una estructura PAINTSTRUCT no inicializada.
Después de una llamada exitosa, eax contiene el manejador al contexto de dispositivo. Luego
llamas a GetClientRect para recobrar la dimensión del área cliente. La dimensión es regresada
en la variable rect variable que tú pasas a DrawText como uno de sus parámetros. La sintaxis
de DrawText es:

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD,


uFormat:DWORD

DrawText es una función de la API de alto-nivel para salida de texto. Maneja algunos detalles
tales como ajuste de línea, centramiento, etc. así que puedes concentrarte sólo en la cadena
que quieres pintar. Su hermana de bajo nivel, TextOut, será examinada en el próximo tutorial.
DrawText formatea una cadena de texto para fijar dentro de los límites de un rectángulo.
Emplea la fuente seleccionada en el momento, color y fondo (en el contexto de dispositivo)
para dibujar texto. Las líneas son ajustadas para fijarla dentro de los límites del rectángulo.
Regresa la altura del texto de salida en unidades de dispositivo, en nuestro caso, pixeles.
Veamos sus parámetros:

hdc manejador al contexto de dispositivo


lpString El puntero a la cadena que quieres dibujar en el rectángulo. La cadena debe
estar terminada en NULL o sino tendrás que especificar su largo en el parámetro de
texto, nCount.
nCount El número de caracteres para salida. Si la cadena es terminada en cero,
nCount debe ser -1. De otra manera nCount debe contener el número de caracteres en
la cadena que quieres dibujar.
lpRect El puntero al rectángulo (una estructura de tipo RECT) donde quieres dibujar la
cadena. Nota que este rectángulo también es un rectángulo recortante [clipping
rectangule], es decir, no podrás dibujar la cadena fuera del rectángulo.
uFormat El valor que especifica como la cadena es desplegada en el rectángulo.
Usamos tres valores combinados por el operador "or":

o DT_SINGLELINE especifica una línea de texto


o DT_CENTER centra el texto horizontalmante.
o DT_VCENTER centra el texto verticalmente. Debe ser usado con
DT_SINGLELINE.

Después de terminar de pintar el área cliente, debes llamar a la función EndPaint para liberar el
manejador del contexto de dispositivo.

Eso es todo. Podemos hacer un sumario de los puntos relevantes:

• Llamas a BeginPaint-EndPaint en respuesta la mensaje WM_PAINT. Haces lo que


gustes con el área cliente de la ventana entre las llamadas a las funciones BeginPaint y
EndPaint.
• Si quieres volver a pintar tu área cliente en respuesta a otros mensajes, tienes dos
posibilidades:
o Usar el par GetDC-ReleaseDC y pintar entre estas dos llamadas
o Llamar a InvalidateRect o a UpdateWindow para invalidar toda el área cliente,
forzando a Windows a que ponga un mensaje WM_PAINT en la cola de
mensajes de tu ventana y pinte durante la sección WM_PAINT

Tutorial 5: Más sobre Textos


Experimentaremos más sobre los atributos de los textos, es decir, sobre las fuentes y los
colores.

Baja el archivo ejemplo aquí.

Teoría:

El sistema de colores de Windows está basado en valores RGB, R=red (rojo), G=Green
(verde), B=Blue (azul). Si quieres especificar un color en Windows, debes establecer el color
que desees en términos de estos tres colores mayores. Cada valor de color tiene un rango
desde 0 a 255 (un valor de un byte). Por ejemplo, si quieres un color rojo puro, deberías usar
255,0,0. O si quieres un color blanco puro, debes usar 255,255,255. Puedes ver en los
ejemplos que obtener el color que necesitas es muy difícil con este sistema ya que tienes que
tener una buena comprensión de como mezclar y hacer corresponder los colores.
Para el color del texto y del fondo, usas SetTextColor y SetBkColor, que requieren un
manejador al contexto del dispositivo y un valor RGB de 32-bit. La estructura dvalor RGB de
32-bit está definida así:

RGB_value struct
unused db 0
blue db ?
green db ?
red db ?
RGB_value ends

Nota que no se emplea el primer byte y que debería ser cero. El orden de los restantes tres
bytes es inverso, es decir. azul, verde y rojo. Sin embargo, no usaremos esta estructura ya que
es embarazoso inicializarla y usarla. Más bien crearemos una macro. La macro recibirá tres
parámetros: los valores rojo, verde y azul. Esto producirá el valor RGB 32-bit deseado y lo
almacenará en eax. La macro es como sigue a continuación:

RGB macro red,green,blue


xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

Puedes poner esta macro en el archivo include para usarla en el futuro.

Puedes "crear" una fuente llamando a CreateFont o a CreateFontIndirect. La diferencia entre


las dos funciones es que CreateFontIndirect recibe sólo un parámetro: un puntero a la
estructura lógica de la fuente, LOGFONT. CreateFontIndirect es la más flexible de las dos,
especialmente si tus programas necesitan cambiar de fuentes con frecuencia. Sin embargo,
como en nuestro ejemplo "crearemos" sólo una fuente para demostración, podemos hacerlos
con CreateFont. Después de llamada a CreateFont, regresará un manejador a la fuente que
debes seleccionar dentro del contexto de dispositivo. Después de eso, toda función de texto de
la API usará la fuente que hemos seleccionado dentro del contexto de dispositivo.

Contenido:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

RGB macro red,green,blue


xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
TestString db "Win32 assembly is great and easy!",0
FontName db "script",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL hfont:HFONT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName
invoke SelectObject, hdc, eax
mov hfont,eax
RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax
invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
invoke SelectObject,hdc, hfont
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Análisis:

invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName

CreateFont creará la fuente lógica que más coincida con los parámetros dados y con los datos
de fuentes disponibles. Esta función tiene más parámetros que cualquier otra función en
Windows. Regresa un manejador a la fuente lógica a ser usada por la función SelectObject.
Examinaremos sus parámetros en detalle.

CreateFont proto nHeight:DWORD,\


nWidth:DWORD,\
nEscapement:DWORD,\
nOrientation:DWORD,\
nWeight:DWORD,\
cItalic:DWORD,\
cUnderline:DWORD,\
cStrikeOut:DWORD,\
cCharSet:DWORD,\
cOutputPrecision:DWORD,\
cClipPrecision:DWORD,\
cQuality:DWORD,\
cPitchAndFamily:DWORD,\
lpFacename:DWORD

nHeight La altura deseada para los caracteres. 0 significa el tamaño por defecto.
nWidth La ancho deseado para los caracteres. Normalmente este valor debería ser 0 que
permite a Windows coordinar el ancho y el alto. Sin embargo, en nuestro ejemplo, el ancho por
defecto hace difícil la lectura del texto, así que usaremos mejor un ancho de 16.
nEscapement Especifica la orientación del próximo caracter de salida relativa al previo en
décimas de grados. Normalmente, se pone en 0. Si se pone en 900 tendremos que todos los
caracteres irán por encima del primer caracter, 1800 los escribirá hacia atrás, o 2700 para
escribir cada caracter desde abajo.
nOrientation Especifica cuánto debería ser rotado el caracter cuando tiene una salida en
décimas de grados. Si se pone en 900 todos los caracteres reposaran sobre sus respaldos,
1800 se emplea para escribirlos upside-down, etc.
nWeight Establece el grosor de las líneas de cada caracter. Windows define los siguientes
tamaños:

FW_DONTCARE equ 0
FW_THIN equ 100
FW_EXTRALIGHT equ 200
FW_ULTRALIGHT equ 200
FW_LIGHT equ 300
FW_NORMAL equ 400
FW_REGULAR equ 400
FW_MEDIUM equ 500
FW_SEMIBOLD equ 600
FW_DEMIBOLD equ 600
FW_BOLD equ 700
FW_EXTRABOLD equ 800
FW_ULTRABOLD equ 800
FW_HEAVY equ 900
FW_BLACK equ 900

cItalic 0 para normal, cualquier otro valor para caracteres en itálicas.


cUnderline 0 para normal, cualquier otro valor para caracteres subrayados.
cStrikeOut 0 para normal, cualquier otro valor para caracteres con una línea a través del
centro.
cCharSet Especifica la configuración de la fuente [character set]. Normalmente debería ser
OEM_CHARSET lo cual permite a Windows seleccionar la fuente dependiente del sistema
operativo.
cOutputPrecision Especifica cuánto la fuente seleccionada debe coincidir con las
características que queremos. Normalmente debería ser OUT_DEFAULT_PRECIS que define
la conducta de proyección por defecto de la fuente.
cClipPrecision Especifica la presisión del corte. La precisión del corte define cómo recortar
los caracteres que son parcialmente fuera de la región de recorte. Deberías poder obtenerlo
con CLIP_DEFAULT_PRECIS que define la conducta por defecto del recorte.
cQuality Especifiica la cualidad de la salida. La cualidad de salida define cuán
cuidadosamente la GDI debe intentar hacer coincidir los atributos de la fuente lógica con los de
la fuente física actual. Hay tres posibilidades: DEFAULT_QUALITY, PROOF_QUALITY and
DRAFT_QUALITY.
cPitchAndFamily Especifiica la pitch y familia de la fuente. Debes combinar el valor pitch
value y el valor de familia con el operador "or".
lpFacename Un puntero a una cadena terminada en cero que especifica la tipografía de la
fuente.

La descripción anterior no tiene nada de comprensible. Deberías revisar la referencia de la APi


de Win32 API para más detalles.
invoke SelectObject, hdc, eax
mov hfont,eax

Después de obtener el manejador lógico de la fuente, debemos usarlo para seleccionar la


fuente dentro del contexto de dispositivo llamando a SelectObject. SelectObject pone los
nuevos objetos GDI tales como bolígrafos, brochas, y fuentes dentro del contexto de dispositivo
a ser usado por las funciones GDI. Este regresa el manejador del objeto reemplazado, el cual
deberíamos salvar para su uso posterior a la llamada a SelectObject. Después de llamar a
SelectObject, cualquier función de salida de texto, usará la fuente que seleccionamos dentro
del contexto de dispositivo.

RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax

Usa la macro RGB para crear un valor de 32-bit RGB para ser usado por SetColorText y
SetBkColor.

invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString

Llama a la función TextOut para dibujar el texto sobre el área cliente. El texto estará en la
fuente y el color que especificamos previamente.

invoke SelectObject,hdc, hfont

Cuando estemos trabajando con fuentes, deberíamos almacenar la fuente original dentro del
contexto de dispositivo. Deberías almacenar siempre el objeto que reemplazaste en el contexto
de dispositivo.

Tutorial 6: Entrada del teclado


Aprenderemos como un programa de Windows recibe entrada de teclado.

Baja el ejemplo aquí.

Teoría:

Como normalmente sólo hay un teclado para cada PC, todos los programas de Windows deben
compartirlo entre sí. Windows es responsable de enviar los golpes de tecla a la ventana que
tiene el foco de entrada.

Aunque pueden haber varias ventanas en el monitor, sólo una de ellas tiene el foco de entrada.
La ventana que tiene el foco de entrada es la única que puede recibir los golpes de tecla.
Puedes diferenciar la ventana que tiene el foco de entrada de las otras ventanas observando la
barra de título. La barra de título del programa que tiene el foco está iluminada.

Realmente, hay dos tipos principales de mensajes de teclado, dependiendo de tu punto de vista
sobre el teclado. Puedes ver el teclado como una colección de teclas. En este caso, si
presionas una tecla, Windows envía un mensaje WM_KEYDOWN a la ventana que tiene el
foco de entrada, que notifica que una tecla ha sido presionada. Cuando sueltas la tecla,
Windows envía un mensaje WM_KEYUP. Tú tratas a las teclas como si fueran botones.

Otra manera de ver el teclado es como un dispositivo de entrada de caracteres. Cuando


presionas "una" tecla, Windows envía un mensaje WM_CHAR a la ventana que tiene el foco de
entrada, diciéndole que el usuario envía "un" caracter a ella. En realidad, Windows envía
mensajes WM_KEYDOWN y WM_KEYUP a la ventana que tiene el foco de entrada y esos
mensajes serán traducidos a mensajes WM_CHAR por una llamada a TranslateMessage. El
procedimiento de ventana puede decidir si procesa los tres mensajes o sólo los mensajes que
interesan. Muchas veces, podrás ignorar WM_KEYDOWN y WM_KEYUP ya que la función
TranslateMessage en el bucle de mensajes traduce los mensajes WM_KEYDOWN y
WM_KEYUP a mensajes WM_CHAR. En este tutorial nos concentraremos en WM_CHAR.

Ejemplo:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
char WPARAM 20h ; el caracter que el programa recibe del teclado

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke TextOut,hdc,0,0,ADDR char,1
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

char WPARAM 20h ; el caracter que el programa recibe del teclado

Esta es la variable que guardará el caracter recibido del teclado. Como el caracter es enviado
en WPARAM del procedimiento de ventana, por simplicidad definimos los tipos de variables
como WPARAM. El valor inicial es 20h o el espacio, ya que cuando nuestra ventana refresque
su área cliente por primera vez, ahí no habrá caracter de entrada. Así que preferimos desplegar
el espacio.
.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE

Esto es agregado al manejador del mensaje WM_CHAR en el procedimiento de ventana. Pone


el caracter dentro de la variable llamada "char" y luego llama a InvalidateRect. InvalidateRect
hace que le rectángulo específico en el área cliente quede invalidado para forzar a Windows
para que envíe el mensaje WM_PAINT al procedimiento de ventana. Su sintaxis es como
sigue:

InvalidateRect proto hWnd:HWND,\


lpRect:DWORD,\
bErase:DWORD

lpRect es un opuntero al rectángulo en el área clienete que queremos declarar inválida. Si este
parámetro es nulo, toda el área cliente será marcada como inválida.
bErase es una bandera que dice a Windows si necesita borrar el fondo. Su ventana es TRUE,
luego Windows borrará el fondo del rectángulo invalidado cuando se llama a BeginPaint.

Así que la estrategia que usamos aquí es: almacenamos toda la información necesaria
involucrada en la acción de pintar el área cliente y generar el mensaje WM_PAINT para pintar
el área cliente. Por supuesto, el código en la sección WM_PAINT debe saber de antemano qué
se espera de ella. Esto parece una manera indirecta de hacer las cosas, pero así es como lo
hace Windows.

Realmente podemos pintar el área cliente durante el proceso del mensaje WM_CHAR llamando
el par de funciones GetDC y ReleaseDC. No hay problema. Pero lo gracioso comienza cuando
nuestra ventana necesita volver a pintar su área cliente. Como el código que pinta el caracter
está en la sección WM_CHAR, el procedimiento de ventana no será capaz de pintar nuestro
caracter en el area cliente. Así que la linea de abajo es: poner todos el código y los datos
necesarios para que realicen la acción de pintar en WM_PAINT. Puedes enviar el mensaje
WM_PAINT desde cualquier lugar de tu código cada vez que quieras volver a pintar el área
cliente.

invoke TextOut,hdc,0,0,ADDR char,1

Cuando se llama a InvalidateRect, envía un mensaje WM_PAINT de regreso al procedimiento


de ventana. De esta manera es llamado el código en la sección WM_PAINT. Llama a
BeginPaint como es usual para obtener el manejador al contexto del dispositivo y luego llama a
TextOut que dibuja nuestro caracter en el area cliente en x=0, y=0. Cuando corres el programa
y presionas cualquier tecla, verás un "eco" [echo] del caracter en la esquina izquierda superior
del área cliente de la ventana. Y cuando la ventana sea minimizada, al ser maximizada de
nuevo tendrá todavía el caracter ahí ya que todo el código y los datos esenciales para volver a
pintar son todos activados en la sección WM_PAINT.

Tutorial 7: Entrada del Ratón


Aprenderemos a recibir y a responder a la entrada del ratón en nuestro procedimiento de
ventana. El programa esperará por los clicks del botón izquierdo del ratón y desplegará una
cadena de texto en exactamente el punto indicado por el ratón sobre el área cliente.

Bajar el ejemplo de aquí.

Teoría:
Como con la entrada del teclado, Windows detecta y envía notificaciones sobre las actividades
del ratón que son relevantes para las ventanas. Esas actividades incluyen los clicks de los
botones izquierdo y derecho del ratón, el movimiento del cursor del ratón sobre la ventana,
doble clicks. A diferencia de la entrada del teclado, que es dirigida a la ventana que tiene el
foco de entrada, los mensajes del ratón son enviados a cualquier ventana sobre la cual esté el
cursor del ratón, activo o no. Además, también hay mensajes del ratón sobre el área no cliente.
Pero la mayoría de las veces, afortunademente podemos ignorarlas. Podemos concentrarnos
en los mensajes relacionados con el área cliente.

Hay dos mensajes para cada botón el ratón: los mensajes WM_LBUTTONDOWN,
WM_RBUTTONDOWN y WM_LBUTTONUP, WM_RBUTTONUP. Para un ratón con tres
botones, están también WM_MBUTTONDOWN and WM_MBUTTONUP. Cuando el cursor del
ratón se mueve sobre el área cliente, Windows envía mensajes WM_MOUSEMOVE a la
ventana debajo del cursor. Una ventana puede recibir mensajes de doble clicks,
WM_LBUTTONDBCLK o WM_RBUTTONDBCLK, si y sólo si la clase de su ventana tiene
activada la bandera correspondiente al estilo CS_DBLCLKS, sino la ventana recibirá sólo una
serie de mensajes del topo botón del ratón arriba o abajo.

Para todos estos mensajes, el valor de lParam contiene la posición del ratón. La palabra [word]
baja es la coordenada 'x', y la palabra alta es la coordenada 'y' relativa a la esquina izquierda
superior del área cliente de la ventana. wParam indica el estado de los botones del ratón y de
las teclas Shift y Ctrl.

Ejemplo:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MouseClick db 0 ; 0=no click yet

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hitpoint POINT <>

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax
WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE

El procedimiento de ventana espera a que el botón izquierdo del ratón haga un click. Cuando
recibe el mensaje WM_LBUTTONDOWN, lParam contiene la coordenada del botón del ratón
en el área cliente. Salva la coordenada en la variable de tipo POINT definida así:

POINT STRUCT
x dd ?
y dd ?
POINT ENDS

y establece la bandera, MouseClick, a TRUE, lo que siginifica que al menos hay un click del
botón izquierdo del ratón sobre el área cliente.

mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax

Como la coordenada 'x' es la palabra baja de lParam y los miembros de la estructura POINT
tiene un tamaño de 32-bits, debemos poner en cero la palabara alta de eax antes de
almacenarla en hitpoint.x.

shr eax,16
mov hitpoint.y,eax

Como la coordenada 'y' es la palabra alta de lParam, debemos ponerla en la palabra baja de
eax antes de almacenarla en hitpoint.y. Hacemos esto desplazando [shifting] el contenido de
eax 16 bits a la derecha.

Después de almacenar la posición del ratón, establecemos la bandera, MouseClick, a TRUE


con el fin de dejar que el código de pintura en la sección WM_PAINT sepa que hay al menos un
click en el área cliente y puede dibujar la cadena en la posición del ratón. Luego llamamos a la
función InvalidateRect para que Windows vuelva a pintar toda el área cliente.

.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
El código de pintura en la sección WM_PAINT debe chequear si MouseClick es uno ( TRUE ),
ya que cuando la ventana fue creada, recibió un mensaje WM_PAINT en ese momento, ningún
click del ratón había ocurrido aún, así que no dibujará la cadena en el área cliente.
Inicializamos MouseClick a FALSE y cambiamos su valor a TRUE cuando ocurre un click del
ratón.

Si ha ocurrido al menos un click de ratón, se dibuja la cadena en el área cliente en la posición


del ratón. Nota que se llama a lstrlen para obtener el tamaño de la cadena a desplegar y envía
este tamaño como último parámetro de la función TextOut.

En este tutorial aprenderemos como incorporar un menú a nuestra ventana.


Bajar el ejemplo 1 y el ejemplo 2.

Teoría:

El menú es uno de los componentes más importantes en nuestra ventana. El menú presenta
una lista de los servicios que un programa ofrece a un usuario. El usuario ya no tiene que leer
el manual incluido con el programa para utilizarlo, ya que puede leerse cuidadosamente el
menú para obtener una visión general de las capacidades de un programa particular y
comenzar a trabajar con él inmediatamante. Como el menú es una herramienta para obtener el
acercamiento del usuario y correr el programa rápidamente, se debería seguir siempre el
estandard. Puesto sucintamente, los primeros dos elementos [items] del menú deberían ser
Archivo [File] y Editar [Edit] y el último debería ser Ayuda [Help]. Puedes insertar tus propios
elementos de menú entre Editar y Ayuda. Si un elemento de menú invoca una caja de diálogo,
deberías anexar una ellipsis (...) a la cadena del menú.

El menú es un tipo de recurso. Hay varios tipos de recursos, tales como dialog box, string table,
icon, bitmap, menú etc. Los recursos son descritos en un archivo separado llamado archivo de
recursos, el cual generalmente tiene extensión .rc. Luego combinas los recursos cion el archivo
fuente durante el estadío de enlace. El resultado final es un archivo ejecutable que contiene
tanto instrucciones como recursos.

Puedes escribir guiones [scripts] de recursos usando un editor de texto. Estos guiones están
compuestos por frases que describen la apariencia y otros atributos de los recursos usados en
un programa particular. Aunque puedes escribir guiones de recursos con un editor de texto,
esto resulta más bien embarazoso. Una mejor altrenativa es usar un editor de recursos que te
permita visualizar con facilidad el diseño de los recursos. Usualmente los paquetes de
compiladores como Visual C++, Borland C++, etc, incluyen editores de recursos

Describes los recursos más o menos así:

Mymenu MENU
{
[menu list here]
}

Los programadores en lenguaje C pueden reconocer que es similar a la declaración de una


estructura. MyMenu sería un nombre de menú seguido por la palabra clave MENU y una lista
de menú entre llaves. Alternativamente, si quieres puedes usar BEGIN y END en vez de las
llaves. Esta sintaxis es mas portable para los programadores en Pascal.

La lista de menú puede ser un enunciado MENUITEM o POPUP.

El enunciado MENUITEM define una barra de menú que no invoca un menú emetgente [popup]
cuando es seleccionado. La sintaxis es como sigue:
MENUITEM "&text", ID [,options]

Se comienza por la palabra clave MENUITEM seguida por el texto que quieres usar como
cadena de texto de la barra de menú. Nota el ampersand (&). Hace que el carácter que le sigue
sea subrayado.

Después de la cadena de texto está el ID del elemento de menú. El ID es un número que será
usado para identificar el elemento de menú respectivo en el mensaje enviado al procedimiento
de ventana cuando el elemento de menú es selccionado. Como tal, cada ID de menú debe ser
único entre ellos.

Las opciones son 'opcionales'. Las disponibles son:

• GRAYED El elemento del menú está inactivo, y no genera un mensaje


WM_COMMAND. El texto está difuminada [grayed].
• INACTIVE El elemento del menú está inactivo, y no genera un mensaje
WM_COMMAND. El texto es desplegado normalmente.
• MENUBREAK Este elemento y los siguientes aparecen en una línea nueva del menú.
• HELP Este elemento y los siguientes están justificados a la derecha.

Puedes usar una de las opciones de arriba o combinarlas con el operador "or". Sólo recuerda
que INACTIVE y GRAYED no pueden ser combinados simultáneamente.

El enunciado POPUP tiene la siguiente sintaxis:

POPUP "&text" [,options]


{
[menu list]
}

El enunciado POPUP define una barra de menú que, cuando es seleccionada, despliega una
lista de elementos de menú en una ventana emergente. La lista de menú puede ser una
enunciado MENUTIEM o POPUP. Hay un tipo especial de enunciado MENUITEM, MENUITEM
SEPARATOR, que dibuja una linea horizontal en la ventana emergente.

El paso siguiente, después de haber terminado con el guión de recursos, es hacer la referencia
a él en el programa.

Esto se puede hacer de dos maneras.

En el miembro lpszMenuName de la estructura WNDCLASSEX. Es decir, si tienes un menú


llamado "FirstMenu", puedes asignar este menú a tu ventana de la siguiente manera:

.DATA

MenuName db "FirstMenu",0

...........................
...........................
.CODE
...........................
mov wc.lpszMenuName, OFFSET MenuName
...........................

• En el parámetro del manejador del menú de CreateWindowEx más o menos así:



• .DATA

MenuName db "FirstMenu",0
hMenu HMENU ?

...........................
...........................
.CODE

...........................
invoke LoadMenu, hInst, OFFSET MenuName
mov hMenu, eax
invoke CreateWindowEx,NULL,OFFSET ClsName,\
OFFSET Caption, WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,\
NULL,\
hMenu,\
hInst,\
NULL\
...........................

Entonces podrías preguntarte, ¿cuál es la diferencia entre estos dos métodos?

Cuando haces referencia al menú en la estructura WNDCLASSEX , el menú llega a ser el


menú "por defecto" para la clase de ventana. Todas las ventanas de esa clase tendrán el
mismo menú.

Si quieres que cada ventana creada a partir de la misma clase tenga diferente menú, debes
elegir la segunda manera. En este caso, cualquier ventana que se le pase un manejador de
menú en su función CreateWindowEx tendrá un menú que "reemplazará" el menú por defecto
definido en la estructura WNDCLASSEX.

Ahora examinaremos como un menú notifica al procedimiento de ventana cuando el usuario


selecciona un elemento del menú.

Cuando el usuario selecciona un elemento del menú, el procedimiento de ventana recibirá un


mensaje WM_COMMAND. La palabra baja de wParam contendrá el ID del elemento del menú.

Ahora tienes suficiente información para usar el menú. Vamos a hacerlo.

Ejemplo:

El primer ejemplo muestra cómo crear y usar un menú especificando el nombre del menú en la
clase de ventana.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0 ; Nombre de nuestro menú en el archivo .RC
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.const
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName ; Poner aquí el nombre del Menú
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
*************************************************************************************************************
*************

Menu.rc
*************************************************************************************************************
*************

#define IDM_TEST 1
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4

FirstMenu MENU
{
POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}
MENUITEM "&Test", IDM_TEST
}

Análisis:

vamos a analizar primero el guón de recursos.

#define IDM_TEST 1 /* equal to IDM_TEST equ 1*/


#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4

Las líneas de arriba definen los IDs de menú usados por el guión de menú. Puedes asignar
cualquier valor al ID siempre que sea único en el menú.

FirstMenu MENU

Declarar tu menú con la palabra clave MENU.

POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}

Definir un menú emergente con cuatro elementos, donde el tercer elemento es un separador.

MENUITEM "&Test", IDM_TEST

Definir una barra de menú en el menú principal.

Ahora examinaremos el código fuente.

MenuName db "FirstMenu",0 ; Nombre del menú en el archivo de recursos.


Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0

MenuName es el nombre del menú en el archivo de recursos. Nota que puedes definir más de
un menú en el archivo de recursos así que debes especificar cual usar. Las restantes tres
líneas definen la cadena de texto a ser desplegada en las cajas de mensaje que son invocadas
cuando el elemento de menú apropiado es selecionado por el usuario.

IDM_TEST equ 1 ; Menu IDs


IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4
Definir IDs de elementos de menú para usar en el procedimiento de ventana. Estos valores
DEBEN ser idénticos a los definidos en el archivo de recursos.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF

En el procedimiento de ventana, procesamos mensajes WM_COMMAND. Cuando el usuario


seleccione un elemento de menú, el ID de ese eemento de menú es enviado al procedimiento
de ventana en la palabra baja de wParam junto con el mensaje WM_COMMAND. Así que
cuando almacenamos el valor de wParam en eax, comparamos el valor en ax con los IDs de
menú que definimos previamente y actuamos de acuerdo con ello. En los primeros tres casos,
cuando el usuario selecciona los elementos de menú Test, Say Hello, y Say GoodBye,
desplegamos una cadena de texto en la caja de mensaje.

Si el usuario selecciona el elemento de menú Exit, llamamos DestroyWindow con el manejador


de nuestra ventana como su parámetro que cerrará nuestra ventana.

Como puedes ver, especificar el nombre del menú en una clase de ventana es muy fácil y
directo. Sin embargo, también puedes usar un método alternativo para cargar un menú en tu
ventana. No mostraré aquí todo el código fuente. El archivo de recursos es el mismo en ambos
métodos. Hay algunos cambios menores en el archivo fuente que mostraré abajo.

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HMENU ? ; handle of our menu

Definir un tipo de variable HMENU para almacenar nuestro manejador de menú.

invoke LoadMenu, hInst, OFFSET MenuName


mov hMenu,eax
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
hInst,NULL

Antes de llamar a CreateWindowEx, llamamos a LoadMenu con el manejador de instancia del


programa y un puntero al manejador de nuestro menú. LoadMenu regresa el manejador de
nuestro menú en el archivo de recursos que se pasó a CreateWindowEx.

Tutorial 9: Controles de Ventanas


Hijas
En este tutorial exploraremos los controles de ventana hija que son unos dispositivos de
entrada y salida muy importantes dentro de nuestros programas.

Baja el ejemplo aquí.

Teoría:

Windows provee algunas clases de ventana predefinidas que podemos usar satisfactoriamente
dentro de nuestros programas. Muchas veces las usamos como componentes de una caja de
diálogo por lo que ellas usualmente son llamadas controles de ventanas hijas. Los controles de
ventanas hijas procesan sus propios mensajes de teclado y de ratón y notifican a las ventanas
padres cuando sus estados han cambiado. Ellos liberan al programador de enormes cargas,
así que deberías usarlas cada vez que sea posible. En este tutorial las pongo sobre una
ventana normal para demostrar cómo puedes crearlas y usarlas, pero en realidad deberías
ponerlas en una caja de diálogo.

Ejemplos de clases de ventanas predefinidas son el botón, la caja de lista [listbox], la caja de
chequeo [checkbox], botón de radio [radio button], edición [edit] etc.

Con el fin de usar el control de vemtana hija, debes crearla con CreateWindow o
CreateWindowEx. Nota que no tienes que registrar la clase de ventana puesto que Windows lo
hace por tí. El parámetro nombre de la clase DEBE ser el nombre de la clase predefinida. Es
decir, si quieres crear un botón, debes especificar "button" como nombre de la clase en
CreateWindowEx. Los otros parámetros que debes llenar son la agarradera o manejador
[handle] de la ventana padre y el ID del control. El ID del control debe ser único entre los
controles. El ID del control es el ID de ese control. Lo usas para diferenciar entre controles.

Después de que el control fue creado, enviará mensajes de notificación a la ventana padre
cuando su estado cambie. Normalmente, creas las ventanas hijas durante el mensaje
WM_CREATE de la ventana padre. La ventana hija envía mensajes WM_COMMAND a la
ventana padre con su ID de control en la palabra baja de wParam, el código de notificación en
la palabra alta de wParam, y su manejador de ventana en lParam. Cada conrtol de ventana hija
tiene su propio código de notificación, así que debes revisar la referencia de la API de Win32
para más información.

También la ventana padre puede enviar órdenes o comandos a la ventana hija, llamando a la
función SendMessage. Esta función envía el mensaje especificado acompañado de otrops
valores en wParam y lParam a la ventana especificada por el manejador de ventana. Es una
función extremadamente útil, ya que puede enviar mensajes a cualquier ventana que conozcas
su manejador.

Así que después de crear ventanas hijas, la ventana padre debe procesar los mensajes
WM_COMMAND para poder recibir códigos de notificación desde las ventanas hijas.

Ejemplo:

Crearemos una ventana que contenga un control de edición y un "pushbutton". Cuando pulses
el botón, un cuadro de mensaje aparecerá mostrando un texto que hayas escrito en el cuadro
de diálogo. Hay también un menú con 4 elementos:

1. Say Hello -- Pone una cadena de texto en la caja de edición


2. Clear Edit Box -- Limpia el contenido de la caja de edición
3. Get Text -- despliega una caja de mensajes con el texto en la caja de edición
4. Exit -- Cierra el programa.
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0
ButtonClassName db "button",0
ButtonText db "My First Button",0
EditClassName db "edit",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndButton HWND ?
hwndEdit HWND ?
buffer db 512 dup(?) ; buffer para almacenar el texto recuperado desde la ventana de
edición

.const
ButtonID equ 1 ; El ID del control botón [button]
EditID equ 2 ; El ID del control de edición [edit]
IDM_HELLO equ 1
IDM_CLEAR equ 2
IDM_GETTEXT equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \
ADDR AppName, WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT, CW_USEDEFAULT,\
300,200,NULL,NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
50,35,200,25,hWnd,8,hInstance,NULL
mov hwndEdit,eax
invoke SetFocus, hwndEdit
invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
75,70,140,25,hWnd,ButtonID,hInstance,NULL
mov hwndButton,eax
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

Analicemos el programa .

.ELSEIF uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
or ES_AUTOHSCROLL,\
50,35,200,25,hWnd,EditID,hInstance,NULL
mov hwndEdit,eax
invoke SetFocus, hwndEdit
invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
ADDR ButtonText,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
75,70,140,25,hWnd,ButtonID,hInstance,NULL
mov hwndButton,eax

Creamos los controles cuando procesamos el mensaje WM_CREATE. Llamamos a


CreateWindowEx con el estilo de ventana extra, WS_EX_CLIENTEDGE, el cual hace que la
ventana cliente se vea con un borde sobreado. El nombre de cada control es un nombre
predefinido, "edit" para el control de edición, "button" para el control botón. Luego
especificamos los estilos de ventanas hijas. Cada control tiene estilos extras además de los
estilos de ventana normal. Por ejemplo, los estilos de botón llevan el prefijo "BS_" para "button
style", los estilos del control de edición llevan "ES_" para "edit style". Tienes que revisar estos
estilos en la referenicia de la API de Win32. Nota que pones un ID de control en lugar del
manejador del menú. Esto no causa problemas ya que el control de una ventana hija no puede
tener menú.

Después de crear cada control, guardamos su manejador en una variable para su futuro uso.

Se llama a SetFocus para dar fooco de entrada a la caja de edición de manera que el usuario
pueda tipear texto dentro de ella immediatamente.

Ahora la parte realmente exitante. Todo control de ventana hija envía una notificación a su
ventana padre con WM_COMMAND.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0

Recuerda que también un menu envía mensajes WM_COMMAND para notificar a su ventana
sobre su estado. ¿Cómo puedes diferenciar si los mensajes WM_COMMAND son originados
desde un menú o desde un control? He aquí la respuesta:

Palabra baja de wParam Palabra alta de wParam lParam


Menu Menu ID 0 0
Control Control ID Código de notificación Manjeador de ventana hija

Como puedes ver, hay que chequear lParam. Si es cero, el mensaje WM_COMMAND actual es
de un menú.No puedes usar wParam para diferenciar entre un menú y un control porque el ID
del menú y el ID del control pueden ser idénticos y el código de notificación puede ser cero.

.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

Puedes poner una cadena de texto dentro de una caja de edición llamando a SetWindowText.
Limpias el contenido de la caja de edición llamando a SetWindowText con NULL.
SetWindowText es una función de la API de propósito general. Puedes usar SetWindowText
para cambiar el encabezamiento o título [caption] de una ventana o el texto sobre un botón.

Para obtener el texto en una caja de edición, usas GetWindowText.

.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF

El fragmento de código de arriba tiene que ver con la condición de si el usuario presiona el
botón. Primero, chequea la palabra baja de wParam para ver si el ID del control coincide con el
del botón. Si es así, chequea la palabra alta de wParam para ver si es el código de notificación
BN_CLICKED que se envía cuando el botón es pulsado.

La parte interesante es después que el código de notificación es BN_CLICKED. Queremos


obtener el texto de la caja de edición y desplegarlo en la caja de edición. Podemos duplicar el
código en la sección IDM_GETTEXT de arriba pero no tiene sentido. Si de alguna manera
podemos enviar un mensaje WM_COMMAND con el valor IDM_GETTEXT en la palabra baja
de wParam a nuestro procedimiento de ventana, podemos evitar duplicación de código y
simplificar nuestro programa. La función SendMessage es la respuesta. Esta función envía
cualquier mensaje a cualquier ventana con cualquieras wParam y lParam que desiemos. Así
que e nvez de duplicar código, llamamos a SendMessage con el manejador de ventana padre,
WM_COMMAND, IDM_GETTEXT, y 0. Esto tiene un efecto idéntico que seleccionar el
elemento "Get Text" de nuestro menú. El procedimiento de ventana no percibirá ninguna
diferencia entre los dos.

Deberías usar estas técnicas en la medida de lo posible para que tu código sea más
organizado.

Por último, no olvides poner la función TranslateMessage en el bucle de mensajes, puesto que,
como debes tipear algún texto en la caja de edición, tu programa debe traducir la entrada cruda
del teclado a texto que pueda ser leído. Si omites esta función, no serás capáz de editar nada
en la caja de edición.
Tutorial 10: Caja de Diálogo [Dialog Box] como
Ventana Principal
Ahora viene la parte realmente interesante sobre GUI, la caja de diálogo. En este tutorial (y en
el próximo), aprenderemos como podemos usar una caja de diálogo como programa principal.

Bajar el primer ejemplo aquí, el segundo ejemplo aquí.

Teoría:

Si juegas bastante con los ejemplos del tutorial anterior, encontrarás que no puedes cambiar el
foco de entrada de un control de ventana hija a otra con la tecla Tab. La única manera de
realizar eso es haciendo click sobre el control que deseas que gane el foco de entrada. Esta
situación es más bien incómoda. Otra cosa que deberías notar es que cambié el color del fondo
de la ventana padre a gris en vez de a blanco, como lo había hecho en los ejemplos previos.
Esto se hace así para que el color de la ventana hija pueda armonizar con el color del área
cliente del ventana padre. Hay otra manera de salvar este problema pero no es fácil. Tienes
que subclasificar todos los controles de ventana hija en tu ventana padre.

La razón de la existencia de tal inconveniente es que los controles de ventana hija están
originalmente diseñados para trabajar dentro de cajas de diálogo, no en una ventana normal.
Los colores por defecto de los controles de ventanas hijas, como los botones, es gris porque el
área cliente de la caja de diálogo normalmente es gris para que armonicen entre sí sin ninguna
intervensión por parte del programador.

Antes de entrar en detalles, deberíamos saber qué es una caja de diálogo. Una caja de diálogo
no es más que una ventana normal diseñada para trabajar con controles de ventanas hijas.
Windows también proporciona un administrador interno de cajas de diálogo ["dialog box
manager"] responsable por gran parte de la lógica del teclado tal como desplazamiento del foco
de entrada cuando el ususario presiona Tab, presionar el botón por defecto si la tecla Enter es
presionada, etc; así los programadores pueden ocuparse de tareas de más alto nivel. Las cajas
de diálogo son usadas primero como dispositivos de entrada/salida. Como tal, una caja de
diálogo puede ser considerada como una "caja negra" de entrada/salida lo que siginifica que no
tienes que saber cómo funciona internamente una caja de diálogo para usarla, sólo tienes que
saber cómo interactuar con ella. Es un principio de la programación orientada a objetos [object
oriented programming (OOP)] llamado encapsulación u ocultamiento de la información. Si la
caja negra es *perfectamente* diseñada , el usuario puede emplarla sin tener conocimiento de
cómo funciona. Lo único es que la caja negra debe ser perfecta, algo difícil de alcanzar en el
mundo real. La API de Win32 API también ha sido diseñada como una caja negra.

Bien, parece que nos hemos alejado de nuestro camino. Regresemos a nuestro tema. Las
cajas de diálogo han sido diseñadas para reducir la carga de trabajo del programador.
Normalmente si tienes que poner controles de ventanas hijas sobre una ventana normal, tienes
que subclasificarlas y escribir tú mismo la lógica del teclado. Pero si quieres ponerlas en una
caja de diálogo, Windows manejará la lógica por tí. Sólo tienes que saber cómo obtener la
entrada del usuario de la caja de diálogo o como enviar órdenes a ella.

Como el menú, una caja de diálogo se define como un recurso. Escribes un plantilla
describiendo las características de la caja de diálogo y sus controles y luego compilas el guión
de recursos con un compilador de recursos.

Nota que todos los recursos se encuentran en el mismo archivo de guión de recursos. Puedes
emplear cualquier editor de texto para escribir un guión de recursos, pero no lo recomiendo.
Deberías usar un editor de recursos para hacer la tarea visualmente ya que arreglar la
disposición de los controles en la caja de diálgo es una tarea dura de hacer manualmente. Hay
disponibles algunos excelentes editores de recursos. Muchos de las grandes suites de
compiladores incluyen sus propios editores de recursos. Puedes usar cualquiera para crear un
guión de recursos para tu programa y luego cortar las líneas irrelevantes tales como las
relacionadas con MFC.

Hay dos tipos principales de cajas de diálogo: modal y no-modal. Una caja de diálogo no-modal
te deja cambiar de foco hacia otra ventana. Un ejempo es el diálogo Find de MS Word. Hay dos
subtipos de caja de diálogo modal: modal de aplicación y modal de sistema. Una caja de
diálogo modal de aplicación no permite cambiar el foco a otra ventana en la misma aplicación
sino cambiar el foco de entrada a la ventana de OTRA aplicación. Una caja de diálogo modal
de sistema no te permite cambiar de foco hacia otra ventana hasta que respondas a la primera.

Una caja de diálogo no-modal se crea llamando a la función de la API CreateDialogParam. Una
caja de diálogo modal se crea llamando a DialogBoxParam. La única diferencia entre una caja
de diálogo de no-modal y una modal de sistema es el estilo DS_SYSMODAL. Si quieres incluir
el estilo DS_SYSMODAL en una plantilla de caja de diálogo, esa caja de diálogo será modal de
sistema.

Puedes comunicarte con cualquier control de ventana hija sobre una caja de diálogo usando la
función SendDlgItemMessage. Su sintaxis es:

SendDlgItemMessage proto hwndDlg:DWORD,\


idControl:DWORD,\
uMsg:DWORD,\
wParam:DWORD,\
lParam:DWORD

Esta llamada a la API es inmensamene útil para interactuar con un control de ventana hija. Por
ejemplo, si quieres obtener el texto de un control de edición, puedes hacer esto:

call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer

Con el fin de saber qué mensaje enviar, deberías consultar la referencia de la API de Win32.
Windows también provee algunas funciones específicas de la API para controles que permiten
obtener y poner datos en los controles rápidamente, por ejemplo, GetDlgItemText,
CheckDlgButton etc. Estas funciones específicas para controles son suministradas para
conveniencia de los programadores de manera que él no tenga que revisar el significado de
wParam y lParam para cada mensaje. Normalmente, deberías usar llamadas a las funciones
específicas de la API para controles cada vez que sean disponibles ya que ellas facilitan el
mantenimiento del código fuente. Recurre a SendDlgItemMessage sólo si no hay disponible
llamadas a funciones específicas de la API.

El manejador de Windows de cajas de diálogos envía varios mensajes a una función " callback"
particular llamada procedimiento de caja de diálogo que tiene el siguiente formato:

DlgProc proto hDlg:DWORD ,\


iMsg:DWORD ,\
wParam:DWORD ,\
lParam:DWORD

El procedimiento de diálogo es similar al procedimiento de ventana excepto por el tipo de valor


de retorno, que es TRUE/FALSE en vez de LRESULT. El administrador interno de la caja de
diálogo dentro de Windows ES el verdadero procedimiento de ventana para la caja de diálogo.
Llama a nuestra caja de diálogo con algunos mensajes que recibió. Así que la regla general es
que: si nuestro procedimiento de diálogo procesa un mensaje, DEBE regresar TRUE (1) en eax
y si no procesa el mensaje, debe regresar FALSE (0) en eax. Nota que un procedimiento de
caja de diálogo no pasa los mensajes no procesados a la llamada DefWindowProc ya que no
es realmente un procedimiento de ventana.
Hay dos usos distintos de una caja de diálogo. Puedes usarlas como ventanas principal de tu
aplicación o usarla como un dispositivo de entrada. Examinaremos el primer acercamiento en
este tutorial. "Usar una caja de diálogo como una ventana principal" puede ser interpretado en
dos sentidos.

1. Puedes usar una plantilla de caja de diálogo como la plantilla de clase que registras al
llamar a RegisterClassEx. En este caso, la caja de diálogo se comporta como una
ventana "normal": recibe mensajes del procedimiento de ventana referido por el
miembro lpfnWndProc de la clase de ventana class, no a través de un procedimiento
de caja de diálogo. El beneficio de esto es que no tienes que crear por tí mismo
controles de ventana hija, Windows los crea por tí cuando se crea la caja de diálogo.
También Windows maneja la lógica del teclado para tí, por ejemplo se encarga de la
orden Tab, etc. Además puedes especificar el cursor y el icono de tu ventana en la
estructura de la clase de ventana.
2.
Tu programa crea la caja de diálogo sin ninguna ventana padre. Esta aproximación al
problema hace inecesario el uso de un bucle de mensajes ya que los mensajes son
enviados directamente al procedimiento de ventana de la caja de diálogo. ¡Ya no tienes
que registrar la clase de ventana!

Este tutorial va a ser un poco largo. Presentaré la primera aproximación seguida por la
segunda.

Ejemplos:

dialog.asm

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "DLGCLASS",0
MenuName db "MyMenu",0
DlgName db "MyDialog",0
AppName db "Our First Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hDlg:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,DLGWINDOWEXTRA
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
mov hDlg,eax
invoke ShowWindow, hDlg,SW_SHOWNORMAL
invoke UpdateWindow, hDlg
invoke GetDlgItem,hDlg,IDC_EDIT
invoke SetFocus,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.IF dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Dialog.rc

#include "resource.h"

#define IDC_EDIT 3000


#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDM_GETTEXT 32000


#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60


STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our First Dialog Box"
CLASS "DLGCLASS"
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13, WS_GROUP
END

MyMenu MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

Análisis:

Vamos a analizar el primer ejemplo.

Este ejemplo muestra como registrar una plantilla de diálogo como una clase de ventana y
crear una "ventana" a partir de esa clase. Simplifica tu programa ya que no tienes que crear tú
mismo los controles de ventana hija. Vamos a analizar primero la plantilla de diálogo.

MyDialog DIALOG 10, 10, 205, 60

Declarar el nombre del diálogo, en este caso, "MyDialog" seguido por la palabra clave
"DIALOG". Los siguientes cuatro números son: x, y , ancho, y alto de la caja de diálogo en
unidades de caja de diálogo no de pixeles [not the same as pixels].

STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |


WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK

Declarar los estilos de la caja de diálogo.

CAPTION "Our First Dialog Box"

Este es el texto que aparecerá en la barra de título de la caja de diálogo.

CLASS "DLGCLASS"

Esta línea es crucial. Es esta palabra clave, CLASS, lo que nos permite usar la caja de diálogo
como una clase de ventana. Después de la palabra clave está el "nombre de la clase"

BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

El bloque de arriba define los controles de ventana hija en la caja de diálogo. Están definidos
entre las palabras claves BEGIN y END. Generalmente la sintaxis es la siguiente:

control-type "text" ,controlID, x, y, width, height [,styles]

los tipos de controles son constantes del compilador de recursos así que tienes que consultar el
manual.

Ahora vamos a ensamblar el código fuente. La parte interesante es la estructura de la clase de


ventana:

mov wc.cbWndExtra,DLGWINDOWEXTRA
mov wc.lpszClassName,OFFSET ClassName

Normalmente, este miembro se deja NULL, pero si queremos registrar una plantilla de caja de
diálogo como una clase de ventana, debemos poner en este miembro el valor
DLGWINDOWEXTRA. Nota que el nombre de la clase debe ser idéntico al que sigue a la
palabra clave CLASS en la plantilla de la caja de diálogo. Los miembros restantes se inicializan
como es usual. Después de que llenas la estructura de la clase de ventana, la registras con
RegisterClassEx. ¿No te resulta esto familiar? Esta es la misma rutina que tienes que hacer
con el fin de registrar una clase de ventana normal.

invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL

Después de registrar la "clase de ventana", creamos nuestra caja de diálogo. En este ejemplo,
lo creo como una caja de diálogo modal con la función CreateDialogParam. Esta función toma
5 parámetros, pero sólo tienes que llenar los primeros dos: el manejador de instancia y el
puntero al nombre de la plantila de la caja de diálogo. Nota que el segundo parámetro no es un
puntero al nombre de la clase.

En este punto, la caja de diálogo y sus controles de ventana hija son creados por Windows. Tu
procedimiento de ventana recibirá el mensaje WM_CREATE como es usual.

invoke GetDlgItem,hDlg,IDC_EDIT
invoke SetFocus,eax

Después de que es creada la caja de diálogo, quiero poner el foco de entrada a la caja de
edición. Si pongo estas instrucciones en la sección WM_CREATE, la llamada a GetDlgItem
fallará ya que en ese momento, ya que en ese instante todavía no se han creado los controles
de ventana hija. La única manera de hacer esto es llamarlo después de que la caja de diálogo y
todos los controles de ventana hija son creados. Así que pongo estas dos lineas después de la
llamada a UpdateWindow. La función GetDlgItem obtiene el ID del control y regresa el
manejador del control de ventana asociado. Así es como puedes obtener el manejador de un
control de ventana hija si tienes su ID.

invoke IsDialogMessage, hDlg, ADDR msg


.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF

El programa introduce el bucle de mensajes y antes de que traduzcamos y despachemos


mensajes, llamamos a la función IsDialogMessage para permitir que el administrador [manager]
de la caja de diálogo maneje el teclado lógico de nuestra caja de diálogo. Si la función regresa
TRUE, significa que el mensaje es enviado a la caja de diálogo y es procesado por el
administrador de la caja de diálogo. Nota ahora la diferencia respecto al tutorial previo. Cuando
el procedimiento de ventana quiere obtener el texto del control de edición, llama a la fución
GetDlgItemText en vez de GetWindowText. GetDlgItemText acepta un ID de control en vez de
un manejador de ventana. Eso facilita la llamada en el caso de que utilices una caja de diálogo.

Ahora vamos a la segunda aproximación de cómo usar una caja de diálogo como ventana
principal. En el siguiente ejemplo, crearé una aplicación de caja de diálogo modal. ¡No
encontrarás un bucle de mensaje ni un procedimiento de ventana porque no son necesarios!

dialog.asm (part 2)

.386
.model flat,stdcall
option casemap:none

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
DlgName db "MyDialog",0
AppName db "Our Second Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
invoke ExitProcess,eax

DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSEIF ax==IDM_EXIT
invoke EndDialog, hWnd,NULL
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.if dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
mov eax,FALSE
ret
.ENDIF
mov eax,TRUE
ret
DlgProc endp
end start

dialog.rc (part 2)

#include "resource.h"

#define IDC_EDIT 3000


#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDR_MENU1 3003

#define IDM_GETTEXT 32000


#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60


STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our Second Dialog Box"
MENU IDR_MENU1
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

IDR_MENU1 MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END
A continuación el análisis:

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

Declaramos el prototipo de función para DlgProc de manera que podamos referirnos a él con el
operador addr en la línea de abajo:

invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL

la línea de arriba llama a la función DialogBoxParam que toma 5 parámetros: el manejador de


instancia, el nombre de la plantilla de la caja de diálogo, el manejador de la ventana padre, la
dirección del procedimiento de de ventana, y los datos específicos del diálogo.
DialogBoxParam crea una caja de diálogo modal. No regresará hasta que la caja de diálogo
sea destruida.

.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0

El procedimiento de la caja de diálogo se observa como un procedimiento de ventana excepto


en que no recibe el mensaje WM_CREATE. El primer mensaje que recibe es
WM_INITDIALOG. Normalmente puedes poner aquí el código de inicialización. Nota que debes
regresar el valor TRUE en eax si procesas el mensaje.

El administrador interno de la caja de diálogo no envía a nuestro procedimiento de diálogo el


mensaje WM_DESTROY por defecto cuando WM_CLOSE es enviado a nuestra caja de
diálogo. Así que si queremos reaccionar cuando el usuario presiona el botón cerrar [close] en
nuestra caja de diálogo, debemos procesar el mensaje WM_CLOSE. En nuestro ejemplo,
enviamos el mensaje WM_COMMAND con el valor IDM_EXIT en wParam. Esto tiene el mismo
efecto que cuando el usuario selecciona el elemento del menú Exit. EndDialog es llamado en
respuesta a IDM_EXIT.

El procesamiento de WM_COMMAND se mantiene igual.

Cuando quieres destruir la caja de diálogo, la única manera es llamar a la función EndDialog.
¡No emplees DestroyWindow! EndDialog no destruye la caja de diálogo de inmediato. Sólo
pone una bandera para ser revisada por el administrador interno de la caja de diálogo y
continúa para ejecutar la siguiente instrucción.

Ahora vamos a revisar el archivo de recursos. El cambio notable es que en vez de usar una
cadena de texto como nombre de menú usamos un valor, IDR_MENU1. Esto es necesario si
quieres agregar un menú a la caja de diálog creada con DialogBoxParam. Nota que en la
plantilla de caja de diálogo tienes que agregar la palabra clave MENU seguida por el ID del
recurso.

Una diferencia que puedes observar entre los dos ejemplos de este tutorial es la carencia de un
icono en el último ejemplo. Sin embargo, puedes poner el icono enviando el mensaje
WM_SETICON a la caja de diálgo durante WM_INITDIALOG.

Tutorial 11: Mas sobre las Caja de


Diálogo [Dialog Box]
En este tutorial vamos a aprender mas sobre las cajas de diálogo [dialog box].
Especificamente, vamos a explorar la manera de como usar cajas de diálogo como nuestra
entrada-salida de datos. Si leíste el tutorial anterior, este te va a resultar ligero. Encontrarás
algunos cambios menores, es todo lo que necesitamos para poder usar cajas de diálogo
adjuntas a nuestra ventana principal. En este tutorial también aprenderemos como usar cajas
de diálogo comunes.

Bájate los ejemplos de cajas de diálogo aquí y aquí. Bajate el ejemplo de una cajas de diálogo
común aquí.

Teoría:

Hay muy poco que decir sobre como usar las cajas de diálogo como entrada-salida de nuestro
programa. Tu programa crea la página principal normalmente y cuando quieres mostrar la cajas
de diálogo, llamas a CreateDialogParam o DialogBoxParam. Con la llamada a
DialogBoxParam, no tendrás que hacer nada mas, sólo procesar los mensajes en el
procedimiento de la cajas de diálogo. Con CreateDialogParam, tendrás que insertar la llamada
a IsDialogMessage en el bucle de mensajes para dejar a la cajas de diálogo el control sobre la
navegación del teclado en tu cajas de diálogo. Como los dos casos son diferentes, no pondré el
codigo fuente aquí. Puedes bajarte los ejemplos y examinarlos tu mismo, aquí y aquí.

Comencemos con las cajas de diálogo comunes. Windows tiene preperadas unas cajas de
diálogo predefinidas que pueden ser usadas por tus aplicaciones. Estas cajas de diálogo
existen para proveer un interfaz estandard de usuario. Consisten en cajas de diálogo de
archivo, impresión, color, fuente, y busqueda. Deberías usarlas lo máximo posible. Las cajas de
diálogo residen en comdlg32.dll. Para usarlas, tendrás que enlazar [link] el archivo
comdlg32.lib. Creas estas cajas de diálogo llamando a la función apropiada en la librería de las
cajas de diálogo. Para el archivo de diálogo "Abrir" [Open], se emplea la función
GetOpenFileName, para la caja de diálgo "Guardar" [Save] GetSaveFileName, para dibujar un
diálogo es PrintDlg y ya está. Cada una de estas funciones toma como parámetro un puntero a
la estructura. Deberás mirarlo en la referencia de la API de Win32. En este tutorial, demostraré
como crear y usar un diálogo "Abrir archivo" [Open file].

Debajo está la el prototipo de la función GetOpenFileName:

GetOpenFileName proto lpofn:DWORD

Puedes ver que sólo recibe un parámetro, un puntero a la estructura OPENFILENAME. El valor
devuelto es TRUE que significa que el usuario a seleccionado un archivo para abrir, de otra
manera devolverá FALSE. Lo siguiente que veremos será la estructura OPENFILENAME.

OPENFILENAME STRUCT

lStructSize DWORD ?
hwndOwner HWND ?
hInstance HINSTANCE ?
lpstrFilter LPCSTR ?
lpstrCustomFilter LPSTR ?
nMaxCustFilter DWORD ?
nFilterIndex DWORD ?
lpstrFile LPSTR ?
nMaxFile DWORD ?
lpstrFileTitle LPSTR ?
nMaxFileTitle DWORD ?
lpstrInitialDir LPCSTR ?
lpstrTitle LPCSTR ?
Flags DWORD ?
nFileOffset WORD ?
nFileExtension WORD ?
lpstrDefExt LPCSTR ?
lCustData LPARAM ?
lpfnHook DWORD ?
lpTemplateName LPCSTR ?

OPENFILENAME ENDS

Vamos a ver el significado de los miembros mas utilizados de la estructura.

lStructSize El tamaño de la estructura OPENFILENAME, en bytes


hwndOwner El manejador (handle) de la caja de diálogo "open file".
Manejador (handle) de la instancia de la aplicación que crea la caja de diálogo
hInstance
"open file".
Las cadenas de filtro en formato de pares de cadenas terninadas en null (0). La
primera entre las dos es la de descripción. La segunda cadena es el patrón de
filtro. por ejemplo:
FilterString db "All Files (*.*)",0, "*.*",0
lpstrFilter db "Text Files (*.txt)",0,"*.txt",0,0
Date cuenta que sólo el patrón en la segunda cadena de este par es usado
actualmente por Windows para filtrar los archivos. Tambien date cuenta de que
tienes que poner un 0 extra al final de la cadena de filtro para advertir el final de
ésta.
Especifica que par de cadenas de filtro que serán usadas inicialmente cuando la
caja de diálogo "open file" es mostrada por primera vez. El índice es de base 1,
nFilterIndex
que es el primer par 1, el segundo par es 2 y así el resto. En el ejemplo de arriba,
si especificamos nFilterIndex como 2, será usado el segundo patrón, "*.txt".
Puntero al buffer que contiene el nombre del archivo usado para inicializar el
control de edición de nombre de archivo en la caja de diálogo. El buffer será
lpstrFile como mínimo de 260 bytes de largo. Después de que el usuario seleccione el
archivo a abrir, el nombre de archivo con toda la dirección (path) es almacenado
en este buffer. Puedes extraer la información de aquí mas tarde.
nMaxFile El tamaño del buffer lpstrFile.
lpstrTitle Puntero al encabezamiento o título de la caja de diálogo "open file"
Flags Determina el estilo y características de la caja de diálogo.
después de que el usuario haya seleccionado el archivo a abrir, este miembro
contiene el índice al primer caracter del nombre de archivo actual. Por ejemplo, si
nFileOffset
el nombre completo con el directorio (path) es "c:\windows\system\lz32.dll", este
miembro contendrá el valor 18.
después de que el usuario seleccione el archivo a abrir, este miembro contiene el
nFileExtension
índice al primer caracter de la extensión del archivo

Ejemplo:

El siguiente programa muestra una caja de diálogo de abrir archivo cuando el usuario
seleccione File-> Open del menu. Cuando el usuario seleccione un archivo en la caja de
diálogo, el programa muestra una caja de mensaje mostrando el nombre completo, nombre de
archivo, y extensión del archivo seleccionado.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_EXIT equ 2
MAXSIZE equ 260
OUTPUTSIZE equ 512

.data
ClassName db "SimpleWinClass",0
AppName db "Our Main Window",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0
FullPathName db "The Full Filename with Path is: ",0
FullName db "The Filename is: ",0
ExtensionName db "The Extension is: ",0
OutputString db OUTPUTSIZE dup(0)
CrLf db 0Dh,0Ah,0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if ax==IDM_OPEN
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
mov ofn.lpstrTitle, OFFSET OurTitle
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset ExtensionName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileExtension
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE
.endif
.else
invoke DestroyWindow, hWnd
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Análisis:

mov ofn.lStructSize,SIZEOF ofn


push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance

Rellenamos en la rutina los miembros de la estructura ofn.

mov ofn.lpstrFilter, OFFSET FilterString

Este FilterString es el filtro para el nombre de archivo que especificamos como sigue:

FilterString db "All Files",0,"*.*",0


db "Text Files",0,"*.txt",0,0

Date cuenta que las cuatro cadenas terminan en 0. La primera cadena es la descripción de la
siguiente cadena. El actual patrón es la cadena par, en este caso, "*.*" y "*.txt". Actualmente
podemos especificar aquí cualquier patrón que queramos. DEBEMOS poner un cero extra
después de la última cadena de patrón para denotar el final de la cadena de filtro. No olvides
esto sino tu caja de diálogo funcionará de forma extraña.

mov ofn.lpstrFile, OFFSET buffer


mov ofn.nMaxFile,MAXSIZE

Especificamos dónde la caja de diálogo pondrá el nombre del archivo que ha seleccionado el
usuario. Date cuenta que tendremos que especificar su tamaño en el miembro nMaxFile.
Podemos extraer más tarde el nombre del archivo de este buffer.

mov ofn.Flags, OFN_FILEMUSTEXIST or \


OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

Flags especifica las características de la caja de diálogo.


Las banderas [flags] OFN_FILEMUSTEXIST y OFN_PATHMUSTEXIST demandan que el
nombre de archivo y dirección (path) que el usuario pone en el control de edición (edit control)
DEBEN existir.
La bandera OFN_LONGNAMES dice a la caja de diálogo que muestre nombres largos de
archivo.
La bandera OFN_EXPLORER especifica que la apariencia de la caja de diálogo debe ser
parecida a la del explorador.
La bandera OFN_HIDEREADONLY oculta el cuadro de selección de solo-lectura en la caja de
diálogo.
Hay muchas banderas más que puedes usar. Consulta tu referencia de la API de Win32.

mov ofn.lpstrTitle, OFFSET OurTitle

Especifica el titulo o encabezado [caption] de la caja de diálogo.

invoke GetOpenFileName, ADDR ofn

Llamada a la función GetOpenFileName. Pasando el puntero a la estructura ofn como


parámetro.
En este momento, la caja de diálogo de abrir archivo es mostrada en la pantalla. La función no
volverá hasta que el usuario seleccione un archivo para abrir o presione el boton de cancelar o
cierre la caja de diálogo. Devolverá el valor TRUE en eax si el usuario selecciona un archivo
para abrir. Sino devolverá FALSE en cualquier otro caso.

.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName

En el caso que el usuario seleccione un archivo para abrir, preparamos la cadena de salida que
se mostrará en la caja de mensajes. Ubicamos un bloque de memoria en la variable
OutputString y entonces usamos la función de la API, lstrcat, para entrelazar las cadenas
siempre. Para poner las cadenas en varias líneas, debemos separar cada línea con el par de
caracteres que alimentan el retorno de carro (13d o 0Dh) y el avance de línea (10d o 0Ah).

mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax

Las líneas de arriba requieren una explicación. nFileOffset contiene el índice dentro de
ofn.lpstrFile. Pero no puedes añadirlo directamente porque nFileOffset es una variable de
tamaño WORD y lpstrFile es de tamaño DWORD. Así que tendré que poner el valor de
nFileOffset en la palabra baja [low word] de ebx y sumárselo al valor de lpstrFile.

invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK

Mostramos la cadena en la caja de mensajes.

invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE

Debemos *limpiar* el OutputString antes de poder meterle cualquier otra cadena. Así que
usamos la función RtlZeroMemory para hacer este trabajo.
Tutorial 12: Manejo de Memoria y E/S de Archivos
En este tutorial aprenderemos a manejar la memoria rudimentariamente y las operaciones de
entrada/salida de archivos. Adicionalmente usaremos cajas de diálogo comunes como
instrumento de entrada-salida.

Bája el ejemplo aquí.

Teoria:

El manejo de la memoria bajo Win32 desde el punto de vista de las aplicaciones es un poco
simple y directo. Cada proceso usa un espacio de 4 GB de dirrecciones de memoria. El modelo
de memoria usado se llama modelo de memoria plana [flat]. En este modelo, todos los
segmentos de registro (o selectores) direccionan a la misma localidad de memoria y el
desplazamiento [offset] es de 32-bit. Tambien las aplicaciones pueden acceder a la memoria en
cualquier punto en su espacio de direcciones sin necesidad de cambiar el valor de los
selectores. Esto simplifica mucho el manejo de la memoria. No hay mas puntos "near" (cerca) o
"far" (lejos).

Bajo Win16, hay dos categorías principales de funciones de la API de memoria: Global y Local.
Las de tipo Global tienen que ver con la memoria situada en diferentes segmentos, por eso hay
funciones para memoria "far" (lejana). Lasfunciones de la API de tipo Local tienen que ver con
un motículo [heap] de memoria local del proceso así que son las funciones de memoria "near"
(cercana). Bajo Win32, estos dos tipos son uno y le mismo tipo. tendrás el mismo resultado si
llamas a GlobalAlloc o LocalAlloc.

Los pasos para ubicar y usar la memoria son los siguientes:

1. Ubicar el bloque de memoria llamando a GlobalAlloc. Esta función devuelve un


manejador (handle) al bloque de memoria pedido.
2. "Bloquear" [lock] el bloque de memoria llamando a GlobalLock. Esta función acepta un
manejador (handle) al bloque de memoria y devuelve un puntero al bloque de memoria.
3. Puedes usar el puntero para leer o escribir en la memoria.
4. Desbloquear [unlock] el bloque de memoria llamando a GlobalUnlock . Esta función
invalida el puntero al bloque de memoria.
5. Liberar el bloque de memoria llamando a GlobalFree. Esta función acepta un
manejador (handle) al bloque de memoria.

Puedes sustituir "Global" por "Local" en LocalAlloc, LocalLock,etc.

El método de arriba puede simplificarse radicalmente usando el flag GMEM_FIXED en la


llamada a GlobalAlloc. Si usas esta bandera [flag], El valor de retorno de Global/LocalAlloc será
el puntero al bloque de memoria reservado, no el manejador (handle). No tienes que llamar a
Global/LocalLock y puedes pasar el puntero a Global/LocalFree sin llamar primero a
Global/LocalUnlock. Pero en este tutorial, usaré el modo "tradicional" ya que te lo puedes
encontrar cuando leas el código de otros programas.

La E/S de archivos bajo Win32 tiene una semblanza más notable que la de bajo DOS. Los
pasos a seguir son los mismos. Sólo tienes que cambiar las interrupciones por llamadas a la
API y ya está. Los pasos requeridos son los siguientes:

1. Abrir o Crear el archivo llamando a la función CreateFile. Esta función es muy versátil:
añadiendo a los archivos, puede abrir puertos de comunicación, tuberías [pipes],
dipositivos de discos. Cuando es correcto, devuelve un manejador (handle) al archivo o
dispositivo. Entonces puedes usar este manejador (handle) para llevar a cabo
operaciones en el archivo o dispositivo.
2.
Mueve el puntero a la posición deseada llamando a SetFilePointer.
3. Realiza la operación de lectura o escritura llamando a ReadFile o WriteFile. Estas
funciones transfieren datos desde un bloque de memoria hacia o desde un archivo. Así
que tendrás que reservar un bloque de memoria suficientemente grande para alojar los
datos.
4. Cierra el archivo llamando a CloseHandle. Esta función acepta el manejador (handle)
de archivo.

Contenido:

El programa de abajo muestra una caja de diálogo de abrir archivo. Deja al usuario seleccionar
un archivo de texto y muestra el contenido de este archivo en un control de edición en su área
cliente. El usuario puede modificar el texto en el control de edición como desee, y puede elegir
guardar el contenido en un archivo.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260
MEMSIZE equ 65535

EditID equ 1 ; ID del control de edición

.data
ClassName db "Win32ASMEditClass",0
AppName db "Win32 ASM Edit",0
EditClass db "edit",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndEdit HWND ? ; manejador (handle) del control de edición
hFile HANDLE ? ; manejador de archivo
hMemory HANDLE ? ; manejador del bloque de memoria reservada
pMemory DWORD ? ; puntero al bloque de memoria reservada
SizeReadWrite DWORD ? ; numero de bytes actualmente para leer o escribir
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,hwndEdit
;========================================================
; Inicializacion de los miembros de la estructura OPENFILENAME
;========================================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Análisis:

invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\


WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax

En la sección WM_CREATE, creamos el control de edición. Date cuenta que los parámetros
que especifican x, y, anchura, altura del control son todos ceros ya que reajustaremos el
tamaño del control despues para cubrir toda el area cliente de la ventana padre.

En este caso, no tenemos que llamar a ShowWindow para hacer que el control de edición
aparezca en la pantalla porque hemos incluido el estilo WS_VISIBLE. Puedes usar este truco
también en la ventana padre.

;==================================================
; Inicializa los miembros de la estructura OPENFILENAME
;==================================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

Despues de crear el control de edición, tendremos que inicializar los miembros de ofn. Como
queremos reciclar ofn en la caja de diálogo para guardar, pondremos solo los miembros
*comunes* que son usados por ambos GetOpenFileName y GetSaveFileName.

La sección WM_CREATE es un lugar amplio para poner las incializaciones únicas (que se
inicializan una sola vez).

.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
Recibimos el mensaje WM_SIZE cuando el tamaño de nuestro área cliente en la ventana
principal cambia. Tambien lo recibimos cuando la ventana es creada por primera vez. Para
poder recibir el mensaje, el estilo de ventana debe incluir los estilos CS_VREDRAW y
CS_HREDRAW. Usamos esta oportunidad para reajustar el tamaño de nuestro control de
edición al mismo tamaño de nuestro área cliente de la ventana padre. Primero tenemos que
saber la anchura y altura del área cliente de la ventana padre. Obtenemos esta información de
lParam. La palabra alta [high word] de lParam contiene la altura y la palabra baja [low word] de
lParam la anchura del area cliente. Entonces usamos la información para reajustar el tamaño
del control de edición llamando a la función MoveWindow, que además de cambiar la posición
de la ventana, permite cambiar su tamaño.

.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn

Cuando el usuario selecciona el elemento de menú File/Open, rellenamos el miembro Flags de


la estructura ofn y llamamos a la función GetOpenFileName para mostrar la caja de diálogo de
abrir archivo.

.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax

Después que el usuario ha seleccionado el archivo que desea abrir, llamamos a CreateFile
para abrir el archivo. Hemos especificado que la función intentará abrir el archivo para lectura y
escritura. Después de abrir el archivo, la función devuelve el manejador (handle) al archivo
abierto que almacenamos en una variable global para futuros usos. Esta función es como
sigue:

CreateFile proto lpFileName:DWORD,\


dwDesiredAccess:DWORD,\
dwShareMode:DWORD,\
lpSecurityAttributes:DWORD,\
dwCreationDistribution:DWORD\,
dwFlagsAndAttributes:DWORD\,
hTemplateFile:DWORD

dwDesiredAccess especifica qué operación quieres que se haga en el archivo.

• 0 Abrir el archivo para pedir sus atributos. Tendrás derecho a escribir o leer los datos.
• GENERIC_READ Abrir el archivo para lectura.
• GENERIC_WRITE Abrir el archivo para escribir.

dwShareMode especifica qué operaciones quieres reservar para que otros procesos puedan
llevarlas a cabo en el archivo que va a ser abierto.

• 0 No compartir el archivo con otros procesos.


• FILE_SHARE_READ permitir a otros procesos leer los datos del archivo abierto
• FILE_SHARE_WRITE permitir a otros procesos escribir datos en el archivo abierto.
lpSecurityAttributes no tiene significado bajo Windows 95.
dwCreationDistribution especifica la acción a ejecutar por CeateFile cuando el archivo
especificado en lpFileName existe o cuando no existe.

• CREATE_NEW Crear un nuevo archivo. La función falla si ya existe el archivo


especificado.
• CREATE_ALWAYS Crea un nuevo archivo. La función sobreescribe el archivo si éste
existe.
• OPEN_EXISTING Abre el archivo. La función falla si el archivo no existe.
• OPEN_ALWAYS Abre el archivo si existe. Si el archivo no existe, la función crea el
archivo como si dwCreationDistribution fuera CREATE_NEW.
• TRUNCATE_EXISTING Abre el archivo. Una vez abierto, el archivo es truncado si su
tamaño es de cero bytes. El proceso llamado debe abrir el archivo como mínimo con el
acceso GENERIC_WRITE. La función falla si el archivo no existe.

dwFlagsAndAttributes especifica los atributos de archivo

• FILE_ATTRIBUTE_ARCHIVE El archivo es del tipo archivo. Las aplicaciones usan


este atributo para marcar las copias de seguridad [backup] o los removibles.
• FILE_ATTRIBUTE_COMPRESSED El archivo o directorio está coprimido. Para el
archivo esto significa que todos los datos del archivo están comprimidos. Para un
directorio esto significa que la compresión es por defecto aplicada a los archivos y
subdirectorios nuevos creados.
• FILE_ATTRIBUTE_NORMAL El archivo no tiene otros atributos activos. Este atributo
es válido sólo si esta solo, no hay ningún otro atributo.
• FILE_ATTRIBUTE_HIDDEN El archivo es oculto. El archivo no es incluido en la lista
ordinaria de directorios.
• FILE_ATTRIBUTE_READONLY El archivo es de solo lectura. Las aplicaciones pueden
leer el archivo pero no pueden ni escribirlo ni borrarlo.
• FILE_ATTRIBUTE_SYSTEM El archivo es parte del sistema operativo o es usado por
este exclusivamente.

invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE


mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax

Cuando se abre el archivo, reservamos un bloque de memoria para usar con las funciones
ReadFile y WriteFile. Especificamos el flag GMEM_MOVEABLE para dejar a Windows mover el
bloque de memoria para consolidar la memoria. La bandera [flag] GMEM_ZEROINIT le dice a
GlobalAlloc que rellene el nuevo bloque de memoria reservado con ceros.

Cuando GlobalAlloc vuelve satisfactoriamente, eax contiene el manejador (handle) al bloque de


memoria reservado. Le pasamos este manejador (handle) a la función GlobalLock que nos
devuelve un puntero al bloque de memoria.

invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL


invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory

Cuando el bloque de memoria esta listo para ser usado, llamamos a la función ReadFile para
leer los datos del archivo. Cuando el archivo es abierto o creado por primera vez, el puntero del
archivo esta en el deplazamiento [offset] 0. Así que en este caso, empezamos a leer el primer
byte del archivo. El primer parámetro de ReadFile es el manejador (handle) del archivo a leer,
el segundo es el puntero al bloque de memoria para contener los datos, el siguiente es el
numero de bytes a leer del archivo, el cuarto parámetro es la direccion de la variable de tamaño
DWORD que rellenaremos con el número de bytes realmente leídos del archivo.
Despues de rellenar el bloque de memoria con los datos, ponemos los datos en el control de
edición mandando el mensaje WM_SETTEXT al control de edición con lParam conteniendo el
puntero al bloque de memoria. Despues de esta llamada, el control de edición muestra los
datos en el área cliente.

invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif

En este punto, no necesitamos tener el archivo abierto por más tiempo ya que nuestra intención
es grabar los datos modificados en el control de edición en otro archivo, no el archivo original.
Asi que cerramos el archivo llamando a CloseHandle con el manejador (handle) como su
parámetro. Seguido desbloquearemos el bloque de memoria y lo liberamos. Actualmente no
tienes que liberar la memoria en este punto, Puedes usar el bloque de memoria durante el
proceso de grabación después. Pero como demostración, yo he elegido liberarla aquí.

invoke SetFocus,hwndEdit

Cuando la caja de diálogo "abrir archivo" es mostrada en la pantalla, el foco de entrada se


centra en ella. Así que después de cerrar el diálogo de "abrir archivo", tendremos que mover el
foco de entrada otra vez al control de edición.

Esto termina la operacion de lectura de archivo. En este punto, el usuario puede editar el
contenido del control de edición. Y cuando quiera salvar los datos a otro archivo, deberá
seleccionar File/Save en el menú que mostrará la caja de diálogo de salvar archivo. La creación
de la caja de diálogo de salvar archivo no es muy diferente de la de abrir archivo.
Efectivamente, se diferencian sólo en el nombre de la función, GetOpenFileName y
GetSaveFileName. Puedes reciclar la mayoría de los miembros de la estructura ofn excepto el
miembro Flags.

mov ofn.Flags,OFN_LONGNAMES or\


OFN_EXPLORER or OFN_HIDEREADONLY

En nuestro caso, queremos crear un nuevo archivo, así que OFN_FILEMUSTEXIST y


OFN_PATHMUSTEXIST deben ser dejados fuera, sino la caja de diálogo no nos dejara crear
un archivo que no exista ya.

El parámetro dwCreationDistribution de la función CreateFile deberá ser cambiada a


CREATE_NEW ya que queremos crear un nuevo archivo.

El resto del código es idéntico a todas las secciones de "abrir archivo" excepto las siguientes
líneas:

invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL

Mandamos el mensaje WM_GETTEXT al control de edición para copiar los datos del bloque de
memoria, el valor devuelto en eax es la longitud de los datos dentro del buffer. Después de que
los datos estén en el bloque de memoria, los escribimos en un nuevo archivo.

Tutorial 13: Archivos Proyectados en


Memoria
Te mostraré qué son los archivos proyectados en memoria y cómo usarlos para tu provecho.
Usar un archivo proyectado en memoria es muy fácil, como verás en este tutorial.

Baja el ejemplo aquí.

Teoría:

Si examinas detenidamente el ejemplo del tutorial previo, encontrarás que tiene un serio
inconveniente: ¿qué pasa si el archivo que quieres leer es más grande que el bloque de
memoria localizado? ¿o qué si la cadena que quieres buscar es cortada por la mitad al final del
bloque de memoria? La respuesta tradicional para la primera cuestión es que deberías leer
repetidas veces en los datos desde el inicio del archivo hasta que encuentres el final del
archivo. La respuesta para la segunda cuestión es que deberías prepararte para el caso
especial al final del bloque de memoria. Esto es lo que se llama el problema del valor del límite.
Presenta terribles dolores de cabeza a los programadores y causa innumerables errores [bugs].

Sería agradable localizar un bloque de memoria, lo suficientemente grande para almacenar


todo el archivo pero nuestro programa debería ser abundante en recursos. Proyección de
archivo al ataque. Al usar proyección de archivo, puedes pensar en todo el archivo como si
estuviera ya cargado en la memoria y puedes usar un puntero a la memoria para leer o escribir
datos desde el archivo. Tan fácil como eso. No necesitas usar las funciones de memoria de la
API y separar más las funciones de la API para E/S de archivo, todas ellas son una y la misma
bajo proyección de archivo.

La proyección de archivos también es usada como un medio de compartir memoria entre los
archivos. Al usar proyección de archivos de esta manera, no hay involucrados archivos reales.
Es más como un bloque de memoria reservado que todo proceso puede *ver*. Pero compartir
datos entre procesos es un asunto delicado, no para ser tratado ligeramente. Tienes que
implementar sincronización de procesos y de hilos, sino tus aplicaciones se quebrarán [crash]
en un orden muy corto.

No tocaremos el tema de los archivos proyectados como un medio de crear una memoria
compartida en este tutorial. Nos concentraremos en cómo usar el archivo proyectado como
medio para "proyectar" un archivo en memoria. En realidad, el cargador de archivos PE usa
proyección de archivo para cargar archivos ejecutables en memoria. Es muy conveniente ya
que sólo las partes pueden ser leídas selectivamente desde el archivo en disco. Bajo Win32,
deberías usar proyección de archivos cada vez que fuera posible.

Sin embargo, hay algunas limitaciones al emplear archivos proyectados en memoria. Una vez
que creas un archivo proyectado en memoria, su tamaño no puede ser cambiado durante esa
sección. Así que proyectar archivos es muy bueno para archivos de sólo lectura u operaciones
de archivos que no afecten el tamaño del archivo. Eso no significa que no puedes usar
proyección de archivo si quieres incrementar el tamaño del archivo. Puedes estimar el nuevo
tamaño y crear archivos proyectados en memoria basados en el nuevo tamaño y el archivo se
incrementará a ese tamaño. Esto es muy conveniente, eso es todo.

Suficiente para la explicación. Vamos a zambullirnos dentro de la implemantación de la


proyección de archivos. Con el fin de usar proyección de archivos, deben seguirse los
siguientes pasos:

1. llamar CreateFile para abrir el archivo que quieres proyectar.


2. llamar CreateFileMapping con el manejador de archivo regresado por CreateFile como
uno de sus parámetros. Esta función crea un objeto de archivo proyectado a partir del
archivo abierto por CreateFile.
3. llamar a MapViewOfFile para proyectar una región del archivo seleccionado o el
archivo completo a memoria. Esta función regresa un puntero al primer byte de la
región proyectada del archivo.
4. Usar el puntero para leer o escribir el archivo
5. llamar UnmapViewOfFile para des-proyectar el archivo.
6. llamar a CloseHandle con el manejador del archivo proyectado como parámetro para
cerrar el archivo proyectado.
7. llamar CloseHandle de nuevo, esta vez con el manejador regresado por CreateFile
para cerrar el archivo actual.

Ejemplo:

El programa que aparece abajo, te permite abrir un archivo a través de una caja de diálogo
"Open File". Abre el archivo utilizando proyección de archivo, si esto tiene éxito, el encabezado
de la ventana es cambiado al nombre del archivo abierto. Puedes salvar el archivo con otro
nombre seleccionando File/Save como elemento de menú. El programa copiará todo el
contenido del archivo abierto al nuevo archivo. Nota que no tienes que llamar a GlobalAlloc
para localizar un bloque de memoria en este programa.

.386
.model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260

.data
ClassName db "Win32ASMFileMappingClass",0
AppName db "Win32 ASM File Mapping Example",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
hMapFile HANDLE 0 ; Manejador al archivo proyectado en memoria, debe
ser
;inicializado con 0 porque también lo usamos
como
;una bandera en la sección WM_DESTROY

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hFileRead HANDLE ? ; Manejador al archivo fuente
hFileWrite HANDLE ? ; Manejador al archivo salida
hMenu HANDLE ?
pMemory DWORD ? ; puntero a los datos en el archivo fuente
SizeWritten DWORD ? ; número de bytes actualmente escritos por
WriteFile
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_CREATE
invoke GetMenu,hWnd ;Obtener el manejador del menú
mov hMenu,eax
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileRead,eax
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
.endif
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileWrite,eax
invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax
invoke GetFileSize,hFileRead,NULL
invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
invoke UnmapViewOfFile,pMemory
call CloseMapFile
invoke CloseHandle,hFileWrite
invoke SetWindowText,hWnd,ADDR AppName
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
.endif
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

end start

Análisis:

invoke CreateFile,ADDR buffer,\


GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL

Cuando el usuario selecciona un archivo en el diálogo Open File, llamamos a CreateFile para
abrirlo. Nota que especificamos GENERIC_READ para abrir este archivo con acceso de sólo
lectura y dwShareMode es cero porque no queremos ningún otro proceso para modificar el
archivo durante nuestra operación.

invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL

Luego llamamos a CreateFileMapping para crear un archivo proyectado en memoria a partir del
archivo abierto. CreateFileMapping tiene la siguiente sintaxis:

CreateFileMapping proto hFile:DWORD,\


lpFileMappingAttributes:DWORD,\
flProtect:DWORD,\
dwMaximumSizeHigh:DWORD,\
dwMaximumSizeLow:DWORD,\
lpName:DWORD

Deberías saber primero que CreateFileMapping no tiene que proyectar todo el archivo a
memoria. Puedes usar esta función para proyectar sólo una parte del archivo actual a memoria.
Especificas el tamaño del archivo proyectado a memoria en los parámetros
dwMaximumSizeHigh y dwMaximumSizeLow. Si especificas un tamaño mayor al archivo
actual, el tamaño de este archivo será expandido. Si quieres que el archivo proyectado sea del
mismo tamaño que el archivo actual, pon ceros en ambos parámetros.

Puedes usar NULL en el parámetro lpFileMappingAttributes para que Windows cree un archivo
proyectado en memoria con los atributos de seguridad por defecto.

flProtect define la protección deseada para el archivo proyectado en memria. En nuestro


ejemplo, usamos PAGE_READONLY para permitir sólo operaciones de lectura sobre el archivo
proyectado en memoria. Nota que este atributo no debe contradecir el atributo usado en
CreateFile, sino CreateFileMapping fallará.

lpName apunta al nombre del archivo proyectado en memoria. Si quieres compartir este archivo
con otros procesos, debes suministrarle un nombre. Pero en nuestro ejemplo, nuestro proceso
es el único que usa este archivo, así que ignoramos este parámetro.
mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax

Si CreateFileMapping es satisfactorio, cambiamos el encabezado [caption] de la ventana al


nombre del archivo abierto. El nombre del archivo con su ubicación [path] completa es
almacenado en un buffer, queremos desplegar sólo el nombre del archivo en el encabezado,
así que debemos agregar el valor del miembro nFileOffset de la estructura OPENFILENAME a
la dirección del buffer.

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED

Como precausión, no queremos que el usuario abra más de un archivo a la vez, así que
difuminar [gray out] el elemento Open del menú y habilitamos el elemento Save.
EnableMenuItem se emplea para cambiar el atributo de los elementos de menú.

Después de esto, esperamos a que el usuario seleccione File/Save como elemento de menú o
cierre nuestro programa. S el usuario elige cerrar el programa, debemos cerrar el archivo
proyectado en memoria y el archivo actual siguiendo un código como el siguiente:

.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL

En el recorte de código anterior, cuando el procedimiento de ventana recibe el mensaje


WM_DESTROY, chequea primero el valor de hMapFile para comprobar si es cero o no. Si no
es cero, llamam a la función CloseMapFile que contiene el siguiente código:

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

CloseMapFile cierra el archivo proyectado en memoria y el archivo actual de manera que no


haya pérdida de recursos cuando nuestro programa salga a Windows.

Si nuestro usuario elige guardar esos datos a otros archivos, el programa le presentará una
caja de diálogo común "save as". Después de que el usuario escribe el nombre del nuevo
archivo, el archivo es creado por la función CreateFile.

invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax

Inmediatamente después de que el archivo de salida es creado, llamamos a MapViewOfFile


para proyectar la parte deseada del archivo proyectado en memoria. Esta función tiene la
siguiente sintaxis:

MapViewOfFile proto hFileMappingObject:DWORD,\


dwDesiredAccess:DWORD,\
dwFileOffsetHigh:DWORD,\
dwFileOffsetLow:DWORD,\
dwNumberOfBytesToMap:DWORD
dwDesiredAccess especifica qué operaciones queremos hacer con el archivo. En nuestro
ejemplo, sólo queremos leer los datos de manera que usamos FILE_MAP_READ.
dwFileOffsetHigh y dwFileOffsetLow especifican el desplazamiento inicial de la proyección
del archivo que queremos proyectar en memoria. En nuestro caso, queremos leerlo todo, de
manera que comenzamos desde el desplazamiento cero en adelante.
dwNumberOfBytesToMap especifica el número de bytes a proyectar en memoria. Si quieres
proyectar todo el archivo (especificado por CreateFileMapping), paamos 0 a MapViewOfFile.
Después de llamar a MapViewOfFile, la porción deseada es cargada en memoria. Obtendrás el
puntero al bloque de memoria que contiene los datos del archivo.

invoke GetFileSize,hFileRead,NULL

Conseguir el tamaño del archivo. El tamaño del archivo es regresado en eax. Si el archivo es
mayor a 4 GB, la palabra alta DWORD del tamaño del archivo es almacenada en
FileSizeHighWord. Ya que no esperamos manejar un archivo de tal tamaño, podemos
ignorarlo.

invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL

Escribir los datos del archvo proyectado en memoria en el archivo de salida.

invoke UnmapViewOfFile,pMemory

Cuando hayamos terminado de realizar las operaciones que deseábamos con el archivo de
entrada, des-proyectarlo (unmapping) de la memoria..

call CloseMapFile
invoke CloseHandle,hFileWrite

Y cerrar todos los archivos.

invoke SetWindowText,hWnd,ADDR AppName

Restablecer el texto original del encabezado.

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED

Habilitar el elemento del Open de menú y eliminar la difuminación del elemento Save As del
menú

Aprenderemos qué es un proceso, cómo crearlo y cómo terminarlo.

Bajar el ejemplo aquí.

Preliminares:

¿Qué es un proceso? He extrañido esta definición del referencia de la API de Win32:

"Un proceso es una aplicación en ejecución que consiste en un espacio de direcciones privado,
código, datos, y otros recursos del sistema operativo, tales como archivos, tuberías, y objetos
de sincronización que son visibles al proceso."

Cómo puedes ver, un proceso "se apropia" de algunos objetos: el espacio de direcciones, el
módulo ejecutante o los módulos, y cualquier cosa que el módulo ejecutante pueda crear o
abrir. Al menos, un proceso debe consistir en un módulo ejecutable, un espacio de direcciones
privado y un hilo. Todo proceso debe tener por lo menos un hilo.

¿Qué es un hilo? Un hilo es realmente una cola o flujo de ejecución. Cuando Windows crea un
proceso, crea sólo un hilo para el proceso. Este hilo generalmente inicia la ejecución desde la
primera instrucción en el módulo. Si el proceso luego necesita más hilos, puede crearlos
explícitamente.

Cuando Windows recibe la orden de crear un proceso, crea un espacio de direcciones privado
para el proceso y luego proyecta el archivo ejecutable en la memoria de ese proceso. Después
de eso crea el hilo primario del proceso.

Bajo Win32, puedes crear procesos desde tus programas llamando a la función CreateProcess.
CreateProcess tiene la siguiente sintaxis:

CreateProcess proto lpApplicationName:DWORD,\


lpCommandLine:DWORD,\
lpProcessAttributes:DWORD,\
lpThreadAttributes:DWORD,\
bInheritHandles:DWORD,\
dwCreationFlags:DWORD,\
lpEnvironment:DWORD,\
lpCurrentDirectory:DWORD,\
lpStartupInfo:DWORD,\
lpProcessInformation:DWORD

No te alarmes por el número de parámetros. Podemos ignorar muchos de ellos.

lpApplicationName --> El nombre del archivo ejecutable, con o sin ubicación, que quieres
ejecutar. Si este parámetro es nulo, debes proveer el nombre del archivo ejecutable en el
parámetro lpCommandLine

lpCommandLine --> Los argumentos en la línea de órdenes del programa que quieres
ejecutar. Nota que si lpApplicationName es NULL, este parámetro debe contener también el
nombre del archivo ejecutable. Como este: "notepad.exe readme.txt"

lpProcessAttributes ylpthreadAttributes --> Especifican los atributos de seguridad para el


proceso y el hilo primario. Si son NULLs, son usados los atributos de seguridad por defecto.

bInheritHandles --> Una bandera que especifica si quieres que el nuevo proceso herede todos
los manejadores abiertos de tu proceso.

dwCreationFlags --> Algunas banderas que determinan la conducta del proceso que quieres
crear, tales como, ¿quieres que el proceso sea cerrado pero inmediatamente suspendido para
que puedas examinarlo o modificarlo antes de que corra? También puedes indicar la clase de
prioridad del(os ) hilo(s) en el nuevo proceso. Esta clase de prioridad es usada para determinar
el plan de prioridades de los hilos dentro del proceso. Normalmente usamos la bandera
NORMAL_PRIORITY_CLASS.

lpEnvironment --> Puntero a un bloque del entorno que contiene algunas cadenas del entorno
para el nuevo proceso. Si este parámetro es NULL, el nuevo proceso hereda el bloque de
entorno del proceso padre.

lpCurrentDirectory --> Puntero que especifica el volumen o disco duro y el directorio para el
proceso hijo. NULL si quieres que el proceso hijo herede el del padre.

lpStartupInfo --> Apunta a una estructura STARTUPINFO que especifica como la ventana
principal del nuevo proceso debería aparecer. la estructura STARTUPINFO contienen muchos
miembros que especifican la apariencia de la ventana principal del proceso hijo. Si no quieres
nada especial, puedes llenar la estructura STARTUPINFO con los valores del proceso padre
llamando a la función GetStartupInfo.

lpProcessInformation --> Apunta a la estructura PROCESS_INFORMATION que recibe


información sobre la identificación del nuevo proceso. La estructura
PROCESS_INFORMATION tiene los siguientes miembros:

PROCESS_INFORMATION STRUCT
hProcess HANDLE ? ; handle to the child process
hThread HANDLE ? ; handle to the primary thread of the child process
dwProcessId DWORD ? ; ID of the child process
dwThreadId DWORD ? ; ID of the primary thread of the child process
PROCESS_INFORMATION ENDS

El manejador del proceso y su ID son dos cosas diferentes. Un ID de proceso es un


identificador único para el proceso en el sistema. Un manejador de proceso es un valor que
regresa Windows para usar en otras funciones API relacionadas con el proceso. Un manejador
de proceso no puede ser usado para identificar un proceso, ya que no es único.

Después de llamar a CreateProcess, se crea un nuevo proceso y la llamada a CreateProcess


regresa de inmediato. Puedes chequear si el nuevo proceso todavía está activo llamando a la
función GetExitCodeProcess que tiene la siguiente sintaxis:

GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

Si esta llamada tiene éxito, lpExitCode contiene el status de terminación del proceso en
cuestión. Si el valor en lpExitCode es igual a STILL_ACTIVE, entonces ese proceso todavía
está corriendo.

Puedes forzar la terminación de un proceso llamando a la función TerminateProcess. Tiene la


siguiente sintaxis:

TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

Puedes especificar el código de salida que desees para el proceso, cualquier valor que te
guste. TerminateProcess no es una manera limpia de terminar un proceso ya que ninguna dll
enganchada al proceso será notificada que el proceso ha terminado.

Ejemplo:

El siguiente ejemplo creará un nuevo proceso cuando el usuario seleccione el elemento de


menú "create process". Intentará ejecutar "msgbox.exe". Si el usuario quiere terminar el nuevo
proceso, puede seleccionar el elemento de menú "terminate process". El programa chequeará
primero si el nuevo proceso ya está destruido, si este no es el caso, el programa llamará a la
función TerminateProcess para destruir el nuevo proceso.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
.const
IDM_CREATE_PROCESS equ 1
IDM_TERMINATE equ 2
IDM_EXIT equ 3

.data
ClassName db "Win32ASMProcessClass",0
AppName db "Win32 ASM Process Example",0
MenuName db "FirstMenu",0
processInfo PROCESS_INFORMATION <>
programname db "msgbox.exe",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HANDLE ?
ExitCode DWORD ? ; contiene el estatus del código de salida de la llamada a
; GetExitCodeProcessl.

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL startInfo:STARTUPINFO
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread
.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Análisis:
El programa crea la ventana principal y regresa el manejador del menú para usarlo en el futuro.
Luego espera a que el usuario seleccione una orden o comando en el menú. Cuando el usuario
selecciona el elemento de menú "Process" en el menú principal, procesamos el mensaje
WM_INITMENUPOPUP para modificar los elementos de menú dentro de el menú emergente
antes de que sea desplegado.

.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif

¿Por qué queremos procesar este mensaje? porque queremos preparar los elementos en el
menú emergente antes de que el usuario pueda verlos. En nuestro ejemplo, si el nuevo
proceso aún no ha comenzado, queremos habilitar el elemento "start process" y difuminar [gray
out] el elemento "terminate process". Hacemos la inversión si el nuevo proceso ya está activo.

Primero chequeamos si el nuevo proceso todavía está activo llamando a la función


GetExitCodeProcess con el manejador de proceso llenado por la función CreateProcess. Si
GetExitCodeProcess regresa FALSE, significa que el proceso no ha comenzado todavía así
que difuminamos el elemento de menú "terminate process". Si GetExitCodeProcess regresa
TRUE, sabemos que ha sido iniciado un nuevo proceso, pero tenemos que chequear luego si
todavía está corriendo. Así que comparamos el valor en ExitCode al valor STILL_ACTIVE, si
son iguales, el proceso todavía está corriendo: debemos difuminar el elemento de menú "start
process" ya que no queremos iniciar varios procesos concurrentes.

.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread

Cuando el usuario selecciona el elemento de menú "start process", primero chequeamos si el


miembro hProcess de la estrcutura PROCESS_INFORMATION ya está cerrado. Si es la
primera vez, el valor de hProcess siempre será cero ya que definimos la estructura
PROCESS_INFORMATION en la sección .data. Si el valor del miembro hProcess no es 0,
significa que el proceso hijo ha terminado pero no hemos cerrado su manejador de proceso
todavía. Así que este es el momento de hacerlo.

Si llamamos a la función GetStartupInfo llenaremos la estructura startupinfo que pasaremos a


la función CreateProcess. Después de que llamamos a la función CreateProcess para
comenzar el nuevo proceso. Nota que no hemos chequeado el valor regersado por
CreateProcess ya que haría más complejo el ejemplo. En la vida real, deberías chequear el
valor regresado por CreateProcess. Inmediatamente después de CreateProcess, cerramos el
manejador de hilo primario regresado en la estructura processInfo. Cerrar el manejador no
significa que hemos terminado el hilo, sólo siginifica que no queremos usar el manejador para
referirse al hilo de nuestro programa. Si no lo cerramos, causará carencia de recursos.

.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0

Cuando el usuario selecciona el elemento de menú "terminate process", chequeamos si el


nuevo proceso ya está activo llamando a la función GetExitCodeProcess. Si todavía está
activo, llamamos la función TerminateProcess para matar el proceso. También cerramos el
manejador del proceso hijo ya que no lo necesitamos más.

Tutorial 15: Programación Multihilo


En este tutorial aprenderemos como crear un programa multihilos [multithreading program].
también estudiaremos los métodos de comunicación entre los hilos.

Bajar el ejemplo aquí.

Teoría:

En el tutorial previo, aprendiste que un proceso consta de al menos un hilo [thread]: el hilo
primario. Un hilo es una cadena de ejecución. también puedes crear hilos adicionales en tu
programa. Puedes concebir la programación multihilos [multithreading programming] como una
programación multitareas [multitasking programming] dentro de un mismo programa. En
términos de implementación, un hilo es una función que corre concurrentemente con el hilo
principal. Puedes correr varias instancias de la misma función o puedes correr varias funciones
simultáneamente dependiendo de tus requerimientos. La programación multihilos es específica
de Win32, no existe una contraparte en Win16.

Los hilos corren en el mismo proceso, así que ellos pueden acceder a cualquiera de sus
recursos tal como variables globales, manejadores etc. Sin embargo, cada hilo tiene su pila
[stack] propia, así que las variables locales en cada hilo son privadas. Cada hilo también es
propietario de su grupo de registros privados, así que cuando Windows conmuta a otros hilos,
el hilo puede "recordar" su último estado y puede "resumir" la tarea cuando gana el control de
nuevo. Esto es manejado internamente por Windows.

Podemos dividir los hilos en dos categorías:

1. Hilo de interface de usuario: Este tipo de hilo crea su propia ventana, y así recibe
mensajes de ventana. Puede responder al usuario a través de su propia ventana. Este
tipo de hilo está sujeto a la regla del Mutex de Win16 que permite sólo un hilo de
interface de usuario en el núcleo de usuario y gdi de 16-bit. Mientras el hilo de interface
de usuario esté ejecutando código de núcleo de usuario y gdi de 16-bit, otros hilos UI
no podrán usar los servicios del núcleo de usuario y gdi. Nota que este Mutex de Win16
es específico a Windows 95 desde su interior, pues las funciones de la API de
Windows 95 se remontan [thunk down] hasta el código de 16-bit. Windows NT no tiene
Mutex de Win16 así que los hilos de interface de usuario bajo NT trabajan con más
fluidez que bajo Windows 95.
2. Hilo obrero [Worker thread]: Este tipo de hilo no crea ninguna ventana así que no
puede recibir ningún mensaje de ventana. Existe básicamente para hacer la tarea
asignada en el trasfondo hence el nombre del hilo obrero.
Recomiendo la siguiente estrategia cuando se use la capacidad multihilo de Win32: Dejar que
el hilo primario haga de interface de usuario y los otros hilos hagan el trabajo duro en el
trasfondo. De esta manera, el hilo primario es como un Gobernador, los otros hilos son como el
equipo del gobernador [Governor's staff]. El Gobernador delega tareas a su equipo mientras
mantiene contacto con el público. El equipo del Gobernador ejecuta con obediencia el trabajo y
lo reporta al Gobernador. Si el Gobernador fuera a realizar todas las tareas por sí mismo, el no
podría atender bien al público ni a la prensa. Esto sería parecido a una ventana que está
realizando una tarea extensa en su hilo primario: no responde al usuario hasta que la tarea ha
sido completada. Este programa podría beneficiarse con la creación de un hilo adicional que
sería el respondable de la extensa tarea, permitiendo al hilo primario responder a las órdenes
del usuario.

Podemos crear un hilo llamando a la función CreateThread que tiene la siguiente sintaxis:

CreateThread proto lpThreadAttributes:DWORD,\


dwStackSize:DWORD,\
lpStartAddress:DWORD,\
lpParameter:DWORD,\
dwCreationFlags:DWORD,\
lpThreadId:DWORD

La función CreateThread se parece un poco a CreateProcess.


lpThreadAttributes --> Puedes usar NULL si quieres que el hilo tenga el manejador de
seguridad por defecto.
dwStackSize --> especifica el tamaño de la pila del hilo. Si quieres que la pila del nuevo hilo
tenga el mismo tamaño que la pila del hilo primario, usa NULL en este parámetro.
lpStartAddress--> Dirección de la función del hilo. Es la función que hará el trabajo del hilo.
Esta función DEBE recibir uno y sólo un parámetro de 32-bits y regresar un valor de 32-bits.
lpParameter --> El parámetro que quieres pasar a la función del hilo.
dwCreationFlags --> 0 significa que el hilo corre inmediatamante después de que es creado.
Lo opuesto es la bandera CREATE_SUSPENDED.
lpThreadId --> La función CreateThread llenará el ID del hilo del nuevo hilo creado en esta
dirección.

Si la llamada a CreateThread tiene éxito, regresa el manejador del hilo creado. Sino, regresa
NULL.

La función del hilo corre tan pronto se realiza la llamada a CreateThread, a menos que
especifiques la bandera CREATE_SUSPENDED en dwCreationFlags. En ese caso, el hilo es
suspendido hasta que se llama a la función ResumeThread.

Cuando la función del hilo regresa con la instrucción ret, Windows llama a la función ExitThread
para la función de hilo implícitamente. Tú mismo puedes llamar a la función ExitThread con tu
función de hilo pero hay un pequeño punto qué considerar al hacer esto. Puedes regresar el
código de salida del hilo llamando a la función GetExitCodeThread. Si quieres terminar un hilo
desde otro, puedes llamar a la función TerminateThread. Pero sólo deberías usar esta función
bajo circunstancias extremas ya que la función termina el hilo de inmediato sin darle chance al
hilo de limpiarse después.

Ahora vamos a ver los métodos de comunicación entre los hilos.

Hay tres de ellos:

• Usar variable globales


• Mensajes de Windows
• Eventos
Los hilos comparten los recursos del proceso, incluyendo variables globales, así que los hilos
pueden usar variables globales para comunicarse entre sí. Sin embargo este método debe ser
usado con cuidado. La sincronización de hilos debe tenerse en cuenta. Por ejemplo, si dos
hilos usan la misma estructura de 10 miembros, ¿qué ocurre cuando Windows de repente jala
hacia sí el control de un hilo mientras éste estaba en medio de la actualización de la
estructura? ¡El otro hilo quedará con datos inconsistentes en la estructura! No cometas ningún
error, los programas multihilos son difíciles de depurar y de mantener. Este tipo de errores
parecen ocurrir al azar lo cual es muy difícil de rastrear.

También puedes usar mensajes Windows para comunicar los hilos entre sí. Si todos los hilos
son interface de usuario, no hay problema: este método puede ser usado como una
comunicación en dos sentidos. Todo lo que tienes que hacer es definir uno o más mensajes de
ventana hechos a la medida que sean significativos sólo para tus hilos. Defines un mensaje
hecho a la medida usando el mensaje WM_USER como el valor base:

WM_MYCUSTOMMSG equ WM_USER+100h

Windows no usará ningún valor desde WM_USER en adelante para sus propios mensajes, así
que puedes usar el valor WM_USER y superiores como tus valores para los mensajes hechos
a la medida.

Sin uno de los hilos es una interface de ususario y el otro es un obrero, no puedes usar este
método como dos vías de comunicación ya que el hilo obrero no tiene su propia ventana, así
que no posee una cola de mensajes. Puedes usar el siguiente esquema:

Hilo de interface de usuario ------> variable(s) global(es)----> Hilo obrero


Hilo obrero ------> mensaje(s) de ventana hecho(s) a la medida----> Hilo de interface de
usuario

En realidad, usaremos este método en nuestro ejemplo.


El último método de comunicación es un objeto de evento. Puedes concebir un objeto de
evento como un tipo de bandera. Si el objeto evento es un estado "no señalado" [unsignalled],
el hilo está durmiendo o es un durmiente, en este estado, el hilo no recibe una porción de
tiempo del CPU. Cuando el objeto de evento está en estado "señalado" [signalled], Windows
"despierta" el hilo e inicia la ejecución de la tarea asignada.

Ejemplo:

Deberías bajar el archivo zip y correr thread1.exe. Haz click sobre el elemento de menú
"Savage Calculation". Esto le ordenará al programa que ejecute "add eax,eax " por
600,000,000 veces. Nota que durante ese tiempo, no puedes hacer nada con la ventana
principal: no puedes moverla, no puedes activar su menú, etc. Cuando el cálculo se ha
completado, aparece una caja de mensaje. Después de eso, la ventana acepta tus órdenes
normalmente.

Para evitar este tipo de inconvenientes al usuario, podemos mover la rutoina de "cálculo" a un
hilo obrero separado y dejar que el hilo primario continúe con su tarea de interface de usuario.
Incluso puedes ver que aunque la ventana principal responde más lento que de costumbre,
todavía responde

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
0,\
ADDR ThreadID
invoke CloseHandle,eax
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD


mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

end start

Análisis:

El programa principal presenta al usuario una ventana normal con un menú. Si el usuario
selecciona el elemento de menú "Create Thread", el programa crea un hilo a través del
siguiente código:

.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

La función de arriba crea un hilo que creará un procedimiento llamado ThreadProc que correrá
concurrentemente con el hilo primario. Después de una llamada satisfactoria, CreateThread
regresa de inmediato y ThreadProc comienza a correr. Puesto que no usamos manejadores de
hilos, deberíamos cerrarlo, sino habrá cierta carencia de memoria. Nota que al cerrar el
manejador del hilo éste no termina. El único efecto es que ya no se puede usar más el
manejador del hilo.

ThreadProc PROC USES ecx Param:DWORD


mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

Como puedes ver, ThreadProc ejecuta un cáculo salvaje que tarda un poco para terminar y
cuando finaliza envía un mensaje WM_FINISH a la ventana principal. WM_FINISH es nuestro
mensaje hecho a la medida definido como:

WM_FINISH equ WM_USER+100h

no tienes que agregar WM_USER con 100h pero es más seguro hacerlo así.

El mensaje WM_FINISH es significativo sólo dentro del programa. Cuando la ventana principal
recibe el mensaje WM_FINISH, responde desplegando una caja de mensaje que dice que el
cálculo ha terminado.

Puedes crear varios hilos en sucesión enviando varias veces el mensaje "Create Thread".
En este ejemplo, la comuicación se realiza en un solo sentido ya que sólo un hilo puede
notificar la ventana principal. Si quieres que el hilo principal envíe órdenes [commands] al hilo
obrero, lo puedes hacer así:

• agregar un elemento de menú que diga algo como "Kill Thread" en el menú
• una variable global que será usada como bandera de mando [command flag]
TRUE=Detener el hilo, FALSE=continuar el hilo
• modificar ThreadProc para chequear el valor de la bandera de mando en el bucle.

Cuando el usuario selecciona el elemento "Kill Thread" del menú, el programa principal pondrá
el valor TRUE en la bandera de mando. Cuando ThreadProc observa que el valor en la
bandera de mando es TRUE, sale del bucle y regresa terminando entonces el hilo.

Aprenderemos qué es un objeto evento y como usarlo en un programa multithilo.

Bajar el ejemplo aquí.

Teoría:

En el tutorial anterior, demostré como se comunican los hilos usando un mensaje de ventana
custom. I left out otros dos métodos: variable global y objeto evento. Usaremos los dos en este
tutorial.
Un objeto evento es como un conmutador [switch]: tiene dos estados: activado [on] o inactivado
[off]. Cuando un objeto es activado [turned on], está en estado "señalado". Cuando es
desactivado [turned off], está en estado "no-señalado". Creas un evento y pones en un recorte
de código en los hilos releventes para ver el estado del objeto evento. Si el objeto evento está
en el estado no señalado, los hilos que esperan serán puestos a dormir [asleep]. Cuando los
hilos están en estado de espera, consumen algo de tiempo del CPU.

Creas un objeto evento llamando a la función CreateEvent que tiene la siguiente sintaxis:

CreateEvent proto lpEventAttributes:DWORD,\


bManualReset:DWORD,\
bInitialState:DWORD,\
lpName:DWORD

lpEventAttribute--> Si especificas un valor NULL, el objeto evento es creado con el descriptor


de seguridad por defecto.
bManualReset--> Si quieres que Windows automáticamente restablezca el objeto evento a
estado no señalado después de llamara WaitForSingleObject, debes especificar FALSE en este
parámetro. Sino debes restablecer manualmente el objeto evento con la llamada a ResetEvent.

bInitialState--> Si quieres que el objeto evento sea creado en el estado señalado, especifica
TRUE como este parámetro sino el objeto evento será creado en estado no señalado.
lpName --> Puntero a una cadena ASCIIZ que es el nombre de un objeto evento. Este nombre
es usado cuando quieres llamar a OpenEvent.

Si la llamada tiene éxito, regresa el manejador al objeto evento creado sino regresa NULL.

Puedes modificar el estado de un objeto evento con dos llamadas a la API: SetEvent y
ResetEvent. La función SetEvent pone el objeto evento en estado señalado. ResetEvent lo
pone en el estado inverso.
Cuando se crea un objeto, debes poner la llamada a WaitForSingleObject en el hilo que espera
por el estado de un objeto evento. WaitForSingleObject tiene la siguiente sintaxis:

WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD

hObject --> Un manejador a uno de los objetos de sincronización. El objeto evento es uno de
los objetos de sincronización.
dwTimeout --> especificar el tiempo en milisegundos que esta función esperará para ser el
objeto señalado. Si el tiempo especificado ha pasado y el evento objeto todavía no está en
estado señalado, WaitForSingleObject regresa a la instrucción que le llamó. Si quieres esperar
por el objeto indefinidamente, debes especificar el valor INFINITE como valor de este
parámetro.

Ejemplo:

El ejemplo de abajo despliega una ventana que espera a que el usuario seleccione una orden
[command] del menú. Si el usuario selecciona "run thread", el hilo comienza el cálculo salvaje.
Cuando finaliza, aparece una caja de mensaje informando al usuario que la tarea está hecha.
Durante el tiempo que el hilo está corriendo, el usuario puede seleccionar "stop thread" para
detener el hilo.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_START_THREAD equ 1
IDM_STOP_THREAD equ 2
IDM_EXIT equ 3
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMEventClass",0
AppName db "Win32 ASM Event Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
StopString db "The thread is stopped",0
EventStop BOOL FALSE

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
hMenu HANDLE ?
ThreadID DWORD ?
ExitCode DWORD ?
hEventStart HANDLE ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED
.elseif ax==IDM_STOP_THREAD
mov EventStop,TRUE
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD


invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000
.WHILE ecx!=0
.if EventStop!=TRUE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif
.ENDW
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
jmp ThreadProc
ret
ThreadProc ENDP
end start

Análisis:

En este ejemplo, demuestro otra técnica para implementar hilos.

.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

Puedes ver que creo un objeto evento durante el proceso del mensaje WM_CREATE. Creo el
objeto evento en estado no señalado con restableci iento automático. Después de que es
creado el objeto evento, creo el hilo. Sin embargo, el hilo no corre de inmediato, porque espera
que el objeto evento esté en el estado señalado según el código siguiente:

ThreadProc PROC USES ecx Param:DWORD


invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000

La primera linea del procedimiento de hilo es la llamada a WaitForSingleObject. Espera


infinitamente por el estado señalado del objeto evento antes de que retorne. Esto significa que
incluso cuando el hilo es creado, lo ponemos en estado durmiente.

Cuando el usuario selecciona la orden "run thread" del menú, ponemos el evento en estado
señalado siguiendo este código:

.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart

La llamada a SetEvent pone el evento en estado señalado lo cual hace que la llamada a
WaitForSingleObject en el procedimiento de hilo regrese y el hilo comience a correr. Cuando el
usuario selecciona la orden [command] "stop thread", ponemos el valor de la variable global
"EventStop" en TRUE.

.if EventStop==FALSE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif
Esto detiene el hilo y salta de nuevo a la llamada a WaitForSingleObject. Nota que no tienes
que restablecer manualmente el objeto evento en estado no señalado porque
especificamos el parámetro bManualReset de la llamada a CreateEvent como FALSE.

Tutorial 17: Librerias De Enlace


Dinamico (DLL)
En este tutorial aprenderemos algo sobre DLLs, qué son y cómo crearlas.
Te puedes bajar el ejemplo aquí.

Teoría:

Si tu programa se agranda demasiado, encontrarás que los programas que escribes


usualmente tienen algunas rutinas en común. Es una pérdida de tiempo reescribirlas cada vez
que empiezas un nuevo programa. Volviendo a los viejos tiempos del DOS, los programadores
almacenaban estas rutinas que usaban comúnmente en una o varias librerías. Cuando querían
usar las funciones, enlazaban la librería al archivo objeto y el enlazador extraía las funciones de
la librería y las insertaba en el ejecutable final. Este proceso se llama enlace estático. Las
librerías de rutinas del lenguaje C son un buen ejemplo. La parte negativa de este método está
en que tienes funciones idénticas en muchos programas que las usan. Tu espacio en disco se
llena almacenando copias idénticas de las funciones. Pero para programas de DOS este
método es bastante aceptable ya que suele haber un único programa activo en memoria. Así
que no hay un desperdicio notable de memoria.

Bajo Windows, la situación es mas crítica porque puedes tener varios programas funcionando
al mismo tiempo. La memoria es consumida rápidamente si tu programa es bastante grande.
Windows tiene la solución a este tipo de problemas: dynamic link libraries [librerias de enlace
dinámico]. Las librerías de enlace dinamico son una especie de recopilación de funciones
comunes. Windows no cargará varias copias de la DLL en la memoria de manera que si hay
muchos programas que la usen solo corriendo al mismo tiempo, habrá una copia de la DLL en
la memoria para todos estos programas. Voy a aclarar este punto un poco. En realidad, los
programas que usan la misma DLL tendrán su propia copia de esta DLL. Esto hará parecer que
hay varias copias de la DLL en memoria. Pero en realidad, Windows hace que esto sea mágico
a través de la paginación de manera que todos los procesos compartan el mismo código de la
DLL. Así que en la memoria fisica sólo hay una copia del código de la DLL. Como siempre,
cada proceso tendrá su sección única de datos de la DLL.

El programa enlaza la DLL en tiempo de ejecución [at run time], no como en las viejas librerías
estáticas. ¿Por qué se la llama librería de enlace dinámico?. Sólo puedes descargar la DLL en
el proceso cuando ya no la necesitas. Si el programa es el único que usa la DLL, será
descargada de la memoria inmediatamente. Pero si la DLL todavía es usada por algún otro
programa, la DLL continúa en memoria hasta que el último programa que la use la descargue.

Como siempre, el enlazador tiene el trabajo más difícil cuando fija las direcciones del archivo
ejecutable final. Como no puede "extraer" las funciones e insertarlas en el ejecutable final, de
alguna manera tendrá que almacenar suficiente información sobre la DLL y las funciones en el
ejecutable final para poder localizar y cargar la DLL correcta en tiempo de ejecución [at run
time].

Ahí es donde interviene la librería de importación [import library]. Una librería de importación
contiene la información sobre la DLL que representa. El enlazador puede extraer la información
que necesita de la librería de importación y mete esos datos en el ejecutable. Cuando el
cargador de Windows carga el programa en memoria, ve que el programa enlace a una DLL,
así que busca esta DLL, la proyecta en el espacio de direcciones del proceso y fija las
direcciones para las llamadas a las funciones en la DLL.

Puedes elegir tú mismo cargar la librería sin dejárselo al cargador de Windows. Este método
tiene sus pro y sus contras:

• No se necesita una librería de importación, así que puedes cargar y usar cualquier
librería siempre aunque no venga con librería de importación. Pero todavía tienes que
saber algo sobre las funciones de su interior, cuantos parámetros cogen y sus
contenidos.
• Cuando dejas al cargador que carge la DLL para tu programa si el cargador no puede
encontrar la DLL desplegará el mensaje "A required .DLL file, xxxxx.dll is missing" (El
archivo DLL requerido, xxxxx.dll no ha sido encontrado)" y poof! tu programa no tiene la
oportunidad de correr aunque esta DLL no sea esencial para esa operación. Si cargas
la DLL tú mismo, cuando la DLL no ha sido encontrada y no es esencial para la
operación tu programa podrá advertir al usuario sobre el suceso y seguir.
• Puedes llamar a funciones *no documentadas* que no están incluidas en la librería de
importación. Suponiendo que conozcas suficiente información acerca de la función .
• Si usas LoadLibrary tienes que llamar a GetProcAddress para todas las funciones que
quieres llamar. GetProcAddress devuelve la dirección del punto de entrada de la
función de una DLL en particular. Así que tu codigo será un poco mas largo o mas
corto, pero nada mas.

Viendo las ventajas/desventajas de la llamada a LoadLibrary, ahora iremos detallando como


crear una DLL.

El siguiente codigo es el esqueleto de una DLL.

;--------------------------------------------------------------------------------------
; DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp
;------------------------------------------------------------------------------------------------------------------------------
--------------------
; Esta es una función ficticia.
; No hace nada. La he puesto esto aquí para mostrar donde puedes insertar las
funciones
; dentro de una DLL.
;------------------------------------------------------------------------------------------------------------------------------
---------------------
TestFunction proc
ret
TestFunction endp
End DllEntry

;-------------------------------------------------------------------------------------
; DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY DLLSkeleton
EXPORTS TestFunction

El programa anterior es el esqueleto de una DLL. Todas las DLL deben tener una función de
punto de entrada. Windows llamará a la función del punto de entrada en caso que:

• La DLL es cargada por primera vez


• La DLL es descargada
• Un hilo es creado en el mismo proceso
• El hilo es destruido en el mismo proceso

DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD


mov eax,TRUE
ret
DllEntry Endp

Puedes nombrar la función del punto de entrada como quieras pero tendrás que terminarla
END <Nombre de la función de entrada>. Esta función tiene tres parametros, sólo los dos
primeros de estos son importantes.
hInstDLL es el manejador del módulo (module handle) de la DLL. Este no es el mismo que el
manejador de la instancia (instance handle) del proceso. Tendrás que guardar este valor si
necesitas usarlo mas tarde. No podrás obtenerlo otra vez fácilmente.
reason puede ser uno de los cuatro valores:

• DLL_PROCESS_ATTACH La DLL recibe este valor cuando es injertada por primera


vez dentro del espacio de direcciones del proceso. Puedes usar esta oportunidad para
hacer la inicialización .
• DLL_PROCESS_DETACH La DLL recibe este valor cuando va a ser descargada del
espacio de direcciones del proceso. Puedes aprovechar esta oportunidad para hacer
algo de limpieza y liberar memoria.
• DLL_THREAD_ATTACH La DLL recibe este valor cuando el proceso crea un nuevo
hilo .
• DLL_THREAD_DETACH La DLL recibe este valor cuando un hilo en el proceso es
destruido.

Devuelves TRUE en eax si quieres que la DLL siga funcionando. Si devuelves FALSE, la DLL
no será cargada. Por ejemplo, si tu codigo de inicialización debe reservar algo de memoria y no
puede hacerlo satisfactoriamente, la función del punto de entrada devolverá FALSE para
indicar que la DLL no puede funcionar.

Puedes poner tus funciones en la DLL detrás o delante de la función de punto de entrada. Pero
si quieres que puedan ser llamadas por otros programas debes poner sus nombres en la lista
de exportaciones en el archivo de módulo de definición (.def).

LA DLL necesita un archivo de módulo de definición en su entorno de desarrollo. Vamos a


echarle un vistazo a esto.

LIBRARY DLLSkeleton
EXPORTS TestFunction
Normalmente deberás tener la primera linea. La declaración LIBRARY define el nombre interno
del módulo de la DLL. Tendrás que proporcionarlo con el nombre de archivo de la DLL. La
definición EXPORTS le dice al enlazador que funciones de la DLL son exportadas, es decir,
pueden ser llamadas desde otros programas. En el ejemplo, queremos que otros módulos sean
capaces de llamar a TestFunction, así que ponemos el nombre en la definición EXPORTS.

Cualquier otro cambio es en las opciones del enlazador. Deberás poner /DLL opciones y
/DEF:<el nombre de archivo de tu def> en las opciones de tu enlazador algo así :

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:\masm32\lib


DLLSkeleton.obj

Las opciones del ensamblador son las mismas, esto es /c /coff /Cp. Así que después de enlazar
el archivo objeto, obtendrás un .dll y un .lib. El .lib es la librería importada que puedes usar para
enlazar a otros programas que usen las funciones de la DLL.

Seguido mostraré como usar LoadLibrary para cargar la DLL.

;---------------------------------------------------------------------------------------------
; UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0

.data?
hLib dd ? ; el manejador (handle) de la librería (DLL)
TestHelloAddr dd ? ; la dirección de la función TestHello

.code
start:
invoke LoadLibrary,addr LibName
;------------------------------------------------------------------------------------------------------------------------------
----------
; Llama a LoadLibrary con el nombre de la DLL deseada. Si la llamada es correcta
; devolverá el manejador (handle) de la librería (DLL). Si no devolverá NULL
; Puedes pasar el manejador (handle) a GetProcAddress u otra función que requiera
; el manejador (handle) de la librería como parametro.
;------------------------------------------------------------------------------------------------------------------------------
-----------
.if eax==NULL
invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax
invoke GetProcAddress,hLib,addr FunctionName
;------------------------------------------------------------------------------------------------------------------------------
--------------------
; Cuando obtienes el manejador (handle) de la librería, lo pasas a GetProcAddress con la

; dirección del nombre de la función en la DLL que quieres llamar. Esto devuelve la
dirección
; de la función si es correcto. De otra manera devuelve NULL
; Las direcciones de las funciones no cambian a menos que descarges y recarges la
librería .
; Así que puedes ponerlas en variables globales para usos futuros.
;------------------------------------------------------------------------------------------------------------------------------
---------------------
.if eax==NULL
invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
.else
mov TestHelloAddr,eax
call [TestHelloAddr]
;------------------------------------------------------------------------------------------------------------------------------
------------------------
; Lo siguiente, puedes llamar la función con un simple call con la variable conteniendo
; la dirección de la función como operando.
;------------------------------------------------------------------------------------------------------------------------------
------------------------
.endif
invoke FreeLibrary,hLib
;-------------------------------------------------------------------------------------------------------------
; Cuando no necesitas mas la librería descargarla con FreeLibrary.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke ExitProcess,NULL
end start

Puedes ver que usando LoadLibrary es un poco mas problemático pero mas flexible

Tutorial 18: Controles Comunes


Aprenderemos qué son los contrioles comunes y cómo usarlos. Este tutorial sólo será una
introducción rápida a ellos.

Bajar el ejemplo de código fuente aquí.

Teoría:

Windows 95 viene con varias ampliaciones de la interface de usuario sobre Windows 3.1x.
Esas ampliaciones enriquecen la GUI. Algunas de ellas eran apliamente usadas antes de que
Windows 95 llegara a los almacenes, tales como la barra de estado [status bar], barras de
herramientas etc. Los programadores tenían que escribir el código para estas ampliaciones.
Ahora Microsoft las ha incluido con Windows 9x y NT. Aprenderemos sobre ellas aquí.

Estos son los nuevos controles:

• Toolbar
• Tooltip
• Status bar
• Property sheet
• Property page
• Tree view
• List view
• Animación
• Drag list
• Header
• Hot-key
• Image list
• Progress bar
• Right edit
• Tab
• Trackbar
• Up-down

Ya que hay muchos, cargarlos todos en memoria y registrarlos sería un despilfarro de recursos.
Todos ellos, con excepción del control "rich edit", están almacenados en comctl32.dll, desde
donde las aplicaciones pueden cargarlas cuando se quiera usarlos. El control "rich edit" reside
en su propia dll, richedXX.dll, porque es muy complicado y debido a que es más grande que su
brethren.

Puedes cargar comctl32.dll incluyendo una llamada a InitCommonControls en tu programa.

InitCommonControls es una función en comctl32.dll, así que refiriéndola en cualquier parte del
código de tu programa hará que el cargador de archivos PE cargue comctl32.dll cuando corra
tu programa.No tienes que ejecutarla , sólo inclúyela en algún lugar de tu código. ¡Esta
función no hace NADA! Su unica instrucción es "ret". Su único propósito es incluir la referencia
a comctl32.dll en la sección de importación de manera que el caragador de archivos PE lla
cargue cada vez que el programa sea caragado. El verdaero canalllo de batalla es el punto de
entrada de la fucnión en la DLL que registra todas las clases de controles comunes cuando es
cargada la dll. Los controles comunes son creados sobre la base de esas clases así como los
controles de ventana hija tales como "edit", "listbox", etc.
El control "rich edit" es otra cosa. Si quieres usarlo, tienes que llamar a a LoadLibrary para
caragarlo explícitamente y luego llamar a FreeLibrary para descargarlo.

Ahora aprenderemos cómo crearlos. Puedes usar un editor de recursos para incorporarlos en
las cajas de diálogo o puedes crearlos túmismo. Casi todos los controles comunes se crean
llamando a CreateWindowEx o a CreateWindow, pasando le el nombre de la clse del control.
Algunos controles comunes tienen funciones de creación específicas, sin embargo, they are
just wrappers around CreateWindowEx para facilitar la creación de esos controles. Las
funciones de creación específicas existenetes son:

• CreateToolbarEx
• CreateStatusWindow
• CreatePropertySheetPage
• PropertySheet
• ImageList_Create

Con el fin de crear controles comunes, tienes que saber sus nombres de clase. Aparecen en la
siguiente lista:

Nombre de la Clase Control Común


ToolbarWindow32 Toolbar
tooltips_class32 Tooltip
msctls_statusbar32 Status bar
SysTreeView32 Tree view
SysListView32 List view
SysAnimate32 Animación
SysHeader32 Header
msctls_hotkey32 Hot-key
msctls_progress32 Progress bar
RICHEDIT Rich edit
msctls_updown32 Up-down
SysTabControl32 Tab

Los controles property sheets, property pages y image list tienen sus propias funciones de
creación específicas. Los controles drag list son cajas de listas [listbox] potenciadas así que no
tienen su propia clase. Los nombres de las clases de arriba pueden ser verificados chequeando
el guión de recursos generado por el editor de recursos de Visual C++. Difieren de los nombres
de clase que aparecen en la lista de la referencia de la api de Windows de Borland y de la lista
del libro Programación en Windows 95 de Charles Petzold. La lista de arriba es la precisa.

Esos controles comunes pueden usar estilos de ventana generales tales como WS_CHILD, etc.
También tienen sus estilos específicos tales como TVS_XXXXX para el control tree view,
LVS_xxxx para el control list view, etc. La referencia de la api de Win32 es tu mejor amigo en
este punto.

Ahora que sabemos cómo crear controles comunes, podemos ver el método de comunicación
entre los controles comunes y sus padres. A diferencia de los controles de ventanas hija, los
controles comunes no se comunican con el padre a través de WM_COMMAND. En vez de eso,
ellos envían mensajes WM_NOTIFY a la ventana padre cuando algunos eventos interesantes
ocurren con ellos. El padre puede controlar al hijo enviándoles mensajes. También hay muchos
mensajes para los nuevos controles. Deberías consultar tu referencia de la api de win32 para
más detalles.

Vamos ver los controles de barra de progreso [progress bar] y barra de estado [status bar] en el
siguiente ejemplo.

Código muestra :

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDC_PROGRESS equ 1 ; IDs de los controles
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0 ; el nombre de la clase de la barra de
progreso
Message db "Finished!",0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX


mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEB
OX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
mov eax,1000 ; the lParam of PBM_SETRANGE message contains the range

mov CurrentStep,eax
shl eax,16 ; the high range is in the high word
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; crear un temporizador
mov TimerID,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.if TimerID!=0
invoke KillTimer,hWnd,TimerID
.endif
.elseif uMsg==WM_TIMER ; cuando ocurre un
evento de temporizador
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0 ; incrementar el progreso en
la barra de progreso
sub CurrentStep,10
.if CurrentStep==0
invoke KillTimer,hWnd,TimerID
mov TimerID,0
invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
invoke MessageBox,hWnd,addr Message,addr
AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Análisis:
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

Deliberadamente pongo InitCommonControls después de ExitProcess para demostrar que


InitCommonControls está justo ahí para poner una referencia a comctl32.dll en la sección de
importación. Como puedes ver, los controles comunes trabajan incluso si InitCommonControls
no se ejecuta.

.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
Aquí es donde creamos el control común. Nota que esta llamada a CreateWindowEx contiene
hWnd como manejador de la ventana padre. También especifica un ID de control para
identificar este control. Sin embargo, como tenemos un manejador del control de ventana, este
ID no es usado. Todos los controles de ventana hija debe tener el estilo WS_CHILD.

mov eax,1000
mov CurrentStep,eax
shl eax,16
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0

Después de que es creada la barra de progreso, podemos establecer su rango. El rango por
defecto es desde 0 a 100. Si no estás satisfecho con esto, puedes especificar tu propio rango
con el mensaje PBM_SETRANGE. lParam de este parámetro contiene el rango, el máximo
rango está en la palabra alta y el mínimo está en la palabra baja. Puedes especificar cuánto
toma un paso [how much a step takes] usando el mensaje PBM_SETSTEP. El ejemplo lo
establece a 10 lo cual significa que cuando tú envías un mensaje PBM_STEPIT a la barra de
progreso, el indicador de progreso se incrementará por 10. También puedes poner tu indicador
de posición enviando mensajes PBM_SETPOS. Este mensaje te da un control más estricto
sobre la barra de progreso.

invoke
CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
mov TimerID,eax

Luego, creamos la barra de estado llamando a CreateStatusWindow. Esta llamada es fácil de


entender, así que no la comentaré. Después de que es creada la ventana de estado, creamos
un temporizador. En este ejemplo, actualizaremos la barra de progreso en un intervalo regular
de 100 ms así que debemos crear un control de teporización. Abajo está el prototipo de la
función SetTimer.

SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD,


lpTimerProc:DWORD

hWnd : Manejador de la ventana padre


TimerID : un identificador del temporizador, nunca igual a cero. Puedes crear tu propio
identificador.
TimerInterval : el intervalo del temporizador en milisegundos que deben pasar antes de que el
temporizador llame al proceso del temporizador o envía un mensaje WM_TIMER
lpTimerProc : la dirección de la función del temporizador que será llamada cuando el intervalo
de tiempo expire. Si este parámetro es NULL, el temporizador enviará más bien el mensaje
WM_TIMER a la ventana padre.

Si esta llamada tiene éxito, regresará el TimerID. Si falla, regresa 0. Esto se debe a que el valor
del ID del temporizador debe ser diferente a cero.

.elseif uMsg==WM_TIMER
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
sub CurrentStep,10
.if CurrentStep==0
invoke KillTimer,hWnd,TimerID
mov TimerID,0
invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
invoke MessageBox,hWnd,addr Message,addr
AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif

Cuando expira el intervalo de tiempo especificado, el temporizador envía un mensaje


WM_TIMER. Aquí pondrás el código que será ejecutado. En este ejemplo, actualizamos la
barra de progreso y luego chequeamos si ha sido alcanzado el límite máximo. Si ha sido
alcanzado, mataremos el temporizador y luego pondremos el texto en la ventana de estado con
el mensaje SB_SETTEXT. Se depliega una caja de mensaje y cuando el usuario hace click
sobre OK, limpiamos el texto en la barra de estado y en la barra de progreso.

En este tutorial aprenderemos a usar el control Tree View (vista de árbol). Es más, también
aprenderemos cómo arrastrar objetos y ponerlos en el control Tree View; además veremos
cómo usar una lista de imágenes en este control. Puedes bajar el ejemplo aquí.

Tutorial 19: Control Tree View

Teoría:
Un control Tree View es un tipo especial de ventana que representa objetos en orden
jerárquico. Un ejemplo es el panel izquierdo del Explorador de Windows. Se puede personalizar
este control para mostrar relaciones entre los objetos.

Puedes crear un control Tree View llamando CreateWindowEx y pasando la cadena


"SysTreeView32" como el nombre de la clase o puedes incorporarlo en una caja de diálogo. No
hay que olvidar poner una llamada a InitCommonControls en el código fuente. Hay varios
estilos específicos al control Tree View. Estos tres son los más usados.

TVS_HASBUTTONS == Despliega botones más (+) y menos (-) al lado de los


elementos [items] padres. El usuario hace click con los botones del ratón sobre
los elementos para expandir o contraer una lista de elementos hijos subordinada
a un elemento padre. Para incluir botones con elementos en la raiz del tree view,
debe especificarse el estilo TVS_LINESATROOT.
TVS_HASLINES == Usa líneas para mostrar la jerarquía de elementos.
TVS_LINESATROOT == Usa líneas para enlazar elementos en la raiz del control
tree-view. Este valor es ignorado ignored si TVS_HASLINES tampoco es
especificado.

El control Tree View, como otros controles comunes, se comunica con su ventana padre a
través de mensajes. La ventana padre le puede enviar varios mensajes y el control Tree View
puede enviar mensajes de "notificación" a su ventana padre. En este proceso, el control Tree
View no difiere de las otras ventanas: cuando algo interesante ocurre en él, envía un mensaje
WM_NOTIFY con información a la ventana padre.

WM_NOTIFY
wParam == ID del control, nada garantiza que este valor sea el único, así que
nosotros
no lo usamos. Es mejor usar hwndFrom o el miembro IDFrom de la
estructura
NMHDR a la que apunta lParam
lParam == Puntero a la estructura NMHDR. Algunos controles pueden pasar un
puntero
más largo pero debe tener una estructura NMHDR como primer
miembro.
Es decir, cuando tenemos lParam, podemos estar seguros que
apunta a una
estructura NMHDR.

Ahora examinaremos la estructura NMHDR.


NMHDR struct DWORD
hwndFrom DWORD ?
idFrom DWORD ?
code DWORD ?
NMHDR ends

hwndFrom es el manejador de la ventana que envía este mensaje WM_NOTIFY.

idFrom es el ID del control que envía este mensaje.

code es el mensaje actual que el control quiere enviar a su ventana padre.

Las notificaciones del control Tree view tienen TVN_ al comienzo, como
TVM_CREATEDRAGIMAGE. El control tree view envía TVN_xxxx en el miembro código de
NMHDR. La ventana padre puede enviar TVM_xxxx para controlarlo.

Agregando elementos a un control list view


Después de crear un control Tree View, podemos agregarle elementos. Puedes hacer esto
enviándole TVM_INSERTITEM.

TVM_INSERTITEM
wParam = 0;
lParam = pointer to a TV_INSERTSTRUCT;

A estas alturas, bebes conocer cierta terminología sobre la relación entre los elementos en el
control Tree View. Un elemento puede ser al mismo tiempo padre, hijo, o ambos. Un elemento
padre es el que tiene algún(os) otro(s) subelemento(s) asociado(s) con él. Al mismo tiempo, el
el elemento padre puede ser un hijo de algún otro elemento. Un elemento sin un padre se llama
elemento raíz. Puede haber muchos elementos raíz en un control tree view. Ahora examinemos
la estructura TV_INSERTSTRUCT

TV_INSERTSTRUCT STRUCT DWORD


hParent DWORD ?
hInsertAfter DWORD ?
ITEMTYPE < >
TV_INSERTSTRUCT ENDS

hParent = Manejador del elemento padre. Si este miembro es el valor TVI_ROOT o NULL, el
elemento es insertado en la raiz del control tree-view.
hInsertAfter = Manejador del elemento después del cual el nuevo elemento va a ser insertado
o uno de los siguientes valores:

• TVI_FIRST == Inserta el elemento al comienzo de la lista.


• TVI_LAST ==

Inserta el elemento al final de la lista.

• TVI_SORT ==

Inserta el elemento dentro de la lista en orden alfabético.

ITEMTYPE UNION
itemex TVITEMEX < >
item TVITEM < >
ITEMTYPE ENDS

Aquí usaremos solamente TVITEM.

TV_ITEM STRUCT DWORD


imask DWORD ?
hItem DWORD ?
state DWORD ?
stateMask DWORD ?
pszText DWORD ?
cchTextMax DWORD ?
iImage DWORD ?
iSelectedImage DWORD ?
cChildren DWORD ?
lParam DWORD ?
TV_ITEM ENDS

Esta estructura es empleada para enviar y recibir información sobre un elemento del tree view,
dependiendo de los mensajes. Por ejemplo, con TVM_INSERTITEM, es usada para especificar
el atributo del elemento a ser insertado dentro del control tree view. Con TVM_GETITEM, será
llenado con información sobre el elemento seleccionado.

imask es usado para especificar cual(es) miembro(s) de la estructura TV_ITEM es (son)


valido(s). Por ejemplo, si el valor en imask es TVIF_TEXT, significa sólo que el miembro
pszText es válido. Puede cpmbinarse con algunas banderas.
hItem es el manejador del elemento en el control tree view. Cada elemento tiene su propio
manejador, lo mismo que una ventana tiene su manejador. Si se quiere hacer algo con un
elemento, debe seleccionarse ese elemento por su manejador.
pszText es el puntero a una cadena terminada en cero que indica el nivel de un elemento
dentro de un control tree view.
cchTextMax es usado sólo cuando se quiere regresar el nivel de un elemento dentro de un
control tree view. Como el prgramador debe suministrar el puntero al bufer en pszText,
Windows debe saber el tamaño del buffer proveído. Hay que suministrar el tamaño del buffer
en este miembro.
iImage y iSelectedImage refieren al índice dentro de una lista de imágenes que contienen las
imágenes a ser mostradas cuando el elemento no es seleccionado (iImage) y cuando es
seleccionado (iSelectedImage). Si corres el Explorador de Windows, verás que en el panel
izquierdo unas imágenes que representan las carpetas; esas imágenes están especificadas por
estos dos miembros.

Con el fin de insertar un elemento dentro del control tree view, al menos deben llenarse los
miembros hParent, hInsertAfter, así como imask and pszText.

Agregando imágenes a un control tree view


Si quieres poner una imagen a la izquierda de la etiqueta de un elemento del control tree view,
se debe crear una lista de imágenes y asociarla con el control tree view. Se puede crear una
lista de imágenes llamando a ImageList_Create.

ImageList_Create PROTO cx:DWORD, cy:DWORD, flags:DWORD, \


cInitial:DWORD, cGrow:DWORD

Si esta función tiene éxito, regresa el manejador a una lista de imágenes vacía.
cx == ancho de cada imagen en esta lista, en pixeles.
cy == altura de cada imagen en esta lista, en pixeles. Todas las imágenes en una lista deben
ser iguales en tamaño. Si se especifica un gran bitmap, Windows usará cx y cy para *cortarla*
en varios pedazos. Así que las imágenes propias deben prepararse como una secuencia de
"pinturas" del mismo tamaño.
flags == especifica el tipo de imágnes en esta lista: si son de color o monócromos y su
profundidad de color. Consulta la refencia de la API de Win32 para mayor información.
cInitial == El número de imágenes que esta lista contendrá inicialmente. Windows usará esta
información para localizar memoria para las imágenes.
cGrow == Cantidad de imágenes a las que la lista de imágenes puede expandirse cuando el
sistema necesita cambiar el tamaño de la lista para hacer disponibles más imágnes. Este
parámetro representa el número de imágenes nuevas que puede tener una lista de imágenes
que ha cambiado de tamaño.

¡Una lista de imágenes no es una ventana! Es sólo un depósito de imágenes que ha de ser
usado por otras ventanas. Después de crear una lista de imágens, se deben agregar imágenes
llamando a ImageList_Add

ImageList_Add PROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD

Esta función regresa -1 si no tiene éxito.


himl == manejador de la lista de imágenes a la cual quieres agregar imágenes. Es el valor
regresado por una llamada satisfactoria a ImageList_Create
hbmImage == manejador del bitmap a ser agregado a la lista de imágenes. Usualmente
almacenas el bitmap en el recurso y lo cargas en la llamada a LoadBitmap. Nota que no tienes
que especificar los parámetros pasados con el número de imágenes contenidas en este bitmap
porque esta información es inferida a partir de los parámetros cx y cy pasados con la llamada a
ImageList_Create.
hbmMask == Manejador [handle] al bitmap que contiene la máscara. Si no se usa ninguna
máscara con la lista de imágenes, se ignora este parámetro.

Normalmente, agregaremos sólo dos imágenes a la lista de imágenes para usar con el control
treeview: una usada cuando el elemento del control tree view no está seleccionado, y otra para
cuando el elemento es seleccionado.

Cuando la lista de imágenes ya está lista, la asociamos con el control treeview enviando
TVM_SETIMAGELIST al control.

TVM_SETIMAGELIST
wParam = tipo de lista de imagen a establecer. Hay dos posibilidades:

o TVSIL_NORMAL Establece la lista de iágenes normal, que contiene los


elementos seleccionados y no seleccionados para el elemento del control
tree-view.
o TVSIL_STATE Establece el estado de la lista de imágenes, que contiene
las imágens para los elementos del control tree-view que están en un
estado definido por el usuario.

lParam = Manejador de la lista de imágenes

Recuperar información sobre el elemento del treeview


Puedes recuperar información sobre un elemento de un control tree view enviando el mensaje
TVM_GETITEM.
TVM_GETITEM
wParam = 0
lParam = puntero a la estructura TV_ITEM structure a ser llenada con la
información

antes de enviar este mensaje, debe llenarse el miembro imask con la(s) bandera(s) que
especifica(n) cual(es) miembro(s) de TV_ITEM queires que llene Windows. Y lo más
importante, debes llenar hItem con el manejador al elemento del cual deseas obtener
información. Pero esto tiene un problema: ¿Cómo puedes conocer el manejador del elemento
del cual deseas recuperar información? ¿Habrá que almacenar todos los manejadores del
control Tree View? La respuesta es simple: no tienes necesidad de hacerlo. Puedes enviar el
mensaje TVM_GETNEXTITEM al control tree view para recuperar el manejador al elemento del
tree view elemento que tiene el (o los) que tú especificaste. Por ejemplo, puedes solicitar el
manejador del primer elemento hijo, del elemento raiz, del elemento seleccionado, etc.

TVM_GETNEXTITEM
wParam = bandera
lParam = manejador a un elemento tree view (sólo necesario para algunos
valores de la bandera)

El valor en wParam es tan importante que prefiero presentar abajo todas las banderas:

o TVGN_CARET Regresa el elemento seleccionado.


o TVGN_CHILD Regresa el primer elemento hijo del elemento especificado por
el parámetro hitem
o TVGN_DROPHILITE Regresa el elemento que es el target de una operación
arrastrar-y-soltar [drag-and-drop].
o TVGN_FIRSTVISIBLE Regresa el primer elemento visible.
o TVGN_NEXT Regresa el siguiente elemento hermano [sibling].
o TVGN_NEXTVISIBLE Regresa el siguiente elemento visible que sigue al
elemento especificado. El elemento especificado debe ser visible. Usa el
mensaje TVM_GETITEMRECT para determinar si un elemento es visible.
o TVGN_PARENT Regresa el padre del elemento especificado.
o TVGN_PREVIOUS Regresa el elemento hermano anterior.
o TVGN_PREVIOUSVISIBLE Regresa el primer elemento visible que precede al
elemento especificado. El elemento especificado debe ser visible. Usa el
mensaje TVM_GETITEMRECT para determinar si un elemento es visible.
o TVGN_ROOT Regresa el primer elemento o el que está en el tope del control
tree-view.

Como puede verse, si se quiere recuperar el manejador a un elemento del control tree view
este mensaje resulta de gran interés. SendMessage regresa el manejador al elemento del tree
view si tiene éxito. Se puede llenar el valor regresado dentro del miembro hItem de TV_ITEM a
ser usado con el mensaje TVM_GETITEM.

Operación Drag and Drop [arrastrar y soltar] en un control tree


view
Esta parte es la razón por la cual decidí escribir este tutorial. Cuando intenté seguir el ejemplo
en la referencia de la api win32 (el win32.hlp desde InPrise), quedé muy frustrado porque
carecía de la información vital sobre este punto. Por error y prueba, finalmente esbozé como
implementar drag & drop [arrastrar y soltar] en un control tree view y no quiero que nadie pase
por lo mismo que yo.
Abajo están los pasos para implementar operaciones drag & drop en un control tree view.

1. Cuando el usuario trata de arrastrar [drag] un elemento, el control tree view control
envía la notificación TVN_BEGINDRAG a la ventana padre. Puedes aprovechar esta
oportunidad para crear una imagen de arrastre [drag image] que sea la imagen a ser
usada para representar el elemento mientras que está siendo arrastrado. Puedes
enviar TVM_CREATEDRAGIMAGE al control tree view para decirle que cree una
imagen de arrastre por defecto a partir de la imagen que está siendo usada por el
elemento que será arrastrado. El control tree view creará una lista de imágenes con
sólo una imagen de arratre y te regresará el manejador de la lista de imágenes.
2. Después de que la imagen de arratre es creada, especificas el "punto caliente"
[hotspot] de la imagen de arratre llamando a ImageList_BeginDrag.

ImageList_BeginDrag PROTO himlTrack:DWORD, \


iTrack:DWORD , \
dxHotspot:DWORD, \
dyHotspot:DWORD
himlTrack es el manejador a la lista de imágenes que contiene la imagen de arratre.
iTrack es el índice a la lista de imágenes que especifica la imagen de arrastre
dxHotspot especifica la distancia relativa al "punto caliente" [hotspot] en el plano
horizontal en la imagen de arrastre ya que esta imagen será usada en el lugar del
cursor del ratón, así que especificamos usar cual parte de la imagen es el "punto
caliente" .
dyHotspot especifica la distancia relativa del "punto caliente" en el plano vertical.
Normalmente, iTrack debería ser 0 si le dices al control tree view que cree la imagen de
arrastre para tí, y dxHotspot y dyHotspot pueden ser 0 si quieres que la esquina
izquierda superior de la imagen de arrastre sea el hotspot.

3. Cuando la imagen de arrastre esté lista para ser desplazada, llamamos a


ImageList_DragEnter para desplegar la imagen de arrastre en la ventana.

ImageList_DragEnter PROTO hwndLock:DWORD, x:DWORD, y:DWORD


hwndLock es el manejador [handle] de la imagen propietaria de la imagen de arrastre.
La imagen de arrastre no será capaz de moverse fuera de esa ventana.
x e y son las coordenadas x- e y- del lugar donde la imagen de arrastre debería estar
desplegada inicialmente. Nota que estos valores son relativos a la esquina izquierda
superior de la ventana, no del área cliente.

4. Ahora que la imagen de arrastre está desplegada en la ventana, tendrás que soportar
la operación de arrastre en el control tree view. Sin embargo, no hay mucho problema
con esto. Tenemos que monitorear el paso del arrastre con WM_MOUSEMOVE y la
localización para la operación de soltar [drop] con mensajes WM_LBUTTONUP. Sin
embargo, si la imagen de arrastre está sobre una de las ventanas hijas, la ventana
padre recibirá cualquier mensaje del ratón. La solución es capturar la entrada del ratón
con SetCapture. Usando esta llamada, los mensajes del ratón serán enviados a la
ventana especificada independientemente de donde esté el cursor del ratón.
5. Dentro del manejador de WM_MOUSEMOVE, actualizarás el paso del arrastre con una
llamada a ImageList_DragMove. Esta función mueve la imagen que está siendo
arrastrada durante una operación de arrastar-y-soltar [drag-and-drop]. Además, si lo
deseas, puedes "iluminar" [hilite] el elemento sobre el cual está la imagen de arrastre
enviando TVM_HITTEST para chequear si la imagen de arrastre está sobre algún
elemento. Si lo está, puedes enviar TVM_SELECTITEM con la bandera
TVGN_DROPHILITE para "iluminar" ese elemento. Nota que antes de enviar el
mensaje TVM_SELECTITEM, debes esconder primero la imagen de arrastre sino tu
imagen de arrastre dejará trazas horribles. Puedes esconder la imagen de arrastre
enviando ImageList_DragShowNolock y, después que la operación de iluminación
[hilite] haya finalizado, llamar de nuevo a ImageList_DragShowNolock para mostrar la
imagen de arrastre.
6. Cuando el ususario suelta el botón izquierdo del ratón, debes hacer varias cosas. Si tú
iluminas [hilite] un elemento, debes quitarle la iluminación [un-hilite it] enviando
TVM_SELECTITEM con la bandera TVGN_DROPHILITE de nuevo, pero esta vez,
lParam DEBE ser cero. Si tú no le quitas la iluiminación el elemento, obtendrás un
efecto extraño: cuando selecciones algún otro elemento, ese elemento será encerrado
por un rectángulo pero la iluminación estará sobre el último elemento iluminado. Luego,
debes llamar a ImageList_DragLeave seguido por ImageList_EndDrag. Debes liberar
al ratón llamando a ReleaseCapture. Si creas una lista de imágenes, debes destruirla
llamando a ImageList_Destroy. Después de eso, puedes ir a lo que que quieres que
haga tu programa cuando la operación arrastrar y soltar [drag & drop] se haya
completado.

Código de demostración:
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD


.const
IDB_TREE equ 4006 ; ID del recurso bitmap
.data
ClassName db "TreeViewWinClass",0
AppName db "Tree View Demo",0
TreeViewClass db "SysTreeView32",0
Parent db "Parent Item",0
Child1 db "child1",0
Child2 db "child2",0
DragMode dd FALSE ; una bandera para determinar si estamos en modo de
arrastre

.data?
hInstance HINSTANCE ?
hwndTreeView dd ? ; manjeador del control tree view
hParent dd ? ; manejador del elemento raíz del control tree view
hImageList dd ? ; manejador de la lista de imágenes usadas en el control tree
view
hDragImageList dd ? ; manejador de la lista de imágenes usada para almacenar la imagen de arrastre

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR
AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEB
OX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,200,400,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL tvinsert:TV_INSERTSTRUCT
LOCAL hBitmap:DWORD
LOCAL tvhit:TV_HITTESTINFO
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\

WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
0,200,400,hWnd,NULL,\
hInstance,NULL ; Create the tree view control
mov hwndTreeView,eax
invoke ImageList_Create,16,16,ILC_COLOR16,2,10 ; crear la lista de imágenes
asociada
mov hImageList,eax
invoke LoadBitmap,hInstance,IDB_TREE ; cargar el bitmap desde el recurso
mov hBitmap,eax
invoke ImageList_Add,hImageList,hBitmap,NULL; Agregar el bitmap a la lista de imágenes
invoke DeleteObject,hBitmap ; borrar siempre el recurso bitmap
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
mov tvinsert.hParent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT
mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
mov tvinsert.item.pszText,offset Parent
mov tvinsert.item.iImage,0
mov tvinsert.item.iSelectedImage,1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov hParent,eax
mov tvinsert.hParent,eax
mov tvinsert.hInsertAfter,TVI_LAST
mov tvinsert.item.pszText,offset Child1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov tvinsert.item.pszText,offset Child2
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
.elseif uMsg==WM_MOUSEMOVE
.if DragMode==TRUE
mov eax,lParam
and eax,0ffffh
mov ecx,lParam
shr ecx,16
mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx
invoke ImageList_DragMove,eax,ecx
invoke ImageList_DragShowNolock,FALSE
invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL
invoke
SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
.endif
invoke ImageList_DragShowNolock,TRUE
.endif
.elseif uMsg==WM_LBUTTONUP
.if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView
invoke ImageList_EndDrag
invoke ImageList_Destroy,hDragImageList
invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
invoke ReleaseCapture
mov DragMode,FALSE
.endif
.elseif uMsg==WM_NOTIFY
mov edi,lParam
assume edi:ptr NM_TREEVIEW
.if [edi].hdr.code==TVN_BEGINDRAG
invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,
[edi].itemNew.hItem
mov hDragImageList,eax
invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
invoke SetCapture,hWnd
mov DragMode,TRUE
.endif
assume edi:nothing
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start
Análisis:
Dentro del manejador WM_CREATE, se crea el control tree view

invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\

WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATRO
OT,0,\
0,200,400,hWnd,NULL,\
hInstance,NULL

Notar los estilos. TVS_xxxx son los estilos específicos del control tree view.

invoke ImageList_Create,16,16,ILC_COLOR16,2,10
mov hImageList,eax
invoke LoadBitmap,hInstance,IDB_TREE
mov hBitmap,eax
invoke ImageList_Add,hImageList,hBitmap,NULL
invoke DeleteObject,hBitmap
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList

Después, se crea una lista de imágenes vacía para que acepte imágenes de 16x16 pixeles en
tamaño, 16-bit de color e inicialmente, contendrá 2 imágenes pero puede ser expandido hasta
10 si se necesita. Luego cargamos el bitmap desde el recurso y lo agregamos a la lista de
imágenes. Después de eso, borramos el manejador del bitmap ya que no será usado más.
Cuando la lista de imágenes esté toda establecida, la asociamos con el control tree view control
enviando TVM_SETIMAGELIST al control tree view.

mov tvinsert.hParent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT
mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
mov tvinsert.u.item.pszText,offset Parent
mov tvinsert.u.item.iImage,0
mov tvinsert.u.item.iSelectedImage,1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert

Insertamos elementos en el control tree view y empezamos del elemento de la raíz. Puesto que
será el elemento raíz, el miembro del hParent es NULL y hInsertAfter es TVI_ROOT. El
miembro imask especifica a ese pszText, iImage y miembros del iSelectedImage de la
estructura de TV_ITEM es válido. Llenamos a esos tres miembros de valores apropiados.
pszText contiene la etiqueta del elemento raíz, iImage es el índice a la imagen en la lista de la
imagen que se desplegará a la izquierda del elemento del no seleccionado, y iSelectedImage
es el índice a la imagen en la lista de la imagen que se desplegará cuando el elemento se
selecciona. Cuando todos los miembros apropiados estén llenos, enviamos el mensaje
TVM_INSERTITEM al control tree view para agregar el elemento raíz a él.

mov hParent,eax
mov tvinsert.hParent,eax
mov tvinsert.hInsertAfter,TVI_LAST
mov tvinsert.u.item.pszText,offset Child1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov tvinsert.u.item.pszText,offset Child2
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert

Después de agregar el elemento [item] raiz, podemos enganchar los elementos a la ventana
hija. El miembro hParent es llenado ahora con el manejador del elemento padre. Y usaremos
imágenes idénticas en la lista de imágenes, así que no cambiemos los miembros iImage e
iSelectedImage.
.elseif uMsg==WM_NOTIFY
mov edi,lParam
assume edi:ptr NM_TREEVIEW
.if [edi].hdr.code==TVN_BEGINDRAG
invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,
[edi].itemNew.hItem
mov hDragImageList,eax
invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
invoke SetCapture,hWnd
mov DragMode,TRUE
.endif
assume edi:nothing

Ahora cuando el usuario trata de arrastrar [drag] un elemento, el control tree view control envía
un mensaje WM_NOTIFY con el código TVN_BEGINDRAG. lParam es el puntero a una
estructura NM_TREEVIEW que contiene algunas piezas de información que necesitamos para
poner su valor dentro de edi y usar edi como el puntero a la estructura NM_TREEVIEW.
assume edi:ptr NM_TREEVIEW es una manera de decirle a MASM que trate a edi como el
puntero a la estructura NM_TREEVIEW. Luego creamos una imagen de arrastre enviando
TVM_CREATEDRAGIMAGE al control tree view. Regresa el manejador de la lista de imágenes
nuevamente creada con una imagen drag image dentro. Llamamos a ImageList_BeginDrag
para establecer el "punto caliente" en la imagen de arrastre. Luego introducimos la operación
de arrastre llamando a ImageList_DragEnter. Esta función emplea la imagen de arrastre en el
lugar especificado de la ventana. Usamos la estructura ptDrag que es un miembro de la
estructura NM_TREEVIEW como el punto donde la imagen de arratre debería ser inicializada.
Después de eso capturamos la entrada del ratón y ponemos la bandera para indicar que ahora
introducimos el modo de arrastre.

.elseif uMsg==WM_MOUSEMOVE
.if DragMode==TRUE
mov eax,lParam
and eax,0ffffh
mov ecx,lParam
shr ecx,16
mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx
invoke ImageList_DragMove,eax,ecx
invoke ImageList_DragShowNolock,FALSE
invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL
invoke
SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
.endif
invoke ImageList_DragShowNolock,TRUE
.endif

Ahora nos concentramos en WM_MOUSEMOVE. Cuando el usuario arrastra [drags] la imagen


de arrastre [the drag image along], nuestra ventana padre recibe el mesaje
WM_MOUSEMOVE. En respuesta a estos mensajes, actualizamos la posición de la imagen de
arrastre con ImageList_DragMove. Después de eso, chequeamos si la drag image está sobre
algún elemento. Hacemos eso enviando el mensaje TVM_HITTEST al control tree view con un
punto para que él lo chequee. Si la imagen de arrastre está sobre el elemento, iluminamos
[hilite] ese elemento enviando el mensaje TVM_SELECTITEM con la bandera
TVGN_DROPHILITE al control tree view. Durante la operación de iluminación, escondemos la
imagen de arrastre de manera que no deje desagradables manchas sobre el control tree view.

.elseif uMsg==WM_LBUTTONUP
.if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView
invoke ImageList_EndDrag
invoke ImageList_Destroy,hDragImageList
invoke
SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
invoke
SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
invoke
SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
invoke ReleaseCapture
mov DragMode,FALSE
.endif

Cuando el usuario suelta el botón izquierdo del ratón, llega a su final la operación de arrastre.
Abandonamos el modo de arrastre llamando a ImageList_DragLeave, seguido por
ImageList_EndDrag y ImageList_Destroy. Para hacer que los elementos del cotrol tree view se
vean bien, también chequeamos el último elemento iluminado [hilited], y lo seleccionamos.
También debemos quitar la iluminación [un-hilite], sino los otros elementos no serán iluminaods
cuando ellos sean seleccionados. Y finalmente, liberamos la captura del ratón.

Tutorial 20: Subclasificación de


Ventanas
En este tutorial, aprenderemos acerca de la subclasificación de ventanas, qué es y como
aprovecharla.
Bajar el ejemplo aquí.

Teoría:

Si programas por un tiempo en Windows encontrarás casos donde tu ventana tiene CASI todos
los atributos que deseas pero no todos. ¿Haz encontrado una situación donde deseas una
clase especial de control de edición que filtre algunos caracteres indeseables? Lo más directo
que se puede hacer es codificar tu propia ventana. Pero relamente es un trabajo duro y
consume tiempo. La subclasificación de Windows al rescate.

En pocas palabras, la subclasificación de ventanas te permite hacerte cargo de la ventana


subclasificada. Tienes el control absoluto sobre ella. Tomemos un ejemplo para clarificar más.
Supongamos que necesitas una caja de texto que acepte sólo cadenas en hexadecimal. Si
usas un control de edición simple, tienes que decir algo cuando el usuario transcribe algo
diferente a números hexadecimales dentro de la caja de texto, es decir. si el usuario escribe
"zb+q" dentro de la caja de texto, no puedes hacer nada con ello, excepto rechazar toda la
cadena de texto. Esto al menos carece de profesionalidad. En esencia, necesitas la habilidad
de examinar cada caracter que el usuario escribió dentro de la caja de texto en el momento que
él lo transcribió.

Ahora examinaremos cómo hacerlo. Cuando el usuario tipea algo dentro de la caja de texto,
Windows envía el mensaje WM_CHAR al procedimiento de ventana del control de edición. Este
procedimiento de ventana reside dentro de Windows así que no podemos modificarlo. Pero
podemos redirigir el flujo de mensajes a nuestro propio procedimiento de ventana. Así
que nuestro procedimiento de ventana obtendrá primero un impacto [shot] de cualquier
mensaje de Windows antes de que sea enviado al control de edición. Si nuestro procedimeinto
de ventana resuelve actuar sobre el mensaje, puede aprovechar ahora para hacerlo. Pero si no
desea manejar el mensaje, lo pasa al procedimiento de ventana principal. De esta manera,
nuestro procedimiento de ventana se inserta dentro de Windows y del control de edición.
Veamos el siguiente flujo:

Antes de la Subclasificación

Windows ==> procedimiento de ventana del control de edición

Después de la Subclasificación

Windows ==> nuestro procedimiento de ventana ----> procedimiento de ventana del


control de edición

Ahora ponemos nuestra atención en cómo subclasificar una ventana. Nota que la
subclasificación no está limitada a los controles, puede ser usada con cualquier ventana.
Pensemos cómo Windows llega a tener conocimiento sobre dónde residen los procedimientos
de ventana de los controles de edición. Una pista?......el miembro lpfnWndProc de la estructura
WNDCLASSEX. Si podemos reemplazar este miembro con la dirección de nuestro
procedimiento de ventana, Windows enviará más bien mensajes a nuestro procedimiento de
ventana.

Podemos hacer esto llamando a SetWindowLong.

SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD

hWnd = manejador de la ventana cuya estructura WNDCLASSEX será cambiada


nIndex == valor a cambiar.

GWL_EXSTYLE Establece un nuevo valor de estilo extendido de ventana.


GWL_STYLE Establece un nuevo valor de estilo de ventana.
GWL_WNDPROC Establece una nueva dirección del procedimiento de ventana.
GWL_HINSTANCE Establece un nuevo manejador de instacia de la aplicación.
GWL_ID Establece un nuevo ID para la ventana.
GWL_USERDATA Establece un valor de 32-bit asociado con la ventana. Cada
ventana tiene un valor correspondiente de 32-bit para usar por la aplicación que creó la
ventana.

dwNewLong = el valor de reemplazo.

Así que la tarea es fácil. Programamos un procedimiento de ventana que maneje los mensajes
para el control de edición y luego llamamos a SetWindowLong con la bandera
GWL_WNDPROC, pasando junto a ella la dirección de nuestro procedimento de ventana como
tercer parámetro. Si la función tiene éxito, el valor de regreso es el valor previo del número
entero especificado de 32-bit, en nuestro caso, la dirección del procedimiento de ventana
original. Necesitamos almacenar este valor para usarlo dentro de nuestro procedimiento de
ventana.

Recuerda que habrá algunos mensajes que no querrás manejar, los pasaremos al
procedimiento de ventana original. Podemos hacerlo llamando a la función CallWindowProc.

CallWindowProc PROTO lpPrevWndFunc:DWORD, \


hWnd:DWORD,\
Msg:DWORD,\
wParam:DWORD,\
lParam:DWORD

lpPrevWndFunc = la dirección del procedimiento de ventana original.


Los restantes cuatro parámetros son pasados a nuestro procedimiento de ventana. Los
pasamos justo con CallWindowProc.

Código Muestra:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD


EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SubclassWinClass",0
AppName db "Subclassing Demo",0
EditClass db "EDIT",0
Message db "You pressed Enter in the text box!",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEB
OX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.if uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
20,300,25,hWnd,NULL,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,eax
;-----------------------------------------
; Subclass it!
;-----------------------------------------
invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
mov OldWndProc,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD


.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") ||
al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr
AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Análisis:

invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc


mov OldWndProc,eax

Después de que el control de ventana es creado, lo subclasificamos llamando a


SetWindowLong, reemplazando la dirección principal del procedimiento de ventana con la
dirección del nuestro. Observa que almacenamos la dirección del procedimiento principal para
usarlo luego con CallWindowProc. Nota también que EditWndProc es un procedimiento de
ventana común y corriente.

.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") ||
al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif

Dentro de EditWndProc, filtramos los mensajes WM_CHAR. Si el caracter está entre 0-9 o a-f,
lo aceptamos pasándolo con el mensaje al procedimiento de ventana original. Si el carácter
está en minúscula, lo convertimos en mayúscula agregándole 20h. Nota que si el caracter no
es el único que esperamos, lo descartamos. No lo pasamos al procedimiento de ventana
original. Así que cuando el usuario escribe algo distinto a 0-9 o a a-f, el caracter no aparecerá
en la ventana de edición.

.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr
AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.end

Ahora quiero demostrar el poder de la subclasificación atrapando la tecla ENTER. EditWndProc


chequea el mensaje WM_KEYDOWN para verificar si es VK_RETURN (la tecla Enter). Si lo es,
despliega una caja de mensaje que dice "You pressed the Enter key in the text box!". Si no es
una tecla Enter, pasa el mensaje al procedimiento de ventana original.

Puedes usar la subclasificación de ventanas para tomar el control sobre otras ventanas. Es una
de las técnicas más poderosas que deberías tener en tu arsenal.

Tutorial 21: Tubería


En este tutorial, exploraremos la tubería [pipe], qué es y qué podemos hacer por él. Para
hacerlo más interesante, me meto en la técnica de cómo cambiar el color del fondo y del texto
de una ventana de edición.

Bajar el ejempo aquí.

Teoría:

Una tubería [pipe] es un conducto o vía de comunicación entre dos terminales. Puedes usar
una tubería para intercambiar datos entre dos procesos diferentes, o dentro del mismo proceso.
Es como un walkie-talkie. Das a la otra parte una configuración y esta parte puede usarla para
comunicarse contigo.

Hay dos tipos de tuberías: anónima y con nombre. Una tubería anónima es, como lo dice el
nombre, anónima: es decir, puedes usarla sin saber su nombre. Una tubería nombrada es lo
opuesto: tienes que conocer su nombre antes de usarla.

También puedes clasificar las tuberías de acuerdo a su propiedad: un-sentido (one-way) o dos-
sentidos (two way). En una tubería de un sentido, los datos pueden fluir sólo en una dirección:
de un terminal a otro. Mientras que en una tubería de dos sentidos, los datos pueden ser
intercambiados entre dos terminales.

Una tubería anónima siempre es de un sentido mientras que una tubería nombrada puede ser
de un sentido o de dos sentidos. Usualmente se usa una tubería nombrada en un entorno de
red donde un servidor puede conectarse a varios clientes.

En este tutorial, examinaremos con cierto detalle las tuberías anónimas. El propósito principal
de una tubería anónima es ser usada como un canal de comunicación entre un proceso padre y
un proceso hijo o entre procesos hijos.

La tubería anónima es realmente útil cuando tratamos con una aplicación de cónsola. Una
aplicación de cónsola es un tipo de programa win32 que usa una cónsola para su entrada y su
salida. Una consola es como una caja DOS. Sin embargo, una aplicación de cónsola es un
programa totalmente en 32-bit. Puede usar cualquier función GUI, como otros porgramas GUI.
Lo que ocurre es que tiene una cónsola para su uso.

Una aplicación de cónsola tiene tres manejadores [handles] que pueden usarse para entrada y
salida. Los llamamos manejadores estándard. Hay tres de ellos: entrada estándard, salida
estándar y error estándard. El manejador de entrada estándard es usado para leer/recobrar la
información de la cónsola y el manejador de salida eestándar es usado para información
salida/impresión para la cónsola. El manejador de error estándar es usado para reportar
condiciones de error ya que su salida no puede ser redireccionada.

Una aplicación de cónsola puede recuperar estos tres manejadores llamando a la función
GetStdHandle, especificando el manejador que quiere obtener. Una aplicación GUI no tiene
una cónsola. Si llamas a GetStdHandle, regresará un error. Si en realidad quieres usar una
consola, puedes llamar a AllocConsole para localizar una nueva cónsola. Sin embargo, no
olvides llamar a FreeConsole cuando hayas hecho lo que tienes que hacer con la cónsola.

Con más frecuencia se emplea una tubería anónima para redireccionar entrada y/o salida de
una aplicación de cónsola hija. Para que esto trabaje el proceso padre puede ser una
aplicación de cónsola o una GUI, pero el proceso hijo debe ser una aplicación de cónsola.
Como debes saber, una aplicación de cónsola usa manejadores estándard para su entrada y
salida. Si queremos redireccionar entrada y/o salida de una aplicación de cónsola, podemos
reemplazar su manejador con el manejador de un terminal de una tubería. Una aplicación de
cónsola no sabe que está usando el manejador de un terminal de una tubería. Lo usará como
un manejador estándar. Esto es un tipo de polimorfismo, como se diría en la jerga POO [OOP:
Object Oriented Programming = Programación Orientada a Objetos]. Esta aproximación al
problema es poderosa ya que no necesitamos modificarde ninguna manera el proceso hijo .

Otra cosa que deberías saber sobre las aplicaciones de cónsola es de donde obtiene los
manejadores estándar. Cuando se crea una aplicación de cónsola, el proceso padre tiene dos
posiblidades: puede crear una nueva cónsola para la hija o puede dejar que la hija herede su
propia cónsola. Para que trabaje la segunda opción, el proceso padre debe ser una aplicación
de cónsola, pero si es una aplicación GUI, debe llamar primero a AllocConsole para localizar
una cónsola.

Comencemos a trabajar. Con el fin de crear una tubería anónima necesitas llamar a
CreatePipe. CreatePipe tiene el siguiente prototipo:

CreatePipe proto pReadHandle:DWORD, \


pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD

• pReadHandle es un puntero a una variable dword que recibirá el manejador al terminal


de lectura de la tubería
pWriteHandle es un puntero a una variable dword que recibirá el manejador al terminal
de escritura de la tubería.
• pPipeAttributes apunta a una estructura SECURITY_ATTRIBUTES que determina si
los manejadores lectura y escritura regresados son heredables por el proceso hijo
• nBufferSize es el tamaño sugerido del buffer que la pipa reservará para su uso. Es sólo
un tamaño sugerido. Puedes usar NULL para decirle a la función que use el tamaño
por defecto.

Si la llamada tiene éxito, el valor regresado es distinto de cero. Si falla, el valor regresado es
cero.

Después de que la llamada tiene éxito, obtendrás dos manejadores, uno para el terminal de
lectura de la tubería y otro para el terminal de escritura.

Ahora resumiremos los pasos para redireccionar la salida estándard de un programa de


cónsola hijo hacia nuestro propio proceso. Nota que mi método difiere del de la referencia de la
api de win32 suministrada por Borland. El método en la referencia de la api win32 asume que el
proceso padre es una aplicación de cónsola y por eso el proceso hijo puede heredar los
manejadores estándar de él. Pero en muchas ocasiones, necesitaremos redireccionar la salida
desde una aplicación de cónsola a una GUI.

1. Crear una tubería anónima con CreatePipe. No olvides establecer el miembro


bInheritable de SECURITY_ATTRIBUTES a TRUE para que el manejador sea
heredable.
2. Ahora debemos preparar los parámetros que pasaremos a CreateProcess ya que los
usaremos para cargar la aplicación de cónsola hija. Otra estructura importante es
STARTUPINFO. Esta estructura determina la apariencia de la ventana principal del
proceso hijo cuando aparece por vez primera. Esta estructura es vital para nuestro
propósito. Puedes esconder la ventana principal y pasar el manejador de la tubería al
proceso de cónsola hijo con ella. Abajo están los miembros que debes llenar:
o cb : tamaño de la estructura STARTUPINFO
o dwFlags : las banderas binarias que determinan cuales de los miembros de la
estructura son validos también gobiernan el estado mostrado/oculto de la
ventana principal. Para nuestro propósito, debería usar una combinación de
STARTF_USESHOWWINDOW y STARTF_USESTDHANDLES
o hStdOutput and hStdError : los manejadores que quieres que el proceso hijo
use como manejadores salida/error estándar. Para nuestro propósito,
pasaremos el manejador de escritura de la tubería como manejador de salida y
error estándar del proceso hijo. De manera que cuando las salidas hijas algo a
la salida/error estándar, realmente pase la información a través de la tubería al
proceso padre.
o wShowWindow gobierna el estado mostrado/oculto de la ventana principal.
Para nuestro propósito, no queremos que la ventana de la cónsola del proceso
hijo se muestre, así que ponemos SW_HIDE en este miembro.
3. Llamar a CreateProcess para cargar la aplicación hija. Después de que CreateProcess
ha tenido éxito, la hija todavía está durmiendo. Es cargada en la memoria pero no corre
de inmediato. Cerrar el manejador de escritura de la tubería. Esto es necesario. Porque
el proceso padre no tiene uso para el manejador de escritura de la tubería, y la pipa no
trabajará si hay más de un terminal de escritura, DEBEMOS cerrarlos antes de leer los
datos de la tubería. Sin embargo, no cierres el manejador de escritura antes de llamar
a CreateProcess, porque sino tu pipa se romperá. Deberías cerrarla justo después del
regreso de CreateProcess y antes de leer los datos del terminal de lectura de la
tubería.
4. Ahora puedes leer los datos del terminal de lectura de la piap con ReadFile. Con
ReadFile, you kick dentro del proceso hijo en modo de ejecución [running mode].
Iniciará la ejecución y cuando escriba algo al manejador de salida estándar (el cual es
realmente el manejador al termianl de escritura de la tubería), los datos serán enviados
a través de la tubería al terminal de lectura. Puedes pensar en ReadFile como una
extracción de datos del terminal de lectura de la tubería. Debes llamar a ReadFile
repetidas veces hasta que retorne 0 lo cual significa que ya no hay más datos que leer.
Puedes hacer algo con los datos que lees de la tubería. En nuestro ejemplo, los pongo
dentro del control de edición.
5. Cerrar el manejador de lectura de la tuberia.

Ejemplos:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101 ; el ID del menú principal
IDM_ASSEMBLE equ 40001

.data
ClassName db "PipeWinClass",0
AppName db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError db "Error during pipe creation",0
CreateProcessError db "Error during process creation",0
CommandLine db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD


LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET
WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,IDR_MAINMENU
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke Reg es terClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke D es patchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL rect:RECT
LOCAL hRead:DWORD
LOCAL hWrite:DWORD
LOCAL startupinfo:STARTUPINFO
LOCAL pinfo:PROCESS_INFORMATION
LOCAL buffer[1024]:byte
LOCAL bytesRead:DWORD
LOCAL hdc:DWORD
LOCAL sat:SECURITY_ATTRIBUTES
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+
ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL,
hInstance, NULL
mov hwndEdit,eax
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetBkColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
.elseif uMsg==WM_SIZE
mov edx,lParam
mov ecx,edx
shr ecx,16
and edx,0ffffh
invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
.elseif uMsg==WM_COMMAND
.if lParam==0
mov eax,wParam
.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE
invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
.if eax==NULL
invoke MessageBox, hWnd, addr CreatePipeError, addr AppName,
MB_ICONERROR+ MB_OK
.else
mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
or startupinfo.dwFlags, STARTF_USESHOWWINDOW+
STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE
invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL,
NULL, NULL, addr startupinfo, addr pinfo
.if eax==NULL
invoke MessageBox,hWnd,addr CreateProcessError,addr
AppName,MB_ICONERROR+MB_OK
.else
invoke CloseHandle,hWrite
.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw
.endif
invoke CloseHandle,hRead
.endif
.endif
.endif
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
.endif
xor eax,eax
ret
WndProc endp
end start

Análisis:

El ejemplo llamará a ml.exe para ensamblar un archivo llamado test.asm y redireccionar la


salida de ml.exe al control de edición en su área cliente.
Cuando el programa es cargado, registra la clase de ventana y crea la ventana principal como
es usual. Lo primero que hace durante la creación de la ventana principal es crear un control de
edición que será usado paera desplegar la salida de ml.exe.

Ahora la parte interesante, cambiaremos el color del texto y del fondo del control de edición.
Cuando un control de edición va a pintar su área cliente, envía el mensaje
WM_CTLCOLOREDIT a su padre.

wParam contiene el manejador del dispositivo de contexto que usará el control de edición para
escribir su propia área cliente. Podemos aprovechar esto para cambiar las características de
HDC.

.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetTextColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret

SetTextColor cambia el color del texto a amarillo. SetTextColor cambia el color de fondo del
texto a negro.Y finalmente, obtenemos el manejador a la brocha negra que queremos regresar
a Windows. Con el mensaje WM_CTLCOLOREDIT, debes regresar un manejador a la brocha
que Windows usará para pintar el fondo del control de edición. En nuestro ejemplo, quiero un
fondo negro así que regreso a Windows un manejador a la brocha negra.

Ahora el usuario selecciona el elemento de menú Assemble, crea una tubería anónima.

.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE

Antes de llamar a CreatePipe, primero debemos llenar la estrcutura SECURITY_ATTRIBUTES.


Nota que podemos usar NULL en el miembro lpSecurityDescriptor si nos tiene sin cuidado la
seguridad. Y el miembro bInheritHandle debe ser TRUE para que los manejadores de la tubería
sean heredados al proceso hijo.

invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

Después de eso llamamos a CreatePipe que, si tiene éxito, llenará las variables hRead y
hWrite con los manejadores a los terminales de lectura y escritura respectivamente.

mov startupinfo.cb,sizeof STARTUPINFO


invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+
STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE

Ahora vamos a llenar la estructura STARTUPINFO. Llamamos a GetStartupInfo para llenar la


estructura STARTUPINFO con los valores por defecto del proceso padre. Debes llenar la
estructura STARTUPINFO con esta llamada si quieres que tu código trabaja con win9x y NT.
Después que regrese la llamada a GetStartupInfo, puedes modificar los miembros que son
importantes. Copiamos el manejador del terminal de escritura de la tubería dentro de
hStdOutput y hStdError ya que queremos que el proceso hijo lo use en vez de los manejadores
estándar de salida/error. También queremos esconder la ventana de la cónsola del proceso
hijo, así que ponemos el valor SW_HIDE dentro del miembro wShowWidow. Por último,
debemos indicar que los miembros hStdOutput, hStdError y wShowWindow son válidos y
deben ser usados especificando las banderas STARTF_USESHOWWINDOW y
STARTF_USESTDHANDLES en el miembro dwFlags.

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL,


NULL, NULL, addr startupinfo, addr pinfo

Ahora creamos el proceso hijo con la llamada a CreateProcess. Nota que el parámetro
bInheritHandles debe ser establecido a TRUE para que trabaje el manejador de la tubería.

invoke CloseHandle,hWrite

Después de que creamos satisfactoriamente el proceso hijo, debemos cerrar el terminal de


escritura de la tubería. Recuerda que pasamos el manejador de escritura al proceso hijo a
través de la estructura STARTUPINFO. Si no cerramos el manejador de escritura de nuestro
terminal, habrán dos terminales de escritura. Y la tubería no trabajará. Debemos cerrar el
manejador de escritura después de CreateProcess pero antes de leer los datos del terminal de
lectura.

.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw

Ahora estamos listos para leer los datos de la salida estándar del proceso hijo. Nos
mantenemos en un bucle infinito hasta que no hayan más datos que leer desde el terminal de
lectura de la tubería. Llamamos a RtlZeroMemory para llenar el buffer con ceros y luego
llmamos a ReadFile, pasando el manejador de lectura de la tubería en lugar de un manejador
de archivo. Nota que sólo leemos un máximo de 1023 bytes ya que necesitamos que los datos
sean una cadena ASCIIZ que podemos pasar al control de edición

Cuando regresa ReadFile con los datos en el buffer, llenamos los datos dentro del control de
edición. Sin embargo, aquí hay un pequeño problema. Si usamos SetWindowText para poner
los datos dentro del control de edición, los nuevos datos sobreescribirán los datos existentes!
Queremos que los datos se anexen al final de los datos existentes.

Para alcanzar esa meta, primero movemos la careta alfinal del texto en el control de edición
enviando el mensaje EM_SETSEL con wParam==-1. Luego, anexamos los datos en ese punto
enviando el mensaje EM_REPLACESEL.

invoke CloseHandle,hRead

Cuando ReadFile retgresa NULL, rompemos el bucle y cerramos el manejador de escritura.

Tutorial 22: Superclasificación


En este tutorial aprenderemos sobre la superclasificación, qué es y para qué sirve. También
aprenderás cómo suministrar navegación con la tecla Tab a los controloes de tu propia
ventana.
Bajar el ejemplo aquí
Teoría:
En tu carrera de programación seguramente encontrarás alguna situación donde necesitarás
controles con un comportamiento *levemente* distinto. Por ejemplo, puedes necesitar diez
controles de edición que acepten sólo números hexadecimales. Hay varias maneras de
alcanzar este objetivo:

• Crear tu propia clase e instanciar los cotroles


Crear esos controles de edición y luego subclasificarlos
• Superclasificar el control de edición

El primer método es demasiado tedioso. Tienes que implementar todas las funcionalidades del
control tú mismo. Es una tarea difícil de hacer con agilidad. El segundo método es mejor que el
primero, pero todavía requiere mucho trabajo. Es bueno sólo si subclasificas unos cuantos
controles pero ya casi es una pesadilla subclasificar más de doce ventanas. La
superclasificación es la técnica que deberías usar para esta ocasión.

La subclasificación es la técnica que empleaas para *tomar el control* de una clase de ventana
particular. Por *tomar el control*, quiero decir que puedes modificar la propiedad de la clase de
la ventana para adaptarla a tus propósitos y luego crear un montón de controles.

Aquí están esbozados los pasos de la subclasificación:

• llamar a GetClassInfoEx para obtener la información de la ventana que quieres


subclasificar. GetClassInfoEx requiere un puntero a la estructura WNDCLASSEX que
será llenada con la información si la llamada resulta satisfactoria.
• Modificar los miembros de WNDCLASSEX que quieres. Sin embargo, hay dos
miembros que DEBES modificar:
o hInstance Debes poner el manejador de instancia de tu programa dentro de
este miembro.
o lpszClassName Debes suministrarlo con un puntero al nuevo nombre de la
clase.
No necesitas modificar el miembro lpfnWndProc pero muchas veces, necesitas
hacerlo. Sólo recuerda salvar el miembro original lpfnWndProc si quieres
llamarla con CallWindowProc.
• Registtrar la estructura WNDCLASSEX modificada. Tendrás una nueva clase de
ventana que tendrá algunas características de la antigua clase de ventana.
• Crear la nueva ventana a partir de la nueva clase.

La superclasificación es mejor que subclasificar s quieres crear mechos controles con las
mismas características.

Ejemplo:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WM_SUPERCLASS equ WM_USER+5


WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
.data
ClassName db "SuperclassWinClass",0
AppName db "Superclassing Demo",0
EditClass db "EDIT",0
OurClass db "SUPEREDITCLASS",0
Message db "You pressed the Enter key in the text box!",0

.data?
hInstance dd ?
hwndEdit dd 6 dup(?)
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX


mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR
ClassName,ADDR AppName,\

WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEB
OX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,220,NULL,NULL,\
hInst,NULL
mov hwnd,eax

.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp
WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM,
lParam:LPARAM
LOCAL wc:WNDCLASSEX
.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc
push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass
invoke RegisterClassEx, addr wc
xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD


.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") ||
al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr
AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Análisis:
El programa creará una ventana con seis controles de edición "modificados" en su área cliente.
Los controles de edición aceptarán sólo dígitos hexadecimales. Realmente, el ejemplo de
subclasificación para hacer supreclasificación. El programa comienza normalmente hasta llegar
a la parte interesante, donde se crea la ventana:

.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc

Primero debemos llenar la estructura WNDCLASSEX con los datos de la clase que queremos
subclasificar, en este caso, la clase EDIT. Recuerda que debes establecer el miembro cbSize
de la estructurta WNDCLASSEX antes de llamar a GetClassInfoEx sino la estrcutura
WNDCLASSEX no será llenada debidamente. Después de que regresa GetClassInfoEx, wc es
llenada con toda la información que necesitamos para crear la nueva clase de ventana.

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass

Ahora debemos modificar algunos miembros de wc. El primero es el puntero al procedimiento


de ventana. Como necesitamos encadenar nuestro procedimiento de ventana con el original,
debemos salvarlo con una variable para poderlo llamar con CallWindowProc. Esta técnica es
idéntica a la de subclasificación, excepto porque modificas la estructura WNDCLASSEX
directamente sin tener que llamar a SetWindowLong. Hay dos miembros que deberán ser
cambiados, sino no padrás registrar la nueva clase de ventana:, hInstance and lpsClassName.
Debes reemplazar el valor original de hInstance con hInstance del propio programa. Y debes
elegir un nuevo nombre para la nueva clase.

invoke RegisterClassEx, addr wc


Cuando todo esté listo, registras la nueva clase de ventana. Obtendrás una nueva clase de
ventana con algunas características de la antigua clase.

xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit

Ahora que hemos registrado la clase, podemos crear ventanas basadas en ella. En el recorte
anterior, uso ebx como contador del número de ventanas creadas. edi es usado como la
coordenada y correspondiente a la esquina izquierda del programa. Cuando se crea una
ventana, su manejador es almacenado en un arreglo o array de variables dwords. Cuando
todas las ventanas son creadas, porn el foco de la ventana a la primera ventana.

En este punto ya tienes 6 controles de edición que sólo aceptan dígitos hexadecimales. Los
procedimientos de ventanas reemplazados manejan el filtro. Realmente, es idéntico al
procedimiento de ventana en la subclasificación. Como puedes ver, ya no tienes que hacer el
trabajo extra de la subclasificación.

Me he metido en un recorte de código para manejar controles de navegación con tab y hacer
más rico este ejemplo. Normalmente, si quieres poner controles en una caja de diálogo, el
propietario de la caja de diálogo maneja las teclas de navegación para tí, de manera que
puedas usar la tecla tab para ir al próximo control o shift-tab para regresar al control previo.
Alas, esta funcionalidad no está disponible si colocas los controles sobre una ventana. Tienes
que subclasificarlos de manera que puedas manejar las teclas Tab por tí mismo. En nuestro
ejemplo no necesitamos subclasificar los controles uno por uno porque los hemos
superclasificado, así que podemos proveer un "propietario del control central de navegación"
para ellos.

.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret

El recorte de código de arriba es tomado del procedimiento EditWndClass. Chequea si el


usuario presionó la tecla Tab, si es así, llama a GetKeyState para chequear si la tecla SHIFT
también está presionada. GetKeyState regresa un valor en eax que determina si la tecla
específica ha sido presionada o no. Si ha sido presionada, el bit alto de eax está establecido,
vale 1. Si no, el bit alto está en blanco, vale 0. Así que probamos el valor de retorno contra
80000000h. Si el bit alto está establecido, significa que el usuario presionó shift+tab lo cual
debemos manejar por separado.

Si el usuario presiona sólo la tecla Tab, llamamos a GetWindow para recuperar el manejador
del próximo control. Usamos la bendera GW_HWNDNEXT para decir a GetWindow que
obtenga el manejador a la ventana próxima a la línea del actual hEdit. Si esta función regeras
NULL, lo interpretamos como si no más manejadorespara obtenert así que el actual hEdit es el
último control en la línea. We will "wrap around" al primer control llamando a GetWindow con la
bendera GW_HWNDFIRST. Similar al caso Tab, shift-tab trabaja de manera inversa.

Tutorial 23: Icono de Bandeja [Tray


Icon]
En este tutorial, aprenderemos cómo poner iconos en la banadeja de sistema y cómo
crear/usar un menú emergente.
Bajate el ejemplo aquí.

Teoría:

La bandeja del sistema es la región rectangular en la barra de tareas donde residen la mayoría
de los iconos. Normalmente, verás como mínimo un reloj digital aquí. También puedes poner
iconos en la barra de sistema. Debajo están los pasos que tienes que seguir para poner un
icono en la barra de sistema:

1. rellena la estructura NOTIFYICONDATA que tiene los siguientes miembros:


o cbSize El tamaño de esta estructura.
o hwnd Manejador (Handle) de la ventana que recibirá la notificacion cuando
ocurran eventos de ratón sobre el icono de la bandeja.
o uID La constante que es usada como identificador del icono. Tu eres el
único que decide este valor. En caso de que tengas mas de un icono de
bandeja, podrás chequear aquí desde cuál icono de bandeja proviene la
notificación del ratón.
o uFlags Especifica qué miembros son válidos
 NIF_ICON El miembro hIcon es válido.
 NIF_MESSAGE El miembro uCallbackMessage es válido.
 NIF_TIP El miembro szTip es válido.
o uCallbackMessage El mensaje común que Windows mandará a la ventana
especificado por el miembro hwnd cuando ocurren eventos sobre el icono de
bandeja. Creas este mensaje tú mismo.
o hIcon El manejador (handle) del icono que quieres poner en la barra de
sistema
o szTip Un arreglo [array] de 64-bytes que contiene la cadena que será
usada como texto tooltip cuando el ratón esté sobre el icno de bandeja.
2. Llama a Shell_NotifyIcon que está definida en shell32.inc. Esta función tiene el
siguiente prototipo:
3.

Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

dwMessage Es el tipo de mensaje a enviar al shell de Windows.


NIM_ADD Añade un icono al area de estado.
NIM_DELETE Borra un icono del area de estado.
NIM_MODIFY Modifica un icono en el area de estado.
pnid es el puntero a la estructura NOTIFYICONDATA rellenada con valores
apropiados
Si quieres añadir un icono a la bandeja, usa el mensage NIM_ADD, si quieres borrar un
icono , usa NIM_DELETE.

Eso es todo lo que hay en ella. Pero la mayoría de las veces no estás contento con sólo poner
un icono aquí. Necesitas ser capaz de responder a eventos de ratón sobre el icono de bandeja.
Puedes hacer esto procesando el mensaje que especificas en el miembro uCallbackMessage
de la estructura NOTIFYICONDATA. Este mensaje tiene los siguientes valores en wParam y
lParam (agradecimientos especiales a s__d por esta informacion):

• wParam contiene el ID del icono. Este es el mismo valor que pones en el miembro uID
de la estructura NOTIFYICONDATA.
• lParam La palabra baja (low word) contiene el mensage de ratón. Por ejemplo, si el
usuario pulsa el botón derecho en el icono, lParam contendra WM_RBUTTONDOWN.

La mayoría de los iconos de bandeja, como siempre, muestran un menú emergente cuando el
usuario pulsa el botón derecho sobre éste. Podemos implementar esta función creando un
menú emergente y entonces llamando a TrackPopupMenu para mostrarlo. Los pasos están
descritos abajo:

1. Crear un menu emergente llamando a CreatePopupMenu. Esta función crea un menu


vacío. Esto devuelve un manejador (handle) al menú en eax si el resultado es
satisfactorio.
2. Añade elementos de menú con AppendMenu, InsertMenu o InsertMenuItem.
3. Cuando quieres mostrar el menú emergente donde está el cursor del ratón, llama a
GetCursorPos para obtener las coordenadas de pantalla del cursor y entonces llama a
TrackPopupMenu para mostrar el menú. Cuando el usuario selecciona un elemento del
menú emergente, Windows manda el mensaje WM_COMMAND a tu procedimiento de
ventana como si fuera una selección de menú normal.

Nota: Precaución con dos molestos comportamientos cuando usas menues emergentes con un
icoo de bandeja:

1. Cuando el menú emergente es mostrado, si pulsas fuera del menú, el menú emergente
no desaparecerá inmediatamente, como debiera. Este comportamiento ocurre porque
la ventana que recibirá la notificación desde el menú emergente DEBERÁ ser la
ventana de primer plano. Así que la llamada a SetForegroundWindow corregirá esto.
2. Despues de llamar a SetForegroundWindow, encontrarás que la primera vez que se
muestra el menú emergente, este trabaja bien pero en las siguientes ocaciones, el
menú emergente se mostrará y cerrará inmediatamente. Este comportamiento es
"intencionado", par citar a MSDN. El cambio de tarea al programa que es dueño del
icono de bandeja en un futuro cercano es necesario. Puedes forzar este cambio de
tarea enviando cualquier mensage a la ventana del programa. Usa PostMessage, no
SendMessage!

Ejemplo:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\shell32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\shell32.lib

WM_SHELLNOTIFY equ WM_USER+5


IDI_TRAY equ 0
IDM_RESTORE equ 1000
IDM_EXIT equ 1010
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "TrayIconWinClass",0
AppName db "TrayIcon Demo",0
RestoreString db "&Restore",0
ExitString db "E&xit Program",0

.data?
hInstance dd ?
note NOTIFYICONDATA <>
hPopupMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEB
OX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL pt:POINT
.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
.elseif uMsg==WM_DESTROY
invoke DestroyMenu,hPopupMenu
invoke PostQuitMessage,NULL
.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif
.endif
.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke
TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Análisis:

El programa mostrará una simple ventana. Cuando aprietes el botón minimizar, se ocultará y
pondrá un icono en la bandeja del sistema. Cuando hagas una doble pulsación sobre el icono,
el programa se restablecerá y borrara el icono de la bandeja de sistema. Cuando pulses el
botón derecho sobre este, se mostrará un menú emergente. Puedes elegir si restaurar el
programa o salir de él.

.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

Cuando se crea la ventana principal, crea un menu emergente y le añade dos elementos.
AppendMenu tiene la siguiente sintaxis:

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD,


lpNewItem:DWORD

• hMenu es el manejador (handle) del menú al que quieres añadir el elemento


• uFlags le dice a Windows sobre el elemento de menú a añadir al menú ,tanto si es un
bitmap o una cadena o un elemento que se auto dibuja, activado, difuminado o
desactivado etc. Puedes conseguir la lista completa en la referencia de la api de win32.
En nuestro ejemplo, usamos MF_STRING, lo que significa que el elemento del menú
es una cadena.
• uIDNewItem es el ID (identidad) del menu item. Este es un valor definido por el usuario
que es usado para representar el elemento del menu.
• lpNewItem especifica el contenido del elemento, dependiendo de lo que especifiques
en el miembro uFlags. Como nosotros hemos especificado MF_STRING en uFlags,
lpNewItem deberá contener el puntero a la cadena a mostrar en el menú emergente.

Después de que es creado el menú emergente, la ventana principal espera pacientemente a


que el usuario pulse el botón de minimizar.
Cuando la ventana es minimizada, recibe el mensage WM_SIZE con el valor SIZE_MINIMIZED
en wParam.

.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
Aprovechamos esta oportunidad para rellenar la estructura NOTIFYICONDATA. IDI_TRAY es
una constante definida al principio del código fuente. Puedes ponerlo al valor que quieras. Esto
no importa porque tienes un único icoo de bandeja. Pero si vas a poner varios iconos en la
bandeja del sistema necesitarás un ID único para cada icono de bandeja. Especificamos todas
las banderas en el miembro uFlags porque especificamos un icono (NIF_ICON), especificamos
un mensaje común (NIF_MESSAGE) y especificamos el texto tooltip (NIF_TIP).
WM_SHELLNOTIFY es un mensaje común definido como WM_USER+5. El valor actual no es
importante siempre que sea único. Yo uso aquí el icono winlogo como icono de bandeja pero
puedes usar cualquier icono en tu programa. Cárgalo desde el recurso con LoadIcon y pon el
manejador (handle) devuelto en el miembro hIcon. Finalmente, rellenamos el szTip con el texto
que queremos que muestre el shell cuando el ratón está sobre el icono.

Ocultamos la ventana padre para dar la ilusión de parecer minimizar al icono de bandeja.

Después podemos llamar a Shell_NotifyIcon con el mensage NIM_ADD para añadir el icono a
la barra del sistema.

Ahora nuestra ventana principal está oculta y el icono está en la banedja del sistema. Si
mueves el ratón sobre él verás el tooltip que muestra el texto que ponemos dentro del miembro
szTip. Después, si haces una pulsación doble en el icono, la ventana principal reaparecerá y el
icono de bandeja se irá.

.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke
TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif

Cuando ocurre algún evento de ratón sobre el icono de bandeja, tu ventana recibe el mensaje
WM_SHELLNOTIFY que es el mensage común que especificas en el miembro
uCallbackMessage. Vuelve a llamar a esta recibiendo el mensaje, wParam contiene la ID del
icono de bandeja y lParam contiene el mensaje de ratón actual. En el código de arriba, primero
chequeamos si este mensaje viene del icono de bandeja que nos interesa. Si esto ocurre,
chequeamos el mensaje del ratón. Como estamos interesados únicamente en la pulsación
derecha o en la doble pulsación izquierda, procesamos sólo los mensages
WM_RBUTTONDOWN y WM_LBUTTONDBLCLK.

Si el mensaje de ratón es WM_RBUTTONDOWN llamamos a GetCursorPos para obtener las


coordenadas actuales en la pantalla del ratón. Cuando la función vuelve, la estructura POINT
es rellenada con las coordenadas en pantalla del ratón. Port coordenada de pantalla, quiero
decir la coordenada de toda la pantalla sin considerar a ninguno de los bordes de ninguna
ventana. Por ejemplo, si la resolución de la pantalla es 640*480, la esquina inferior derecha de
la pantalla es x==639 e y==479. Si quieres convertir la coordenada de pantalla a coordenada
de ventana usa la función ScreenToClient.

Como siempre, para nuestro propósitos, queremos mostrar el menú emergente en la posición
actual del ratón con la llamada a TrackPopupMenu y esto requiere coordenadas de pantalla,
podemos usar las coordenadas rellenadas directamente en GetCursorPos.
TrackPopupMenu tiene la siguiente sintaxis:
TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD,
y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD

• hMenu es el manejador (handle) del menú emergente a mostrar


• uFlags especifica las opciones de la funcion. Dice donde posicionar el menú relativo a
las coordenadas especificadas después y qué botón del ratón será usado para sacar el
menú. En nuestro ejemplo usamos TPM_RIGHTALIGN para posicionar el menú
emergente a la izquierda de las coordenadas.
• x e y especifican la localización del menú en las coordenadas de pantalla.
• nReserved deberá ser NULL
• hWnd es el manejador (handle) de la ventana que recibirá los mensajes desde el
menú.
• prcRect es el rectángulo en la pantalla donde es posible pulsar sin desconsiderar el
menú. Normalmente ponemos NULL aquí, de manera que cuando el usuario pulse en
cualquier sitio fuera del menú emergente, el menú no es desconsiderado.

Cuando el usuario hace una doble pulsación sobre el icono de la bandeja mandamos el
mensaje WM_COMMAND a nuestra ventana especificando IDM_RESTORE para emular la
selección del usuario del elemento Restore [Restaurar] del menú emergente, restaurando la
ventana principal y borrando el icono de la bandeja del sistema. Para poder estar preparados
para recibir el mensaje de doble pulsación, la ventana principal deberá tener el estilo
CS_DBLCLKS.

invoke Shell_NotifyIcon,NIM_DELETE,addr note


mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif

Cuando el usuario selecciona el elemento del menú Restore, borramos el icono de la bandeja
llamando otra vez a Shell_NotifyIcon, ahora especificamos como mensaje NIM_DELETE. Lo
siguiente es restaurar la ventana principal a su estado original. Si el usuario selecciona Exit en
el menu, también borramos el icono de la barra y destruimos la ventana principal llamando a
DestroyWindow.

Tutorial 24: Ganchos de Windows


En este tutorial aprenderemos sobre los ganchos [hooks] en Windows. Los ganchos de
Windows son muy poderosos. Con ellos, puedes "pescar" [pook] dentro de otros procesos e
incluso modificar su conducta.
Bajar el ejemplo aquí.

Teoría:
Los ganchos de Windows pueden considerarse uno de los rasgos más poderosos de Windows.
Con ellos podemos atrapar eventos que ocurrirán en tus procesos o en otros remotos. A través
del "enganche" [hooking], dices cosas a Windows sobre una función, una función filtro llamada
también procedimiento del gancho, que será llamado cada vez que ocurra un evento que te
interese. Hay dos tipos de ellos: ganchos locales y ganchos remotos.

• Ganchos Locales atraparán eventos que ocurrirán en tus propio proceso.


• Ganchos Remotos atraparán eventos que ocurrirán en otro(s) proceso(s). Hay dos
tipos de ganchos remotos
o específicos-a-un-hilo [thread-specific] atrapan eventos que ocurren en un hilo
específico de otro proceso. En pocas palabras, quieres observar un hilo
específico en un proceso específico.
o ancho-de-sistema [system-wide] atrapa todos los eventos de todos los hilos en
todos los procesos en el sistema.

Cuando instales ganchos, recuerda que ellos afectan el sistema de desempeño. Los ganchos
de ancho de sistema [system-wide] son los más notorios en este aspecto. Como TODOS los
eventos relacionados serán dirigidos a través de tu función filtro, tu sistema puede ralentizarse
un poco. Así que si quieres usar un gancho de ancho de sistema, deberías usarlo
juiciosamente y desactivarlo tan pronto ya no lo necesites. También tienes una probabilidad
más alta de quebrar [crashing] los otros procesos, ya que puedes mediar [meddle] con otros
procesos y si algo va mal en tu función filtro, se pueden derribar los otros procesos y lanzarlos
al olvido. Recuerda: el poder viene con responsabilidades.

Tienes que entender como trabaja un gancho antes de que puedas usarlo con eficiencia.
Cuando creas un gancho, Windows crea una estructura de datos en la memoria, que contiene
información sobre el gancho, y lo agrega a una lista enlazada de ganchos existentes. El gancho
nuevo es agregado en frente de los ganchos antiguos. Cuando un evento ocurre, si instalas un
gancho local, la función filtro en tu proceso es llamada de una manera directa. Pero si es un
gancho remoto, el sistema debe inyectar el código para el procedimiento del gancho dentro del
espacio de direcciones del(os) otro(s) proceso(s). Y el sistema puede hacer eso sólo si la
función reside en una DLL. De esta manera, si quieres usar un gancho remoto, tu
procedimiento de gancho debe residir en una DLL. Hay dos excepciones a esta regla: ganchos
de grabación diaria [journal record] y ganchos de ejecución diaria [journal playback]. El
procedimiento de gancho para estos dos ganchos debe residir en el hilo que instala los
ganchos. La razón por la que debe ser así, es porque ambos ganchos tienen que ver con la
intercepción de bajo-nivel de los eventos de entrada del hardware. Los eventos de entrada
deben ser grabados/ejecutados [recorded/playbacked] en el orden que aparecen. Si el código
de estos dos ganchos está en un DLL, los eventos de entrada pueden dispersarse entre varios
hilos y es imposible saber su orden. Así que la solución es: el procedimiento de gancho de esos
dos gancho deben estar solamente en un hilo, en el hilo que instala los ganchos.

Hay 14 tipos de ganchos:

• WH_CALLWNDPROC llamado cuando SendMessage es llamado


• WH_CALLWNDPROCRET llamado cuando regresa el mensaje SendMessage
• WH_GETMESSAGE llamado cuando GetMessage o PeekMessage son llamados
• WH_KEYBOARD llamado cuando GetMessage o PeekMessage regresan
WM_KEYUP o WM_KEYDOWN desde la cola de mensajes
• WH_MOUSE llamado cuando GetMessage o PeekMessage regresan un mensaje de
ratón desde la cola de mensajes
• WH_HARDWARE llamado cuando GetMessage o PeekMessage regresa algún
mensaje hardware que no está relacionado con el teclado ni con el ratón.
• WH_MSGFILTER llamado cuando una caja de diálogo, el menú o la barra de
pergamino [scrollbar] está apunto de [is about] procesar un mensaje. Este gancho es
local. Se usa específicamente para esos objetos que tienen sus propios bucles de
mensajes internos.
• WH_SYSMSGFILTER lo mismo que WH_MSGFILTER pero de ancho de sistema
[system-wide]
• WH_JOURNALRECORD llamado cuando Windows regresa mensajes desde
hardware input queue
• WH_JOURNALPLAYBACK llamado cuando es solicitado un evento del hardware
desde la cola de entrada del sistema.
• WH_SHELL llamado cuando algo interesante sobre el shell ocurre, tal como cuando la
barra de tareas necesita redibujar su botón.
• WH_CBT usado específicamente para entrenamiento [computer-based training (CBT)].
• WH_FOREGROUNDIDLE usado internamente por Windows. Poco usada para
aplicaciones generales
• WH_DEBUG usado para depurar el procedimiento de enganche

Ahora que sabemos algo de teoría, podemos ver ahora cómo instalar/desinstalar los ganchos.
Para instalar un gancho, llamas a SetWindowsHookEx que tiene la siguiente sintaxis:

SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD,


ThreadID:DWORD

• HookType es uno de los valores en la lista de arriba, e.g., WH_MOUSE,


WH_KEYBOARD
• pHookProc es la dirección del procedimiento de gancho que será llamada para
procesar los mensajes para el gancho específico. Si el gancho es remoto, debe residir
en una DLL. Si no, debe estar en tu proceso.
• hInstance es el manejador de instacia de la DLL en la cual reside el procedimiento de
gancho. Si el gancho es local, este valor debe ser NULL
• ThreadID es el ID del hilo para el cual quieres instalar el gancho que lo espíe. Este
parámetro es el que determina si el gancho es local o remoto. Si este parámetro es
NULL, Windows interpretará el gancho como un gancho remoto de ancho de sistema
[system-wide] que afecta todos los hilos del sistema. Si quieres especificar el ID de un
hilo en tu propio proceso, este hilo es local. Si especificas el ID del hilo de otro proceso,
el gancho es thread-specific remote one. Hay dos excepciones a esta regla:
WH_JOURNALRECORD y WH_JOURNALPLAYBACK siempre son ganchos de
ancho de sistema [system-wide] locales que no se requieren que estén en una DLL. Y
WH_SYSMSGFILTER siempre es un gancho remoto de ancho de sistema [system-
wide]. Realmente es idéntica al gancho WH_MSGFILTER con ThreadID==0.

Si la llamada tiene éxito, regresa el manejador de gancho en eax. Si no, regresa NULL. Debes
salvar el manejador del gancho para poder desinstalarlo después.

Puedes desinstalar un gancho llamando a UnhookWindowsHookEx que sólo acepta un


parámetro, el manejador del gancho que quieres desinstalar. Si la llamada tiene éxtito, regresa
un valor diferente a cero en eax. En caso contrario, regresa NULL.

Ahora que sabes cómo instalar/desinstalar ganchos, podemos examinar el procedimiento de


gancho.
El procedimiento de gancho será llamado cada vez que ocurre un evento asociado con el tipo
de gancho que haz instalado. Por ejemplo, si instalas el gancho WH_MOUSE, cuando ocurre
un evento de ratón, será llamado tu procedimiento de gancho. Independientemente del tipo de
gancho que instalaste, el procedimiento de gancho siempre tiene el siguiente prototipo:

HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD

o nCode especifica el código del gancho.


o wParam y lParam contienen información adicional sobre el evento

HookProc es realmente un comodín [placeholder] para el nombre de la función. Puedes


llamarla de la forma que quieras siempre que conserves el prototipo de arriba. La interpretación
de nCode, wParam y lParam depende del tipo de gancho que instales, así como del valor de
retorno del procedimiento de gancho. Por ejemplo:

WH_CALLWNDPROC
• nCode sólo puede ser HC_ACTION lo cual significa que hay un mensaje enviado a la
ventana
• wParam contiene el mensaje que está siendo enviado, si no lo tiene es cero
• lParam apunta a una estructura CWPSTRUCT
• return value: no se usa, regresa cero

WH_MOUSE

• nCode puede ser HC_ACTION o HC_NOREMOVE


• wParam contiene el mensaje del ratón
• lParam apunta a una estructura MOUSEHOOKSTRUCT
• return value: cero si el mensaje debería ser procesado. 1 si el mensaje deberúia ser
descartado.

La línea de abajo es: debes consultar tu referencia de la api de win32 para detalles sobre los
significados de los parámetros y regresar un valor al gancho que quieres instalar.

Ahora hay un poco de catch sobre el procedimiento de gancho. Recuerda que los ganchos
estan encadenados en una lista enlazada con el gancho instalado más recientemente colocado
en la cabeza de la lista. Cuando ocurre un evento, Windows llamará sólo al primer gancho de la
cadena. Es responsabilidad de tu procedimiento de gancho llamar al siguiente gancho en la
cadena. Tú no eliges llamar al siguiente gancho, pero mejor deberías saber qué estás
haciendo. Muchas veces, es una buena práctica llamar al siguiente procedimiento de manera
que otros ganchos puedan tener una impresión [shot] del evento. Puedes invocar al siguiente
gancho llamando a CallNextHookEx que tiene el siguiente prototipo:

CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD,


lParam:DWORD

• hHook es tu propio manejador de gancho. La función usa este manejador para


atravesar la lista enlazada y buscar el siguiente procedimiento de gancho que debería
ser llamado.
• nCode, wParam y lParam puedes pasar estos tres valores que recibes de Windows a
CallNextHookEx.

Una nota importante sobre los ganchos remotos: el procedimiento de gancho debe residir en
una DLL que será proyectada dentro de otro proceso. Cuando Windows proyecta la DLL dentro
de otros procesos, no proyectará la(s) sección(es) de datos(s) dentro de otros procesos. En
pocas palabras, todos los procesos comparten una copia sencilla del código, ¡pero ellos
tendrán su propia copia privada de la sección de datos de la DLL! Esto puede resultar una gran
sorpresa para el incuto. Puedes pensar que cuando almacenas un valor dentro de una variable
en la sección de datos de una DLL, ese valor será compartido entre todos los procesos que
cargan la DLL dentro de su espacio de direcciones. No es tan cierto. En una situación normal,
esta conducta es deseable ya que provee la ilusión de que cada proceso tiene su propia copia
de la DLL. Pero no cuando hay involucrados ganchos de Windows. Queremos que la DLL sea
idéntica en todos los procesos, incluyendo los datos. La solución: debes marcar la sección de
datos como compartida. Puedes hacer esto especificando el atributo de la(s) sección(es) en el
conmutador [switch] del enlazador [linker]. Para MASM, necesitas usar este conmutador:

/SECTION:<section name>, S

El nombre de la sección de datos inicializada es .data y el de la de los datos no-inicializados


es .bss. Por ejemplo, si quieres ensamblar una DLL que contiene un procedimiento de gancho
y quieres que la sección de datos no inicialoizados sea compartida entre procesos, debes usar
la siguiente línea:

link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........


El atributo S marca la sección como compartida.

Ejemplo:

Hay dos módulos: uno es el programa principal que hará la parte de GUI y otra es la DLL que
instalará/desinstalará el gancho.

;--------------------------------------------- Este es el código fuente del programa principal


-----------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG


wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG equ 101
IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001
IDC_WNDPROC equ 1002
IDC_HOOK equ 1004
IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD


LOCAL hLib:DWORD
LOCAL buffer[128]:byte
LOCAL buffer1[128]:byte
LOCAL rect:RECT
.if uMsg==WM_CLOSE
.if HookFlag==TRUE
invoke UninstallHook
.endif
invoke EndDialog,hDlg,NULL
.elseif uMsg==WM_INITDIALOG
invoke GetWindowRect,hDlg,addr rect
invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right,
rect.bottom, SWP_SHOWWINDOW
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
.elseif uMsg==WM_COMMAND
.if lParam!=0
mov eax,wParam
mov edx,eax
shr edx,16
.if dx==BN_CLICKED
.if ax==IDC_EXIT
invoke SendMessage,hDlg,WM_CLOSE,0,0
.else
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
.else
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
.endif
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgFunc endp
end start

;----------------------------------------------------- Este es el código fuente de la DLL


--------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
mov eax,TRUE
ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD


invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

InstallHook proc hwnd:DWORD


push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp

End DllEntry
;---------------------------------------------- Est es el makefile de la DLL
----------------------------------------------

NAME=mousehook
$(NAME).dll: $(NAME).obj
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS
/LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm

Análisis:

El ejemplo desplegará una caja de diálogo [dialog box] con tres controles de edición que serán
llenados con el nombre de la clase, el manejador [handle] de ventana y la dirección del
procedimiento de ventana asociada con la ventana bajo el cursor del ratón. Hay dos botones,
Hook (gancho) y Exit (Salir). Cuando presionas el botón Hook, el programa engancha la
entrada del ratón y el texto en el botón cambia a Unhook (desenganchar). Cuando mueves el
cursor del ratón sobre la ventana, la info acerca de esa ventana será desplegada en la ventana
principal del ejemplo. Cuando presionas el botón Unhook, el programa remueve el ganchjo del
ratón.

El programa principal usa una caja de diálogo [dialog box] como su ventana principal. Define
un mensaje hecho a la medida [custom message], WM_MOUSEHOOK que será usado entre el
programa principal y la DLL de gancho. Cuando el programa principal recibe este mensaje,
wParam contiene el manejador de la ventana sobre la cual está el cursor del ratón. Por
supuesto, este es un plan arbitrario. Yo decido enviar un manejador en wParam por razones de
simplicidad. Tú puedes escoger tu propio método de comunicación entre la ventana principal y
la DLL de gancho.

.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif

El programa mantiene una bandera, HookFlag, para monitorear el estado del gancho. Es
FALSE si no se instala el gancho y TRUE si el gancho es instalado.

Cuando el usuario presiona el botón Hook, el programa chequea si el gancho ya está instalado.
Si no lo está, se llama a la función InstallHook en la DLL de gancho para instalarlo. Nota que
pasamos el manejador de la ventana principal como parámetro de la función de manera que la
DLL de gancho pueda enviar mensajes WM_MOUSEHOOK a la ventana correcta,es decir la
tuya propia.

Cuando el programa es cargado, la DLL de gancho tabmbién es cargada. Realmente, las DLLs
son cargadas inmediatamente después de que el programa está en memoria. El punto de
entrada de la DLL es llamado incluso antes de que se ejecute la primera instrucción del
programa principal. Así que cuando el programa principal ejecuta la(s) DLL(s) es/son
inicializada(s). Ponemos el siguiente código en el punto de entrada de la DLL de gancho:

.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
El código salva el manejador de instancia de la DLL de gancho misma a una variable global
llamada hInstance para usar dentro de la función InstallHook. Ya que la función del punto de
entrada de la DLL es llamada antes de que sean llamadas otras funciones de la DLL, hInstance
siempre es válido. Ponemos hInstance en la sección .data, así que este valor es guardado en la
base de la sección por proceso [is kept on per-process basis]. Debido a que cuando el cursor
del ratón pasa sobre una ventana, la DLL de gancho es proyectada en el proceso. Imagina que
ya hay una DLL que ocupa las direcciones de la DLL de gancho que se intentó cargar, la DLL
de gancho debería ser re-proyectada a otra dirección. El valor de hInstance será actualizado
para las de las nuevas direcciones cargadas. Cuando el usuario presiona el botón Unhook y
luego el botón Hook, SetWindowsHookEx será llamada de nuevo. Sin embargo, esta vez, se
usará el nuevo espacio de direcciones cargado como el manejador de instacia lo cual será
erróneo porque en este proceso ejemplo la dirección de carga de la DLL de gancho no ha sido
cambiada. El gancho será local donde puedes enganchar sólo los eventos de ratón que ocurren
en tu propia ventana. Difícilmente deseable [hardly desirable].

InstallHook proc hwnd:DWORD


push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

La función InstallHook es muy simple. Salva el manejador de ventana pasado como su


parámetro a una variable global llamada hWnd para ser usada luego. Luego llama a
SetWindowsHookEx para instalar un gancho de ratón. El valor de retorno de
SetWindowsHookEx es almacenado en una variable global llamada hHook para usar con
UnhookWindowsHookEx.

Después de que es llamada SetWindowsHookEx, el gancho del ratón es funcional. Cada vez
que ocurre un evento de ratón del sistema, es llamada MouseProc (tu procedimiento de
ventana).

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD


invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

Lo primero que hace es llamar a CallNextHookEx para dar a otros ganchos el chance de
procesar el evento del ratón. Después de eso, llma a la función WindowFromPoint para
regresar el manejador de la ventana en la coordenada especificada del monitor. Nota que
usamos la estructura POINT en la estructura MOUSEHOOKSTRUCT apuntada por lParam
como la coordenada actual del ratón. Después de que enviamos el manejador de ventana a la
ventana principal a través de PostMessage con el mensaje WM_MOUSEHOOK. Algo que
deberías recordar es que: no deberías usar SendMessage dentro del procedimiento de gancho,
ya que puede causar estancamiento de mensajes. Es más recomendable PostMessage. La
estructura MOUSEHOOKSTRUCT se define abajo:

MOUSEHOOKSTRUCT STRUCT DWORD


pt POINT <>
hwnd DWORD ?
wHitTestCode DWORD ?
dwExtraInfo DWORD ?
MOUSEHOOKSTRUCT ENDS

• pt es la coordenada del monitor actual dónde está el cursor del ratón


• hwnd es el manejador de la ventana que recibirá el mensaje del ratón. Usualmente es
la ventana debajo del cursor del ratón pero no siempre. Si una ventana llama a
SetCapture, la entrada del ratón será desviada ahora más bien a la ventana. Debido a
esta razón, no uso el miembro hwnd de esta estructura sino que más bien elijo llamara
a WindowFromPoint.
• wHitTestCode especifica la prueba del valor hit-test. Este valor da más información
sobre la posición actual del cursor del ratón. Especifica en qué parte de la ventana está
el cursor del ratón. Para una lista completa, chequea en tu referencia de la api de
win32 el tópico sobre el mensaje WM_NCHITTEST.
• dwExtraInfo contiene información extra asociada con el mensaje. Normalmente, este
valor se establece llamando a mouse_event y regresado por la llamada a
GetMessageExtraInfo.

Cuando la ventana principal recibe un mensaje WM_MOUSEHOOK, usa el manejador de


ventana en wParam para regresar la información acerca de la ventana.

.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif

Para evitar parpadeos [flickers], chequeamos el texto que está todavía en los controles de
edición y el texto que se pondrá dentro de ellos para comprobar si on idénticos. Si lo son, los
saltamos.
Regresamos el nombre de la clase llamando a GetClassName, la dirección del procedimiento
de ventana llamando a GetClassLong con GCL_WNDPROC y luego los formateamos dentro de
cadenas y los ponemos dentro de los controles de edición apropiados.

invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

Cuando el usuario presiona el botón Unhook, el programa llama a la función UninstallHook en


la DLL de gancho. UninstallHook llama a UnhookWindowsHookEx. Después de eso, cambia el
texto del botón una vez más a "Hook", HookFlag a FALSE y se limpia el contenido de los
controles de edición.
Nota que el conmutador [switch] del enlazador en el makefile.

Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

Especifica a la sección .bss como una sección compartida para hacer que todos los procesos
compartan la sección de datos no inicializados de la DLL de gancho. Sin este conmutador
[switch], tu DLL de gancho no funcionará correctamente.

Tutorial 25: Simple Bitmap


En este tutorial, aprenderemos como usar bitmaps en nuestros programas. Para ser exactos,
Aprenderemos a desplegar in bitmap en el área cliente de una ventana. Baja el ejemplo.

Teoría
Los bitmaps pueden ser vistos como pinturas almacenadas en la computadora. Hay muchos
formatos de pinturas usados con los computadores pero Windows sólo soporta como nativos
los archivos Gráficos de Bitmap de Windows (.bmp). Los bitmaps a que me referiré en este
tutorial son archivos gráficos de Windows. La manera más fácil de usar un bitmap es emplearlo
como recurso. Hay dos maneras de hacer eso. Puedes incluir el bitmap en el archivo de
definición del recurso (.rc) de la siguiente manera:

#define IDB_MYBITMAP 100


IDB_MYBITMAP BITMAP "c:\project\example.bmp"

Este método usa una constante para representar el bitmap. La primera línea crea una
constante llamada IDB_MYBITMAP que tiene el valor de 100. Usaremos esta etiqueta para
referirnos al bitmap en el programa. La siguiente línea declara un recurso bitmap. Dice al
compilador de recursos dónde encontrar el actual archivo bmp.

El otro método usa un nombre para representar el bitmap de la siguiente manera:

MyBitMap BITMAP "c:\project\example.bmp"

Este método requiere que te refieras al bitmap en tu programa usando la cadena "MyBitMap"
en vez de un valor.

Cualquiera de estos métodos trabaja bien siempre que sepas cuál estás usando.

Ahora que ponemos el bitmap en el archivo de recurso, podemos continuar con los pasos a
seguir para desplegarlo en el área cliente de nuestra ventana.

1. Llamar a LoadBitmap para obtener el manejador [handle] al bitmap. LoadBitmap tiene


la siguiente definición:

LoadBitmap proto hInstance:HINSTANCE,


lpBitmapName:LPSTR

Esta función regresa un manejador al bitmap. hInstance es el manejador de instancia


de nuestro programa. lpBitmapName es un puntero a la cadena con el nombre del
bitmap (en caso de que uses el segundo método para referirte al bitmap). Si usas una
constante para referirte al bitmap (como IDB_MYBITMAP), puedes poner este valor
aquí. (En el ejemplo de arriba sería 100). Un pequeño ejemplo, en orden:

Primer Método:

.386
.model flat, stdcall
................
.const
IDB_MYBITMAP equ 100
...............
.data?
hInstance dd ?
..............
.code
.............
invoke GetModuleHandle,NULL
mov hInstance,eax
............
invoke LoadBitmap,hInstance,IDB_MYBITMAP
...........

Segundo Método:

.386
.model flat, stdcall
................
.data
BitmapName db "MyBitMap",0
...............
.data?
hInstance dd ?
..............
.code
.............
invoke GetModuleHandle,NULL
mov hInstance,eax
............
invoke LoadBitmap,hInstance,addr BitmapName
...........

1. Obtener un manejador al contexto del dispositivo (DC). Puedes obtener este manejador
llamando a BeginPaint en respuesta al mensaje WM_PAINT o llamando a GetDC en
algún lado.
2. Crear un contexto de dispositivo de memoria que tenga el mismo atributo que el
contexto de dispositivo que obtuvimos. La idea aquí es crear un tipo de superficie
oculta para dibujo [hidden drawing surface] sobre la cual podamos dibujar el bitmap.
Cuando terminamos con al operación, copiamos el contenido de la hidden drawing
surface al contexto de dispositivo actual en una llamada a función. Es un ejemplo de
técnica de doble-buffer usada para desplegar con rapidez imágenes sobre la pantalla.
Puedes crear esta superficie oculta para dibujo [hidden drawing surface] llamando a
CreateCompatibleDC.

CreateCompatibleDC proto hdc:HDC

Si esta función tiene éxito, regresa el manejador del contexto de dispositivo de


memoria en eax. hdc es el manejador al contexto de dispositivo con que quieres que el
DC de memoria sea compatible.

1. Ahora que obtuviste una superficie oculta para dibujo, puedes dibujar sobre ella
seleccionando el bitmap dentro de ella. Esto se hace llamando a SelectObject con el
manejador al DC de memoria como primer parámetro y el manejador al bitmap como
segundo parámetro. SelectObject tiene la siguiente definición:

SelectObject proto hdc:HDC, hGdiObject:DWORD

1. El bitmap ahora es dibujado sobre el contexto de dispositivo de memoria. Todo lo que


necesitamos hacer aquí es copiarlo al dispositivo de despliegue actual, realnmente el
verdadero contexto de dispositivo. Hay varias funciones que pueden realizar esta
operación tales como BitBlt y StretchBlt. BitBlt copia el contenido de un DC a otro de
manera que es más rápido mientras que StretchBlt pueda estrechra o comprimir el
bitmap para fijar el área de salida. Usaremos aquí BitBlt por simplicidad. BitBlt tiene la
siguiente definición:

BitBlt proto hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD,


nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD,
nySrc:DWORD, dwROP:DWORD

hdcDest es el manejador del contexto de dispositivo que sirve como destino de la


operación de transferencia del bitmap
nxDest, nyDest son las coordenadas de la esquina superior izquierda del área de
salida
nWidth, nHeight son el ancho y la altura del área de salida
hdcSrc es el manejador del contexto de dispositivo que sirve como fuente de la
operación de transferencia del bitmap
nxSrc, nySrc son las coordenadas de la esquina superior izquierda del rectángulo de
origen.
dwROP es el código de la operación-raster (de ahí las siglas ROP) que gobierna cómo
combinar los datos de los colores del bitmap con los datos de los colores existentes en
el área de salida para alcanzar el resultado final. Muchas veces, sólo quieres sobre-
escribir los datos de colores existentes con los nuevos.

1. Cuando ya hayas hecho lo que ibas a hacer con el bitmap, suprímelo con una llamada
a la API DeleteObject.

¡Eso es todo! Para recapitular, necesitas poner el bitmap dentro del guión de recursos. Luego
cárgalo desde el recurso con LoadBitmap. Obtendrás un manejador al bitmap. Luego obtienes
el manejador al contexto de dispositivo del área sobre el cual quieres pintar el bitmap. Luego
creas un contexto de dispositivo de memoria compatible con el contexto de dispositivo que
obtuviste. Selecciona el bitmap dentro del DC de memoria y luego copia el contenido de la
memoria del DC al verdadero DC.

Código de Ejemplo:
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD


IDB_MAIN equ 1

.data
ClassName db "SimpleWin32ASMBitmapClass",0
AppName db "Win32ASM Simple Bitmap Example",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hBitmap dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemDC:HDC
LOCAL rect:RECT
.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;---------------------------------------------------------------------
; El guión de recursos
;---------------------------------------------------------------------
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"

Análisis:
No hay mucho que analizar en este tutorial ;)

#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"

Definir una constante llamada IDB_MAIN, asignando 1 como su valor. Y luego usar esa
constante como el identificador del recurso del bitmap. El archivo bitmap a ser incluido
en el recuros "tweety78.bmp" que reside en la misma carpeta como guión de recursos.

.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax

En respuesta a WM_CREATE, llamamos a LoadBitmap para cargar al bitmap desde el


recurso, pasando el identificador del recurso del bitmap como segundo parámetro a la
API. Obtenemos el manejador al bitmap cuando regresa la función.
Ahora que el bitmap está cargado, podemos pintarlo en el área cliente de nuestra
ventana principal.

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps

Elegimos pintar el bitmap en respuesta al mensaje WM_PAINT. Primero llamamos a BeginPaint


para obtener el manejador al contexto de dispositivo. Luego creamos un DC de memoria
compatible con CreateCompatibleDC. Lo siguiente es seleccionar el bitmap dentro del DC de
memoria con SelectObject. Determinamos la dimensión del área cliente con GetClientRect.
Ahora podemos desplegar el bitmap en el área cliente llamando a BitBlt que copia el bitmap
desde el DC de memoria al DC real. Cuando la imagen ya esté hecha, ya no tenemos
necesidad del DC de memoria, así que lo suprimimos con DeleteDC. Terminamos la sección de
pintura con EndPaint.

.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL

Cuando no necesitemos ya el bitmap, lo suprimimos [delete] con DeleteObject

Tutorial 26: Splash Screen


Ahora que sabemos cómo usar un bitmap, podemos progresar hacia un uso más creativo de él.
Splash screen. Baja el ejemplo.

Teoría
Una splash screen [pantalla de salpicadura] es una ventana que no tiene barra de título, ni caja
de menú de sistema, ni borde, que despliega un bitmap por un lapso de tiempo y luego
desaparece automáticamente. Usualmente es usada durante el inicio de un programa, para
desplegar el logo del programa o distraer la atención del usuario mientras el programa hace
alguna inicialización extendida. En este tutorial implementaremos un splash screen.

El primer paso es incluir el bitmap en el archivo de recursos. Sin embargo, si piensas un poco
en esto, verás que hay un consumo precioso de memoria cuando se carga un bitmap que será
usado sólo una vez y se mantiene en la memoria hasta que el programa es cerrado. Una mejor
solución es crear una DLL de *recursos* que contenga el bitmap y que tenga el único propósito
de desplegar la splash screen. De esta manera, puedes cargar la DLL cuando quieras
desplegar la splash screen y descargarla cuando ya no sea necesaria. Así que tendremos dos
módulos: El programa principal y la DLL con el splash. Pondremos el bitmap dentro de los
recursos de la DLL.

El esquema general es como sigue:

1. Poner el bitmap dentro de la DLL como un recurso bitmap


2. El programa principal llama a LoadLibrary para cargar la dll en memoria
3. Se llama a la función del punto de entrada de la DLL. Se creará un temporizador y se
establecerá cuánto tiempo permanecerá desplegada la splash screen. Luego se
registrará y creará una ventana sin encabezado y sin borde, y desplegará el bitmap en
el área cliente.
4. Cuando se cumple el lapso de tiempo establecido, la splash screen es removida de la
pantalla y el control es regresado a la ventana principal
5. El programa principal llama a FreeLibrary para descargar la DLL de la memoria y luego
se dirigirá a realizar la tarea que se supone que hará.

Examinaremos los mecanismos en detalle.

Cargar/Descargar una DLL

Puedes cargar dinámicamente una DLL con la función LoadLibrary que tiene la siguiente
sintaxis:

LoadLibrary proto lpDLLName:DWORD

Toma sólo un parámetro: la dirección del nombre de la DLL que quieres cargar en memoria. Ssi
la llamada es satisfactoria, regresa el manejador del módulo de la DLL sino regresa NULL.

Para descargar una DLL, llama a FreeLibrary:

FreeLibrary proto hLib:DWORD

Toma un parámetro: el manejador del módulo de la DLL que quieras descargar. Normalmente,
obtienes el manejador a partir de LoadLibrary

Cómo usar un temporizador [timer]

Primero, debes crear un temporizador con SetTimer:

SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

hWnd es el manejador de una ventana que recibirá el mensaje de notificación del


temporizador. Este parámetro puede ser NULL para especificar que no hay ventana asociada
con el temporizador.
TimerID es un valor definido por el usuario empleado para el ID del temporizador.
uElapse es el valor del lapso de tiempo en milisegundos.
lpTimerFunc Es la dirección de una función que procesará los mensajes de notificación del
temporizador. Si pasas NULL, los mensajes del temporizador serán enviados a la ventana
especificada por el parámetro hWnd.

SetTimer regresa el ID del temporizador si tiene éxito. De otra manera regresa NULL. Así que
es mejor usar el ID del temporizador de 0.

Puedes crear un temporizador de dos maneras :

• Si tienes una ventana y quieres los mensajes de notificación del temporizador para ir a
esa ventana, debes pasar todos los cuatro parámetros a SetTimer (el valor de
lpTimerFunc debe ser NULL).
• Si no quieres una ventana o si no quieres procesar los mensajes del temporizador en el
procedimiento de ventana, debes pasar NULL a la función en lugar de un manejador de
ventana. Debes especificar el valor de la dirección del temporizador que procesará los
mensajes del temporizador.
Usaremos la primera aproximación en este ejemplo.

Cuando se cumple el período de tiempo, se envía el mensaje WM_TIMER a la ventana


asociada con el temporizador. Por ejemplo, si especificas un uElapse de 1000, tu ventana
recibirá un mensaje WM_TIMER cada segundo.

Cuando ya no necesites el temporizador, lo destruyes con KillTimer:

KillTimer proto hWnd:DWORD, TimerID:DWORD

Ejemplo:
;---------------------------------------------------------------------
--
; El programa principal
;---------------------------------------------------------------------
--
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SplashDemoWinClass",0
AppName db "Splash Screen Example",0
Libname db "splash.dll",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
invoke LoadLibrary,addr Libname
.if eax!=NULL
invoke FreeLibrary,eax
.endif
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;--------------------------------------------------------------------
; La DLL Bitmap
;--------------------------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib
.data
BitmapName db "MySplashBMP",0
ClassName db "SplashWndClass",0
hBitMap dd 0
TimerID dd 0

.data
hInstance dd ?
.code

DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD


.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
push hInst
pop hInstance
call ShowBitMap
.endif
mov eax,TRUE
ret
DllEntry Endp
ShowBitMap proc
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,0
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL
mov hwnd,eax
INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
.WHILE TRUE
INVOKE GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
INVOKE TranslateMessage, ADDR msg
INVOKE DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
ShowBitMap endp
WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemoryDC:HDC
LOCAL hOldBmp:DWORD
LOCAL bitmap:BITMAP
LOCAL DlgHeight:DWORD
LOCAL DlgWidth:DWORD
LOCAL DlgRect:RECT
LOCAL DesktopRect:RECT

.if uMsg==WM_DESTROY
.if hBitMap!=0
invoke DeleteObject,hBitMap
.endif
invoke PostQuitMessage,NULL
.elseif uMsg==WM_CREATE
invoke GetWindowRect,hWnd,addr DlgRect
invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow
invoke LoadBitmap,hInstance,addr BitmapName
mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax
.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\

hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

End DllEntry

Análisis:
Primero examinaremos el código en la ventana principal.

invoke LoadLibrary,addr Libname


.if eax!=NULL
invoke FreeLibrary,eax
.endif

Llamamos a LoadLibrary para cargar la DLL llamada "splash.dll". Y después de eso,


descargarla de la memoria con FreeLibrary. LoadLibrary no regresará hasta que la DLL haya
terminado con su inicialización.

Eso es todo lo que hace le programa principal. La parte interesante está en la DLL.

.if reason==DLL_PROCESS_ATTACH ; Cuando la dll es cargada


push hInst
pop hInstance
call ShowBitMap

Cuando la DLL es cargada, Windows llama a su punto de entrada con la bandera


DLL_PROCESS_ATTACH. Aprovechamos esta oportunidad para desplegar la splash screen.
Primero almacenamos el manejador de instacia de la DLL para usarla luego. Luego llamamos a
una función llamada ShowBitMap para realizar la tarea. ShowBitMap registra una clase de
ventana, crea una ventana e introduce el bucle de mensaje como es usual. La parte interesante
está en la llamada a CreateWindowEx:

INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\


WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL

Nota que el estilo de la ventana es sólo WS_POPUP lo cual hará que la ventana no tenga
bordes ni tampoco encabezamiento [caption]. También limitamos el ancho y la altura de la
ventana a 250x250 pixeles.

Ahora cuando la ventana es creada durante en el manejador del WM_CREATE, movemos la


ventana al centro del monitor con el siguiente código.

invoke GetWindowRect,hWnd,addr DlgRect


invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow

Regresan las siguientes dimensiones del escritorio y la ventana luego calcula la coordenada
apropiada de la esquina izquierda superior de la ventana para convertirse en centro.

invoke LoadBitmap,hInstance,addr BitmapName


mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax

Lo siguiente es cargar el bitmap desde el recurso con LoadBitmap y crea un temporizador con
el ID de temporizador de 1 y el intervalo de tiempo de 2 segundos. El temporizador enviará
mensajes WM_TIMER a la ventana cada 2 segundos.

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\

hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps

Cuando la ventana recibe el mensaje WM_PAINT, crea un DC de memoria, selecciona el


bitmap dentro del DC de memoria, obtiene el tamaño del bitmap con GetObject luego pone el
bitmap en la ventana llamando a StretchBlt que se ejecuta como BitBlt pero puede estrechar o
comprimir el bitmap a la dimensión deseada. En este caso, queremos que el bitmap se fije
dentro de la ventana así que usamos StretchBlt en vez de BitBlt. Después de eso, borramos el
DC de memoria.

.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd

Sería frustrante para el usuario tener que esperar hasta que la splash screen desaparezca.
Podemos suministrarle al usuario un elección. Cuando haga click sobre la splash screen,
desaparecerá. Por eso es que necesitamos procesar el mensaje WM_LBUTTONDOWN en la
DLL. Durante la recepción del mensaje, la ventana es destruida por la llamada a
DestroyWindow.

.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID

Si el usuario elige esperar, la splash screen desaparecerá cuando el lapso de tiempo


especificado se haya cumplido (en nuestro ejemplo, es 2 segundos). Podemos hacer esto
procesando el mensaje WM_TIMER. Al recibir este mensaje, cerramos la ventana enviando el
mensaje WM_LBUTTONDOWN a la ventana. Esto es para evitar duplicación de código. No
tenemos que emplear luego el temporizador, así que lo destruimos llamando a KillTimer.

Cuando la ventana es cerrada, la DLL regresará el control al programa principal.


Tutorial 27: Control 'Tooltip'
Aprenderemos sobre el control tooltip [sugerencia]: ¿Qué es, y cómo
crearlo y usarlo. Baja el ejemplo.

Teoría:

Un 'tooltip' [una sugerencia] es una pequeña ventana rectangular que


se despliega cuando el ratón pasa sobre algún área específica. Una
ventana 'tooltip' contiene algún texto que el programador quiere que
sea desplegado. En este aspecto, un 'tooltip' tiene el mismo rol que
una ventana de estado pero desaparece cuando el usuario mueve el
ratón o hace click lejos del área diseñada. Probablemente estarás
familiarizado con los 'tooltips' asociados a los botones de barras de
herramientas. Esos 'tooltips' son funcionalidades convenientes
suministradas por las barras de herramientas. Si quieres 'tooltips'
para otras ventanas/controles, necesitas crear tu propio control
'tooltip'.

Ahora que sabes qué es un 'tooltip', veamos cómo podemos crearlo y


usarlo. Los pasos están esbozados aquí:

1. Crear un control 'tooltip' con CreateWindowEx


2. Definir una región que el control 'tooltip' monitoreará para el
movimiento del puntero del ratón.
3. Subsumir la región del control 'tooltip'
4. Transmitir los mensajes del ratón del área subsumida al control
'tooltip' (este paso puede ocurrir antes, dependiendo del
método usado para to transmitir los mensajes)

Ahora examinaremos cada paso en detalle.

Creación del Tooltip

El control 'tooltip' es un control común. Por eso, necesitas llamar


InitCommonControls en algún lugar de tu código fuente de manera
que MASM implícitamente enlace tu programa a comctl32.dll. Creas
un control 'tooltip' con CreateWindowEx. El escenario típico sería más
o menos este:

.data
TooltipClassName db "Tooltips_class32",0
.code
.....
invoke InitCommonControls
invoke CreateWindowEx, NULL, addr TooltipClassName, NULL,
TIS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance,
NULL

Nota el estilo de ventana: TIS_ALWAYSTIP. Este estilo especifica que


el 'tooltip' será mostrado cuando el puntero del ratón esté sobre el
área designada independientemente del esrtdo de la ventana que
contiene el área. Para ponerlo con mayor simplicidad, si usas esta
bandera, cuando el puntero del ratón pase sobre el área que
registraste para el control 'tooltip', la ventana 'tooltip' aparecerá
incluso si la ventana debajo del puntero del ratón está activa.

No tienes que incluir los estilos WS_POPUP y


WS_EX_TOOLWINDOW en CreateWindowEx porque el
procedimiento de ventana del control 'tooltip' la agrega
automáticamente. Tampoco necesitas especificar, la altura y el ancho
de la ventana del control 'tooltip': el control 'tooltip' los ajustará
automáticamente para fijar el texto del 'tooltip' que será desplegado,
así que suministramos CW_USEDEFAULT en todos los cuatro
parámetros. Los restantes parámetros no son tan relevantes.

Especificando la herramienta

El control 'tooltip' es creado pero no mostrado de inmediato.


Queremos que la ventana 'tooltip' se muestre cuando el puntero del
ratón pase sobre algún área. Ahora es el momento de especificar ese
área. Llamamos a tal área "herramienta" [tool]. Una herramienta es
un área rectangular sobre el área cliente de una ventana que el
control tooltip monitoreará para el puntero del ratón. Si el puntero del
ratón pasa sobre la herramienta, aparecerá la ventana 'tooltip'. El
área rectangular puede cubrir todo el área cliente o sólo parte de ella.
Así que podemos dividir la herramienta en dos tipos: una
implementada como una ventana y otra implementada como un área
rectangular en el área cliente de alguna ventana. Ambas tienen sus
usos. La herramienta que cubre todo el área cliente de una ventana
es usada con mucha frecuencia con controles tales como botones,
controles de edición, etc. No necesirtas especificar la coordenada y
las dimensiones de la herramienta: se asume que estará sobre todo el
área cliente de la ventana. La herramienta que es implementada
como un área rectangular sobre el área cliente es útil cuando quieres
dividir el área cliente de una ventana en varias regiones sin usar
ventanas hijas. Con este tipo de herramienta, necesitas especificar la
coordenada de la esquina superior izquierda y el ancho y la altura de
la herramienta. Con este tipo de herramienta, no necesitas especificar
la esquina superior izquierda y el ancho y la altura de la herramienta.

Especificas la herramienta con la estructura TOOLINFO que tiene la


siguiente definición:

TOOLINFO STRUCT
cbSize DWORD ?
uFlags DWORD ?
hWnd DWORD ?
uId DWORD ?
rect RECT <>
hInst DWORD ?
lpszText DWORD ?
lParam LPARAM ?
TOOLINFO ENDS

Nombre
Explicación
del Campo
El tamaño de la estructura TOOLINFO. DEBES llenar este
miembro. Windows no monitoreará el error si este campo no
cbSize
es llenado debidamente y recibirás extraños e impredecibles
resultados.
Los bits de bandera que especifican . Este valor puede ser
una combinación de las siguientes banderas:

• TTF_IDISHWND "ID is hWnd". Si especificas esta


bandera, significa que quieres usar una herramienta
que cubre todo el área cliente de una ventana (el
primer tipo de herramienta de arriba). Si usas esta
bandera, debes llenar el miembro uId de esta
estructura con el manejador de la ventana que quieres
usar. Si no especificas esta bandera, significa que
quieres el segundo tipo de herramienta, la que es
implementada como área rectangular sobre la ventana
cliente. En ese caso, necesitas llenar el miembro rect
con la dimensión del rectángulo.
• TTF_CENTERTIP Normalmente la ventana 'tooltip' a la
derecha y abajo del puntero del ratón. Si especificas
esta bandera, la ventana 'tooltip' aparecerá
uFlags
directamente debajo de la herramienta y será centrada
independientemente de la posición del puntero del
ratón.
• TTF_RTLREADING Pudes olvidar esta bendera si tu
programa no está diseñado específicamente para
sistemas arábicos o hebreos. Esta bandera despliega el
texto de la sugerencia [tooltip] de derecha a izquierda.
No trabaja bajo otros sistemas.

• TTF_SUBCLASS Si usas esta bandera, significa que le


dices al control 'tooltip' que subclasifique la ventana
que la herramienta sobre la cuál está la herramienta de
manera que el control 'tooltip' pueda interceptar
mensajes de ratón que son enviados a la ventana. Esta
manera es muy conveniente. Si no usas esta bandera,
tienes que trabajar más para transmitir los mensajes
del ratón al control 'tooltip'.
hWnd Manejador a la ventana que contiene la herramienrta. Si
especificas la bendera TTF_IDISHWND, este campo es
ignorado ya que Windows usará el valor en el miembro uId
como manejador de ventana. Necesitas llenar este campo si:

• No usas la bendera TTF_IDISHWND (en otras palabras,


usas una herramienta rectangular)

• Especificas el valor LPSTR_TEXTCALLBACK en el


mimebro lpszText. Este valor le dice al control 'tooltip'
que, cuando necesita desplegar la ventana 'tooltip',
debe consultar a la ventana que contiene la
herramienta para el texto a ser desplegado. Si quieres
cambiar tu texto de sugerencia [tooltip]
dinámicamente, deberías especificar el valor
LPSTR_TEXTCALLBACK en el miembro lpszText. El
control 'tooltip' enviará el mensaje de notificación
TTN_NEEDTEXT a la ventana identificada por el
manejador en el campo hWnd.
El valor en este campo puede tener dos significados,
dependiendo si el miembro uFlags contiene la bandera
TTF_IDISHWND.

• ID de la herramienta definido para la aplicación si la


bandera TTF_IDISHWND no está especificada. Puesto
que esto significa que usas una herramienta que cubre
sólo una parte del área cliente, es lógico que puedas
tener muchas de tales herramientas sobre el mismo
área cliente (sin superponerse). El control 'tooltip'
necesita una manera de diferenciarlas. En este caso, el
manejador de ventana en el miembro hWnd no es
suficiente ya que todas las herramientas está sobre la
misma ventana. Los IDs definidos para la aplicación son
entonces necesarios. Los IDs pueden ser de cualquier
uId valor con tal de que su valor sea único entre ellos.

• El manejador de la ventana cuya totalidad del área


cliente es usada como herramienta si la bandera
TTF_IDISHWND es especificada. Te puedes preguntar
por qué este campo se usa para almacenar el
manejador de ventana en vez del campo hWnd de
arriba. La respuesta es: el miembro hWnd ya puede ser
llenado si se especifica el valor
LPSTR_TEXTCALLBACK en el miembro lpszText y la
ventana que es responsable de suministrar el texto del
'tooltip' y la ventana que contiene la herramienta NO
no puede ser la misma (Puedes diseñar tu programa de
manera que una ventana pueda servir para ambos roles
pero esto restringe bastante. En este caso, Microsoft te
da mayor libertad. Ánimo.)
Una estructura RECT que especifica la dimensión de la
herramienta. Esta estructura define un rectángulo relativo a
la esquina izquierda superior del área cliente de la ventana
especificada dpor el miembro hWnd. En pocas palabras,
rect debes llenar esta estructura si quieres especificar una
herramienta que cubra sólo una parte del área cliente. El
control 'tooltip' ignorará este miembro si especificas la
bandera TTF_IDISHWND (eliges usar una herramienta que
cubra todo el área cliente)
El manejador de la instancia que contiene el recurso de
cadena que será usado como texto de sugerencia [tooltip
text] si el valor en el miembro lpszText especifica el
identificador del recurso de cadena. Esto puede sonar
hInst
confuso. Lee primero la explicación del miembro lpszText y
entenderás para qué es usado este campo. El control 'tooltip'
ignorará este campo si el campo lpszText no contiene un
identificador del recursos de cadena.
lpszText Este campo puede tener diversos valores:

• Si especificas el valor LPSTR_TEXTCALLBACK en este


campo, el control tooltip enviará el mensaje de
notificación TTN_NEEDTEXT a la ventana identificada
por el manejador en el campo hWnd para la cadena de
texto a ser desplegada en la ventana tooltip. Este es el
método más dinámico de actualización del texto de
sugerencia: puedes cambiar el texto de sugerencia
cada vez que la ventana 'tooltip' sea desplegada.
• Si especificas un identificador de recursos de cadena en
este campo, cuando el control 'tooltip' necesite
desplegar el texto de sugerencia en la ventana 'tooltip',
busca por le cadena en la tabla de cadenas de la
instancia especificada por el mimbro hInst. El control
'tooltip' identifica un identificador de cadena de recurso
chequeando la palabra alta de este campo. Ya que un
identificador de cadena es un valor de 16-bits, la
palabra alta de este campo siempre será cero. Este
método es útil si planificas portear [to port] tu
programa a otros lenguajes. Puesto que el recurso de
cadena es definido en el guión de recursos, no
necesitas modificar el código fuente. Sólo tienes que
modificar las tablas de cadenas y los textos de
sugerencia cambiarán sin el riesgo de introducir errores
en tu programa

• Si el valor de este campo no es


LPSTR_TEXTCALLBACK y la palabra alta no es cero, el
control 'tooltip' interpreta el valor como el puntero a
una cadena de texto que será usada como texto de
sugerencia. Este método es el más fácil de usar pero el
menos flexible.

Para recapitular, necesitas llenar la estructura TOOLINFO antes de


subsumirla al control 'tooltip'. Esta estructura describe las
características de la herramienta que deseas.

Registrar la herramienta [tool] con el control 'tooltip'

Después de que llenes la estructura TOOLINFO, debes subsumirla al


control 'tooltip'. Un control 'tooltip' puede servirse de muchas
herramientas, por lo que es innecesario crear más de un control
'tooltip' para una ventana. Para registrar una herramienta con un
control 'tooltip', envías el mensaje TTM_ADDTOOL al control 'tooltip'.
El valor de wParam no se usa y lParam debe contener la dirección
de la estructura TOOLINFO que quieres registrar.

.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti

SendMessage para este mensaje regresará TRUE si la herramienta


es satisfactoriamente registrada con el control 'tooltip', FALSE si ese
no es el caso.
Puedes cancelar el registro de la herramienta enviando el mensaje
TTM_DELTOOL al control 'tooltip'.

Transmitiendo los Mensajes del Ratón al Control


'Tooltip'

Cuando el paso de arriba es completado, el control 'tooltip' sabe qué


área debería monitorearse para los mensajes del ratón y qué texto
debería desplegar el conrtrol tooltip. Lo único que falta es el *gatillo*
[trigger] para la acción. Piensa en ello: el área especificada por la
herramienta está sobre el área cliente de la otra ventana. ¿Cómo
puede interceptar el control 'tooltip' los mensajes del ratón para esa
ventana? Necesita hacerlo así con el fin de que pueda medir la
cantidad de tiempo que el puntero del ratón se halla sobre un punto
en la herramienta de manera que cuando caduque el lapso de tiempo
especificado, el control 'tooltip' muestre la ventana con la sugerencia
[tooltip window]. Hay dos métodos para alcanzar esta meta, una
requiere la cooperación de la ventana que contiene la herramienta y
la otra sin la cooperación sobre la parte de esa ventana.
• La ventana que contiene la herramientea debe transmitir los
mensajes del ratón al control 'tooltip' enviando mensajes
TTM_RELAYEVENT al control. El valor lParam debe contener
la dirección de una estructura MSG que especifica el mensaje a
ser transmitido al control 'tooltip'. Un control 'tooltip' procesa
sólo los siguientes mensajes del ratón:
o WM_LBUTTONDOWN
o WM_MOUSEMOVE
o WM_LBUTTONUP
o WM_RBUTTONDOWN
o WM_MBUTTONDOWN
o WM_RBUTTONUP
o WM_MBUTTONUP

Todos los mensajes son ignorados. Así que en el procedimiento


de ventana de la ventana que contiene la herramienta, debe
haber un conmutador [switch] que haga algo como esto:

WndProc proc hWnd:DWORD, uMsg:DWORD,


wParam:DWORD, lParam:DWORD
.......
if uMsg==WM_CREATE
.............
elseif uMsg==WM_LBUTTONDOWN ||
uMsg==WM_MOUSEMOVE || uMsg==WM_LBUTTONUP ||
uMsg==WM_RBUTTONDOWN ||
uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP
|| uMsg==WM_MBUTTONUP
invoke SendMessage, hwndTooltip,
TTM_RELAYEVENT, NULL, addr msg
..........

• Puedes especificar la bandera TTF_SUBCLASS en el miembro


uFlags de la estructura TOOLINFO. Esta bandera le dice al
cintrol 'tooltip' que subclasifique la ventana que contiene la
herramienta de manera que pueda interceptar los mensajes del
ratón sin la cooperación de la ventana. Este método es más
fácil de usar ya que no requiere más código que la
especificación de la bandera TTF_SUBCLASS y el control
'tooltip' maneja él mismo todas las intercepciones de mensajes.

Eso es. En este paso, tu control tooltip es completamente funcional.


Hay varios mensajes útiles relativos al 'tooltip' sobre los cuáles
deberías tener conocimiento.

• TTM_ACTIVATE. Si quieres deshabilitar/habilitar el control


'tooltip' dinámicamente, este mensaje es para tí. Si el valor
wParam es TRUE, es habilitado el control 'tooltip'. Si el valor
wParam es FALSE, el control 'tooltip' es deshabilitado. Un
control 'tooltip' es habilitado cuando es creado así que no
necesitas enviar este mensaje para activarlo.
• TTM_GETTOOLINFO and TTM_SETTOOLINFO. Si quieres
obtener/cambiar los valores en la estructura TOOLINFO después
de que fue subsumida al control 'tooltip', usa estos mensajes.
Necesitas especificar esta herramienta con los valores correctos
de uId y hWnd. Si sólo quieres cambiar el miembro rect, usa el
mensaje TTM_NEWTOOLRECT. Si sólo quieres cambiar el
texto, usa TTM_UPDATETIPTEXT.
• TTM_SETDELAYTIME. Con este mensaje, puedes especificar el
tiempo de retardo que usa el control 'tooltip' cuando está
desplegando el texto de sugerencia y mucho más.

Ejemplo:

El siguiente ejemplo es una simple caja de diálogo con dos botones. El


área cliente de la caja de diálogo es dividida en 4 áreas: superior
izquierda, superior derecha, baja izquierda, baja derecha. Cada área
es especificada como una herramienta con sus propios textos de
sugerencia [tooltip text]. Los dos botones también tienen sus propios
textos de sugerencia.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr
DlgProc,NULL
invoke ExitProcess,eax

DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD


LOCAL ti:TOOLINFO
LOCAL id:DWORD
LOCAL rect:RECT
.if uMsg==WM_INITDIALOG
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax
mov id,0
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS
push hDlg
pop ti.hWnd
invoke GetWindowRect,hDlg,addr rect
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText1,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText2,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText3,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText4,id,addr rect
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
.elseif uMsg==WM_CLOSE
invoke EndDialog,hDlg,NULL
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgProc endp

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD


LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp

SetDlgToolArea proc uses edi esi


hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
mov edi,lpti
mov esi,lprect
assume esi:ptr RECT
assume edi:ptr TOOLINFO
.if id==0
mov [edi].rect.left,0
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
mov [edi].rect.bottom,eax
.elseif id==1
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.elseif id==2
mov [edi].rect.left,0
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.else
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.endif
push lpText
pop [edi].lpszText
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
assume edi:nothing
assume esi:nothing
ret
SetDlgToolArea endp
end start
Análisis:

Después de que es creada la ventana del diálogo principal, creamos


el control 'tooltip ' con CreateWindowEx.

invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax

Después de eso procedemos a definir cuatro herramientas para cada


esquina de la caja de diálogo.

mov id,0 ; usado como el ID de la herramienta


mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS ; dice al control tooltip que subclasifique la
ventana del diálogo.
push hDlg
pop ti.hWnd ; manejador para la ventana que contiene la
herramienta
invoke GetWindowRect,hDlg,addr rect; obtener la dimensión del área cliente
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr
rect

Inicializamos los miembros de la estructura TOOLINFO. Nota que


queremos dividir el área cliente en 4 herramientas, así que
necesitamos saber la dimensión del área cliente. Esa es la razón por
la cual llamamos a GetWindowRect. No queremos transmitir
mensajes de ratón al control 'control' nosotros mismos, así que
especificamos la bandera TIF_SUBCLASS.

SetDlgToolArea es una función que calcula el rectángulo asociado


de cada herramienta y registra la herramienta para el control 'tooltip'.
No entraré en detalles engorrosos sobre el cálculo, es suficiente decir
que divide el área cliente en 4 áreas con los mismos tamaños. Luego
envía el mensaje TTM_ADDTOOL al control 'tooltip', pasando la
dirección de la estructura TOOLINFO en el parámetro lParam.

invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

Después de que son registradas las 4 herramientas, podemos


ocuparnos en los botones de la caja de diálogo. Podemos manejar
cada botón a través de su ID pero esto es muy tedioso.En vez de eso
usaremos la llamada a la API EnumChildWindows para enumerar
todos los controles en la caja de diálogo y luego registrarlos para el
control 'tooltip'. EnumChildWindows tiene la siguiente sintaxis:

EnumChildWindows proto hWnd:DWORD,


lpEnumFunc:DWORD, lParam:DWORD
hWnd es el manejador de la ventana padre. lpEnumFunc es la
dirección de la función EnumChildProc que será llamada para cada
control enumerado. lParam es el valor definido de la aplicación
[application-defined value] que será pasado a la función
EnumChildProc. La a función EnumChildProc tiene la siguiente
definición:

EnumChildProc proto hwndChild:DWORD, lParam:DWORD

hwndChild es el manejador a un control enumerado por


EnumChildWindows. lParam es el mismo valor lParam que pasas a
0 EnumChildWindows.
En nuestro ejemplo, llamamos a EnumChildWindows más o menos
así:

invoke EnumChildWindows,hDlg,addr EnumChild,addr ti

Pasamos la dirección de la estructura TOOLINFO en el parámetro


lParam porque registraremos cada control de ventana hija para el
control 'tooltip' en la función EnumChild. Si no queremos usar este
método, necesitaremos declarar ti como una variable global que
puede introducir errores [bugs].

Cuando llamamos a EnumChildWindows, Windows enumerará los


controles de ventanas hijas sobre nuestra caja de diálogo y llamamos
a la función EnumChild una vez para cada control enumerado. De
esta manera, si nuestra caja de diálogo tiene dos controles,
EnumChild será llamada dos veces.

La función EnumChild llena los miembros relevantes de la estructura


TOOLINFO y luego registra la herramienta con el control 'tooltip'.

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD


LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId ; usamos todo el área cliente del control como herramienta
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer ; usar el texto de la ventana como texto de sugerencia
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp

Nota que en este caso, un tipo diferente de herramienta: una que


cubre todo el área cliente de la ventana. Así que necesitamos llenar
todo el campo uID con el manejador de ventana que contiene la
herramienta. También debemos especificar la benadera
TTF_IDISHWND en el miembrio uFlags.
Tutorial 28: API de Depuración de
Win32 Parte 1
En este tutorial, aprenderás que ofrece Win32 a los desarrolladores interesados en
las primitivas de depuración. Sabrás como depurar un proceso cuando hayas
finalizado con este tutorial.
Bajar el ejemplo.

Teoría:

Win32 tiene varias funciones en su API que permiten a los programadores usar
algunas de las potencialidades de un depurador. Son llamadas las Apis de
depuración de Win32 o primitivas. Con ellas puedes:

• Cargar un programa o enganchar un programa que esté corriendo para su


depuración
• Obtener información de bajo nivel sobre el programa que estás depurando,
tal como el ID, la dirección del punto de entrada, la base de la imagen ,etc..
• Ser notificado sobre los eventos de depuración relacionados tales como
cuando un proceso/hilo inicia/termina, cuándo las DLLs son
cargadas/descargadas, etc.
• Modificar el proceso/hilo que está siendo depurado

En pocas palabras, puedes escribir el código de un depurador sencillo con estas


APIs. Como este tema es amplio, lo he dividido en varias partes: este tutorial será la
primera. Explicaré los conceptos básicos y daré un marco general para usar las APIs
de depuración de Win32 en este tutorial.

Los pasos al usar las APIs de depuración de Win32 son:

1. Crear un proceso o enganchar un proceso en curso. Este es el primer


paso al usar las APIs de depuración de Win32. Como tu programa va actuar
como un depurador, necesitas un programa a depurar. El programa que está
siendo depurado será llamado un "depurando" [debuggee]. Puedes adquirir
un "depurando" [debugee] de dos maneras:
o Puedes crear tú mismo el proceso "depurando" con CreateProcess.
Con el fin de crear un proceso para depuración, debes especificar la
bandera DEBUG_PROCESS. Esta bandera le dice a Windows que
queremos depurar el proceso. Windows enviará notificaciones de los
eventos de importantes para la depuración [important debugging-
related (debug events)] que ocurren en el "depurando". El proceso en
depuración será suspendido inmediatamente hasta que tu programa
esté listo. Si el proceso en depuración crea también un proceso hijo,
Windows enviará también eventos de depuración que ocurran en
todos esos procesos hijos a tu programa. Usualmente, esta conducta
es indeseada. Puedes deshabilitar esta conducta especificando la
bandera DEBUG_ONLY_THIS_PROCESS en combinación con la
bandera DEBUG_PROCESS.
o Puedes anexar tu programa a un proceso que esté corriendo con
DebugActiveProcess.
2. Esperar por eventos de depuración. Después de que tu programa ha
adquirido un proceso para su depuración, el hilo primario del proceso en
depuración es suspendido hasta que tu programa llame a
WaitForDebugEvent. Esta función trabaja como otras funciones
WaitForXXX,, es decir, bloquea el hilo que llama hasta que ocurre el evento
por el cual se espera. En este caso, espera por eventos de depuración a ser
enviados por Windows. Veamos su definición:

WaitForDebugEvent proto lpDebugEvent:DWORD,


dwMilliseconds:DWORD

lpDebugEvent es la dirección de una estructura DEBUG_EVENT que será


llenada con información sobre el evento de depuración que ocurre dentro del
depurando.

dwMilliseconds es el lapso de tiempo en milisegundos que esta función


esperará hasta que ocurra el evento de depuración. Si este período caduca y
no ocurre ningún evento de depuración, WaitForDebugEvent regresa al
programa que ha hecho la llamada. Pero si especificas la constante
INFINITE en este argumento, la función no regresará hasta que ocurra un
evento de depuración.

Ahora examinemos con más detalles la estructura DEBUG_EVENT.

DEBUG_EVENT STRUCT
dwDebugEventCode dd ?
dwProcessId dd ?
dwThreadId dd ?
u DEBUGSTRUCT <>
DEBUG_EVENT ENDS

dwDebugEventCode contiene el valor que especifica qué tipo de evento de


depuración ocurre. En pocas palabras, pueden haber muchos tipos de
eventos, tu programa necesita chequear el valor en este campo para
conocer qué tipo de evento ocurre y responder apropiadamente. Los valores
posibles son:

Valor Significado
Un proceso ha sido creado. Este evento
será enviado cuando el proceso en
depuración es creado (y todavía no está
CREATE_PROCESS_DEBUG_EVE
correindo) o cuando tu programa se anexe
NT
a un proceso con DebugActiveProcess.
Este es el primer evento que recibirá tu
programa.
EXIT_PROCESS_DEBUG_EVENT Un proceso termina.
Se ha creado un nuevo hilo en el proceso
en depuración o tu programa se anexa a
CREATE_THREAD_DEBUG_EVEN un proceso que ya está corriendo. Nota
T que no recibirás esta notificación cuando
el hilo primario del proceso en depuración
sea creado.
EXIT_THREAD_DEBUG_EVENT Termina un hilo en el proceso en
depuración. Tu programa no recibirá este
evento para el hilo primario. En pocas
palabras, puedes pensar en el hilo
primario del proceso en depuración como
un equivalente del mismo proceso en
depuración. Así que, cuando tu programa
ve CREATE_PROCESS_DEBUG_EVENT,
es realmente el
CREATE_THREAD_DEBUG_EVENT del
hilo primario.

El proceso en depuración carga una DLL.


Recibirás este evento cuando el cargador
del PE resuelva primero los enlaces a las
LOAD_DLL_DEBUG_EVENT
DLLs (llamas a CreateProcess para
cargar el depurando) y cuando el proceso
en depuración llama a LoadLibrary.
Una DLL es descargada del proceso en
UNLOAD_DLL_DEBUG_EVENT
depuración.
Ocurre una excepción en el proceso en
depuración. Importante: Este evento
ocurrirá una vez justo antes de que el
proceso en depuración comience a
ejecutar su primera instrucción. La
excepción realmente es una ruptura de
depuración [a debug break] (int 3h).
EXCEPTION_DEBUG_EVENT
Cuando quieres resumir el proceso en
depuración, llamas a
ContinueDebugEvent con la bandera
DBG_CONTINUE. No uses la bandera
DBG_EXCEPTION_NOT_HANDLED sino el
proceso en depuración rehusará correr
bao NT (en Win98, trabaja bien).
Este evento es generado cuando el
proceso en depuración llama a la función
OUTPUT_DEBUG_STRING_EVEN
DebugOutputString para eviar una
T
cadena de caracteres con un mensaje a tu
programa.
RIP_EVENT Ocurre un error en el sistema al depurar...

dwProcessId y dwThreadId son los id del proceso y del hilo del proceso
donde ocurre el evento de depuración. Puedes usar estos valores como
identificadores del proceso/hilo en el cual estás interesado. Recuerda que si
usas CreateProcess para cargar el proceso en depuración, también
obtienes los IDs del proceso y del hilo del proceso en depuración en la
estructura PROCESS_INFO. Puedes usar estos valores para diferenciar
entre los eventos de depuración que ocurren en el proceso en depuración y
su proceso hijo (en caso de que no hayas especificado la bandera
DEBUG_ONLY_THIS_PROCESS).

u es una union que contiene más información sobre el proceso en


depuración. Puede ser una de las siguientes estructuras dependiendo del
valor de dwDebugEventCode arriba.

valor en dwDebugEventCode Interpretación de u


Una estructura
CREATE_PROCESS_DEBUG_EVE
CREATE_PROCESS_DEBUG_INFO
NT
llamada CreateProcessInfo
Una estructura
EXIT_PROCESS_DEBUG_EVENT EXIT_PROCESS_DEBUG_INFO llamada
ExitProcess
Una estructura
CREATE_THREAD_DEBUG_EVEN
CREATE_THREAD_DEBUG_INFO llamada
T
CreateThread
Una estructura
EXIT_THREAD_DEBUG_EVENT EXIT_THREAD_DEBUG_EVENT llamada
ExitThread
Una estructura LOAD_DLL_DEBUG_INFO
LOAD_DLL_DEBUG_EVENT
llamada LoadDll
Una estructura
UNLOAD_DLL_DEBUG_EVENT UNLOAD_DLL_DEBUG_INFO llamada
UnloadDll
Una estructura
EXCEPTION_DEBUG_EVENT EXCEPTION_DEBUG_INFO llamada
Exception
Una estructura
OUTPUT_DEBUG_STRING_EVEN
OUTPUT_DEBUG_STRING_INFO llamada
T
DebugString
RIP_EVENT A RIP_INFO llamada RipInfo

En este tutorial no entraré en detalles sobre todas las estructuras, aquí sólo
será cubierta la estructura CREATE_PROCESS_DEBUG_INFO.

Asumiendo que nuestro programa llama a WaitForDebugEvent y regresa .


Lo primero que deberíamos hacer es examinar dwDebugEventCode para
ver qué tipo de evento de depuración ocurrió en el proceso en depuración.
Por ejemplo, si el valor en dwDebugEventCode es
CREATE_PROCESS_DEBUG_EVENT, puedes interpretar el miembro en u
como CreateProcessInfo y acceder a él con u.CreateProcessInfo.

3. Hacer lo que quieras hacer con tu programa para ese evento de


depuración. Cuando regresa WaitForDebugEvent, signifca que acaba de
ocurrir un evento de depuración en el depurando o ha ocurrido una pausa.
Tu programa necesita examinar el valor en dwDebugEventCode con el fin
de reaccionar apropiadamente al evento. En este sentido, es como procesar
mensajes de Windows: eliges manejar algunos e ignorar otros.
4. Dejar que el proceso en depuración continúe la ejecución. Cuando
ocurre un evento de depuración, Windows suspende el proceso en
depuración. Cuando hayas terminado la manipulación del evento, necesitas
poner en movimiento el proceso en depuración de nuevo. Haces esto
llamando a la función ContinueDebugEvent.

ContinueDebugEvent proto dwProcessId:DWORD,


dwThreadId:DWORD, dwContinueStatus:DWORD

Esta función resume el hilo que fue suspendido previamante porque ocurrió
un evento de depuración.
dwProcessId y dwThreadId los IDs de proceso y de hilo del hilo que será
resumido. Usualmente tomas estos dos valores de los miembros
dwProcessId y dwThreadId de la estructura DEBUG_EVENT.
dwContinueStatus especifica cómo continuar el hilo que reportó el evento de
depuración. Hay dos valores posibles: DBG_CONTINUE y
DBG_EXCEPTION_NOT_HANDLED. Para los otros eventos de depuración,
esos dos valores hacen lo mismo: resumen el hilo. La excepción es el
EXCEPTION_DEBUG_EVENT. Si el hilo reporta un evento de depuración
excepción, significa que ocurrió una excepción en el hilo del proceso en
depuración. Si especificas DBG_CONTINUE, el hilo ignorará su manipulación
de la excepción y continuará con la ejecución. En este escenario, tu
programa debe examinar y resolver la excepción misma antes de resumir el
hilo con DBG_CONTINUE sino la excepción ocurrirá una vez más, una vez
más.... Si especificas DBG_EXCEPTION_NOT_HANDLED, tu programa está
diciendo a Windows que no manejará la excepción: Windows usaría el
manejador de excepción por defecto del proceso en depuración para
manejar la excepción.
En conclusión, si el evento de depuración refiere a una excepción en el
proceso en depuración, deberías llamar a ContinueDebugEvent con la
bandera DBG_CONTINUE si tu programa ya removió la causa de la
excepción. De otra manera, tu programa debe llamar a
ContinueDebugEvent con la bendera DBG_EXCEPTION_NOT_HANDLED.
Excepto en un caso en el que siempre debes usar la bandera
DBG_CONTINUE: el primer EXCEPTION_DEBUG_EVENT que tiene el valor
EXCEPTION_BREAKPOINT en el miembro ExceptionCode. Cuando el
proceso en depuración vaya a ejecutar su primera instrucción, tu programa
recibirá el evento de depración exepción. Realmente es un quiebre de
depuración [debug break] (int 3h). Si respondes llamando a
ContinueDebugEvent con la bandera DBG_EXCEPTION_NOT_HANDLED,
Windows NT reusará correr el proceso en depuración (porque nada cuida de
él). Siempre debes usar la bandera DBG_CONTINUE en este caso para decir
a Windows que quieres que el hilo continúe.

5. Continuar este ciclo en un bucle infinito hasta que termine el


proceso en depuración. Tu programa debe estar en un bucle infinito muy
parecido al bucle de mensajes hasta que el proceso en depuración termine.
El bucle tiene más o menos el siguiente aspecto:

.while TRUE
invoke WaitForDebugEvent, addr DebugEvent, INFINITE
.break .if
DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
<Handle the debug events>
invoke ContinueDebugEvent, DebugEvent.dwProcessId,
DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

Aquí está el truco: una vez que empiezas a depurar un programa, ya no


puedes desprenderte del proceso en depuración hasta que termine.

Resumamos los pasos de nuevo:

1. Crear un procesos o anexar tu programa a un proceso que esté


corriendo.
2. Esperar por los eventos de depuración
3. Hacer lo que tu programa quiere hacer en respuesta al evento en
depuración.
4. Dejar que el proceso en depuración continúe su ejecución.
5. Continuar este ciclo en un bucle infinito hasta que el proceso en
depuración termina

Ejemplo:
Este ejemplo depura un programa win32 y muestra información importante tal
como el manejador del proceso, el Id del proceso, la base de la imagen , etc.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
db "Process Handle: %lx",0Dh,0Ah
db "Thread Handle: %lx",0Dh,0Ah
db "Image Base: %lx",0Dh,0Ah
db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES
or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+
DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName,
MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo,
DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess,
DBEvent.u.CreateProcessInfo.hThread,
DBEvent.u.CreateProcessInfo.lpBaseOfImage,
DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName,
MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,
DBG_CONTINUE
.continue
.endif
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName,
MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName,
MB_OK+MB_ICONINFORMATION
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,
DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

Análisis:
El programa llena la estructura OPENFILENAME y luego llama a GetOpenFileName
para pernitir que el usuario elija un programa para su depuración.

invoke GetStartupInfo,addr startinfo


invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+
DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

Cuando el usuario elige uno, llama a CreateProcess para cargar el programa.


Llama a GetStartupInfo para llenar la estructura STARTUPINFO con sus valores
por defecto. Nota que usamos la bandera DEBUG_PROCESS combinada con
DEBUG_ONLY_THIS_PROCESS con el fin de depurar solamente este programa, sin
incluir sus procesos hijos.

.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE

Cuando es cargado el proceso en depuración, introducimos el bucle infinito de


depuración, llamando a WaitForDebugEvent. WaitForDebugEvent no regresará
hasta que ocurra un evento de depuración en el 'proceso en depuración' porque
especificamos INFINITE como su segundo parámetro. Cuando ocurre un evento de
depuracion, WaitForDebugEvent regresa y DBEvent es llenada con información
sobre el evento de depuración.

.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName,
MB_OK+MB_ICONINFORMATION
.break

Primero chequeamos el valor en dwDebugEventCode. Si es


EXIT_PROCESS_DEBUG_EVENT, desplegamos una caja de mensaje que dice "The
debuggee exits" [El evento de depuración ha culminado] y luego salimos del bucle
de depuración.
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo,
DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess,
DBEvent.u.CreateProcessInfo.hThread,
DBEvent.u.CreateProcessInfo.lpBaseOfImage,
DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName,
MB_OK+MB_ICONINFORMATION

Si el valor en dwDebugEventCode es CREATE_PROCESS_DEBUG_EVENT,


entonces desplegamos cierta información interesante sobre el proceso en
depuración en una caja de mensaje. Obtenemos esa información a partir de
u.CreateProcessInfo. CreateProcessInfo es una estructura del tipo
CREATE_PROCESS_DEBUG_INFO. Puedes obtener más info sobre esta estructura
en la referencia de la API de Win32.

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,
DBG_CONTINUE
.continue
.endif

Si el valor en dwDebugEventCode es EXCEPTION_DEBUG_EVENT, debemos


chequear luego por el tipo exacto de excepción. Es una línea larga de referencia de
estructura anidada pero puedes obtener el tipo de excepción del miembro
ExceptionCode. Si el valor en ExceptionCode es EXCEPTION_BREAKPOINT y
ocurre por primera vez (o si estamos seguros de que el proceso en depuración no
tiene incrustado int 3h), podemos asumir con seguridad que esta excepción ocurrió
cuando el proceso en depuración iba a ejecutar la primera instrucción. Cuando
hayamos hecho lo que ibamos a hacer con el procesamiento, debemos llamar a l
ContinueDebugEvent con la bandera DBG_CONTINUE para dejar que corra de
nuevo el proceso en depuración. Luego volvemos a esperar el siguiente evento de
depuración.

.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName,
MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName,
MB_OK+MB_ICONINFORMATION
.endif

Si el valor en dwDebugEventCode es CREATE_THREAD_DEBUG_EVENT o


EXIT_THREAD_DEBUG_EVENT, desplegamos una caja de mensaje que diga eso.

invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,


DBG_EXCEPTION_NOT_HANDLED
.endw

Excepto para el caso EXCEPTION_DEBUG_EVENT de arriba, llamamos a


ContinueDebugEvent con la bandera DBG_EXCEPTION_NOT_HANDLED para
resumir el proceso en depuración.

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
Cuando termina el proceso en depuración, estamos fuera del bucle de depuración y
debemos cerrar los manejadores del proceso y del hilo del proceso en depuración.
Cerrar los manejadores no significa que estamos matando el proceso/hilo. Sólo
significa que no queremos usar más esos manejadores para referir al proceso/hilo.

Tutorial 29: API de Depuración de


Win32 Parte 2
Continuamos con el tema de win32 debug API. En este tutorial, aprenderemos como
depurar el depurando [debugee].

Baja el ejemplo

Teoría:
En el tutorial previo, aprendimos como cargar el debuggee y a manejar eventos de
depuración que ocurren en su proceso. Para que sea útil, nuestro programa debe
ser capaz de modificar el proceso depurado. Hay varias APIs para realizar este
propósito.

• ReadProcessMemory Esta función permite leer la memoria en el proceso


especificado. El prototipo de la función es:

ReadProcessMemory proto hProcess:DWORD,


lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD,
lpNumberOfBytesRead:DWORD

hProcess es el manejador al proceso que quieres leer.


lpBaseAddress es la dirección del proceso objeto [target process] que
quieres comenzar a leer. Por ejemplo, si quieres leer 4 bytes del proceso en
depuración comenzando en 401000h, el valor en este parámetro debe ser
401000h.
lpBuffer es la dirección del buffer que recibirá los bytes leídos del proceso.
nSize es el número de bytes que quieres leer
lpNumberOfBytesRead es la dirección de la variable de tamaño dword que
recibe el número de bytes realmente leídos. Si no esrelevante para tí,
puedes usar NULL.

• WriteProcessMemory es la contraparte de ReadProcessMemory. Te


permite escribir la memoria del proceso objeto. Sus parámetros son
exactamente los mismos que los de ReadProcessMemory

Las siguientes dos funciones de la API necesitan un poco de explicación. Bajo


un sistema operativo multitarea como Windows, pueden haber varios
programas corriendo al mismo tiempo. Windows da a cada hilo un pedazo de
tiempo. Cuando expira ese lapso, Windows paraliza el hilo presente y
conmuta a otro hilo que tiene la prioridad más alta. Justo antes de conmutar
al otro hilo, Windows salva los valores de los registros del hilo presente de
manera que cuando llegue el momento de resumir el hilo, Windows pueda
restaurar el último *entorno* del hilo. Los valores salvados de los registros
son llamados colectivamente en un contexto.

• Volvamos a nuestro asunto. Cuando ocurre un evento de depuración,


Windows suspende el proceso en depuración. El contexto del proceso en
depuración es salvado. Como el proceso en depuración es suspendido,
podemos estar seguros de que los valores en el contexto se mantendrá sin
alteración. Podemos obtener los valores en el contexto con
GetThreadContext y podemos cambiarlos con SetThreadContext.
• Estas dos APIs son muy poderosas. Con ellas tienes en tus dedos [fingertips]
el poder de una VxD [the VxD-like power] sobre los procesos en depuración:
puedes alterar los valores del registro salvado y justo antes de que el
proceso en depuración resuma la ejecución, los valores del contexto serán
re-escritos dentro de los registros. Cualquier cambio que hagas al contexto
es reflejado en el proceso en depuración. Piensa en ello: ¡incluso puedes
alterar el valor del registro eip y desviar el flujo de la ejecución a donde
quieras! No serás capaz de hacer eso bajo circunstancias normales.
• GetThreadContext proto hThread:DWORD, lpContext:DWORD

hThread es el manejador del hilo del que quieres obtener el contexto


lpContext es la dirección de la estructura CONTEXT que será llenada
cuando la función regresa satisfactoriamente.

SetThreadContext tiene exactamente los mismos parámetros. Vemos que


la estructura CONTEXT se ve más o menos como:

• CONTEXT STRUCT
• ContextFlags dd ?
;--------------------------------------------------------------------------------------------
--------------
; Esta sección es regresada si ContextFlags contiene el valor
; CONTEXT_DEBUG_REGISTERS
• ;--------------------------------------------------------------------------------------------
---------------
iDr0 dd ?
iDr1 dd ?
iDr2 dd ?
iDr3 dd ?
iDr6 dd ?
iDr7 dd ?
• ;--------------------------------------------------------------------------------------------
--------------
; Esta sección es regresada si ContextFlags contiene el valor
; CONTEXT_FLOATING_POINT
• ;--------------------------------------------------------------------------------------------
---------------
• FloatSave FLOATING_SAVE_AREA <>
• ;--------------------------------------------------------------------------------------------
--------------
; Esta sección es regresada si ContextFlags contiene el valor
; CONTEXT_SEGMENTS
• ;--------------------------------------------------------------------------------------------
---------------
• regGs dd ?
regFs dd ?
regEs dd ?
regDs dd ?
• ;--------------------------------------------------------------------------------------------
--------------
; Esta sección es regresada si ContextFlags contiene el valor
CONTEXT_INTEGER
• ;--------------------------------------------------------------------------------------------
---------------
• regEdi dd ?
regEsi dd ?
regEbx dd ?
regEdx dd ?
regEcx dd ?
regEax dd ?
• ;--------------------------------------------------------------------------------------------
--------------
; Esta sección es regresada si ContextFlags contiene el valor
; CONTEXT_CONTROL
• ;--------------------------------------------------------------------------------------------
---------------
• regEbp dd ?
regEip dd ?
regCs dd ?
regFlag dd ?
regEsp dd ?
regSs dd ?
• ;--------------------------------------------------------------------------------------------
--------------
; Esta sección es regresada si ContextFlags contiene el valor
; CONTEXT_EXTENDED_REGISTERS
• ;--------------------------------------------------------------------------------------------
---------------
• ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
CONTEXT ENDS

Como puedes observar, los miembros de esta estructuras son imitaciones de


los registros del procesador real. Antes de que puedas usar esta estructura,
necesitas especificar cuáles grupos de registros quieres leer/escribir en el
miembro ContextFlags. Por ejemplo, si quieres leer/escribir todos los
registros, debes especificar CONTEXT_FULL en ContextFlags. Si quieres
sólo leer/escribir regEbp, regEip, regCs, regFlag, regEsp or regSs, debes
especificar CONTEXT_CONTROL en ContextFlags.

Una cosa que debes recordar cuando uses la estrucura CONTEXT: debe ser
alineada en el límite de una dword sino obtendrás extraños resultados bajo
NT. Debes especificar "align dword" justo arriba de la línea que lo declara,
como este:

align dword
MyContext CONTEXT <>

Ejemplo:
El primer ejemplo demuestra el uso de DebugActiveProcess. Primero necesitas
correr un proceso objeto llamado win.exe que va en un bucle infinito justo antes de
que la ventana sea mostrada en el monitor. Luego corres el ejemplo, se anexará a
win.exe y modificará el código de win.exe de manera que win.exe salga del bucle
infinito y muestre su ventana.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h

.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>

.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr
context
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess,
context.regEip ,addr buffer, 2, NULL
invoke MessageBox, 0, addr TargetPatched, addr AppName,
MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId,
DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,
DBG_EXCEPTION_NOT_HANDLED
.endw
.else
invoke MessageBox, 0, addr SearchFail, addr
AppName,MB_OK+MB_ICONERROR .endif
invoke ExitProcess, 0
end start

;--------------------------------------------------------------------
; El código fuente parcial de win.asm, nuestro debuggee. Es realmente
; el ejemplo de ventyana simple en el tutorial 2 con un bucle infinito
; insertado justo antes de que entre el bucle de mensajes.
;----------------------------------------------------------------------
......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
jmp $ <---- Aquí está nuestro bucle infinito. Ensambla a EB FE
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

Análisis:
invoke FindWindow, addr ClassName, NULL

Nuestro programa necesita anexarse él mismo al depurando [debuggee] con


DebugActiveProcess lo cual requiere el Id del proceso del depurando. Podemos
obtener el Id del proceso llamando a GetWindowThreadProcessId que en cambio
necesita del manejador [handle] de ventana como parámetro. Así que necesitamos
obtener primero el manejador de ventana.

Con FindWindow, podemos especificar el nombre de la clase de ventana que


necesitamos. Regresa el manejador de la ventana creada por esa clase. Si regresa
NULL, no hay ninguna ventana de esa clase.

.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId

Después de que obtengamos el Id del proceso, podemos llamar a


DebugActiveProcess. Luego introducimos el bucle de depuración que espere por
los eventos de depuración.

.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr
context

Cuando obtenemos CREATE_PROCESS_DEBUG_INFO, significa que el depurando


está suspendido, listo para que nosotros hagamos la cirugía sobre él. En este
ejemplo, podemos sobreescribir la instrucción del bucle infinito en el depurando
(0EBh 0FEh) con NOPs ( 90h 90h).
Primero, necesitamos obtener la dirección de la instrucción. Puesto que el
depurando está ya en el bucle por el tiempo que nuestro programa está anexo a él,
eip siempre apuntará a la instrucción. Todo lo que necesitamos hacer es obtener el
valor de eip. Usamos GetThreadContext para alcanzar esa meta. Ponemos el
miembro ContextFlags a CONTEXT_CONTROL para decirle a GetThreadContext
que queremos llenar los miembros del registro de control "control" de la estructura
CONTEXT.

invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess,


context.regEip ,addr buffer, 2, NULL

Ahora que obtenemos el valor de eip, podemos llamar a WriteProcessMemory


para sobreescribir la instrucción "jmp $" con NOPs, ayudar efectivamente de esta
manera a que el depurando salga del bucle infinito. Después de que desplegamos el
mensaje al usuario y luego llmamos a ContinueDebugEvent para resumir el
debuggee. Puesto que la instrucción "jmp $" está sobreescrita por NOPs, el
debuggee será capaz de continuar mostrando su ventana e introducir su bucle de
mensajes. La evidencia es que veremos su ventana en el monitor.

El otro ejemplo usa una aproximación al problema levemente diferente para


detener [break] el debuggee fuera del bucle infinito.

.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread,
addr context
add context.regEip,2
invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread,
addr context
invoke MessageBox, 0, addr LoopSkipped, addr AppName,
MB_OK+MB_ICONINFORMATION
.......
.......

Todavía se llama a GetThreadContext para obtener el valor actual de eip pero en


vez de sobreescribir la instrucción "jmp $", se incrementa en 2 el valor de regEip
para "saltar por encima" ["skip over"] de la instrucción. El resultado es que cuando
el archivo en depuración [debuggee] vuelve a ganar el control, resume la ejecución
en la siguiente instrucción después de "jmp $".

Ahora puedes ver el poder de Get/SetThreadContext. También puedes modificar las


otras imágenes de registros y sus valores serán reflejados de regerso al depurando.
Incluso puedes insertar la instrucción int 3h para poner puntos de quiebre
[breakpoints] en el proceso debuggee.

Tutorial 30: API de Depuración de


Win32 Parte 3
En este tutorial, continuaremos la exploración de la api de depuración de win32.
Específicamente, aprenderemos como trazar el depurando [debugee].
Baja el ejemplo.

Historia de revisiones:

12/2/2000: Se olvidó el alineamiento dword de la estructura CONTEXT

Teoría:
Si haz usado antes un depurador, estarás ya familiarizado con el trazado. Cuando
"trazas" un programa, se detiene después de ejecutar cada función, dándote la
oportunidad de examinar los valores de registros/memoria. Paso simple [single-
stepping] es el nombre oficial del trazado.

El rasgo de paso simple [single-step] es proveído por el propio CPU. El 8vo bit de la
bandera de registro es llamado la bandera bit de trampa [trap flag]. Si esta bandera
(bit) está establecida, el CPU se ejecuta en modo paso simple [single-step]. El CPU
generará una excepción de depuración después de cada instrucción. Después de
que se genera la excepción de depuración, la bandera de trampa es limpiada
automáticamente.

También podemos aplicar paso simple [single-step] a los depurandos [debuggee],


usando la api de win32. Los pasos son los siguientes:

1. Llamar a GetThreadContext, especificando CONTEXT_CONTROL en


ContextFlags, para obtener el valor del registro de bandera.
2. Poner el bit de trampa en el miembro regFlag de la estructura CONTEXT
3. Llamar a SetThreadContext
4. Esperar por los eventos de depuración, como es usual. El depurando
[debuggee] se ejecutará en modo de paso simple [single-step]. Después de
que se ejecuta cada instrucción, obtendremos EXCEPTION_DEBUG_EVENT
con el valor EXCEPTION_SINGLE_STEP en
u.Exception.pExceptionRecord.ExceptionCode
5. Si necesitas trazar la próxima instrucción , necesitarás poner de nuevo el bit
de trampa.

Ejemplo:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
db "Total Instructions executed : %lu",0
TotalInstruction dd 0

.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
align dword
context CONTEXT <>

.code
start:
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or
OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE,
DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr
startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT

invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction


invoke MessageBox, 0, addr buffer, addr AppName,
MB_OK+MB_ICONINFORMATION
.break
.elseif
DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION
_BREAKPOINT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId,
DBEvent.dwThreadId, DBG_CONTINUE
.continue
.elseif
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION
_SINGLE_STEP
inc TotalInstruction
invoke GetThreadContext,pi.hThread,addr context or
context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId,
DBEvent.dwThreadId,DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId,
DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.endif
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
invoke ExitProcess, 0
end start

Análisis:
El programa muestra la caja de diálogo openfile. Cuando el usuario elige un archivo
ejecutable, ejecuta el programa en modo de paso simple, contando el número de
instrucciones ejecutadas hasta que el depurando [debugee] sale a Windows.

.elseif
DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION
_BREAKPOINT

Aprovechamos esta oportunidad para poner el depurando [debuggee] en modo de


paso simple [single-step mode]. Recuerda que Windows envía un
EXCEPTION_BREAKPOINT justo antes de que se ejecute la primera instrucción del
depurando.

mov context.ContextFlags, CONTEXT_CONTROL


invoke GetThreadContext, pi.hThread, addr context

Llamamos a GetThreadContext para llenar la estructura CONTEXT con los


valores actuales en los registros del depurando. Más específicamente, necesitamos
el valor actual del registro de bandera.

or context.regFlag,100h

Ponemos el bit de trampa (8vo bit) en la imagen del registro de bandera.

invoke SetThreadContext,pi.hThread, addr context


invoke ContinueDebugEvent, DBEvent.dwProcessId,
DBEvent.dwThreadId, DBG_CONTINUE
.continue
Luego llamamos a SetThreadContext para sobreescribir los valores en la
estructura CONTEXT con el (los) nuevo(s) y llamar a ContinueDebugEvent con la
bandera DBG_CONTINUE para resumir el archivo en depuración [debuggee].

.elseif
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION
_SINGLE_STEP
inc TotalInstruction

Cuando se ejecuta una función en el archivo de depuración, recibimos un


EXCEPTION_DEBUG_EVENT. Debemos examinar el valor de
u.Exception.pExceptionRecord.ExceptionCode. Si el valor es
EXCEPTION_SINGLE_STEP, entonces es generado este evento de depuración
debido al modo de paso simple [single-step mode]. En este caso podemos
incrementar en uno la variable TotalInstruction porque sabemos que se ha
ejecutado exactamente una instrucción en el archivo bajo depuración.

invoke GetThreadContext,pi.hThread,addr context or


context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId,
DBEvent.dwThreadId,DBG_CONTINUE
.continue

Como la bandera de trampa es limpiada después que se genera la excepción de


depuración, debemos poner la bandera de trampa de nuevo si queremos continuar
en modo de paso simple [single-step mode].

Adevertencia: No uses el ejemplo en este tutorial con un programa muy


grande: el trazado sería muy LENTO. Podrías tener que esperar hasta diez
minutos antes de que puedas cerrar el archivo en depuración.