Combo boxes owner draw

El funcionamiento de los controles combo box owner-draw es muy similar al de los controles list box. Mucho de lo que se comentó para estos controles se aplica iguamente a los combo box.

De modo que también existen dos estilos distintos owner-draw que se pueden aplicar a los controles combo box CBS_OWNERDRAWFIXED y CBS_OWNERDRAWVARIABLE.

El primero define controles combo box owner-draw en los que la altura de todos los ítems es la misma. En el segundo caso, las alturas de cada ítem pueden ser diferentes.

Como también pasa en los controles list box, en los combo box con estilos owner-draw tampoco se activa por defecto el estilo CBS_HASSTRINGS. Si estamos personalizando nuestros controles, lo más probable que éste no contenga cadenas, o al menos, no sólo cadenas.

Sin embargo, es posible que aún tratándose de un combo box con un estilo owner-draw, nuestro control contenga cadenas. En ese caso podemos activar el estilo CBS_HASSTRINGS, sobre todo si queremos que los ítems se muestren por orden alfabético.

           hctrl = CreateWindowEx(
              0,
              "COMBOBOX"       /* Nombre de la clase */
              "",              /* Texto del título */
              CBS_HASSTRINGS | CBS_OWNERDRAWVARIABLE |
              CBS_DROPDOWNLIST | CBS_SORT |
              WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, /* Estilo */
              9, 19,           /* Posición */
              320, 250,        /* Tamaño */
              hwnd,            /* Ventana padre */
              (HMENU)ID_COMBO, /* Identificador del control */
              hInstance,       /* Instancia */
              NULL);           /* Sin datos de creación de ventana */

Si no activamos el estilo CBS_HASSTRINGS, el valor que usemos al insertar el ítem será almacenado en el dato del ítem de 32 bits.

void IniciarCombo(HWND hctrl)
{
   int i;

   for(i = 0; i < 10; i++)
      SendMessage(hctrl, CB_ADDSTRING, 0, i);
}

Combo box owner-draw de altura fija

La ventana propietaria del control recibirá el mensaje WM_MEASUREITEM cuando el control combo box sea creado.

En el parámetro lParam recibiremos un puntero a una estructura MEASUREITEMSTRUCT que contiene las dimensiones del control.

En el parámetro wParam recibiremos el valor del identificador del control, o lo que es lo mismo, el valor del miembro CtlID de la estructrura MEASUREITEMSTRUCT apuntada por el parámetro lParam. Este valor identifica el control del que procede el mensaje WM_MEASUREITEM.

Tengamos en cuenta que pueden existir varios controles con el estilo owner-draw, y no tienen por qué ser necesariamente del tipo combo box. Si este valor es cero, el mensaje fue enviado por un menú. Si el valor es distinto de cero, el mensaje fue enviado por un combobox o por un listbox.

Nuestra aplicación debe rellenar de forma adecuada la estructura MEASUREITEMSTRUCT apuntada por el parámetro lParam regresar. De este modo se indica al sistema operativo qué dimensiones tiene el control.

El mensaje WM_MEASUREITEM se envía a la ventana propietaria del combo box antes de enviar el mensaje WM_INITDIALOG o WM_CREATE, de modo que en ese momento Windows aún no ha determinado la altura y anchura de la fuente usada en el control.

Si se procesa este mensaje se debe retornar el valor TRUE.

    switch(msg)                  /* manipulador del mensaje */
    {
        case WM_CREATE:
           ...
        case WM_MEASUREITEM:
           lpmis = (LPMEASUREITEMSTRUCT) lParam;
           lpmis->itemHeight = 40;
           return TRUE;
...

Combo box owner-draw de altura variable

En este caso, la ventana propietaria del control recibirá el mensaje WM_MEASUREITEM cada vez que se inserte un nuevo ítem en el control combo box. Esto nos permitirá ajustar la altura de cada ítem con valores diferentes.

El proceso del mensaje es idéntico que con el estilo CBS_OWNERDRAWFIXED. La diferencia es que este mensaje se enviará para cada ítem, y siempre después del mensaje WM_INITDIALOG o WM_CREATE.

Dibujar cada ítem

Tanto en un caso como en el otro, Windows enviará un mensaje cada vez que se inserte un nuevo ítem, cuando el estado de un ítem cambie o cuando un ítem deba ser mostrado.

Esto se hace mediante un mensaje WM_DRAWITEM. En el parámetro wParam recibiremos el identificador del control del que procede el mensaje, o cero si es un menú. En el parámetro lParam recibiremos un puntero a una estructura DRAWITEMSTRUCT, que contiene toda la información relativa al ítem que hay que mostrar.

Si se procesa este mensaje hay que retornar el valor TRUE.

Procesar este mensaje puede ser un proceso bastante complejo, ya que el estado de un ítem puede tomar varios valores diferentes, y seguramente, cuando decidimos crear un control owner-draw es porque queremos hacer algo especial.

La estructura DRAWITEMSTRUCT tiene esta forma:

typedef struct tagDRAWITEMSTRUCT {  // dis  
    UINT  CtlType; 
    UINT  CtlID; 
    UINT  itemID; 
    UINT  itemAction; 
    UINT  itemState; 
    HWND  hwndItem; 
    HDC   hDC; 
    RECT  rcItem; 
    DWORD itemData; 
} DRAWITEMSTRUCT;

En nuestro caso, CtlType tendrá el valor ODT_COMBOBOX, pero tengamos en cuenta que habrá que discriminar este miembro si tenemos controles owner-draw de distintos tipos.

CtlID contiene el identificador del control, igual que el parámetro wParam.

itemID contiene el índice del ítem . Si el combo box está vacío, el valor será -1.

itemAction puede tener tres valores diferentes, que en ocasiones requerirán un tratamiento distinto por parte de nuestro programa:

  • ODA_DRAWENTIRE indica que el ítem debe ser dibujado por entero.
  • ODA_FOCUS indica que el control ha perdido o recuperado el foco. Para saber si se trata de uno u otro caso se debe comprobar el miembro itemState.
  • ODA_SELECT indica que el estado de selección del ítem ha cambiado. Para saber si el ítem está ahora seleccionado o no también se debe comprobar el miembro itemState.

itemState indica el estado del ítem. El valor puede ser uno o una combinación de los siguientes:

  • ODS_COMBOBOXEDIT se está dibujando el campo de selección (control edit) del combo box owner drawn.
  • ODS_DEFAULT se trata del ítem por defecto.
  • ODS_DISABLED el ítem está deshabilitado.
  • ODS_FOCUS el ítem tiene el foco.
  • ODS_SELECTED el ítem está seleccionado.

hwndItem contiene el manipulador de ventana del control.

hDC contiene el manipulador de contexto de dispositivo del control. Este valor nos será muy útil, ya que el proceso de este mensaje será en encargado de dibujar el ítem.

rcItem contiene un rectángulo que define el contorno del ítem que estamos dibujando. Además este rectángulo define una región de recorte, de modo que no podremos dibujar nada fuera de él.

itemData contiene el valor del 32 bits asociado al ítem.

Con esto tenemos toda la información necesaria para dibujar cada ítem, y nuestro programa será el responsable de diferenciar los distintos estados de cada uno.

        case WM_DRAWITEM:
           lpdis = (LPDRAWITEMSTRUCT) lParam;
           if(lpdis->itemID == -1) {  /* Se trata de un menú, no hacer nada */
              break;
           }
           switch (lpdis->itemAction) {
             case ODA_SELECT:
             case ODA_DRAWENTIRE:
             case ODA_FOCUS:
               /* Borrar el contenido previo */
               FillRect(lpdis->hDC, &lpdis->rcItem, (HBRUSH)(COLOR_WINDOW+1));
               /* Obtener datos de las medidas de la fuente */
               GetTextMetrics(lpdis->hDC, &tm);
               /* Calcular la coordenada y para escribir el texto de ítem */
               y = (lpdis->rcItem.bottom + lpdis->rcItem.top - tm.tmHeight) / 2;
               /* Cada tipo de comida se muestra en un color diferente */
               if(comida[lpdis->itemData].tipo == 'p')
                  SetTextColor(lpdis->hDC, RGB(0,128,0));
               else
               if(comida[lpdis->itemData].tipo == 'c')
                  SetTextColor(lpdis->hDC, RGB(0,0,255));
               else
               if(comida[lpdis->itemData].tipo == 'b')
                  SetTextColor(lpdis->hDC, RGB(255,0,0));
               /* Mostrar el icono */
               icono = LoadIcon(hInstance, MAKEINTRESOURCE(Icono+lpdis->itemData));
               DrawIcon(lpdis->hDC, 4, lpdis->rcItem.top+2, icono);
               DeleteObject(icono);
               /* Mostrar el texto */
               TextOut(lpdis->hDC, 42, y, 
                  comida[lpdis->itemData].nombre, 
                  strlen(comida[lpdis->itemData].nombre));
               /* Si el ítem está seleccionado, trazar un rectángulo negro alrededor */
               if (lpdis->itemState & ODS_SELECTED) {
                 SetTextColor(lpdis->hDC, RGB(0,0,0));
                 DrawFocusRect(lpdis->hDC, &lpdis->rcItem);
               }
           }
           break;
...

Este ejemplo usa la lista de países de ejemplos anteriores, hemos hecho que los países de más de 92391 km2 se muestren en color verde, y el resto en azul.

Por supuesto, esta es una aplicación muy sencilla de un combo box owner-draw. Es posible personalizar tanto como queramos estos controles, mostrando mapas de bits o cualquier gráfico que queramos.

Otros mensajes para combo box con estilos owner-draw

Disponemos de otros mensajes destinados a controles owner-draw.

El mensaje CB_GETITEMHEIGHT se puede usar para obtener la altura de los ítems en un combo box owner-draw. Si el control tiene el estilo CBS_OWNERDRAWFIXED tanto el parámetro lParam como wParam deben ser cero. Si el control tiene el estilo CBS_OWNERDRAWVARIABLE, el parámetro wParam debe contener el índice del ítem cuya altura queramos recuperar.

De forma simétrica, disponemos del mensaje CB_SETITEMHEIGHT para ajustar la altura de los ítems. Si se trata de un control con el estilo CBS_OWNERDRAWFIXED debe indicarse cero para el parámetro wParam, y la altura se especifica en el parámetro lParam, para lo que será necesario usar la macro MAKELPARAM:

SendMessage(hctrl, CB_SETITEMHEIGHT, 0, MAKELPARAM(23, 0));

Si se trata de un control con el estilo CBS_OWNERDRAWVARIABLE procederemos del mismo modo, pero indicando en el parámetro wParam el índice del ítem cuya altura queremos modificar.

El mensaje WM_DELETEITEM

Cuando se elimina un ítem de un combo box cuyo dato de ítem no sea nulo, en Windows 95; o para ítems pertenecientes a controles owner draw, en el caso de Windows NT, el sistema envía un mensaje WM_DELETEITEM al procedimiento de ventana de la ventana propietaria del control. Concretamente, esto ocurre cuando se usan los mensajes CB_DELETESTRING o CB_RESETCONTENT o cuando el propio control es destruído.

Esto nos da una oportunidad de tomar ciertas decisiones o realizar ciertas tareas cuando algunos ítems concretos son eliminados.

En el parámetro wParam recibiremos el identificador del control en el que se ha eliminado el ítem. En el parámetro lParam recibiremos un puntero a una estructura DELETEITEMSTRUCT. Esta estructura está definida como:

typedef struct tagDELETEITEMSTRUCT { // ditms  
    UINT CtlType; 
    UINT CtlID; 
    UINT itemID; 
    HWND hwndItem; 
    UINT itemData; 
} DELETEITEMSTRUCT;

CtlType contiene el valor ODT_COMBOBOX.

CtlId contiene el valor del identificador del control.

itemID el valor del índice del ítem eliminado.

hwndItem el manipulador de ventana del control.

itemData el dato del ítem asignado al ítem eliminado.

Dimensiones de la lista desplegable

Ya sólo quedan por comentar dos mensajes más relacionados con los combo boxes.

Los dos están relacionados con el tamaño de la lista desplegable, uno de ellos nos premite obtener el rectángulo correspondiente a esa lista, CB_GETDROPPEDCONTROLRECT, el parámetro wParam no se usa, y debe ser cero, el parámetro lParam se usa para pasar un puntero a una estructura RECT en la que se nos devolverán las coordenadas que definen ese rectángulo.

El otro mensaje es CB_SETDROPPEDWIDTH, que nos permite modificar la anchura de la lista. La nueva anchura se especifica mediante el parámetro wParam, el parámetro lParam no se usa, y debe ser cero.

Definición del orden

Por último, cuando un control combo box tiene el estilo CBS_SORT, el procedimiento de ventana de la ventana propietaria del control recibe uno o varios mensajes WM_COMPAREITEM para determinar la posición de cada nuevo ítem insertado en el control.

Esto nos permite definir nuestro propio orden para los ítems en el control, en lugar de usar el orden alfabético por defecto.

El mensaje se puede recibir varias veces para cada ítem insertado, ya que generalmente no será suficiente una comparación para determinar el orden.

En el parámetro wParam recibiremos el identificador del control, y en lParam un puntero a una estructura COMPAREITEMSTRUCT, con todos los datos necesarios para determinar el orden entre dos ítems del combo box. Esta estructura tiene esta definición:

typedef struct tagCOMPAREITEMSTRUCT { // cis  
    UINT  CtlType; 
    UINT  CtlID; 
    HWND  hwndItem; 
    UINT  itemID1; 
    DWORD itemData1; 
    UINT  itemID2; 
    DWORD itemData2; 
} COMPAREITEMSTRUCT;

CtlType contendrá el valor ODT_COMBOBOX.

CtlID el valor del identificador del control.

hwndItem el manipulador de ventana del control.

itemID1 el índice del primer ítem a comparar.

itemData1 el valor del dato del ítem del primer ítem a comparar.

itemID2 el índice del segundo ítem a comparar.

itemData2 el valor del dato del ítem del segundo ítem a comarar.

El valor de retorno debe ser -1, 0 ó 1, dependiendo de si el primer ítem precede al segundo en el orden establecido, si son iguales o si el segundo precede al primero, respectivamente.

Por ejemplo, si para nuestra aplicación establecemos que el orden depende del valor del dato del ítem, de menor a mayor, devolveremos -1 si itemData1 es menor que itemData2, 0 si son iguales y 1 si el valor de itemData1 es mayor que itemData2.

O como en el ejemplo 74, hemos definido un orden para mostrar las comidas según su tipo, primero las comidas, después los postres y al final las bebidas. Dentro de cada tipo se aplica el orden alfabético:

        case WM_COMPAREITEM:
           lpcis = (LPCOMPAREITEMSTRUCT) lParam;
           /* Establecer un orden: 
              1) Por tipos, primero comidas, después postres y último bebidas
              2) Dentro de cada tipo, usar el orden alfabético */
           if(comida[lpcis->itemData1].tipo == comida[lpcis->itemData2].tipo) 
              return strcmp(comida[lpcis->itemData1].nombre, comida[lpcis->itemData2].nombre);
           else if(comida[lpcis->itemData1].tipo == 'c') return -1;
           else if(comida[lpcis->itemData1].tipo == 'b') return 1;
           else if(comida[lpcis->itemData2].tipo == 'c') return 1;
           else return -1;
           break;

Ejemplo 74


  Nombre Fichero Fecha Tamaño Contador Descarga
D Ejemplo 74 win074.zip 2007-03-15 12642 bytes 112