Inicio Reflexiones Guitarra Engineering Programación Win32

Mi primer programa
Capítulo 2, por Ricardo González Garza


Este capítulo supone que tienes experiencia en. . .
Manejo del entorno Windows
- ¿Qué es...? una ventana, un menú, un clic, etc.
C++:
- Manejo de variables, constantes, funciones, archivos de cabecera, punteros, etc.
- En el uso de strings (strings.h) especialmente con la función strcmp()

Tiempo estimado de lectura y práctica: 45 min.

2.1 Indicaciones

Voy a comenzar con algo pequeño y verá lo fácil que es utilizar directamente la API de Windows. Aprenderá a mostrar un mensaje en pantalla y las características del código C++ en una aplicación en Windows. Para ello se seguirán estos 5 sencillos pasos:

  1. Abrir el entorno de desarrollo (en mi caso Dev-C++)
  2. Crear un nuevo proyecto Windows (ver apéndice B.1) que llamaremos Ejercicio1. Es importante hacerlo correctamente ya que si olvida indicar que trabajamos en un proyecto Windows, no logrará ejecutar el programa.
  3. Agregar un Archivo C++ al proyecto (ver apéndice B.2) que llamaremos Programa1.
  4. Escribir el siguiente código en el archivo Programa1.cpp. Para ir adquiriendo confianza, te recomiendo transcribirlo completamente (sin hacer uso del Copy&Paste).
Programa1.cppCopiar cógido
#include <windows.h>

int WINAPI WinMain(HINSTANCE p1, HINSTANCE p2, LPSTR p3, int p4)
{
   MessageBox(NULL, "¡Bienvenido a la programación Windows!", "Ejemplo", MB_OK);
   return 0;
}
  1. Generar el proyecto (ver apéndice B.3) y esperar un momento.

¡Increíble! Ha sido capaz de mostrar un mensaje en pantalla completamente funcional como en la fig. 2.1.1. No estoy exagerando, ha sido un gran logro en su aprendizaje. Lo que ha hecho le hubiera tomado semanas enteras pero solo le ha tomado unos cuantos minutos gracias a que accede a funciones de la API. Dése cuenta que ha sido capaz de crear una ventana que cuenta con una barra de título (1), una etiqueta de texto (3) y dos botones (Aceptar 2 y Cerrar 4), el mensaje tiene buena apariencia, se puede mover por toda la pantalla, sin contar demás detalles, ¡esto pone interesante!.

fig. 2.1.1 Ejercicio1 ejecutándose.

En caso de haber errores: el compilador le mostrará un mensaje con una descripción (aunque a veces no sea de mucha ayuda). Todo varía dependiendo del tipo de compilador. El problema más común es que el compilador no este configurado para trabajar en un proyecto Windows y no encuentre la función main(). Al indicarle a nuestro entorno de desarrollo que se trabaja en un proyecto Windows, estas configuraciones necesarias se activan automáticamente (he ahí la importancia) y se evitan estos errores, le recomiendo visitar el apéndice B.
Otro posible error, que no le debería de ocurrir a estas alturas, es haber escrito mal el código. Verifique que ha puesto los puntos y coma (;) en su lugar correspondiente, al igual que las llaves que abren y cierran ({), y no olvide que C++ distingue entre mayúsculas y minúsculas. Si este es su caso, le recomiendo repasar algún manual de C++ y dejar este curso para otro momento.

2.2 #include <windows.h>

Esta es la primera línea del programa anterior y servirá para incluir el archivo de cabecera windows.h que contiene todas las funciones, estructuras, macros y constantes numéricas que forman la API. Resumiendo, en todo proyecto para crear aplicaciones Windows es necesario incluir a windows.h.

Si le ayuda, puede verlo como un equivalente a las librerías estándar utilizadas en las aplicaciones de consola (ej. iostream).

El archivo de encabezado windows.h proporciona una ruta de acceso a más de mil declaraciones de constantes, declaraciones typedef y cientos de prototipos de funciones.

2.3 La función WinMain()

Ésta es la tercera línea del programa (contando la línea en blanco). En las aplicaciones de consola se utilizan la función "main()" para indicar el punto de partida del programa. Esa función ya no resulta para aplicaciones Windows. WinMain() sustituye a la función "main()".

La función WinMain() es el punto donde inicia y termina la ejecución de una aplicación Windows. Al igual que el archivo de cabecera windows.h, todas las aplicaciones de Windows necesitan la función WinMain(). Esto es ventajoso ya que así podemos distinguir entre un programa genérico diseñado para consola y un programa para Windows.

Recorde cómo escribió esta función:

MuestraCopiar cógido
int WINAPI WinMain(HINSTANCE p1, HINSTANCE p2, LPSTR p3, int p4)
{
   ...
}

Al finalizar la función WinMain(), se deberá devolver un número entero al sistema operativo, por lo que habremos de declararla como int.

La constante WINAPI, escrita entre el tipo de retorno y el nombre de la función, indica al compilador que la función WinMain() es la encargada de limpiar la pila y por el momento no necesita comprender este hecho. Está definida como __stdcall dentro de windows.h (en windef.h). Puede suceder que lea algún otro código y encuentres que en lugar de WINAPI utilicen otras constantes como APIENTRY o PASCAL, son exactamente lo mismo. Lo que pasa es que todas ellas existen por cuestiones de compatibilidad de códigos (la evolución de las librerías, ¡por supuesto!), pero mi recomendación será siempre utilizar WINAPI que es la constante más reciente.

La función WinMain() cuenta con 4 parámetros. Con toda la intención del mundo y de manera didáctica, los parámetros (p1, p2, p3 y p4) tienen nombres extraños y nos dan una vaga idea de lo que almacenan (es una mala práctica que no se debe tener y que se corrigirá más adelante).

Los dos primeros parámetros, p1 y p2, contienen un número único generado automáticamente por el sistema operativo al momento de ejecutar el programa (son del tipo HINSTANCE). El número de p1 sirve para referirnos a nuestra aplicación y diferenciarla de las demás. El número de p2 existe por cuestiones de compatibilidad con aplicaciones Win 3.x, pero por ahora basta decir que ya no se usa y que siempre guardará el valor cero (NULL).

El tercer parámetro p3 es un puntero hacia una cadena de caracteres terminada en cero (LPSTR o char*). En ella se guardarán los argumentos de secuencias de comandos indicados cuando se manda a ejecutar la aplicación (puede ser que sean enviados desde la consola, un acceso directo, el explorador de Windows u otro programa). Por ejemplo, si se ejecuta el programa desde la opción Menú Inicio / Ejecutar de esta forma: programa1.exe /AYUDA... la variable p3 contendrá la cadena "/AYUDA" (que puede procesar mediante código y darle un significado para decidir qué hacer con ella). Normalmente este parámetro contiene un NULL ya que al ejecutar el programa no se indican argumentos.

El cuarto y último parámetro p4 es un número entero que representa alguna de las muchas constantes predefinidas de Windows, lo explicaré adelante en este capítulo. Este número indica la manera en que la ventana deba mostrarse, ya sea maximizada, minimizada, etc. Como ejemplo, este parámetro toma un valor cuando los accesos directos se configuran para abrir la ventana Maximizada o en algún otro estado en especial.

2.4 Acuerdos

Cuando diseñaron Windows, se utilizó una metodología especial para nombrar variables, constantes, clases, estructuras, etc: la Notación Húngara. Se le llama así en honor a su inventor, Charles Simonyi, de origen húngaro. Él propuso que cada nombre debe llevar un prefijo que identifique al tipo de dato, así por ejemplo, un programador cualquiera puede saber fácilmente el tipo de variable que se trata sin tener que buscar su declaración.

Aunque no es estrictamente necesario, es de mucha utilidad y es una práctica común al programar para Windows. No es un requisito que utilice estos prefijos, pero le será de mucha ayuda conocerlos. Aquí va una lista de prefijos más utilizados para nombrar constantes y variables:

Prefijo Tipo de dato
b bool
c char (un solo caracter)
cb contador de bytes
d DOUBLE
fn función
h manipulador (handle)
l long o LONG
n int
p puntero
pt CPoint o POINT estructura usada para almacenar coordenadas x, y
str CString
sz cadena de caracteres terminada en cero (string with zero)
w WORD

Se utilizará a lo largo de todo el curso, así que no intente aprenderse toda la tabla... mejor tómela de referencia y ya verá que con el tiempo identificará casi inconscientemente su tipo. Generalmente se usa la primera letra o una abreviación del tipo de dato.

Ejemplos: nEdad es un número entero de alguna edad; hInstance es un manipulador de instancia (después veremos a que se refiere con "manipulador de instancia").

Se pueden realizar combinaciones mientras sea válido, por ejemplo, lp para long pointer o dw para double word. Así, lpszNombre indica que es un puntero largo que apunta a una cadena de caracteres terminada en cero y que contiene un nombre (aunque algunos optan por nombrarlo lpNombre para no hacerlo muy grande, lo correcto es ponerlo completo).

¡Alto!, ¿lp o puntero largo? en realidad es un simple puntero y ya. Antiguamente existían dos tipos de punteros: cortos (de 2 bytes) y largos (de 4 bytes). A partir de Windows 95 se usan solo los punteros de 4 bytes y ha quedado la costumbre de seguirles llamando largos.

Basado en esto, se rescribirá la función WinMain() como debería de ser:

Función WinMain() usando declaraciones de windows.hCopiar cógido
int WINAPI WinMain(HINSTANCE hInstancia, HINSTANCE hInstanciaPrev,
                   LPSTR lpszLineaCmd, int nEstadoVentana)
{
   ...
}

2.5 Tipo de datos y constantes

En la programación en Windows existen tipos de datos "nuevos". En nuestro programa anterior tuvimos la oportunidad de trabajar dos: HINSTANCE y LPSTR. Como ya se habrá dado cuenta, estos tipos no existen en la programación C++ estándar. ¿Por qué complicar más las cosas? Es por una razón muy simple, si se realizan actualizaciones al sistema, los programadores no tienen que cambiar sus códigos ni aprender las nuevas reglas (en teoría solo se remplazan los archivos de cabecera). Los tipos de datos están definidos por instrucciones typedef o #define.

Otra cosa, es muy común que las funciones trabajen y regresen números para indicar algo en específico, un ejemplo, el cuarto parámetro de la función WinMain(), nEstadoVentana, tomará el valor de 2 para indicar que la ventana debe mostrarse minimizada o 3 para indicar que debe ser maximizada. Obviamente no se espera que memorice el significado de cada número para cada función, sería un caos, por lo que se ha creado una lista de constantes que explican por sí solas la utilidad de la información contenida en el valor. En este primer programa ya ha utilizado dos de esas constantes: NULL y MB_OK (ambas valen cero). Puede utilizar los números en vez de las constantes, da exactamente lo mismo, aunque probablemente pierda mucho tiempo tratando de recordar su significado al realizar modificaciones.

Sabiendo esto y si así lo desea, puedes escribir el código de la siguiente manera:

Otra forma de escribir el códigoCopiar cógido
#include <windows.h>

int __stdcall WinMain(HINSTANCE hInstancia, HINSTANCE hInstanciaPrev,
                      char* lpszLineaCmd, int nEstadoVentana)
{
   MessageBox(0, "¡Bienvenido a la programación Windows!", "Ejemplo", 0);
   return 0;
}

No lo recomiendo, pero sí lo puede hacer. La desventaja es que el código será tarde o temprano quedará obsoleto si actualizan la API y perderá legibilidad. También se tendrá que recurrir a la documentación de Windows para averiguar que significa el 0 en el cuarto parámetro de MessageBox(), a diferencia de utilizar su constante MB_OK que por lo menos da una idea que se trata del botón Aceptar.

Se estarán continuamente manejando esas constantes numéricas. Volviendo al cuarto parámetro de la función WinMain(), no diré que nEstadoVentana tomará el valor de 2 o 3, sería complicado. En lugar de decir eso, me referiré a que regresará valores SW_SHOWMINIMIZED o SW_SHOWMAXIMIZED, ¡más fácil!.

Si es curioso puede examinar los archivos de cabecera de windows.h, donde es posible encontrar sus siguientes declaraciones:

Definiciones encontradas en winuser.h (dentro de windows.h)Copiar cógido
  #define SW_SHOWNORMAL 1
  #define SW_SHOWMINIMIZED 2
  #define SW_SHOWMAXIMIZED 3

¿Complicado? Las constantes se distinguen porque están escritas en MAYÚSCULAS, tienen un prefijo generalmente de dos letras (SW_, CS_, WM_, etc) y siempre es una buena idea usarlas. El prefijo indica la categoría o su finalidad como por ejemplo, SW_ que se refiere a Show Window (mostrar ventana). Con el tiempo irá memorizando las más comunes y de ahora en adelante sabrá que la documentación de Windows es un apoyo inseparable, como se verá en otro capítulo más avanzado, donde le diré cómo y dónde conseguirla.

2.6 La función MessageBox()

Nuevamente, si se busca en el archivo windows.h (dentro de winuser.h) encontrá el prototipo de esta función:

Prototipo de la función MessageBox (dentro de winuser.h)Copiar cógido
  int WINAPI MessageBoxW(HWND,LPCWSTR,LPCWSTR,UINT);

  #define MessageBox MessageBoxW

La función MessageBox() tiene 4 parámetros de los cuales ahora solo interesan los últimos 3: el segundo y tercer parámetro son punteros hacia cadenas de texto (LPCWSTR); el segundo indica el contenido del mensaje a mostrar en pantalla, y el tercero es un string que se mostrará en la barra de título del mensaje. El cuarto parámetro es un número entero sin signo (unsigned int o UINT) en el cual indicará el estilo del cuadro de mensaje.

Hasta ahora al cuarto parámetro le hemos indicado el valor MB_OK (que sabemos vale cero) porque ese número indica que quiere mostrar el botón Aceptar. Si al cuarto parámetro se le indica otro número, es posible mostrar un ícono de error o de pregunta, además de mostrar otros botones diferentes de "Aceptar". Esto no se hace al azar, ya he explicado que contamos con constantes numéricas para indicar qué botones y qué ícono mostraremos en pantalla. Aquí va una pequeña lista de constantes numéricas ya declaradas en windows.h y que puede combinar para mostrar diferentes mensajes:

DefinicionesCopiar cógido
// BOTONES
#define MB_OK 0                    //botón "Aceptar"
#define MB_OKCANCEL 1              //botones "Aceptar" y "Cancelar"
#define MB_ABORTRETRYIGNORE 2      //botones "Anular", "Reintentar", "Omitir"
#define MB_YESNOCANCEL 3           //botones "Sí", "No" y "Cancelar"
#define MB_YESNO 4                 //botones "Sí" y "No"

// ICONOS
#define MB_ICONINFORMATION 64      //ícono de información
#define MB_ICONEXCLAMATION 0x30    //ícono de alerta, signo de exclamación
#define MB_ICONERROR 16            //ícono de error, alto, tacha roja
#define MB_ICONQUESTION 32         //ícono de pregunta

Para combinar dos constantes (ejemplo MB_ICONERROR y MB_OKCANCEL) debe utilizar el operador OR numérico, es decir, las juntas por medio del "|" (MB_ICONERROR | MB_OKCANCEL ... o también ... 16 | 1 ... o inclusive ... 17). ¿Muy sencillo no?, es una técnica que se utilizará en muchas funciones API para fusionar los significados de dos o más constantes, vea este pequeño ejemplo:

EjemploCopiar cógido
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstancia, HINSTANCE hInstanciaPrev,
                   LPSTR lpszCmd, int nEstadoVentana)
{
  MessageBox(0,"¿Guardar cambios?", "Título", MB_YESNO | MB_ICONQUESTION);
  return 0;
}

Nota aclaratoria: Hay ocasiones en que la suma de las constantes (MB_YESNO + MB_ICONQUESTION) rinden el mismo resultado que utilizando el operador OR numérico (MB_YESNO | MB_ICONQUESTION). Algunos programadores suman las constantes y les funciona, pero no es lo mismo y no debe seguir sus pasos. Para evitar errores, le recomiendo utilizar siempre el operador OR numérico "|", nunca sume las constantes en sus códigos.
Supongo que a su nivel debería entender este "fenómeno". Con esto se busca que si por accidente se repite una constante, el resultado esperado no cambie (sabemos que MB_OKCANCEL | MB_OKCANCEL = MB_OKCANCEL), cosa que si se hubieran sumado hubiera resultado en otra cosa (MB_OKCANCEL + MB_OKCANCEL = MB_ABORTRETRYIGNORE ... sustituyendo... 1 + 1 = 2). ¡Utilice |!

2.7 Ejercicios

1. Profundizando en la función MessageBox. Deberá mostrar un mensaje en pantalla, con la función MessageBox(), de manera que sea idéntico a la siguiente imagen (fig. 2.7.1):

fig. 2.7.1 Ejercicio de la función MessageBox().

2. Prueba de fuego. Esta es la prueba de fuego, supuestamente ahora es capaz de crear y entender este programa:

Ejercicio1.cppCopiar cógido
#include <windows.h>
#include <string.h>

int WINAPI WinMain(HINSTANCE hInstancia, HINSTANCE hInstanciaPrev,
                   LPSTR lpszLineaCmd, int nEstadoVentana)
{
    if (strcmp(lpszLineaCmd, "PASS:Windows") == 0)
        MessageBox(NULL, "¡Contraseña aceptada!",
                   "Bienvenida", MB_OK | MB_ICONINFORMATION);
    else
        MessageBox(NULL, "Contraseña incorrecta", "Error", MB_OK | MB_ICONERROR);
    return 0;
}

fig. 2.7.2 Menú Inicio / Ejecutar.

Pues sucede que si genera un ejecutable de ese programa, va a Menú Inicio / Ejecutar y teclea su ruta junto con la contraseña, como en la fig. 2.7.2, recibirá un mensaje como en la fig. 2.7.3. ¿Cómo ve? apenas estamos calentando motores y ya es capaz de implementar una sencilla restricción en sus programas.


fig. 2.7.3 Mensaje de contraseña aceptada.

En caso de haber errores: Si le muestra siempre el mensaje de contraseña incorrecta, es por eso, no ha introducido correctamente la contraseña. Este error no le debería de pasar en este curso a este nivel, pues sabe que la función strcmp() compara el ASCII de sus dos argumentos y regresa 0 si son iguales. Por lo tanto, deberá escribir la contraseña tal y como fue programada (usando mayúsculas y minúsculas).
Otros posibles errores, son no haber configurado el código para compilar para Windows o haber escrito mal el código, ¡cuidado! quiere decir que no ha comprendido el capítulo de inicio a fin.

2.8 Resumiendo. . .

Ya sabe que siempre hay que incluir el archivo de cabecera windows.h y que el punto de partida de un programa Windows es la función WinMain(), la cual tiene cuatro parámetros de quienes podemos deducir brevemente sus significados si hacemos uso la notación húngara:

Parámetro Significado
HINSTANCE hInstancia manipulador de instancia
HINSTANCE hInstanciaPrev manipulador de instancia previa
LPSTR lpszLineaCmd puntero de cadena terminada en cero que apunta a la línea de comandos
int nEstadoVentana un número entero para el estado de la ventana

Los nombres por sí mismos nos dan una gran referencia de su finalidad, aun sin embargo, estoy consciente de las dudas que crean los dos primeros parámetros (¿qué es un manipulador? ¿qué es una instancia?) por lo que en los dos siguientes capítulos abriré espacios para explicar detenidamente esos conceptos y básicamente incrementaremos el nivel del curso para que no se quejen los listos.



comentarios@rickygzz.com.mx