Capítulo 40 Control List box avanzado

Insertar controles list box durante la ejecución

Al igual que vimos con los controles edit, también es posible insertar controles list box durante la ejecución. En el caso del control list box tendremos que insertar una ventana de la clase "LISTBOX". Para insertar el control también usaremos las funciones CreateWindow y CreateWindowEx.

    HWND hctrl;
...
        case WM_CREATE:
           hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
           /* Insertar control Edit */
           hctrl = CreateWindowEx(
              0,
              "LISTBOX",       /* Nombre de la clase */
              "",              /* Texto del título, no tiene */
              LBS_STANDARD | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, /* Estilo */
              9, 19,           /* Posición */
              104, 99,         /* Tamaño */
              hwnd,            /* Ventana padre */
              (HMENU)ID_LISTA, /* Identificador del control */
              hInstance,       /* Instancia */
              NULL);           /* Sin datos de creación de ventana */
           /* Inicialización de los datos de la aplicación */
           SendMessage(hctrl, LB_ADDSTRING, 0, (LPARAM)"Cadena nº 1");
           SendMessage(hctrl, LB_ADDSTRING, 0, (LPARAM)"Cadena nº 4");
           SendMessage(hctrl, LB_ADDSTRING, 0, (LPARAM)"Cadena nº 3");
           SendMessage(hctrl, LB_ADDSTRING, 0, (LPARAM)"Cadena nº 2");
           SendMessage(hctrl, LB_SELECTSTRING, (UINT)-1, (LPARAM)Datos.Item);
           SetFocus(hctrl);
           return 0;

Como vemos, usamos los mismos valores que en el fichero de recursos: identificador, clase de ventana (en este caso "LISTBOX"), estilo, posición y dimensiones.

Al igual que en el caso del control edit, el identificador del control se suministra a través del parámetro hMenu, por lo que será necesario hacer un casting del valor del identificador a HMENU.

Ahora será nuestro procedimiento de ventana, (si el control se ha insertado en una ventana), el encargado de procesar los mensajes procedentes del control. Recordemos que en los ejemplos que hemos visto hasta ahora esto lo hacía el procedimiento de diálogo.

Cambiar la fuente de un control list box

También es posible modificar la fuente de un control list box enviando un mensaje WM_SETFONT. El lugar apropiado es, por supuesto, al procesar el mensaje WM_INITDIALOG cuando se inicie un cuadro de diálogo, o al procesar el mensaje WM_CREATE cuando se inicie una ventana,.

En el parámetro wParam pasamos un manipulador de fuente, y usaremos la macro MAKELPARAM para crear un valor LPARAM, en el que especificaremos la opción de repintar el control, que se almacena en la palabra de menor peso de LPARAM.

Esto nos permite modificar la fuente durante la ejecución, reflejando los cambios en pantalla.

   static HFONT hfont;
...
           hfont = CreateFont(24, 0, 0, 0, 300,
              FALSE, FALSE, FALSE, DEFAULT_CHARSET,
              OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
              PROOF_QUALITY, DEFAULT_PITCH | FF_ROMAN,
              "Times New Roman");
           SendMessage(hctrl, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));
...
        case WM_DESTROY:
           DeleteObject(hfont);
...

En el caso de crear una fuente especial para nuestros controles, debemos recordar destruirla cuando ya no sea necesaria, generalmente al destruir la ventana.

Por supuesto, también podemos usar una fuente de stock:

           hfont = (HFONT)GetStockObject( DEFAULT_GUI_FONT );
           SendMessage(hctrl, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));

Cambiar los colores de un control list box

Para terminar, también podemos personalizar más nuestros controles list box, cambiando los colores del texto y del fondo. Para ello deberemos procesar el mensaje WM_CTLCOLORLISTBOX.

Este mensaje se envía a la ventana padre del control justo antes de que el sistema lo vaya a dibujar, y nos permite cambiar los colores del texto y fondo. Para ello nos suministra en el parámetro wParam un manipulador del contexto de dispositivo del control, y en lParam un manipulador del control, mediante el cual podemos saber a qué control concreto se refiere el mensaje.

El valor de retorno, si se procesa este mensaje, debe ser un manipulador de pincel con el color de fondo del control.

    static HBRUSH pincel;
    HWND hcrtl;

    switch (msg)                  /* manipulador del mensaje */
    {
        case WM_CREATE:
           hcrtl = CreateWindowEx(...);
           pincel = CreateSolidBrush(RGB(0,255,0));
           SetFocus(hcrtl);
           return 0;
        case WM_CTLCOLORLISTBOX:
           SetBkColor((HDC)wParam, RGB(0,255,0));
           SetTextColor((HDC)wParam, RGB(255,255,255));
           return (LRESULT)pincel;
        case WM_DESTROY:
           DeleteObject(pincel);
           PostQuitMessage(0);    /* envía un mensaje WM_QUIT a la cola de mensajes */
           break;
...

Ejemplo 56

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 56 win056.zip 2007-03-15 3135 bytes 60

Mensajes de notificación

Los list box también envían mensajes de notificación para informar sobre determinados eventos.

Los mensajes de notificación se reciben a través de un mensaje WM_COMMAND. En la palabra de menor peso del parámetro wParam se envía el identificador del control. El manipulador del control se envía en el parámetro lParam y el código del mensaje de notificación en la palabra de mayor peso de wParam.

Nota:

En el API de Windows 3.x el código del mensaje de notificación se envía en el parámetro lParam. Hay que tener esto en cuenta si se intenta portar código entre estas plataformas.

Veamos a continuación los mensajes de notificación que existen para los controles list box:

Doble clic

Cada vez que el usuario hace doble clic sobre uno de los ítems de un list box, se envía un mensaje de notificación LBN_DBLCLK a la ventana padre.

Falta espacio

Si no es posible conseguir memoria para completar una operación sobre el list box, se envía un mensaje de notificación LBN_ERRSPACE.

Pérdida y recuperación de foco

Cada vez que el usuario selecciona otro control se envía un mensaje de notificación LBN_KILLFOCUS.

Cuando el usuario selecciona un control list box, se envía un mensaje de notificación LBN_SETFOCUS.

Selección y deselección

Cada vez que la selección de un list box se modifique se envía un mensaje LBN_SELCHANGE.

Cuando el usuario cancela la selección de un ítem, se envía el mensaje LBN_SELCANCEL.

   /* Respuesta a mensaje de notificación de cambio de selección:
    Si la selección cambia se actualiza la ventana. */
        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case ID_LISTA:
                 switch(HIWORD(wParam)) {
                   case LBN_SELCHANGE:
                      InvalidateRect(hwnd, NULL, TRUE);
                      break;
                 }
                 break;

Mensajes más comunes

En el capítulo 8 ya vimos algunos de los mensajes de uso más frecuente en list box, los repasaremos ahora y comentaremos algunos más:

Entre los conocidos, tenemos los siguientes mensajes:

LB_ADDSTRING para añadir cadenas a un list box, la dirección de la cadena se envía en el parámetro lParam.

   sprintf(cad, "NUEVA CADENA");
   SendMessage(hctrl, LB_ADDSTRING, 0, (LPARAM)cad);

El mensaje LB_SELECTSTRING sirve para seleccionar una cadena determinada. En el parámetro wParam se envía el índice en que debe comenzar la búsqueda y en lParam la dirección de la cadena a buscar.

   /* Seleccionar primera cadena que empiece por "E", después del 6º ítem */
   int i=6;
...
   SendMessage(hctrl, LB_SELECTSTRING, (WPARAM)i, (LPARAM)"E");

Para obtener el índice del ítem actualmente seleccionado se usa el mensaje LB_GETCURSEL. Este mensaje no precisa parámetros.

   int i;
...
   i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);

Los mensajes LB_GETTEXT y LB_GETTEXTLEN nos sirven para leer cadenas desde un list box. Para el primero se indica en el parámetro wParam el índice del ítem a recuperar, y en lParam la dirección del buffer donde se lee la cadena. El segundo mensaje nos sirve para obtener la longitud de la cadena de un ítem, indicado mendiante su índice en el parámetro wParam.

   /* Obtener cadena seleccionada */
   int i, l;
   char *cad;
...
   i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
   l = SendMessage(hctrl, LB_GETTEXTLEN, (WPARAM)i, (LPARAM)cad);
   cad = (char*)malloc(l+1);
   SendMessage(hctrl, LB_GETTEXT, (WPARAM)i, (LPARAM)cad);

Pero existen otros mensajes que suelen ser muy útiles a la hora de usar list boxes:

Por ejemplo, podemos eliminar líneas mediante el mensaje LB_DELETESTRING, en el que indicaremos en el parámetro wParam el valor del índice a eliminar.

   /* Eliminar cadena actualmente seleccionada */
   int i;
...
   i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
   SendMessage(hctrl, LB_DELETESTRING, (WPARAM)i, 0);

El mensaje LB_FINDSTRING nos permite buscar una cadena que coincida con el prefijo especificado en el parámetro lParam, a partir del índice indicado en wParam.

   /* Seleccionar siguiente cadena, a partir de la
    seleccionada actualmente, que empiece por "CO" */
   int i;
...
   i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
   i = SendMessage(hctrl, LB_FINDSTRING, (WPARAM)i, (LPARAM)"CO");
   SendMessage(hctrl, LB_SETCURSEL, (WPARAM)i, 0);

El mensaje LB_FINDSTRINGEXACT es parecido, pero no usa el parámetro lParam como un prefijo, sino que busca una cadena que coincida exactamente con ese parámetro.

   /* Seleccionar la cadena igual a "Portugal" */
   int i;
...
   i = SendMessage(hctrl, LB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)"Portugal");
   SendMessage(hctrl, LB_SETCURSEL, (WPARAM)i, 0);

El mensaje LB_GETCOUNT no tiene parámetros, y sirve para obtener el número de elementos que contiene un list box.

   /* Obtener número de ítems */
   int i;
...
   i = SendMessage(hctrl, LB_GETCOUNT, 0, 0);
   sprintf(cad, "Número de ítems: %d", i);
   MessageBox(hwnd, cad, "List Box", MB_OK);

LB_GETTOPINDEX tampoco tiene parámetros, y sirve para recuperar el índice del primer ítem visible de un list box.

   /* Indice de primer ítem visible */
   int i;
...
   i = SendMessage(hctrl, LB_GETTOPINDEX, 0, 0);
   sprintf(cad, "Pimer ítem visible: %d", i);
   MessageBox(hwnd, cad, "List Box", MB_OK);

LB_INSERTSTRING nos permite insertar un ítem en una posición determinada por el valor del parámetro wParam, y con el texto indicado en lParam. Esta inserción se hace en la posición indicada, aunque la lista tenga el estilo LBS_SORT.

   /* Insertar un ítem antes del seleccionado actualmente */
   int i;
...
   sprintf(cad, "CADENA INSERTADA");
   i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
   SendMessage(hctrl, LB_INSERTSTRING, (WPARAM)i, (LPARAM)cad);

Mediante el mensaje LB_RESETCONTENT, sin parámetros, podemos vaciar un list box por completo.

   /* Vaciar list box */
   SendMessage(hctrl, LB_RESETCONTENT, 0, 0);

El mensaje LB_SETCURSEL nos permite seleccionar un ítem, indicado en el parámetro wParam. Además, se elimina la selección previa, y el contenido del list box se desplaza, si es necesario, para mostrar la nueva cadena seleccionada.

   /* Seleccionar ítem siguiente al actual */
   int i;
...
   i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
   SendMessage(hctrl, LB_SETCURSEL, (WPARAM)i+1, 0);

Si queremos asegurar que un ítem determinado será visible, sin seleccionarlo, podemos usar el mensaje LB_SETTOPINDEX, indicando en el parámetro wParam el índice del ítem a visualizar.

   /* Asegurarse de que el ítem 18 es visible */
   SendMessage(hctrl, LB_SETTOPINDEX, (WPARAM)18, 0);

Ejemplo 57

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 57 win057.zip 2007-03-15 3854 bytes 53

El dato del ítem

En todos los ejemplos que hemos visto siempre hemos recuperado cadenas de un list box, pero también podemos trabajar con índices. Por ejemplo, crearemos un programa que nos muestre las capitales y superficies de varios países, que podremos seleccionar de un list box. Para ello almacenaremos esos datos en un array:

struct Pais {
   char *Nombre;
   char *Capital;
   int Superficie;
} paises[22] =
   {
      "Argentina", "Buenos Aires", 2766890,
      "Mexico", "Mexico DC", 1972550,
      "Brasil", "Brasilia", 8514876,
      "Peru", "Lima", 1285220,
      "Colombia", "Bogotá", 1138910,
      "Bolivia", "La Paz", 1098580,
      "Venezuela", "Caracas", 912050,
      "Chile", "Santiago", 756096,
      "España", "Madrid", 504782,
      "Paraguay", "Asunción", 406750,
      "Ecuador", "Quito", 283560,
      "Uruguay", "Montevideo", 176220,
      "Nicaragua", "Managua", 129494,
      "Honduras", "Tegucigalpa", 112090,
      "Cuba", "La Habana", 110860,
      "Guatemala", "Guatemala", 108890,
      "Portugal", "Lisboa", 92391,
      "Panamá", "Panamá", 78200,
      "Costa Rica", "San José", 51100,
      "República Dominicana", "Santo Domingo", 48730,
      "El Salvador", "San Salvador", 21040,
      "Puerto Rico", "San Juan", 9104
   };

Generalmente trabajaremos con list boxes definidos con el estilo LBS_STANDARD, lo cual implica, entre otras cosas, que las cadenas se muestran por orden alfabético.

En nuestro ejemplo esto plantea un problema. El array está ordenado por superficies, no alfabéticamente, por lo tanto, una vez insertadas las cadenas, los índices de los ítems en el list box no coincidirán con los índices en el array, lo cual sería muy útil, ya que podríamos recuperar el índice del ítem activo y mostrar la información correspondiente a ese índice en el array.

Para evitar esto, por supuesto, podemos eliminar el estilo LBS_SORT, con lo que el orden del list box coincidiría con el del array. Pero esto no nos interesa, ya que complica la tarea del usuario, que debe buscar en un list box aparentemente desordenado.

Otra forma de evitarlo es leer la cadena seleccionada y buscarla en el array de forma secuencial. Sin embargo, esta solución no es muy elegante, y sería francamente mala si la lista contiene muchos elementos.

También podemos ordenar el array alfabéticamente, pero esta solución tampoco es satisfactoria, ya que podría haber errores de orden, o sencillamente, podríamos usar el mismo array para crear un list box de capitales.

El API nos permite usar otra solución. Ya sabemos que cada ítem tiene asociado un índice y una cadena. Pero también tiene asociado un dato entero de 32 bits: el ítem data, o dato de ítem.

A cada ítem le podemos asignar un valor entero mediante el mensaje LB_SETITEMDATA, y recuperarlo mediante LB_GETITEMDATA.

Podemos aprovechar que el valor de retorno del mensaje LB_ADDSTRING es el índice del ítem insertado, y usar ese valor en el mensaje LB_SETITEMDATA, para asignar el valor del índice en el array:

void IniciarLista(HWND hctrl)
{
   int i;
   int actual;

   for(i = 0; i < 22; i++) {
      actual = SendMessage(hctrl, LB_ADDSTRING, 0, (LPARAM)paises[i].Nombre);
      SendMessage(hctrl, LB_SETITEMDATA, (WPARAM)actual, i);
   }
}

De este modo, podremos recuperar los datos del array correspondientes a un ítem:

        case WM_PAINT:
           i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
           i = SendMessage(hctrl, LB_GETITEMDATA, (WPARAM)i, 0);
           hdc = BeginPaint(hwnd, &ps);
           SetBkMode(hdc, TRANSPARENT);
           sprintf(cad, "País: %s", paises[i].Nombre);
           TextOut(hdc, 300, 20, cad, strlen(cad));
           sprintf(cad, "Capital: %s", paises[i].Capital);
           TextOut(hdc, 300, 40, cad, strlen(cad));
           sprintf(cad, "Superficie: %d Km²", paises[i].Superficie);
           TextOut(hdc, 300, 60, cad, strlen(cad));
           EndPaint(hwnd, &ps);
           break;

Ya veremos que el dato de ítem tiene otras utilidades, pero en muchos casos nos proporciona una forma útil de almacenar un dato relativo a un ítem. Al tratarse de un entero de 32 bits también puede contener punteros.

Ejemplo 58

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 58 win058.zip 2007-03-15 3635 bytes 60

Funciones para ficheros y directorios

Una de las aplicaciones más frecuentes de los list box es la elección de ficheros. Por ese motivo, el API proporciona algunas funciones para iniciar y seleccionar ítems en un list box a partir de los datos de directorios.

La función DlgDirList nos permite iniciar el contenido de un list box a partir de los ficheros, carpetas, unidades de disco, etc.

Esta función necesita cinco parámetros. El primero es un manipulador de la ventana o diálogo que contiene el list box que vamos a inicializar. El segundo es un puntero a una cadena con el camino del directorio a mostrar. Esta cadena tiene que tener espacio suficiente, ya que la función puede modificar su contenido. El tercer parámetro es el identificador del list box. El cuarto el identificador de un control estático, que se usa para mostrar el camino actualmente mostrado en el list box. El último parámetro nos permite seleccionar el tipo de entradas que se mostrarán en el list box.

Mediante este último parámetro podemos restringir el tipo de entradas, impidiendo o permitiendo que se muestren directorios o unidades de almacenamiento, o limitando los atributos de los ficheros y directorios a mostrar.

Ya hemos dicho que se necesita un control estático. Como aún no hemos visto el modo de insertar estos controles directamente en la ventana, para este ejemplo lo haremos sin más explicaciones, aunque como se puede ver, no tiene nada de raro:

   HWND hestatico;
...
           hestatico = CreateWindowEx(
              0,
              "STATIC",        /* Nombre de la clase */
              "",              /* Texto del título, no tiene */
              WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, /* Estilo */
              9, 4,            /* Posición */
              344, 18,         /* Tamaño */
              hwnd,            /* Ventana padre */
              (HMENU)ID_TITULO,/* Identificador del control */
              hInstance,       /* Instancia */
              NULL);           /* Sin datos de creación de ventana */
           SendMessage(hestatico, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));

Por supuesto, podemos usar los comodines '*' y '?' para los nombres de fichero.

Veamos un ejemplo para uniciar un list box a partir del directorio actual, mostrando los discos y directorios, y limitando los ficheros a los que se ajusten a *.c:

...
   IniciarLista(hwnd, "*.c");
...

void IniciarLista(HWND hwnd, char* p)
{
   char path[512];

   strcpy(path, p);

   DlgDirList(
    hwnd,           /* manipulador de cuadro de diálogo con list box  */
    path,           /* puntero a cadena de camino o nombre de fichero */
    ID_LISTA,       /* identificador de list box                      */
    ID_TITULO,      /* identificador de control estático              */
    DDL_DIRECTORY | DDL_DRIVES /* atributos de ficheros a mostrar     */
   );
}

La función DlgDirSelectEx nos permite leer la selección actual de una lista inicializada mediante la función DlgDirList. Si el valor de retorno de esta función es distinto de cero, la selección actual es un directorio o unidad de almacenamiento, por lo que será posible hacer un cambio de directorio. Si el valor de retorno es cero, se trata de un fichero.

Aprovecharemos esto para navegar a lo largo de los discos de nuestro ordenador, para lo que responderemos al mensaje de notificación LBN_DBLCLK, cambiando a la nueva ubicación o mostrando el nombre del fichero seleccionado:

        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case ID_LISTA:
                 switch(HIWORD(wParam)) {
                   case LBN_DBLCLK:
                       if(DlgDirSelectEx(hwnd, cad, 512, ID_LISTA)) {
                         strcat(cad, "*.c");
                         IniciarLista(hwnd, cad);
                      } else
                      else
                         MessageBox(hwnd, cad, "Fichero seleccionado", MB_OK);
                      break;
...

También existen dos mensajes relacionados con este tema.

El mensaje LB_DIR tiene un uso equivalente a la función DlgDirList. En el parámetro wParam se indican los atributos de los ficheros a mostrar, así como si se deben mostrar directorios y unidades de almacenamiento. En el parámetro lParam se suministra el nombre de fichero, (que puede tener comodines) o el camino de los ficheros a insertar.

Por ejemplo, podemos añadir los ficheros de cabecera al contenido del list box, de modo que se muestren los ficheros fuente en c y los de cabecera:

void IniciarLista(HWND hwnd, char* p)
{
   char path[512];

   strcpy(path, p);

   DlgDirList(hwnd, path, ID_LISTA, ID_TITULO,
      DDL_DIRECTORY | DDL_DRIVES);

   strcpy(path, "*.h");
   SendMessage(GetDlgItem(hwnd, ID_LISTA), LB_DIR,
      (WPARAM)0, (LPARAM)path);

Pero este mensaje no está previsto para usarse junto a la función DlgDirList, ya que tienen objetivos parecidos. En su lugar podemos usar otro mensaje, que nos permite añadir ficheros a una lista previamente creada.

Este otro mensaje, LB_ADDFILE, nos permite añadir ficheros sueltos al contenido del list box. Los ficheros sólo se añaden si existen, y también pueden usar comodines. Este ejemplo añadiría los ficheros ejecutables que existan en el directorio actualmente mostrado:

...
   SendMessage(GetDlgItem(hwnd, ID_LISTA), LB_ADDFILE,
      0, (LPARAM)"*.exe");

Ejemplo 59

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 59 win059.zip 2007-03-15 3117 bytes 51

Listbox de selección sencilla y múltiple

Bien, hasta ahora sólo hemos hablado de list boxes de selección sencilla. En estos list boxes sólo se puede seleccionar un ítem (o ninguno), y cada nueva selección anula a la anterior.

Pero también podemos crear list boxes de selección múltiple, en los que será posible seleccionar rangos de ítems, o varios ítems, aunque no estén seguidos.

Para ello, lo primero será crear el list box con el estilo LBS_MULTIPLESEL o el estilo LBS_EXTENDEDSEL.

Ambos estilos definen listas de selección múltiple. La diferencia está en el modo en que se seleccionan los ítems.

En un list box con el estilo LBS_MULTIPLESEL la selección se hace de manera individual, cada vez que se pulsa sobre un ítem no seleccionado, pasa a estado seleccionado, y viceversa.

En un list box con el estilo LBS_EXTENDEDSEL, las selecciones se comportan de la misma forma que en un list box de selección sencilla. Pero mediante las teclas de mayúculas y de control podemos hacer selecciones múltiples. La tecla de mayúsculas nos permite seleccionar rangos. El último ítem seleccionado se comporta como un "ancla", de modo que si pulsamos la tecla de mayusculas y pulsamos con el ratón sobre otro ítem se seleccionarán todos los ítems entre en "ancla" y el actual.

Por otra parte, la tecla de control permite usar el list box del mismo modo que si tuviese el estilo LBS_MULTIPLESEL. Con la tecla de control pulsada, si pulsamos sobre el ratón en un ítem seleccinado, se deseleccionará, y viceversa.

Selecciones

Disponemos de varios mensajes para tratar la selección de ítems en list boxes de selección múltiple.

El mensaje LB_GETSELCOUNT nos sirve para obtener el número de ítems seleccionados actualmente en un list box:

   int nSeleccionados;
...
   nSeleccionados = SendMessage(hlista, LB_GETSELCOUNT, 0, 0);

Mediante el mensaje LB_GETSELITEMS podemos obtener una lista de los índices de los ítems seleccionados. Para ello indicaremos en el parámetro lParam un puntero a un array de enteros, en los que recibiremos la lista de índices, y en wParam el número máximo de ítems que podemos recuperar:

   int nSeleccionados;
   int *seleccionado;
...
   nSeleccionados = SendMessage(hlista, LB_GETSELCOUNT, 0, 0);
   /* Obtener memoria para nSeleccionados índices */
   seleccionado = (int *)malloc(nSeleccionados*sizeof(int));
   /* Obtener la lista */
   SendMessage(hlista, LB_GETSELITEMS,
      (WPARAM)nSeleccionados, (LPARAM)seleccionado);
   /* Tratamiento */
   for(i = nSeleccionados-1; i >= 0; i--) {
      SendMessage(hlista, LB_GETTEXT, (WPARAM)seleccionado[i], (LPARAM)cad);
   }
   /* Liberar buffer */
   free(seleccionado);

Otra alternativa para averiguar qué ítems están seleccionados consiste en enviar un mensaje LB_GETSEL a cada uno de los ítems. Este mensaje nos devuelve un valor distinto de cero si el ítem está seleccionado, y cero si no lo está. En el parámetro wParam indicaremos el índice del ítem:

   int n, i;
   char cad[64];
...
   n = SendMessage(hlista, LB_GETCOUNT, 0, 0);
   for(i = 0; i < n; i++) {
      if(SendMessage(hlista, LB_GETSEL, (WPARAM)i, 0)) {
         SendMessage(hlista, LB_GETTEXT, (WPARAM)i, (LPARAM)cad);
         MessageBox(hwnd, cad, "Ítem seleccionado", MB_OK);
      }
   }

Finalmente, el mensaje LB_SETSEL nos permite seleccionar o deselecionar un ítem determinado. En el parámetro lParam indicaremos en índice del ítem, en el parámetro wParam especificaremos mediante el valor TRUE que queremos seleccionar el ítem, y mediante FALSE que queremos deselecionarlo:

   int n, i;
   char cad[64];
...
   /* Seleccionar ítems de posiciones pares (con índice impar) */
   n = SendMessage(hlista1, LB_GETCOUNT, 0, 0);
   for(i = 0; i < n; i++) {
      SendMessage(hlista1, LB_SETSEL, (WPARAM)i%2, (LPARAM)i);
   }

También podemos seleccionar rangos de ítems mediante el mensaje LB_SELITEMRANGE. En este caso, en el parámetro wParam también indicaremos el tipo de selección, TRUE para seleccionar y FALSE para deselecionar. En el parámetro lParam indicaremos el rango, en la palabra de menor peso el primer ítem y en la de mayor peso, el último. Para crear un LPARAM usaremos la macro MAKELPARAM:

   int n;
...
   n = SendMessage(hlista1, LB_GETCOUNT, 0, 0);
   SendMessage(hlista1, LB_SELITEMRANGE, (WPARAM)TRUE, MAKELPARAM(0, n));

El mensaje LB_SELITEMRANGEEX es similar, aunque en el parámetro wParam se indica el primer ítem y en el parámetro lParam el último. Si el índice del primero es menor que el del segundo, se seleccionará el rango. Si el mayor es el segundo, el rango se deseleccionará.

Mensajes especiales para list box de selección extendida

Existen ciertos mensajes para manipular el ítem ancla y el ítem actual (caret).

El mensaje LB_GETANCHORINDEX nos permite obtener el ítem ancla, y el mensaje LB_GETCARETINDEX, el ítem actual. En ninguno de los dos mensajes se necesitan parámetros:

   int i;
   char cad[64];
...
   case CM_ANCLA:
     i = SendMessage(hlista2, LB_GETANCHORINDEX, 0, 0);
     SendMessage(hlista2, LB_GETTEXT, (WPARAM)i, (LPARAM)cad);
     MessageBox(hwnd, cad, "Ítem ancla", MB_OK);
     break;
   case CM_CARET:
     i = SendMessage(hlista2, LB_GETCARETINDEX, 0, 0);
     SendMessage(hlista2, LB_GETTEXT, (WPARAM)i, (LPARAM)cad);
     MessageBox(hwnd, cad, "Ítem caret", MB_OK);
     break;
...

De forma simétrica, podemos asignar los ítems ancla y actual mediante los mensajes LB_SETANCHORINDEX y LB_SETCARETINDEX, respectivamente. En ambos casos indicaremos en el parámetro wParam el valor del íncide en cuestión, (aunque ignoro qué utilidad puede tener esto):

   int n;
...
   n = SendMessage(hlista2, LB_GETCOUNT, 0, 0);
   SendMessage(hlista2, LB_SETANCHORINDEX, 0, 0);
   SendMessage(hlista2, LB_SETCARETINDEX, (WPARAM)n, 0);

Ejemplo 60

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 60 win060.zip 2007-03-15 3801 bytes 49

List box sin selección

Además de los list box de selección sencilla y de selección múltiple, también es posible crear list boxes sin selección. Para ello bastará con crear el list box con el estilo LBS_NOSEL.

Podemos usar estos list boxes para mostrar listas de valores para una consulta por parte del usuario, pero que no precisen una selección.

List box multicolumna

Los ítems en un list box no tienen por qué mostrarse en una única columna, como hemos hecho en los ejemplos anteriores. Si se especifica el estilo LBS_MULTICOLUMN se aprovechará toda la anchura del list box para mostrar varias columnas de ítems.

En los list boxes de varias columnas se aprovecha toda la altura del control para mostrar tantos ítems como sea posible, y el resto se muestran en otras columnas. Se añadirán tantas columnas como sea necesario, y será posible desplazarse horizontalmente, aunque para poder usar la barra de desplazamiento horizontal habrá que especificar el estilo WS_HSCROLL al crear el control. Las columnas que no quepan en el área del list box no serán visibles pero al desplazarnos lateralmente se mostrán nuevas columnas y se ocultarán por el lado contrario.

Disponemos de un mensaje para establecer la anchura de las columnas, se trata de LB_SETCOLUMNWIDTH. En el parámetro wParam indicaremos la anchura de las columnas, en pixels:

SendMessage(hctrl, LB_SETCOLUMNWIDTH, (WPARAM)40, 0);

Ejemplo 61

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 61 win061.zip 2007-03-15 3039 bytes 59

Paradas de tabulación

En principio no es posible crear list boxes en los que a cada fila corresponda un ítem, y que para cada ítem se creen varias columnas con informaciones diferentes. (Esto es algo que se hace con otro tipo de control que veremos más adelante: los list view).

Sin embargo, existe una forma limitada de hacer algo parecido. Consiste en usar el estilo LBS_USETABSTOPS, y en separar las columnas dentro de cada ítem con caracteres de tabulación. Si no se especifica este estilo, los caracteres de tabulación no se expanden en espacios, y el list box no tendrá el aspecto de una tabla.

Podemos definir la anchura de cada columna mediante el mensaje LB_SETTABSTOPS, indicando en el parámetro wParam el número de paradas de tabulación y en el parámetro lParam un array con las separaciones de cada parada en pixels:

   int tab[4] = {50,80,130,160};
   int i;
   char cad[128];
...
   for(i = 0; i < 40; i++) {
      sprintf(cad, "Ítem %03d\tcol 2\tcolumna 3\tinfo x\t%d", i, i*213);
      SendMessage(hlista, LB_ADDSTRING, 0, (LPARAM)cad);
   }
...
   SendMessage(hctrl, LB_SETTABSTOPS, (WPARAM)4, (LPARAM)tab);

Ejemplo 62

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 62 win062.zip 2007-03-15 2974 bytes 56

Actualizaciones de gran número de ítems

Hay dos posibles situaciones de potencialmente peligrosas en las las actualizaciones que afecten a muchos ítems en un list box.

Por una parte, el proceso puede requerir una cantidad importante de memoria, cuando se añaden muchos ítems.

Por otra parte, el proceso puede requerir mucho tiempo, ya sea porque se deben añadir muchos ítems o porque se deben hacer muchas modificaciones que impliquen el borrado e inserción de ítems.

Optimizar la memoria

En versiones de Windows anteriores al uso de la memoria virtual, era necesario tener en cuenta la memoria disponible antes de insertar un gran número de ítems en un list box. Para eso se usaba el mensaje LB_INITSTORAGE, en el que indicamos en el parámetro wParam el número de ítems a añadir, y en el parámetro lParam la candidad de memoria estimada necesaria para acomodar esos ítems.

   /* Prepararse para insertar 10000 ítems de
   32 bytes por ítem, aproximadamente */
   SendMessage(hctrl, LB_INITSTORAGE, 10000, 320000);
   IniciarLista(hctrl);

No es necesario ser demasiado preciso con la canditad de memoria requerida, se trata sólo de una estimación, si nos quedamos cortos, los ítems que no quepan se insertarán del modo normal. Si nos quedamos largos, la memoria sobrante se podrá aprovechar en nuevas inserciones.

Este mensaje sólo es necesario en Windows 95, en NT no nos preocupa la memoria necesaria para almacenar los ítems, ya que el modelo de memoria virtual dispone de una cantidad prácticamente ilimitada.

Optimizar el tiempo

El problema del tiempo sí es importante. Cada vez que se añade o elimina un ítem, el list box intenta actualizar la pantalla para reflejar los cambios. Esto, cuando los cambios son muy numerosos, hará que aparentemente la aplicación no responda, y que el tiempo invertido en las actualizaciones sea mayor del necesario.

Para evitar esto podemos hacer uso del estilo LBS_NOREDRAW. Si este estilo está activo no se actualizará la ventana del list box aunque se produzcan cambios. Este estilo se puede activar y desactivar mendiante un mensaje WM_SETREDRAW, indicando en el parámetro wParam el valor TRUE para desactivarlo (activar el redibujado) o FALSE para activarlo (desactivar el redibujado).

   SendMessage(hctrl, WM_SETREDRAW, FALSE, 0);
   IniciarLista(hctrl);
   SendMessage(hctrl, WM_SETREDRAW, TRUE, 0);

Ejemplo 63

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 63 win063.zip 2007-03-15 3057 bytes 50

Responder al teclado

Podemos hacer que nuestro control list box responda a determinadas pulsaciones del teclado, cuando tenga el foco, usando el estilo LBS_WANTKEYBOARDINPUT.

Mediante este estilo, la ventana padre del list box recibirá un mensaje WM_VKEYTOITEM cada vez que el usuario pulse una tecla.

Nuestro procedimiento de ventana o diálogo podrá procesar este mensaje y actuar en consecuencia. En el parámetro wParam recibiremos dos valores, en la palabra de menor peso el código de tecla virtual de la tecla pulsada, y en la palabra de mayor peso, la posición del caret. En el parámetro lParam recibiremos el manipulador del control list box.

En el siguiente ejemplo podemos hacer que nuestro list box responda a la tecla 'B' borrando el ítem actualmente seleccionado, y a la tecla 'I' insertando de nuevo los valores iniciales:

        case WM_VKEYTOITEM:
          if((HWND)lParam == hctrl) { /* Asegurarse de que el mensaje proviene de nuestra lista */
             switch(LOWORD(wParam)) {
               case 'B': /* Borrar actual */
                 i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
                 SendMessage(hctrl, LB_DELETESTRING, (WPARAM)i, 0);
                 SetFocus(hctrl);
                 return -2; /* Tratamiento de tecla terminado */
               case 'I': /* Leer valores iniciales */
                 IniciarLista(hctrl);
                 SetFocus(hctrl);
                 return -2; /* Tratamiento de tecla terminado */
             }
          }
          return -1; /* Acción por defecto */

El valor de retorno es importante. Un valor de -1 indica que el list box debe realizar la acción por defecto definida para la tecla. Esto permite, en nuestro ejemplo, que las teclas del cursor (y en general, todas menos la 'B' y la 'I'), sigan realizando sus funciones normales. Un valor de -2 indica que el list box no tiene que realizar ninguna acción para esta pulsación de tecla. Un valor igual o mayor que 0 se refiere a un índice de un ítem, e indica que el list box debe realizar la acción por defecto con ese índice.

Ejemplo 64

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 64 win064.zip 2007-03-15 3034 bytes 52

Aspectos gráficos del list box

En cuanto al aspecto gráfico del list box tenemos otras opciones que podemos controlar.

Ajustar la anchura de un list box

Por una parte, ya vimos que podemos añadir una barra de desplazamiento horizontal creando nuestro list box con el estilo WS_HSCROLL. Esto lo podemos hacer aunque no se trate de un list box de columnas múltiples. Puede ser útil si la anchura de los ítems sobrepasa la del list box.

Sin embargo, usar este estilo no asegura que la barra de desplazamiento sea mostrada. Para que la barra aparezca hay que ajustar la extensión horizontal del list box mediante un mensaje LB_SETHORIZONTALEXTENT, indicando en el parámetro wParam la nueva extensión horizontal, en pixels.

Si la extensión horizontal es mayor que la anchura del list box, se mostrará la barra de desplazamiento, en caso contrario la barra no aparecerá.

Esto nos plantea una duda, ¿cómo calcular la extensión necesaria según las longitudes de las cadenas contenidas en el list box?

Bueno, podríamos hacerlo a ojo, pero esta técnica es arriesgada, ya que si nos quedamos cortos no será posible visualizar por completo algunos ítems.

Lo mejor es calcular la longitud de cada cadena al insertarla, y si es mayor que la extensión actual, actualizar el valor de la extensión. Para obtener el valor de la extensión actual se usa el mensaje LB_GETHORIZONTALEXTENT.

Claro que esto plantea un problema si se eliminan ítems, ya que nos obligaría a calcular las longitudes de todas las cadenas que quedan en el list box. Sin embargo, podemos ignorar estos casos, y mantener la extensión, ya que la visibilidad de todos los ítems está asegurada.

Para calcular la longitud de una cadena en pixes, vimos en el capítulo 24, que podemos usar la función GetTextExtentPoint32, por ejemplo, en la siguiente función:

int CalculaLongitud(HWND hwnd, char *cad)
{
   HDC hdc;
   SIZE tam;
   HFONT hfont;

   hfont = (HFONT)GetStockObject( DEFAULT_GUI_FONT );
   hdc = GetDC(hwnd);
   SelectObject(hdc, hfont);
   GetTextExtentPoint32(hdc, cad, strlen(cad), &tam);
   /*LPtoDP(hdc, (POINT *)&tam, 1);*/
   ReleaseDC(hwnd, hdc);

   return tam.cx;
}

Para que el cálculo sea correcto debemos seleccionar en el DC la misma fuente que usamos en el list box. Además, habría que tener en cuenta que la función GetTextExtentPoint32 devuelve el tamaño de la cadena en unidades lógicas, y en rigor habría que convertir esos valores a unidades de dispositivo. Esto es innecesario, ya que en un control no se realiza ninguna proyección.

Así, cada vez que insertemos un ítem en el list box, deberemos comprobar si resulta ser el más largo:

   char item[300];
   int x;
   int eActual;

   eActual = SendMessage(hlista, LB_GETHORIZONTALEXTENT, 0, 0);

   strcpy(item, "Ítem de una anchura tal que no cabe en "
        "el list box que hemos definido, o al menos no debería caber, "
        "si las cosas salen tal y como las hemos calculado, claro.");

   x = CalculaLongitud(hlista, cad);
   if(x > eActual) eActual = x;
   SendMessage(hlista, LB_ADDSTRING, 0, (LPARAM)cad);
   SendMessage(hlista, LB_SETHORIZONTALEXTENT, eActual, 0);

Ajustar la altura de los ítems

Por defecto, la altura de los ítems se calcula en función de la fuente asignada al list box. Podemos obtener el valor de la altura del ítem mediante el mensaje LB_GETITEMHEIGHT. Si se trata de un list box con un estilo owner-draw cada ítem puede tener una altura diferente, y se puede especificar el índice del ítem en el parámetro wParam. En los list box normales, el valor de wParam debe ser cero.

   h = SendMessage(hctrl, LB_GETITEMHEIGHT, 0, 0);

Para modificar la altura de un ítem se usa el mensaje LB_SETITEMHEIGHT, en el caso de list boxes con un estilo owner-draw se puede asignar una altura diferente a cada ítem. En ese caso, especificaremos el índice del ítem en el parámetro wParam, y la altura deseada en la palabra de menor peso del parámetro lParam, usando la macro MAKELPARAM. Veremos esto con más detalle al estudiar los estilos owner-draw.

   SendMessage(hctrl, LB_SETITEMHEIGHT, 0, MAKELPARAM(30,0));
   InvalidateRect(hctrl, NULL, TRUE);

Items y coordenadas

Podemos obtener las coordenadas del rectángulo que contiene a un ítem determinado mediante el mensaje LB_GETITEMRECT, indicando en el parámetro wParam el índice del ítem y en el parámetro lParam un puntero a una estructura RECT que recibirá las coordenadas del rectángulo:

   int i;
   RECT re;
...
   i = SendMessage(hctrl, LB_GETCURSEL, 0, 0);
   SendMessage(hctrl, LB_GETITEMRECT, i, (LPARAM)&re);

También podemos obtener el índice del ítem correspondiente a las coordenadas de un punto dentro del list box. Para ello usaremos el mensaje LB_ITEMFROMPOINT, indicando en el parámetro lParam las coordenadas del punto, en la palabra de menor peso la coordenada x y en la de mayor peso, la coordenada y. Usaremos la macro MAKELPARAM para crear el valor del parámetro a partir de las coordenadas:

   int i;
...
   i = SendMessage(hctrl, LB_ITEMFROMPOINT, 0, MAKELPARAM(40, 123));

Ejemplo 65

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 65 win065.zip 2007-03-15 3704 bytes 55

Localizaciones

Ya hemos visto que en los controles list box los ítems se muestran por orden alfabético, al menos en los que hemos usado hasta ahora. Pero el orden alfabético no es algo universal, y puede cambiar dependiendo del idioma.

Generalmente esto no nos preocupará, ya que el idioma usado para elegir el orden se toma del propio sistema. Sin embargo, puede haber casos en que nos interese modificar o conocer el idioma usado en un list box.

Para obtener el valor de la localización actual se usa el mensaje LB_GETLOCALE. El valor de retorno es un entero de 32 bits, en el que la palabra de menor peso contiene el código de país, y el de mayor peso el del lenguaje, este último a su vez, se compone de un identificador de lenguaje primario y un identificador de sublenguaje.

Se pueden usar las macros PRIMARYLANGID y SUBLANGID para obtener el identificador de lenguaje primario y el de sublenguaje, respectivamente.

   int i;
   char cad[120];
...
   i = SendMessage(hctrl, LB_GETLOCALE, 0, 0);
   sprintf(cad, "País %d, id lenguaje primario %d, "
      "id de sublenguaje %d",
      HIWORD(i), PRIMARYLANGID(LOWORD(i)), SUBLANGID(LOWORD(i)));
   MessageBox(hwnd, cad, "Localización", MB_OK);

También podemos modificar la localización actual mediante un mensaje LB_SETLOCALE, indicando en el parámetro wParam el nuevo valor de localización. Podemos crear uno de estos valores mediante las macros MAKELCID y MAKELANGID:

   SendMessage(hctrl, LB_SETLOCALE,
                   MAKELCID(MAKELANGID(LANG_SPANISH, SUBLANG_SPANISH),
                            SSORT_DEFAULT), 0);

La macro MAKELCID crea un identificador de localización a partir de un identificador de lenguaje y una constante que debe ser SSORT_DEFAULT.

La macro MAKELANGID crea un identificador de lenguaje a partir de un identificador de lenguaje primario y de un identificador de sublenguaje.

Ejemplo 66

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 66 win066.zip 2007-03-15 3193 bytes 54

Otros estilos

Nos quedan algunas cosas que comentar sobre los estilos de los controles list box.

Generalmente, hasta ahora, hemos basado nuestros list boxes en el estilo LBS_STANDARD. En realidad, este estilo se define como la combinación de dos estilos: LBS_SORT y LBS_NOTIFY.

El estilo LBS_SORT indica que los ítems en el list box deben mostrarse ordenados alfabéticamente. El estilo LBS_NOTIFY indica que se debe enviar un mensaje de notificación a la ventana padre del control cada vez que el usuario haga clic o boble clic sobre un ítem.

Si no se especifica el estilo LBS_SORT, los ítems se muestran en el mismo orden en que se añaden, (excepto aquellos insertados mediante el mensaje LB_INSERTSTRING).

Si no se especifica el estilo LBS_NOTIFY, la ventana padre no recibirá los mensajes de notificación: LBN_DBLCLK, LBN_SELCHANGE y LBN_SELCANCEL.

Además, aunque no se especifique, todos los controles list box que no tengan un estilo owner-draw, se definen con el estilo LBS_HASSTRINGS por defecto. Este estilo indica que los ítems consisten en cadenas de texto, el sistema se encarga de mantener la memoria para almacenar estas cadenas.

También podemos usar el estilo LBS_DISABLENOSCROLL, de modo que la barra de desplazamiento vertical se muestre siempre, aunque todos los ítems puedan ser visualizados. Este estilo no se puede aplicar a los controles con el estilo LBS_MULTICOLUMN.

Por último, hay que comentar algo sobre las dimensiones verticales de los controles list box. Cuando se crea uno de estos controles, la altura del control no se respeta de forma exacta, sino que se reduce lo necesario para que el list box contenga un número exacto de ítems. Así, si por ejemplo, indicamos una altura de 184, y cada ítem ocupa 18, la altura usada para crear el control será de 180, ya que en la distancia especificada caben 18 ítems, pero no 19. De este modo no se mostrará una parte de un ítem, y el control se dibujará más rápidamente.

Este comportamiento se puede evitar mediante el uso del estilo LBS_NOINTEGRALHEIGHT. Los list boxes con este estilo se crearán con la altura exacta indicada al definirlos.

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
Ejemplo 67 win067.zip 2007-03-15 100628 bytes 50

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.