NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Tutorial WINAPI C++ 1.3 (Creación de un ObjetoVentana)

06 de Abril del 2010 por Josep Antoni Bover, 27 visitas, 0 comentarios, 0 votos
Categorías : Windows, Programación, C y C++.
La web está en segundo plano, animación en pausa.
Cargando animación...
Tutorial WINAPI C++ 1.3 (Creación de un ObjetoVentana)

Este es el primer punto interesante del tutorial, como encapsular una ventana en una clase que podamos heredar y modificar a nuestro antojo.

La idea es tener una clase que podamos heredar, y que a partir de ella podamos crear distintos modelos de ventana, sin tener que re-escribir mucho código.

En especial este tutorial nos demostrara como crear varias ventanas del mismo tipo re-utilizando el mismo objeto.

Para empezar el problema más importante que se nos presenta es que la función WindowProcedure debe ser estática, y esto no nos interesa, ya que esta función es la primera que necesitaremos sobre-escribir. Además esto también nos produciría algún que otro problema cuando intentásemos acceder a miembros de la misma clase no estáticos.

La solución más viable es enlazar ese WindowProcedure estático a otra función no estática que además será virtual. De esta forma queda solucionado el problema más grave a la hora de diseñar esta clase, aunque sea a costa de otro callback más dentro del WindowProcedure.

Otra cosa que debemos pensar es que un control es básicamente una ventana, pero no tiene porqué tener las mismas funciones, así que estaría bien crear un modelo base para ventanas y controles. A partir de este modelo ya podremos crearnos ventanas y controles más específicos.

Veamos como quedaría estructurada la cosa :

Archivo : PlantillaEventos.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// Clase base para ventanas y controles
template <typename TIPO_DEVUELTO, const long VALOR_DEVUELTO = 0>
class PlantillaEventos : public ObjetoHWND {
public : ///////////////////////// Miembros publicos
// Constructor
PlantillaEventos(void) { _MouseDentro = false; };
// Destructor
~PlantillaEventos(void) { };
///////////////////////////////// Funciones para obtener los eventos
// -Función enlazada al mensaje WM_CLOSE
virtual TIPO_DEVUELTO Evento_Cerrar(void) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_ERASEBKGND
virtual TIPO_DEVUELTO Evento_BorrarFondo(HDC hDC) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_PAINT
virtual TIPO_DEVUELTO Evento_Pintar(HDC hDC, PAINTSTRUCT &PS) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_MOUSEMOVE
virtual TIPO_DEVUELTO Evento_Mouse_Movimiento( const int cX, const int cY,
const UINT Params ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_MOUSELEAVE
virtual TIPO_DEVUELTO Evento_Mouse_Saliendo(void) {
return VALOR_DEVUELTO;
};
// -Función enlazada a los mensajes WM_?BUTTONDOWN
virtual TIPO_DEVUELTO Evento_Mouse_BotonPresionado( const UINT Boton,
const int cX,
const int cY,
const UINT Params ) {
return VALOR_DEVUELTO;
};
// -Función enlazada a todos los mensajes WM_?BUTTONUP
virtual TIPO_DEVUELTO Evento_Mouse_BotonSoltado( const UINT Boton,
const int cX,
const int cY,
const UINT Params ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_KEYDOWN
virtual TIPO_DEVUELTO Evento_Teclado_TeclaPresionada( const UINT Caracter,
const UINT Repetir,
const UINT Params ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_KEYUP
virtual TIPO_DEVUELTO Evento_Teclado_TeclaSoltada( const UINT Caracter,
const UINT Repeticion,
const UINT Params ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_CHAR
virtual TIPO_DEVUELTO Evento_Teclado_Caracter( const UINT Caracter,
const UINT Repeticion,
const UINT Params ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_SYSCHAR
virtual TIPO_DEVUELTO Evento_Teclado_CaracterDelSistema(const UINT Caracter,
const UINT Repetir,
const UINT Params) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_KILLFOCUS
virtual TIPO_DEVUELTO Evento_Foco_Perdido(HWND hWndNuevoFoco) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_SETFOCUS
virtual TIPO_DEVUELTO Evento_Foco_Obtenido(HWND hWndUltimoFoco) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_TIMER
virtual TIPO_DEVUELTO Evento_Temporizador(const UINT IDTemporizador) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_SIZE
virtual TIPO_DEVUELTO Evento_Dimension_Cambio( const UINT Tipo,
const int nAncho,
const int nAlto ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_SIZING
virtual TIPO_DEVUELTO Evento_Dimension_Cambiando( const UINT Lado,
const RECT *NDim ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_COMMAND
virtual TIPO_DEVUELTO Evento_Comando( const UINT nID,
const UINT nNotificacion,
HWND hWndControl ) {
return VALOR_DEVUELTO;
};
// -Función enlazada al mensaje WM_NOTIFY
virtual TIPO_DEVUELTO Evento_Notificacion(const UINT cID, LPNMHDR Datos) {
return VALOR_DEVUELTO;
};
// -[INICIO devildrey33.Evento_ObjetoBoton_Click]-
// -Función enlazada al mensaje WM_BOTON_CLICK
virtual TIPO_DEVUELTO Evento_ObjetoBoton_Click( ObjetoBoton *BtnPresionado,
const UINT nBoton ) {
return VALOR_DEVUELTO;
};
// -[FIN devildrey33.Evento_ObjetoBoton_Click]-
///////////////////////////////// -Gestor de mensajes virtual
virtual TIPO_DEVUELTO CALLBACK GestorMensajes(UINT uMsg,WPARAM wParam,LPARAM lParam);
protected: /////////////////////// Miembros protegidos
// -Variable que determina si el mouse esta dentro de
// la ventana / controol
bool _MouseDentro;
};

Para empezar creamos una plantilla destinada a controlar todos los eventos de una ventana. Esta plantilla depende del TIPO_DEVUELTO que por norma sera LRESULT pero puede ser BOOL en caso de los dialogos, y retornara VALOR_DEVUELTO en los mensajes por defecto (que sera 0 para casi todo, menos para los controles estandar de windows).

Aunque no veamos el codigo de GestorMensajes, hay que destacar que el GestorMensajes debera retornar VALOR_DEVUELTO si se ha procesado el evento en este GestorMensajes, o USAR_GESTOR_POR_DEFECTO si no se ha procesado ninguno de nuestros eventos. Al devolver USAR_GESTOR_POR_DEFECTO le estamos diciendo al _GestorMensajes estatico que devuelva el WindowProcedure por defecto.

Si creamos cualquier control que mande eventos a su ventana padre, aqui seria donde deberiamos definir dicho evento. Ahora veamos el ObjetoHWND :

Archivo : ObjetoHWND.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Clase base para ventanas y controles
class ObjetoHWND {
public : //////////////////// Miembros publicos
// Constructor
ObjetoHWND(void);
// Destructor
~ObjetoHWND(void);
//////////////////////////// Funciones basicas
// -Función que retorna el hWnd de la ventana
inline HWND hWnd(void) { return _hWnd; };
// -Función que retorna la ID de la ventana
virtual UINT ID(void);
// -Función que destruye la ventana
virtual BOOL Destruir(void);
// -Función para mostrar / ocultar la ventana
virtual BOOL Visible(const bool nMostrar);
// -Función para saber si la ventana esta visible
virtual BOOL Visible(void);
// -Función para activar / desactivar la ventana
virtual BOOL Activado(const bool nActivar);
// -Función para saber si la ventana esta activada
virtual BOOL Activado(void);
// -Función para mover la ventana
virtual BOOL Mover(const int cX, const int cY, const int cAncho, const int cAlto, const BOOL Repintar = TRUE);
// -Función para asignar el foco del teclado a esta ventana
virtual HWND AsignarFoco(void);
protected : ///////////////// Miembros protegidos
// -HWND de la ventana
HWND _hWnd;
// -Funcion para registrar una clase ventana
ATOM RegistrarClase(const TCHAR *NombreClaseVentana, const int nIconoRecursos, WNDPROC WindowProcedureInicial);
};

La clase ObjetoHWND tiene una función que retorna el hWnd de esta ventana, una función para destruir la ventana, y varias funciones basicas para cualquier ventana como son Activado, Visible, Mover, y ID.

Archivo : ObjetoVentana.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Clase que hereda ObjetoHWND y se especializa en ventanas padre
class ObjetoVentana : public PlantillaEventos<LRESULT, USAR_GESTOR_POR_DEFECTO> {
public : //////////////////// Miembros publicos
// -Constructor
ObjetoVentana(void);
// -Destructor
~ObjetoVentana(void);
// Función para crear la ventana
HWND CrearVentana( HWND hWndParent, const TCHAR *nNombre, UINT nEstilos, const TCHAR *nTitulo,
const int cX, const int cY, const int cAncho, const int cAlto, HMENU nMenu,
DWORD nEstiloExtendido = NULL, const int nIconoRecursos = 32512 );
private : /////////////////// Miembros privados
// -Gestor de mensajes estatico inicial
static LRESULT CALLBACK _GestorMensajes(HWND nhWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
};

La clase ObjetoVentana hereda la clase ObjetoHWND, y contiene la función _GestorMensajes estatica que se usara al iniciar / crear la ventana y enlazara a GestorMensajes virtual.

Para que quede claro, estas dos clases serán tomadas como una base, y nosotros heredaremos de la plantilla PlantillaEventos<LRESULT, USAR_GESTOR_POR_DEFECTO> para hacer nuestra clase VentanaPadre. Los mensajes mandados a dicha VentanaPadre realizaran el siguiente recorrido : ObjetoVentana::_GestorMensajes() -> PlantillaEventos::GestorMensajes(), y por último la API DefWindowProc() si no se procesa el mensaje en el GestorMensajes. La API DefWindowProc tiene como objetivo procesar todos los mensajes tal y como lo haría Windows por defecto, por ejemplo acciones como minimizar, maximizar, restaurar, mover, cambiar tamaño, etc….

Bien, ahora vamos a ver cómo podemos enlazar nuestro _GestorMensajes estático a nuestro GestorMensajes virtual. Existen varias alternativas, pero solo voy a exponer la que me parece más ideal para todos los casos, ya que es compatible con 64 bits e incluso con el VC6, aunque no sea la más rápida.

La idea es almacenar un puntero de nuestra clase Ventana en un espacio que tiene Windows reservado en cada ventana/control para almacenar datos extra. Para lograr esto le pasaremos un puntero de nuestra clase Ventana a la función definitivamente de la siguiente forma :

Archivo : ObjetoVentana.cpp
1
2
3
4
5
6
7
8
HWND ObjetoVentana::CrearVentana( HWND hWndParent, const TCHAR *nNombre, UINT nEstilos, const TCHAR *nTitulo,
const int cX, const int cY, const int cAncho, const int cAlto, HMENU hMenu,
DWORD nEstiloExtendido, const int nIcono ) {
ATOM A = RegistrarClase(nNombre, nIcono, _GestorMensajes);
_hWnd = CreateWindowEx( nEstiloExtendido, nNombre, nTitulo, nEstilos, cX, cY,
cAncho, cAlto, hWndParent, hMenu, GetModuleHandle(NULL), this );
return _hWnd;
}

Como podemos observar a la API CreateWindowEx le pasamos en el último parámetro el puntero this, de esta forma al iniciarse la ventana desde el _GestorMensajes tendremos el puntero a nuestra clase Ventana para poder enlazarlo definitivamente, y pasarle sus mensajes correctamente a la función virtual GestorMensajes. Al usar la API CreateWindowEx estaremos invocando entre otros el mensaje WM_CREATE para esta ventana, el cual será captado por la función _GestorMensajes.

Archivo : ObjetoVentana.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
LRESULT CALLBACK ObjetoVentana::_GestorMensajes(HWND nhWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_CREATE : {
ObjetoVentana *PreVentana = reinterpret_cast<ObjetoVentana *>(
reinterpret_cast<CREATESTRUCT *>(lParam)->lpCreateParams
);
if (PreVentana == NULL) return FALSE;
PreVentana->_hWnd = nhWnd;
SetWindowLongPtr(nhWnd, GWLP_USERDATA, PtrToLong(PreVentana));
PreVentana->GestorMensajes(uMsg, wParam, lParam);
return TRUE;
}
default : {
ObjetoVentana *Ventana = reinterpret_cast<ObjetoVentana *>(
LongToPtr(GetWindowLongPtr(nhWnd, GWL_USERDATA))
);
if (Ventana != NULL) {
LRESULT Ret = Ventana->GestorMensajes(uMsg, wParam, lParam);
if (Ret == USAR_GESTOR_POR_DEFECTO) Ret = DefWindowProc(nhWnd, uMsg, wParam, lParam);
return Ret;
}
return DefWindowProc(nhWnd, uMsg, wParam, lParam);
}
}
};

Esta sería la implementación para nuestro _GestorMensajes estático, solo debemos hacer dos cosas, interceptar el mensaje WM_CREATE para obtener el puntero a nuestra clase Ventana y asignarlo definitivamente de forma que este _GestorMensajes mande los mensajes a su Ventana::GestorMensajes correspondiente. Para ello utilizamos la API SetWindowLongPtr, que nos permite asignar a un área de memoria específica para esta ventana, en el cual asignamos nuestro puntero a la clase Ventana.

Más abajo tenemos la sección default, que intentara pasar todos los mensajes que no sean WM_CREATE a su ventana padre utilizando el puntero asignado anteriormente a la ventana. Para obtener dicho puntero se utiliza la API GetWindowLongPtr además de una serie de castings, si resulta que el resultado es NULL (cosa que no debería) se utilizara la API DefWindowProc para procesar el mensaje.

Algunos puede que penséis, porque tanto lio en el WM_CREATE si podemos hacer el SetWindowLongPtr justo después de llamar a CreateWindowEx, pues esto se ha hecho así debido a que al usar CreateWindowEx se mandan una serie de mensajes, a los cuales no tendríamos acceso si utilizamos SetWindowLongPtr al terminar CreateWindowEx, y esto nos puede traer problemas extra en el primer pintado de la ventana, y con alguna cosa más.

Ahora veamos como construir una ventana a partir de estas clases :

Archivo : Tutorial_CrearObjetoVentana.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "..\Objetos Tutorial\ObjetoVentana.h"
// Objeto final : ObjetoHWND -> PlantillaEventos -> ObjetoVentana -> MiVentana
class MiVentana : public ObjetoVentana {
public :
MiVentana(void) { };
~MiVentana(void) { };
// Re-emplazamos el evento cerrar y añadimos la API PostQuitMessage
// De esta forma cuando se cierre la ventana se cerrara la aplicación
LRESULT Evento_Cerrar(void) {
PostQuitMessage(0);
return 0;
};
};
// WinMain
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
MiVentana Ventana[5];
size_t i = 0;
for (i = 0; i < 5; i++) {
Ventana[i].CrearVentana( NULL, TEXT("Mi_Ventana"), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
TEXT("Ejemplo tutorial 1.3"), 100 + (i * 10), 100 + (i * 10), 400, 300, NULL );
}
// Bucle principal para obtener eventos de la ventana
MSG Mensaje;
while (TRUE == GetMessage(&Mensaje, NULL, 0, 0)) {
TranslateMessage(&Mensaje);
DispatchMessage(&Mensaje);
}
return 0;
}

Como podéis ver se ha creado una tercera clase 'MiVentana' y se ha re-emplazado la función 'Evento_Cerrar' de tal forma que cuando se cierre la ventana también terminara la aplicación, dicha función esta enlazada al mensaje WM_CLOSE. Ahora ya tenemos un objeto Ventana con el cual podemos crear ventanas con pocas lineas y de forma mas cómoda. En este ejemplo se crean 5 ventanas desde la clase MiVentana, y como en el Evento_Cerrar se ha incluido la API PostQuitMessage, cuando cerremos cualquiera de las 5 ventanas terminara la aplicación y se cerraran todas las demás.

Si nos fijamos en la plantilla de eventos podemos re-emplazar cualquiera de sus funciones virtuales para utilizar dichos eventos. Por ejemplo podriamos re-emplazar el Evento_Mouse_Movimiento, y cambiar el titulo de la ventana por las coordenadas que nos devuelve dicho evento, etc.... Esta forma de trabajar nos ahorrara muchos problemas en el futuro, ademas de que tendremos un codigo mucho mas ordenado.

Última modificación 07/10/2012 :

Había un error al crear el objeto plantilla ObjetoVentana al que le pasábamos 0 en vez de pasarle USAR_GESTOR_POR_DEFECTO, el cual provocaba que en algunas ocasiones no se llegara a llamar el gestor de ventanas por defecto.

Gracias a Javi Luque por informarme del error.

La siguiente parte del tutorial nos enseñara los conceptos más básicos sobre pintar gráficos en ventanas : 1.4 - Entorno gráfico de windows (GDI).

Descargar tutorial WinAPI completo Calculadora compilada