domingo, 19 de enero de 2014

La programación Windows I

En la serie de entradas tituladas “El lenguaje C” vimos cómo desde el sitial privilegiado ofrecido por un lenguaje de alto nivel como el lenguaje C podemos elaborar programas que, una vez compilados a código binario ejecutable en lenguaje de máquina, permiten que una computadora pueda ser utilizada como tal para aplicaciones que pueden ir desde contables y financieras hasta aplicaciones científicas. Pero lo que vimos funcionaba bien en sistemas operativos como UNIX y PC-DOS en los cuales la interacción con la máquina se lleva a cabo a través de una pantalla en la cual se empieza con una ventana de líneas de comandos. Esto era lo usual en los tiempos en los que las terminales de computadoras eran pantallas monocromáticas fabricadas con la (ya obsoleta) tecnología de los voluminosos tubos de rayos catódicos (CRT) en donde lo que se ponía en la pantalla eran caracteres de texto. La introducción de tarjetas (adaptadores) de video capaces de superar la limitación de usar la pantalla de la terminal únicamente para imprimir en ella caracteres de texto logró que en la pantalla se pudiesen obtener gráficas, y extendiendo a los monitores monocromáticos la tecnología CRT usada en los añejos (y voluminosos) televisores de color se empezaron a obtener gráficas en lo que sería el paso eventual tiempo después a la obtención de imágenes fotográficas de alta calidad. Aún desde antes de que hicieran su aparición los monitores del tipo CRT con capacidades crecientes de resolución y variedades cada vez más grandes de colores, la añeja interfaz de líneas de comandos experimentó una transición hacia una interfaz visual que permitió empezar a prescindir de la interfaz de líneas de comandos. En efecto, los avances en la tecnología del hardware hicieron posible una transición suave pero acelerada de las ventanas de líneas de comandos a ventanas de carácter cien por ciento gráfico en donde hasta los caracteres de texto eran reconstituídos mediante el uso adecuado de los pixeles (puntitos de imagen) disponibles en el entorno gráfico. Esto se consolidó con la aparición de las interfaces visuales como la interfaz visual Windows 1.0 introducida por la empresa Microsoft, una interfaz visual que fue evolucionando hasta que se llegó a la interfaz visual Windows 3.1, tras lo cual la interfaz visual en sí se convirtió en la plataforma central del sistema operativo.

La pregunta crucial al llevarse a cabo la transición del sistema operativo basado en las ventanas de líneas de comandos al sistema operativo basado en un sistema operativo cien por ciento gráfico como Windows fue, desde luego, ¿qué lenguaje de programación es el que habrá de ser usado para construír programas ejecutables capaces de poder funcionar bajo el entorno nuevo de las interfaces visuales?

El entorno de programación Visual Basic 1.0 proporcionó a los programadores por vez primera una manera de poder elaborar programas ejecutables capaces de poder ser invocados desde la interfaz visual de Windows, recurriendo para ello a la primera gran aplicación práctica de la programación orientada a objetos, proporcionándose para ello lo que se conoce como una “Caja de Herramientas” (Toolbox) que permitió empezar con una ventana prototipo (conocida como Forma en los entornos de programación Visual Basic) para ir metiendo en dicha ventana varios objetos (botones de opción, cajas de texto, cajas de imagen, etcétera) creados a partir de las clases definidas en la Caja de Herramientas. Sin embargo, esto por sí solo no era suficiente para los programadores profesionales. Supóngase que en uno de los primeros entornos Visual Basic se hubiese querido construír una ventana que pudiese actuar como una reproductora de música MP3. En la Caja de Herramientas no había absolutamente nada para reproducir sonido, y mucho menos había clases para poder crear objetos capaces de grabar sonido a través de una tarjeta de sonido agregada a la computadora casera. Con el tiempo, Microsoft fue agregando clases adicionales a la Caja de Herramientas, y permitió que otros productores de software pudieran desarrollar clases de objetos capaces de lograr cosas para las cuales los paquetes de Visual Basic no ofrecían recurso alguno. Para el desarrollo de estas nuevas clases de objetos, puesto que no formaban parte de Visual Basic ni ofrecía Visual Basic la forma de desarrollarlos, ¿cuál era el lenguaje de programación a ser usado? ¿Era necesario desarrollar un lenguaje de programación completamente nuevo, diferente del lenguaje C, para poder seguir avanzando? Independientemente del esfuerzo requerido para desarrollar un lenguaje de programación completamente nuevo, a muy pocos programadores les atrae la idea de comenzar de cero. Es como haber aprendido a hablar y a escribir en árabe, para que poco tiempo después del esfuerzo invertido se nos diga que tenemos que aprender a hablar y a escribir en chino. Esto, inclusive como broma, sería tomado por cualquier programador como una broma de muy mal gusto.

La alternativa lógica es: ¿por qué no usar un lenguaje de programación como el lenguaje C, o inclusive el mismo lenguaje C, pero modificado de modo tal que se puedan elaborar programas que se puedan ejecutar en código binario bajo una interfaz visual o bajo un sistema operativo gráfico como Windows de Microsoft? Esto es mucho más aceptable y digerible que comenzar de cero, y fue precisamente la ruta que (afortunadamente) Microsoft decidió tomar en una época en la que no había “mapas” trazando la ruta a seguir y cada “mapa” nuevo se tenía que ir construyendo sobre la marcha a sabiendas de que los pasos dados serán los fundamentos sobre los cuales se seguirán construyendo en forma progresiva cosas aún más avanzadas. Es así como hemos llegado al día de hoy. Lo que el lector tiene en sus manos es el resultado de una evolución paulatina y progresiva y no el resultado de echar todo por la borda en un momento dado (y ha habido varias épocas en las que esa posibilidad ha sido tentadora) para empezar de cero con algo completamente nuevo y diferente.

Para calar la magnitud del salto dado al pasar de pantallas monocromáticas que solo podía poner texto alfanumérico a pantallas de colores capaces de manipular entornos gráficos con control absoluto sobre cada puntito de imagen en la pantalla, resulta conveniente hacer un resumen de lo que fue sucediendo antes de que las interfaces gráficas como Windows hicieran su aparición.

En el sistema operativo tipo DOS (llamado PC-DOS cuando fue elaborado por Microsoft para uso exclusivo de la empresa IBM, y llamado MS-DOS cuando fue elaborado por Microsoft para ser vendido independientemente por Microsoft a otros fabricantes de computadoras personales caseras, aunque con la garantía de una compatibilidad casi total entre el PC-DOS y el Ms-DOS ) de líneas de comandos, con memoria RAM sumamente limitada, el programa ejecutable usado para el procesamiento de comandos reside en un programa llamado COMMAND.COM. Al aparecer en la pantalla el prompt (peticionador automatizado del sistema pidiendo al usuario algún tipo de entrada) “A>” (correspondiente a un diskette magnético) o el prompt “C>” (correspondiente a un disco duro). Puesto que la interfaz para las líneas de comandos es lo primero que ven los usuarios de ese tipo de máquinas, muchos creyeron erróneamente que el programa COMMAND.COM era el sistema operativo. En rigor de verdad, COMMAND.COM era el procesador de los comandos de línea, pero era tan solo uno de los componentes del sistema operativo, y de hecho lo podemos considerar como un programa especial que se ejecuta bajo el control del sistema operativo. Los tres componentes más importantes de este sistema operativo son el DOS-BIOS (un archivo del sistema que ha aparecido bajo varios nombres tales como IBMBIO.COM, IBMIO.SYS, o IO.SYS); el núcleo (kernel) DOS puesto en el archivo IBMDOS.COM o MSDOS.SYS; y el procesador de comandos COMMAND.COM que a su vez consiste de tres módulos: una porción residente en la memoria, una porción transitoria y la rutina de inicialización. El primer sistema operativo tipo DOS para computadoras caseras fabricadas con los microprocesadores Intel basados en la arquitectura 8086/8088 fue construído laboriosamente por Microsoft recurriendo a la programación en ensamblador, esto porque la poca cantidad de memoria RAM disponible en la máquina requería que el código ejecutable fuese lo más pequeño y compacto posible, pero eventualmente al ir evolucionando el MS-DOS de la versión 1.0 hasta la versión 6.0 la elaboración de los sistemas operativos de Microsoft se empezó a basar en su desarrollo en lenguaje C.

El procesador de comandos COMMAND.COM era capaz de tomar un comando de línea introducido por el usuario desde el teclado como:

   cls (clear screen, borrar todo lo que hay en la pantalla y empezar de nuevo)

   copy (copiar archivo)

   dir (listar los contenidos de un directorio o sub-directorio)

   erase (borrar un archivo)

   rmdir (eliminar un directorio ya vacío)

Podemos visualizar estos comandos como funciones en lenguaje C que pueden ser invocadas por una función principal main() en C que define al programa principal COMMAND.COM. Así pues, podemos ir pasando de una función a otra conforme se van ejecutando comandos DOS.

A continuación tenemos una secuencia de ejecución de comandos de línea en una ventana tipo DOS simulando acciones que ocurren a partir del momento en que se enciende la máquina y la parte principal del sistema operativo DOS se ha terminado de cargar en la memoria RAM (en el ejemplo mostrado, lo que imprime el sistema operativo en la pantalla aparece en color blanco, mientras que lo que introduce el usuario desde el teclado aparece en color amarillo, aunque en una pantalla monocromática solo habrá un mismo color para ambas cosas):


 Current date is Tue 1-01-1980
 Enter new date (mm-dd-yy): 10-16-84
 Current time is 0:01:30.00
 Enter new time: 7:10

 The IBM Personal Computer DOS
 Version 3.10
 C> A:
 A> dir

 Volume in drive A has no label
 Directory of A:\

 NOTA        DOC            94    10-16-84     7:16
 CARTA      DOC           94    10-16-84     7:16
                   2 Files(s)    3604448 bytes free

 A> erase a:nota.doc

 A> dir

 Volume in drive A has no label
 Directory of A:\

 CARTA      DOC           94    10-16-84     7:16
                1 Files(s)    361472 bytes free

 A>


Como puede verse, el usuario primero actualiza la fecha del sistema y después actualiza la hora (se trata de una máquina viejita de las que carecían de una batería interna para mantener el día y la hora exacta de la computadora, y se trata de un ritual incómodo que se tenía que repetir cada vez que la máquina era encendida), cambia de unidad de memoria del disco duro C al diskette magnético A, usa el comando dir para enterarse de los archivos que están almacenados en el diskette magnético, borra uno de los archivos con el comando erase y vuelve a listar el directorio para asegurarse de que el archivo seleccionado fue borrado. Esta interfaz primitiva requería que el usuario se aprendiese de memoria los comandos disponibles en el sistema operativo, y si se cometía una equivocación al introducir un comando DOS se tenía que repetir el procedimiento.

Sin embargo, la cosa se vuelve radicalmente distinta cuando estamos trabajando con ventanas tipo Windows. Ya se mencionó que una máquina con una interfaz o un sistema operativo tipo Windows instalado en ella necesariamente tiene que tener instalado un adaptador de video para llevar a cabo funciones de graficado. No es posible que desde un programa escrito en alguna versión de C para una máquina con Windows instalado en ella se pueda trazar una línea o un círculo en cualquier parte de la pantalla, ya que ello “arruinaría” la gran ventana principal que sirve como fondo de imagen para la interfaz visual tipo Windows que es conocida comúnmente como el Escritorio (Desktop). Siendo en su quintaesencia un sistema gráfico, Windows debe tener disponibles herramientas o mejor dicho funciones para el trazado de líneas o círculos o rectángulos o lo que sea. Sin embargo, para que no se “manche” el aspecto del Escritorio, todas las funciones de graficado deben poder actuar única y exclusivamente adentro de ventanas Windows creadas para tal efecto, trátese de ventanas en las cuales quedarán confinados procesadores de palabras como Word de Microsoft, o hojas de trabajo como Excel, o editores de imágenes fotográficas como Photoshop. Y crear una ventana Windows, así sea una ventana simple y llana, requiere no sólo especificar las dimensiones rectangulares de la ventana (en unidades apropiadas tales como elementos de imagen o “puntitos pixel”) sino la posición que tendrá la ventana al ser puesta en el Escritorio, ¿arriba o abajo?, ¿a la izquierda o a la derecha? Es necesario especificar también si la ventana quedará fija en una sola posición o si podrá ser arrastrada de un lugar a otro dentro del Escritorio con la ayuda del Mouse. Es necesario especificar también si la ventana puede ser redimensionada ya sea horizontalmente o verticalmente o en ambas dimensiones. Es necesario especificar también el color de fondo dentro de la imagen (por ejemplo, blanco, o gris, o cualquier otra cosa). Es necesario especificar también el título (caption) que se le dará a la ventana en la barra superior de la ventana. Y estamos hablando únicamente de lo más esencial. Si la ventana será el punto de arranque de alguna aplicación como el programa de matemáticas MathCAD, seguramente tendrá que poseer por lo menos una barra de opciones de menú.

¿Pero cómo se habrán de programar en C los programas elaborados para ser ejecutados bajo una interfaz gráfica Windows o un sistema operativo Windows? Recuérdese que, para un sistema de líneas de comandos, el programa fuente C tiene que tener como mínimo una función, la función principal main():

main() { ... }

Lo que va puesto entre los corchetes son las instrucciones del código a ser ejecutado, los pasos que se deben llevar a cabo en lenguaje de máquina aunque especificados en el programa fuente en lenguaje de alto nivel. Si echamos un vistazo preliminar a un programa C escrito para el sistema operativo Windows, encontramos que de inicio hay una variante en la función principal:

WinMain() { ... }

Pero esto es aún es insuficiente. Recuérdese que en un programa fuente en C escrito de manera formal se especifican tanto el tipo de dato regresado por la función C así como una lista de los argumentos de la función que contiene los argumentos que van a ser suministrados así como el tipo de dato de cada argumento suministrado:

void main(int argc, char *argv[], char *env[] argv)

Esta clase de especificación formal para un programa C escrito para un sistema operativo de líneas de comandos se convierte en un requerimiento absoluto cuando se está programando en lenguaje C para un entorno Windows (ya sea un Windows usado como mera interfaz visual tal como Windows 1.0 o un Windows usado como sistema operativo tal como Windows 95), y la función principal tiene que tener la siguiente forma:

   int PASCAL WinMain(HINSTANCE hInstance,
                      HINSTANCE hPrevInst,
                      LPSTR lpszCmdLine,
                      int nCmdShow)
                                    { ... }

El ascenso en complejidad es evidente. La función principal WinMain() de un programa que será ejecutado bajo control de una interfaz Windows o un sistema operativo Windows tiene cuatro argumentos:

   hInstance

   hPrevInst

   lpzCmdLine

   nCmdShow

Cada uno de estos argumentos suministrados a un programa ejecutable Windows, siendo datos, deben corresponder a cierto tipo de dato que determinará el número de bytes que serán asignados en la memoria RAM a cada argumento. El único tipo de dato que podemos reconocer a estas alturas es el que corresponde al argumento nCmdShow, que es un dato cuyo tipo es un entero int en base a lo que ya habíamos visto previamente en las entradas “El lenguaje C”. ¿Pero qué exactamente es un dato del tipo HINSTANCE? ¿Cuántos bytes requiere para su almacenamiento en la memoria RAM? Obsérvese que los primeros dos argumentos de la función WinMain() son ambos de tipo HINSTANCE. ¿Y qué es el tipo de dato LPSTR? ¿Cuántos bytes se requieren para su almacenamiento en el RAM?

Lo anterior únicamente suministra la plataforma fundamental para la función principal WinMain() desde la cual se definirá un programa que se ejecutará bajo Windows. Y a estas alturas, se sobreentiende que un programa Windows solo se puede lanzar y ejecutar desde una plataforma Windows que haya sido instalada previamente en una máquina, no se puede lanzar y ejecutar en una máquina que simplemente tenga un sistema operativo tipo DOS aunque la máquina tenga un adaptador de video instalado en ella. Al estarse llevando a cabo la transición del sistema operativo PC-DOS (MS-DOS) al sistema operativo Windows, además de poderse adquirir máquinas que ya tenían instalado en ellas el sistema operativo Windows, la empresa Microsoft puso a la venta paquetes de actualización (conocidos como upgrades) con los cuales se podía instalar Windows en máquinas que ya tuvieran instalado el sistema operativo MS-DOS, aunque para que la instalación pudiera llevarse a cabo la máquina tenía que cumplir ciertos requerimientos tales como tener cierta cantidad mínima de memoria RAM instalada, poseer como mínimo cierto tipo de procesador Intel (o compatible), poseer un disco duro (¡indispensable!), esto además de tener ya instalada una tarjeta de video capaz de generar gráficos (¡también indispensable!). Obviamente, los requerimientos del hardware fueron creciendo en demanda de mayor memoria RAM, mayor potencia del procesador CPU, mayor capacidad de almacenamiento en el disco duro, y mayor capacidad en la tarjeta de video instalada en la máquina, conforme Windows fue pasando de una versión a otra (de Windows 3.1 a Windows 95 a Windows 98 a Windows Millenium a Windows XP a Windows Vista a Windows 8 y así sucesivamente).

¿Pero qué de las funciones que serán invocadas desde la función principal WinMain()? ¿Cuáles son las funciones que serán usadas, definidas en lenguaje C? Interesantemente, lo que es en su quintaesencia el lenguaje C menos las funciones suministradas en bibliotecas para cada tipo de sistema operativo< sigue siendo igual. Eso no cambia, eso sigue igual. Lo que cambia son las funciones que serán invocadas desde WinMain().

Así como para poder efectuar operaciones tanto de entrada/salida así como de manipulación de hileras desde un programa ejecutable elaborado inicialmente en lenguaje C para un sistema operativo de líneas de comandos tipo UNIX/DOS recurrimos a una variedad de funciones de biblioteca tales como:


printf()    scanf()    gets()    strcpy()

del mismo modo existen funciones que pueden ser usadas desde un programa ejecutable elaborado inicialmente en lenguaje C para un sistema operativo Windows, funciones tales como:

wsprintf()    MessageBox()    CreateWindow()

¿Pero en dónde se encuentran estas funciones? ¿Las suministra cada fabricante de cada compilador C que se venda para programar en Windows? No. Las suministra el mismo sistema operativo Windows. Y no podía ser de otra manera. Si en un sistema operativo “primitivo” basado en líneas de comandos, además de la interacción estrecha con el hardware basado en los procesadores Intel (o los procesadores AMD capaces de poder procesar el mismo conjunto de instrucciones en lenguaje de máquina que los CPU de Intel) era necesaria cierta interacción con el sistema operativo, bajo Windows se hace evidente una dependencia casi total en el sistema operativo Windows para poder hacer cualquier cosa. Windows no solo dicta lo que se puede hacer sino cómo se debe de hacer.

Desde que hizo su aparición Windows, las funciones a ser utilizadas por la función principal WinMain() quedaron definidas dentro de un paquete conocido genéricamente como la Interfaz de Programación de Aplicaciones Windows o Windows API (Applications Programming Interface). Cuando Windows ha evolucionado de una versión a otra (por ejemplo, de Windows 95 a Windows 98), lo que ha ido en aumento es la cantidad de funciones disponibles en el Windows API, reflejando los aumentos en las capacidades del hardware. Así, si al principio en el Windows API no había funciones para la construcción de redes locales de computadora conectadas a través de puertos Ethernet, al hacer su aparición dicha opción de hardware en las computadoras el Windows API fue ampliado para darle entrada a funciones capaces de aprovechar la elevada capacidad de modems conectados a líneas DSL. Y al pasar Windows de cierta modalidad a otra, por ejemplo de Windows de 32 bits a Windows de 64 bits, las funciones continuaron siendo las mismas (usando los mismos nombres) excepto que fueron ampliadas y modificadas para poder hacer uso de los procesadores de 64 bits.

Habíamos visto previamente en esta obra que desde el entorno de Visual Basic, con su interfaz visual basada en la programación orientada a objetos, era posible llevar a cabo de modo relativamente rápido e intuitivo la programación de muchas aplicaciones sin necesidad de tener que saber el lenguaje C. ¿No es entonces preferible limitar nuestros conocimientos a Visual Basic? Desafortunadamente, Visual Basic es insuficiente para enfrentar la tarea de la programación del sistema cuando se entra en la programación detallada del hardware en donde no hay “objetos” programables visualmente por la simple razón de que a nivel detallado del hardware no se cuenta con elementos para trabajar puesto que Visual Basic fue desarrollado principalmente para cosas que tienen interacción directa con el usuario y no con cosas del hardware que el usuario nunca llega a ver en detalle y de las cuales el usuario no tiene por qué tomar conocimiento de ellas. Por otro lado, y esto es aún más importante:

Las funciones del Windows API están definidas usando el lenguaje C.

Antes de que hiciera su aparición la primera versión de Windows, Windows 1.0, lo que se usaba ampliamente para comunicarse con el hardware era el lenguaje C. Era lógico que para la elaboración de programas de aplicación para Windows se le diera la más alta prioridad al “lenguaje ensamblador de alto nivel” de mayor uso en el mundo de la programación. Posteriormente, llegó el entorno de programación Visual Basic, pero este entorno de programación dependía de un Windows API elaborado en lenguaje C. Eventualmente, para darle mayor flexibilidad a las versiones posteriores de Visual Basic, se proporcionaron medios para permitirle “comunicarse” con el Windows API. Pero este último estaba elaborado en lenguaje C, sin tratar de incorporar algo de la sintaxis del lenguaje BASIC (lo cual habría dado lugar a un lenguaje híbrido que podía ser mal recibido por los programadores de aplicaciones). Si en esta obra se introdujo al lector previamente al entorno Visual Basic antes de introducirlo al lenguaje C fue porque dicho entorno permite “visualizar” de modo intuitivo y pedagógico los conceptos más esenciales de la programación orientada a objetos. Pero una vez logrado este objetivo, el siguiente paso consistió en sentar los fundamentos del lenguaje C, el mismo lenguaje usado para definir el Windows API, el mismo lenguaje que permite llevar a cabo la programación de sistemas desde Windows y para Windows. Y esto es lo que estamos haciendo aquí mismo.

Antes de seguir hablando en generalidades que posiblemente aburran al lector, veremos una muestra de un programa Windows escrito en lenguaje C. El programa Windows esencialmente reporta, a través de una “Caja de Mensaje” (Message Box) la cantidad de memoria RAM que tiene instalada la máquina. Supondremos que el programa fuente C se encuentra archivado bajo el nombre MEMORIA.C:

// Programa MEMORIA.C
// Se pone una caja de mensaje en el escritorio
// El programa no usa la ventana principal

#define STRICT          // chequeo estricto de tipos
#include "windows.h"    // archivo include para todos los programas Windows

//////////////////////////////////////////////////////////////////
// WinMain() -- punto de entrada del programa                   //
//////////////////////////////////////////////////////////////////
#pragma argsused             // ignorar argumentos no usados

int PASCAL WinMain(HINSTANCE hInstance,  // que programa soy?
                   HINSTANCE hPrevInst,  // hay otro programa?
                   LPSTR lpszCmdLine,    // argumentos de comandos de linea
                   int nCmdShow)
   {
   DWORD dwMemDisp;       // memoria libre disponible (unsigned long)
   char szBuffer[80];     // buffer para hilera

   dwMemDisp = GetFreeSpace(0);  // obtener memoria libre

   // hacer una hilera
   wsprintf(szBuffer, "Memoria disponible: %lu", dwMemDisp);

   // poner una caja de mensaje
   MessageBox(NULL, szBuffer, "MEMORIA", MB_OK);
   return NULL;              // regresar a Windows 
   }  // finalizar WinMain

El entorno Borland C++ 4.0 fue usado en la serie de entradas “El lenguaje C” para llevar a cabo la compilación de programas escritos en C, con el sistema operativo MS-DOS como destinatario final del medio en el que se ejecutaban los programas ya convertidos a archivos ejecutables. Este tipo de entorno (al igual que otros entornos de programación) no solo podía compilar programas elaborados en C para el sistema operativo DOS. También podía compilar programas elaborados en C para Windows, programas como el que se acaba de dar arriba. ¿Significa esto que un mismo compilador es usado en dicho entorno para compilar programas C para DOS y programas C para Windows? Desde luego que no. Recuérdese que un entorno de programación no es más que una cómoda interfaz visual para un conjunto de herramientas de programación. Y el entorno del que estamos hablando podía invocar no uno sino dos compiladores distintos, un compilador para programas tipo DOS, y otro compilador para programas tipo Windows. Se puede, claro está, elaborar un solo compilador para ambos tipos de programas, pero a fin de cuentas se trataría de una ilusión, porque el “compilador maestro” consistiría en realidad de dos compiladores diferentes, y el compilador invocado dependería del tipo de programa (DOS o Windows) a ser compilado.

Usando el entorno Borland C++ 4.0 para compilar el programa Windows que se acaba de dar arriba, compilando el programa hacia un archivo ejecutable Windows 3.1, y ejecutando dicho programa, obtenemos algo como lo siguiente:


Esto se parece harto a algo que vimos en la serie de entradas “El entorno Visual Basic”, específicamente, en “El entorno Visual Basic VIII”. De hecho, es lo mismo, excepto que allí lo logramos usando Visual Basic, y aquí lo estamos logrando usando el lenguaje C. ¿Entonces no hay diferencia entre el usar Visual Basic y el usar el lenguaje C para programar en Windows? Sí la hay, pero se trata de una diferencia sutil e importante, por el simple hecho de que la programación Windows en C es algo más fundamental y básico que la programación Windows en un entorno Visual Basic que es una mera interfaz visual grande que facilita la construcción de intefaces de programas ejecutables. Cuando la programación Windows se hizo realidad por vez primera, esto se logró usando C y no Visual Basic. El lenguaje C precedió a Visual Basic, y Visual Basic en realidad no es más que una máscara para ocultar algunos detalles engorrosos de algo que está elaborado en C. En efecto, Visual Basic es simplemente una concha (shell), y un buen programador puede prescindir por completo de Visual Basic para lograrlo todo en C, aunque un programador listo aprovechará ambas opciones para obtener el programa más eficiente posible en el menor tiempo posible. Esto es tan evidente, que llegó el punto en el que tanto Visual Basic como Visual C (la versión de Microsoft para programar en C para Windows) terminaron siendo combinados en un solo paquete conocido como Visual Studio, la cual fue evolucionada tiempo después a la metodología Visual Basic .NET que incorpora las capacidades requeridas para poder elaborar programas complejos para la Web.

Veamos más de cerca el programa Windows que se acaba de dar.

Por principio de cuentas, tenemos el uso intensivo de una doble diagonal (//) en el código fuente. Todo lo que vaya puesto en una línea de texto hacia la derecha de la doble diagonal es tomado como un comentario y es ignorado por el compilador C para Windows.

El lector recordará que, para lograr obtener interacciones con la máquina mediante funciones de entrada de texto (a través del teclado) y salida de texto (a través de la pantalla), se incluía el archivo de cabecera STDIO.H. con la línea:

        #include <stdio.h>

Pero esto aplicaba a programas elaborados para ser usados en ventanas DOS de líneas de comandos. Para programas Windows, esto resulta inútil, y el archivo de cabecera que tiene que ser invocado es WINDOWS.H, con la línea:

         #include <windows.h>

Uno de los obstáculos que enfrenta el programador en Windows es que, aunque sea escrito en el lenguaje C, un programa Windows no se parece mucho a un programa C tradicional. Quizá la mayor diferencia en la apariencia es que, encima de los tipos de datos tradicionales como int y char, encontramos nombres de tipos poco familiares escritos en mayúsculas. En el programa Windows que se ha dado arriba, hay tres tipos de datos derivados: HINSTANCE, LPSTR y DWORD. Otros programas Windows usarán otros tipos de datos tales como UINT, WORD, HWND, HGLOBAL, y así sucesivamente. Sin embargo, los nuevos nombres de tipos son equivalentes a, o mejor dicho derivados de (lo cual justifica su designación como tales) tipos ya existentes. Veamos cómo define Windows tales tipos, y por qué queremos usarlos.

Un tipo derivado es simplemente un tipo de dato C normal al cual se le ha dado otro nombre. Podemos encontrar los significados “verdaderos” de los tipos derivados de datos buscándolos en el archivo de cabecera WINDOWS.H. En dicho archivo de cabecera encontramos enunciados C como los siguientes:

   typedef unsigned long DWORD;

   typedef char FAR* LPSTR;

De este modo, escribir el tipo de dato DWORD es lo mismo que escribir:

      unsigned long

y escribir el tipo de dato LPSTR es lo mismo que escribir:

      char FAR*

Tales declaraciones typedef crean nuevos nombres para tipos de datos ya existentes. De este modo DWORD es hecho equivalente a unsigned long; y LPSTR que simboliza un Puntero Long a una hilera (STRing) que es hecho equivalente a char FAR* en donde FAR a su vez es hecho equivalente a _far (esta es una convención de terminología que empezó cuando al aparecer las primeras computadoras basadas en la arquitectura IBM, el lenguaje C fue adaptado para manejar modelos de memoria RAM repartidos en “porciones” o segmentos de 64 kilobytes, usándose punteros cercanos near para especificar domicilios RAM dentro del alcance de cierto segmento, y usándose punteros lejanos far) para especificar domicilios RAM correspondientes a segmentos distintos), y en este caso _far designa un puntero de 32 bits en lugar de un puntero de 16 bits.

Encontrar la definición del tipo de dato HINSTANCE en el archivo de cabecera WINDOWS.H no es tan fácil, pero juntando toda la información pertinente se encuentra que HINSTANCE corresponde al tipo:

const void _near*

que viene siendo un puntero cercano constante hacia un nulo (void).

Al efectuar la declaración:

DWORD  dwMemDisp;

que es el primer enunciado que aparece dentro de WinMain(), en donde DWORD es el tipo de dato, y dwMemDisp es la variable que se está definiendo, resalta el hecho de que en la designación de la variable se usa la notación húngara de Charles Simonyi, en donde el prefijo dw de la variable indica que se trata de una variable de tipo DWORD.

Una cosa sorprendente acerca del uso de los tipos de datos derivados es que, en la mayoría de los casos, podemos usar los tipos de datos derivados sin que nos importe cuál es el “verdadero” significado que hay detrás. Esto se va volviendo más claro conforme se va adquiriendo proficiencia en la programación en Windows.

Otro tipo de variable que hemos encontrado arriba es la variable HINSTANCE. Esta variable es una especie de agarradera (handle), una agarradera a una instancia (aquí Microsoft empezó a deslizar algo de la terminología que se usa en la programación orientada a objetos). Una agarradera es simplemente un número entero arbitrario asignado por el sistema a una entidad particular. Sirve para identificar la entidad, trátese de una instancia, una ventana, un control, o un bloque de memoria. En las primeras versiones de Windows, había una docena de tipos de datos relacionados para describir diferentes tipos de agarraderas. La designación de todas las agarraderas comienza con la letra H. De este modo, HWND es el tipo usado para una agarradera hacia una ventana (Handle to a Window), HGLOBAL es el tipo usado para una agarradera de memoria global, y así sucesivamente.

Una razón por la cual en la programación Windows usamos tipos derivados en lugar de los tipos básicos tales como int y char es porque los tipos derivados suministran más información a alguien que esté leyendo un listado de un programa Windows escrito en C. Es más fácil reconocer HINSTANCE como una agarradera hacia una instancia, mientras que su equivalente const void _near* no proporciona ninguna información acerca del propósito del tipo de dato. De este modo, los tipos derivados hacen que los listados de los programas Windows sean más fáciles de leer. Por otro lado, son más breves, reduciendo el amontonamiento de simbología que hace la lectura más confusa. Igualmente importante es que el compilador podrá checar de manera más rigurosa el código fuente en C cuando se usan distintos nombres de tipos para distintas clases de variables. Le pueden decir a un programador si está usando la clase incorrecta de agarradera como un argumento a una función aunque ambas agarraderas tengan el mismo tipo. Finalmente, al ir evolucionando Windows, Microsoft se reservó el derecho de ir cambiando los tipos usados para ciertas entidades. A modo de ejemplo, las agarraderas de instancias solían ser del tipo unsigned int, hasta que fueron convertidas en punteros a nulos, un cambio llevado a cabo para poder hacer que los programas Windows pudieran ser portátiles hacia procesadores CPU diferentes. Si se hubieran usado los tipos básicos al escribir un programa Windows, el programador se vería obligado a tener que revisar todas las variables de agarraderas cada ocasión que Microsoft revisara Windows. Usando los tipos derivados, el programador se evita el penoso trabajo de tener que estar cambiando el código fuente.

Por si las razones dadas arriba no son suficientes para convencernos de la utilidad de los tipos derivados, se agregará que mucha de la documentación que se ha desarrollado en torno a la programación Windows ha sido elaborada usando la notación que se está introduciendo aquí, y es mejor irse acostumbrando a ella desde un principio para poder entender la lógica de los programas Windows existentes, sobre todo los que fueron elaborados partiendo de Windows 3.1 que es la versión de Windows a partir de la cual Windows ya había madurado al Windows API con que nos estamos familiarizando (se recalca que Windows 3.1 era una mera interfaz visual y no un sistema operativo; sin embargo Windows había madurado ya a tal grado que ya no hubo una interfaz visual Windows 4.0, el salto fue dado directamente a Windows 95, el primer sistema operativo Windows de 32 bits).

A continuación se reproduce una tabla de los tipos derivados que ya estaban siendo usados en Windows a partir de Windows 3.1:


Tipos derivados en Windows
Notación Significado
 FALSE  0
 TRUE  1
 BOOL  int (0 ó no-cero)
 BYTE  unsigned char
 WORD  unsigned short
 UINT  unsigned int
 DWORD  unsigned long
 FAR  _far
 NEAR  _near
 LONG  long
 VOID  void
 PASCAL  _pascal
 NULL  0 ó 0L (dependiendo del
 modelo de memoria)
WPARAM  UINT
 LPARAM  LONG
 LRESULT  LONG
 FARPROC  int (FAR PASCAL *)()
 puntero far a función
 DLGPROC  FARPROC
 WINAPI  _far _pascal
 CALLBACK  _far _pascal
 HANDLE  const void NEAR *
 (para STRICT definido,
 en caso contrario UINT)
 HWND  HANDLE
 agarradera de ventana
 (window handle)
 HINSTANCE  HANDLE
 agarradera de instancia
 (instance handle)
 HDC  HANDLE
 agarradera de contexto
 de dispositivo (device
 context handle)
 HMENU  HANDLE
 agarradera de menu
 (menu handle)
 HLOCAL  HANDLE
 agarradera a memoria local
 (local memory handle)
 HGLOBAL  HANDLE
 agarradera a memoria global 
 (global memory handle)
 PSTR  char _near *
 puntero near a una hilera
 LPSTR  char _far *
 puntero far a una hilera
 LPCSTR  const char _far *
 puntero far a una hilera de
 lectura únicamente
 Pxxx  xxx _near *
 Puntero (PWORD es
 WORD NEAR *)
 LPxxx  xxx _far *
 Puntero far (LPWORD es
 WORD FAR *)


Muchos de los programadores expertos en el lenguaje C llano y convencional que nunca antes habían estado familiarizados con la programación Windows encontraron desconcertante el uso de los tipos derivados. Puede que el lector se esté diciendo a sí mismo: si me cuesta trabajo reconocer inclusive los tipos básicos usados en el lenguaje C, ¿qué tan lejos podré llegar con la programación Windows con sus tipos derivados? Pero esto no resulta ser problema alguno. Una vez que se han estado leyendo algunos listados de programas Windows, el programador empieza a aceptar los tipos derivados como tales sin preocuparse por lo que pueda haber detrás de ellos. Es posible que al principio se pueda sentir alguna frustración al no saberse de inmediato lo que representan los tipos derivados, pero usualmente no es necesario llegar tan a fondo. Si una función regresa un argumento de tipo DWORD, y queremos asignar este valor a una variable, simplemente la declaramos como una variable de tipo DWORD. No necesitamos saber que DWORD está definido como un <b>unsigned long</b>, inclusive podemos meternos en problemas si usamos tal conocimiento en nuestros programas, porque como ya se dijo al ir evolucionando Windows a versiones posteriores la empresa Microsoft puede efectuar cambios en los tipos subyacentes. En pocas palabras, resulta mejor ignorar olvidar el hecho de los tipos derivados han sido derivados de algo más básico, y usarlos simplemente como tipos nuevos.

Además de tener que familiarizarnos con los tipos derivados empleados en la programación Windows que se han mencionado arriba, hay que tomar conocimiento del sistema notacional basado en la notación húngara inventada por Charles Simonyi. Ya se había dado un anticipo de esto en la entrada titulada “La programación visual”, y fue precisamente para la programación Windows que se desarrolló este tipo de notación. La idea básica en Windows es asignarle a las variables un prefijo usando <i>letras minúsculas que describan el propósito de la variable o el tipo de dato que define a la variable. De este modo, el prefijo h representa una agarradera (handle), el prefijo dw representa una variable double word, lp representa un puntero long (far) y sz representa una hilera de caracteres terminada en cero (una hilera ordinaria en C).

En el programa MEMORIA.C que se dió arriba, tenemos las siguientes variables en cuyos nombres se emplea la notación húngara:

hInstance,    hPrevInst,   lpszCmdLine,   nCmdShow,   dwMemDisp,   szBuffer

El nombre de cada variable a la cual se le ha prefijado notación húngara es también importante, y debe decirnos algo acerca del propósito de la variable, recomendándose que el nombre de la variable empiece con una letra mayúscula para así poder distinguir el nombre de la variable del prefijo en notación húngara que se le ha anexado. De este modo, agregando el prefijo “h” a la variable “Instance” nos produce la variable “hInstance”, que otro programador podrá leer de inmediato como “una agarradera a una instancia”. Del mismo modo, agregar el prefijo “dw” a la variable “MemDisp” que a su vez es una abreviatura que representa la “Memoria Disponible” (RAM) en el sistema produce la variable dwMemDisp.

La notación húngara simplifica la detección de errores. A modo de ejemplo, si en la lectura de un programa Windows encontramos una variable llamada dwGaussian usada como un argumento a una función que requiere una variable del tipo HINSTANCE, podemos darnos cuenta de inmediato del error que se ha cometido con la sola lectura de la variable. Sin el prefijo de notación húngara y en un programa Windows que consta de miles de líneas de código, aún con la ayuda de un buen compilador Windows resulta una tarea endiabladamente difícil llevar a cabo la detección de errores de este tipo. Al igual que la programación estructurada que eliminó casi por completo el uso de los saltos incondicionales del tipo GOTO, el empleo de la notación húngara se ha convertido casi en una necesidad al tener que manejar proyectos de complejidad elevada capaces de ponerle los pelos de punta hasta un hacker virtuoso.

El uso de la notación húngara no es obligatorio, es optativo, al compilador de programas Windows no le importa si es usada o no. Se trata simplemente de una ayuda mnemónica para ayudar al programador. Pero es una convención que Microsoft ha usado mucho en la documentación técnica de Windows, y se facilita mucho la lectura de dicha documentación cuando uno está acostumbrado a ella. A continuación se reproducen las convenciones que empezaron a ser utilizadas al ser introducido Windows, de las cuales derivan las convenciones actuales con sus variaciones de estilo introducidas por ejércitos de programadores:


Notación húngara común a partir de Windows 3.1
Prefijo
 húngaro 
 Significado  Ejemplos
ch  caracter  chTeclado
w  unsigned int (WORD)  wParam, wBanderas
dw  unsigned long (DWORD)   dwConteo, dwAlloc
n  número (int)  nConteo, nCmdMostrar
i  entero (int)  iValor, iRetorno
l  entero long  lParam
a  array  aBuffer[]
ach  array de caracteres  achBuffer[]
b  bytes (int)  bArticulos
b  boolean (BOOL)  bBandera, bHabilitar
p  puntero  pHilera
np  puntero cercano (near)  npHilera
lp  puntero lejano long (far)  lpCmdLine, lpHilera
lpfn  puntero long a  función  lpfnProcDlg
i  índice (hacia un array)  ichNombres (hacia array
 de chars)
h  agarradera (handle)  hInstance, hDC, hIcon
c  conteo  cManzanas
cb  conteo de bytes  cbHilera
 sz  hilera terminada en nulo  szNombreArchivo
 x  coordenada-x  xPosicion
 y  coordenada-y  yPosicion


La lista dada por la tabla no es exhaustiva, y ciertamente ya había crecido al llegar Windows a su versión Windows XP.

Continuando con la lectura del programa MEMORIA.C, podemos leer que la primera línea con la que el programa Windows empieza es:

#define STRICT

usando la directiva #define para el preprocesador C. Esta línea de definición puede parecer sorprendente porque no parece que se está definiendo absolutamente nada, como en otras líneas de definición tales como:

   #define  TMARGIN   3

   #define  LMARGIN  10

   #define  CYCHAR   18

¿Entonces cuál es el propósito de meter a STRICT como definición de algo que no parece ser nada? Como ya hemos empezado a darnos cuenta, los programas Windows pueden ser más difíciles de ser depurados de errores que los programas tradicionales tipo MS-DOS elaborados para ser usados desde una ventana de líneas de comandos, y por esta razón se vuelve importante atrapar la mayor cantidad posible de errores durante el tiempo de compilación, en vez de esperar a que los errores se vuelvan evidentes en tiempo de ejecución del programa ya compilado. A partir de Windows 3.1, que podemos considerar como la versión ya madura de la programación Windows, Windows le permite al programador definir una variable llamada STRICT (estricto) que obliga al compilador a llevar a cabo un chequeo más estricto de los tipos de las variables. Cuando se ha definido STRICT, los argumentos suministrados a todas las funciones de Windows tienen que ser del tipo derivado correcto. No se puede usar una variable del tipo HWND (agarradera a una ventana, handle to a window) cuando el programa espera que se utilice el tipo HINSTANCE (agarradera a una instancia, handle to an instance). Aunque ambos tipos derivados tienen el mismo substrato (y por lo tanto el código podría ser compilado sin problema alguno), STRICT le permite al compilador advertirle al programador que se ha cometido un error. Lo más importante a tener en cuenta es que la directiva #define STRICT tiene que ser puesta antes de la directiva #include usada para incluír el archivo de cabecera WINDOWS.H. No es mandatorio usar STRICT, pero la programación Windows recomienda ampliamente su uso. Y recuérdese que STRICT es una directiva para ser usada por el preprocesador, no por el compilador.

En lo que toca a la instrucción en el programa Windows que sigue a la especificación de la variable STRICT:

#include WINDOWS.H

hay que enfatizar que el archivo de cabecera WINDOWS.H es una de las llaves a la programación Windows, ya que contiene una gran cantidad de definiciones que son requeridas por todos los programas Windows, incluyendo desde luego los prototipos (declaraciones) para cientos de definiciones propias de Windows, así como definiciones de estructuras, constantes, y tipos de datos derivados. Todos los programas Windows tienen que #incluír el archivo de cabecera WINDOWS.H. Resulta sumamente instructivo acostumbrarse a consultar el archivo de cabecera WINDOWS.H como una referencia rápida en caso de que se quiera aclarar la definición de alguna función Windows.

La tercera instrucción que podemos leer en el programa MEMORIA.C es la siguiente:

#pragma argsused

Como puede verse en el programa MEMORIA.C, ninguno de los argumentos especificados en la lista de argumentos de la función principal WinMain(), hInstance, hPrevInst, lpszCmdLine y nCmdShow, son usados dentro de la función WinMain(), lo cual no trae consecuencias mayores. Sin embargo, la mayoría de los compiladores verá esto como algo sospechoso, sobre todo al usarse la definición de STRICT, y al llevarse a cabo la compilación se generarán mensajes de advertencia tales como “Parameter hPrevInst is never used”. Estos mensajes pueden ser molestos cuando el programador está tratando de crear en cada compilación que se lleve a cabo algo que no genere mensajes de advertencia. La directiva #pragma se encarga de esto, indicándole al compilador que no es necesario preocuparse por argumentos de función que no sean utilizados. Es importante aclarar que la directiva #pragma no es parte del lenguaje C, se trata de algo que trabaja en algunos compiladores específicos. Igualmente importante es señalar el hecho de que esta directiva aplica únicamente a una sola función. Es necesario insertarla antes de la función con la finalidad de inhabilitar los mensajes de error “unused argument” para dicha función, pero no para funciones subsecuentes.

Sin importar cuántas funciones aparezcan dentro de un programa Windows, todo programa Windows empieza con la función WinMain(). Y como puede verse en el programa MEMORIA.C, se trata de la única función definida dentro del programa. Esta función desempeña el mismo propósito en los programas Windows que la función main() en los programas C tradicionales que corren bajo los sistemas operativos de líneas de comandos. Windows le pasa el control de la máquina a WinMain() cuando una aplicación empieza a ser ejecutada por el sistema operativo. Cuando la ejecución de la función termina, el control de la máquina le es regresado a Windows. La siguiente ilustración muestra la manera en la cual funciona el proceso:




Esto es lo mismo que sucede en los programas que corren bajo los sistemas operativos como PC-DOS basados en una interfaz de líneas de comandos antes del advenimiento de Windows:




Las transferencias de control que ocurren en ambos casos son similares (el control de la máquina es pasado del sistema operativo al programa, y terminada la ejecución del programa el control de la máquina le es devuelto al sistema operativo), lo único que cambia es el entorno utilizado. Puesto que tanto los programas que corren bajo un sistema operativo como el PC-DOS (o el MS-DOS) como los programas que corren bajo Windows tienen como extensión de archivo al sufijo “.EXE” (por ejemplo, WORDSTAR.EXE), podemos preguntarnos: ¿qué sucede si tratamos de correr un programa Windows en una máquina que aún no tiene instalado a Windows? En tal caso, el programa Windows simple y sencillamente no se ejecutará bajo un sistema operativo DOS. Todos los programas de aplicación tienen al principio un identificador que permite efectuar una verificación de la compatibilidad que hay entre un programa de aplicación (y esto incluye hasta la versión del sistema operativo usado) y el sistema operativo instalado en la máquina. Si son incompatibles, el programa de aplicación ni siquiera empezará a ejecutarse, y el usuario recibirá una notificación acerca de la incompatibilidad que impide que el programa sea ejecutado (se checan desde luego otras cosas, tales como el que haya suficiente memoria RAM en el sistema para la ejecución de la aplicación, que el procesador CPU sea de una versión capaz de ejecutar todas las instrucciones en lenguaje de máquina especificadas por el programa, etcétera).

Todo programa Windows tiene que tener una función <b>WinMain(), aunque también puede tener otras funciones siguiendo la filosofía de elaboración de programas escritos usando el lenguaje C basada a su vez en el concepto de las funciones. En los listados de programas Windows elaborados en C, la primera línea de una función en la cual se especifican el tipo del retorno de la función (el tipo del dato regresado por la función) así como los argumentos especificados entre paréntesis en la lista de argumentos es conocida como el declarador. En el caso de la función WinMain(), su declarador es:




Los primeros dos argumentos proporcionados a la función WinMain(), hInstance y hPrevInstance, tienen que ver con instancias. Esta designación fue tomada directamente de la terminología usada en la programación orientada a objetos. Recuérdese de la serie de entradas “El entorno Visual Basic” que cuando a partir de un “armazón” básico o “esqueleto” se crea un objeto, se está creando una instancia de dicha armazón. Si usamos la misma armazón para crear un segundo objeto de la misma clase, decimos que se crea una segunda instancia. Y si usamos la misma armazón para crear un tercer objeto de la misma clase, decimos que se crea una tercera instancia. Y así sucesivamente. El Bloc de Notas de Windows es un ejemplo de ello. Podemos abrir un Bloc de Notas, y tras esto podemos abrir un segundo Bloc de Notas, y tras esto podemos abrir un tercer Bloc de Notas. Cada Bloc de Notas puede ser usado para crear un documento diferente que será archivado con un nombre de archivo diferente. Puesto que Windows es un sistema operativo multitareas, bajo dicho sistema operativo se pueden estar ejecutando varios programas distintos al mismo tiempo (en realidad, en una computadora basada en un solo procesador CPU solo se puede ejecutar un programa a la vez, pero el control va siendo pasado rápidamente de un programa a otro programa con tal rapidez que se crea la ilusión de que se están ejecutando varios programas al mismo tiempo). Y así, varias instancias del mismo programa pueden estarse ejecutando a la vez. En lo que concierne al usuario, cada instancia es un programa independiente, aunque la armazón básica sea la misma. Tanto internamente como para el programador Windows, las instancias no son independientes. ¡Todas ellas comparten el mismo código! En pocas palabras, las instrucciones ejecutables que definen al programa solo son cargadas en la memoria RAM una sola ocasión, lo cual ahorra considerablemente el espacio de memoria RAM usada. Sin embargo, cada instancia posee sus propios datos, de modo tal que puede operar en forma independiente de las otras instancias. Además, Windows impone una carga considerable de código extra sobre cada instancia, de modo tal que la cantidad de memoria RAM usada por cada instancia termina inflándose. La siguiente figura nos muestra cómo maneja Windows instancias múltiples del mismo programa (solo puede haber una instancia activa a la vez, la figura muestra a la instancia 2 usando el código que es común a las tres instancias):




A continuación tenemos una captura de imagen de una computadora en la cual se tienen tres instancias del programa MEMORIA.EXE (el archivo fuente MEMORIA.C ya compilado a un programa ejecutable) corriendo en una máquina con el sistema operativo Windows XP instalado en ella (obsérvese que cada instancia del programa Windows ejecutable reporta una cantidad de memoria ligeramente diferente; esto se debe a que cada instancia ocupa algo del espacio de memoria RAM que ya no estará disponible en la máquina cuando se ejecute la siguiente instancia):




Veamos ahora en mayor detalle los argumentos proporcionados a la función WinMain(), empezando por el primer argumento, hInstance. Supóngase que hay varias instancias de un mismo programa ejecutándose en la máquina, y que cierta aplicación necesita saber qué instancia es, algo comparable a preguntarse a uno mismo “¿cuál es mi nombre?”. Esto se puede averiguar checando el argumento hInstance de la función WinMain(), ya que su valor es un número único y distinto para cada instancia. En lo que toca al argumento hPrevInst, una aplicación también puede decir si fue la primera instancia puesta en marcha por el usuario. Esto se sabe cuando el argumento hPrevInst sea NULL (nulo).

El tercer argumento, lpszCmdLine, puede parecer desconcertante. Después de todo, la interfaz visual de Windows tiene como principal propósito prescindir de las líneas de comandos, permitiendo llevar a cabo todas las operaciones de modo visual. ¿Entonces cómo entran en el panorama los comandos de línea? Esto se originó durante la etapa de transición en la cual se estaba llevando a cambio la actualización de la interfaz tipo DOS de líneas de comandos a la interfaz gráfica tipo Windows. Muchos usuarios querían seguir teniendo la opción de poder hacer las cosas al modo del cada vez más obsoleto DOS en lugar de tener que aprender a hacer las cosas visualmente al modo de Windows. En Windows 3.1, esto se lograba usando el Administrador de Programas (Program Manager) para poner en marcha un programa, dándole entrada a una o varias palabras después del nombre del programa. Y se podía seguir trabajando en una ventana DOS emulada bajo Windows. Los especificadores dados al poner en marcha el programa Windows eran proporcionados tal y como se acostumbraba hacer bajo DOS:

C:\WINDOWS\archivos\progswindows\aplicacion  uno  dos

En esta línea, “uno” y “dos” son los argumentos de una línea de comandos. La hilera completa de caracteres es pasada a la aplicación que se estará ejecutando. Y de hecho, el argumento lpszCmdLine es un puntero hacia una hilera de caracteres conteniendo el texto proporcionado por el usuario al invocar el programa. Los argumentos de línea de comandos pueden ser nombres de archivos a ser abiertos por el programa de aplicación, o cualesquier otra información, y el “modus operandi” es semejante a la manera en la cual se usan los argumentos de líneas de comandos en un entorno DOS. De cualquier modo, los argumentos de líneas de comandos pronto dejaron de ser populares en Windows y otros entornos con interfaces gráficas en virtud de que los programas bajo Windows son lanzados simplemente haciendo un doble clic con el Mouse sobre un ícono en lugar de escribir nombres de archivos.

Por último, el cuarto argumento, nCmdShow, dice si una aplicación Windows ha sido creada como una ventana de tamaño completo (maximizada) o creada como un ícono (minimizada). Sin embargo, el programa MEMORIA.C ni siquiera tiene una ventana principal, ya que usa una Caja de Mensaje que en realidad no es una ventana Windows sino una simple caja de mensaje.

En C, a toda función se le puede asignar un valor de retorno. Como puede verse en el declarador para la función principal WinMain(), el tipo de dato del valor de retorno es int. Por otro lado, la palabra clave PASCAL le indica al compilador que genere el código ejecutable usando la convención de invocación Pascal. Esta convención tiene que ver con la manera en la que los argumentos de una función le son pasados a la función por el programa invocador. Los argumentos le son pasados a una función almacenándolos primero en la región especial de memoria conocida como la pila (stack) antes de que el control de la máquina sea transferido a la función. Cuando se usa la convención de invocación C en lugar de la convención de invocación Pascal, el último argumento es el primero en ser puesto en la pila, lo cual hace posible procesar funciones que pueden tener una cantidad indefinida de argumentos (como printf() en los programas C tradicionales). Con la convención de invocación Pascal, el primer argumento es el que entra a la pila primero. Esto hace que haya una mayor eficiencia en la ejecución de los programas Windows. Desde un principio, Windows empezó a utilizar la convención de invocación Pascal precisamente por la mayor eficiencia que se podía lograr.

Para un programa breve como el programa MEMORIA.C que hemos visto arriba, el valor de retorno dado por WinMain() debe ser NULL (nulo). Pero hay muchos otros programas Windows más complejos que ciertamente utilizan valores que no son nulos.

Antes de que Windows terminara de madurar bajo Windows 3.1, los programas escritos para correr bajo versiones más viejas de Windows usaban tipos derivados para algunos argumentos de WinMain() así como los de otras funciones. Esto lo podemos comprobar consultando programas elaborados para Windows 3.0, en los cuales el declarador de la función principal WinMain() tenía el siguiente prototipo:

int PASCAL WinMain(HANDLE, HANDLE, LPSTR, int);

Como puede verse, los dos primeros argumentos eran especificados como argumentos del tipo (derivado) HANDLE. A partir de Windows 3.1, el uso de HINSTANCE le permitió a los compiladores llevar a cabo un mejor chequeo de tipos, siempre y cuando se definiera el chequeo estricto de tipos STRICT en la manera en que se ha hecho arriba.

A continuación, veremos el interior de lo que se hay dentro de la función principal WinMain() en el programa MEMORIA.C, en donde se invocan tres funciones: GetFreeSpace(), wsprintf() y MessageBox(). Estas tres funciones forman parte de la gran colección de funciones intrínsecas a Windows conocida como el Windows API. A partir de Windows 3.1, ya había cientos de funciones API usadas para crear y manipular los elementos de la interfaz gráfica del usuario, y con cada versión posterior de Windows se fueron agregando cientos de funciones adicionales para poder hacer posible incorporar el manejo de las capacidades crecientes del hardware (tales como la interconexión inalámbrica Wi-Fi, los dispositivos portátiles de memoria USB 2.0 y 3.0, la intercomunicación a través de modems DSL, etc.) Entre las funciones del Windows API encontramos funciones para la creación de cajas de diálogo, ventanas y menús. También hay funciones usadas para actividades de entrada/salida tales como salida de texto y gráficas, así como entrada del teclado y del Mouse; además de estas funciones hay otras funciones para manejar actividades tales como el manejo de archivos y administración de la memoria. La siguiente figura ilustra la relación que hay entre las aplicaciones Windows y el Windows API:




Al llevarse a cabo la transición del DOS a Windows, aún se podían invocar y utilizar funciones tradicionales de entrada/salida como printf() y scanf(), aunque estas funciones son irrelevantes en el entorno Windows.

Como ya se mencionó previamente, los prototipos (declaraciones) de las funciones del Windows API se encuentran en el archivo de cabecera WINDOWS.H que podemos inspeccionar con cualquier editor de texto (como el Bloc de Notas) o con un procesador de palabras (como Microsoft Word). Sin embargo, el código ejecutable (en lenguaje binario de máquina) para cada una de las funciones se encuentra en alguna de varias bibliotecas de enlazado dinámico (DDL) que forman parte de Windows. No debe preocuparnos la manera en la cual se lleva a cabo el enlazado dinámico de las funciones del Windows API a un programa Windows que hayamos elaborado, ya que esto es manejado automáticamente tanto por el programa de aplicación ya compilado como por Windows.

La pieza central del programa MEMORIA.C es la función de Windows API GetFreeSpace() que nos regresa el número de bytes de memoria global disponible y el cual asignamos a la variable dwMemDisp con la instrucción:

dwMemDisp = GetFreeSpace(0);  // obtener memoria libre

La función GetFreeSpace() toma un solo argumento de tipo UINT (que es unsigned int). Este argumento es ignorado en Windows 3.1 y por lo tanto simplemente lo fijamos a cero. Como puede intuírse con la ayuda de la notación húngara, la función nos regresa un valor de tipo DWORD (unsigned long) que representa el número de bytes de la memoria global.

En lo que toca a la función wsprintf() del Windows API usada en la instrucción:

wsprintf(szBuffer, "Memoria disponible: %lu", dwMemDisp);

esta función es reminiscente de la función DOS printf(), posiblemente la función C de salida más utilizada en los programas C elaborados para los sistemas operativos de líneas de comandos, la cual con la ayuda de códigos de formato acomoda caracteres de texto y valores numéricos que serán colocados en una hilera de texto. En el lenguaje C ordinario pre-Windows, además de la función de salida printf(), hay otra función un poco menos conocida llamada sprintf() que usa las mismas convenciones para generar hileras de salida, excepto que en vez de poner la hilera de texto en la pantalla de un monitor la coloca dentro de una memoria buffer para su uso posterior. La función wsprintf() de Windows es el equivalente de la función sprintf(), con la única diferencia (en las primeras versiones de Windows) de que utiliza punteros far lejanos (de 32 bits) en lugar de punteros near cercanos (de 16 bits). En el programa MEMORIA.C usamos la función wsprintf() para combinar la hilera “Memoria disponible:” con el valor numérico que nos regresa la función GetFreeSpace(), y el resultado es puesto dentro del buffer szBuffer (el código de formato %lu se usa para variables que son del tipo long y unsigned). El contenido del buffer es usado posteriormente como argumento para la función MessageBox() que nos crea la Caja de Mensaje.

En la documentación de la función wsprintf() encontramos algunas peculiaridades, tales como el hecho de que no podemos usar en ella especificadores de formato de punto flotante tales como %f, %g y así sucesivamente; dicha función no sabe nada acerca de lo que es el punto flotante. Sin embargo, se pueden seguir usando los códigos de formato %c para caracteres, %s para hileras de texto, y %d para enteros con signo. Pero hay otra peculiaridad que impone la necesidad de que si se utiliza una hilera de texto como variable para ser usada como argumento entonces a la variable se le tiene que aplicar una operación de “cast” para hacerla una variable del tipo LPSTR (puntero lejano a una hilera). Si no se hace el cast, la función será compilada correctamente aunque la hilera no será mostrada en lo absoluto. Un ejemplo de este tipo de operación de cast es el siguiente:

wsprintf(szBuffer, "El nombre del archivo es %s", (LPSTR)szNombreArchivo);

Por último, se tiene la función MessageBox() del Windows API, la función más orientada a windows en el programa MEMORIA.C. Es una función muy útil disponible en todas las versiones de Windows, la cual se encarga de crear y poner en la pantalla una Caja de Mensaje, la cual es simplemente una ventana simple de tamaño fijo que imprime algo de texto y tiene uno o más botones para cerrar la caja. La instrucción en el programa MEMORIA.C tiene la línea:

MessageBox(NULL, szBuffer, "MEMORIA", MB_OK);

El primer argumento de la función es la agarradera (handle) de una ventana que es la progenitora de la Caja de Mensaje. En este caso específico, la Caja de Mensaje no tiene progenitor alguno, de modo tal que especificamos el primer argumento como NULL (nulo). El segundo argumento es el domicilio en la memoria RAM que contiene el texto que será puesto dentro de la caja. El tercer argumento es el texto que aparecerá como el título de la Caja de Mensaje en la parte superior de la ventanita, el cual sugiere en la mayoría de los casos el nombre de la aplicación o la intención de la aplicación. La Caja de Mensaje es redimensionada automáticamente en torno al texto de la caja y el título de la caja. El cuarto argumento nos permite especificar el tipo de botón (o botones) que será(n) puesto(s) en la caja, lo cual podemos fijar recurriendo a una de las varias constantes que aparecen definidas en el archivo de cabecera WINDOWS.H:


Constantes de botón para Caja de Mensaje
Constante Boton(es) instalado(s)
 MB_OK  Botón OK
 MB_YESNO  Botones Yes y No
 MB_OKCANCEL  Botones OK y Cancel
 MB_RETRYCANCEL  Botones Retry y Cancel
 MB_YESNOCANCEL  Botones Yes, No y Cancel
 MB_ABORTRETRYIGNORE  Botones Abort, Retry e Ignore


En el programa MEMORIA.C usamos la constante MB_OK para hacer que aparezca un botón OK sencillo usado para cerrar la Caja de Mensaje. Cuando el usuario hace clic con el Mouse en el botón OK la Caja de Mensaje desaparece y el programa se cierra regresando el control de la máquina a Windows. Se obtiene el mismo efecto oprimiendo en el teclado el botón de entrada [Enter].

Además de las opciones disponibles para el botón o botones que serán puestos en la Caja de Mensaje, también es posible poner dentro de la misma íconos a un lado del texto, tales como un signo de admiración (!), un signo de interrogación (?) y así sucesivamente, lo cual no ha sido necesario en el programa Windows sencillo que hemos visto arriba.

El programa MEMORIA.C solo consta de unas cuantas líneas de código. Y sin embargo, es mucho lo que logra. Si prescindiéramos por completo de Windows y tratáramos de crear algo parecido en una máquina con algún viejo sistema operativo DOS instalado en ella, máquina que necesariamente tendría que estar habilitada con un adaptador de video y un monitor multicolor, tendríamos que escribir las líneas de código necesario para crear una ventanita capaz de ser movida de un lado a otro en la pantalla con la ayuda del Mouse, el código para poner una barra de título a la cajita, el código para imprimer texto dentro de la cajita, el código para colocar un botón OK dentro de la cajita, y el código para posicionar el texto dentro de la cajita centrándolo en forma adecuada. El programa que tendríamos que escribir en el lenguaje C tradicional requeriría cientos y cientos de líneas de código. Sin embargo, en una máquina que tenga instalado Windows en ella con el Windows API accesible para lo que queramos hacer a la manera de Windows en un programa de aplicación elaborado por nosotros, podemos obtener con unas cuantas líneas de código efectos que de otra manera requerirían cientos o quizá miles de líneas de código.

Además de lo que se puede lograr con Windows, el programador puede recurrir a otros trucos y técnicas que no se han mencionado aún. Una cosa que podemos hacer para acelerar de modo significativo la elaboración de nuestros programas Windows es la compilación de los archivos de cabecera usados en un proyecto. Entre los archivos de cabecera susceptibles de ser compilados está el archivo WINDOWS.H, el cual tiene que ser compilado una sola ocasión la primera vez que se construye un programa. Una vez compilado, en vez de usar WINDOWS.H podemos usar su versión ya compilada, lo cual a la larga puede representar un ahorro considerable de tiempo. La compilación de archivos de cabecera varía de un entorno de programación a otro y por lo tanto no se entrará en muchos detalles. Pero es algo a tener presente cuando se trabaja en la programación de computadoras a un nivel profesional.