56 Control de calendario

Ejemplo de control calendario mensual
Ejemplo de control calendario mensual

El control de calendario mensual permite elegir fechas mediante la selección desde un calendario que muestra uno o más meses. Estos controles proporcionan una forma sencilla de seleccionar fechas o intervalos de fechas.

Como en todos los controles comunes que estamos viendo, hay que asegurarse de que la DLL ha sido cargada invocando a la función InitCommonControlsEx indicando el valor de bandera ICC_DATE_CLASSES en el miembro dwICC de la estructura INITCOMMONCONTROLSEX que pasaremos como parámetro.

  INITCOMMONCONTROLSEX iCCE;
...
  iCCE.dwSize = sizeof(INITCOMMONCONTROLSEX);
  iCCE.dwICC = ICC_DATE_CLASSES;
  InitCommonControlsEx(&iCCE);

Insertar durante la ejecución

Como todos los controles que hemos visto hasta ahora, los de calendario también se pueden insertar durante la ejecución. Tan sólo hay que crear una ventana de la clase "MONTHCAL_CLASS". Para hacerlo usaremos las funciones CreateWindow o CreateWindowEx.

        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            CreateWindowEx(0, MONTHCAL_CLASS, NULL,
                WS_BORDER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
                10,10,200,200,
                hwnd, (HMENU)ID_CALENDAR1,
                hInstance, NULL);
            break;

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

Si queremos cambiar la fuente usada para mostrar el control, habrá que crear una fuente y asignársela usando un mensaje WM_SETFONT. Y no hay que olvidar liberar estos 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;

Un control de calendario mensual puede mostrar uno o más meses. El número de meses que se mostrarán dependerá de las dimensiones del control, tanto en anchura como en altura.

En la imagen del ejemplo se muestran tres meses en sentido horizontal, pero si las dimensiones del control lo permiten se podrían mostrar en vertical o en una cuadrícula con varias columnas y filas.

Disponemos de dos mensajes para ayudarnos a calcular las dimensiones del control:

  • MCM_GETMINREQRECT: calcula las dimensiones mínimas para mostrar un único mes en el control. En lParam tenemos que pasar un puntero a una estructura RECT que recibirá el tamaño mínimo del control.
  • MCM_SIZERECTTOMIN: calcula las dimensiones mínimas para mostrar varios meses en el control. En este caso deberemos pasar en lParam un puntero a una estructura RECT con las dimensiones aproximadas a la entrada, y nos devolverá, en la misma estructura, las dimensiones corregidas para mostrar los meses que caben en el rectángulo de entrada.

En el primer caso, los miembros left y top serán cero, y en right y bottom se devolverá la anchura y altura del control, respectivamente.

En el segundo caso la anchura disponible se calcula como right-left y la altura como bottom-top, y al retornar, left y top serán cero, y right y bottom serán la anchura y altura, respecitivamente.

El mensaje MCM_SIZERECTTOMIN sólo funciona si están activos los estilos visuales, usando el manifiesto adecuado.

En ambos casos deberemos usar la función MoveWindow para redimensionar el control.

Ajustar el tamaño del control para mostrar un mes:

    // Ejemplo para mostrar un mes:
    RECT re;
...
        SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETMINREQRECT, 0,(LPARAM)&re);
        MoveWindow( GetDlgItem(hwnd, ID_CALENDAR1), 10, 10, re.right, re.bottom, TRUE);

Ajustar el tamaño del control para mostrar varios meses:

    // Ejemplo para mostrar varios meses:
    RECT re;
...
        re.top = 10;
        re.left = 20;
        re.right = 500;
        re.bottom = 200;
        SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETMINREQRECT, 0,(LPARAM)&re);
        MoveWindow( GetDlgItem(hwnd, ID_CALENDAR1), 40, 40, re.right, re.bottom, TRUE);

Alternativamente se pueden usar las macros MonthCal_SizeRectToMin y MonthCal_GetMinReqRect en lugar de MCM_SIZERECTTOMIN y MCM_GETMINREQRECT, respectivamente.

Desde fichero de recursos

También podemos insertar controles de calendario en nuestros cuadros de diálogo usando ficheros de recursos.

Esto nos facilita las cosas, sobre todo si diseñamos nuestros recursos usando un editor.

Se usa un control general CONTROL, con la clase MONTHCAL_CLASS, y los estilos generales y específicos que queramos aplicar.

LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_DIALOG1 DIALOG 0, 0, 331, 225
STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | DS_SHELLFONT | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "Ms Shell Dlg"
{
    CONTROL         "", 0, MONTHCAL_CLASS, WS_TABSTOP | MCS_NOTODAY, 13, 10, 250, 202, WS_EX_LEFT
    DEFPUSHBUTTON   "OK", IDOK, 269, 10, 50, 14, 0, WS_EX_LEFT
    PUSHBUTTON      "Cancel", IDCANCEL, 269, 27, 50, 14, 0, WS_EX_LEFT
}

Estilos

Disponemos de algunos estilos específicos para estos controles que permiten personalizar su aspecto y comportamiento.

  • MCS_DAYSTATE: permite mostrar algunas fechas en negrita, para ello el control envíará códigos de notificación MCN_GETDAYSTATE para solicitar a la aplicación la información necesaria.
  • MCS_MULTISELECT: permite que el usuario pueda seleccionar un rango de fechas.
  • MCS_WEEKNUMBERS: muestra a la izquierda de cada fila de cada calendario el número de la semana dentro del año.
  • MCS_NOTODAYCIRCLE: oculta el resaltado del día actual en el calendario correspondiente.
  • MCS_NOTODAY: oculta la leyenda al pié del calendario con la fecha del día actual. Esta leyenda actúa, cuando es visible, como un botón que selecciona la fecha actual en el control.
  • MCS_NOTRAILINGDATES: oculta las fechas de los primeros días de la primera semana correspondientes al mes anterior al primero mostrado y las de los últimos días de la última semana correspondientes a la mes siguiente al último mostrado.
  • MCS_SHORTDAYSOFWEEK: las leyendas para los días de la semana se muestran con una letra (L, M, X, etc) en lugar de usar tres caracteres (lu., ma., mi., etc).
  • MCS_NOSELCHANGEONNAV: si el usuario ha seleccionado alguna fecha o un rango de fechas, la selección no cambia si al navegar a otros meses desaparece del rango de meses mostrado en el control. De este modo el usuario puede seleccionar más fechas de las que son visibles en el control.

Control de calendario de selección simple

Por defecto, salvo que se especifique el estilo MCS_MULTISELECT, los controles de calendario sólo permiten seleccionar una fecha.

Seleccionar fecha

Para cambiar la fecha seleccionada en un control de calendario podemos usar el mensaje MCM_SETCURSEL, indicando el lParam un puntero a una estructura SYSTEMTIME con la fecha a seleccionar. Sólo se usaran los miembros relativos a la fecha, y el resto se ignoran.

    SYSTEMTIME fecha;
    ...
    fecha.wDay = 23;
    fecha.wMonth = 3;
    fecha.wYear = 2023;
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETCURSEL, 0, (LPARAM)&fecha);

La macro MonthCal_SetCurSel es equivalente al mensaje MCM_SETCURSEL.

Obtener fecha seleccionada

Para obtener el valor de la fecha actualmente seleccionada usaremos el mensaje MCM_GETCURSEL, pasando en lParam un puntero a una estructura SYSTEMTIME, que recibirá el valor de la fecha actualmente seleccionada en el control.

SYSTEMTIME fecha;
    ...
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETCURSEL, 0, (LPARAM)&fecha);

La macro MonthCal_GetCurSel es equivalente.

Selección múltiple

Si se especifica el estilo MCS_MULTISELECT, el usuario podrá seleccionar un rango de fechas. Si no se modifica explícitamente, el rango máximo es una semana.

Si se selecciona más de una fecha, deben ser consecutivas. La selección se hace pulsando con el botón izquierdo sobre una fecha y, manteniendo pulsado el botón, moverse a otra fecha. También se puede seleccionar una fecha y, manteniendo pulsada la tecla SHIFT, seleccionar una segunda fecha.

Si entre la primera y la segunda fecha seleccionadas hay más días que el máximo rango permitido, sólo se seleccionará el número máximo de días a partir de la primera selección.

Asignación de varias fechas

Para asignar un rango de fechas a un control de calendario se usa el mensaje MCM_SETSELRANGE, indicando en lParam un puntero a un array de dos elementos que contendrán las fechas mínima y máxima a seleccionar.

El orden en que se indiquen las fechas es indiferente, pero es importante que el rango sea menor o igual al máximo permitido para el control, o el mensaje fallará.

SYSTEMTIME fecha[2];
...
    fecha[0].wDay = 20;
    fecha[0].wMonth = 8;
    fecha[0].wYear = 2048;
    fecha[1].wDay = 15;
    fecha[1].wMonth = 8;
    fecha[1].wYear = 2048;
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETSELRANGE, 0, (LPARAM)&fecha);

La macro MonthCal_SetSelRange es equivalente.

Obtener asignación múltiple

Para obtener el rango de fechas seleccionadas en el control se usa el mensaje MCM_GETSELRANGE, pasando en lParam un puntero a un array de dos elementos que recibirá las fechas que definen el rango.

SYSTEMTIME fecha[2];
...
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETSELRANGE, 0, (LPARAM)&fecha);

También se puede usar la macro equivalente MonthCal_GetSelRange.

Rango máximo de selección

Para modificar el rango máximo de fechas que se pueden seleccionar en un control de calendario usaremos el mensaje MCM_SETMAXSELCOUNT, indicando en wParam el nuevo valor máximo del rango.

    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETMAXSELCOUNT, (WPARAM)30, 0);

O usar la macro equivalente MonthCal_SetMaxSelCount.

Para obtener el valor del rango máximo seleccionable se usa el mensaje MCM_GETMAXSELCOUNT, o la macro MonthCal_GetMaxSelCount.

Selección fuera de la vista

Por defecto, si el ususario navega a través del calendario y la selección actual queda fuera de la vista, automáticamente se seleccionará un nuevo rango del mismo tamaño en la vista actual. Esto también se aplica a controles de calendario de selección simple.

Podemos evitar este comportamiento asignado el estilo MCS_NOSELCHANGEONNAV. De este modo la selección actual se mantiene aunque no sea visible en el control.

Esta es la única forma de que un usuario pueda seleccionar rangos más grandes de los que permite mostrar el control.

Fechas seleccionables

Si queremos limitar el rango de fechas que el usuario puede seleccionar disponemos del mensaje MCM_SETRANGE. En wParam indicaremos una combinación de los valores GDTR_MAX y GDTR_MIN, dependiendo de que límites queramos establecer, y en lParam pasaremos un puntero a un array de dos elementos con las fechas mínima y máxima permitidas.

Si sólo queremos establecer el límite superior usaremos el valor GDTR_MAX en wParam, y si sólo queremos establecer un límit inferior, el valor GDTR_MIN. En los dos casos hay que pasar el array con dos elementos, pero sólo se tendrán en cuenta los valores en función del valor de wParam.

SYSTEMTIME fecha[2];
...
    /* Sólo se permiten fechas entre el 1 de enero y el 31 de diciembre de 2022 */
    fecha[0].wDay = 1;
    fecha[0].wMonth = 1;
    fecha[0].wYear = 2022;
    fecha[1].wDay = 31;
    fecha[1].wMonth = 12;
    fecha[1].wYear = 2022;
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETRANGE, (WPARAM)(GDTR_MAX|GDTR_MIN), (LPARAM)&fecha);

La macro MonthCal_SetRange es equivalente.

Para obtener el rango de fechas disponibles en un control de calendario se usa el mensaje MCM_GETRANGE, indicando en lParam un array de dos elementos que recibirá las fechas mínima y máxima del rango seleccionable por el usuario.

El valor de retorno será una combinación de los valores GDTR_MAX y GDTR_MIN, que indicará qué elementos del array contienen valores válidos. Si el valor de retorno es cero, significa que no se han establecido límites.

También podemos usar la macro MonthCal_GetRange para esta tarea.

Aspecto gráfico

Disponemos de varias opciones para modificar el aspecto en pantalla de los controles de calendario, tamaños, colores, fuentes, etc.

Borde

Calendario con margen
Calendario con margen

Cuando hablamos del borde de un control de calendario nos referimos al margen entre el límite exterior del control y el conjunto de los calendarios de cada mes que se muestran en el interior. Ese margen rodea a todos los meses.

Podemos establecer el tamaño del borde mediante un mensaje MCM_SETCALENDARBORDER. En wParam indicaremos un valor de tipo BOOL, TRUE indicará que queremos establecer un nuevo valor para la anchura del borde en pixels, indicado en lParam, y FALSE para restablecer el valor por defecto.

Es importante establecer el valor del borde antes de calcular el tamaño del control. Al igual que la fuente, el tamaño del borde influye en el cálculo del tamaño del control.

La macro MonthCal_SetCalendarBorder se despliega como un envío de éste mensaje.

    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETCALENDARBORDER, (WPARAM)TRUE, (LPARAM)30);

Para recuperar el valor actual del tamaño del borde se usa el mensaje MCM_GETCALENDARBORDER, o la macro equivalente MonthCal_GetCalendarBorder.

Colores

Podemos modificar los colores de un control de calendario, pero sólo si no están activos los estilos visuales. Para ello disponemos del mensaje MCM_SETCOLOR.

En wParam indicaremos de qué parte del control queremos cambiar el color, y en lParam el color que vamos a asignar, en formato COLORREF.

Para indicar qué parte del control queremos modificar exiten varias constantes:

  • MCSC_BACKGROUND: color de fondo de la separación entre meses.
  • MCSC_MONTHBK: corresponde a la zona 'fondo' de la imagen. El color de fondo de la zona donde se muestran los días.
  • MCSC_TEXT: corresponde a la zona 'texto' de la imagen. El color del texto de los días del mes actual.
  • MCSC_TRAILINGTEXT: corresponde a la zona 'Texto extremos'. El color del texto de los días correspondientes al mes anterior y siguiente al actual.
  • MCSC_TITLEBK: corresponde a la zona 'Fondo de título'.
  • MCSC_TITLETEXT corresponde a la zona 'Texto de título'.

Para recuperar el color actualmente asignado a una de estas zonas se puede usar el mensaje MCM_GETCOLOR, indicando en wParam la constante correspondiente a la zona cuyo color queremos recuperar.

Alternativamente, también se pueden usar las macros MonthCal_SetColor y MonthCal_GetColor, respectivamente.

Estado de días

Si el estilo MCS_DAYSTATE está activo, podremos especificar un estado resaltado para cada día de los meses mostrados en el control, que se indicará mostrando el texto del día correspondiente en negrita.

Como cada mes tiene como máximo 31 días, y el estado de resalte es un valor binario, se usa un valor de 32 bits para especificar el estado de todos los días de un mes. Más concretamente, se usa un valor de tipo MONTHDAYSTATE. Los bits con valor 1 indicarán que el día correspondiente se deberá mostrar resaltado.

Por otra parte, como un control de calendario puede mostrar varios meses, tendremos que especificar tantas de estas estructuras como meses contenga el control.

Así, usando el mensaje MCM_SETDAYSTATE podemos establecer qué días se mostrarán resaltados en un control de calendario, indicando en lParam la dirección de un array de elementos de tipo MONTHDAYSTATE, uno por cada mes a asignar, y en wParam el número de elementos que contiene el array.

También podemo usar la macro MonthCal_SetDayState.

Evidentemente, cada vez que los meses mostrados en el control cambien deberemos asignar los nuevos estados a los meses mostrados.

Pero todo esto es mucho más sencillo si procesamoe el código de notificación MCN_GETDAYSTATE.

Nota:

Hay un error en el fichero de cabecera "commctrl.h" que se incluye con MinGW en la definición de este código de notificación.
Donde pone:
#define MCN_GETDAYSTATE (MNN_FIRST+3)
Debe poner:
#define MCN_GETDAYSTATE (MCN_FIRST-1)

Siempre que tengamos asignado el estilo MCS_DAYSTATE, cada vez que cambien los meses mostrados en el control recibiremos un código de notificación MCN_GETDAYSTATE. En lParam tendremos un puntero a una estructura NMDAYSTATE con toda la información necesaria para actualizar el estado de los meses mostrados en el control.

Como toda las estructuras recibidas en códigos de notificación, esta también contiene en primer lugar una estructura NMHDR con la información relativa a la notificación (manipulador de ventana, identificador y código de notificación). Además contiene una estructura SYSTEMTIME, con la fecha de comienzo del rango requerido, un puntero a un array de valores MONTHDAYSTATE, que deberemos asignar al procesar el mensaje y un valor entero, con el número de elementos que debe contener ese array.

Por ejemplo, el control de la imagen anterior está mostrando dos meses, enero y febrero de 2022. Enero empieza en sábado, lo que implica que deberán mostrarse los últimos días del mes anterior de esa semana. Esto es así aunque hayamos asignado el estilo MCS_NOTRAILINGDATES, que oculta esos días.

El segundo mes termina en lunes, lo que implica que deberán mostrarse los primeros días del mes siguiente. Por lo tanto el array deberá contener cuatro elementos y la fecha que se solicitará será la del día uno del mes anterior al primero, es decir, el 27 de diciembre de 2021 (el último lunes de diciembre de 2021).

    /* En este ejemplo supondremos que sólo se pueden seleccionar fechas del año 2022 */
    /* El array 'estados' contiene los estados resaltados para los sábados y domingos desde 
       diciembre de 2021 a enero de 2023 */
    MONTHDAYSTATE estados[] =
    {0x3060c18,
     0x3060c183, 0x60c1830, 0x60c1830, 0x20c18306, 0x183060c1, 0x3060c18,
     0x60c18306, 0xc183060, 0x183060c, 0x3060c183, 0x60c1830, 0x4183060c,
     0x183060c1};
    LPNMDAYSTATE lpnmDS;
...    
    case WM_NOTIFY:
            lpnmDS = (LPNMDAYSTATE)lParam;
            switch(lpnmDS->nmhdr.code) {
                case MCN_GETDAYSTATE:
                    if(lpnmDS->stStart.wYear == 2021)
                        lpnmDS->prgDayState = &estados[0];
                    else
                        lpnmDS->prgDayState = &estados[lpnmDS->stStart.wMonth];
                    return 1;

Lo cierto es que de la fecha recibida sólo nos interesa el mes y el año, el resto de los datos son irrelevantes.

El día será el correspondiente al primer día de la semana del primer mes, aunque ese día corresponda al mes anterior. El valor de wDayOfWeek será válido y nos puede servir para generar los valores de estados automáticamente.

Los bits en la estructura MONTHDAYSTATE se empiezan a contar a partir de la derecha, el bit menos significativo corresponde al primer día del mes.

Las operaciones de rotación de bits son sencillas, para rotar un bit a la izquerda el valor contenido en un entero basta multiplicar por dos, o podemos usar directamente el operador de rotación de bits. Por ejemplo, si en el ejemplo anterior queremos resaltar el 6 de enero de 2022 podemos usar esta sentencia:

    estados[1] |= (MONTHDAYSTATE)(0x1 << 5);

Calendarios contenidos

Disponemos de un mensaje para obtener información sobre el número de calendarios. El mensaje MCM_GETCALENDARCOUNT nos devuelve el número de calendarios actualmente mostrados en el control, no es necesario indicar ningún parámetro.

La macro MonthCal_GetCalendarCount es equivalente.

Obtener información

Por otra parte, el mensaje MCM_GETCALENDARGRIDINFO sirve para obtener información sobre cada una de las zonas que definen un calendario. En lParam pasaremos un puntero a una estructura MCGRIDINFO en la que se nos devolverá la información requerida.

Antes de enviar el mensaje deberemos iniciar algunos miembros de la estructura. cbSize debe contener el tamaño de la estructura:

mcGI.cbSize = sizeof(MCGRIDINFO);

El miembro dwPart debe contener el valor de la constante que indica qué información en concreto queremos obtener. Puede ser uno de los siguientes valores:

  • MCGIP_CALENDARCONTROL: el control completo, que puede contener hasta 12 calendarios.
  • MCGIP_NEXT: el botón de navegación "siguiente".
  • MCGIP_PREV: el botón de navegación "anterior".
  • MCGIP_FOOTER: el pié el calendario.
  • MCGIP_CALENDAR: un calendario específico. Se den asignar también los miembros iCalendar y pszName.
  • MCGIP_CALENDARHEADER: la cabecera de calendario. Se den asignar también los miembros iCalendar y pszName.
  • MCGIP_CALENDARBODY: el cuerpo del calendario. Hay que asignar el miembro iCalendar.
  • MCGIP_CALENDARROW: una fila de calendario determinada por iCalendar e iRow.
  • MCGIP_CALENDARCELL: una celda de calendario dada determinada por iCalendar, iRow, iCol, bSelected y pszName.

También deberemos indicar un valor para el miembro dwFlags, dependiendo de qué información queremos que nos sea retornada. Puede ser una combinación de uno o varios de los valores siguientes:

  • MCGIF_DATE: se devolverán los valores de stStart y stEnd.
  • MCGIF_RECT: se devolverá rc.
  • MCGIF_NAME: se devolverá pszName.

Para determinar de qué parte del calendario estamos solicitando la información, habrá que asignar otros campos como iCalendar, iRow, iCol, bSelected o pszName, dependiendo de cada caso.

En el caso de que se solicite información en forma de cadenas, en pszName asignaremos la dirección de un buffer, y el cchName el tamaño de ese buffer.

Al retornar, los miembros bSelected, stStart, stEnd, rc y la cadena apuntada por pszName contendrán la información solicitada, dependiendo en cada caso de los valores de entrada.

MCGRIDINFO mcGI;
WCHAR cad[100];
...
        mcGI.cbSize = sizeof(MCGRIDINFO);
        mcGI.dwPart = MCGIP_CALENDAR;
        mcGI.iCalendar = 0;
        mcGI.pszName = cad;
        mcGI.cchName = 100;
        mcGI.dwFlags = MCGIF_NAME;
        SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETCALENDARGRIDINFO, 0, (LPARAM)&mcGI);
        /* En mcGI.pszName estará la cadena con la fecha actualmente seleccionada, por ejemplo
           5 de enero de 2022 */

La macro MonthCal_GetCalendarGridInfo es equivalente al mensaje MCM_GETCALENDARGRIDINFO.

Otro dato que podemos solicitar es el de las fechas actualmente mostradas en el control. No confundir con las fechas seleccionables.

Rangos visibles

El mensaje MCM_GETMONTHRANGE devuelve en lParam los valores de SYSTEMTIME que determinan el rango de fechas. Si en wParam indicamos el valor GMR_DAYSTATE se incluirán las fechas de los meses mostrados parcialmente al inicio y al final de cada calendario, su se indica el valor GMR_VISIBLE sólo se incluirán las fechas de los meses que sean mostrados completamente.

El array de dos fechas que pasemos en lParam debe ser una dirección válida. El sistema no proporciona esa memoria automáticamente.

SYSTEMTIME fecha[2];
...
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETMONTHRANGE, (WPARAM)GMR_VISIBLE, (LPARAM)fecha);

La macro MonthCal_GetMonthRange es equivalente.

Medidas de la cadena 'hoy'

Podemos obtener la medida de la anchura máxima para la cadena 'hoy', mostrada al pié del control si no se ha especifciado el estilo MCS_NOTODAY, usando el mensaje MCM_GETMAXTODAYWIDTH, sin parámetros.

    INT x;
...
    x = SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_GETMAXTODAYWIDTH, 0, 0);

También se puede usar la macro MonthCal_GetMaxTodayWidth.

Tipos de calendario

Exiten varios tipos de calendarios, además del calendario gregoriano que usamos generalmente en occidente. En otras partes del mundo se usan calendario diferentes: Japón, Taiwan, Corea, etc.

El control de calendario dispone de un identificador que determina qué tipo de calendario se está usando. Podemos asignar un nuevo identificador mediante el mensaje MCM_SETCALID, indicando en wParam el valor del nuevo identificador. Este valor puede ser uno de los valores definidos para CALID.

Para obtener el identificador de un control de calendario se usa el mensaje MCM_GETCALID.

También se pueden usar las macros MonthCal_SetCALID y MonthCal_GetCALID, respectivamente.

Nota:

He intentado hacer algún ejemplo de cambio de ID, pero aparentemente no tiene ningún efecto en la apariencia del control.

Otra opción de la que disponemos es elegir qué día empieza cada semana. En mi configuración de Windows las semanas empiezan en lunes, como se vé en las imágenes de ejemplo, pero esto se puede modificar usando el mensaje MCM_SETFIRSTDAYOFWEEK, indicando en lParam qué día de la semana será el primero, empezando en 0 para el lunes.

Si no se modifica, el valor por defecto es LOCALE_IFIRSTDAYOFWEEK, que depende de la configuración regional.

    /* El primer día de la semana es el domingo */
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETFIRSTDAYOFWEEK, 0, (LPARAM)6);

El valor de retorno es un DWORD, donde la palabra de mayor peso será TRUE si el valor previo no era LOCALE_IFIRSTDAYOFWEEK, y la palabra de menor peso contendrá el valor previo para el primer día de la semana.

La macro MonthCal_SetFirstDayOfWeek equivale a ese mensaje.

Para obtener el valor actual para el primer día de la semana de un control de calendario se usa el mensaje MCM_GETFIRSTDAYOFWEEK, sin parámetros o la macro equivalente MonthCal_GetFirstDayOfWeek.

Navegacion

Los controles de calendario disponen de dos pequeños botones con iconos en forma triangular que apuntan a la izquierda y derecha, y que permiten navegar a través de los meses hacia atrás y hacia delante. Por defecto, cada vez que se pulsa uno de esos botones el control retrocede o avanza un mes (o dependiendo de la vista, un año, una década o un siglo).

Podemos modifiar ese comportamiento usando el mensaje MCM_SETMONTHDELTA que modificará el valor del salto. Por ejemplo, si nuesto control muestra tres meses, podemo hace que con cada desplazamiento se muestren tres meses anteriores o posteriores asignando el valor 3 al delta.

    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETMONTHDELTA, (WPARAM)2, 0);

Para obtener el valor actual de delta se usa el mensaje MCM_GETMONTHDELTA.

Las macros MonthCal_SetMonthDelta y MonthCal_GetMonthDelta son equivalentes, respectivamente.

Hoy

Si no se ha especificado el estilo MCS_NOTODAY, el control mostrará una línea que si es pulsada por el usuario seleccionará la fecha actual, actualizando el control para que esa fecha sea visible, si es necesario.

Por defecto, la fecha para 'hoy' es la fecha local actual, pero podemos modificar esa fecha usando el mensaje MCM_SETTODAY, indicando en lParam el nuevo valor para la fecha de 'hoy' en una estructura SYSTEMTIME.

SYSTEMTIME hoy;
...
    /* Hasta nueva orden, hoy siempre será 15 de marzo de 2022 */
    hoy.wDay = 15;
    hoy.wMonth = 3;
    hoy.wYear = 2022;
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETTODAY, 0, (LPARAM)hoy);

Si modificamos esta fecha el control no la actualizará automáticamente cuando pase la media noche. Para volver a usar el valor por defecto se debe usar el mensaje con un valor cero para lParam.

Usar la macro MonthCal_SetToday es equivalente.

Para recuperar el valor actual de 'hoy' se puede usar el mensaje MCM_GETTODAY, indicando en lParam una estructura SYSTEMTIME válida que recibirá el valor de la fecha.

También se puede usar la macro equivalente MonthCal_GetToday.

Vistas

Además de la vista por defecto, que muestra los días de cada mes y que es la que permite seleccionar una fecha concreta, existen otras vistas que nos permiten hacer una navagación más rápida.

Vistas: mensual, anual, de década, de siglo.
Vistas: mensual, anual, de década, de siglo.

Durante la ejecución del programa podemos cambiar de vista pulsando sobre la primera línea del calendario, que contiene el texto de 'mes' de 'año', eso cambia la vista a la anual, y la leyenda 'año', mostrando los nombres de los doce meses.

Pulsando en un mes volveremos a la vista mensual, pulsando en la leyenda del 'año' pasaremos a la vista de década, que mostrará doce años, aunque el primero y el último estarán en gris, y la leyenda mostrará 'año inicio'-'año final' de la década.

De nuevo, pulsando sobre un año volveremos a la vista anual, y pulsando sobre la leyenda pasaremos a la vista de siglo. Se mostrarán doce rangos de diez años, estando el primero y último en gris, y la leyenda mostrará 'año inicio'-'año final' del siglo. Al pulsar sobre una década volveremos a la vista de décadas.

La vista también se puede modificar desde la aplicación usando el mensaje MCM_SETCURRENTVIEW, indicando en lParam una de las constantes de vista:

  • Vista mensual: MCMV_MONTH.
  • Vista anual: MCMV_YEAR.
  • Vista de década: MCMV_DECADE.
  • Vista de siglo: MCMV_CENTURY.
    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETCURRENTVIEW, 0, (LPARAM)MCMV_CENTURY);

También se puede usar la macro equivalente MonthCal_SetCurrentView.

Para obtener el tipo de vista actual de un control se puede usar el mensaje MCM_GETCURRENTVIEW, o la macro correspondiente MonthCal_GetCurrentView.

Formato de juego de caracteres

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

La macro MonthCal_SetUnicodeFormat también sirve para lo mismo.

El mensaje MCM_GETUNICODEFORMAT obtiene el valor de la bandera de formato Unicode. Al igual que la macro MonthCal_GetUnicodeFormat.

Puntos de prueba

Un punto de prueba, o "test point" es una función que nos permite averiguar a qué zona en concreto pertenece un punto determinado de la pantalla. El mensaje MCM_HITTEST nos permite probar puntos, pasando en lParam una estructrua MCHITTESTINFO.

La estructura debe ser inicializada antes de enviar el mensaje. El miembro cbSize debe contener el tamaño de la estructura.

El miembro pt, de tipo POINT contendrá las coordenadas del punto a probar.

MCHITTESTINFO mcTI; ... mcTI.cbSize = sizeof(MCHITTESTINFO); mcTI.pt.x = 14; mcTI.pt.y = 43; SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_HITTEST, 0, (LPARAM)&mcTI);

El resto de los miembros son de salida:

  • uHit: será una constante que indica la zona concreta en la que está el punto. (Ver MCHITTESTINFO para una lista de los posibles valores).
  • st: es una estructura SYSTEMTIME que devolverá la fecha correspondiente a la zona donde está el punto, si a esa zona le corresponde una fecha.
  • rc: es una estructura RECT que devuelve el área de la zona a la que pertenece el punto.
  • iOffset: cuando hay más de un calendario en el control, indica el desplazamiento de aquel al que pertenece el punto.
  • iRow e iCol: fila y columna concreta en la que está el punto, si está en una de las casillas.

Todos los desplazamientos, iOffset, iRow e iCol empiezan a contar desde cero.

La macro MonthCal_HitTest es equivalente.

Notificaciones

Veremos ahora otros códigos de notificación que puede enviar el control de calendario a través de un mensaje WM_NOTIFY.

Cambio de selección

Cada vez que el usuario cambie la selección de la fecha o la selección cambie automáticamente al navegar por el control o como consecuencia de un mensaje MCM_SETCURSEL o MCM_SETSELRANGE, se generará un código de notificación MCN_SELCHANGE.

En lParam recibiremos un puntero a una estructura NMSELCHANGE, en el miembro nmhdr, que es una estructura NMHDR con información sobre la notificación: manipulador de ventana del control, su identificador y el código de notificación.

También recibiremos dos miembros stSelStart y stSelEndde tipo SYSTEMTIME con las fechas de inicio y final seleccionadas.

/* No se pueden seleccionar sábados o domingos */
NMHDR* pnmhdr;
NMSELCHANGE* pnmSC;
...
    case WM_NOTIFY:
        pnmhdr = (NMHDR*)lParam;
        switch(pnmhdr->code) {
            case MCN_SELCHANGE:
                pnmSC = (NMSELCHANGE*)lParam;
                if(pnmSC->stSelStart.wDayOfWeek==6) {
                    pnmSC->stSelStart.wDay--;
                }
                if(pnmSC->stSelStart.wDayOfWeek==0) {
                    pnmSC->stSelStart.wDay++;
                }
                SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETCURSEL, 0, (LPARAM)&pnmSC->stSelStart);
                break;
...
        }
        return 0;

Selección

La notificación MCN_SELECT es similar a MCN_SELCHANGE, con la diferencia de que sólo se envía si se trata de una selección explícita del usuario. Es decir, cuando el usuario hace doble click sobre una fecha concreta, o cuando pulsa sobre la zona 'hoy' para seleccionar la fecha actual.

En lParam recibiremos un puntero a una estructura NMSELCHANGE, igual que con el mensaje de notificación MCN_SELCHANGE.

Cambio de vista

Cuando de produzca un cambio de vista se enviará un código de notificación MCN_VIEWCHANGE, y en lParam un puntero a una estructura NMVIEWCHANGE.

La estructura contiene un miembro nmhdr, que es una estructura NMHDR con información sobre el código de notificación y el control que lo envía.

Además contiene dos miembros de tipo DWORD, dwOldView y dwNewView que contendrán los valores anterior y posterior del tipo de vista del control.

/* No se puede cambiar de vista */
NMHDR* pnmhdr;
NMSELCHANGE* pnmSC;
...
    case WM_NOTIFY:
        pnmhdr = (NMHDR*)lParam;
        switch(pnmhdr->code) {
                case MCN_VIEWCHANGE:
                    pnmVC = (NMVIEWCHANGE*)lParam;
                    SendDlgItemMessage(hwnd, ID_CALENDAR1, MCM_SETCURRENTVIEW, 0, (LPARAM)MCMV_MONTH);
                    break;

Operaciones de arrastre

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

Ejemplo 98

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 98 win098.zip 2021-10-02 6702 bytes 437