Escolar Documentos
Profissional Documentos
Cultura Documentos
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.
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>
.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).
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.
.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:
No tienes que usar las tres secciones en tu programa. Declara solo la(s)
sección(es) que quieres usar.
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.
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.
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:
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.
A través de mis ejemplos, usaré windows.inc de hutch que puedes bajar desde
http://win32asm.cjb.net
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.
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
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:
• /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.
Entonces enlacemos:
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:
.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
Definimos dos cadenas terminadas en cero en la sección .data. Recuerda que toda cadena
ANSI en Windows debe terminar en NULL (0 hexadecimal).
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á.
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:
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.
Teoría:
Esbozaré los pasos requeridos para crear una ventana sobre el escritorio:
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.
.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
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
end start
Análisis:
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
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.
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 ?
.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.
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.
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.
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.
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.
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd
.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.
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.
.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.
En este tutorial, aprenderemos como "pintar" texto en el área cliente de una ventana. También
aprenderemos sobre el contexto del dispositivo.
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.
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:
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
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
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.
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:
Después de terminar de pintar el área cliente, debes llamar a la función EndPaint para liberar el
manejador del contexto de dispositivo.
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:
Contenido:
.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
.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.
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
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.
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.
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.
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.
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
.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
.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:
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
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.
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
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
.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.
.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.
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
Mymenu MENU
{
[menu list here]
}
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.
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 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.
.DATA
MenuName db "FirstMenu",0
...........................
...........................
.CODE
...........................
mov wc.lpszMenuName, OFFSET MenuName
...........................
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\
...........................
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.
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
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:
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
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.
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.
.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
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
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:
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
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
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:
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.
.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.
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.
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:
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:
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:
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
Dialog.rc
#include "resource.h"
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:
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.
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].
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:
los tipos de controles son constantes del compilador de recursos así que tienes que consultar el
manual.
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.
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.
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
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
dialog.rc (part 2)
#include "resource.h"
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:
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:
.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
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.
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].
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
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
Análisis:
Este FilterString es el filtro para el nombre de archivo que especificamos como sigue:
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.
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.
.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.
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.
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.
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
.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
Análisis:
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
.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:
• 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.
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 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
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.
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.
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].
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.
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
CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp
end start
Análisis:
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:
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.
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
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
CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp
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
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 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
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ú
Preliminares:
"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:
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"
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.
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
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 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:
.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
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.
.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
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.
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:
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.
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:
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:
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
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.
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:
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.
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:
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:
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
Análisis:
.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:
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.
Teoría:
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.
;--------------------------------------------------------------------------------------
; 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:
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:
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).
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í :
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.
;---------------------------------------------------------------------------------------------
; 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
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í.
• 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.
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:
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
.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 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
.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
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
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í.
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.
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.
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.
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
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_SORT ==
ITEMTYPE UNION
itemex TVITEMEX < >
item TVITEM < >
ITEMTYPE 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.
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.
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
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:
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:
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.
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.
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
.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
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
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
.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.
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.
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
Después de la Subclasificació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.
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.
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
.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
Análisis:
.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
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.
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:
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.
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
.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
Análisis:
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
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.
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
.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
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.
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
.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
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
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
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
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.
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:
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:
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
.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
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:
.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.
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
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.
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.
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.
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.
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:
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.
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
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:
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
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.
.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
.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
.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
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].
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).
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:
.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
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.
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:
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.
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.
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.
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:
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
.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
;---------------------------------------------------------------------
; 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
.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
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.
Puedes cargar dinámicamente una DLL con la función LoadLibrary que tiene la siguiente
sintaxis:
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.
Toma un parámetro: el manejador del módulo de la DLL que quieras descargar. Normalmente,
obtienes el manejador a partir de LoadLibrary
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.
• 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.
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
.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
;--------------------------------------------------------------------
; 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
.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.
Eso es todo lo que hace le programa principal. La parte interesante está en la DLL.
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.
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.
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
.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
Teoría:
.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
Especificando la herramienta
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:
.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
Ejemplo:
.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
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
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
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:
DEBUG_EVENT STRUCT
dwDebugEventCode dd ?
dwProcessId dd ?
dwThreadId dd ?
u DEBUGSTRUCT <>
DEBUG_EVENT ENDS
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.
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).
En este tutorial no entraré en detalles sobre todas las estructuras, aquí sólo
será cubierta la estructura CREATE_PROCESS_DEBUG_INFO.
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.
.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
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.
.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==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 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.
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.
• 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
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
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr
context
.......
.......
.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
.......
.......
Historia de revisiones:
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.
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
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
or context.regFlag,100h
.elseif
DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION
_SINGLE_STEP
inc TotalInstruction