Documente Academic
Documente Profesional
Documente Cultură
Este tutorial no pretende ensear el lenguaje C++ ni tampoco cmo usar algn compilador
en particular (Borland C++, Visual C++, BCC-Win32, etc...). Sin embargo me tomar un
momento en el apndice para dar algunas notas sobre como usar los compiladores que
conozco.
Si no sabes que es una macro o que que es typedef o si no sabes cmo funciona la sentecia
switch(), entonces te recomiendo que primero leas un buen libro o tutorial sobre el
lenguaje C.
Notas Importantes:
A veces a lo largo del texto voy a indicar ciertas cosas que son IMPORTANTES de leer,
por lo tanto debes prestarle atencin. La primer nota es:
Los fuentes includos en el archivo zip no son opcionales! No he incluido todo el cdigo
en el tutorial, solo aquel que es relevante para lo que estoy explicando en ese momento.
Para ver como este cdigo se integra con el resto del programa debes mirar el codigo fuente
incluido en el archivo zip.
Y segundo:
Leelo todo! Si tienes alguna pregunta durante una seccin del tutorial, solo ten un poco de
paciencia ya que esta podra ser respondida en el resto del mismo.
Otra cosa para recordar es que si tienes alguna pregunta sobre un tema A sta podra
finalmente ser respondida en una discusin de B o C o quizs D. Por lo tanto si tienes
alguna duda sigue leyendo el tutorial.
OK, Pienso que esto son todos los consejos que tengo que dar por el momento. Adelante...
Recuerda compilar esto como C, no como C++. Esto probablemente no importe pero debido
a que todo el cdigo es C puro tiene sentido comenzar por el camino correcto. En muchos
casos todo lo necesario es simplemente poner el cdigo en un archivo con extensin .c en
vez de .cpp. Si todo esto te marea simplemente guarda el archivo con el nombre test.c y
listo.
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
MessageBox(NULL, "Goodbye, cruel world!", "Note", MB_OK);
return 0;
}
Si esto no funciona, tu primer paso es leer que errores obtuviste y si no los entiendes
bscalos en la ayuda o en algn documento que acompae tu compilador. Asegrate de
haber especificado "GUI Win32" y no "Console" dentro de la opcin
project/compile/target de tu compilador. Desafortunadamente no puedo ayudarte mucho
con esta parte ya que los errores y cmo corrregirlos varan de compilador en compilador (y
de persona en persona).
Quizs obtengas algunos "Warnings" dicindote que no has usado los parmetros provistos
en WinMain( ), pero esto est bien. Ahora que sabemos que puedes compilar un programa
veamos esta pequea porcin de cdigo:
HINSTANCE hInstance
Handle al mdulo ejecutable del programa (el archivo .exe en memoria).
HINSTANCE hPrevInstance
Siempre nulo (o NULL) en programas Win32.
LPSTR lpCmdLine
Un string (o cadena de caracteres) con los argumentos de linea de comando. No
incluye el nombre del programa.
int nCmdShow
Un valor entero que puede ser pasado a ShowWindow( ), veremos esto mas
adelante.
hInstance es usado para cosas como cargar recursos y cualquier otra tarea que sea
realizada en una base por-modulos. Un mdulo puede ser el archivo .EXE cargado en
memoria o una librera DLL cargada en tu programa. En la mayor parte de este tutorial (por
no decir todo el tutorial) habr un solo mdulo del cual preocuparse: el EXE.
Especificaciones de llamada:
Seguramente has encontrado que muchos de los tipos de datos tienen definiciones
especficas de windows, UINT para unsigned int (entero no signado) LPSTR para char*,
etc...lo que elijas depende de ti. Si te sientes mas cmodo utilizando char* en lugar de
LPSTR eres libre de hacerlo. Solo asegrate bien de que tipo de dato se trata antes de
substituir algo.
Slo recuerda algunas cosas y luego ser fcil de interpretar. El prefijo LP representa Long
Pointer (Puntero Largo). En Win32 la parte larga es obsoleta por lo tanto no te preocupes
por ella. Si no conoces que es un puntero puedes hacer lo siguiente: 1) Buscar un tutorial de
C, o 2) Seguir adelante y saltearte un parte. Realmente recomiendo la primer opcin,
aunque muchas personas elijen la segunda... Despus no digas que no te avis.
La letra C que sigue luego de LP indica que se trata de un puntero "Constante". Por lo tanto
LPCSTR representa "Long Pointer to Const String" o "Puntero Largo un String Constante"
pero como ya dijimos, en Win32 podemos ignorar la parte "long" as que esto podra leerse
"Puntero a un String Constante", uno que no se puede modificar. Por otro lado LPSTR no es
constante y puede ser modificado.
Quizs has visto una "T" ah en medio. No te preocupes por esto por ahora, a menos que
intencionalmente ests trabajando con UNICODE, no significa nada.
Una Simple Ventana.
Ejemplo: simple window
A veces las personas ingresan al IRC y preguntan "Como hago una ventana...?".
Bien, no es tan simple como parece. No es difcil una vez que sepas lo que ests
haciendo, pero hay algunas cosas que necesitas hacer para poder crear un ventana. Y son
mas de las que pueden ser explicadas en una sala de chat o en una nota rpida.
Siempre me gust hacer las cosas primero y aprenderlas luego... por lo tanto aqui est el
cdigo de una simple ventana que ser explicado en breve.
#include <windows.h>
if(!RegisterClassEx(&wc))
{
MessageBox(NULL, "Window Registration Failed!", "Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
if(hwnd == NULL)
{
MessageBox(NULL, "Window Creation Failed!", "Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
Generalmente este es el programa ms simple que puedes escribir par crear una ventana
funcional, digamos unas 70 lneas de cdigo. Si has compilado el primer ejemplo, entonces
este debera funcionar sin problemas.
La Clase Ventana no tiene nada que ver con las clases de C++.
if(!RegisterClassEx(&wc))
{
MessageBox(NULL, "Window Registration Failed!", "Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
Este es el cdigo que usamos en WinMain( ) para registrar nuestra Clase Ventana en el
sistema. Para ello, rellenamos los campos de una estructura WNDCLASSEX y llamamos a
RegisterClassEx( ). Los campos de la estructura son los siguientes:
cbSize
El tamao de la estructura
style
El estilo de la clase (CS *), no confundirse con el estilo de la ventana (WS *).
Generalmente este campo puede ser puesto en cero.
lpfnWndProc
Puntero al Window Procedure de esta clase.
cbClsExtra
Cantidad extra de asignacin de memoria para datos de la clase. Generalmente cero.
cbWndExtra
Cantidad extra de asignacin de memoria por ventana de este tipo. Generalmente
cero.
hInstance
Handle a la instancia de la aplicacin (la que obtenemos en el primer parmetro de
WinMain( ) )
hIcon
Handle al cono grande (32x32), mostrado cuando el usuario presiona Alt+Tab.
hCursor
Handle al cursor que ser mostrado sobre la ventana.
hbrBackground
Pincel para fijar el color de fondo de nuestra ventana.
lpszMenuName
Nombre del recurso Men para usar con las ventanas de esta clase.
lpszClassName
Nombre para identificar la clase.
hIconSm
Handle al cono pequeo (16x16), usado en la barra de tareas y en la esquina
superior izquierda de la ventana.
No te preocupes si esto no queda muy claro an, las distintas partes que vimos sern
explicadas mas adelante. Otra cosa para recordar es no intentar memorizar toda esta
estructura. Yo raramente (de hecho nunca) memorizo estructuras o parmetros de
funciones, esto es un desperdicio de tiempo y esfuerzo. Si conoces las funciones que
necesitas llamar, entoces es una cuestin de segundos mirar los parmetros exactos en la
ayuda de tu compilador. Si no tienes los archivos de ayuda entonces consigue algunos
porque sin ellos estas perdido. Eventualmente irs aprendiendo los parmetros de las
funciones que mas usamos.
HWND hwnd;
hwnd = CreateWindowEx(
WS_EX_CLIENTEDGE,
g_szClassName,
"The title of my window",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
NULL, NULL, hInstance, NULL);
La principal causa de que las personas no conozcan cuales son las fallas de sus programas
es que no chequean los valores de retorno de las llamadas a funciones para determinar si
stas han fallado o no. CreateWindow( ) puede fallar an si eres un experimentado
programador debido a que hay una gran cantidad de errores que son fciles de cometer.
Hasta que aprendas como identificar rpidamente dichos errores date la oportunidad de
resolverlos y siempre chequea los valores de retorno!
if(hwnd == NULL)
{
MessageBox(NULL, "Window Creation Failed!", "Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
Despus que hemos creado la ventana y hemos chequeado para asegurarnos de que
tengamos un handle vlido, mostramos la ventana utilizando el ltimo parmetro provisto
en WinMain( ) y luego lo actualizamos para asegurarnos que se halla redibujado en la
pantalla a si mismo de manera apropiada.
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
Este es el corazn del programa, la mayoria de las cosas que tu programa realiza pasan a
travs de este punto de control.
TranslateMessage( ) hace algunos procesos adicionales en los eventos del teclado, como
generar mensajes WM_CHAR que van junto a los mensajes WM_KEYDOWN. Finalmente
DispatchMessage( ) enva el mensaje a la ventana que haba recibido dicho mensaje. Esta
podra ser nuestra ventana principal, otra ventana, o un control e incluso una ventana que
fu creada "detrs de la escena" por el sistema o algn otro progama. Esto no es algo que
debera precocuparte debido a que lo nico que nos interesa saber es que obtenemos el
mensaje y lo envamos, el sistema se encarga de hacer el resto asegurndose de trabajar con
la ventana correcta.
Bien, tenemos una ventana pero esta no hace nada excepto lo que DefWindowProc( )
le permite que haga: cambiar el tamao, ser maximizada, minimizada etc... Nada
realmente excitante.
En la siguiente seccin voy a mostrar como modificar lo que ya tenemos para que haga algo
nuevo. As, yo solo podra decirte "Procesa este mensaje y haz esto en l..." y tu sabrs lo
que digo y sers capaz de hacerlo sin mirar un ejemplo entero. Esto es lo que espero, de
todas maneras presta atencin.
El orden en cual manejamos los mensajes raramente importa. Solo asegrate de poner el
"break" despues de cada uno. Como puedes ver necesitamos otro case dentro de nuestro
switch(). Ahora queremos que algo suceda cuando llegamos a esta nueva parte de nuestro
programa.
Primero presentar el cdigo que queremos agregar (el que le mostrar al usuario el nombre
del archivo del programa) y luego lo integrar dentro del nuestro programa. Mas adelante,
probablemente solo te muestre el cdigo y te dejar que lo integres tu mismo. Esto es, por
su puesto, mejor para mi porque no tengo que andar tipeando demasiado y mejor para vos
porque sers capaz de agregar el cdigo dentro de CUALQUIER programa, no solamente el
que estamos presentando. De todas maneras, si no ests seguro de como hacerlo puedes
mirar el cdigo fuente correspondiente a esta seccin includo en el archivo zip.
Este cdigo no puede ser copiado en cualquier lugar dentro de nuestro programa.
Especificamente queremos que ste se ejecute cuando el usuario clickea el mouse, por lo
tanto veremos como poner dicho cdigo dentro de nuestro programa:
Observa el nuevo conjunto de llaves { }. Estas son necesarias cuando declaramos una
variable dentro de una sentencia switch( ). Esto es conocimiento bsico de C pero pienso
que es bueno recordarlo para aquellos que estn haciendo las cosas por el camino difcil.
Una vez que has agregado el cdigo, complalo. Si funciona, has click dentro de la ventana
y vers desplegado un cuadro con el nombre del archivo .exe .
Habrs notado que hemos agregado dos variables, hInstance y szFileName. Observa la
funcin GetModuleFileName y vers que el primer parmetro es un HINSTANCE
refirindose al mdulo ejecutable (nuestro programa,el archivo .exe). Donde obtenemos
eso? GetModuleHandle( ) es la respuesta. Las referencias de GetModuleHandle( )
indican que si pasamos NULL como parmetro, nos retornar un "Handle al archivo usado
para crear el proceso de llamada" que es exactamente lo que necesitamos: el HINSTANCE
anteriormente mencionado. Poniendo todo esto, finalizamos con la siguiente declaracin:
char szFileName[MAX_PATH];
Por lo tanto, si has agregado esto en el cdigo, complalo. Si funciona haz click en la
ventana y debers ver desplegado un mensaje con el nombre del archivo.
Si esto no funciona, aqu esta el cdigo completo. Compralo con el tuyo para encontrar
que errores has cometido.
#include <windows.h>
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszMenuName = NULL;
wc.lpszClassName = g_szClassName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if(!RegisterClassEx(&wc))
{
MessageBox(NULL, "Window Registration Failed!", "Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
hwnd = CreateWindowEx(
WS_EX_CLIENTEDGE,
g_szClassName,
"The title of my window",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
NULL, NULL, hInstance, NULL);
if(hwnd == NULL)
{
MessageBox(NULL, "Window Creation Failed!", "Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
Que es un Mensaje?
Un mensaje es un valor entero. Si observas en tus archivos de cabecera (lo cual es una
buena prctica cuando investigamos el funcionamiento de las API`s) puedes encontrar
cosas como esta:
#define WM_INITDIALOG 0x0110
#define WM_COMMAND 0x0111
Cada mensaje en windows puede tener hasta dos parmetros, wparam y lparam.
Originalmente wparam tena 16 bits y lparam tena 32 bits, pero en Win32 ambos son de 32
bits. No todos los mensajes usan estos parmetros y cada mensaje los usa de manera
diferente. Por ejemplo, el mensaje WM_CLOSE no usa ninguno de los dos y por lo tanto
deberias ignorarlos. El mensaje WM_COMMAND usa ambos, wparam contiene dos valores
HIWORD(wparam) es la notificacin del mensaje (si se aplica) y LOWORD(wparam) es el ID
del control o men que envi el mensaje. lparam es el HWND (Windows Handle) del control
que envi el mensaje o nulo (NULL) si el mensaje no proviene de un control.
HIWORD( ) y LOWORD( ) son macros definidas por windows que simplemente retornan la
palabra superior (High Word) y la palabra inferior (Low Word), respectivamente, de un
valor de 32 bits. En Win32 una palabra es un valor de 16 bits, haciendo un DWord (palabra
doble) un valor de 32 bits.
Una vez que comiences a usar cajas de dilogo necesitars enviar mensajes a los controles
para poder comunicarte con ellos. Puedes hacer esto primero atravs de GetDlgItem( )
(utilizando el ID como parmetro) para obtener el handle al control y luego usar
SendMessage( ). O puedes usar SendDlgItemMsg( ), el cual combina ambos pasos.
Como parmetros utilizamos el handle de la ventana y un ID de un hijo y
SendDlgItemMsg( ) retorna el handle del hijo y le enva el mensaje. Ambas formas de
enviar mensajes funcionan bien en todas las ventanas, no solamente en cajas de dilogo.
He intentado esto con el cdigo del ejemplo anterior y funciona. Sin embargo hay varios
aspectos como la traduccin UNICODE/ANSI, entre otros, que este mtodo no tendr en
cuenta y probablemente haga fallar hasta la aplicacin mas trivial. Por lo tanto hazlo como
prueba pero no lo hagas en el cdigo real.
Como puedes ver tu aplicacin pasa la mayor parte del tiempo dando vueltas y vueltas en el
bucle de mensajes donde alegremente envas mensajes a las felices ventanas que los
procesarn. Pero que haces cuando quieres que tu programa salga? Debido a que estamos
usando un bucle while( ) si GetMessage( ) retornara FALSE (i.e 0), el bucle podra
finalizar y de esta manera podramos alcanzar el final de nuestro WinMain( ), es decir salir
del programa. Esto es exactamente lo que PostQuitMessage() realiza; pone un WM_QUIT
en la cola y GetMessage( ) en vez de retornar un valor positivo, llena la estrucura MSG y
retorna 0. Hasta este punto el campo wparam de la variable Msg contiene el valor que le
pasamos en PostQuitMessage( ) y podemos ignorarlo o retornarlo al WinMain( ) donde
se usar como valor de salida cuando el programa finalice.
Deberan usarse este o cdigos que tengam siempre el mismo efecto. Espero que ahora
tengas un mejor entendimiento del bucle de mensajes, si no es as, no te asustes, las cosas
tendrn mas sentido despus que hayan sido usadas un par de veces.
Uso de Recursos
Puedes mirar los apndices al final del tutorial para mas informacin sobre el uso de
recursos con VC++ y BC++.
Antes de ver el tema en profundidad voy a cubrir el tema de recursos as no tendr que
reescribir el mismo en cada seccin. Por ahora no necesitas compilar el codigo, solo es un
ejemplo.
Los recursos son porciones pre-definidas de datos que se almacenan en formato binario
dentro de los archivos ejecutables y se organizan dentro de un archivo con extensin ".rc"
(resource script). Los compiladores comerciales cuentan con un editor visual de recursos
que permite crear recursos sin editar manualmente este archivo. A veces la nica manera de
poder crear un recurso es editar este archivo manualmente sobre todo si tu compilador no
tiene un editor visual o no soporta la caractersitica exacta que necesitas.
El editor de recursos includo con MSVC++ hace muy difcil la tarea de editar los recursos
manualmente, debido a que fuerza un formato propio en ellos y si intentas guardar uno que
has creado manualmente estropea completamente el archivo. En general no deberas
preocuparte por crear archivos de recursos desde cero, pero conocer como modificarlos
manualmente puede ser muy til. Otra incomodidad es que MSVC++ nombrar el archivo
de recuros por default con el nombre "resource.h" an si deseas llamarlo con otro nombre.
Utilizaremos este con el propsito de simplicidad para este documento, pero te mostrar
como cambiarlo en el apndice sobre compiladores.
#include "resource.h"
Que hay acerca de #include "resource .h"?. Bien tu programa necesita una forma de
indentificar el cono y la mejor forma de hacerlo es asignndole a este un nico ID
(IDI_MYICON). Podemos hacer esto creando el archivo de cabecera "resource.h" e incluir
ste tanto en el archivo de recursos (.rc) como en el programa (.c)
Como puedes ver, hemos asignado a IDI_MYICON el valor 101. De ahora en mas podramos
olvidarnos del identificador y simplemente usar 101 cada vez que necesitamos referenciar
el cono. Pero IDI_MYICON es un poco mas claro, mas representativo y mas facil de
recordar, sobre todo si estamos trabajando con una gran cantidad de recursos.
#include "resource.h"
IDR_MYMENU MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "E&xit", ID_FILE_EXIT
END
END
Generar y mantener un registro de todos los identificadores de recursos puede ser una tarea
costosa cuando trabajamos con grandes proyectos, esta es la razn por la que muchas
personas utilizan un editor visual de recursos, el cual se encarga de hacer todo este trabajo
por nosotros. Aunque pueden generar problemas de vez en cuando y podras finalizar con
multiples recursos con el mismo ID o problemas similares, es bueno ser capaz de ir y poder
corregir esos errores uno mismo.
Cuando se les pasa un valor entero LoadIcon( ) y otras APIs que cargan recursos pueden
identificar la diferencia entre un entero y un puntero a un entero chequeando la palabra
superior de dicho valor. Si es cero (como podra ser el caso de un entero con un valor
menor a 65535) entonces asume que se trata del ID de un recurso. Esto efectivamente nos
limita a usar IDs inferiores a 65535, que a menos que tengas una gran cantidad de recursos,
no deberi ser un problema. Si es distinto de cero, entonces asume que el valor es un
puntero y busca el recurso por el nombre. Nunca le confes este trabajo a la API a menos
que est explicitamente establecido en la documentacin. Por ejemplo, esto no funciona
para comandos del men como ID_FILE_EXIT debido a que solo pueden ser enteros.
Menes e Iconos
Ejemplo: menu_one
Esta es slo una pequea seccin para mostrar como agregar menes bsicos en nuestra
ventana. Generalmente utilizaremos un recurso men pre-definido que estar en un archivo
.rc y que luego ser compilado y enlazado con el archivo .exe. Esto suele depender de cada
compilador, los compiladores comerciales cuentan con un editor de recursos que puedes
utilizar para crear tus menes, pero en este ejemplo voy a mostrar el texto del archivo .rc
para que puedas agregar menes manualmente. Generalmente tengo un archivo .h que
incluyo dentro de mis archivos .c y .rc .Este archivo contiene los identificadores para
controles, items de un men, etc...
En este ejemplo puedes comenzar con el cdigo de la ventana que vimos en el ejemplo
simple_window, e ir agregando el cdigo de esta seccin.
No hay demasiado aqu, pero nuestro men ser muy simple. Los nombres y valores puedes
escogerlos arbitrariamente. Ahora escribamos nuesto archivo .rc
#include "resource.h"
IDR_MYMENU MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "E&xit", ID_FILE_EXIT
END
POPUP "&Stuff"
BEGIN
MENUITEM "&Go", ID_STUFF_GO
MENUITEM "G&o somewhere else", 0, GRAYED
END
END
Debers agregar el archivo .rc a tu project o makefile dependiendo de que herramienta ests
usando.
Tambin debes agregar la linea #include "resource.h" en tu archivo fuente (.c) para que
los identificadores de los comandos del men y los del recurso men estn definidos.
La forma mas fcil de asignar un men y un cono a nuestra ventana es especificar ambos
cundo registramos la clase ventana, como esto:
wc.lpszMenuName = MAKEINTRESOURCE(IDR_MYMENU);
wc.hIcon = LoadIcon(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDI_MYICON));
wc.hIconSm = (HICON)LoadImage(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDI_MYICON), IMAGE_ICON, 16, 16, 0);
Cambia esto y observa que sucede. Tu ventana ahora debera tener un men File y un men
Stuff, con los respectivos items dentro. Esto es asumiendo que nuestro archivo .rc fu
apropiadamente compilado y enlazado en nuestro programa. (nuevamente mira las notas
sobre compiladores)
He usado LoadIcon( ) para cargar el cono grande debido a que es mas simple, sin
embargo solo cargar los conos en la resolucin por default de 32x32, por lo tanto para
cargar la imagen pequea necesitamos usar LoadImage( ) .Observa que los archivos de
conos y de recursos pueden contener mltiples imgenes y en este caso, la nica imagen
que hay, contiene los dos tamaos que estamos cargando.
Ejemplo: menu_two
Una alternativa cuando usamos el recurso men es crear uno en tiempo de ejecucin. Esto
lleva un poco mas de trabajo de programacin, pero agrega mas flexibilidad y a veces es
necesario.
Tambin podemos usar conos que no estn almacenados como recursos, podramos elegir
almacenar nuestros conos en un archivo separado y cargarlos en tiempo de ejecucin. Esto
tambin podra darnos la opcin de permitir al usuario seleccionar un cono con los
dialogos comunes (common dialogs) que veremos luego, o alguna otra opcin que tenga el
mismo efecto.
Por esta vez pon estos dos ID's en el comienzo de tu archivo .c, debajo de los #include's.
A continuacin agregaremos el siguiente cdigo dentro de nuestro manejador del mensaje
WM_CREATE.
case WM_CREATE:
{
HMENU hMenu, hSubMenu;
HICON hIcon, hIconSm;
hMenu = CreateMenu();
hSubMenu = CreatePopupMenu();
AppendMenu(hSubMenu, MF_STRING, ID_FILE_EXIT, "E&xit");
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu, "&File");
hSubMenu = CreatePopupMenu();
AppendMenu(hSubMenu, MF_STRING, ID_STUFF_GO, "&Go");
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu,
"&Stuff");
SetMenu(hwnd, hMenu);
Este crea un men igual al que hemos definido en nuestro archivo de recursos y se lo asigna
a nuestra ventana. Un men que es asignado a una ventana es automticamente removido
cuando el programa termina, por lo tanto no necesitamos preocuparnos por librarnos de l
cuando el programa finalice.
El cdigo para los conos es muy simple, llamamos a LoadImage( ) dos veces: primero
para cargar el cono de 16x16 y luego el cono de 32x32. No podemos usar LoadIcon( )
en todo momento debido a que ste slo carga recursos, no archivos. En el parmetro del
handle a la instancia, especificamos el valor NULL debido a que no estamos cargando un
recurso de nuestro mdulo y en vez de pasar el ID de un recurso, pasamos el nombre del
archivo donde se encuentra el cono. Por ltimo, pasamos el flag LR_LOADFROMFILE para
indicar que la funcin trate el string que le pasamos como un nombre de archivo y no como
el nombre de un recurso.
hMenu = CreateMenu();
hSubMenu = CreatePopupMenu();
AppendMenu(hSubMenu, MF_STRING, ID_FILE_EXIT, "E&xit");
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu,
"&File");
hSubMenu = CreatePopupMenu();
AppendMenu(hSubMenu, MF_STRING, ID_STUFF_GO, "&Go");
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu,
"&Stuff");
SetMenu(hwnd, hMenu);
break;
case ID_STUFF_GO:
break;
}
break;
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, Message, wParam, lParam);
}
return 0;
}
Como podemos ver, hemos procesado el mensaje WM_COMMAND y ste tiene adentro otro
switch( ). Este switch( ) distingue usando el valor de la palabra inferior de wparam: si
es el caso de un mensaje WM_COMMAND, entonces contiene el ID del control o men que
envi el mensaje.
Obviamente queremos que el item Exit cierre el programa. Por lo tanto en el procesador del
mensaje WM_COMMAND, ID_FILE_EXIT usaremos el siguiente cdigo:
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_FILE_EXIT:
PostMessage(hwnd, WM_CLOSE, 0, 0);
break;
case ID_STUFF_GO:
break;
}
break;
Te dejo como tarea realizar el otro comando ID_STUFF_GO para que haga algo.
Habrs notado que ahora el archivo menu_one.exe muestra como cono el cono que
agregamos como recurso, mientras que menu_two.exe no lo hace debido a que estamos
cargando un archivo externo. El Windows Explorer simplemente muestra el primer icono
(ordenados numericamente por ID) y debido a que slo tenemos un cono, este es el que
ser mostrado. Si quieres asegurarte de que un cierto cono sea mostrado por el Windows
Explorer simplemente agrega este como recurso y asgnale un numero bajo de ID... como 1.
De aqu en ms, en tu programa, ya no necesitas referirte al archivo que contiene al cono y
puedes asignarle a cada ventana un cono diferente si lo deseas.
Dilogos, los mejores amigos de los
programadores de GUIs
Ejemplo: dlg_one
Los dilogos no estn limitados a los dilogos estandar para abrir un archivo, por el
contrario, pueden lucir como se te ocurra. El punto atractivo de los dilogos es que proveen
una forma rpida de ordenar y crear Interfaces Graficas al Usuario (GUI) y tambin realizar
algunos procesos por default, disminuyendo la cantidad de cdigo que debemos escribir.
Una cosa para recordar es que los dilogos son solo ventanas. La diferencia entre un
dialogo y una ventana "normal" es que, con los dilogos, el sistema hace algunos procesos
adicionales por default, como crear e inicializar controles y manipular el orden del tab.
Todas las APIs que son aplicables a las ventanas "normales" funcionan correctamente en
los dilogos y viceversa.
El primer paso es crear el recurso dialogo. La manera en que se hace depende de cada
compilador/IDE, al igual que con todos los recursos. Aqui, voy a mostrarte el texto plano
del dialogo en el archivo .rc y debes incorporarlo tu mismo en tu proyecto.
La segunda linea comienza con STYLE y sigue con los estilos de ventana que sern usados
para crear el dilogo. Esto debera estar explicado en CreateWindow() en tus archivos de
ayuda. Para poder usar las constantes predefinidas necesitamos agregar #include
"windows.h" en nuestro archivo de recursos .rc o en el caso de VC++ podemos usar
"winres.h" o "afxres.h". Si utilizas un editor de recursos estos archivos sern includos
automticamente, si es necesario.
La lnea FONT especifica el nombre y tamao de la fuente que utilizaremos para este
dilogo. Esta puede no ser la misma para todas las computadoras, depende de las fuentes
que cada persona tenga instaladas y de los tamaos que haya especificado.
DEFPUSHBUTTON "&OK",IDOK,174,18,50,14
Esta es la lnea para el botn OK. El "&" indica que la letra que sigue (en este caso la "O")
puede ser combinada con la tecla Alt para activcar el control mediante el teclado (Alt+O,
en este caso). Todo esto es parte del proceso automtico que he mensionado. IDOK es el
identificador del control. IDOK es un control pre-definido por lo tanto no necesitamos
definirlo. Los cuatro nmeros al final son las coordenadas X e Y de la esquina superior
izquierda y el ancho y alto de botn, todas en unidades de dilogos.
Si utilizas un editor de recursos para crear dilogos, esta informacin debera ser puramente
acadmica, de todos modos suele ser necesario conocer como hacerlo directamente en el
archivo de recursos (.rc), especialmente si no tienes un editor visual de recursos.
Hay dos controles que tienen un ID IDC_STATIC (el cual es -1), esto es usado para indicar
que nunca necesitamos acceder a ellos, por lo tanto no necesitamos un identificador. Sin
embargo esto no quita la posibilidad de darle un ID o que editor de recursos lo haga
automticamente.
El texto "\r\n" en la linea del CTEXT es el par CR-LF que representa el comienzo de una
nueva linea.
Bien, habiendo agregado esto a nuestro archivo de recursos (.rc) necesitamos escribir un
Dialog Procedure para procesar los mensajes de este dialogo. No te preocupes esto no es
nada nuevo, prcticamente es lo mismo que con nuestro Window Procedure (aunque no
exactamente).
return TRUE;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDOK:
EndDialog(hwnd, IDOK);
break;
case IDCANCEL:
EndDialog(hwnd, IDCANCEL);
break;
}
break;
default:
return FALSE;
}
return TRUE;
}
Otra diferencia es que, generalmente, retornamos FALSE para aquellos mensajes que no
procesamos y TRUE para aquellos que si procesamos, A MENOS que el mensaje especifique
que valor debemos retornar. Observa que esto es lo que hicimos anteriormente, por default,
no debemos hacer nada y retornar FALSE, mientras que los mensajes que si procesamos
entran en el switch() y retornan TRUE.
Suficiente, hagmoslo...
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_HELP_ABOUT:
{
int ret = DialogBox(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDD_ABOUT), hwnd, AboutDlgProc);
if(ret == IDOK){
MessageBox(hwnd, "Dialog exited with IDOK.", "Notice",
MB_OK | MB_ICONINFORMATION);
}
else if(ret == IDCANCEL){
MessageBox(hwnd, "Dialog exited with IDCANCEL.",
"Notice",
MB_OK | MB_ICONINFORMATION);
}
else if(ret == -1){
MessageBox(hwnd, "Dialog failed!", "Error",
MB_OK | MB_ICONINFORMATION);
}
}
break;
// Other menu commands...
}
break;
Este es el cdigo que he usado para crear mi dialogo "About", seguramente te preguntas por
que est puesto dentro del manejador WM_COMMAND, si no tienes claro este aspecto, puedes
revisar la seccin de menus e iconos. ID_HELP_ABOUT es el dentificador del item men
Help\About.
Debido a que queremos que el men de nuestra ventana pricipal cree el dilogo,
obviamnete debemos poner este cdigo en el WinProc( ) de nuestra ventana principal, no
en el Dialog Procedure.
IDD_ABOUT es el ID del recurso dilogo. hwnd el el handle a la ventana padre del dilogo.
AboutDlgProc( ) es, por su puesto, el Dialog Procedure usado para controlar el dilogo.
Dilogos de Modelamiento
Ejemplo: dlg_two
Es una buena idea chequear siempre los valores de retorno, y si son vlidos (distintos de
NULL) mostramos la ventana usando ShowWindow(), con DialogBox( ) esto no es
necesario debido a que el sistema llama a ShowWindow() por nosotros.
case WM_DESTROY:
DestroyWindow(g_hToolbar);
PostQuitMessage(0);
break;
Por ltimo, pero no menos importante, queremos ser capaz de mostrar y ocultar nuestra
barra de herramientas cada vez que el usuario lo desee, por lo tanto agregamos dos
comandos al men y los procesamos as:
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_DIALOG_SHOW:
ShowWindow(g_hToolbar, SW_SHOW);
break;
case ID_DIALOG_HIDE:
ShowWindow(g_hToolbar, SW_HIDE);
break;
//... other command handlers
}
break;
Hasta aqu, debes ser capaz de crear tu propio men usando el editor de recursos o
manualmente, si no es as (como pasa siempre) puedes observar el ejemplo dlg_two que
viene junto a este tutorial.
Si ejecutaste el programa e intentaste correr el foco entre los dos botones usando la tecla
tab, probablemente habrs observado que esto no funciona, como asi tampoco funciona
activar los botones presionando Alt-O o Alt-P. Por que no? DialogBox() implementa su
propio bucle de mensajes y procesa estos eventos por default, pero CreateDialog( ) no lo
hace. Podemos hacerlo nosotros, llamando a IsDialogMessage( ) en nuestro bucle de
mensajes, el cual har los procesos por default en lugar nuestro.
Es importante notar que IsDialogMessaje( ) tambin puede ser usado con ventanas que
no son dilogos, con la intencin de darles el comportamiento de un dilogo. Recuerda que
un dilogo es una ventana y la mayora (por no decir todas) de las API's para dilogos
funcionarn correctamente en cualquier ventana.
Un aspecto para destacar es que pasa si tienes mas de una barra de herramientas? Bien,
una posible solucin es tener una lista (o un arreglo) y ciclar en nuestro bucle de mensajes
pasando cada handle a IsDialogMessage( ) hasta que encontremos el correcto y si no lo
es, hacemos el procesamiento regular. Este es un problema de programacin genrico y no
uno que est relacionado con Win32, por lo tanto se deja como ejercicio para el lector.
Ya hemos usado botones en ejemplos anteriores, por lo tanto deberas estar mas o
menos familiarizado con ellos, sin embargo debido a que he usado botones en este
ejemplo los he agregado como parte del ttulo solo con el propsito de completitud.
Controles
Una cosa para recordar acerca de los controles es que son solo ventanas. Al igual que
cualquier otra ventana tienen un window procedure, una clase ventana etc... que es
registrada por el sistema. Todo lo que puedes hacer con las ventanas normales, puedes
hacerlo con los controles.
Mensajes
Los mensajes varan ampliamente para cada control, y cada control tiene su propio conjunto
de mensajes. En algn momento el mismo mensaje ser usado para mas de un tipo de
control, pero en general solo funcionarn en el control para el cual estn destinados. Esto es
especialmente molesto con los mensajes listbox y combobox (LB* y CB* ) los cuales si
bien realizan tareas casi idnticas NO SON intercambiables.
Por otro lado, los mensajes genricos como WM_SETTEXT son soportados por casi todos los
controles. Despus de todo un control es una ventana.
Aqui est el cdigo usado como interface para el control edit de este ejemplo:
Esto es para cambiar el texto contenido dentro del control (esto puede ser usado para
cualquier control que tenga un valor de texto asociado a l, STATICs, BUTTONs, etc...).
Recuperar el texto de un control tambin es fcil, si bien es apenas mas trabajo que
cambiarlo...
GlobalFree((HANDLE)buf);
}
Primero que todo, necesitamos reservar memoria para almacenar el string, para esto
necesitamos conocer cuanta memoria reservar. No hay una funcin
GetDlgItemTextLenght( ), pero existe GetWindowTextLenght( ), por lo tanto todo lo
que necesitamos hacer es obtener el handle al control usando GetDlgItem( ).
Ahora que sabemos la longitud, podemos reservar la memoria necesaria para almacenar el
string. Aqu, he agregado un chequeo para ver si existe algn texto (i.e la longitud del string
es mayor que cero) para no estar trabajando sobre un string vaco. Asumiendo que existe
dicho texto llamamos a GlobalAlloc( ) para reservar la memoria. Si eres usuario de
DOS/UNIX GlobalAlloc( ) es equivalente a calloc( ). Esto reserva algo de memoria,
inicializa su contenido a cero (0) y retorna un puntero a dicha memoria. Hay diferentes
flags que puedes pasar en el primer parmetro para hacer que se comporte de manera
diferente para diferentes propsitos, pero esta es la nica manera en que la voy a usar
durante el resto del tutorial.
Observa que le he sumado 1 a la longitud del string en dos lugares, Por que? Bien,
GetWindowTextLenght( ) retorna el nmero de caracteres que contiene el texto del
control SIN INCLUIR el terminador nulo. Esto significa que si no sumamos 1 el texto
podra caber pero el terminador nulo sobrepasara los limites del bloque de memoria,
causando una violacin de acceso o sobreescribiendo otros datos. Debes ser cuidadoso
cuando trabajas con longitudes de strings en windows, debido a que algunas APIs y
mensajes esperan que las longitudes incluyan el caracter nulo y otras no lo hacen. Siempre
lee la documentacin cuidadosamente.
Hay un segundo conjunto de APIs llamadas LocalAlloc( ), LocalFree( ), etc... que son
parte de las APIs de Windows 16-bits. En Win32 las funciones para memoria Local* y
Free* son identicas.
Cuando ingresamos texto est todo bien, pero que pasa si queremos que el usuario ingrese
un numero? Esta es una tarea muy comn y afortunadamente hay una API para hacer esto
mas simple. Esta API se encarga de toda la asignacin de memoria, como tambien convertir
el string a un valor entero.
BOOL bSuccess;
int nTimes = GetDlgItemInt(hwnd, IDC_NUMBER, &bSuccess, FALSE);
Otra caracterstica muy usada para controles de edicin es el estilo ES_NUMBER, el cual
permite que solo sean ingresados los caracteres del 0 al 9. Esto el muy til si solo quieres
que se ingresen enteros positivos, en otro caso no tiene mucha utilidad debido a que no
puedes ingresar caracteres como el menos (-), el punto decimal (.) o la coma (,).
Listas
Otro control muy usado son las listas. Este es el ltimo control estandar que voy a cubrir
por ahora, debido a que francamente no hay muchos interesantes y si an no ests aburrido,
yo si lo estoy :)
Agregado de Items
La primer cosa que quieres hacer con una lista es agregar items en ella.
Como puedes ver, esta es una tarea muy simple. Si la lista tiene el estilo LBS_SORT, el
nuevo item ser agregado en orden alfabtico, en otro caso simplemente ser agregado al
final de la lista.
Este mensaje retorna el ndice del nuevo item, el cual podemos usar para realizar alguna
tarea sobre el tem, como asociar algunos datos a l. Generalmente esto es como un puntero
a una estructura conteniendo mas informacin, o quizs un ID que usars para identificar el
tem.
Notificaciones
El proposito de las listas es permitir al usuario seleccionar cosas de una lista. Sin embargo,
a veces queremos ser capaz de hacer algo en el acto, quizs mostrar informacin diferente o
actualizada, basada en los items que fueron seleccionados. Para hacer esto necesitamos
procesar los mensajes de notificacin que nos enva la lista. En este caso estamos
interesados en LBN_SELCHANGE, el cual nos dice que la seleccin ha sido modificada por el
usuario. LBN_SELCHANGE es enviado via WM_COMMAND, pero a diferenecia del procesamiento
de los mensajes WM_COMMAND que hacemos con los botones y menes, que son generalmente
en respuesta a un click, una lista enva el mensaje WM_COMMAND por varias razones y
necesitamos un segundo chequeo para averiguar que nos est diciendo. El Cdigo de
Notificacin es pasado como el HIWORD de wParam, la otra mitad del parmetro nos da el ID
del control.
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDC_LIST:
// It's our listbox, check the notification code
switch(HIWORD(wParam))
{
case LBN_SELCHANGE:
// Selection changed, do stuff here.
break;
}
break;
// ... other controls
}
break;
En este ejemplo he usado una lista multi-seleccin, por lo tanto obtener la lista de items
seleccionados es un poco mas complicado. Si fuera una lista de seleccin simple,
podramos simplemente envar LB_GETCURSEL para recuperar el ndice del item.
Primero, necesitamos obtener los indices de los items seleccionados para que podamos
reservar un bffer de memoria en donde guardar los ndices.
GlobalFree(buf);
En este ejemplo, buf[0] es el primer ndice, buf[1] el segundo, etc.. siendo el ltimo
indice buf[count-1].
Una de las cosas que podrias querer realizar con la lista de ndices, es recuperar los datos
asociados con cada item y luego hacer algn proceso con ste. Esto se hace simplemente
enviando otro mensaje.
Si los datos tienen algn otro tipo de valor (cualquier cosa distinta de 32-bits) simplemente
debemos convertirlo al tipo apropiado. Por ejemplo si almacenamos HBITMAPs en vez de
ints...
Estticos
Al igual que los botones, los controles estticos son ampliamente triviales, pero una vez
mas por el propsito de completitud, voy a incluirlos aqu. Los controles estticos son
generalmente aquello que es, esttico, significando que no cambian o no hacen nada en
especial. Son muy usados para mostrar texto al usuario, sin embargo puedes hacer de ellos
algo un poco mas util asignndoles un nico ID (VC++ asigna un ID por default,
IDC_STATIC, que tiene un valor de -1 y significa "Sin ID") y luego asignarles el texto en
tiempo de ejecucin para presentar texto dinmico al usuario.
En el ejemplo, he usado uno para mostrar los datos de los items seleccionados en la caja de
lista, asumiendo que se puede seleccionar solo un item.
Dilogos FAQ
Ejemplo: dlg_three
Windows enva a nuestro dialog procedure una gran cantidad de mensajes relacionados a
los colores y procesando dichos mensajes podemos cambiar el color de ciertas cosas. Por
ejemplo para cambiar el color de el dilogo en si mismo podemos procesar el mensaje
WM_CTLCOLORDLG, para cambiar el color de un control esttico podemos procesar el mensaje
WM_CTLCOLORSTATIC, etc...
Primero debemos crear un pincel (Brush) para pintar el fondo y luego almacenarlo para
poder usarlo despus. El mensaje WM_CTLCOLORDLG y otros mensajes relacionados sern
llamados an durante el trancurso de la ejecucin de nuestro programa y si cada vez que
esto sucede creamos un pincel podriamos desperdiciar una gran cantidad de RAM con
pinceles muertos. De esta forma tenemos mas control y podemos borrarlo cuando el dilogo
es destrudo, es decir, cuando ya sabemos que no lo vamos a necesitar mas.
Observa la lnea que fija el color de fondo en modo transparente... si quitamos esta linea el
fondo ser llenado con el pincel que hemos especificado, pero cuando el control dibuje el
texto, ste ser escrito encima con el color de fondo por default! Poniendo el modo de
dibujo de texto en transparente corregimos este problema. La otra opcin sera usar
SetBkColor( ) para poner como color de fondo el mismo color del pincel, pero prefiero la
otra solucin.
Cambiar los colores en la mayoria de los controles estndar funciona de la misma manera,
solo hay que observar los mensajes WM_CTLCOLOR* en la referencia de Win32. Observa que
un control Edit enviar un mensaje WM_CTLCOLORSTATIC si es de solo lectura y
WM_CTLCOLOREDIT si no lo es.
Si tienes mas de un control esttico y quieres que tengan diferentes colores, entonces
cuando procesas los mensajes, necesitars chequear el ID del control que enva el mensaje
para identificar de que control proviene y asi cambiar los colores al control adecuado.
Tenemos el HWND del control en lParam y a partir de este podemos obtener el ID del control
usando GetDlgCtrlID( ). Observa que el editor de recursos les da a los controles estticos
un ID IDC_STATIC igual a -1, por lo tanto si necesitamos distinguirlos debemos asignarles
nuevos IDs.
Para poner el icono de aplicacin por default, podemos usar el siguiente cdigo:
Cuando reemplazes tu propio cono por uno por default, recuerda cambiar el parmetro
HINSTANCE de LoadIcon( ) por el de la instancia de tu aplicacin (si no lo tienes
almacenado en WinMain( ), puedes obtener ste llamando a GetModuleHandle( )).
Por ejemplo, si le damos al control una altura de 100 pixeles, el sistema ajusta el tamao
del control con un valor por default (digamos 30 en este caso) y cuando hacemos click en la
flecha la lista desplegada podra ser de una mximo de 70 pixeles, haciendo un total de 100
pixeles.
Si utilizas el editor de recursos de VC++ para ubicar el combo box dentro de tu dialogo,
observars que no puedes cambiar el tamao vertical. A menos que hagas click en la flecha
en el editor y ste cambiar el foco para indicar que ests cambiando el tamao de la lista
desplegada. Luego puedes especificar el ancho con el valor que quieras.
Quiero darte un link a una pgina muy buena en MSDN, pero Microsoft parece estar
determinado a impedirme dar links a pginas individuales, debido a que cambian
rpidamente o no funcionan por un perodo. Por lo tanto tendrs que darte cuenta como
llegar vos mismo, busca las secciones como User Interface Services y Windows Controls, a
veces dentro de la seccin Plataform
Pienso que dar un ejemplo sobre crear controles en ejecucin, si bien es muy usado,
podra ser en vano a menos que la aplicacin realmente haga algo til, por lo tanto en
esta seccin voy a comenzar con el desarrolo de un editor de texto y lo iremos
desarrollando hasta que alcanzemos un programa til que soporte abrir, editar y guardar
archivos de textos.
El primer paso, el cual es cubierto por esta seccin, ser simplemente crear la ventana y el
control EDIT que servir como centro de nuestro programa.
Comenzaremos con el esquelto del cdigo de la aplicacin Simple Window, agregaremos
un #define para el ID de nuestro control y los siguientes dos manejadores de mensajes en
nuestro window procedure:
hfDefault = GetStockObject(DEFAULT_GUI_FONT);
SendMessage(hEdit, WM_SETFONT, (WPARAM)hfDefault,
MAKELPARAM(FALSE, 0));
}
break;
case WM_SIZE:
{
HWND hEdit;
RECT rcClient;
GetClientRect(hwnd, &rcClient);
Para crear los controles, al igual que lo hacemos para cualquier otra ventana, utilizamos la
API CreateWindowEx( ). Pasamos una clase pre-registrada, en este caso la clase del
control "EDIT", y obtenemos un control edit estndar. Cuando usamos dilogos para crear
nuestros controles, bsicamente estamos escribiendo una lista de controles a crear, tal que,
cuando llamamos a DialogBox( ) o CreateDialog( ) el sistema lee la lista de controles
en el recurso dilogo y por cada uno llama a CreateWindowEx( ), con la posicin y estilo
con que fueron definidos.
hEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "",
WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE |
ES_AUTOVSCROLL | ES_AUTOHSCROLL,
0, 0, 100, 100, hwnd, (HMENU)IDC_MAIN_EDIT,
GetModuleHandle(NULL), NULL);
if(hEdit == NULL)
MessageBox(hwnd, "Could not create edit box.", "Error", MB_OK |
MB_ICONERROR);
Puedes ver que esta llamada a CreateWindowEx( ) especifica cierta cantidad de
estilos y no es extrao tener alguno mas, especialmente para los
controles comunes los cuales tienen una larga lista de opciones. Los
primeros cuatro estilos WS_ deberian ser obvios, estamos creando el
control como un hijo de nuestra ventana, queremos que sea visible y tenga
barras de desplazamiento vertical y horizontal. Los tres estilos que son
especficos de los controles EDIT (ES_MULTILINE | ES_AUTOVSCROLL |
ES_AUTOHSCROLL) especifican que el control edit debe poseer mltiples
lineas de texto y desplazarse automticamente cuando tipeamos mas alla
del lmite inferior de la ventana y del limite derecho de la misma.
Los estilos regulares (WS_*) puedes encontrarlos en: listed here. y los
estilos extendidos (WS_EX_*) son explicados en: CreateWindowEx() estas
referencias son de MSDN donde tambin puedes encontrar links a los
estilos especficos de cada control (ES_* en el caso del control EDIT).
Debido a que esto es una tarea bastante comn, existen dilogos predefinidos por el sistema
que pueden ser usados para permitir al usuario seleccionar el nombre de un archivo. Los
dilogos mas comunes, para abrir y para guardar archivos, son accedidos a travs de
GetOpenFileName( ) and GetSaveFileName( ) respectivamente, ambos de los cuales
utilizan una estructura OPENFILENAME.
OPENFILENAME ofn;
char szFileName[MAX_PATH] = "";
ZeroMemory(&ofn, sizeof(ofn));
if(GetOpenFileName(&ofn))
{
// Do something usefull with the filename stored in szFileName
}
El campo lpstrFile apunta al bffer que hemos asignado para almacenar el nombre del
archivo, debido a que la longitud del nombre del archivo no puede ser mayor que MAXPATH,
ste es el valor que escogeremos para el tamao de dicho bffer.
Los flags indican que el dilogo slo debe permitir al usuario ingresar nombres de archivos
que ya existen (esto es porque queremos abrirlos, no crearlos) y para ocultar la opcin de
abrirlos en modo de slo lectura, opcin que no queremos proveer. Finalmente proveemos
una extensin por default, por lo tanto si el usuario tipea "foo" y el archivo no es
encontrado, intentaremos abrir "foo.txt".
En este caso no necesitamos que el archivo exista, pero si necesitamos que el directorio
exista debido a que no vamos a crearlo si no existe. Adems, si el archivo existe,
preguntamos al usuario si est seguro que desea sobreescribirlo.
lStructSize
Especifica la longitud de la estructura en bytes.
Sin embargo, en Windows, todos stos mtodos finalmente llaman a las funciones de la
API Win32, las cuales usaremos aqu. Si ests utilizando otro mtodo para la entrada/salida
de archivos, entonces te resultar fcil entender el mtodo que vamos a ver.
Lectura
Digamos, por ejemplo, que le has permitido al usuario seleccionar un archivo usando
GetOpenFileName( )...
Hay una funcin completa para leer un archivo de texto dentro de un control edit. Esta
funcin, toma como parmetro el handle al control edit y el nombre del archivo a leer.
Adems esta particular funcin tiene un buen chequeo de errores, debido a que la
entrada/salida de archivos est expuesta a una gran cantidad de ellos y por lo tanto,
necesitamos chequearlos.
Una vez que hemos abierto el archivo y hemos chequeado si CreateFile( ) se ejecut con
xito, chequeamos el tamao del archivo para averiguar cuanta memoria necesitamos
reservar para poder leer el archivo entero. Cuando reservamos dicha memoria, chequeamos
para asegurarnos que se hizo con xito, y luego llamamos a ReadFile( ) para cargar el
contenido del disco dentro del bffer en memoria. Las funciones de la API para archivos no
entienden sobre Archivos de Texto, por lo tanto no proveen mecanismos para leer una linea
de texto, o agregar terminadores NULL al final de nuestros strings. Esta es la razn por la
que hemos asignado un byte extra, despus de leer el archivo agregamos el terminador
NULL para que luego podamos pasar el bffer de memoria como si fuera un string a
SetWindowsText( ).
Una vez que todo esto se ha ejecutado con xito, ponemos en nuestra variable booleana
bSucces (que indica si la operacin se ha ejecutado con xito) el valor TRUE. Por ltimo,
liberamos el bffer de memoria y cerramos el archivo.
Escritura
BOOL SaveTextFileFromEdit(HWND hEdit, LPCTSTR pszFileName)
{
HANDLE hFile;
BOOL bSuccess = FALSE;
dwTextLength = GetWindowTextLength(hEdit);
// No need to bother if there's no text.
if(dwTextLength > 0)
{
LPSTR pszText;
DWORD dwBufferSize = dwTextLength + 1;
Es muy similar a leer archivos, pero con algunos cambios. Primero que todo, cuando
llamamos a CreateFile( ) especificamos que queremos acceso de lectura (Read), que el
archivo debera simpre ser creado nuevamente (y si existe debera ser borrado cuando es
abierto) y si no existe que sea creado con los atributos de los archivos normales.
A continuacin obtenemos del control edit, la longitud necesaria del bffer de memoria,
debido a que ste es la fuente de los datos. Una vez que hemos asignado la memoria,
solicitamos el string al control edit usando GetWindowText( ) y luego lo escribimos al
achivo con WriteFile( ). Nuevamnete, al igual que con ReadFile( ), es necesario el
parmetro que retorna cuanto fu escrito, an cuando no lo usemos.
Al igual que con todos los controles comunes, debemos llamar a InitCommonControl
ANTES de intentar usarlos. Necesitaremos #include <commctrl.h> para poder usar esta
funcin y para obtener la funciones y declaraciones necesarias para el uso de los controles
comunes. Tambin necesitaremos agregar comctl32.lib en la configuracin de nuestro
enlazador (linker), si no se encuentra configurado de esa manera. Observa que
InitCommonControls( ) es una vieja API, para obtener mas control podemos usar
InitCommonControlsEx( ) la cual es requerida por los ms recientes controles comunes.
Sin embargo, debido a que no voy a usar ninguna de las caractersticas avanzadas,
InitCommonControls( ) ser adecuado y mas simple.
Barras de Herramientas
Podemos crear una barra de herramientas usando CreateToolbarEx( ) pero no es la
idea... Lo que necesitamos hacer es crear la barra de herramientas...
Esto es bastante simple, TOOLBARCLASSNAME es una constante definida por los encabezados
(headers) de los controles comunes. hwnd es la ventana padre, la ventana en la que
aparecer la barra de herramientas. IDC_MAIN_TOOL es un identificador que podemos usar
posteriormente para obtener el HWND de la barra de herramientas por medio de GetDlgItem(
).
Este mensaje es necesario para permitirle al sistema darse cuenta que versin de la librera
de controles comunes estamos usando. Debido a que nuevas versiones agregan cdigo a la
estructura, dando el tamao de la misma el sistema puede darse cuenta que comportamiento
estamos esperando.
TBBUTTON tbb[3];
TBADDBITMAP tbab;
y luego agregamos los los bitmaps estndar a la barra de herramientas usando la lista de
imgenes en la libreria de los controles comunes...
tbab.hInst = HINST_COMMCTRL;
tbab.nID = IDB_STD_SMALL_COLOR;
SendMessage(hTool, TB_ADDBITMAP, 0, (LPARAM)&tbab);
Ahora que hemos cargado nuestras imgenes, podemos agregar algunos botones que las
utilicen...
ZeroMemory(tbb, sizeof(tbb));
tbb[0].iBitmap = STD_FILENEW;
tbb[0].fsState = TBSTATE_ENABLED;
tbb[0].fsStyle = TBSTYLE_BUTTON;
tbb[0].idCommand = ID_FILE_NEW;
tbb[1].iBitmap = STD_FILEOPEN;
tbb[1].fsState = TBSTATE_ENABLED;
tbb[1].fsStyle = TBSTYLE_BUTTON;
tbb[1].idCommand = ID_FILE_OPEN;
tbb[2].iBitmap = STD_FILESAVE;
tbb[2].fsState = TBSTATE_ENABLED;
tbb[2].fsStyle = TBSTYLE_BUTTON;
tbb[2].idCommand = ID_FILE_SAVEAS;
Los ndices de las lista de imgenes estn definidos en los encabezados de los controles
comunes y tambin son listados en MSDN.
Hemos asignado a cada botn un ID (ID_FILE_NEW, etc...) lo cual es idntico a los IDs de
los items equivalentes en el Men. Estos botones generarn mensajes WM_COMMAND
identicos a los del Men, por lo tanto no se requiere algn procesamiento extra! Si
fueramos a agregar un botn para un comando que no aparece en el Men, simplemente
definimos un nuevo ID y agregamos un manejador a WM_COMMAND.
Barras de Estado
Algo que se suele encontrar a menudo en las aplicaciones son las barras de estado, la
pequea barra en la parte inferior de la ventana que muestra informacin. Son muy fcil de
usar, solo hay que crearlas...
hStatus = CreateWindowEx(0, STATUSCLASSNAME, NULL,
WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP, 0, 0, 0, 0,
hwnd, (HMENU)IDC_MAIN_STATUS, GetModuleHandle(NULL), NULL);
Y luego (opcionalmente) especificamos el numero de secciones que deseamos. Si no
especificamos ninguna, simplemente tendr una que usa el ancho entero de la barra.
Podemos especificar o recuperar el texto usando SetWindowText( ) como con lo hacamos
con muchos de los otros controles. Si especificamos ms de una seccin, necesitamos
especificar el ancho de cada una y luego usar SB_SETTEXT para especificar el texto de cada
seccin.
Para definir los anchos declaramos un arreglo de enteros donde cada valor es el ancho en
pixeles de una seccin. Si queremos que una seccin use el espacio restante fijamos como
valor -1.
El wParam nuevamente es para calcular cuantos elementos hay en el arreglo. Una vez que
hemos terminado de agregar secciones, fijamos el primer valor (de ndice 0) para verla en
accin.
Tamao Apropiado
A diferencia de los Menes, las barras de estado y de herramientas son controles separados
que viven dentro del rea cliente de la ventana padre. Por lo tanto si solo dejamos nuestro
cdigo WM_SIZE anterior, se van a solapar con el control edit que hemos agregado en los
ejemplos anteriores. En WM_SIZE, ponemos las barras de estado y de herramientas en
posicin y luego restamos el alto del area cliente para que podamos mover nuestro control
edit para rellenar el espacio restante...
HWND hTool;
RECT rcTool;
int iToolHeight;
HWND hStatus;
RECT rcStatus;
int iStatusHeight;
HWND hEdit;
int iEditHeight;
RECT rcClient;
GetWindowRect(hTool, &rcTool);
iToolHeight = rcTool.bottom - rcTool.top;
GetWindowRect(hStatus, &rcStatus);
iStatusHeight = rcStatus.bottom - rcStatus.top;
GetClientRect(hwnd, &rcClient);
Desafortunadamente esta es una porcin de cdigo bastante larga, pero es simple... las
barras de herramiemntas se autoposicionarn cuando envemos el mensaje TB_AUTOSIZE y
las barras de estado harn lo mismo cuando enviemos WM_SIZE (las libreras de los
controles comunes no entienden nada a cerca de consistencia)
Introduccin a IMD
Primero un poco de repaso... Todas las ventanas tienen un Area Cliente, aqu es donde la
mayora de los programas dibujan imgenes, ubican los controles, etc... el Area Cliente no
est separada de la ventana, simplemente es una pequea regin especializada de la
ventana. A veces una ventana puede ser todo el rea cliente, y veces nada, a veces el rea
cliente puede hacerse mas pequea par caber en menes, ttulos, barras de desplazamiento,
etc...
En trminos de IMD (en ingls, MDI - Multiple Document Interface), nuestra ventana
principal es llamada Frame y sta es probablemente la nica ventana que podramos tener
en un programa IUD (Interface con un Unico Documento). En IMD hay una ventana
adicional llamada Ventana Cliente IMD, la cual es hija de nuestro Frame y a diferencia del
Area Cliente, es una ventana completa y separada de las dems que tiene un rea cliente y
probablemente algunos pixeles para un borde. Nunca procesaremos mensajes directamente
del Cliente IMD, esto es hecho por la clase ventana pre-definida "MDI_CLIENT". Podemos
comunicarnos y manipular el rea cliente IMD, como as tambin las ventanas que sta
contiene, atravs de mensajes.
Cuando entramos a la ventana que muestra nuestro documento, o lo que sea que muestre el
programa, envamos un mensaje al Cliente IMD para decirle que cree una nueva ventana
del tipo que hemos especificado. La nueva ventana es creada como una ventana hija del
Cliente IMD, no de nuestro Frame Window. Esta nueva ventana es una hija IMD (IMD
Child). La hija IMD Child es hija del Cliente IMD, el cual a su vez, es hijo del Frame
IMD... La Hija IMD probablemente tenga sus propias ventanas hijas, por ejemplo el control
edit en el programa del ejemplo de esta seccin.
Tenemos que escribir dos (o mas) Window Procedures. Uno, como siempre, para nuestra
ventana principal (el Frame) y uno ms para la Hija IMD. Podemos tener tambin mas de
un tipo de hija, en cuyo caso escribiremos window procedures separados para cada tipo.
Si te he confundido hablando de Clientes IMD y todo eso, quizs este diagrama puede
ayudarte a aclarar un poco las cosas.
Ahora si, IMD
IMD requiere algunos cambios astutos a lo largo de un programa, por lo tanto lee esta
seccin cuidadosamente... si tu programa no funciona o tiene un comportamiento extrao es
porque erraste alguna de las alteraciones que vamos a hacer al programa regular.
Antes de que creemos nuestra ventana IMD necesitamos hacer un cambio al procesamiento
por default que utilizamos en nuestro Window Procedure... debido a que estamos creando
un Frame que residir en un Cliente IMD, necesitamos cambiar la llamada a
DefWindowProc( ) a DefFrameProc( ) la cual agrega un procesamiento especializado de
mensajes para Frames.
default:
return DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);
El prximo paso es crear la ventana Cliente IMD, como una hija de nuestro Frame. Esto lo
hacemos en el WM_CREATE, como antes...
CLIENTCREATESTRUCT ccs;
ccs.idFirstChild es el nmero para usar como el primer ID para los items que el Cliente
agrega a la Ventana men... queremos que esto sea fcilmente distinguible de nuestros
identificadores de menes para que podamos procesar los comandos del men y pasarlos
desde la ventana a DefFrameProc( ) para que los procese. En el ejemplo he especificado
un identificador definido como 50000, que es lo suficientemente grande para que ninguno
de los identificadores de los comandos del men sean mayor que ste.
Ahora para que este men funcione apropiadamente, necesitamos agregar algn
procesamiento especial a nuestro manejador del mensaje WM_COMMAND:
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_FILE_EXIT:
PostMessage(hwnd, WM_CLOSE, 0, 0);
break;
Adems de la ventana principal de un programa (el Frame), necesitamos crear una nueva
clase ventana para cada tipo de ventana hija que necesitamos. Por ejemplo, podemos tener
una que muestre texto y otra para mostrar grficos o fotos. En este ejemplo solo crearemos
un tipo de hijo, el cual ser como el editor de los ejemplos anteriores.
BOOL SetUpMDIChildWindowClass(HINSTANCE hInstance)
{
WNDCLASSEX wc;
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MDIChildWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE+1);
wc.lpszMenuName = NULL;
wc.lpszClassName = g_szChildClassName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if(!RegisterClassEx(&wc))
{
MessageBox(0, "Could Not Register Child Window", "Oh Oh...",
MB_ICONEXCLAMATION | MB_OK);
return FALSE;
}
else
return TRUE;
}
El window procedure para una IMD Hija es parecido a culaquier otro pero con algunas
excepciones. Primero de todo, los mensajes por default, en lugar de ser pasados a
DefWindowProc( ), son pasados a DefMDIChildProc( ).
En este caso particular, tambin queremos deshabilitar los menes Edit y Window cuando
no se necesitan (slo porque es bueno hacerlo), por lo tanto procesando el mensaje
WM_MDIACTIVATE los habilitamos o deshabilitamos dependiendo si la ventana est activa o
no. Si tenemos mltiples tipos de ventana hijo, aqu es donde podemos poner el cdigo para
cambiar completamente el men, la barra de herramientas o hacer alteraciones a otros
aspectos del programa para reflejar las acciones y comandos que son especficos del tipo de
ventana que est siendo activada.
Para ser an mas completos, podemos deshabilitar los items Close y Open del men,
debido a que no sern de utilidad cuando las ventanas no estn activas. He deshabilitado
todos estos items por default en el recurso, por lo tanto no necesitamos agregar cdigo extra
para hacer esto cuando la aplicacin se ejecuta por primera vez.
hfDefault = GetStockObject(DEFAULT_GUI_FONT);
SendMessage(hEdit, WM_SETFONT, (WPARAM)hfDefault,
MAKELPARAM(FALSE, 0));
}
break;
case WM_MDIACTIVATE:
{
HMENU hMenu, hFileMenu;
UINT EnableFlag;
hMenu = GetMenu(g_hMainWindow);
if(hwnd == (HWND)lParam)
{ //si est activa, activamos los menus
EnableFlag = MF_ENABLED;
}
else
{ //cuando se desactiva,
desactivamos los menus
EnableFlag = MF_GRAYED;
}
DrawMenuBar(g_hMainWindow);
}
break;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_FILE_OPEN:
DoFileOpen(hwnd);
break;
case ID_FILE_SAVEAS:
DoFileSave(hwnd);
break;
case ID_EDIT_CUT:
SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_CUT, 0,
0);
break;
case ID_EDIT_COPY:
SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_COPY, 0,
0);
break;
case ID_EDIT_PASTE:
SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_PASTE, 0,
0);
break;
}
break;
case WM_SIZE:
{
HWND hEdit;
RECT rcClient;
GetClientRect(hwnd, &rcClient);
}
return 0;
}
Los comandos edit son fciles, debido a que el control edit est desarrollado para
soportarlos, slo le decimos que hacer.
Recuerdas que he mencionado que hay algunas cosas que necesitas recordar o tu
aplicacin se comportar de manera extraa? Observa que he llamado DefMDIChildProc(
) al final del WM_SIZE, esto es importante porque de no ser as, el sistema no tendr la
chance de hacer su propio procesamiento sobre el mensaje. Puedes buscar sobre
DefMDIChildProc( ) en MSDN para encontrar una lista sobre los mensajes que procesa y
siempre asegurarte de pasrselos.
Las ventanas Hijas IMD nos son creadas directamente, al contrario, le enviamos a la
ventana cliente el mensaje WM_MDICREATE dicindole que tipo de ventana queremos crear
rellenando los miembros de una estructura MDICREATESTRUCT. Puedes ver los distintos
miembros de sta estructura en la documentacin. El valor de retorno del mensaje
WM_MDICREATE es el handle a la nueva ventana.
HWND CreateNewMDIChild(HWND hMDIClient)
{
MDICREATESTRUCT mcs;
HWND hChild;
mcs.szTitle = "[Untitled]";
mcs.szClass = g_szChildClassName;
mcs.hOwner = GetModuleHandle(NULL);
mcs.x = mcs.cx = CW_USEDEFAULT;
mcs.y = mcs.cy = CW_USEDEFAULT;
mcs.style = MDIS_ALLCHILDSTYLES;
Un miembro de la estructura MDICREATESTRUCT que no usamos pero puede ser muy til es
el lParm. Este puede ser usado para enviar cualquier valor de 32 bits (un puntero) a la
ventana hija que ests creando con el propsito de proveerle cualquier informacin. En el
manejador WM_CREATE de nuestra ventana hija, el valor lParam para el mensaje WM_CREATE
apuntar a la estructura MDICREATESTRUCT. El miembro lpCreateParams de dicha
estructura apuntar a MDICREATESTRUCT que enviamos junto con WM_MDICREATE. Por lo
tanto para acceder al valor lparam de la ventana hija necesitamos hacer algo como esto en
el window procedure de dicha ventana...
case WM_CREATE:
{
CREATESTRUCT* pCreateStruct;
MDICREATESTRUCT* pMDICreateStruct;
pCreateStruct = (CREATESTRUCT*)lParam;
pMDICreateStruct = (MDICREATESTRUCT*)pCreateStruct-
>lpCreateParams;
/*
pMDICreateStruct now points to the same MDICREATESTRUCT that you
sent along with the WM_MDICREATE message and you can use it
to access the lParam.
*/
}
break;
Si no quieres complicarte con esos dos punteros extras, puedes acceder a lParam en un solo
paso de la siguiente manera: ((MDICREATESTRUCT*)((CREATESTRUCT*)lParam)-
>lpCreateParams)->lParam
Ahora podemos implementar los comandos File del men en nuestro Frame window
procedure:
case ID_FILE_NEW:
CreateNewMDIChild(g_hMDIClient);
break;
case ID_FILE_OPEN:
{
HWND hChild = CreateNewMDIChild(g_hMDIClient);
if(hChild)
{
DoFileOpen(hChild);
}
}
break;
case ID_FILE_CLOSE:
{
HWND hChild = (HWND)SendMessage(g_hMDIClient,
WM_MDIGETACTIVE,0,0);
if(hChild)
{
SendMessage(hChild, WM_CLOSE, 0, 0);
}
}
break;
case ID_WINDOW_TILE:
SendMessage(g_hMDIClient, WM_MDITILE, 0, 0);
break;
case ID_WINDOW_CASCADE:
SendMessage(g_hMDIClient, WM_MDICASCADE, 0, 0);
break;
GDI
Una cosa buena que tiene MS Windows, a diferencia de DOS, es que para mostrar grficos
no necesitamos conocer nada acerca del hardware de video que estamos usando. Por el
contrario, Windows provee una API llamada Graphics Device Interface (Interface del
Dispositivo Grfico) o GDI, en forma abreviada. LA GDI usa un conjunto de objetos
grficos genricos que pueden ser usados para dbujar en la pantalla, en la memoria, o en la
impresora.
Dispositivo de Contextos
La GDI gira entorno a un objeto llamado Dispositivo de Contexto (DC), representado por el
tipo de dato HDC (Handle al Dispositivo de Contexto). Un HDC es bsicamente un handle a
algo en lo que podemos dibujar, puede representar la pantalla entera, una ventana completa,
el rea cliente de una ventana, un bitmap almacenado en memoria o puede ser una
impresora. La parte buena es que no necesitamos saber a cul de todas estas cosas hace
referencia, se usa bsicamente de la misma manera, lo cual es muy til para escribir
funciones de dibujo que podemos usar en cualquiera de estos dispositivos sin necesidad de
cambiarla para cada uno de estos.
Un HDC, al igual que la mayora de los objetos GDI, es opaco. Esto significa que no
podemos acceder a sus datos de forma directa... pero podemos pasrselo a varias funciones
GDI que operarn en sus datos, ya sea para dibujar algo, para obtener informacin a cerca
de ste, o para cambiar de alguna manera el objeto.
Por ejemplo, si queremos dibujar en una ventana, primero debemos obtener un HDC que
represente la ventana, para esto utilizamos GetDC( ). Luego podemos usar cualquiera de
las funciones GDI que toman un HDC, como por ejemplo BitBlt( ) para dibujar
imgenes, TextOut( ) para dibujar texto, LineTo( ) para lineas, etc...
Bitmaps
Los bitmaps puden ser cargados en su mayora como los conos de los primeros ejemplos.
Hay una funcin LoadBitmap( ) para simplemente cargar un recurso bitmap y una funcin
LoadImage( ) para cargar bitmaps desde cualquier archivo *.bmp.
Una de las sutilezas de la GDI es que no podemos dibujar directamente sobre objetos
bitmap (HBITMAP). Recuerda que las operacioens de dibujo son abstractas y dicha
abstraccin la realiza el Dispositivo de Contextos, por lo tanto para usar cualquiera de estas
funciones de dibujo sobre un bitmap necesitamos crear un Memory DC y luego seleccionar
el HBITMAP dentro de ste usando SelectObject( ). El efecto es que el "dispositvo" al
cual el HDC hace referencia es el bitmap en memoria y cuando operamos sobre el HDC los
resutados de las funcioens grficas son aplicados al bitmap. Como vimos, esto es una forma
muy conveniente de hacer las cosas porque podemos escribir cdigo que dibuja sobre un
HDC y luego usarlo sobre un Window DC o sobre un Memory DC sin tener que realizar
cambios o chequeos.
Tenemos la opcin de manipular nosotros mismos los datos del bitmap en memoria.
Podemos hacer esto con el Dispositivo de Bitmaps Independientes (DIB) y podemos an
combinar operaciones GDI y operaciones manuales en el DIB. Sin embargo, por el
momento, esto est mas all del alcance de este tutorial bsico y por ahora slo cubriremos
las operaciones GDI mas simples.
Una vez que hemos finalizado de usar un HDC es muy importante liberarlo (cmo lo
hacemos depende de cmo lo obtuvimos, hablaremos de esto dentro de un momento). Los
objetos GDI estn limitados en nmero. En versiones de Windows anteriores a Windows
95, no slo estaban increblemente limitados sin que tambin compartan ampliamente los
recursos del sistema, por lo tanto, si un programa usaba demasiados, ninguno de los dems
programas poda ser capaz de dibujar nada! Afortunadamente, en Windows 2000 o XP, esto
ya no sucede ms y podemos obtener una gran cantidad de recursos sin que suceda algo
malo... pero an as es fcil olvidarse de liberar objetos GDI y rpidamente podemos
obtener un agotamiento de los recursos en sistemas como Windows 9x. Tericamente no
deberamos poder agotar los recursos GDI en sistemas basados en tecnologa NT
(NT/2K/XP) pero puede llegar a suceder en casos extremos, o si acertamos en el bug
apropiado...
Aparentemente hay un bug en un protector de pantalla escrito por MS y esto se debe a que
el bitmap por default no fu reemplazado o destrudo y eventualmente provoca un desborde
de los recursos GDI. Debes estar atentos!, es un error fcil de cometer.
SelectObject(hdcMem, hbmOld);
DeleteDC(hdcMem);
EndPaint(hwnd, &ps);
}
break;
Obtener el Window DC
PAINTSTRUCT es una estructura que contiene informacin acerca de la ventana que esta
siendo pintada, que es la que contina con el mensaje paint. Para las tareas ms simples,
podemos ignorar la informacin que esta estructura contiene, pero sta es necesaria en la
llamada a BeginPaint( ). BeginPaint( ) est diseada para procesar especifcamente el
mensaje WM_PAINT. Cuando no procesamos el mensaje WM_PAINT podemos usar GetDC( ),
el cual veremos en los ejemplos de la animacin dentro de un momento... pero en el
mensaje WM_PAINT es importante usar BeginPaint( ) y EndPaint( ).
BeginPaint( ) retorna un HDC que representa el HWND que le pasamos, el de la ventana que
est procesando el WM_PAINT. Cualquier operacin de dibujo que realizemos sobre este
HDC ser inmediatamente mostrada en la pantalla.
Ahora llamamos a SelectObject( ) para seleccionar el bitmap dentro del DC, siendo
cuidadosos de almacenar el bitmap por default para que podamos reemplazarlo
posteriormente y no retener objetos GDI.
Dibujo
Una vez que hemos llenado los valores ancho y alto dentro de la estructura BITMAP
podemos llamar a BitBlt( ) para copiar la imagen desde nuestro Memory DC hasta el
Window DC y luego mostrarlo en pantalla. Como siempre puedes buscar cada parmetro en
MSDN, pero ac estn resumidos: El destino, la posicin, el tamao, el origen y la posicin
origen y finalmente el Raster Operation (cdigo ROP), el cual especifica como hacer la
copia. En ese caso queremos una copia exacta del recurso, nada ms.
BitBlt probablemente sea la funcin mas feliz de toda la API Win32 y es la dieta pricipal de
todos aquellos que estn aprendiendo a escribir juegos o alguna otra aplicacin grfica en
Windows. Esta fu probablemente la primer API de la cual he memorizado todos los
parmetros.
Limpieza
Hasta este punto el bitmap debera estar en pantalla y es necesario que lo borremos nosotros
mismos. Lo primero que hay que hacer es restaurar el Memory DC al estado en el que
estaba cuando lo obtuvimos, para esto, reemplazamos nuestro bitmap con el bitmap por
default que habamos guardado. Luego podemos borrarlo por completo usando DeleteDC(
).
Destruir un HDC a veces es un poco confuso debido a que hay al menos 3 formas de
hacerlo que dependen de como lo obtuvimos. Aqu hay una lista de los mtodos ms
comunes para obtener un HDC y luego liberarlo.
GetDC() - ReleaseDC()
BeginPaint() - EndPaint()
CreateCompatibleDC() - DeleteDC()
Por ltimo, al final de nuestro programa, liberamos todos los recursos que tenemos
asignados. Tcnicamente hablando, esto no es absolutamente necesario debido a que las
plataformas modernas de Windows son muy buenas liberando todos los recursos cuando
nuestro programa termina, aunque siempre es conveniente hacerlo uno mismo ya que hay
viejas versiones de Windows que no hacen esto de manera efectiva.
case WM_DESTROY:
DeleteObject(g_hbmBall);
PostQuitMessage(0);
break;
Bitmaps Transparentes
Ejemplo: bmp_two
Transparencia
Darles a los bitmaps la apariencia de tener secciones transparentes es bastante simple e
involucra el uso de una Mscara blanca y negra junto al color de la imagen que queremos
que luzca transparente.
Para que el efecto funcione apropiadamente se necesitan reunir las siguientes condiciones:
Primero la imagen debe ser de color negro en todas las reas que queremos que luzca
transparente. Segundo, La mscara debe ser blanca en las reas que queremos que
sean transparentes y negro en caso contrario. La imgen y la mscara son mostradas en la
figura includa como ejemplo en esta seccin y corresponden a las dos imgenes que se
encuentran alineadas mas a la izquierda.
Operaciones BitBlt
Como hace esto para proveernos transparencia? Primero usamos BitBlt( ) sobre la
mscara usando la operacin SRCAND como ltimo parmetro y luego encima de esto
usamos BitBlt( ) sobre la imagen usando la operacion SRCPAINT. El resultado es que las
reas que queremos que sean transparentes no cambian en el HDC destino mientras el resto
de la imagen es dibujada de forma usual.
SelectObject(hdcMem, g_hbmMask);
BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCAND);
SelectObject(hdcMem, g_hbmBall);
BitBlt(hdc, 0, bm.bmHeight, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0,
SRCPAINT);
Bastante simple eh? Afortunadamente lo es, pero resta una pregunta... de donde viene la
mscara? Hay bsicamente dos formas de obtener la mscara...
Debido a que la primer opcin no es nada nuevo, deberas ser capaz de hacerlo si lo deseas.
La segunda forma proviene de aplicar algunos malabares usando BitBlt( ) y por lo tanto
voy a mostrar una forma de realizarla.
Esto funciona perfectamente, debido a que todos lo que necesitamos hacer es poner el color
de fondo con el color que queremos transparentar y hacemos BitBlt( ) desde nuestra
imagen a la mscara. Observa que esto solo funciona con una mscara monocromo
(blanco y negro)... esto es, bitmaps que solo tienen 1 bit per pixel para definir el color. Si
intentamos esto con una imagen a color que slo tiene pixeles blanco y negro, pero que el
tamao para definir el color por pixel es mayor que un bit, digamos 16 bits o 24 bits, esto
no funciona.
Receuerdas la primer condicin anterior para obtener mscaras exitosas? Esta deca que
nuestra imagen necesitaba ser negra donde queramos que fuera transparente. Debido a que
el bitmap que he usado en este ejemplo ya cumple esa condicin, no necesita ningn
tratamiento especial. Pero si deseas usar este cdigo en otra imagen que tiene un color
diferente para transparentar (rosa es una eleccin comn), entonces necesitamos tomar un
segundo paso, esto es, usar la mscara que acabamos de crear para alterar la imagen
original, donde todo lo que queremos transparentar es negro. No hay problema si otros
lugares de la imagen tambin son de color negro, debido a que esos lugares no son blancos
en la mscara y por lo tanto no sern tranparentes. Podemos realizar esto utilizando
BitBlt( ) desde la nueva mscara a la imagen original, usando la operacin SCRINVERT,
la cual fija todas la reas que son blancas en la mscara a negras en la imagen a color.
Esta es una parte de un proceso complejo, por lo tanto es bueno tener a mano una funcin
muy til que hace todo esto por nosotros, aqu est:
hdcMem = CreateCompatibleDC(0);
hdcMem2 = CreateCompatibleDC(0);
SelectBitmap(hdcMem, hbmColour);
SelectBitmap(hdcMem2, hbmMask);
SetBkColor(hdcMem, crTransparent);
// Liberamos.
DeleteDC(hdcMem);
DeleteDC(hdcMem2);
return hbmMask;
}
Ahora que tenemos nuestra super funcin, podemos crear una mscara a partir de la imagen
original una vez que la hallamos cargado:
case WM_CREATE:
g_hbmBall = LoadBitmap(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDB_BALL));
if(g_hbmBall == NULL)
MessageBox(hwnd, "Could not load IDB_BALL!", "Error", MB_OK |
MB_ICONEXCLAMATION);
El segundo parmetro es, por su puesto, el color de la imagen original que queremos que
sea transparente, en este caso es negro.
SRCAND
La operacin SCRAND o ROP (Raster Operation) para BitBlt( ), significa combinar los
bits usando AND. Esto es: solo los bits que valen 1 en la imagen fuente Y en la imagen
destino quedan el el resultado final. Usamos esto con nuestra mscara para poner en negro
todos los pixeles que tendrn eventualmente un color de la imagen original. La mscara
tiene el color negro (en binario, todos 0) donde queremos que halla color y blanco (todos 1
en binario) donde queremos que halla trasnsparencia. Cualquier valor combinado con 0,
usando AND, es 0 y por lo tanto todos los pixeles que son negros en la mscara son puesto
en 0 en el resultado final. Cualquier valor que es combinado con 1 usando AND permanece
inalterable, por lo tanto si vala 1 al comienzo, finaliza valiendo 1 y si vala 0 finaliza
valiendo 0... Por lo tanto todos los pixeles que eran blanco en nuestra mscara, no alteran su
valor despus de la llamada a BitBlt(). El resultado es el del la imagen de la esquina
derecha del ejemplo.
SCRPAINT
SCRPAINT usa la operacin OR, por lo tanto si al menos uno de los 2 bits vale 1, entonces el
resultado valdr 1. Usamos esto en la imagen a color. Cuando la parte negra (transparente)
de nuestra imagen a color es combinada con los datos en el desitno usando OR, el resultado
es que los datos permanecen inalterados, debido a que cualquier valor combinado con 0
usando la operacin OR permanece igual.
SCRINVERT
Esta es la operacin XOR usada para poner en negro el color transparente en nuestra
imagen original (Si no ya no es negro). Combinando un pixel negro de la mscara con un
pixel que no sea del color de fondo en el destino lo deja inalterable, mientras que al
combinar un pixel blanco de la mscara (que generamos definiendo un color particular
como "fondo") con el color de un pixel del fondo del destino, esto hace que se cancele y se
ponga en negro.
Ejemplo 2
El cdigo ejemplo en el proyecto bmp_two que viene junto con esta seccin contiene el
cdigo para la imagen del ejemplo de esta seccin. Este consiste en primero dibujar la
mscara y la imagen a color exactamente como son usando SRCCOPY, luego usa en cada una
por separado las operaciones SRCAND y SRCPAINT repectivamente y finalmente las combina
para producir el resultado final.
El fondo en este ejemplo es gris para hacer mas obvia la transparencia ya que usar estas
operaciones sobre un fondo blanco o negro hace dificil darse cuenta si est funcionando o
no
Timers y Animaciones
Ejemplo: anim_one
Antes de comenzar...
Antes que veamos cosas animadas necesitamos crear una estructura para almacenar la
posicin de la esfera entre las actualizaciones. Esta estructura almacenar la posicin actual
y el tamao de la esfera, como as tambin los valores delta (cuanto queremos mover la
esfera en cada frame).
Una vez que hemos declarado el tipo de la estructura, tambin declaramos una instancia
global de la misma. Esto est bien, debido a que solo tenemos una esfera, si fueramos a
animar varias de ellas deberiamos probablemente usar un arreglo o una lista para
contenerlas de una forma mas conveniente.
Tambin hemos definido una constante BALL_MOVE_DELTA la cual indica cuan lejos
queremos que la esfera se desplace en cada actualizacin. La razn para almacenar deltas
en la estructura BALLINFO es que queremos mover la esfera hacia arriba o hacia abajo, hacia
izquierda o derecha, de forma independiente, BALL_MOVE_DELTA es slo un nombre
significativo que nos facilitar la tarea de cambiar el valor en el futuro.
BITMAP bm;
GetObject(g_hbmBall, sizeof(bm), &bm);
ZeroMemory(&g_ballInfo, sizeof(g_ballInfo));
g_ballInfo.width = bm.bmWidth;
g_ballInfo.height = bm.bmHeight;
g_ballInfo.dx = BALL_MOVE_DELTA;
g_ballInfo.dy = BALL_MOVE_DELTA;
Configurando el Timer
La forma ms fcil de agregar un timer dentro de un programa en Windows es con
SetTimer(), aunque no es la mejor forma y tampoco es la forma recomendada para
aplicacines multimedia o juegos, sin embargo es lo suficientemente buena para
aplicaciones simples como sta. Cuando necesites algo mejor, chale una mirada a
timeSetEvent() en MSDN, la cual es mas apropiada.
const int ID_TIMER = 1;
ret = SetTimer(hwnd, ID_TIMER, 50, NULL);
if(ret == 0)
MessageBox(hwnd, "Could not SetTimer()!", "Error", MB_OK |
MB_ICONEXCLAMATION);
Aqu hemos declarado un ID para el timer para que podamos referirnos a ste
posteriormente (para eliminarlo) y pusimos el timer en el manejador del mensaje
WM_CREATE de nuestra ventana principal. Cada vez que transcurre un determinado tiempo,
el timer enva un mensaje WM_TIMER a la ventana y pasa como parmetro en wParam su ID.
Debido a que slo tenemos un timer no necesitamos el ID, pero es muy til si utilizamos
mas de uno.
Hemos configurado el timer para que enve una seal cada 50 milisegundos, lo cual resulta
en aproximadamente 20 cuadros por segundo. He dicho aproximadamente debido a que,
como dije, SetTimer( ) es un poco impresiso, pero este no es un cdigo crtico donde
milisegundos mas milisegundos menos causarn estragos, por lo tanto podemos usarlo.
Animacin en WM_TIMER
Ahora, cuando obtenemos el mensaje WM_TIMER queremos calcular la nueva posicin de la
esfera y dibujarla en la nueva posicin.
case WM_TIMER:
{
RECT rcClient;
HDC hdc = GetDC(hwnd);
GetClientRect(hwnd, &rcClient);
UpdateBall(&rcClient);
DrawBall(hdc, &rcClient);
ReleaseDC(hwnd, hdc);
}
break;
He puesto el cdigo para actualizar y dibujar la esfera en sus respectivas funciones. Esto es
una buena prctica y nos permite dibujar la esfera fuera de WM_TIMER o de WM_PAINT sin
necesidad de duplicar el cdigo. Observa que el mtodo que utilizamos para obtener el
HDC en cada caso es diferente , por lo tanto es mejor dejar este cdigo en los manejadores
de mensajes y pasar el resultado dentro de la funcin DrawBall( ).
if(g_ballInfo.x < 0)
{
g_ballInfo.x = 0;
g_ballInfo.dx = BALL_MOVE_DELTA;
}
else if(g_ballInfo.x + g_ballInfo.width > prc->right)
{
g_ballInfo.x = prc->right - g_ballInfo.width;
g_ballInfo.dx = -BALL_MOVE_DELTA;
}
if(g_ballInfo.y < 0)
{
g_ballInfo.y = 0;
g_ballInfo.dy = BALL_MOVE_DELTA;
}
else if(g_ballInfo.y + g_ballInfo.height > prc->bottom)
{
g_ballInfo.y = prc->bottom - g_ballInfo.height;
g_ballInfo.dy = -BALL_MOVE_DELTA;
}
}
Todo lo que esto hace es bsicamente matemtico, sumamos el valor delta a la posicin x
para mover la esfera. Si el valor pasa afuera del rea cliente lo ponemos nuevamente en el
rango y cambiamos el valor delta a la direccin opuesta para que la esfera "rebote" en los
bordes.
SelectObject(hdcMem, g_hbmBall);
BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width,
g_ballInfo.height, hdcMem, 0, 0, SRCPAINT);
SelectObject(hdcMem, hbmOld);
DeleteDC(hdcMem);
SelectObject(hdcBuffer, hbmOldBuffer);
DeleteDC(hdcBuffer);
DeleteObject(hbmBuffer);
}
Doble Buffering
Cuando dibujamos directamente al HDC de la ventana, es totalmente posible que la pantalla
se actualice antes de que lo hagamos... por ejemplo, despus de que dibujemos la mscara y
antes que dibujemos la imagen original encima el usuario puede ver un parpadeo del fondo
antes de que nuestro programa tenga una chance de de dibujar sobre ste la imagen en
color. Mientras mas lerda sea la computadora y mientras mas operaciones de dibujo
realicemos, aparecern mas parpadeos.
Para hacer esto creamos en memoria un HBITMAP temporal que es del tamao exacto del
rea que vamos a dibujar sobre la pantalla. Tambin necesitamos un HDC para que
podamos usar BitBlt() sobre el bitmap.
Ahora que tenemos un lugar en memoria donde dibujar, todas las operaciones de dibujo
usan hdcBuffer en vez de hdc (la ventana) y los resultados son almacenados sobre el
bitmap en memoria hasta que hallamos terminado. Luego podemos copiar dicha imagen a
la ventana en un solo "disparo".
En este ejemplo estoy creando y destruyendo el bitmap usado para el doble buffering con
cada frame, hice esto porque es ms fcil siempre crear un nuevo bffer que registrar
cuando cambia la posicin de la ventana y cambiar el tamao del bffer. Pero podra ser
ms eficiente crear un doble bffer global y no permitir cambiar el tamao de la ventana o
slo cambiar el tamao del bitmap cuando es cambiado el tamao de la ventana, en vez de
crearlo y destruirlo todo el tiempo. Se deja como tarea al lector implementar esta
optimizacin si desea mejorar el dibujado sobre la pantalla, para un juego o algo por el
estilo.
Eliminando el Timer
Es una buena idea liberar todos los recursos cuando la ventana es destruida y en este caso
tambin se incluye el timer que hemos usado. Para detenerlo, simplemente llamamos a
KillTimer( ) y le pasamos el ID que hemos usado para crearlo.
KillTimer(hwnd, ID_TIMER);
Texto y Fuentes
Ejemplo: font_one
Carga de Fuentes
La GDI Win32 tiene algunas capacidades admirables para trabajar con diferentes tipos de
estilos de presentacin, lenguajes, conjuntos de caracteres etc... Uno de los inconvenientes
de esto es que trabajar con fuentes luce algo intimidatorio para el que recin se inicia.
CreateFont(), la principal API para trabajar con fuentes tiene 14 parmetros, los cuales
especifican el alto, estilo, ancho, familia y otros varios atributos.
Afortunadamente no es tan difcil como parece y una gran parte del trabajo es realizada con
valores por default. Todos menos dos de los valores de CreateFont() pueden ser puestos
en 0 o en NULL para que el sistema use los valores por default dando como resultado una
fuente plana y comn.
HFONT hf;
HDC hdc;
long lfHeight;
hdc = GetDC(NULL);
lfHeight = -MulDiv(12, GetDeviceCaps(hdc, LOGPIXELSY), 72);
ReleaseDC(NULL, hdc);
hf = CreateFont(lfHeight, 0, 0, 0, 0, TRUE, 0, 0, 0, 0, 0, 0, 0,
"Times New Roman");
if(hf)
{
DeleteObject(g_hfFont);
g_hfFont = hf;
}
else
{
MessageBox(hwnd, "Font creation failed!", "Error", MB_OK |
MB_ICONEXCLAMATION);
}
Este es el cdigo usado para crear la fuente en la imagen ejemplo. La fuente es Times New
Roman de 12 puntos con el estilo Itlica El flag para obtener la fuente en itlica es el sexto
parmetro de CreateFont( ), el cual se puede apreciar que est puesto en TRUE. El ltimo
parmetro indica el nombre de la fuente que queremos usar.
La nica parte tramposa de este cdigo es el valor usado para el tamao de la fuente, el
parmetro lfHeight de CreateFont( ). Generalmente las personas, cuando trabajan con
fuentes, estn acostumbradas a trabajar con tamaos especificados en Puntos, Tamao 10,
Tamao 12, etc... Sin embargo, CreateFont( ) no acepta tamaos en puntos,solo acepta
Unidades Lgicas, las cuales son diferentes en nuestra pantalla que en nuestra impresora y
an entre impresoras y pantallas.
La razn de que exista esta situacin es porque la resolucin de los diferentes dispositivos
es bastante diferente... las impresoras pueden imprimir de 600 a 1200 pixeles por pulgada,
mientras que una pantalla con suerte llega a mostrar unos 200. Si usamos el mismo tamao
para la pantalla que para la impresora, quizs no podamos ver algunos caracteres en
particular.
Todo lo que tenemos que hacer es convertir el tamao punto al tamao lgico para un
dispositivo determinado. En este caso el dispositivo es la pantalla, por lo tanto obtenemos
el HDC de la pantalla y obtenemos el nmero de pixeles lgicos por pulgada usando
GetDeviceCaps() y metemos esto dentro de la frmula provista tan generosamene en
MSDN la cual usa MulDiv( ) para convertir nuestro tamao punto, en este caso 12, al
tamao lgico que CreateFont( ) espera. Luego almacenamos este en lfHeight y lo
pasamos como primer parmetro de CreateFont( ).
Cuando llamamos por primera vez a GetDC( ) para obtener el HDC de nuestra ventana, la
fuente por default que es seleccionada es System, la cual para ser honesto, no es muy
atractiva. La forma ms simple de obtener una fuente mejor sin tener que pasar por
CreateFont() es llamar a GetStockObject() y preguntar por la DEFAULT_GUI_FONT.
Este es un objeto del sistema y podemos usarlo todas las veces que deseamos sin necesidad
retener memoria, podemos llamar a DeleteObject() para borrarlo pero esto no causar
efecto, lo cual es una buena idea porque no tendremos que averiguar si dicha fuente fu
creada con CreateFont( ) o con GetStockObject( ) antes de intentar eliminarla.
Mostrando Texto
Ahora que tenemos una "linda" fuente, como hacemos para mostrar algn texto en la
pantalla? Esto es, asumiendo que no queremos usar un control Edit o un control Static.
Las opciones bsicas son TextOut( ) y DrawText( ). TextOut() es mas simple pero
tiene menos opciones y no hace cubrimientos de palabras o alineacin por nosotros.
char szSize[100];
char szTitle[] = "These are the dimensions of your client area:";
HFONT hfOld = SelectObject(hdc, hf);
SetBkColor(hdc, g_rgbBackground);
SetTextColor(hdc, g_rgbText);
if(g_bOpaque)
{
SetBkMode(hdc, OPAQUE);
}
else
{
SetBkMode(hdc, TRANSPARENT);
}
SelectObject(hdc, hfOld);
La primer cosa que hacemos es usar SelectObject( ) para obtener la fuente que
queremos usar dentro de nuestro HDC y para prepararnos para dibujar. Todas las operaciones
de texto usarn, de aqu en mas, esta fuente hasta que se seleccione alguna otra.
Ahora vamos a definir los colores del texto y del fondo. Cuando fijamos el color de fondo
no cambiamos todo el fondo a un determinado color, solo afecta a ciertas operaciones que
dibujan texto usando el color de fondo. Esto tambin depende del Modo de Fondo (o
Background Mode) que est seleccionado actualmente. Si est definido como OPAQUE (el
default) entonces cada vez que se escriba texto, el color de fondo del texto ser el que est
seleccionado como background color. Si est definido como TRANSPARENT, el texto es
dibujado sin color de fondo y quedar como fondo del texto lo que sea que encuentre
dibujado debajo de ste.
Ahora realmente dibujamos el texto usando DrawText( ), pasamos el HDC a usar y el string
a dibujar. El tercer parmetro es la longitud del string, pero hemos pasado -1 debido a que
DrawText( ) es lo suficientemente astuto para darse cuenta cual es la longitud del texto.
En el cuarto parmetro pasamos prc, el puntero al cliente RECT. DrawText( ) dibujar
dentro de este rectngulo basndose en los otros flags que le pasamos.
Para la segunda llamada solo imprimimos una sola lnea sin ajustar el texto al rectngulo y
la centramos horizontalmente y verticalmente (lo cual har DrawText() cuando dibujemos
una sola lnea).
Redibujar el Cliente
Slo una nota sobre el programa ejemplo... cuando WNDCLASS es registrada he puesto los
estilos CS_VREDRAW y CS_HREDRAW. Esto provoca que el rea cliente entera sea sea
redibujada si la ventana cambia su tamao, mientras que por default solo se dibujan las
partes que han cambiado. Esto luce algo malo debido a que el texto centrado es movido
cuando la ventana cambia su tamao pero no se actualiza como esperamos.
Eleccin de Fuentes
En general cualquier programa que utiliza fuentes le permite al usuario escoger su propia
fuente, como tambin el color y los atributos de estilo a usar.
Al igual que los dilogos comunes para abrir y guardar archivos, hay un dilogo comn
para escoger una fuente. Este es llamado ChooseFont( ) y funciona junto con la estructura
CHOOSEFONT.
if(ChooseFont(&cf))
{
HFONT hf = CreateFontIndirect(&lf);
if(hf)
{
g_hfFont = hf;
}
else
{
MessageBox(hwnd, "Font creation failed!", "Error", MB_OK |
MB_ICONEXCLAMATION);
}
g_rgbText = cf.rgbColors;
}
}
El hwnd en esta llamada es simplemente la ventana que vamos a usar como padre del
dilogo.
La forma ms fcil de usar este dilogo es junto con una estructura LOGFONT, la cual es
bastante parecida a la estructura HFONT que venamos usando. Hacemos que el campo
lpLogFont apunte a LOGFONT, que acabamos de llenar con informacin y tambin
agregamos el flag CF_INITTOLOGFONTSTRUCT para que ChooseFont( ) use este campo. El
flag CF_EFFECTS le dice a ChooseFont( ) que permita al usuario seleccionar un color
como as tambin efectos de subrayado y tachado.
Los estilos Negrita e Itlica no se cuentan como efectos, son considerados partes de la
fuente y de hecho algunas fuentes solo vienen en Negrita o en Itlica. Si queremos chequear
o prevenir al usuario de seleccionar una fuente negrita o itlica, podemos chequear los
campos lfWeight y lfItalic, respectivamente, de la estructura LOGFONT despus de que
el usuario ha hecho su eleccin. Luego se le puede preguntar al usuario si desea hacer otra
seleccin o algn cambio a los campos antes de llamar a CreateFontIndirect().
El color de una fuente no est asociado con un HFONT y por lo tanto debe ser almacenado
por separado. EL campo rgbColors de la estructura CHOOSEFONT es usado para pasar el
color inicial y para recuperar luego el nuevo color.
Eleccin de colores
Para permitirle al usuario cambiar el color de la fuente, existe el dilogo comn
ChooseColor( ). Este es el cdigo usado para permitirle al usuario seleccionar el color de
fondo en programa ejemplo.
COLORREF g_rgbBackground = RGB(255, 255, 255);
COLORREF g_rgbCustom[16] = {0};
void DoSelectColour(HWND hwnd)
{
CHOOSECOLOR cc = {sizeof(CHOOSECOLOR)};
if(ChooseColor(&cc))
{
g_rgbBackground = cc.rgbResult;
}
}
Esto es algo complicado, nuevamente estamos usando el parmetro hwnd como padre del
dilogo. El parmetro CC_RGBINIT indica que debemos comenzar con el color que pasamos
en el campo rgbResult, el cual tambin es donde obtenemos el color que el usuario ha
seleccionado cuando el dilogo se cierra.
He realizado esto en los ejemplos anteriores, pero tiene sentido mencionarlo aqu debido a
que es relevante y muy breve:
Donde hfFont es, por su puesto, el HFONT que queremos usar y IDC_OF_YOUR_CONTROL es
el ID del control al cual le queremos cambiar la fuente.
Puedes encontrar mas libros recomendados y links donde comprar en #Winprog Store.
Programming Windows
by Charles Petzold. El libro para empezar con Win32 API. Si quieres escribir
programas usando solo la API (la cual cubre este tutorial), entonces necesitas este
libro.
Programming Windows with MFC
by Jeff Prosise. Si quieres aventurarte en la MFC (DESPUES de aprender a usar la
API Win32), este libro es para vos. Si no te gusta la MFC pero quieres buscar
trabajo desarrollando aplicaciones en Windows, entoces consigue este libro de todos
modos.
Programming Applications for Windows
by Jeffrey Richter. No es para principientes, cubre el manejo de procesos y
threads, dlls, manejo de memoria, manejo de excepciones y otros.
Visual C++ Windows Shell Programming
by Dino Esposito. Para aquellos que estn interesados en los aspectos visuales e
interfaces amigables en Windows, este libro cubre extensiones de la interfaz de
Windows, cmo trabajar eficientemente con archivos y drag and drop (arrastrar y
soltar con el mouse), cmo personalizar la barra de tareas y el Explorador de
Windows y otra gran cantidad de aspectos. Ideal para aquellos que desean escribir
aplicaciones con Interfaces Graficas al Usuario en Windows.
Network Programming for Microsoft Windows
Informacin actualizada de programacin en redes, que incluye NetBIOS, mailslots
y pipes y por su puesto, los siempre importantes, Windows sockets incluyendo
winsock2 y raw sockets. Tambin contiene informacin especfica sobre las
distintas plataformas incluyendo 2000 y CE.
Links
MSDN Online
Este sitio tiene referencias a todas las tecnologas imaginables de Microsoft,
incluyendo Win32 API y documentacin MFC. Si no es includa con tu compilador
(VC++) puedes conseguir esta informacin de manera gratuita.
#winprog homepage
Mira en FAQ y Store
Debido a que este es el .Net SDK, no viene con los headers (*.h) y las libreras necesarias
para el desarrollo Win32 API, ya que estos son partes de la plataforma SDK. De todas
maneras, la plataforma SDK es gratuita, solo necesitas bajar el Core SDK y si deseas
puedes descargar algunos otros componentes.
Platform SDK
Cmo Usarlas
Debido a que se provee una comprensiva documentacin, tambin accesible va MSDN
online necesitars estudiarla un poco para aprender acerca de los compiladores VC++ y sus
herramientas. Para comenzar aqu estn las formas bsicas de crear un programa...
cl foo.c
Y para crear un aplicacin simple en Windows como los ejemplo de ste tutorial:
rc dlg_one.rc
cl dlg_one.c dlg_one.res user32.lib
Borland C++ 5.5 Lo bueno de este compilador es que viene con un debugger! No lo he
usado, por lo tanto no puedo ofrecerte mucha ayuda sobre l, pero es mejor que nada. Si has
usado Turbo C++ en la poca del DOS, puedo decirte que es algo parecido.
Por alguna razn Internet Explorer parece tener algn problema descargando este archivo,
ya que hacer click sobre el link no funciona. Lo mejor es hacer click con el botn derecho y
luego hacer click en copiar vnculo, por ltimo usa tu cliente FTP favorito para descargarlo.
Turbo Debugger
Por ltimo, pero no menos importante, un archivo de ayuda de Windows con una
Referencia Completa de Win32 API. Tiene algunos aos, pero todava es apropiada y es
mucho mas conveniente que MSDN online, a menos que necesites acceder a los agregados
mas recientes en la API. Yo los uso regularmente.
Cmo usarlas
Comandos Bsicos
La opcin -tW especifica una aplicain GUI Win32, en vez de la aplicacin en consola que
se utiliza por default. Puedes compilar mltiples archivos dentro de un solo archivo .exe
agregando los otros archivos al final de dicho comando.
Este es un aspecto muy frustante para muchos usuarios de lnea de comando, parece ser que
borland intenta hacerlo lo mas difcil posible. El compilador de recursos brc32 no se
comporta como lo haca en las primeras versiones donde poda enlazar los recursos
compilados dentro de el archivo .exe. Cuando ejecutas brc32 sin opciones para obtener la
ayuda, todava muestra una opcin para poner el enlazado .exe enOFF, pero parece no
haber una forma de ponerlo en ON.
BC++ ahora tienen un mtodo alternativo para incluir recursos en un programa mediante el
uso de un #pragma (una directiva no-estndar del preprocesador que el compilador ignorar
si no la reconoce).
Usar la directiva #pragma te permitir compilar programas de una forma simple como la
anterior, pero recuerda que antes necesitas compilar el archivo .rc usando brc32.
La forma difcil
Estos son los comando que hay que usar para compilar el ejemplo dlg_one, incluyendo el
archivo de recursos.
La opcin -c de bcc32 significa "solo compilar", no enlazar dentro de un .exe. Las opciones
-x -Gn libera algunos archivos extra que genera el enlazador, que probablemente no
necesites.
Para hacer las cosas mas fcil, lo mejor es hacer todo esto en un archivo makefile. He
preparado uno genrico que debera funcionar con todos los ejemplos de este tutorial y
deberas ser capaz de adaptarlo a cualquiera de tus programas.
APP = dlg_one
EXEFILE = $(APP).exe
OBJFILES = $(APP).obj
RESFILES = $(APP).res
LIBFILES =
DEFFILE =
.AUTODEPEND
BCC32 = bcc32
ILINK32 = ilink32
BRC32 = brc32
clean:
del *.obj *.res *.tds *.map
En este caso especfico, esto significa una de dos cosas: Ests intentando escribir una
aplicacin Win32 GUI (o una aplicacin no-consola) y accidentalmente la ests
compilando como Console aplication... realmente ests escribiendo y compilando una
aplicacin de consola pero no escribiste o no compilaste apropiadamente una funcin
main().
Correccin
Si ests usando VC++, debes re-crear el proyecto y seleccionar como tipo de proyecto
Win32 Aplication (NO "Console").
Si ests el compilador de lnea de comando, BC++, debes usar -tW para especificar que se
trata de una aplicacin Windows.
Si ests compilando cdigo que no sea de este tutorial, no puedo garantizarte que sea
correcto y por lo tanto puede ser un error que proviene de otro lado. Debes determinar si es
seguro convertir el valor para remover el error o si realmente ests intentando que una
variable sea algo que no es.
Correcin
Si quieres usar C, simplemente renombra el archivo .cpp a uno con extensin .c. En otro
caso, debes agregar un cast, todo el codigo de este tutorial funciona sin ningn tipo de
cambios si se lo compila como C++.
Y as...
Mi Respuesta
Primero de todo vamos a aclarar que son la API y la MFC. API es un trmino genrico que
significa Aplication Programming Interface (Interface para la Programacin de
Aplicaciones). Sin embargo, en el contexto de la programacin en Windows, esto se refiere
especficamente a la API de Windows, la cual es el nivel mas bajo de interaccin entre las
aplicaciones y el sistema operativo. Los drivers, por su puesto, tienen an niveles inferiores
y trabajan con diferentes conjuntos de llamadas a funciones, pero para la gran majora de
desarrollo en Windows esto no es relevante. MFC es una Librera de Clases, un conjunto
de clases C++ que han sido escritas para reducir la cantidad de trabajo que tomara realizar
ciertas cosas utilizando la API. La MFC adems introduce dentro de la aplicacin un
esqueleto (o framework) Orientado a Objetos, de cual puedes tomar ventaja o ignorarlo si
deseas, como lo hacen la mayora de los principiantes debido a que este framework no est
apuntado a escribir reproductores de mp3, clientes IRC o juegos.
Pero la MFC no es mas fcil? En cierta forma, es mas fcil cuando muchas tareas comunes
son realizadas por uno mismo, reduciendo la cantidad de cdigo que realmente necesitamos
tipear. Sin embargo, menos cdigo no significa "mas fcil" cuando no entendemos el
cdigo que necesitamos escribir, o cuando no entendemos cmo funciona dicho cdigo.
Generalmente los principiantes que utilizan los wizards para comenzar sus aplicaciones, no
tienen idea que hace la mayora del cdigo que se ha generado y pierden una gran parte del
tiempo intentando darse cuenta donde agregar cosas o que cambios deben hacer para para
alcanzar un cierto resultado. Si comienzas a escribir tus programas desde cero, ya sea
utilizando la API o MFC, entonces comprenders todo lo que est puesto all ya que fu
puesto por vos mismo y solo usars las cosas que entiendes como funcionan.
Otro factor importante es que la mayora de las personas que estn aprendiendo por primera
vez la API Win32, no tienen una fuerte base en C++. Intentar comprender la programacin
en Windows utilizando la MFC y aprender C++ al mismo tiempo puede ser una tarea
monumental. Si bien no es imposible, estar lejos de ser mas productivo que hacerlo
teniendo conocimientos sobre C++ y la API.
Entonces, bsicamente...
Lo que pienso es que deberas aprender la API hasta que te sientas cmodo con ella y luego
intentar con la MFC. Si luego, pareciera ser que tiene sentido y puedes ahorrar tiempo con
la MFC, entonces sala.
Sin embargo, y esto es importante... si quieres trabajar con la MFC sin entender la API y
luego buscas ayuda por algo y la respuesta que obtienes hace referencia a la API (como por
ejemplo "Usa el HDC provisto en el mensaje WM_CTLCOLORSTATIC"), seguramente
quedes colgado ya que no entiendes como traducir un objeto de la API a uno de la MFC. En
ese momento estars en problemas y las personas se frustrarn contigo por no aprender todo
aquello que necesitas de la API antes de intentar usar la MFC.
Personalmente prefiero trabajar con la API, solo me parece mejor. Pero si tengo que
escribir una terminal para una base de datos o un host para un conjunto de controles
ActiveX, considerara seriamente usar la MFC ya que podra eliminarme una gran cantidad
de cdigo que, en otro caso, debera reinventar.
En Borland C++ era libre de controlar la disposicin y el contenido de los archivos .rc y
cuando usaba el editor de recursos, slo las cosas que yo especficamente cambiaba eran
cambiadas en el archivo. En cambio, el editor de recursos de VC++, reescribe
completamente el archivo .rc y posiblemente destruye o ignora los cambios que
personalmente realizo.
Compatibilidad
Un pequeo cambio que se hizo en este tutorial fu lograr que los archivos de recursos
compilen de forma correcta bajo VC++ y BC++ sin realizar cambios. En el tutorial original
he usado la convencin de nombres para los encabezados de recursos utilizada por Borland,
la cual era nombre_proyecto.rh. Sin embargo, por default, en VC++ este encabezado
SIEMPRE es llamado resource.h, por lo tanto por simplicidad he adoptado este ltimo
para la revisin actual del tutorial que no impacta en BC++ (no del todo).
Para los curiosos, es posible cambiar el nombre del recurso que usa VC++ editando
manualmente el archivo .rc y cambiando el nombre en dos lugares, primero donde es
inlcudo (#include) y segundo en el recurso TEXTINCLUDE donde est contenido.
El segundo problema es que, por default, VC++ requiere que sea includo dentro del
archivo de recursos el archivo afxres.h , mientras que BC++ tiene todas las macros del
preprocesador que se necesitan, definidas de forma automtica y por lo tanto no requiere
dicha inclusin. Otra cosa asombrosa es que afxres.h solo es instalado si instalas MFC, lo
cual no todo el mundo hace, an cuando ests creando una aplicacin API la cul solo
requiere winres.h, que siempre es instalada.
Debido a que he trabajado con VC++ y he usado su editor de recursos, pude resolver este
problema alterando ligeramente cada archivo .rc incluyendo lo siguiente:
#ifndef __BORLANDC__
#include "winres.h"
#endif
#include "afxres.h"
Para aquellos que estn usando VC++ pueden encontrar la opcin de cambiar este texto
dentro del IDE atravs de "View > Resource Includes". Generalmente, en la prctica
normal, no hay necesidad de hacer siempre esto, slo es una forma que he usado para
solucionar el problema de trabajar con VC++ y BC++ al mismo tiempo.
Para aquellos que usan BC++, pido disculpas por toda la basura que es generada por el
editor de recursos de VC++, pero no puedo hacer nada al respecto.