Ventana redonda y botones con imágenes .

 

 

Por: Demian Panello.

 

¿Ventanas siempre iguales?.

 

El GDI de Windows proporciona el aspecto típico de una ventana, (o diálogo), o sea un rectángulo con un área cliente y una barra de título. También proporciona ventanas con marcos, (frame windows), con menúes y barras de herramientas. Pero siempre son rectángulos, y la verdad es que éste es el aspecto más útil para aplicaciones en general.

No obstante, a veces, uno podría necesitar otro aspecto de ventana si lo que pretende es por ejemplo una aplicación visualmente atractiva, típica de los programas multimedia. Ventanas con formas elípticas, por ejemplo, con algún skin, (gráfico que "envuelve" la ventana dándole un aspecto más original) y botones con imágenes.

Bien, este artículo simplemente presentará los pasos iniciales para lograr alterar la forma de una ventana típica y la creación de botones con imágenes.

Si quiere seguirme, vaya creando una aplicación Dialog Based.

 

Regiones:

 

En las aplicaciones gráficas, (que permiten realizar dibujos o manipular gráficos), es muy común la creación de regiones con alguna forma particular sobre algún sector de una ventana. La MFC ofrece una clase llamada CRgn que encapsula toda la funcionalidad de las regiones, (o sea todas las funciones del SDK de Windows sobre regiones, tipo de handle: HRGN).

Teniendo un objeto CRgn, con sus funciones miembros se puede crear, alterar y obtener información sobre una región. Hay funciones de CRgn que permiten crear regiones elípticas, rectangulares y poligonales, (con ésta última podríamos por ejemplo, crear una región triangular).

También se pueden combinar regiones si lo que se desea es representar alguna forma irregular.

En definitiva, una vez creado el objeto CRgn se pueden definir las siguientes regiones:

 

CreateRecRgn: Define un objeto CRgn con una región rectangular.

CreateRecRgnIndirect: Define un objeto CRgn con una región rectangular por medio de una estructura RECT u objeto CRect.

CreateEllipticRgn: Define un objeto CRgn con una región elíptica.

CreateEllipticRgnIndirect: Define un objeto CRgn con una región elíptica por medio de una estructura RECT u objeto CRect.

CreatePolygonRgn: Define un objeto CRgn con una región poligonal. El polígono se cierra automáticamente, si es necesario, por medio de una línea recta que une el último punto con el primero.

CreatePolyPolygonRgn: Define un objeto CRgn con una región poligonal por medio de una serie de polígonos cerrados.

CreateRoundRgn: Define un objeto CRgn con una región rectangular con las esquinas redondas.

CombineRgn: Define una nueva región, (un nuevo objeto CRgn), con el resultado de la unión de dos regiones, (otros dos objetos CRgn).

 

Para nuestro propósito, cambiar el aspecto del diálogo de rectangular a redondo o elíptico, necesitaremos una región, por lo tanto agregaremos una variable miembro al diálogo de tipo CRgn llamada m_rgn.

Además modificaremos una propiedad del dialogo: pulse con el derecho del mouse sobre el diálogo vaya a Properties y en la solapa Styles quite el tilde a Title bar, de esta forma nuestra ventana no tendrá barra de título.

 

En OnInitDialog() antes de la sentencia return escriba:

 

    //******** Código que cambia la forma del diálogo ***************

CRect recDialogo;  //Objeto CRect para las dimensiones del diálogo
GetClientRect(recDialogo);    //obtengo las dimensiones

//Creo una región elíptica que abarque todo el diálogo
m_rgn.CreateEllipticRgnIndirect (recDialogo); (1)
SetWindowRgn((HRGN) m_rgn, TRUE);
//*****************************************************************

 

Listo, si ejecuta la aplicación verá que ya tiene un diálogo redondo o elíptico dependiendo de las dimensiones que le dió al mismo en tiempo de diseño.

En (1) podría haber escrito: m_rgn.CreateEllipticRgn(0, 0, recDialogo.Width(), recDialogo.Height());

es lo mismo.

Una vez que se crea una región, (tarea realizada en (1) ), luego hay que llamar a la función SetWindowRgn() que establece la región indicada en el primer parámetro, (nuestro ejemplo m_rgn). En el segundo se coloca TRUE para indicarle al sistema que redibuje la ventana.

 

Botones con imágenes:

 

Habrá observado que de no ser por los botones Aceptar y Cancelar no podríamos cerrar la ventana, ya que le quitamos la barra de título. Bien, ahora avanzaremos más, pulse dos veces sobre el botón Aceptar, (se generará OnOk()) y dos veces sobre Cancelar, (se generará OnCancel()) y comente el contenido de ambas funciones, luego elimine los botones.

Si lo ejecuta nuevamente, descubrirá que ya no tenemos formas de cerrar esta particular ventana redonda, entonces ahora nos tomaremos el trabajo de colocarle dos botones que simulen la acción de minimizar y cerrar.

Bien, pero no van a ser dos botones comunes, sino que los símbolos - y X de minimizar y cerrar serán iconos.

 

Coloque 2 botones en el dialogo más o menos en la posición que indica la figura:

 

 

No importa el tamaño, pero sí las siguientes propiedades:

 

(de izquierda a derecha)

 

Primer botón Segundo botón

Id: IDC_MINIMIZAR.

Id: IDC_CERRAR.
Caption: MINI.   (todo en mayúsculas) Caption: CERRAR.   (todo en mayúsculas)
OwnerDraw: tildado. OwnerDraw: tildado.
Bitmap: tildado. Bitmap: tildado.

 

La propiedad OwnerDraw le indica que el propio botón se encargará de su representación, (de dibujarse) y Bitmap que contendrá un mapa de bits.

Hay algo muy importante que aclarar antes de continuar; un botón puede tener 4 estados: normal, pulsado, recibe el foco y desactivado, si le asignamos sólo una imagen a un botón, nos perderíamos de ver o apreciar el cambio de estado entre normal o pulsado, o sea, ese efecto de que se hunde al momento de pulsarlo, también nos perderíamos el cambio cuando se desactiva y cuando recibe el foco. Estos últimos ahora no nos interesa porque no vamos a desactivarlo nunca y cuando recibe el foco podría ser necesario pero lo vamos a omitir.

Si necesitaremos 2 imágenes por cada botón, una para el estado normal y otra para pulsado.

Así que, ¡manos a la obra!, vaya al editor de recursos y genere los siguientes mapas de bits especificándole las propiedades adecuadas, (ó sea más astuto y cópielo de aquí) : )

 

ID: "CERRARU" ID: "CERRARD" ID: "MINID" ID: "MINIU"

 

ATENCION: Los ID de estos gráficos deben estar entre comillas, ya que se trata del Caption del botón más U o D de Up y Down

 

Seguramente pensará, "este no se rompió mucho la cabeza para hacer esas imágenes" ..., es verdad, así que deje volar su imaginación y diseñe imágenes más elaboradas.

 

 

La clase CBitmapButton:

 

Todo no termina con diseñar las imágenes, ahora hay que conectarlas a los botones. Necesitaremos dos objetos CBitmapButton, por lo tanto agregue dos variables a la clase del diálogo de tipo CBitmapButton llamadas m_bmpCerrar y m_bmpMini, (si ha leido otros artículos del sitio o algunos de los tutoriales no hará falta que le explique como hacer esto).

 

En OnInitDialog(), a continuación de lo que escribió en el primer apartado, pero antes de return, escriba:

 

//Cargo las imágenes para los botones Cerrar y Minimizar.
VERIFY(m_bmpCerrar.AutoLoad (IDC_CERRAR, this));
VERIFY(m_bmpMini.AutoLoad (IDC_MINIMIZAR, this));

 

Estamos usando la función AutoLoad() de los objetos CBitmapButton, cuyos parámetros son: el ID del botón y un puntero al objeto que tiene el botón, o sea nuestro diálogo o sea this.

Evaluamos estas llamadas a Autoload() con la macro VERIFY que nos intercepta algún error.

 

Finalmente escribiremos los mensajes de los botones OnCerrar() y OnMinimizar():

 

void CVentana_redondaDlg::OnCerrar()
{
    //Pregunto si desea salir.
    if (MessageBox("¿Desea cerrar la ventana?", "Salir", MB_YESNO|MB_ICONQUESTION)==IDYES)
        EndDialog(IDOK);   
}

void CVentana_redondaDlg::OnMinimizar()
{
    //Minimizo el diálogo
    this->ShowWindow(SW_MINIMIZE);    
}

 

Si ejecuta el programa tendrá una ventana con fomar elíptica, (redonda si su diálogo es cuadrado), con los botones de cerrar y minimizar personalizados.

 

Mover una ventana sin barra de título:

 

Un detalle más: por lo general una ventana permite que se la mueva manteniendo pulsado el botón izquierdo del mouse sobre la barra de título, pero si nuestra ventana no tiene barra, ¿cómo hacemos para que el usuario pueda moverla?.

Respuesta: engañando al sistema.

 

Hay un mensaje que nos permite averiguar en qué zona de una ventana se ha movido el mouse, por ejemplo:

 

HTMAXBUTTON: sobre el botón de maximizar.

HTCAPTION: sobre la barra de título.

HTCLIENT: sobre la región cliente de una ventana.

HTLEFT: sobre el borde izquierdo.

HTHSCROLL: sobre una barra de desplazamiento horizontal.

...etc, (son 24 las constantes).

 

Este mensaje es WM_NCHITTEST que no se encuentra en la lista de mensaje que ofrece el Class Wizard, así que tendremos que agregarlo a mano.

Tenga en cuenta que moveremos la ventana manteniendo pulsado el botón izquierdo del mouse en cualquier lugar de la ventana, (nuestra ventana es todo CLIENTE ya que eliminamos la barra de título), así que también necesitaremos el mensaje WM_LBUTTONDOWN para nuestro diálogo.

Pero primero agregaremos la entrada en el mapa de mensajes de WM_NCHITTEST especificándole cual será la función que lo manipulará.

 

Localice en el archivo de implementación de la clase del diálogo, (en mi ejemplo es el archivo "ventana_redondaDlg.cpp" porque "ventana_redonda" ha sido el nombre que le dí al proyecto), el grupo de instrucciones encerradas entre BEGIN_MESSAGE_MAP(CVentana_redondaDlg, CDialog) y END_MESSAGE_MAP y agregue lo que se encuentra resaltado en amarillo.

 

//Aqui en el mapa de mensajes se ha agregado a mano el mensaje
//WM_NCHITTEST necesario para poder mover la ventana que carece de barra.

BEGIN_MESSAGE_MAP(CVentana_redondaDlg, CDialog)
    //{{AFX_MSG_MAP(CVentana_redondaDlg)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(IDC_CERRAR, OnCerrar)
    ON_BN_CLICKED(IDC_MINIMIZAR, OnMinimizar)
    ON_WM_LBUTTONDOWN()
    ON_BN_CLICKED(IDC_ACERCA, OnAcerca)
    //}}AFX_MSG_MAP
    ON_MESSAGE(WM_NCHITTEST, OnNCHitTest)
END_MESSAGE_MAP()

 

La macro ON_MESSAGE está indicando que el mensaje WM_NCHITTEST será manejado por la función OnNCHitTest, (puede tener cualquier otro nombre si al fin de cuentas tendremos que crearla nosotros).

 

Agregue la función OnNCHitTest al diálogo, cuyo retorno o Type function sea LRESULT y los dos parámetros que recibe son WPARAM wParam y LPARAM lParam.

 

Nota: en el artículo Uso del joystick en una aplicación MFC también realizo este procedimiento de agregar un mensaje sin el Class Wizard, allí necesitaba los mensajes del joystick.

 

En OnNCHitTest() escriba:

 

LRESULT CVentana_redondaDlg::OnNCHitTest(WPARAM wParam, LPARAM lParam)
{
    //obtengo las coordenas donde ocurrió la pulsación
    int xPos = LOWORD(lParam);
    int yPos = HIWORD(lParam);

    //llamo a la función de la clase base para obtener el lugar donde fue pulsado
    //ejemplo: HTCLIENT si ocurrió en el área cliente o HTCAPTION si ocurrió en la barra
    //donde está el título.

    UINT hitTest = CDialog::OnNcHitTest(CSize(xPos, yPos));

   //averiguo donde ocurrio la pulsación, si es en el cliente retorno que fue

   //en la barra para simular la acción de mover.
   if (hitTest==HTCLIENT)
        return HTCAPTION;
    else
        return hitTest;         //en otro lugar que no me importa.
}

 

Y en OnLButtonDown:

 

void CVentana_redondaDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
    CDialog::OnLButtonDown(nFlags, point);
    //Envio el mensaje WM_NCLBUTTONDOWN, (que a su vez llama a WM_NCHITTEST),
    //especificandole que me interesa la pulsación sobre la barra de título o sea HTCAPTION.

    PostMessage(WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM( point.x, point.y));
}

 

Aquí se envía el mensaje WM_NCLBUTTONDOWN, (que da a lugar a un WM_NCHITTEST  ), junto con los dos parámetros necesarios, wParam es HTCAPTION y lParam la concatenación de los dos valores de 16 bits point.x y point.y que la macro MAKELPARAM se encarga de armar para obtener uno de 32 bits que será el lParam que recibirá la función OnNcHitTest().

Se usa PostMessage() y no SendMessage() porque la primera coloca el mensaje en la cola de mensajes y vuelve mientras que la otra no retorna hasta que se procese el mensaje.

 

Eso es todo. Probablemente ésta última parte de mover la ventana nos es de lo más clara, pero quizás leyendo algunos de estos temas en la ayuda de MSDN todo se comprenda un poco más.

Aqui va una captura del diálogo, (le agregué un static con texto y un botón común para ver el diálogo Acerca de...).

 

 

 

Nota: Esta no es la única forma de cambiar el aspecto de una ventana y tampoco la más prolija. Un desarrollo más serio hubiera implicado derivar una clase, por ejemplo, CVentanaRedonda de CWnd ó de CFrameWindow ó de CDialog y entonces sobreescribir los métodos Create y Paint. El método aquí usado no está explotando las cualidades de la programación orientada a objetos, pero de todas formas, por su simpleza, sirve conocerlo.

 

Descargar fuente de los ejemplos: ventana_redonda.zip (32 Kb).

Volver a la página principal