Principal | Gráficos 3D | Gráficos 2D | Fractales | Math | Códigos | Tutoriales | Links
INTRODUCCION A LA PROGRAMACION BAJO WINDOWS (parte 1)El entorno Windows se ha convertido en el estándar de ejecución de juegos y aplicaciones, y el MS-DOS puede considerarse prácticamente muerto, de manera que en este artículo se va a tratar de ayudar a los programadores de MS-DOS a ingresar de una manera sencilla en el mundode la programacion bajo Windows. MS-DOS fue la estrella indiscutible de los sistemas operativos (en cuanto a número de usuarios y a facilidad de utilización), y a todos aquellos que dedicamos parte de nuestra vida a aprender y dominar todas sus funcionalidades y caracteristicas nos resulta dificil aceptar que está acabado. Las nuevas tecnologías de 32bits han desplazado por completo a nuestro querido MS-DOS, y si somos o pretendemos ser desarrolladores de software no nos queda otro remedio que comenzar a implementar nuestras ideas en este sistema operativo. Mediante esta serie de tutoriales se va a tratar de conseguir que los
programadores de MS-DOS (C/C++/Pascal) que deseen aprender a programar
en el entorno Windows lo hagan de una manera sencilla. Por supuesto, la
orientación de tutoriales aparte de introductoria tratará de derivar
hacia el objetivo de poder abordar la programación bajo OpenGL. La finalidad
es, introducir en la filosofía de la programación de juegos y aplicaciones
gráficas bajo Windows en C y C++, con la ayuda del compilador Visual
C/C++. COMPILACION BAJO WINDOWSLo primero que se va a comentar es la manera de compilar un programa bajo Windows. Para ello tomaremos como ejemplo el Listado 1, que simplemente muestra un DialogBox (caja de diálogo informativo con botones de acciones) y suena un fichero WAV utilizando funciones de la API (Application Programmable Interface, conjunto de funciones disponibles por Windows para el programador). /* Listado 1 Programa de Windows de ejemplo Incluir en Project Settings del VisualC++ la libreria winmm.lib esto nos permite ejecutar el sonido */ #include <windows.h> int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { PlaySound( "shotgun.wav", NULL, SND_FILENAME | SND_ASYNC); MessageBox( NULL, "Primer ejemplo de programación WINDOWS", "Shotgun !!!", MB_OK|MB_ICONASTERISK) ; return(0); } Para nuestro objetivo se va a describir el proceso de creación y compilación del Listado 1 en el compilador de Microsoft Visual C++ 6.0. Abrimos el menú File, y seleccionamos la opción New, en Project name le asignamos un nombre al proyecto, en Location seleccionamos un directorio donde se van a guardar los archivos del proyecto, en Projects seleccionamos el tipo de proyecto en este caso Win32 Application. Seleccionamos el tipo de aplicacion que deseamos crear, en este caso An empty project, una aplicacion vacia Tras esto se creará el entorno de trabajo y podremos ver ya la ventana de Class View y FileView que aparece nuestro proyecto (desde esa ventana podemos movernos a cualquier fichero o clase del proyecto mediante un doble click). Es el momento de crear el fichero .cpp que contendrá el código de nuestro programa. Para ello vamos de nuevo al menú File > New , pero esta vez seleccionamos C++ source File, en la seccion File name ponemos el nombre del archivo que vamos a crear. Escribimos el código del programa del Listado 1 (podemos copiar y pegar) y lo guardamos .. Por otra parte, necesitaremos incluir las librerías que vamos a utilizar en nuestro código. En nuestro caso hemos usado a propósito la función PlaySound de la librería multimedia para ilustrar este proceso, vamos al menu Project > Settings... y en Object/library modules agregamos winmm.lib. El proceso de compilación/linkado y ejecución del programa se puede realizar mediante el menú Build, Build Listado_1.exe o F7, y para ejecutar el programas Execute Listado_1.exe o Ctrl+F5.
FILOSOFIA DE WINDOWSMuchos de nosotros hemos caído en el error de pensar que los programas que se ejecutan bajo Windows lo hacen mucho más lentos de lo que lo harían bajo DOS. Esa conclusión la hemos sacado a partir de dos premisas, una cierta y la otra falsa, como vamos a ver a continuación. Lo que es totalmente obvio es que la mayoría de PC’s disponen tan sólo de un procesador o CPU, y que no es lo mismo dedicar todo el tiempo de la CPU a un programa (MSDOS, monotarea) que a varios (WINDOWS, multitarea). Esa es nuestra primera premisa, la verdadera. La segunda premisa (la errónea) constituye el pensar que si ejecutamos 2 programas de MSDOS bajo Windows, cada uno funcionará al menos a la mitad de la velocidad del original. Windows (o los sistemas multitarea en general) no basa el multiproceso en dar una porción de tiempo a cada programa (timeslicing); si esto fuera así, realmente al ejecutar 2 programas cada uno iría a la mitad de la velocidad del original. La clave de Windows consiste en que es un sistema basado en mensajes. Al crear un programa de Windows, como siempre, habremos de inicializar las ventanas de la aplicación (como ya veremos), pero la ejecución del programa no es lineal (como en MSDOS, donde resulta muy fácil seguir el flujo del programa desde el punto de entrada (main()) hasta la salida (final de main()). En Windows también disponemos de un punto de entrada al programa (WinMain()), pero tras ejecutarse el código de inicialización de nuestra aplicación, ésta no sigue un flujo lineal (línea por línea esperando a que Windows le asigne su porción de tiempo), sino que se queda esperando, recogiendo mensajes de Windows y contestando a éstos de manera que la aplicación haga lo deseado. A título de ejemplo está el programa del Listado 2. Este programa tiene como punto de entrada la función WinMain, donde (como explicaremos a continuación) se inicializa la ventana principal y se entra al bucle de mensajes (la porción de código donde vemos la función GetMessage()), es decir, la aplicación se dedica a recibir mensajes de Windows. /* Listado 2 Programa Windows de ejemplo. */ #include <windows.h> //--- Declaración de funciones del programa ------------------ int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int ); LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ); //--- Declaración de variables del programa ------------------ char WindowName[] = "Ventana de Windows"; char WindowTitle[] = "¡Hola, Mundo!"; //=== Función principal WinMain()============================ int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { HWND hwnd; MSG msg; WNDCLASSEX wcx; // Definimos la estructura de clase de ventana (campos): wcx.cbSize = sizeof( WNDCLASSEX ); // tamaño de la estruct. wcx.style = CS_HREDRAW | CS_VREDRAW; // valores más usuales wcx.lpfnWndProc = WndProc; // función de ventana wcx.cbClsExtra = 0; wcx.cbWndExtra = 0; // informaciones extra wcx.hInstance = hInstance; // instancia actual // icono, cursor, fondo e icono pequeño de la clase de ventana: wcx.hIcon = LoadIcon(NULL, IDI_WINLOGO); wcx.hCursor = LoadCursor(NULL, IDC_ARROW); wcx.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH ); wcx.hIconSm = LoadIcon(NULL, IDI_WINLOGO); wcx.lpszMenuName = NULL; // nombre del menú wcx.lpszClassName = WindowName; // nombre de la ventana // Registramos la clase de ventana ya preparada: if( !RegisterClassEx( &wcx ) ) return( FALSE ); // si hay error salir // Creamos la ventana con CreateWindowEx(): hwnd = CreateWindowEx( WS_EX_OVERLAPPEDWINDOW, // estilo extendido WindowName, // nombre de la ventana WindowTitle, // título de la ventana WS_OVERLAPPEDWINDOW, // estilo de ventana CW_USEDEFAULT, CW_USEDEFAULT, // Posición (x,y) en pantalla 400, 300, // ancho y alto de la ventana NULL, NULL, // ventana padre e hija+menú hInstance, // instancia actual NULL // no hay más información ); // Comprobamos la creación de la ventana: if( !hwnd ) return( FALSE ); // si hay error, salir // Hacemos visible la ventana y la actualizamos: ShowWindow( hwnd, nCmdShow ); UpdateWindow( hwnd ); // Bucle de mensajes, envía los mensajes hacia WndProc while( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ); // convertimos el mensaje DispatchMessage( &msg ); // devolvemos el control a w95 } // devolvemos el valor recibido por PostQuitMessage(). return( msg.wParam ); } //=== Función del procedimiento de ventana WndProc()=============== LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) { HDC hdc; PAINTSTRUCT ps; RECT rect; switch( message ) { // mensaje producido en la creación de la ventana case WM_CREATE: break; case WM_PAINT: hdc = BeginPaint( hwnd, &ps ); GetClientRect( hwnd, &rect ); DrawText( hdc, "¡Hola Mundo!", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER ); EndPaint( hwnd, &ps ); break; // mensaje producido al cerrar la ventana case WM_DESTROY: PostQuitMessage( 0 ); break; // resto de mensajes, dar una respuesta estándar. // dejamos que el propio windows los responda : default: return( DefWindowProc( hwnd, message, wParam, lParam ) ); } return(0); } Estos mensajes se procesan en una función aparte (que se ejecuta paralela y
automáticamente cuando se recibe un mensaje, por lo que no es necesario
llamarla explícitamente desde WinMain()). Si, por ejemplo (ver
función WndProc()) se recibe un mensaje WM_DESTROY, quiere decir
que el usuario ha pulsado el botón de cierre de la aplicación y hay que
realizar las acciones necesarias para cerrar la ventana. Si se recibe
un mensaje WM_PAINT, Windows nos está indicando que podemos dibujar/escribir
lo que deseemos en nuestra ventana, que es hora de redibujar su contenido
pues ha ocurrido algún suceso que ha borrado parte de ella (mover otra
ventana sobre su superficie, cambiar su tamaño, etc.). EL ESQUELETO BASICOA continuación vamos a comentar el programa de ejemplo que puede observarse
en el Listado 2, que imprime la cadena “Hola Mundo” en pantalla. El código
tiene un tamaño considerable, es cierto, pero por esa cantidad de líneas
hemos obtenido una ventana con un texto centrado, donde esa ventana tiene
botones de maximizado, minimizado y cierre, puede moverse y ampliarse,
etc. (imaginemos por un momento la cantidad de código que tendríamos que
escribir para hacer lo mismo bajo DOS: un sistema gráfico, un sistema
de ventanas...). HWND hwnd; MSG msg; WNDCLASSEX wcx; En el próximo tutorial veremos todos los campos que tienen estos tipos de datos y otros también muy frecuentes en Windows, pero por ahora nos basta con saber que los tipos HWND corresponden a handles a ventanas (estamos declarando una estructura que contendrá un identificar de nuestra ventana dentro de todas las existentes en el entorno windows), los MSG son estructuras para contener mensajes de Windows, y WNDCLASSEX nos servirá para decirle a Windows cómo queremos que sea la ventana de nuestra aplicación. Veamos algunos campos de esta estructura y como inicializarlos: // Definimos la estructura de clase de ventana (campos): wcx.style = CS_HREDRAW | CS_VREDRAW; // valores más usuales wcx.lpfnWndProc = WndProc; // función de ventana wcx.hIcon = LoadIcon(NULL, IDI_WINLOGO); wcx.hCursor = LoadCursor(NULL, IDC_ARROW); wcx.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH ); wcx.hIconSm = LoadIcon(NULL, IDI_WINLOGO); wcx.lpszMenuName = NULL; // nombre del menú wcx.lpszClassName = WindowName; // nombre de la ventana Con la anterior porción de código le indicaremos a Windows (más adelante en el código, cuando registremos la ventana) que queremos una ventana con el icono de windows en su barra de título (IDI_WINLOGO), con el cursor de flecha (IDC_ARROW), de fondo blanco (WHITE_BRUSH), sin menú (lpszMenuName=NULL) y con el nombre que le indicamos en el último campo. Tras rellenar la estructura, registramos la clase de ventana, es decir, le pedimos a Windows que la reconozca: // Registramos la clase de ventana ya preparada: if( !RegisterClassEx( &wcx ) ) return( FALSE ); Tras eso creamos la ventana propiamente dicha: // Creamos la ventana con CreateWindowEx(): hwnd = CreateWindowEx( WS_EX_OVERLAPPEDWINDOW, // estilo extendido WindowName, // nombre de la ventana WindowTitle, // título de la ventana WS_OVERLAPPEDWINDOW, // estilo de ventana CW_USEDEFAULT, CW_USEDEFAULT, // Posición (x,y) en pantalla 400, 300, // ancho y alto de la ventana NULL, NULL, // ventana padre e hija+menú hInstance, NULL // instancia actual ); Mediante la función CreateWindowEx() le pedimos a Windows una
ventana de tipo OVERLAPPED_WINDOW extendida (ya veremos en la próxima
entrega los diferentes estilos de ventana que podemos crear), con el nombre
y título especificados en el segundo y tercer parámetro. Con el resto
de parámetros le pedimos que la coloque en la posición (x,y) por defecto
de la pantalla (CW_USEDEFAULT), y que sea de tamaño 400x300 pixels. El
resultado devuelto por CreateWindowEx es un handle de ventana, que almacenaremos
en la variable hwnd declarada al principio del programa. Es importante
chequear los errores (aunque son muy improbables). // Hacemos visible la ventana y la actualizamos: ShowWindow( hwnd, nCmdShow ); UpdateWindow( hwnd ); La función UpdateWindow() lo que hace en realidad es enviar
un mensaje WM_PAINT. Ese mensaje es enviado por Windows a nuestros programas
para que sepamos cuando hay que redibujar nuestra ventana porque algo
ha borrado parte de la misma, o incluso cuando cambiamos su tamaño. El
resultado es que necesitamos redibujar su contenido, así que Windows nos
envía un mensaje WM_PAINT que recogeremos y procesaremos nosotros en una
función especial (función CALLBACK) que comentaremos a continuación. Mediante
UpdateWindow() nos autoenviamos un mensaje WM_PAINT de manera
que nuestra función procesa-mensajes haga el primer dibujado de la ventana
(para que escriba en ella por primera vez). // Bucle de mensajes, envía los mensajes hacia WndProc while( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ); // convertimos el mensaje kbd DispatchMessage( &msg ); // lanzamos WndProc } la anterior porción de código recoge los mensajes enviados por Windows y los “traduce” de manera que se los envía a la función CALLBACK que hayamos definido al registrar la ventana. Si volvemos atrás en el código, vemos algo así como: wcx.lpfnWndProc = WndProc; // función de ventana El campo lpfnWndProc es un puntero (lp) a función (fn). Si examinamos la función WndProc() nos daremos cuenta de que tiene un formato especial: es una función CALLBACK, nuestra procesadora de mensajes. FUNCIONES CALLBACK - RESPONDIENDO MENSAJESLa función CALLBACK WndProc es una función especial. No la llamamos desde ningún punto de programa ni
es el punto de entrada. Esta función simplemente es un “procesador de mensajes”, un procedimiento de
ventana. En el bucle de mensajes situado en WinMain, la función DispatchMessage() lo que hace realmente
es “llamar” a nuestra función de ventana (WndProc) convirtiendo el mensaje de Windows de manera que es
pasado como parámetro a esta función. LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); El parámetro hwnd es el handle a nuestra ventana (que utilizaremos al
realizar operaciones sobre ella). Message es un entero largo (UINT) que contiene
un identificador del mensaje que nos ha sido enviado. Los 2 restantes parámetros
(wparam y lparam) son valores de 32 bits que constituyen parámetros extra
interesantes para el procesamiento del mensaje. switch( message ) { // mensaje producido en la creación de la ventana case WM_CREATE : break; case WM_PAINT: hdc = BeginPaint( hwnd, &ps ); GetClientRect( hwnd, &rect ); DrawText( hdc, "¡Hola Mundo!", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER ); EndPaint( hwnd, &ps ); break; // mensaje producido al cerrar la ventana case WM_DESTROY: PostQuitMessage( 0 ); break; // resto de mensajes, dar una respuesta estándar. // dejamos que el propio windows los responda : default: return( DefWindowProc( hwnd, message, wParam, lParam ) ); } No necesitamos aprender los identificadores numéricos que corresponden
a cada mensaje, para ello disponemos de constantes predefinidas más sencillas de
recordar. En el código anterior se tratan diferentes mensajes, como WM_CREATE,
WM_DESTROY y WM_PAINT. COMENTARIO DE WM_PAINT EN NUESTRO EJEMPLOQueda por comentar el código que acompaña al mensaje WM_PAINT. Como ya se ha comentado, el mensaje WM_PAINT indica que ha habido un cambio en la ventana y que tenemos que redibujar su contenido. Es decir, cuando una porción de la ventana es modificada de forma ajena a nuestro programa (es invalidada), recibimos un mensaje WM_PAINT para que la volvamos a validar (redibujar): hdc = BeginPaint( hwnd, &ps ); GetClientRect( hwnd, &rect ); DrawText( hdc, "¡Hola Mundo!", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER ); EndPaint( hwnd, &ps ); La función BeginPaint() requiere que le indiquemos la ventana con
la que estamos trabajando, y a cambio nos devuelve un hdc (Handle to Device
Context, o handle al contexto de dispositivo). Un hdc es un identificador
del área gráfica que ocupa nuestra ventana, para poder dibujar en ella
con las funciones del GDI (Graphics Device Interface, o interface dispositivo
de gráficos) utilizando este handle. QUE HA CAMBIADO DESDE MS-DOSEl mundo Windows tiene un estilo que en principio intimida, pero que no debe afrontarse con un afán de aprendizaje total. En vez de esto, debe tomarse con otro punto de vista: lo más importante aquí es comprender el concepto básico (lo que hemos aprendido hoy) de Windows (los mensajes), y después tener una buena referencia a mano: esto significa tener un manual o revista donde poder consultar las funciones de Windows y sus parámetros, acudiendo a ella en vez de memorizar todos los tipos de parámetros y estructuras. Códigos fuentes de los dos ejemplos de este tutorialLos ejemplos los compile con el VisualC++ 6.0 |
valcoey@hotmail.com
Ramiro Alcocer, 2001
Principal | Gráficos 3D | Gráficos 2D | Fractales | Math | Códigos | Tutoriales | Links