Capítulo 54 Control ComboBoxEx

Ejemplo de control ComboBoxEx

Un control ComboBoxEx es, como su nombre indica, un control ComboBox extendido. La mayor diferencia con los ComboBox que hemos visto hasta ahora es que permite mostrar imágenes para cada item.

Por lo demás, siguen siendo un controles ComboBox, es decir, contienen una caja de edición y una lista desplegable con las posibles opciones. Mantiene los estilos de control de los ComboBoxes básicos:

  • CBS_DROPDOWN: el control de edición asociado permite introducir valores que no estén en la lista y una lista desplegable.
  • CBS_DROPDOWNLIST: el control de edición está deshabilitado y sólo se pueden seleccionar valores que ya estén en la lista desplegable.
  • CBS_SIMPLE: el cuadro de edición se muestra separado de la lista, y la lista siempre se muestra desplegada. Este estilo no funciona bien con algunas características de los ComboBoxEx.

Insertar durante la ejecución

Del mismo modo que hemos visto para otros controles, también es posible insertar controles ComboBoxEx durante la ejecución. Tan sólo hay que crear una ventana de la clase "WC_COMBOBOXEX". Para hacerlo usaremos las funciones CreateWindow o CreateWindowEx.

        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            hCtrl = CreateWindowEx(0, WC_COMBOBOXEX, NULL,
                WS_BORDER | WS_VISIBLE | WS_CHILD | CBS_DROPDOWN,
                0, 0, 200, 200,
                hwnd, (HMENU)ID_COMBOBOXEX,
                hInstance, NULL);
            break;

En el parámetro hMenu indicaremos el identificador del control y elegiremos los estilos y dimensiones adecuados para nuestro caso.

Hay que recordar que cuando se insertan controles durante la ejecución, la fuente por defecto es system. Si queremos cambiarla habrá que crear una fuente y asignársela al control usando un mensaje WM_SETFONT. Hay que recordar liberar recursos antes de cerrar la aplicación, usando DeleteObject.

    static HFONT hFont;
...
        case WM_CREATE:
            hFont = CreateFont(18, 0, 0, 0, 300, FALSE, FALSE, FALSE,
                DEFAULT_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
                PROOF_QUALITY, DEFAULT_PITCH | FF_ROMAN, "Times New Roman");
            // Asignamos la fuente a nuestro gusto.
            SendMessage(hCtrl, WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
...
        case WM_DESTROY:
            DeleteObject(hFont);
            break;

Estilos

Siguen disponibles los estilos de los controles ComboBox, ver capítulo 43 para más detalles.

Además, existen otros estilos extendidos, que básicamente sirven para limitar algunas de las nuevas características.

ComboBoxEx Elipsis
  • CBES_EX_CASESENSITIVE: las búsquedas de cadenas en la lista distinguen mayúsculas de minúsculas.
  • CBES_EX_NOEDITIMAGE: no se muestra imagen de item en la caja de edición ni en la lista.
  • CBES_EX_NOEDITIMAGEINDENT: equivale a CBES_EX_NOEDITIMAGE
  • CBES_EX_NOSIZELIMIT: Permite que el tamaño vertical del control ComboBoxEx sea más pequeño que el del control ComboBox incluido.
  • CBES_EX_PATHWORDBREAKPROC: se usarán los caracteres '/', '\', y '.' como delimitadores de palabra. Esto ayuda a moverse por palabras en nombres de fichero y URLs
  • CBES_EX_TEXTENDELLIPSIS: cuando el texto de un ítem no quepa en el ancho del control, en lugar de cortarse se sustituye el final por puntos suspensivos.

Para asignar y retirar estilos extendidos a un control ComboBoxEx se usa el mensaje CBEM_SETEXTENDEDSTYLE en wParam se indica una máscara de qué estilos queremos asignar o retirar y en lParam qué estilos queremos modificar. Por ejemplo, si queremos asignar CBES_EX_CASESENSITIVE y quitar CBES_EX_TEXTENDELLIPSIS, usaremos CBES_EX_CASESENSITIVE en wParam y CBES_EX_CASESENSITIVE | CBES_EX_TEXTENDELLIPSIS en lParam. Esto hace que se puedan asignar o retirar estilos extendidos sin necesidad de leer los estilos extendidos actuales.

        SendMessage(hCtrl, CBEM_SETEXTENDEDSTYLE, (WPARAM)CBES_EX_CASESENSITIVE, 
            (LPARAM)CBES_EX_CASESENSITIVE | CBES_EX_TEXTENDELLIPSIS);

Para recuperar los estilos actuales de un control ComboBoxEx se usa el mensaje CBEM_GETEXTENDEDSTYLE.

Lista de imágenes

Cada item en un control ComboBoxEx tiene asociadas tres imágenes, una para mostrar normalmente, otra para mostrar cuando el item está activo y una tercera que se usa para superponer. El mensaje CBEM_SETIMAGELIST sirve para asignar una lista de imágenes a un control ComboBoxEx.

Para recuperar la lista de imágenes asociada a un control ComboBoxEx se usa el mensje CBEM_GETIMAGELIST, sin parámetros.

Es importante eliminar la lista de imagenes antes de terminar la aplicación.

    HIMAGELIST hIml;
    HWND hCtrl;
    HBITMAP hbmp;
...
        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            hIml = ImageList_Create(24, 24, ILC_COLOR16|ILC_MASK, 19, 4);
            hbmp = LoadBitmap(hInstance, "imagenes");
            ImageList_AddMasked(hIml, hbmp,  RGB(255,255,255));
            DeleteObject(hbmp);
            hCtrl = CreateWindowEx(0, WC_COMBOBOXEX, NULL,
                WS_BORDER | WS_VISIBLE | WS_CHILD | CBS_DROPDOWN | CBS_SORT,
                0, 0, 0, 200,
                hwnd, (HMENU)ID_COMBOBOXEX,
                hInstance, NULL);
            SendMessage(hCtrl, CBEM_SETIMAGELIST,0,(LPARAM)hIml);
...
        case WM_DESTROY:
            hIml = (HIMAGELIST)SendMessage(hCtrl, CBEM_GETIMAGELIST, 0, 0);
            ImageList_Destroy(hIml);
...

Insertar items

Para insertar items en un control ComboBoxEx se usa el mensaje CBEM_INSERTITEM. En lParam hay que pasar un puntero a una estructura COMBOBOXEXITEM con la información correspondiente al item.

En el miembro mask de la estructura se deben activar las banderas que indican qué miembros de la estructura contienen valores válidos. En iItem el índice del item.

En el miembro iItem se especifica la posición de inserción. El valor debe estar entre 0 y el número de items en el control. Si se especifica un valor mayor, la operación de inserción fallará.

Para insertar el item en la última posición se puede usar el valor retornado por el mensaje CB_GETCOUNT.

Para asignar un texto al item hay que asignar valores a los miembros pszText. El miembro cchTextMax se ignora cuando se está insertando un item.

El miembro iImage es el índice de la imagen dentro de la lista de imágenes asignada al control que se muestra cuando el item no esté seleccionado.

El miembro iSelectedImage es el índice de la imagen dentro de la lista de imágenes asignada al control que se muestra cuando el item esté seleccionado.

El miembro iOverlay se supone que es para generar una imagen superponiendola a otra existente (presumiblemente iImage), sin embargo no parece que funcione y la documentación al respecto es muy incompleta.

El miembro iIndent sirve para añadir espacios a la izquierda del item. Cada espacio equivale a 10 pixels.

Finalmente, el miembro lParam permite almacenar un valor que puede usar la aplicación para su funcionamiento.

BOOL InsertarItem(HWND hCtrl, int iItem, int imagen, int imagensel, int indent, char *texto) {
    COMBOBOXEXITEM cbei;

    cbei.mask = CBEIF_TEXT | CBEIF_INDENT | CBEIF_IMAGE | CBEIF_SELECTEDIMAGE;
    cbei.iItem          = iItem;
    cbei.pszText        = texto;
    cbei.iImage         = imagen;
    cbei.iSelectedImage = imagensel;
    cbei.iIndent        = indent;

    // Intenta insertar el item, retorna FALSE si falla.
    if(SendMessage(hCtrl, CBEM_INSERTITEM,0,(LPARAM)&cbei) == -1)
        return FALSE;
    return TRUE;
}

Cuando se insertan items, los valores de algunos miembros de la estructura COMBOBOXEXITEM pueden omitirse y se podrán asignar cuando el control necesite acceder a ellos.

En el caso de las imágenes, iImage, iSelectedImage e iOverlay se puede asignar el valor I_IMAGECALLBACK en lugar de un índice o un valor concreto. En el caso de pszText se puede usar el valor LPSTR_TEXTCALLBACK.

Cuando el control necesite mostrar uno de esos items requerirá la información a la aplicación mediante un mensaje de notificación CBEN_GETDISPINFO.

Este mensaje de notificación recibirá a través de lParam un puntero a una estructura NMCOMBOBOXEX, que a su vez contiene una estructura COMBOBOXEXITEM. En esa estructura, el miembro mask nos indicará que miembros de la estructura debemos asignar antes de retornar con un valor nulo.

Si además añadimos el valor CBEIF_DI_SETITEM a mask, el control almacenará la información suministrada, y no volverá a solicitarla.

En este ejemplo se calculan los índices de la imágenes y los márgenes de cada item en función del valor de iIndex:

BOOL InsertarItem(HWND hCtrl, int iItem) {
    COMBOBOXEXITEM cbei;

    cbei.mask = CBEIF_TEXT | CBEIF_INDENT | CBEIF_IMAGE | CBEIF_SELECTEDIMAGE;
    cbei.iItem          = iItem;
    cbei.pszText        = LPSTR_TEXTCALLBACK;
    cbei.iImage         = I_IMAGECALLBACK;
    cbei.iSelectedImage = I_IMAGECALLBACK;

    // Intenta insertar el item, retorna FALSE si falla.
    if(SendMessage(hCtrl, CBEM_INSERTITEM,0,(LPARAM)&cbei) == -1)
        return FALSE;
    return TRUE;
}
...
    NMCOMBOBOXEX* nmCBE;
...
        case WM_NOTIFY:
            pnmhdr = (LPNMHEADER)lParam;
            switch(pnmhdr->hdr.code) {
                case CBEN_GETDISPINFO:
                    if( nmCBE->ceItem.mask & CBEIF_IMAGE)
                        nmCBE->ceItem.iImage = 1; // Imagen 1 de lista
                    if( nmCBE->ceItem.mask & CBEIF_SELECTEDIMAGE)
                        nmCBE->ceItem.iSelectedImage = 2; // Imagen 2 de lista
                    if( nmCBE->ceItem.mask & CBEIF_TEXT)
                        strcpy(nmCBE->ceItem.pszText, "Desconocido");
                    nmCBE->ceItem.mask |= CBEIF_DI_SETITEM;
                    return 0;
                    break;
            }

También se envía a la aplicación un mensaje de notificación CBEN_INSERTITEM cada vez que se inserte un item en el control. En este caso también se recibirá a través de lParam un puntero a una estructura NMCOMBOBOXEX con los datos del item insertado.

Modificar un item

Es posible modificar los atributos de un item que ya esté en la lista, para ello usaremos el mensaje CBEM_SETITEM. El comportamiento es similar al de insertar un item, pasando en lParam un puntero a una estructura COMBOBOXEXITEM con los miembros asignados con los nuevos valores del item y el índice del item en el miembro iItem.

    COMBOBOXEXITEM cbeitem;
...
        cbeitem.mask = CBEIF_TEXT;
        cbeitem.iItem = 4;
        cbeitem.pszText = "Modificado";
        SendMessage(hCtrl, CBEM_SETITEM, 0, (LPARAM)&cbeitem);

Obtener información de un item

Para recuperar información sobre un ítem usaremos el mensaje CBEM_GETITEM pasando en lParam un puntero a una estructura COMBOBOXEXITEM, en la que iniciaremos los miembros iItem para indicar de qué item queremos recuperar datos, y mask para determinar que miembros de la estructura queremos obtener.

Eliminar un item

Para eliminar items se usa el mensaje CBEM_DELETEITEM. En wParam se determina el número del iItem a borrar.

    SendMessage(hCtrl, CBEM_DELETEITEM, (WPARAM)1, 0);

Cuando se borra un item se envía un mensaje de notificación CBEN_DELETEITEM a la aplicación. En lParam se envia un puntero a una estructura NMCOMBOBOXEX con la información sobre el item eliminado.

Edición de valores

Si el control no tiene el estilo CBS_DROPDOWNLIST, la caja de edición estará activa.

Cada vez que el usuario pulsa sobre la caja de edición, o que se active por otra causa (TAB o atajo de teclado o que se haya pulsado el botón de despliegue de la lista), la aplicación recibirá un mensaje de notificación CBEN_BEGINEDIT. A partir de ese momento estaremos en proceso de edición. Cunado la edición termine se enviará a la aplicación recibirá un mensaje de notificación CBEN_ENDEDIT.

Con el mensaje de notificación CBEN_ENDEDIT, en lParam se envía un puntero a una estructura NMCBEENDEDIT. En esta estructura, el miembro fChanged nos indicará si el valor actual de la caja de edición ha sido modificado. iNewSelection contendrá el índice del item de la lista, siempre que el contenido actual de la caja de edición esté en la lista. En caso contrario vadrá -1. szText contiene el valor de la caja de edición y iWhy nos indica el motivo por el que se ha dado por concluida la edición, ya sea pérdida de foco, despliegue de la lista o pulsación de ESC o INTRO.

    NMCBEENDEDIT* nmCEE;
...
        case WM_NOTIFY:
            pnmhdr = (LPNMHEADER)lParam;
            switch(pnmhdr->hdr.code) {
                case CBEN_BEGINEDIT:
                    return 0;
                case CBEN_ENDEDIT:
                    nmCEE = (NMCBEENDEDIT*)lParam;
                    if(nmCEE->fChanged && nmCEE->iNewSelection == -1) {
                        InsertarItem(hCtrl, -1, 17, 8, 2, nmCEE->szText);
                        c = SendMessage(hCtrl, CB_GETCOUNT, 0, 0);
                        SendMessage(hCtrl, CB_SETCURSEL, (WPARAM)c, 0);
                    }
                    return 0;
            }
            break;

También disponemos de un mensaje para averiguar si el contenido de la caja de edición CBEM_HASEDITCHANGED, pero sólo funcina si se usa antes de que se envíe el mensaje de notificación NMCBEENDEDIT, ya que si se envía después siempre devuelve FALSE.

Ejemplo 94

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 94 win094.zip 2021-09-10 19534 bytes 16

Ficheros de recursos

No existe un tipo de control específico para crear ComboBoxEx en un fichero de recursos, de modo que siempre deberemos insertar estos controles en ejecución. Podemos, sin embargo, usar un control ComboBox para situar el control si usamos un editor de recursos como ResEdit, y convertir las coordenadas de diálogo a coordenadas de pantalla usando la función MapDialogRect:

//
// Dialog resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
DialogoEx DIALOG 0, 0, 321, 57
STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | DS_SHELLFONT | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
CAPTION "ComboBoxEx"
FONT 8, "Helv"
{
/*   COMBOBOX usado como plantilla para COMOBOXEX, comentado posteriormente para ser insertado en ejecución
    COMBOBOX        ID_COMBOBOXEX, 8, 9, 221, 81, CBS_DROPDOWN | CBS_HASSTRINGS | CBES_EX_CASESENSITIVE, WS_EX_LEFT */
    PUSHBUTTON      "Cancel", IDCANCEL, 259, 27, 50, 14, 0, WS_EX_LEFT
    DEFPUSHBUTTON   "OK", IDOK, 259, 10, 50, 14, 0, WS_EX_LEFT
}

Ejemplo de procedimiento de diálogo:

BOOL CALLBACK DialogProcedure(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    HIMAGELIST hIml;
    HBITMAP hbmp;
    HFONT hfont;
    RECT re;
 
    switch(uMsg) {
        case WM_INITDIALOG:
            hIml = ImageList_Create(24, 24, ILC_COLOR16|ILC_MASK, 19, 4);
            hbmp = LoadBitmap((HINSTANCE)lParam, "imagenes");
            ImageList_AddMasked(hIml, hbmp,  RGB(255,255,255));
            DeleteObject(hbmp);
            hfont = CreateFont(-11, 0, 0, 0, 0, FALSE, FALSE, FALSE, 1, 0, 0, 0, 0, ("Ms Shell Dlg"));

            re.top = 8;
            re.left = 9;
            re.bottom = 221;
            re.right = 81;
            MapDialogRect(hwndDlg, &re);

            CreateWindowEx(0, WC_COMBOBOXEX, NULL,
                WS_BORDER | WS_VISIBLE | WS_CHILD | WS_TABSTOP | CBS_DROPDOWN | CBS_SORT | ES_WANTRETURN,
                re.top, re.left, re.bottom, re.right,
                hwndDlg, (HMENU)ID_COMBOBOXEX,
                (HINSTANCE)lParam, NULL);
            // Asignamos la fuente a nuestro gusto.
            SendDlgItemMessage(hwndDlg, ID_COMBOBOXEX, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(FALSE, 0));
            SendDlgItemMessage(hwndDlg, ID_COMBOBOXEX, CBEM_SETIMAGELIST,0,(LPARAM)hIml);

            // Insertar items:
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 0, 9, 0, 0, "Reloj");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 1, 10, 1, 1, "Bateria");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 2, 11, 2, 2, "Usuario");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 3, 12, 3, 0, "Libro");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 4, 13, 4, 1, "Caja");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 5, 14, 5, 2, "Café");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 6, 15, 6, 0, "Puzzle");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 7, 16, 7, 1, "Galletas");
            InsertarItem(hwndDlg, ID_COMBOBOXEX, 8, 17, 8, 2, "Tierra");

            SendDlgItemMessage(hwndDlg, ID_COMBOBOXEX, CB_SETCURSEL, (WPARAM)0, 0);
            return TRUE;
...
    }
}

Mensajes de formato de caracteres

Se puede cambiar la bandera de formato Unicode usando el mensaje CBEM_SETUNICODEFORMAT indicando en wParam el valor de la bandera, cero para caracteres ANSI y distinto de cero para caracteres Unicode.

El mensaje CBEM_GETUNICODEFORMAT obtiene el valor de la bandera de formato Unicode.

Controles base

Los controles ComboBoxEx constan de dos controles base: un control de edición y un ComboBox. Es posible obtener un manipulador de cada uno de ellos. Para el control ComboBox se usa el mensaje CBEM_GETCOMBOCONTROL y para el control de edición un mensaje CBEM_GETEDITCONTROL,

Operaciones de arrastre

Si la aplicación implementa operaciones de drag and drop al recibir un mensaje de notificación CBEN_DRAGBEGIN debe iniciar una de ellas.

Ya hablamos algo sobre este tipo de operaciones en el capítulo 48, pero probablemente volvamos a dedicar tiempo a este tipo de funciones.

Temas de Windows

Por último, también disponemos de un mensaje para aplciar un tema de Windows al control ComboBoxEx: CBEM_SETWINDOWTHEME.

Para más información sobre los temas de Windows, visita este enlace.

Ejemplo 95

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 95 win095.zip 2021-09-10 19924 bytes 9