Todo sobre los botones.  PARTE 2

 

Por: Demian Panello.

 

En esta segunda parte veremos una de las actividades más comunes programando bajo Windows en C++ que se llama "subclassing". Aquí encontrará cómo:

 

- Cambiar el color de un botón.

- Responder a los movimientos del puntero del mouse sobre el botón.

- Redibujar completamente el aspecto del botón.

- Dibujar el recuadro del foco en un botón.

- Responder a la pulsación del botón derecho del mouse sobre nuestro botón.

 

Subclassing:

 

Se conoce como "subclassing" al método que permite modificar algún comportamiento o aspecto gráfico de los controles típicos. Por ejemplo si quiero lograr un botón redondo debo aplicar "subclassing" o si quiero obtener mensaje particulares de algún control también.

Técnicamente "subclassing" trata de la personalización de una clase derivada de algún control. 

En el ejemplo que veremos aquí tendremos un botón que se pintará de amarillo cuando pasemos sobre él con el mouse. Para lograr esto deberemos derivar una clase de CButton y asociar algún botón del diálogo a ésta.

Teniendo una clase derivada  de CButton tendremos entonces la posibilidad de utilizar sus mensajes y sobrescribir las funciones virtuales de la clase base, obteniendo un botón personalizado.

 

Existen dos tipos de "subclassing":

1) Instance subclassing: Es cuando uno aplica "subclassing"  a una sola instancia de una ventana, (control).

2) Global subclassing: Es cuando uno aplica "subclassing" a todas las ventanas existentes de un mismo control.

Aquí sólo veremos "instance subclassing".

 

Aplicación de ejemplo.

 

Empecemos con el ejemplo. Cree un proyecto Dialog Based con todas las características por defecto, (si quiere puede quitar la opción de About Box).

En este ejemplo haremos que el botón "Cancelar" al pasar con el puntero del mouse sobre él, cambie de color y si pulsamos el botón derecho del mouse, también sobre el botón "Cancelar", se muestre un mensaje.

 


Observe atentamente lo siguiente: el botón "Cancelar", (lo mismo "Aceptar" y cualquier otro que coloque en el diálogo), es una instancia de CButton, una clase que se encuentra definida en lo "profundo" de las librerías de MFC, no tenemos manera de modificar esa clase "a piaccere", (fíjese que no aparece la clase CButton en el Class View), pero nada nos prohíbe de crear una clase nueva derivada de CButton y luego indicar que el botón "Cancelar" sea un objeto de ésta. Además al derivar nuestra clase de CButton la tendremos... ahí nomás disponible, en el Class View, para poder "tocarla" y hacer de ella lo que sea viendo cuales son las consecuencias en ejecución sobre el botón. Esto no pasa solamente con los botones sino con cualquier control o clase de MFC que defina funciones virtuales, pues ellas son el "gancho" para poder aplicar "subclassing". Este es uno de los "grandes trucos" de Visual C++ para lograr cosas que parecen imposible.


Entonces pulse Ctrl + W para acceder al Class Wizard, luego pulse el botón Add Class y luego New, aparecerá la ventana New Class. El nombre de la clase será CMiBoton, derivada de CButton:

 

 

Una vez que pulsó OK se habrá creado una nueva clase y estará disponible en el Class View.

 

 

Detectar el movimiento del mouse sobre el botón.

 

Nuestra nueva clase CMiBoton como ha sido derivada de CButton heredó los mensajes de ventana que CButton tenía, (que a su vez había sido derivada de CWnd). Ahora nos interesa el mensaje WM_MOUSEMOVE que ocurre cuando se mueve el puntero del mouse. El mouse se puede mover sobre el control o no, para poder averiguar cuando está sobre el botón declararemos una variable miembro, (de CMiBoton) de tipo BOOL llamada m_bMOB, (MOB = "mouse over the button") y la inicializaremos en FALSE en el constructor.

La idea es: verificar, cuando se mueve el mouse, (OnMouseMove), si m_bMOB es FALSE, de ser así le asignamos TRUE y activamos un timer y por cada intervalo del mismo, (1/100 de segundos por ejemplo), averiguamos si el puntero del mouse está dentro de los límites del control, cuando no lo esté volvemos a poner m_bMOB en FALSE y desactivamos el timer.

Esto es sólo para detectar la presencia del mouse sobre el botón, pero como queremos además cambiarle el color en ese momento, habrá llamadas a la función Invalidate(TRUE) en ambos procesos, (OnMouseMove y OnTimer), para que redibuje el botón usando el código que escribiremos en OnDrawItem. Pero esto es adelantarse un poco, así que por ahora agreguemos a la clase CMiBoton los mensajes WM_MOUSEMOVE y MW_TIMER, (¡no se olvide de agregar la variable miembro m_bMOB y definirla con el valor inicial FALSE en el constructor!).

Una vez generadas las funciones CMiBoton::OnMouseMove() y CMiBoton::OnTimer() escriba:

 

void CMiBoton::OnMouseMove(UINT nFlags, CPoint point) 
{

//Si m_bMOB es FALSE.
if (!m_bMOB) 
{
  //Variable en TRUE ya que el mouse está sobre el botón. 
  m_bMOB = TRUE; 
  Invalidate(TRUE);   //Se redibuja el botón.

  //Activo el timer para que vuelva a verificar cada 1/100 si el mouse
  //aún sigue estando sobre el botón.

  SetTimer(1, 100, NULL); 
}

CButton::OnMouseMove(nFlags, point);
}

 

Y OnTimer():

 

void CMiBoton::OnTimer(UINT nIDEvent) 
{

//Obtengo el punto donde está el mouse
CPoint mp(GetMessagePos());

//Paso el punto a coordenadas de mi ventana, (vienen con respecto a toda la pantalla).
ScreenToClient(&mp);

//Obtengo el rectángulo del botón.
CRect rect;
GetClientRect(rect);

//Me fijo si el punto donde está el mouse pertenece a este rectángulo.
if (!rect.PtInRect(mp))
  {

   //Si no lo está, la variable es FALSE y "detengo" el timer.
   m_bMOB = FALSE;
   KillTimer(1);

   //Se redibuja el control
   Invalidate(TRUE);
  }
CButton::OnTimer(nIDEvent);
}

 

En esta función lo más importante es: la obtención del punto donde ocurrió el mensaje por medio de la función GetMessagePos() y luego averiguar si dicho punto está o no dentro del rectángulo cliente del botón, (atención que obtener un objeto CRect con los valores de la función GetClientRect() dentro de esta clase CMiBoton implica que se trata del rectángulo cliente del botón no del diálogo), por medio de la función miembro de CRect, PtInRect().

 

Dibujar un botón.

 

Habíamos quedado con que cambiaríamos el color del botón cuando el mouse esté sobre él, bien, con los dos procedimiento anteriores ya resolvimos una parte, averiguamos cuando el puntero del mouse está sobre el botón y cuando no, nos falta entonces escribir código que redibuje el botón de acuerdo a las circunstancias, amarillo si el puntero está sobre el botón y el color por defecto si no lo está.

Cada vez que ocurre un llamado a la función Invalidate(TRUE) se redibuja toda una ventana, bueno, nosotros tenemos una ventana en esta clase, el mismo botón. La llamada a Invalidate(TRUE) envía un mensaje WM_DRAWITEM. Si no nos interesara cambiar el color del botón entonces ocurriría una llamada a WM_DRAWITEM de la clase base, CButton, y el botón se dibujaría como cualquier otro botón de la clase CButton pero como precisamente queremos modificar el aspecto del control, (por lo menos cambiarle el color), podemos aprovechar que DrawItem es virtual en la clase base y sobrescribir su contenido, eso si, siendo responsables ahora de redibujar completamente el botón, no sólo pintarlo de amarillo sino dibujar el recuadro con sus bordes, poner el texto y hasta incluso "simular" ese efecto de hundimiento sobre el diálogo cuando se lo pulsa. Es mucho trabajo ¿no?, pero vale la pena aunque en este ejemplo sólo lo hagamos para cambiar el color, piense que tiene a su disposición la creación completa del aspecto del botón.

Si se pregunta cómo saber qué funciones son virtuales en la clase base,  una forma sencilla es pulsando con el derecho en el Class View sobre la clase CMiBoton y seleccionando Add Virtual Function aparecerá una ventana con todas las funciones virtuales de esa clase base, (aproveche y agregue DrawItem ahora) o en MSDN escriba el nombre de una clase MFC, (por ejemplo CButton), y allí encontrará información sobre sus funciones virtuales.

Esta función sirve para dibujar sólo los controles OwnerDraw, (vea más adelante).

Escriba en OnDrawItem():

 

void CMiBoton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) 
{
  CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
  CRect rect = lpDrawItemStruct->rcItem;
  UINT state = lpDrawItemStruct->itemState;

  CString strText;
  GetWindowText(strText);

//De acuerdo al estado del botón, (normal o pulsado) se dibuja los bordes.
if (state & ODS_SELECTED)
   pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH |    DFCS_PUSHED);
else
   pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH);

//Se modifica el rectángulo a dibujar tomando sus bordes, (da la apariencia 3D).
rect.DeflateRect( CSize(GetSystemMetrics(SM_CXEDGE),                         GetSystemMetrics(SM_CYEDGE)));

//Si el mouse está sobre el botón, le cambio el color.
if (m_bMOB)
   pDC->FillSolidRect(rect, RGB(255, 255, 200)); //amarillo claro.

//Dibujo el texto del botón.
if (!strText.IsEmpty())
  {
    CSize textLong = pDC->GetTextExtent(strText);
    CPoint pt( rect.CenterPoint().x - textLong.cx/2, rect.CenterPoint().y - textLong.cy/2 );

    if (state & ODS_SELECTED)  
        pt.Offset(1,1); //Si fue pulsado, desplazo un pixel hacia la derecha y abajo el punto.

    int nMode = pDC->SetBkMode(TRANSPARENT); //Modo transparente.

     if (state & ODS_DISABLED)
         pDC->DrawState(pt, textLong, strText, DST_TEXT|DSS_DISABLED, TRUE, 0,         (HBRUSH)NULL);
    else
         pDC->TextOut(pt.x, pt.y, strText);

    pDC->SetBkMode(nMode);
   }

if (state & ODA_FOCUS)  //Si el control recibió el foco.
    pDC->DrawFocusRect(rect);   //Dibujo el rectángulo del foco.

}

 

¿Muchas llamados a funciones raras?, bueno sí, pero a estas líneas son un procedimiento estandar para dibujar un control, a excepción de la llamada a FillSolidRect() que la usamos para pintar el botón. Analicemos esta función.

 

DrawItem recibe un puntero a la estructura DRAWITEMSTRUCT que trae información sobre el control dibujar.

 

typedef struct tagDRAWITEMSTRUCT 

{

  UINT   CtlType;

  UINT   CtlID;

  UINT   itemID;

  UINT   itemAction;

  UINT   itemState;

  HWND   hwndItem;

  HDC    hDC;

  RECT   rcItem;

  DWORD  itemData; 

} DRAWITEMSTRUCT;

 

CtlType: Tipo de control:

CtlID: El id del control. No se usa en caso de tratarse de un menú.

ItemID: El ID de un ítem del menú ó el índice de un elemento de un ListBox o ComboBox.

ItemAction: Define la acción a dibujar:

ItemState: Indica el estado visual del control luego de dibujarlo, (por ejemplo ha sido pulsado el botón)

hwndItem: El handle (HWND) de un combobox, listbox o button. Es el handle (HMENU) si se trata de un menú.

 

hDC: Handle del Dispositivo de Contexto. Este handle se debe usar para realizar los dibujos.

 

rcItem: El rectángulo del control que será usado por el handle hDC. 

 

itemData: Para un control ListBox o ComboBox este campo contiene el valor pasado por:

      

       - CCombo::AddString()

       - CCombo::InsertString()

       - CListBox::AddString()

       - CListBox::AddString()

 

y si se trata de un menú este campo trae el valor pasado por:

        - CMenu::AppendMenu()

        - CMenu::InsertMenu()

        - CMenu::ModifyMenu()

 

Para tan solo dibujar el botón solamente usaremos itemState, hDC y rcItem. Como es más útil dibujar con un objeto CDC, creamos uno a partir del handle hDC llamando a la función FromHandle() y para no tener que "arrastrar" el nombre de la variable estructura, (lpDrawItemStruct), pasamos los valores que usaremos a variables.    

Primero chequeamos si el bit de state que indica que el botón ha sido pulsado está activo:

if (state & ODS_SELECTED)

Si esto devuelve 1, (verdadero), entonces usamos la función DrawFrameControl(), pasándole, entre otros parámetros, DFCS_BUTTONPUSH | DFCS_PUSHED que dibuja el control en el estado pulsado en caso que el if (...) devuelva 0, (falso), también usamos DrawFrameControl() pero sin la constante DFCS_PUSHED para que dibuje el rectángulo del botón en su estado normal.

Si presta atención en los botones estándar de Windows verá que el color, (sea cual sea), se concentra en el interior del mismo y no en los bordes, lo que da un aspecto 3D, por eso luego de realizar el dibujo del rectángulo con DrawFrameControl(), modificaremos nuestro objeto rect "achicándolo" un poco para así cuando lo usemos para pintar el botón, el color no tomará los bordes de nuestro actual rectángulo dibujado. Esto se logra con DeflacteRect() con un parámetro de tipo CSize, (hay varias versiones sobrecargadas de DeflacteRect()), basado en los puntos superior izquierdo e inferior derecho del rectángulo, (cuando escriba este programa pruebe comentar esta línea y verá que cuando mueve el puntero del mouse sobre el botón éste se pinta por completo perdiendo el aspecto 3D).

Entonces si el mouse está sobre el botón, (o sea la variable m_bMOB es TRUE), lo pintamos de amarillo claro con la función FillSolidRect(), puede probar también otros colores con la macro RGB().

Nos queda escribir el texto del botón, (en realidad vamos a dibujar el texto). Obtenemos el tamaño del rectángulo que contiene al texto, dado por la altura y la longitud del texto que depende del tipo de fuente, con la función de contexto de dispositivo GetTextExtend(). Luego se obtiene el punto medio de ese rectángulo para poder escribir el texto centrado. Si el botón ha sido pulsado, se desplaza ese punto un pixel hacia la derecha y uno hacia la izquierda, ya que el botón se va a "hundir" sobre el formulario usando la función Offset() de CPoint, (podríamos haber incrementado en 1 el campo x de pt y el campo y).

Ponemos el modo en transparente para que al dibujar el texto no se dibuje el rectángulo donde está y averiguamos si el estado del botón está inhabilitado, porque de ser así llamamos a la función DrawState() que nos facilita dibujar el texto con aspecto de inhabilitado, miren algún botón inhabilitado de alguna aplicación y verán a que me refiero, (el texto está en gris y como "grabado" en el botón). Esta función se encarga de dibujar imágenes, bitmaps en particular o simple texto, aplicándole algún efecto especial de acuerdo a un estado, "normal" o "disabled", en nuestro caso las constantes DST_TEXT|DSS_DISABLED indican que dibujaremos texto con aspecto de inhabilitado.

Si el botón no se encuentra inhabilitado, simplemente usamos la función TextOut() 

Nos queda una última cosa que prevenir cuando se dibuje el botón. Dibujar el rectángulo punteado que representa el foco cuando el botón lo reciba, así que verificamos si el estado es ODA_FOCUS, de ser así tenemos que dibujar el foco, para eso usamos la función DrawFocusRect().

 

Seguramente pensará "qué complejo ha sido todo esto", en realidad no debe recordar todas estas funciones, lo que sí es necesario saber es qué hace y cuándo ocurre DrawItem, luego podrá con este ejemplo comprender mejor las funciones e incluso experimentar cosas, (esto es sumamente recomendable pues experimentando se aprende mucho y se fijan conceptos).

 

¿Qué hace y cuándo ocurre DrawItem?:

 

DrawItem, (en el fondo el mensaje WM_DRAWITEM), se encarga de dibujar un control OWNER DRAW. Tener un control Owner Draw significa que uno se encargara de dibujar su aspecto. ¿Cuándo hay que dibujarlo?. Un control se dibuja muchas veces cuando la aplicación está activa, por ejemplo cuando se inicia la aplicación ocurre una llamada a DrawItem, cuando se muestra el control luego de estar oculto por otra ventana también ocurre una llamada a DrawItem y cuando se llama a la función Invalidate(TRUE) también ocurre una llamada a DrawItem. Así que hay que cuidarse mucho de no hacer mucho trabajo en esta función, (como acceso a disco), ya que se tardaría mucho en dibujar y podría ser desastroso. Si el control no fuera Owner Draw, entonces no hay que escribir código en Draw Item ya que el sistema gestiona todo de forma automática dibujando los aspectos estándares de los controles.

DrawItem nos provee una estructura llamada DrawItemStruct con mucha información útil para comprender qué debemos hacer con el control o mejor cómo debemos dibujar el control. Y deberemos ser precavidos de dibujar todos los posibles estados del control, (pruebe comentar las últimas dos líneas de DrawItem en nuestro ejemplo y verá que nunca sabremos cuando el botón recibió el foco porque no hemos contemplado ese estado).

 

El botón derecho del mouse:

 

Agregue a la clase CMiBoton el mensaje WM_RBUTTONDOWN y escriba:

 

void CMiBoton::OnRButtonDown(UINT nFlags, CPoint point) 
{
//Al pulsar el botón derecho del mouse, muestro un mensaje.
MessageBox("Soy un botón que responde al botón derecho del mouse", "Mensaje", MB_ICONEXCLAMATION);

CButton::OnRButtonDown(nFlags, point);
}

 

Bueno ya tenemos nuestra clase derivada de CButton con la función DrawItem sobrescrita, nos falta asociarle un botón, bien, pulse con el botón derecho del mouse sobre el botón "Cancelar", seleccione Properties y tilde la propiedad Owner Draw. Luego pulse CTRL+W para acceder al Class Wizard y en la solapa Member Variables seleccione IDCANCEL y pulse Add Variable.

En la ventana que aparece escriba el nombre de variable m_miBoton de Category= Control y Variable Type = CMiBoton.

 

 

Listo, pruebe la aplicación y verá que al pasar el mouse sobre el botón éste cambia de color.

 

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

Todo sobre los botones parte 1.

Volver a la página principal