El control de edición

El campo de selección de un combo box puede ser un control de edición o un control estático. El la parte que contiene el valor de la selección actual o el texto introducido por el usuario.

Podemos usar el mensaje WM_GETTEXT para obtener el texto que contiene actualmente, o el mensaje WM_SETTEXT para modificarlo.

También podemos usar el mensaje CB_GETEDITSEL para obtener las posiciones de inicio y final de la selección de texto actual, si existe; o usar el mensaje CB_SETEDITSEL para seleccionar parte del texto dentro del control de edición.

Si se usa el estilo CBS_AUTOHSCROLL es posible introducir más texto del que se puede visualizar en la anchura del control, en caso contrario la cantidad de texto que es posible introducir estará limitada por esa anchura.

También se puede limitar el número de caracteres que el usuario puede teclear mediante el mensaje CB_LIMITTEXT.

Hay dos mensajes de notificación relacionados con el control de edición. Cuando el se modifica el contenido del control de edición, la ventana padre recibe primero el mensaje de notificación CBN_EDITUPDATE, para indicar que el texto se ha alterado. Después de que el texto se ha mostrado, Windows envía el mensaje CBN_EDITCHANGE.

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 combo 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 combo box. Para eso se usaba el mensaje CB_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, CB_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 combo box intenta actualizar la pantalla para reflejar los cambios, al menos en combo boxes con el estilo CBS_SIMPLE donde la lista está permanentemente desplegada. 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.

En el caso de los controles combo box no disponemos de un estilo equivalente al estilo LBS_NOREDRAW de los list box, que inhibe las actualizaciones de la lista. En el caso de los combo boxes, cuando puedan contener muchos valores en la lista, será mejor usar otros estilos diferentes de CBS_SIMPLE.

Aspectos gráficos del combo box

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

Ajustar la anchura de un combo box

Por una parte, ya vimos que podemos añadir una barra de desplazamiento horizontal creando nuestro combo box con el estilo WS_HSCROLL. 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 CB_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 combo 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 combo 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 CB_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 combo 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 combo 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. Pero esto es innecesario, ya que en un control no se realiza ninguna proyección.

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

   char item[300];
   int x;
   int eActual;
   
   eActual = SendMessage(hlista, CB_GETHORIZONTALEXTENT, 0, 0);
   
   strcpy(item, "Ítem de una anchura tal que no cabe en "
        "el combo 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, CB_ADDSTRING, 0, (LPARAM)cad);
   SendMessage(hlista, CB_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 CB_GETITEMHEIGHT. Si se trata de un combo 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 combo box normales, el valor de wParam debe ser cero.

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

Para modificar la altura de un ítem se usa el mensaje CB_SETITEMHEIGHT, en el caso de combo 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, CB_SETITEMHEIGHT, 0, MAKELPARAM(30,0));
   InvalidateRect(hctrl, NULL, TRUE);

Localizaciones

Ya hemos visto que en los controles combo 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 combo box.

Para obtener el valor de la localización actual se usa el mensaje CB_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, CB_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 CB_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, CB_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.