Capítulo 11 Control básico ComboBox

Los ComboBoxes son una combinación de un control Edit y un Lisbox. Son los controles que suelen recordar las entradas que hemos introducido antes, para que podamos seleccionarlas sin tener que escribirlas de nuevo, en ese sentido funcionan igual que un Listbox, pero también permiten introducir nuevas entradas.

Hay modalidades de ComboBox en las que el control Edit está inhibido, y no permite introducir nuevos valores. En esos casos, el control se comportará de un modo muy parecido al que lo hace un Listbox, pero, como veremos más adelante, tienen ciertas ventajas.

Exiten tres tipos de ComboBoxes:

  • Simple: es la forma que muestra siempre el control Edit y el ListBox, aunque ésta esté vacía.
  • DropDown: despliegue hacia abajo. Se muestra un pequeño icono a la derecha del control Edit. Si el usuario lo pulsa con el ratón, se desplegará el ListBox, mientras no se pulse, la lista permanecerá oculta.
  • DropDownList: lo mismo, pero el control Edit se sustituye por un control Static.

Ficheros de recursos

Para nuestro ejemplo incluiremos un control ComboBox de cada tipo, así veremos las peculiaridades de cada uno:

#include <windows.h>
#include "win010.h"

Menu MENU 
BEGIN
 POPUP "&Principal"
 BEGIN
  MENUITEM "&Diálogo", CM_DIALOGO
 END
END

DialogoPrueba DIALOG 0, 0, 205, 78
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Combo boxes"
FONT 8, "Helv"
BEGIN
 CONTROL "&Simple", -1, "static", 
    SS_LEFT | WS_CHILD | WS_VISIBLE, 
    8, 2, 60, 8
 CONTROL "ComboBox1", ID_COMBOBOX1, "COMBOBOX", 
    CBS_SORT | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    8, 13, 60, 43
 CONTROL "&Dropdown", -1, "static", 
    SS_LEFT | WS_CHILD | WS_VISIBLE, 
    73, 2, 60, 8
 CONTROL "ComboBox2", ID_COMBOBOX2, "COMBOBOX", 
    CBS_DROPDOWN | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    72, 13, 60, 103
 CONTROL "Dropdown &List", -1, "static", 
    SS_LEFT | WS_CHILD | WS_VISIBLE, 
    138, 2, 60, 8
 CONTROL "ComboBox3", ID_COMBOBOX3, "COMBOBOX", 
    CBS_DROPDOWNLIST | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    136, 13, 60, 103
 CONTROL "Aceptar", IDOK, "BUTTON", 
    BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    28, 60, 50, 14
 CONTROL "Cancelar", IDCANCEL, "BUTTON", 
    BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    116, 60, 50, 14
END

Hemos añadido los nuevos controles ComboBox. Para más detalles acerca de estos controles ver controles combobox.

Ahora veremos más detalles sobre los estilos de los controles ComboBox:

 CONTROL "ComboBox1", ID_COMBOBOX1, "COMBOBOX", 
    CBS_SORT | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    8, 13, 60, 43
 CONTROL "ComboBox2", ID_COMBOBOX2, "COMBOBOX", 
    CBS_DROPDOWN | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    72, 13, 60, 103
 CONTROL "ComboBox3", ID_COMBOBOX3, "COMBOBOX", 
    CBS_DROPDOWNLIST | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
    136, 13, 60, 103
  • CONTROL es la palabra clave que indica que vamos a definir un control.
  • A continuación, en el parámetro text, en el caso de los combobox sólo sirve como información, y no se usa.
  • id es el identificador del control. El identificador será necesario para inicializar y leer los contenidos y selecciones del combobox.
  • class es la clase de control, en nuestro caso "COMBOBOX".
  • style es el estilo de control que queremos. En nuestro caso es una combinación de un estilo combobox y varios de ventana:
    • CBS_SORT: Indica que los valores en la lista se apareceran por orden alfabético. CBS_DROPDOWN crea un ComboBox del tipo DropDown. CBS_DROPDOWNLIST crea un ComboBox del tipo DropDownList.
    • WS_CHILD: crea el control como una ventana hija.
    • WS_VISIBLE: crea una ventana inicialmente visible.
    • WS_TABSTOP: para que cuando el foco cambie de control al pulsar TAB, pase por este control.
  • coordenada x del control.
  • coordenada y del control.
  • width: anchura del control.
  • height: altura del control.

Iniciar controles ComboBox

Iniciar los controles ComboBox es análogo a iniciar los controles ListBox. En general, necesitaremos introducir una lista de valores en el listbox, para que el usuario pueda usarla.

El lugar adecuado para hacerlo también es al procesar el mensaje WM_INITDIALOG de nuestro cuadro de diálogo, y el mensaje para añadir cadenas es CB_ADDSTRING. Hay que recordar también que para enviar mensajes a un control se usa la función SendDlgItemMessage.

Para hacer más fácil la inicialización de las listas, usaremos los mismos valores en las tres. Para ello definiremos, dentro de nuestra estructura de datos para compartir con el cuadro de diálogo, un array de cadenas con los valores necesario para inicializar los ComboBox:

También usaremos la misma estructura para almacenar los valores iniciales y de la última selección de los tres comboboxes:

/* Datos de la aplicación */
typedef struct stDatos {
   char Lista[6][80]; // Valores de los comboboxes
   char Item[3][80]; // Opciones elegidas
} DATOS;

Y asignaremos valores iniciales a las selecciones y a la lista de opciones al procesar el mensaje WM_CREATE:

    static DATOS Datos;
    int i;
...        
        case WM_CREATE:
           hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
           strcpy(Datos.Item[0], "a");
           strcpy(Datos.Item[1], "c");
           strcpy(Datos.Item[2], "e");
           for(i = 0; i < 6; i++) 
              sprintf(Datos.Lista[i], "%c) Opción %c", 'a'+i, 'A'+i);
           return 0;

La parte de inicialización de los comboboxes se hace al procesar el mensaje WM_INITDIALOG:

    int i;
    static DATOS *Datos;
...    
        case WM_INITDIALOG:
           Datos = (DATOS*)lParam;
           // Añadir cadenas. Mensaje: LB_ADDSTRING
           for(i = 0; i < 6; i++) {
              SendDlgItemMessage(hDlg, ID_COMBOBOX1, 
                 CB_ADDSTRING, 0, (LPARAM})Datos->Lista[i]);
              SendDlgItemMessage(hDlg, ID_COMBOBOX2, 
                 CB_ADDSTRING, 0, (LPARAM)Datos->Lista[i]);
              SendDlgItemMessage(hDlg, ID_COMBOBOX3, 
                 CB_ADDSTRING, 0, (LPARAM)Datos->Lista[i]);
           }
           SendDlgItemMessage(hDlg, ID_COMBOBOX1, CB_SELECTSTRING, 
              (WPARAM)-1, (LPARAM)Datos->Item[0]);
           SendDlgItemMessage(hDlg, ID_COMBOBOX2, CB_SELECTSTRING, 
              (WPARAM)-1, (LPARAM)Datos->Item[1]);
           SendDlgItemMessage(hDlg, ID_COMBOBOX3, CB_SELECTSTRING, 
              (WPARAM)-1, (LPARAM)Datos->Item[2]);
           SetFocus(GetDlgItem(hDlg, ID_COMBOBOX1));
           return FALSE;

También podemos preseleccionar alguna de las cadenas de los comboboxes. Para seleccionar una de las cadenas se usa un mensaje: CB_SELECTSTRING. Usaremos el valor -1 en wParam para indicar que se busque en toda la lista.

Devolver valores a la aplicación

Por supuesto, querremos que cuando el usuario esté satisfecho con los datos que ha introducido, y pulse el botón de aceptar, el dato de nuestra aplicación se actualice con el texto del ítem seleccionado.

De nuevo podemos recurrir a mensajes para pedirle al combobox el valor de la cadena actualmente seleccionada. En este caso se trata dos mensajes combinados, análogos a los usados en los Listbox. Uno es CB_GETCURSEL, que se usa para averiguar el índice de la cadena actualmente seleccionada.

El otro es CB_GETLBTEXT, que devuelve la cadena del índice que le indiquemos.

Cuando trabajemos con memoria dinámica y con ítems de longitud variable, será interesante saber la longitud de la cadena antes de leerla desde el listbox. Para eso podemos usar el mensaje CB_GETLBTEXTLEN.

Pero esto es válido para comboboxes del tipo DropDownList, que se comportan como un Listbox, sin embargo en las otras modalidades de controles ComboBox, el ítem seleccionado no tiene por qué ser igual que el texto que contiene el control Edit.

Para capturar el contenido del control Edit asociado a un ComboBox se puede usar la función GetDlgItemText. Y también el mensaje WM_GETTEXT y WM_GETTEXTLENGTH.

Como tenemos tres tipos de ComboBox, usaremos un método diferente con cada uno de ellos. Veamos cómo podría quedar el tratamiento del mensaje WM_COMMAND:

        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case IDOK:
                 // En el ComboList Simple usaremos:
                 GetDlgItemText(hDlg, ID_COMBOBOX1, Datos-Item[0], 80);
                 // En el ComboList DropDown usaremos:
                 SendDlgItemMessage(hDlg, ID_COMBOBOX2, WM_GETTEXT, 
                    80, (LPARAM)Datos->Item[1]);
                 // En el ComboList DropDownList usaremos:
                 indice = SendDlgItemMessage(hDlg, ID_COMBOBOX3, 
                    CB_GETCURSEL, 0, 0);
                 SendDlgItemMessage(hDlg, ID_COMBOBOX3, 
                    CB_GETLBTEXT, indice, (LPARAM)Datos-Item[2]);
                 wsprintf(resultado, "%s\n%s\n%s", 
                    Datos-Item[0], Datos-Item[1], Datos-Item[2]);
                 MessageBox(hDlg, resultado, "Leido", MB_OK);
                 EndDialog(hDlg, FALSE);
                 return TRUE;
              case IDCANCEL:
                 EndDialog(hDlg, FALSE);
                 return FALSE;
			}
			break;
		}

Pero ahora surge un problema. Si en un combobox introducimos una cadena que no está en nuestra lista, y posteriormente volvemos a entrar en el cuadro de diálogo, no podremos editar el valor inicial. Es más, ni siquiera nos será mostrado.

Para evitar eso deberíamos añadir cada nuevo valor introducido a la lista. Nuestro ejemplo es un poco limitado, ya que no tiene previsto que la lista pueda crecer, y desde luego, no guardará los valores de la lista cuando el programa termine, de modo que estén disponibles en sucesivas ejecuciones. Pero de momento nos conformaremos con ciertas modificaciones mínimas que ilustren cómo solventar este error.

Para empezar, reservaremos espacio suficiente para almacenar algunos valores extra, y modificaremos la estructura de datos:

#define MAX_CADENAS 100
...

/* Datos de la aplicación */
typedef struct stDatos {
   int nCadenas;
   char Lista[MAX_CADENAS][80];
   char Item[3][80];
} DATOS;

También deberemos inicializar el valor de nCadenas, al procesar el mensaje WM_CREATE:

              Datos.nCadenas = 6;

Modificaremos la rutina para inicializar los ComboBoxes:

           // Añadir cadenas. Mensaje: LB_ADDSTRING
           for(i = 0; i < Datos->nCadenas; i++) {
              SendDlgItemMessage(hDlg, ID_COMBOBOX1, 
                 CB_ADDSTRING, 0, (LPARAM)Datos->Lista[i]);
              SendDlgItemMessage(hDlg, ID_COMBOBOX2, 
                 CB_ADDSTRING, 0, (LPARAM)Datos->Lista[i]);
              SendDlgItemMessage(hDlg, ID_COMBOBOX3, 
                 CB_ADDSTRING, 0, (LPARAM)Datos->Lista[i]);
           }
...

Y para leer los valores introducidos, y añadirlos a la lista si no están. Para eso usaremos el mensaje CB_FINDSTRINGEXACT, que buscará una cadena entre los valores almacenados en la lista, si se encuentra devolverá un índice, y si no, el valor CB_ERR.

Existe otro mensaje parecido, CB_FINDSTRING, pero no nos vale, porque localizará la primera cadena de la lista que comience con los mismos caracteres que la cadena que buscamos. Por ejemplo, si hay un valor en la lista "valor a", y nosotros buscamos "valor", obtendremos el índice de la cadena "valor a", que no es lo que queremos, al menos en este ejemplo.

El proceso del comando IDOK quedaría así:

              case IDOK:
                 // En el ComboList Simple usaremos:
                 GetDlgItemText(hDlg, ID_COMBOBOX1, Datos->Item[0], 80);
                 if(SendDlgItemMessage(hDlg, ID_COMBOBOX1, CB_FINDSTRINGEXACT, 
                    (WPARAM)-1, (LPARAM)Datos->Item[0]) == CB_ERR)
                    strcpy(Datos->Lista[Datos->nCadenas++], Datos->Item[0]);
                 // En el ComboList DropDown usaremos:
                 SendDlgItemMessage(hDlg, ID_COMBOBOX2, WM_GETTEXT, 
                    80, (LPARAM)Datos->Item[1]);
                 if(SendDlgItemMessage(hDlg, ID_COMBOBOX1, CB_FINDSTRINGEXACT, 
                    (WPARAM)-1, (LPARAM)Datos->Item[1]) == CB_ERR &&
                    strcmp(Datos->Item[0], Datos->Item[1])) 
                    strcpy(Datos->Lista[Datos->nCadenas++], Datos->Item[1]);
                 // En el ComboList DropDownList usaremos:
                 indice = SendDlgItemMessage(hDlg, ID_COMBOBOX3, 
                    CB_GETCURSEL, 0, 0);
                 SendDlgItemMessage(hDlg, ID_COMBOBOX3, 
                    CB_GETLBTEXT, indice, (LPARAM)Datos->Item[2]);
                 wsprintf(resultado, "%s\n%s\n%s", 
                    Datos->Item[0], Datos->Item[1], Datos->Item[2]);
                 MessageBox(hDlg, resultado, "Leido", MB_OK);
                 EndDialog(hDlg, FALSE);
                 return TRUE;

Ejemplo 10


  Nombre Fichero Fecha Tamaño Contador Descarga
D Ejemplo 10 win010.zip 2004-05-17 3572 bytes 679

Comentarios de los usuarios (8)

JDC
2012-10-18 23:14:12

Hola no entiendo porque no funciona este codigo para generar un combobox en la ventana padre, cuando intento desplegar la lista aparece vacia:

char cadena[40];
...
case WM_CREATE:
...
objetos=CreateWindowEx(
            0,
            "COMBOBOX",
            NULL,
            CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
            20,150,
            70,20,
            hwnd,
            (HMENU)ID_COMBO01,
            hInstance,
            NULL);
        SendMessage(objetos,WM_SETFONT,(WPARAM)hfont,MAKELPARAM(TRUE,0));
        strcpy(cadena, "Todo");
        SendDlgItemMessage(hwnd, ID_COMBO01, CB_ADDSTRING, 0, (LPARAM)cadena);  
JDC
2012-10-19 08:02:47

Ya me di cuenta que estaba mal, las dimensiones del combobox deben ser suficientemente grandes para mostrar todas las opciones, habia que modificar asi:

20,150,

70,500,

salu2

David
2013-01-22 04:58:04

el ejemplo no me funciona uso vs2012, en la parte de devolver un valor, no se de donde sale este 'resultado', no esta definido en ninguna parte...

Salvador Pozo
2013-01-22 19:09:40

Hola, David:

Para probar los ejemplos te aconsejo que los descargues desde el enlace. Los fragmentos que se incluyen en el texto son sólo una guía, y no siempre están completos.

En este caso, "resultado" está declarado como un array de 250 caracteres.

No deberías tener muchos problemas para probar los ejemplos en otros compiladores, aunque las versiones para descargar están creadas usando Code::Blocks.

Hasta pronto.

David
2013-01-22 21:53:20

Gracias x la respuesta tan rapido, aunque me sigue dando error cuando intento abrir el dialogo... no se xq creo que es el vs2012, y x cierto el link esta caido x eso no pude bajar el ejemplo... Muy bueno el curso :D

JDC
2013-02-14 04:04:31

Hola los links estan caidos te recomiendo subirlos a 4shared es el mejor lugar para guardar archivos que conozco

Borja
2013-02-28 12:53:19

¡Gracias por resubir todos los archivos a 4shared!

DSS
2013-12-31 19:05:04

Tengo un problema, cuando compilo me aparece el siguiente mensaje:

" declaración implícita incompatible de la función interna 'sprintf' [activado por defecto] "

Lo "solucione" incluyendo la librería stdio.h, pero al ejecutar se muestran los Comboboxes vacíos, sin el texto que se supone se almacena con el sprintf.

Esto me pasa con el código de un ejemplo anterior que he ido modificando por mi mismo (esta igual que el ejemplo 10), pero al ejecutar el "win010" que se descarga desde aquí, me aparecen los Comboboxes bien, con cada uno de los textos de las opciones (y no me da problema el sprintf a pesar de que no incluyen stdio.h)

¿Porque será esto? s: