Capítulo 52 Control UpDown

Validación de datos

Un control UpDown está formado por un par de botones con flechas. Esos botones se usan para incrementar o decrementar un valor, generalmente asociado a otro control. Cuando este control asociado existe, se le denomina ventana amiga (buddy window). El valor asociado se denomina posición actual.

Desde el punto de vista del usuario, el control UpDown y su ventana amiga se comportan como un único control, de modo que las flechas constituyen una forma alternativa de modificar el valor mostrado por el control.

Creación de un control UpDown

Para crear un control UpDown se puede usar la función CreateUpDownControl, aunque no es muy aconsejable, ya que los valores de la posición y rango estarán limitados a 16 bits. De hecho, esta función se considera obsoleta.

En su lugar es preferible usar la función CreateWindowEx, especificando el valor UPDOWN_CLASS como clase de ventana de control. Es imprescindible indicar al menos los estilos WS_CHILD y WS_VISIBLE, además de los específicos para estos controles que consideremos necesarios en nuestro caso.

        CreateWindowEx(0, UPDOWN_CLASS, NULL,
            WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_WRAP,
            10, 10,
            20, 20,
            hwnd, (HMENU)100,
            hInstance, NULL);

Como con el resto de los controles comunes, es necesario asegurarse de que la DLL ha sido cargada mediante una llamada a la función InitCommonControls.

La apariencia de este tipo de controles también se ve afectada si se activan o no los estilos visuales. La apariencia con los estilos visuales activos es algo mejor, ya que el conjunto del control UpDown y la ventana amiga tienen una apariencia más homogénea y la idea de que se trata de un control único es más evidente.

Decordemos que para activar los estilos visuales hay que incluir un fichero de manifiesto en el fichero de recursos.

Especificar una ventana amiga

Generalmente no crearemos controles de este tipo sin una ventana asociada, de modo que tendremos que asociarle una.

Hay dos formas de hacer esto:

  • Automáticamente: si indicamos el estilo UDS_AUTOBUDDY, se tomará como ventana amiga para el control la anterior en el orden Z, que será el control que hayamos creado justo antes.
  •         CreateWindowEx(0, "EDIT", "100", ES_RIGHT | ES_NUMBER | WS_VISIBLE | WS_CHILD | WS_BORDER,
                            10, 10, 60, 22, hwnd,(HMENU)100, hInstance, NULL);
            CreateWindowEx(0, UPDOWN_CLASS, NULL,
                            WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_WRAP | UDS_SETBUDDYINT | UDS_AUTOBUDDY,
                            0, 0,
                            0, 0,
                            hwnd, (HMENU)101,
                            hInstance, NULL);
    
  • Manualmente: enviando un mensaje UDM_SETBUDDY.
  •         CreateWindowEx(0, "EDIT", "100", ES_RIGHT | ES_NUMBER | WS_VISIBLE | WS_CHILD | WS_BORDER,
                            10, 10, 60, 22, hwnd,(HMENU)100, hInstance, NULL);
            CreateWindowEx(0, UPDOWN_CLASS, NULL,
                            WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_WRAP | UDS_SETBUDDYINT,
                            0, 0,
                            0, 0,
                            hwnd, (HMENU)101,
                            hInstance, NULL);
            SendDlgItemMessage(hwnd, 101, UDM_SETBUDDY, (WPARAM)GetDlgItem(hwnd, 100), 0);
    

Nota: La asociación más frecuente es entre un control UpDown y un control edit. Este conjunto forma lo que normalmente se conoce como un control spinner (control giratorio). Sin embargo, en muchos sitios se conocen los controles UpDown como Controles Spin, por ejemplo, en el editor de recursos "ResEdit".

De forma simétrica, para obtener el manipulador de ventana de la ventana amiga asociada a un control UpDown, se usa el mensaje UDM_GETBUDDY.

Estilos

Hay varios estilos que se pueden aplicar a los controles UpDown, además de UDS_AUTOBUDDY.

UDS_ALIGNLEFT y UDS_ALIGNRIGHT afectan a la posición del control con respecto a la ventana amiga asociada. El primer estilo alinéa el control a la izquierda y el segundo a la derecha de la ventana amiga.

Si se especifica UDS_HORZ, las flechas del control apuntan a derecha e izquierda, si se omite, apuntan arriba y abajo.

Si se especifica el estilo UDS_HOTTRACK, se modificará el aspecto del control cuando el cursor del ratón está sobre él, resaltando la flecha correspondiente.

El estilo UDS_SETBUDDYINT provoca que se modifique el texto asociado a la ventana amiga, enviando un mensaje WM_SETTEXT con el valor actual de control UpDown.

El estilo UDS_NOTHOUSANDS omite los puntos separadores de miles en el texto asociado a la ventana amiga. Si no se especifica, se insertarán puntos separadores.

UDS_ARROWKEYS hace que las flechas del cursor se comporten como si se pulsarán las flechas del control.

Por último UDS_WRAP ajusta los valores del control dentro de los márgenes indicados, de modo que si se sobrepasa el máximo, se vuelve al valor mínimo, y si se baja por debajo del mínimo, se vuelve al máximo. Si no se indica este estilo, al llegar al máximo se mantiene el valor, y sólo es posible disminuirlo, y al llegar al mínimo, sólo se puede incrementar el valor actual.

Rango y posición actual

Del mismo modo que los controles de desplazamiento, podemos fijar los límites inferior y superior de los controles UpDown, de modo que la posición actual no pueda tomar valores fuera de ese rango.

Para hacerlo disponemos de dos mensajes UDM_SETRANGE y UDM_SETTANGE32. El primero para rangos especificados por valores de 16 bits, y el segundo para valores de 32 bits.

        SendDlgItemMessage(hwnd, 101, UDM_SETRANGE32, (WPARAM)-100, (LPARAM)100);
        SendDlgItemMessage(hwnd, 101, UDM_SETRANGE, 0, (LPARAM) MAKELONG((short) 100, (short) -100));

Para modificar el valor actual de la posición del control también disponemos de dos mensajes, UDM_SETPOS y UDM_SETPOS32, el primero cuando el rango esté definido con valores de 16 bits, y el segundo para rangos de 32 bits.

Los mensajes complementarios sirven para leer los valores de rangos y posiciones actuales:

El mensaje UDM_GETRANGE obtiene el rango empaquetado en un entero de 32 bits. La palabra de menor peso contiene el límite superior, y la de mayor peso, el inferior.

El mensaje UDM_GETRANGE32 obtiene el rango en forma de enteros de 32 bits. Para ello hay que pasarle dos parámetros. En wParam un puntero a un entero con signo que recibirá el límite inferior, y en lParam un puntero a un entero con signo que recibirá el límite superior. Cualquiera de los punteros puede ser NULL, si no queremos obtener alguno de los límites.

Por último, para obtener la posición actual se usa el mensaje UDM_GETPOS o UDM_GETPOS32. El primero con una precisión de 16 bits, y el segundo con 32 bits de precisión.

Ficheros de recursos

Para usar controles UpDown a un cuadro de diálogo dentro de un fichero de recursos basta con añadir un control de la clase UPDOWN_CLASS. Si además se especifica el estilo UDS_AUTOBUDDY, hay que tener especial cuidado en el orden en que se definen los controles, ya que el control UpDown se asociará con el control especificado inmediantamente antes:

    EDITTEXT        ID_EDIT5, 10, 9, 93, 14, ES_AUTOHSCROLL
    CONTROL         "", ID_UPDOWN5, UPDOWN_CLASS, UDS_ARROWKEYS | UDS_AUTOBUDDY | UDS_HOTTRACK | UDS_SETBUDDYINT, 104, 9, 11, 14

Aceleradores

Es probable que ya lo hayas notado, pero los controles UpDown no se comportan del mismo modo si se pulsa una vez sobre una de sus flechas que si se mantiene pulsado durante un tiempo. Cuanto más tiempo se mantiene pulsada una de las flechas, a mayor velocidad cambia su valor actual. Este comportamiento es lo que se llama "aceleradores", y se puede modificar a nuestra discrección para adaptarlo a nuestro gusto o a nuestras necesidades.

Los aceleradores trabajan por tramos, y podemos añadir tantos de esos tramos como queramos. Cada uno de ellos se define por dos valores, el primero es un tiempo expresado en segundos, y el segundo es el valor del incremento que se aplica a partir de que transcurra ese tiempo.

Para cada tramo se usa una estructura UDACCEL. Si queremos usar aceleradores en varios tramos, crearemos un array de estas estructuras.

Supongamos que queremos que el primer tramo efectúe incrementos de uno en uno, el segundo de diez en diez, a partir de que trasncurran cinco segundos de pulsación, el tercero de cincuenta en cincuenta, a partir de los diez segundos de pulsación (contando desde el principio). El cuarto de cien en cien, a partir de los quince segundos. El array quedaría de esta forma:

    UDACCEL uda[] = {
        {0, 1},
        {5, 10},
        {10, 50},
        {15, 100}
    };

Es importante tener en cuenta que los tiempos se cuentan siempre desde el principio de la pulsación, de modo que la estructura anterior cambia los incrementos cada cinco segundos.

También hay que tener presente que los valores actuales del control no serán exactamente el resultado de aplicar el incremento sobre el valor actual, sino que los valores actuales serán múltiplos del incremento. Por ejemplo, si partimos de cero, y aplicamos la tabla de aceleradores anteriores, durant los primeros cinco segundos se aplican incrementos de una unidad. Los incrementos no son cada segundo, y desde el primero al segundo incremento pasa más tiempo que en los sucesivos, de modo que no podemos prever cual será el valor actual del cursor al cabo de cinco segundos, pero supongamos que es 42. A partir de ese momento, los incrementos serán múltiplos de diez, de modo que el siguiente valor no será 52, sino 50, y a partir de ahí, y durante los siguientes cinco segundos, los incrementos será de diez en diez. Y así sucesivamente.

Cada vez que se suelte el botón del ratón, la tabla de aceleradores volverá al comienzo.

Para asignar una tabla de aceleradores a un control se usa el mensaje UDM_SETACCEL, indicando como parámetro wParam el número de tramos, y en lParam el puntero al primer elemento del array de tramos:

        SendDlgItemMessage(hwnd, ID_UPDOWN2, UDM_SETACCEL, (WPARAM)sizeof(uda)/sizeof(UDACCEL), (LPARAM)uda);

Para recuperar la tabla de aceleradores de un control podemos usar el mensaje UDM_GETACCEL. En wParam indicaremos el número máximo de tramos que podemos recuperar, dependiendo del tamaño del array en el que los almacenaremos. En lParam indicaremos un puntero al primer elemento del array que recibirá la tabla de aceleradores. El valor de retorno será el número de tramos recuperados:

        int i;
        i=SendDlgItemMessage(hwnd, ID_UPDOWN3, UDM_GETACCEL, 4, (LPARAM)uda);

Bases de numeración

Cuando se usa el estilo UDS_SETBUDDYINT, el texto de la ventana amiga asociada al control se actualiza automáticamente según el valor actual del control. En este caso, tenemos dos posibilidades a la hora de elegir la base de numeración para expresar esos valores: decimal o hexadecimal. Para establecer el valor de la base de numeración a utilizar podemos usar el mensaje UDM_SETBASE, indicando en wParam el valor de la base deseada, que sólo puede ser 10 ó 16.

        SendDlgItemMessage(hwnd, ID_UPDOWN1, UDM_SETBASE, 16, 0);

Para recuperar el valor actual de la base de numeración utilizada por un control, se usa el mensaje UDM_GETBASE, con los dos parámetros a cero. El valor de la base se obtiene en el valor de retorno.

        i = SendDlgItemMessage(hwnd, ID_UPDOWN1, UDM_GETBASE, 0, 0);

Mensajes de notificación

Los controles UpDown pueden envíar tres mensajes diferentes a sus ventanas padre. Dos de ellos dependen del estilo, si es UDS_HORZ enviarán mensajes WM_HSCROLL cada vez que se pulse una de las flechas. Si no se especifica ese estilo, el control tendrá orientación vertical, y en su lugar enviará mensajes WM_VSCROLL.

Podemos usar estos mensajes para actualizar la ventana amiga asociada de forma manual, de modo que podamos personalizar el valor mostrado. De ese modo podemos añadir caracteres de formato, o trabajar con valores no enteros, o incluso con valores no numéricos, siempre y cuando a cada posible valor del ámbito de valores del control UpDown le hagamos corresponder un valor de otro dominio.

Por ejemplo, podemos asociar un control edit de sólo lectura a un control UpDown para elegir frutas dentro de los valores definidos en un array de cadenas:

    const char *fruta[] = {
        "Peras",
        "Manzanas",
        "Naranjas",
        "Melocotones",
        "Plátanos",
        "Fresas",
        "Castañas",
        "Nueces",
        "Piñas"
    };
...
        case WM_CREATE:
            CreateWindowEx(0, "EDIT", "", ES_READONLY | WS_VISIBLE | WS_CHILD | WS_BORDER,
                              10, 40, 120, 22, hwnd,(HMENU)ID_EDIT4, hInstance, NULL);
            CreateWindowEx(0, UPDOWN_CLASS, NULL,
                              WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_AUTOBUDDY | UDS_HOTTRACK,
                              0, 0,
                              0, 0,
                              hwnd, (HMENU)ID_UPDOWN4,
                              hInstance, NULL);
            SendDlgItemMessage(hwnd, ID_UPDOWN4, UDM_SETRANGE32, (WPARAM)0, (LPARAM)(sizeof(fruta)/sizeof(fruta[0])-1));
            SendDlgItemMessage(hwnd, ID_UPDOWN4, UDM_SETPOS32, 0, (LPARAM)0);
            SendDlgItemMessage(hwnd, ID_EDIT4, WM_SETTEXT, 0, (LPARAM)fruta[0]);
...
		case WM_VSCROLL:
            if((HWND)lParam == GetDlgItem(hwnd,ID_UPDOWN4)) {
               SendDlgItemMessage(hwnd, ID_EDIT4, WM_SETTEXT, 0, (LPARAM)fruta[SendDlgItemMessage(hwnd, ID_UPDOWN4, UDM_GETPOS32, 0, 0)]);
            }
            break;

Nada nos impide asociar el control UpDown con un control estático para elegir opciones mostrando iconos o mapas de bits.

El tercer mensaje es un mensaje de notificación, que se recibe a través de un mensaje WM_NOTIFY. Este mensaje se envía cada vez que la posición actual del control UpDown vaya a ser modificada. Al procesar este mensaje podemos permitir o evitar que tal modificación tenga efecto.

Podemos usar esta posibilidad para hacer que los rangos del valores del control UpDown varíen de forma dinámica, por ejemplo, en función de los valores de otros controles, o para restringir los valores dentro del rango definido por UDM_SETRANGE o UDM_SETRANGE32, etc.

Al recibir el mensaje de notificación, WM_NOTIFY, en lParam recibiremos un puntero a una estructura NMUPDOWN.

Esta estructura contiene en primer lugar una estructura NMHDR, que es una cabecera común a todos los mensajes de notificación que contiene un manipulador de ventana del control que envía el mensaje, el identificador del control que envía el mensaje y un código de notificación que especifica el motivo.

Usaremos el código para saber de qué mensaje de notificación se trata, y el identificador de control para saber qué control lo envía.

Los otros dos campos de la estructura NMUPDOWN, iDelta e iPos contienen, respectivamente, el próximo incremento del valor de la posición, y la posición actual del control UpDown.

Al procesar este mensaje, si retornamos con TRUE, no se tendrá en cuenta el incremento solicitado por el usuario. Si se retorna con FALSE, sí. Además, se tendrá en cuenta cualquier modificación que hagamos de los valores de iDelta o iPos, de modo que podremos influir en el valor actual del control, independientemente del incremento solicitado por el usuario y del valor previo del control.

Por ejemplo, podemos evitar que el control tome valores que sean múltiplos de tres:

    NM_UPDOWN *nmup;
    LPNMHDR pnmhdr;
...
        case WM_NOTIFY:
            pnmhdr = (LPNMHDR)lParam;
            switch(pnmhdr->code) {
                case UDN_DELTAPOS:
                    if(pnmhdr->idFrom == ID_UPDOWN3) {
                        nmup = (NM_UPDOWN*)lParam;
                        if(!((nmup->iPos+nmup->iDelta) % 3))
                            if(nmup->iDelta > 0) nmup->iDelta++; else nmup->iDelta--;
                        return FALSE;
                    }
                    break;
            }
            break;

Ejemplo 90

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 90 win090.zip 2021-07-29 3647 bytes 39