Capítulo 49 Ventana de estado

Las ventanas de estado son ventanas hijas que generalmente se colocan en la parte inferior de las ventanas, y que ocupan todo el ancho de la ventana padre. Se usan para mostrar mensajes de estado o de ayuda de la aplicación. Se trata de un control estático, ya que sólo se usa para mostrar información, y no para recogerla del usuario.

Cómo crear ventanas de estado

Hay dos formas de crear ventanas de estado. La más sencilla es usar la función CreateStatusWindow a la que proporcionaremos cuatro parámetros. El primero es una combinación de estilos de ventana, entre los que debe aparecer WS_CHILD, y por supuesto, WS_VISIBLE, si queremos que la ventana se muestre. El segundo es el texto que queremos que aparezca, y que seguramente varíe a lo largo de la ejecución del programa. El tercero es un manipulador de la ventana padre y el cuarto un identificador del control, que usaremos para enviarle mensajes a la ventana de estado.

           CreateStatusWindow(WS_CHILD|WS_VISIBLE, "Texto de prueba", hwnd, ID_STATUS);

La segunda forma es usar la función CreateWindowEx, especificando como clase de ventana el valor STATUSCLASSNAME. Las coordenadas y dimensiones de la ventana serán ignoradas, de modo que pueden ser cero.

           CreateWindowEx(0, STATUSCLASSNAME, "Texto de prueba", WS_CHILD|WS_VISIBLE,
		       0, 0, 0, 0, hwnd, (HMENU)ID_STATUS, hInstance, 0);

Como en todos los controles comunes que estamos viendo, hay que asegurarse de que la DLL ha sido cargada invocando a la función InitCommonControls.

Estilos

Por defecto, las ventanas de estado se colocan en la parte inferior de la ventana padre, es decir, estas ventanas tienen activo por defecto el estilo CCS_BOTTOM. También se puede especifiar el estilo CCS_TOP, que la coloca en la parte superior, aunque no es nada habitual.

También se puede usar el estilo SBARS_SIZEGRIP para incluir un mapa de bits a la derecha de la barra que se usa para redimensionar la ventana. Aunque ese estilo se puede combinar con CCS_TOP, carece de sentido hacerlo, ya que no funcionará.

Cuando se usan barras de estado es importante que el procedimiento de ventana de la ventana padre procese el mensaje WM_SIZE, de modo que cada vez que la ventana padre cambie de tamaño, se envíe el mensaje a la ventana de estilo para que se adapte al nuevo tamaño de su ventana padre.

        case WM_SIZE:
           SendDlgItemMessage(hwnd, ID_STATUS, WM_SIZE, 0, 0);
           break;

Ayuda para menús

Una de las aplicaciones de la ventana de estado es mostrar ayudas contextuales en función de la opción de menú actualmente seleccionada.

Para ello se usa la función MenuHelp. Según la documentación del API, esta función procesa los mensajes WM_MENUSELECT y WM_COMMAND.

Hay que pasar siete parámetros a la función, el primero es el mensaje, WM_MENUSELECT o WM_COMMAND, seguido de los parámetros wParam y lParam del mensaje. El cuarto es un manipulador del menú d ela ventana. El Quinto un manipulador de la instancia desde la que se cargarán las cadenas con los mensajes de ayuda. El sexto es un manipulador de la ventana de estado y el séptimo y último es un array de enteros con los desplazamientos de los identificadores de las cadenas con respecto a los identificadores de comandos asociados al menú.

En un menú sencillo, con un único nivel de submenús, el primer valor en el array de desplazamientos es el desplazamiento entre los identificadores de ítem de menú y sus cadenas de ayuda. Por ejemplo, si los identificadores de menú empiezan en 100, y los de las cadenas asociadas en 200, el primer valor del array será 100 (200-100). El segundo valor se usa para las cadenas de los popups, que no tienen identificador de menú. En su lugar se usa la posición, 0 para el primero, 1 para el segundo, etc. De modo que si el primer identificador de cadena para los menús popup es 800, ese será el valor del desplazamiento almacenado en el array de desplazamientos.

El array de desplazamientos contiene parejas de enteros sin signo, de modo que debe tener un número par de valores. Además, el final del array se marca con dos valores cero.

    static UINT desplazamientos[] = {
        100, 800,
        0, 0
    };
...
	case WM_MENUSELECT:
           MenuHelp(msg, wParam, lParam, GetMenu(hwnd), hInstance, GetDlgItem(hwnd, ID_STATUS), ids);
           break;

Si tenemos un segundo nivel de menús desplegables, tendremos que añadir una nueva pareja de valores al array de desplazamientos. El primer valor de la pareja será un desplazamiento entre identificadores de menú y de cadena y el segundo el número de orden del submenú.

Esto implica algunos problemas si, por ejemplo, nuestro menú tiene dos opciones de menú horizontal, y en la primera posición del segundo hay un submenú popup. El desplazamiento de ese segundo submenú es 0:

LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
menu MENU
{
    POPUP "Principal"
    {
        MENUITEM "Nuevo", IDM_NUEVO
        MENUITEM "Abrir...", IDM_ABRIR
        MENUITEM "Guardar", IDM_GUARDAR
        MENUITEM "Guardar como...", IDM_GUARDAR_COMO
        MENUITEM SEPARATOR
        MENUITEM "Salir", IDM_SALIR
    }
    POPUP "Ver"
    {
        POPUP "Tamaño de letra"
        {
            MENUITEM "Pequeña", IDM_PEQUE
            MENUITEM "Mediana", IDM_MEDIANA
            MENUITEM "Grande", IDM_GRANDE
        }
        MENUITEM "Barra", IDM_BARRA
    }
}

LANGUAGE LANG_SPANISH, SUBLANG_SPANISH_MODERN
STRINGTABLE
{
    IDS_NUEVO                   "Nuevo fichero"
    IDS_ABRIR                   "Abrir un fichero existente"
    IDS_GUARDAR                 "Guardar fichero actual"
    IDS_GUARDAR_COMO            "Guardar una copia del fichero actual con otro nombre"
    IDS_SALIR                   "Salir de la aplicación"
    IDS_BARRA                   "Mostrar u ocultar barra de estado"
    IDS_PEQUE                   "Establece un tamaño de letra pequeño"
    IDS_MEDIANA                 "Establece un tamaño de letra normal"
    IDS_GRANDE                  "Establece un tamaño de letra grande"
    IDS_PRINCIPAL               "Menú principal"
    IDS_VER                     "Opciones de visualización"
    IDS_TAMANO                  "Opciones para el tamaño de letra"
}

Para esta estructura de menú, los valores del array de desplazamientos son:

    static UINT desplazamientos[] = {
        100, 800,
        802, 0,
        0, 0
    };

Pero, tal como está diseñada la función, se cargará la misma cadena para todos los menús desplegables con desplazamiento cero, es decir, para el menú principal y para el de tamaño de letra.

Aunque hay algunas cosas que podemos hacer para que funcione correctamente con nuestros menús, las soluciones serán engorrosas y posiblemente no funcionen en futuras versiones del API. Lo más habitual es no mostrar textos de ayuda para los menús popup, sino sólo para las opciones de ítems de menú.

Nota:

Además, esta función tiene otras limitaciones que desaconsejan o limitan su uso en nuestros programas, por ejemplo, no es fácil que funcione correctamente con menús creados en la ejecución, ya que las cadenas se cargan desde recursos de cadena. Todas las cadenas se cargan desde la misma instancia, de modo que para los menús creados desde varias dll no funcionará bien.

Ejemplo 83

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 83 win083.zip 2012-06-15 3327 bytes 29

Tamaño y altura

Generalmente, las dimensiones de las barras de estado se ajustan automáticamente por su procedimiento de ventana, ignorando cualquier tamaño indicado por la aplicación. La anchura es la misma que la del área de cliente de la ventana padre, y la altura se ajusta en función del tamaño de fuente seleccionada por el contexto de dispositivo de la ventana de estado.

Por eso es importante, como ya comentamos antes, que la ventana padre procese el mensaje WM_SIZE, y lo reenvíe a la ventana de estado.

Sin embargo, la aplicación puede asignar la altura mínima de la ventana de estado, o más concretamente, del área útil de la ventana de estado, descontando los bordes. Esto se hace con el mensaje SB_SETMINHEIGHT, indicando en wParam la altura mínima, en pixels. Generalmente esto sólo será necesario en barras de estado owner-draw.

También podemos recuperar la dimensiones de los bordes de una barra de estado mediante el mensaje SB_GETBORDERS. En el parámetro lParam tendremos que pasar la dirección de un array de tres enteros en los que recibiremos las anchuras del borde horizontal, vertical y el la distancia entre los rectángulos interiores que contienen el texto, respectivamente.

Ventanas de estado con varias partes

Podemos dividir la barra de estado en partes de diferente anchura, cada una de las cuales puede mostrar un texto diferente, que usaremos para informar al usuario sobre el estado de ciertos valores de la aplicación.

Para dividir la barra de estado en partes se usa un mensaje SB_SETPARTS, indicando en el parámetro wParam el número de partes y el lParam la dirección de un array con tantos valores de enteros como partes, cada uno de los cuales es la distancia, desde el borde izquierdo de la ventana al borde derecho de cada parte, en pixels y en coordenadas de cliente.

Para que la barra de estado ocupe todo el ancho de la ventana padre es necesario que el valor de anchura para la última parte coincida con la anchura del área de cliente. También se puede usar el valor -1 para esa última anchura, de modo que la última parte se ensanchará automáticamente hasta ocupar toda la anchura disponible.

    int anchura[] = {54, 74, 94, -1};
...
		CreateStatusWindow(WS_CHILD|WS_VISIBLE, "Texto de prueba", hwnd, ID_STATUS);
		SendDlgItemMessage(hwnd, ID_STATUS, SB_SETPARTS, 4, (LPARAM)anchura);

Pero probablemente estemos acostumbrados a que la barra de estado ocupe todo el ancho de la ventana, y que la parte que ajusta la anchura sea la primera, y no la última, siendo el resto de las partes siempre igual de anchas.

Para conseguir ese efecto aprovecharemos el procesamiento del mensaje WM_SIZE, obtendremos la anchura del área de cliente, y ajustaremos las anchuras de cada parte en función de esa anchura.

    RECT re;
    int anchura[4];
...
        case WM_SIZE:
           GetClientRect(hwnd, &re);
           anchura[3] = re.right-20;
           anchura[2] = anchura[3]-60;
           anchura[1] = anchura[2]-60;
           anchura[0] = anchura[1]-60;
           SendDlgItemMessage(hwnd, ID_STATUS, SB_SETPARTS, 4, (LPARAM)anchura);
           SendDlgItemMessage(hwnd, ID_STATUS, msg, wParam, lParam);

El número de partes en las que se puede dividir una ventana de estados está limitado a 255. Se trata de un límite poco importante, ya que generalmente usaremos muchas menos partes.

También es posible obtener las dimensiones actuales de las partes de una ventana de estado, usando un mensaje SB_GETPARTS. En wParam indicaremos el número de partes a recuperar, si ese número es mayor que el número de partes existentes, sólo se recuperarán las que existan. En lParam pasaremos un puntero a un array de enteros en los que recibiremos las distancias de los bordes derechos de cada parte. El valor de retorno es el número de partes cuyas anchuras son recuperadas. Si se usa el valor cero para lParam, recuperaremos sólo el número de partes.

		int nPartes;
		int *anchura;
		anchura = (int*)malloc(sizeof(int)*10);
		nPartes = SendDlgItemMessage(hwnd, ID_STATUS, SB_GETPARTS, 10, anchura));
		free(anchura);

Manejar texto

En cada parte de una ventana de estado se puede modificar el texto o recuperarlo.

Para modificarlo se usa el mensaje SB_SETTEXT. En el parámetro wParam se indica el índice, empezando en cero, de la parte en que se quiere modificar el texto, combinado con el tipo de texto a mostrar. Ese tipo puede ser 0, que indica que se trace un borde hundido, SBT_POPOUT que indica un borde sobresaliente, SBT_NOBORDERS que indica que no se tracen bordes, SBT_OWNERDRAW para ventanas de estado owner-draw o SBT_RTLREADING para lenguajes que se escriben de derecha a izquierda.

En lParam se pasa un puntero a la cadena terminada con cero que se debe mostrar, o un valor entero de 32 bits, en caso de ventanas de estado owner-draw.

	SendDlgItemMessage(hwnd, ID_STATUS, SB_SETTEXT, 3|SBT_POPOUT, (LPARAM)"\tbloq num");

Por otra parte, podemos usar caracteres tabuladores ('\t') para modificar la posición del texto. Si no se usa ninguno, la cadena se alinéa a la izquierda de la parte indicada. El texto a la derecha de un tabulador se muestra centrado en la parte indicada, y el texto después del segundo tabulador se muestra alineado a la derecha.

Para recuperar texto desde una parte de una ventana de estado usaremos el mensaje SB_GETTEXT, y para calcular la longitud del texto en una parte, SB_GETTEXTLENGTH.

En el mensaje SB_GETTEXT indicaremos en el parámetro wParam el índice, basado en cero, de la parte cuyo texto queremos recuperar y en lParam un puntero a un buffer que recibirá la cadena. El valor de retorno indicará el tipo de borde usado para trazar el texto en la palabra de mayor peso, y la longitud de la cadena recuperada en la de menor peso.

En el mensaje SB_GETTEXTLENGTH sólo usaremos el parámetro wParam para indicar el índice de la parte. El valor de retorno será similar al del mensaje SB_GETTEXT.

    char *txt;
    int len;
...
           len = LOWORD(SendDlgItemMessage(hwnd, ID_STATUS, SB_GETTEXTLENGTH, 3, 0));
           txt = (char*)malloc(len+1);
           SendDlgItemMessage(hwnd, ID_STATUS, SB_GETTEXT, 3, (LPARAM)txt);
...
           free(txt);

Si se trata de una ventana de estado con un única parte, se pueden usar los mensajes WM_SETTEXT, WM_GETTEXT y WM_GETTEXTLENGTH, tratando la ventana de estado como un simple control de texto estático.

Finalmente, hay otra posibilidad de mostrar texto en una ventana de estado sin necesidad de crearla, usando la función DrawStatusText. Aunque en realidad lo que muestra se parece más a un control de texto estático.

El primer parámetro es un manipulador del DC de la ventana, el segundo un puntero a una estructrua RECT con las coordenadas de la zona ocupada por el texto. Este rectángulo se usa para mostrar los bordes y la posición del texto. El tercer parámetro es el texto a mostrar, los tabuladores funcionan de forma similar a como lo hacen en el mensaje SB_SETTEXT. El cuarto parámetro indica el tipo de bordes a trazar.

    HDC hdc;
	RECT re;

    hdc = GetDC(hwnd);
    re.left = 20; re.top = 10;
    re.right = 150; re.bottom = 30;
    DrawStatusText(hdc, &re, "\tTexto estático", SBT_POPOUT);
    ReleaseDC(hwnd, hdc);

Ejemplo 84

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 84 win084.zip 2012-06-15 2443 bytes 29

Ventanas de estado owner-draw

Cada una de las partes de una ventana de estado se puede definir como owner-draw. Esto proporciona un mayor control sobre los contenidos de esas partes, ya que nos permite incluir mapas de bits, o en general, cualquier gráfico GDI que queramos, en lugar de sólo texto.

Para definir una parte como owner-draw basta con enviar un mensaje SB_SETTEXT, añadiendo el tipo SBT_OWNERDRAW al identificador de la parte, en el parámetro wParam, y usando el parámetro lParam para un valor de 32 bits que posteriormente se usará para dibujar el contenido de la parte. Ese parámetro puede ser un puntero a una estructura, un mapa de bits, un entero, etc. El significado del valor queda definido por la aplicación, es decir, por nosotros.

        SendDlgItemMessage(hwnd, ID_STATUS, SB_SETTEXT, 2|SBT_OWNERDRAW, lParam);
...
        SendDlgItemMessage(hwnd, ID_STATUS, SB_SETTEXT, 3|SBT_OWNERDRAW, (LPARAM)hBitmapSi);

Cada vez que la aplicación necesita actualizar el contenido de una parte owner-draw de una ventana de estado, se envía un mensaje WM_DRAWITEM a la ventana padre. El parámetro wParam del mensaje contiene el identificador de ventana de la ventana de estado, y el parámetro lParam es un puntero a una estructura DRAWITEMSTRUCT. Toda la información necesaria para dibujar el contenido de la parte está incluida en esta estructura.

Cuando el mensaje WM_DRAWITEM es recibido para mostrar una parte de una barra de estado owner-draw el significado de algunos campos de la estructura DRAWITEMSTRUCT es algo diferente de la que se explica en la documentación:

Miembro Descripción
CtlType No definido, no se usa.
CtlID Identificador de la barra de estado.
itemID Índice de la parte a dibujar.
itemAction No definido, no se usa.
itemState No definido, no se usa.
hwndItem Manipulador de la ventana de estado.
hDC Manipulador del contexto de dispositivo de la ventana de estado.
rcItem Coordenadas de la parte de la ventana a dibujar. Estas coordenadas son relativas a la esquina superior izquierda de la ventana de estado.
itemData Valor de 32 bits definido por la aplicación especificado mediante el parámetro lParam del mensaje SB_SETTEXT.
    DRAWITEMSTRUCT *dis;
    HDC memDC;
    POINT ptCur;
    char cad[40];
...
        case WM_DRAWITEM:
           dis = (DRAWITEMSTRUCT*)lParam;
           switch(dis->itemID) {
             case 2:
               memDC = CreateCompatibleDC(dis->hDC);
               SelectObject(memDC, hBitmapCoor);
               ptCur.x = LOWORD(dis->itemData);
               ptCur.y = HIWORD(dis->itemData);
               sprintf(cad, "%d,%d", ptCur.x, ptCur.y);
               SetBkMode(dis->hDC, TRANSPARENT);
               TextOut(dis->hDC, dis->rcItem.left+20, dis->rcItem.top, cad, strlen(cad));
               BitBlt(dis->hDC, dis->rcItem.left, dis->rcItem.top, 16, 16, memDC, 0, 0, SRCCOPY);
               DeleteDC(memDC);
               break;
             case 3:
               memDC = CreateCompatibleDC(dis->hDC);
               SelectObject(memDC, (HBITMAP)dis->itemData);
               BitBlt(dis->hDC, dis->rcItem.left, dis->rcItem.top, 16, 16, memDC, 0, 0, SRCCOPY);
               DeleteDC(memDC);
               break;
             default:
               break;
           }
           break;

En este ejemplo vemos cómo utilizamos los valores de la estructrua DRAWITEMSTRUCT para discriminar la parte a dibujar, seleccionar el DC de la ventana de salida, obtener las coordenadas y el valor del parámetro. En el caso de la parte 2, el parámetro son las coordenadas del ratón, en la palabra de menor peso la coordenada x y en la de mayor peso la coordenada y. En el caso de la parte 3, lParam contiene un manipulador de mapa de bits.

Ejemplo 85

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 85 win085.zip 2012-06-15 3502 bytes 27

Ventanas de estado simples

Siempre se puede cambiar el tipo de ventana de estado al modo simple (con una única parte), mendiante un mensaje SB_SIMPLE. El texto asignado a la ventana de estado simple se mantiene almacenado de forma independiente del de las partes cuando no está en modo simple, de modo que se puede cambiar de un modo a otro sin que se pierdan los contenidos de ninguna parte. Es habitual que los textos de ayuda de los menús se muestren en modo simple, y una vez el menú ha perdido el foco, se vuelva al modo no simple, sin necesidad de actualizar cada parte.

La única limitación a tener en cuenta en ventanas de estado simples es que no adminten el modo owner-draw.