Trabajando con ventanas en win32
Crear una aplicación en Windows puede parecer complicado, ¡y vaya que lo es al principio! Las codificaciones te parecerán interminables y confusas, te toparás con nuevos tipo de datos, términos incomprensibles y un sin fin de funciones nuevas... pero nada será en vano, debes pasar por aquí para disfrutar las ventajas de saber utilizar la API de Windows.
Cuando consigas ganar experiencia, te darás cuenta de que lo complicado no es mas que una mera apariencia, irás reconociendo funciones y sabrás qué usar y dónde buscar para realizar lo que tienes en mente. La experiencia se gana superando esa prueba mental que impone la programación en Windows.
En este capítulo trataremos algo complejo pero con lo que ya estás familiarizado: las ventanas. Es un tema básico de la programación en Windows, pues como ya sabemos, Windows traducido al español quiere decir "ventanas" y no deberá extrañarte saber que le dedicaremos capítulos enteros a ellas, eso sí, enfocado desde el punto de vista del programador.
Sé que ahora tienes muchas dudas sobre conceptos, tipos de datos, constantes... todo un mundo de información que necesito mostrarte en tan poco tiempo; no te preocupes, todo está fríamente calculado y vendrás resolviéndolas a medida que avanzas en el curso. Por ahora solo me enfoco en las bases, en lo principal, lo realmente útil; los detalles vendrán en su momento indicado.
Bueno, vamos a hacer costumbre esto de iniciar con un código. Para esto, seguiremos los 5 sencillos pasos del capítulo anterior
1. Abrir el entorno de desarrollo (en mi caso Dev-C++)
2. Crear un nuevo proyecto Windows (ver apéndice B.1) que llamaremos Ejercicio2. Es importante hacerlo correctamente ya que si olvidas indicar que trabajamos en un proyecto Windows, no lograrás ejecutar el programa.
3. Agregar un Archivo C++ a nuestro proyecto (ver apéndice B.2) al que llamaremos Programa2.
4. Escribir el siguiente código en el archivo Programa2.cpp. Para fines prácticos recomiendo usar el Copy&Paste, aunque no hay ningún problema si lo transcribes cuidadosamente.
Programa2.cpp
#include
/* La función ProcedimientoVentana() es necesaria porque es la
encargada de recibir los eventos (movimientos del ratón,
tecla, clics a un botón, etc). En este caso, solo monitorea
el momento el que el usuario decide cerrar la ventana para
descargar la aplicación de memoria */
LRESULT CALLBACK ProcedimientoVentana(HWND hwnd, UINT mensaje,
WPARAM wParam, LPARAM lParam)
{
switch (mensaje)
{
case WM_DESTROY:
PostQuitMessage (0);
return 0;
}
return DefWindowProc (hwnd, mensaje, wParam, lParam);
}
/* Esta cadena es necesaria para que la clase de ventana
y la función CreateWindowEx() se refieran a un mismo nombre */
char szNombreAplicacion[] = "Ejercicio2";
int WINAPI WinMain(HINSTANCE hInstancia, HINSTANCE hInstanciaPrev,
LPSTR lpLineaCmd, int nEstadoVentana)
{
//Los 4 pasos para crear una ventana...
//1. Establecer los 12 campos de la "clase de ventana"
WNDCLASSEX ventana;
ventana.cbSize = sizeof(WNDCLASSEX);
ventana.style = CS_HREDRAW | CS_VREDRAW;
ventana.lpfnWndProc = ProcedimientoVentana;
ventana.cbClsExtra = 0;
ventana.cbWndExtra = 0;
ventana.hInstance = hInstancia;
ventana.hIcon = LoadIcon (NULL, IDI_APPLICATION);
ventana.hCursor = LoadCursor (NULL, IDC_ARROW);
ventana.hbrBackground = (HBRUSH) (COLOR_BTNFACE+1);
ventana.lpszMenuName = NULL;
ventana.lpszClassName = szNombreAplicacion;
ventana.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
//2. Dar de alta en el sistema a la "clase de ventana"
if( !RegisterClassEx(&ventana) )
{
MessageBox(NULL,
"No ha sido posible dar de alta a la ventana, abortando ejecución",
"¡Error!", MB_ICONEXCLAMATION | MB_OK);
return FALSE;
}
//3. Crear la ventana en base a la "clase de ventana" anteriormente registrada
HWND hwnd;
hwnd = CreateWindowEx(
0,
szNombreAplicacion, //Nombre de clase de la ventana a la que pertenece
"Programa de ejemplo", //Título de la ventana
WS_OVERLAPPEDWINDOW, //Tipo de ventana
CW_USEDEFAULT, //Posición de la ventana en pantalla en "x"
CW_USEDEFAULT, //Posición de la ventana en pantalla en "y"
300, //Ancho
200, //Alto
HWND_DESKTOP,
NULL,
hInstancia,
NULL
);
if(hwnd == NULL)
{
MessageBox(NULL, "Error al crear la ventana, abortando ejecución",
"¡Error!", MB_ICONEXCLAMATION | MB_OK);
return FALSE;
}
//4. Mostrar la ventana en pantalla
ShowWindow(hwnd, SW_SHOWDEFAULT);
//Necesario para manipular los mensajes o eventos de la ventana
MSG mensaje;
while(GetMessage(&mensaje, 0, 0, 0) == TRUE)
{
TranslateMessage(&mensaje);
DispatchMessage(&mensaje);
}
return mensaje.wParam;
}
5. Generar el proyecto (ver apéndice B.3) y esperamos un momento a que se muestre en pantalla el programa (ver fig. 3.1.1).
fig. 3.1.1 Programa2.cpp en ejecución.
¡Es mucho código para solo poder generar esto! No es cierto, hacer una ventana como esa sin usar las funciones API te habría tomado mucho tiempo. Hacer un programa para consola es relativamente más fácil que hacerlo para Windows, pero hoy en día un programa que no tiene interfase gráfica amigable no se usa ni se vende (salvo extraordinarias excepciones).
3.2 Cómo funciona una aplicación Windows
Una aplicación bajo un sistema gráfico como Windows trabaja de una forma muy diferente a las tradicionales. No podemos precisar el momento en que el usuario decida mover el puntero del ratón, dar un clic sobre un botón o minimizar la ventana. Por ello, el programa debe contar con un código que se encargue de recibir estos eventos en todo momento.
Windows está al pendiente de cualquier evento ya sea un movimiento del ratón, la pulsación de una tecla, el cambio de aplicación activa, la alteración del tamaño de una ventana, etc. Conforme se van presentando, los almacena en una lista que se le conoce como cola de mensajes.
¿Cómo puedo hacer que mi programa se de cuenta o tome control sobre estos eventos? Existen varias funciones API que en conjunto visitan la cola de mensajes y regresan los eventos que corresponden a la aplicación; como programadores podemos decidir qué hacer con estos eventos por medio de instrucciones de condición (if, switch). Es un tema que se profundizará en un capítulo posterior.
Te he introducido al tema de la cola de mensajes y los eventos porque en el código del Programa2, y en cualquier otro programa común de Windows, se utilizan estos conceptos. En el caso del Programa2, que aparentemente no hace nada, se encuentra monitoreando el cierre de la ventana (evento), es decir, cuando el usuario da un clic sobre el botón cerrar o cuando el usuario presiona Alt + F4. ¿Por qué monitorear ese evento? Porque cuando el usuario decide cerrar la ventana, nosotros como programadores queremos que finalice la aplicación.
Nota aclaratoria: Verás, cerrar una ventana y finalizar la aplicación son dos cosas completamente distintas. Esto puede parecerte raro ya que generalmente ambos se presentan al mismo tiempo (cuando cerramos una ventana se finaliza la aplicación), pero ello no implica que sean la misma cosa porque en realidad una aplicación puede seguir recibiendo eventos y seguir trabajando con o sin ventana. Por este motivo, monitoreamos cuando el usuario decide cerrar la ventana para generar otro evento que significa "dejar de procesar eventos/finalizar aplicación"; de no ser así, la aplicación continuaría activa en el sistema aunque no veamos ninguna ventana.
Basta decir que la programación en Windows está repleta de eventos, o sea, son un tema importantísimo. Imagina que se tiene un documento el cual no ha sido guardado, el programa puede monitoriar cuando el usuario desee cerrar la ventana para así mostarle al usuario un aviso "¡Si me cierras pierdes la información! ¿guardar cambios?". Calma, ¡ya tendremos tiempo para aprenderlo!
3.3 Crear una ventana
No quisiera extenderme en el tema de eventos sin antes ser capaces de responder a la siguiente pregunta... ¿cómo crear una ventana? Es algo un tanto complicado de explicar, aunque ya sabemos algunas cosas importantes: debemos incluir el archivo de cabecera windows.h y la función WinMain() que es el punto de partida de nuestro programa. Por razones obvias, ya te imaginarás que generalmente en el punto de partida es donde se decide crear una ventana (podrás comprobarlo en el código del Programa2).
Ahora bien, para crear una ventana hay que seguir 4 pasos. Primero decimos cuales son y en qué consisten y después nos dedicaremos a aclarar las dudas sobre conceptos, tipos de datos, etc. Los pasos son los siguientes:
1. Establecer los 12 campos de la clase de ventana
2. Dar de alta en el sistema a la clase de ventana
3. Crear la ventana en base a la clase de ventana anteriormente registrada
4. Mostrar la ventana en pantalla
1. Establecer los 12 campos de la clase de ventana. Antes de crear una ventana es obligatiorio establecer 12 configuraciones necesarias de la clase de ventana. Verás, una clase de ventana almacena una serie de atributos que definen el aspecto y comportamiento de una ventana. Algunos ejemplos sobre la información que contiene son el color de fondo, ícono, menú y un puntero hacia una función que será la encargada de procesar los mensajes de la ventana.
No te dejes llevar por su nombre ya que su denominación de "clase" no tiene nada que ver con la programación orientada a objetos, así decidieron llamarle, simplemente es una estructura que nos sirve para indicar los datos de iniciación de una ventana (o de una serie de ventanas). Examinando los archivos de cabecera de windows.h podemos ver su declaración:
typedef struct _WNDCLASSEXA {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEXA,*LPWNDCLASSEXA,*PWNDCLASSEXA;
typedef WNDCLASSEXA WNDCLASSEX,*LPWNDCLASSEX,*PWNDCLASSEX;
Observamos tipos de datos con los que no estamos familiarizados (UINT, WNDPROC, HANDLE, HICON...), no les tomes mucha importancia. Por ahora solo vamos a concentrarnos en los nombres de las variables involucradas ya que "esconden" información reelevante. Te he mostrado herramientas para que seas capaz de independizarte, así que vamos a deducir con la notación húngara lo siguiente:
Identificador Significado
cbSize contador de bytes de un tamaño
style estilo
lpfnWndProc puntero "largo" a una función de procedimiento de ventana
cbClsExtra contador de bytes extra de la clase
cbWndExtra contador de bytes extra de la ventana
hInstance manipulador de instancia
hIcon manipulador del ícono
hCursor manipulador del cursor
hbrBackground manipulador de la brocha para color del fondo
lpszMenuName puntero "largo" a la cadena del nombre del menú
lpszClassName puntero "largo" a la cadena del nombre de la clase
hIconSm manipulador de icono pequeño
Como puedes ver, la finalidad de algunos campos es muy descriptiva con solo leer su nombre. A pesar de esto, te introduciré a cada uno de ellos en su momento oportuno. Mientras tanto, te mostraré una sección de código del Programa2 donde tendrás la oportunidad de observar cómo se precisa esta "configuración" para crear una ventana. Te advierto que aparecen nuevas funciones, constantes y, por si fuera poco, otro nuevo tipo de dato (HBRUSH), no te preocupes por entender cada línea, solamente intento darte una vista muy general:
//1. Establecer los 12 campos de la "clase de ventana"
WNDCLASSEX ventana;
ventana.cbSize = sizeof(WNDCLASSEX);
ventana.style = CS_HREDRAW | CS_VREDRAW;
ventana.lpfnWndProc = ProcedimientoVentana;
ventana.cbClsExtra = 0;
ventana.cbWndExtra = 0;
ventana.hInstance = hInstancia;
ventana.hIcon = LoadIcon (NULL, IDI_APPLICATION);
ventana.hCursor = LoadCursor (NULL, IDC_ARROW);
ventana.hbrBackground = (HBRUSH) (COLOR_BTNFACE+1);
ventana.lpszMenuName = NULL;
ventana.lpszClassName = szNombreAplicacion;
ventana.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
Así es como se ha decidido iniciar los campos de la estructura WNDCLASSEX llamada ventana, es decir, de la clase de ventana. Los campos más importantes (en negritas) son lpfnWndProc que apunta hacia la función encargada de recibir y manejar los eventos de la ventana (clics, pulsaciones, movimientos, etc), y lpszClassName que contendrá el nombre de la clase de ventana (que será usada más adelante en el paso 3 en la función CreateWindowEx).
Como comentario adicional, puedes observar que en el campo style se combinan dos constantes (CS_HREDRAW y CS_VREDRAW) utilizando el operador OR, como vimos la sesión pasada.
Continuamos al paso 2. Dar de alta en el sistema a la clase de ventana:
//2. Dar de alta en el sistema a la "clase de ventana"
if( !RegisterClassEx(&ventana) )
{
MessageBox(NULL,
"No ha sido posible dar de alta a la ventana, abortando ejecución",
"¡Error!", MB_ICONEXCLAMATION | MB_OK);
return FALSE;
}
A grandes rasgos, lo anterior llama a la función RegisterClassEx() para registrar la clase de ventana (la estructura de datos en el paso 1). Una vez registrada la clase de ventana, puedes crear tantas ventanas como quieras las cuales tendrán el mismo ícono, color, menú, etc.
Cuando las configuraciones no son aceptadas, la función regresa el valor cero y significa que no podemos crear la ventana. Generalmente sucede cuando hemos llenado incorrectamente alguno de los campos de la clase de ventana. En caso de que no sea aceptada la clase de ventana (no tiene caso seguir ejecutando la aplicación), finalizamos la aplicación con return FALSE por que una aplicación termina cuando termina la función WinMain() y ese return forza a que esto suceda. Observa que antes de que finalice la aplicación por acción del return, por cortesía se ha decidido presentar al usuario un mensaje que explique lo sucedido (se ha utilizado la función MessageBox() que vimos en el capítulo anterior).
3. Crear la ventana en base a la clase de ventana anteriormente registrada Aquí tienes que llamar a una función CreateWindowEx() donde también hay configuraciones que indicar, aunque estas son en su mayoría configuraciones del aspecto de la ventana. El pedazo de código:
//3. Crear la ventana en base a la "clase de ventana" anteriormente registrada
HWND hwnd;
hwnd = CreateWindowEx(
0,
szNombreAplicacion, //Nombre de clase de la ventana a la que pertenece
"Programa de ejemplo", //Título de la ventana
WS_OVERLAPPEDWINDOW, //Tipo de ventana
CW_USEDEFAULT, //Posición de la ventana en pantalla en "x"
CW_USEDEFAULT, //Posición de la ventana en pantalla en "y"
300, //Ancho
200, //Alto
HWND_DESKTOP,
NULL,
hInstancia,
NULL
);
if(hwnd == NULL)
{
MessageBox(NULL, "Error al crear la ventana, abortando ejecución",
"¡Error!", MB_ICONEXCLAMATION | MB_OK);
return FALSE;
}
Quizás el parámetro más importante de la función CreateWindowEx() sea el segundo (en negrillas), ya que contiene el nombre de la clase de ventana a la cual pertenece. Si esta clase de ventana no existe o no ha sido aceptada por el sistema, tan sencillo como que no crea la ventana y en ese caso la función retorna un valor de NULL. Por eso, no es de extrañarse que después de la función el código tenga una condición que compruebe si el valor regresado por CreateWindowEx() es igual a NULL (hwnd == NULL), ya que si no es posible la creación de ventana se muestra un mensaje y se finaliza la aplicación (no tiene caso seguir ejecutándola).
Y por último, 4. Mostramos la ventana:
//4. Mostrar la ventana en pantalla
ShowWindow(hwnd, SW_SHOWDEFAULT);
En Windows es necesaria la función ShowWindow() para mostrar una ventana. El identificador de ventana creada por la llamada para CreateWindowEx() está alojado en el parámetro hwnd. Hay muchas posibilidades para mostrar la ventana como SW_SHOWMAXIMIZED que hace que la ventana este activa y ocupe toda la pantalla y SW_SHOWMINIMIZED.
3.4 Cambiar el color de fondo de la ventana
Antes que nada te explico que el objetivo de este apartado es que interactúes con el código y que te vayas soltando un poco. No te preocupes por entrar en detalles que por ahora no necesitas saber, los cómos y los porqués vendrán después. Hasta el momento, hemos logrado crear una ventana con un color que viene configurado en Windows (por defecto es gris claro como en la fig. 3.1.1), es decir, el color que el usuario ha configurado en "Propiedades de pantalla/Apariencia".
Como ya sabes, en el paso 1 definimos doce campos de la clase de ventana; uno de los campos es el que fija el color del fondo de la ventana (hbrBackground). Como ya estarás sospechando, para cambiar el color del fondo de la ventana tendremos que modificar el contenido de ese campo.
En el Programa2, la clase de ventana tiene el nombre de ventana. Vamos a localizar la línea que dice "ventana.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);" y después la modificaremos por una y solo una de las siguientes líneas:
//Hay varias formas de cambiar el color...
//========================================
//Utilizando los colores definidos por el usuario en "Propiedades
//de pantalla" (como el gris claro que usamos originalmente)
ventana.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
ventana.hbrBackground = (HBRUSH) (COLOR_BTNTEXT + 1);
ventana.hbrBackground = (HBRUSH) (COLOR_BTNHIGHLIGHT + 1);
ventana.hbrBackground = (HBRUSH) (COLOR_APPWORKSPACE + 1);
ventana.hbrBackground = (HBRUSH) (COLOR_BACKGROUND + 1);
ventana.hbrBackground = (HBRUSH) (COLOR_HIGHLIGHT + 1);
ventana.hbrBackground = (HBRUSH) (COLOR_MENU + 1);
//Utilizando colores básicos
ventana.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
ventana.hbrBackground = (HBRUSH)GetStockObject( BLACK_BRUSH );
//Creando un color específico producto de la mezcla de los 3
//colores básicos RGB(rojo, verde, azul)
ventana.hbrBackground = CreateSolidBrush( RGB(0xFF, 0xFF, 0xFF) );
ventana.hbrBackground = CreateSolidBrush( RGB(0x00, 0x00, 0x00) );
ventana.hbrBackground = CreateSolidBrush( RGB(0xFF, 0x00, 0x00) );
ventana.hbrBackground = CreateSolidBrush( RGB(0x00, 0xFF, 0x00) );
ventana.hbrBackground = CreateSolidBrush( RGB(0x00, 0x00, 0xFF) );
ventana.hbrBackground = CreateSolidBrush( RGB(0xC0, 0xC0, 0xC0) );
No creo que tenga pierde la indicación, es seleccionar alguna instrucción y reemplazarla en el código. Hecho el cambio, se vuelve a compilar el programa para observar los efectos. Si así lo deseas, puedes seguir practicando con cada una de las líneas anteriormente mostradas.
3.5 La función CreateWindowEx()
La función CreateWindowEx() tiene 12 parámetros (como mnemotecnica, la clase de ventana tiene 12 campos). El prototipo de la función es el siguiente:
HWND CreateWindowEx(
DWORD dwExStyle, // un tal "estilo extendido"
LPCTSTR lpClassName, // puntero al nombre de la clase a la que
// pertenece
LPCTSTR lpWindowName, // p. a la cadena que contiene el título
// de ventana
DWORD dwStyle, // estilo de ventana (características)
int x, // posición horizontal de la ventana
int y, // posición vertical de la ventana
int nWidth, // Ancho de la ventana
int nHeight, // Alto de la ventana
HWND hWndParent, // manipulador de ventana padre
HMENU hMenu, // manipulador del menú
HINSTANCE hInstancia, // manipulador de Instancia (2do param. WinMain())
LPVOID lpParam // puntero al valor lpParam
);
Por ahora puedes modificar los parámetros en negrillas, es decir, los parámetros 3, 5, 6, 7 y 8. Cuida que el tercer parámetro sea una "cadena de caracteres" y que los parámetros del 5 al 8 sean números enteros. Modifica y vuelve a compilar para observar los cambios.
Como puedes observar en el prototipo de la función, ésta regresa un tipo de dato HWND. Ese dato es muy útil para manipular a la ventana. Por ahora solo sabemos que ese dato regresado será igual a NULL cuando ocurre un error al crear la ventana (comunmente porque no existe la clase de ventana o aún no se ha dado de alta en el sistema satisfactoriamente). Es por ello que agregamos a nuestro código una línea de verificación (if(hwnd == NULL) { ... [fin de aplicación] ... }).
3.6 Ejercicios
1. Crear una ventana. Modifica el código del Programa2 para crear una ventana de 400x200 (ancho x alto), con el color de fondo rojo ( CreateSolidBrush(RGB(0xFF, 0x00, 0x00)) ), en la barra de título deberá mostrar "Programando en Windows"
fig. 3.6.1 Así debe quedar la ventana del ejercicio 1.
Etiquetas: trabajando con ventanas en win32

0 comentarios:
Publicar un comentario
Suscribirse a Enviar comentarios [Atom]
<< Inicio