List box a medida (owner-draw)

Al crear cualquier control casi siempre se puede especificar el estilo owner-draw, que quiere decir que la ventana propietaria del control es la responsable del trazado gráfico del control, en lugar de ser el propio procedimiento de ventana del control en que se encargue de esa tarea, como hemos hecho hasta ahora en todos los casos.

Esto deja la responsabilidad de todos los aspectos gráficos a nuestra aplicación, y en concreto, a nuestro procedimiento de ventana o diálogo.

En relidad, actuar de este modo nos complica la vida, pero a cambio, nos da mucho mayor control sobre el aspecto gráfico de las ventanas, y si nos interesa personalizar nuestras aplicaciones, tendremos que recurrir a este estilo.

Estilos owner-draw para list box

Existen dos estilos distintos owner-draw que se pueden aplicar a los controles list box LBS_OWNERDRAWFIXED y LBS_OWNERDRAWVARIABLE.

El primero define controles list 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.

Recordarás que en el punto anterior comentamos que los controles list box con estilos owner-draw no tienen activado por defecto el estilo LBS_HASSTRINGS.

Esto es, en cierta medida, bastante lógico, ya que intentamos personalizar el aspecto del control, por lo que es probable que éste no contenga cadenas, o al menos, no sólo cadenas.

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

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

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

void IniciarLista(HWND hctrl)
{
   int i;

   for(i = 0; i < 22; i++)
      SendMessage(hctrl, LB_ADDSTRING, 0, i);
}

List box owner-draw de altura fija

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

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

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 list-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 list 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 = 20;
           return TRUE;
...

List 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 list box. Esto nos permitirá ajustar la altura de cada ítem con valores diferentes.

El proceso del mensaje es idéntico que con el estilo LBS_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_LISTBOX, 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 list 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_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;
               /* Los países cuya superficie sea mayor que 92391 km2 se muestran en verde,
                  el resto, en azul */
               if(paises[lpdis->itemData].Superficie > 92391)
                  SetTextColor(lpdis->hDC, RGB(0,128,0));
               else
                  SetTextColor(lpdis->hDC, RGB(0,0,255));
               /* Mostrar el texto */
               TextOut(lpdis->hDC, 6, y, 
                  paises[lpdis->itemData].Nombre, 
                  strlen(paises[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 list box owner-draw. Es posible personalizar tanto como queramos estos controles, mostrando mapas de bits o cualquier gráfico que queramos.

También hemos hecho uso de una función nueva: DrawFocusRect. Esta función sirve para trazar un rectángulo que indique que el ítem tiene el foco. Este rectángulo se traza usando el modo XOR, por lo que dos llamadas consecutivas para el mismo rectángulo eliminan la marca.

El mensaje WM_DELETEITEM

Cuando se elimina un ítem de un list 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 LB_DELETESTRING o LB_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_LISTBOX.

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.

Ejemplo 67


  Nombre Fichero Fecha Tamaño Contador Descarga
D Ejemplo 67 win067.zip 2007-03-15 100628 bytes 129

Otros mensajes para list box con estilos owner-draw

Disponemos de otros mensajes destinados a controles owner-draw.

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

De forma simétrica, disponemos del mensaje LB_SETITEMHEIGHT para ajustar la altura de los ítems. Si se trata de un control con el estilo LBS_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, LB_SETITEMHEIGHT, 0, MAKELPARAM(23, 0));

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

Definición del orden

Por último, cuando un control list box tiene el estilo LBS_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 list 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_LISTBOX.

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.