Capítulo 55 Control de selección de fecha y hora

Ejemplo de control Fecha y hora

Un control de selección de fecha y hora sirve, evidentemente, para que el usuario pueda introducir en una aplicación valores de fechas u horas válidos. Estos controles nos facilitan la vida, ya que, por lo que respecta a fechas, nos proporciona una forma sencilla de validarlas y acceder a calendarios. Y en lo que respecta a horas, facilita la validación de datos.

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

Los controles de selección de fecha y hora también se pueden insertar durante la ejecución. Tan sólo hay que crear una ventana de la clase "DATETIMEPICK_CLASS". Como de costumbre, para hacerlo usaremos las funciones CreateWindow o CreateWindowEx.

        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            hCtrl = CreateWindowEx(0, DATETIMEPICK_CLASS, NULL,
                WS_BORDER | WS_CHILD | WS_VISIBLE | WS_TABSTOP | DTS_LONGDATEFORMAT,
                10,10,220,30,
                hwnd, (HMENU)ID_DATETIME,
                hInstance, NULL);
            break;

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

No olvidemos que cuando se insertan controles durante la ejecución, la fuente por defecto es system. Si queremos cambiarla habrá que crear una fuente y asignársela al control 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;

Desde fichero de recursos

Estos controles también pueden situarse en cuadros de diálogo definidos en un fichero de recursos.

Se usa un control general CONTROL, con la clase DATETIMEPICK_CLASS, y los estilos generales y específicos deseados.

LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_DIALOG2 DIALOG 0, 0, 242, 47
STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | DS_SHELLFONT | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
CAPTION "Diálogo"
FONT 8, "Ms Shell Dlg"
{
    CONTROL         "", ID_DATETIME, DATETIMEPICK_CLASS, WS_TABSTOP | DTS_LONGDATEFORMAT, 11, 6, 146, 17, WS_EX_LEFT
    PUSHBUTTON      "Cancel", IDCANCEL, 176, 24, 50, 14, 0, WS_EX_LEFT
    DEFPUSHBUTTON   "OK", IDOK, 176, 7, 50, 14, 0, WS_EX_LEFT
}

Estilos

Podemos dividir los estilos específicos de estos controles en varios tipos: los de formato, los que afectan a al aspecto, y los que afectan al comportamiento.

Estilos de formato:

  • Estilos de formato
    DTS_LONGDATEFORMAT: permite editar fechas expresadas en formato largo.
  • DTS_SHORTDATEFORMAT: permite editar fechas expresadas en formato corto, en principio se usarán dos caracteres para el año, aunque esto cambia en función de las opciones de visualizacióndel sistema operativo.
  • DTS_SHORTDATECENTURYFORMAT: similar a DTS_SHORTDATEFORMAT, con la diferencia de que los años se muestran con cuatro dígitos.
  • DTS_TIMEFORMAT: permite editar horas.

Hay que tener presente que el formato en que se expresan las fechas y horas dependerá de la definicion de ciertas opciones locales del sistema. Cambiará el orden en que aparece cada campo (día, mes y año u horas, minutos y segundos), y el idioma de los nombres de días de la semana y de los meses. También varían las horas en función del formato elegido por el usuario en las opciones del sistema operativo, de 12 (PM o AM) ó 24 horas.

Por ejemplo, en el caso de la imagen mostrada no se aprecia diferencia entre los estilos DTS_SHORTDATEFORMAT y DTS_SHORTDATECENTURYFORMAT, y en ambos casos los años se expresan con cuatro dígitos.

Estilos de aspecto:

  • Estilos de aspecto
    DTS_SHOWNONE: permite que opcionalmente el usuario no introduzca una fecha. El control muestra un checkbox, inicialmente marcado, que indica si el control contiene una fecha o no. Si el checkbox no está marcado, el control no contendrá una fecha, y cualquier intento de leerla no funcionará.
  • DTS_UPDOWN: muestra un control updown a la derecha del control. Cuando se trate de horas (estilo DTS_TIMEFORMAT), se mostrará por defecto. Cuando se trate de fechas no se podrá desplegar el control de calendario mensual, pero se podrán modificar los campos (dia, mes y año), seleccionándolos y usando el control updown para incrementar o decrementar el valor de cada uno de ellos.

  • DTS_RIGHTALIGN: hace que el control de calendario mensual se muestre alineado a la derecha del control de fecha y hora, en lugar de a la izquierda, que es el valor por defecto.

Estilo de comportamiento:

  • DTS_APPCANPARSE: permite hacer entradas personalizadas en el control de fecha y hora. El usuario puede pulsar la tecla F2 e introducir un texto. La aplicación recibirá un mensaje de notificación DTN_USERSTRING y tendrá que analizar la entrada del usuario para validarla y convertirla a un valor de fecha válido.

Asignar un valor

Por defecto, si no se asigna un valor inicial a un control de fecha y hora, se asignará la fecha y hora actual en el momento en que se crea el control. Esta información se almacena en una estructura SYSTEMTIME, de modo que aunque el control sólo visualice una fecha o una hora, en realidad contiene toda la información de tiempo de sistema.

Para asignar un valor diferente podemos usar indistintamente el mensaje DTM_SETSYSTEMTIME o la macro DateTime_SetSystemtime.

Si optamos por el mensaje, en wParam indicaremos el valor GDT_VALID, si vamos a inicializar el control con una fecha/hora válida o GDT_NONE, si queremos que el control esté en un estado "sin fecha". Para este segundo caso el control debe tener el estilo DTS_SHOWNONE, y como consecuencia se eliminará la marca del checkbox, quedanto el texto en gris.

En lParam pasaremos un puntero a una estructura SYSTEMTIME con el valor de fecha y/o hora a asignar.

Si todo va bien, el mensaje retornará un valor distinto de cero.

Si optamos por la macro, el primer parámetro es el manipulador de ventana del control, el segundo equivale al wParam del mensaje y el tercero equivale al lParam del mensaje.

    SYSTEMTIME st;
    int v;
... 
    st.wYear = 2020;
    st.wMonth = 1;
    st.wDay = 1;
    st.wHour = 12;
    st.wMinute = 15;
    st.wSecond = 0;
    st.wMilliseconds = 0;
    // Mensaje:
    v = SendDlgItemMessage(hwnd, ID_DATETIME, DTM_SETSYSTEMTIME, (WPARAM)GDT_VALID, (LPARAM)&st);
    // Macro
    v = DateTime_SetSystemtime(GetDlgItem(hwnd, ID_DATETIME), GDT_VALID, &st);

Obtener un valor

Para obtener el valor actual de un control de fecha y hora, siempre que no tenga el estilo DTS_SHOWNONE y el checkbox esté sin marcar, podemos usar el mensaje DTM_GETSYSTEMTIME o bien la macro DateTime_GetSystemtime.

En el caso del mensaje tan sólo hay que pasar en lParam un puntero a una estructrura SYSTEMTIME que recibirá el valor actual del control.

Con la macro el primer parámetro será el manipulador de ventana del control, y el segundo un puntero a una estructrura SYSTEMTIME donde se situará el valor recuperado del control.

En ambos casos el valor de retorno será GDT_VALID si tiene éxito o GDT_NONE si el control tiene el estilo DTS_SHOWNONE y el checkbox no está marcado, en ese caso el valor de retorno no será válido.

    SYSTEMTIME st;
    int v;
... 
    v = SendDlgItemMessage(hwnd, ID_DATETIME, DTM_GETSYSTEMTIME, 0, (LPARAM)&st);
    v = DateTime_GetSystemtime(GetDlgItem(hwnd, ID_DATETIME), (LPARAM)&st);

Establecer rangos

Es posible limitar el rango de fechas que el usuario puede seleccionar en el control. Para ello disponemos del mensaje DTM_SETRANGE y de la macro DateTime_SetRange, que podemos usar indistintamente.

En el caso del mensaje tendremos que pasar en wParam una combinación de los valores GDTR_MIN y GDTR_MAX, para indicar si queremos establecer el margen mínimo, el máximo o ambos. En lParam pasaremos un puntero a un array de dos estructuras SYSTEMTIME, en el que el primer elemento será el margen mínimo y el segundo el máximo del rango.

En el caso de la macro, el primer parámetro será un manipulador de ventana del control, el segundo una combinación de los valores GDTR_MIN y GDTR_MAX y el tercero un array de estructuras SYSTEMTIME que definen el rango.

En ambos casos el valor de retorno será distinto de cero si la asignación de rango es establecida, y cero si no es así.

Si no se especifica alguna de las constantes GDTR_MIN o GDTR_MAX, ese estremo del rango no se establecerá, y no será necesario que el valor de SYSTEMTIME correspondiente tenga un valor válido.

En este ejemplo se establece un rango de fechas válidas entre el 15 de noviembre de 2020 y el 15 de diciembre de 2020.

    SYSTEMTIME rango[2];
    int v;
...
    rango[0].wYear = 2020;
    rango[0].wMonth = 11;
    rango[0].wDay = 15;
    rango[1].wYear = 2020;
    rango[1].wMonth = 12;
    rango[1].wDay = 15;
    v = SendDlgItemMessage(hwnd, ID_DATETIME, DTM_SETRANGE, (WPARAM)(GDTR_MIN | GDTR_MAX), (LPARAM)rango);
    v = DateTime_SetSystemtime(GetDlgItem(hwnd, ID_DATETIME), GDTR_MIN | GDTR_MAX, rango);

Obtener rangos

De manera similar podemos recuperar el rango establecido para un control mediante un mensaje DTM_GETRANGE o usando la macro DateTime_GetRange.

En el caso del mensaje pasaremos en lParam la dirección de un array de dos estructuras SYSTEMTIME que recibirá los valores mínimo y máximo del rango.

Para la macro, el primer parámetro es un manipulador de ventana del control, y el segundo la dirección del array.

En ambos casos el valor de retorno será una combinación de los valores GDTR_MIN y GDTR_MAX, que nos indicará qué valores del array son válidos.

    SYSTEMTIME rango[2];
    int v;
...
    v = SendDlgItemMessage(hwnd, ID_DATETIME, DTM_GETRANGE, 0, (LPARAM)rango);
    v = DateTime_GetSystemtime(GetDlgItem(hwnd, ID_DATETIME), rango);

Atributos del calendario mensual

En el próximo capítulo veremos más detalles sobre el control de calendario mensual. Por ahora nos centraremos en algunas opciones de las que disponemos para personalizar este control que se muestra cuando seleccionamos fechas desplegando el calendario.

Algunas de estas opciones sólo estarán disponibles si no activamos los temas visuales, es decir, si no incluimos en el manifiesto la línea:

    <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" 
            publicKeyToken="6595b64144ccf1df" language="*"/>

En caso contrario las modificaciones de colores y fuentes no tendrán ningún efecto.

El aspecto gráfico y el comportamiento del control de fecha y hora es bastante diferente si usamos o no los temas visuales.

Obtener el manipulador de ventana

El control de calendario mensual se crea en el momento en que el usuario pulsa el botón de despliegue, y se destruye cuando el usuario selecciona un valor de fecha. Esto quiere decir que no existe un manipulador de ventana de este control dentro del control de selección de fecha y hora, de modo que tendremos que obtener ese manipulador cada vez que lo necesitemos.

Para ello podemos procesar el código de notificación DTN_DROPDOWN que la aplicación recibirá a través de un mensaje WM_NOTIFY.

Si tenemos que realizar alguna tarea cuando el control de calendario mensual sea destruido, por ejemplo, liberar algún recurso, podemos usar el código de notificación DTN_CLOSEUP.

Para obtener el manipulador de la ventana de calendario mensual podemos usar el mensaje DTM_GETMONTHCAL o la macro DateTime_GetMonthCal.

En el caso del mensaje no se requieren parámetros. Para la macro tan sólo es necesario indicar el manipulador de ventana del control de selección de fecha y hora.

    NMHDR* pnmHdr;
    HWND hCMwnd;
...
        case WM_NOTIFY:
            pnmHdr = (NMHDR*)lParam;
            switch(pnmHdr->code) {
                case DTN_DROPDOWN:
                    hCMwnd = SendMessage(pnmHdr->hwndFrom, DTM_GETMONTHCAL, 0, 0);
                    hCMwnd = DateTime_GetMonthCal(pnmHdr->hwndFrom);
                    printf("Abierto %d\n", hCMwnd);
                    break;
                case DTN_CLOSEUP:
                    printf("Cerrado\n");
                    break;
            }
            break;

Cambiar la fuente

Los mensajes DTP_*MC* hacen referencia al control hijo calendario mensual, pero no se deben enviar a ese control, sino al propio control de selección de fecha y hora. Es decir las opciones que modifiquemos (colores, fuentes o estilos) para el control hijo de calendario mensual tendrán efecto cada vez que se despliegue ese control, ya que se almacenarán con las propiedades del control padre.

Podemos modificar la fuente usada por el control de calendario mensual hijo mediante el mensaje DTM_SETMCFONT o la macro DateTime_SetMonthCalFont.

Cuando se envíe el mensaje el wParam pasaremos un manipulador de la fuente a usar y en lParam un valor BOOL que indicará si el control debe ser redibujado. Cuando usemos la macro, el primer parámetro será el manipulador de ventana del control de selección de fecha y hora, y los otros dos parámetros son los mismos que para el mensaje.

    SendDlgItemMessage(hwnd, ID_DATETIME, DTM_SETMCFONT, (WPARAM)hFont, (LPARAM)TRUE);
    DateTime_SetMonthCalFont(GetDlgItem(hwnd, ID_DATETIME), hFont, TRUE)

También podemos recuperar la fuente que se usará para mostrar el control de calendario mensual mediante el envío de un mensaje DTM_GETMCFONT o usando la macro DateTime_GetMonthCalFont.

El mensaje no requiere parámetros, y la macro sólo necesita que pasemos un manipulador de ventana del control de selección de fecha y hora.

Cambio de estilos

Disponemos de varias opciones de estilos de control de calendario mensual, por ejemplo, mostrar los números de las semanas, ocultar la fecha actual de la parte inferior, no mostrar los días del mes anterior y siguiente del actual, etc.

Para asginar los estilos del control de calendario mensual de un control de selección de fecha y hora disponemos del mensaj DTM_SETMCSTYLE y de la macroDateTime_SetMonthCalStyle.

Si usamos el mensaje indicaremos el lParam el estilo o combinación de estilos que queremos asignar. Si usamos la macro, el primer parámetro será el manipulador de ventana del control de selección de fecha y hora y el segundo los estilos a asignar.

Para recuperar el valor de los estilos actuales del control de calendario mensual asociado a un control de selección de fecha y hora podemos usar el mensaje DTM_GETMCSTYLE o la macro DateTime_GetMonthCalStyle.

El mensaje no requiere ningún parámetro, y la macro sólo un manipulador de control de selección de fecha y hora.

Cambio de colores

Colores calendario

Sólo si no están activos los estilos visuales, podemos modificar los colores del control de calendario mensual. Para ello disponemos del mensaje DTM_SETMCCOLOR o de la macro DateTime_SetMonthCalColor.

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

En el caso de la macro, el primer parámetro es un manipulador del control de selección de fecha y hora, y los dos siguientes son los mismos que en el mensaje.

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

  • MCSC_BACKGROUND: color de fondo entre meses. Esta parte no se muestra en controles con un único mes, y no tiene efecto en controles de selección de fecha y hora.
  • 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 DTM_GETMCCOLOR o la macro DateTime_GetMonthCalColor.

Si se usa el mensaje, indicaremos en wParam la constante correspondiente a la zona cuyo color queremos recuperar. En el caso de la macro, el primer parámetros erá un manipulador de control de selección de fecha y hora y el segundo la constante de la zona.

Cerrar calendario

Podemos cerrar el control de calendario mensual enviando un mensaje DTM_CLOSEMONTHCAL o usando la macro DateTime_CloseMonthCal.

El mensaje no necesita parámetros, y la macro sólo un manipulador de ventana del control de selección de fecha y hora.

Este mensaje hará que se cierre el calendario, sin necesidad de que el usuario haya elegido una fecha, y se envíe un mensaje de notificación DTN_CLOSEUP a la ventana propietaria del control de selección de fecha y hora.

Asignar formato

En este contexto, el formato se refiere a la cadena que se muestra cuando se elige el estilo de formato DTS_LONGDATEFORMAT. Si el sistema operativo está en español, por defecto tiene la forma "[dia de semana], [día del mes] de [nombre del mes] de [año]", pero esto puede cambiarse usando el mensaje DTM_SETFORMAT o la macro DateTime_SetFormat.

Usando el mensaje indicaremos en lParam un puntero a la nueva cadena de formato, o NULL si queremos restablecer la cadena por defecto. Con la macro el primer parámetro será el manipulador de ventana del control, y el segundo el puntero a la nueva cadena de formato.

Los literales que se quieran insertar en el formato deberán indicarse entre comillas simples. Para los campos de fecha hay ciertas reglas:

  • d: Día del mes, uno o dos dígitos.
  • dd o d: Día del mes, dos dígitos, si tiene uno se añade un cero inicial.
  • ddd: nombre del día de la semana, con dos letras.
  • dddd: nombre del día de la semana, completo.
  • h: Hora, uno o dos dígitos en formato de 12 horas.
  • hh: Hora, con dos dígitos, en formato de 12 horas, se añade un cero inicial si es necesario.
  • H: Hora, uno o dos dígitos en formato de 24 horas.
  • HH: Hora, con dos dígitos, en formato de 24 horas, se añade un cero inicial si es necesario.
  • m: Minuto, con uno o dos dígitos.
  • mm: Minuto, con dos dígitos, se añade un cero inicial si es necesario.
  • M: Mes, con uno o dos dígitos
  • MM: Mes, con dos dígitos, se añade un cero inicial si es necesario.
  • MMM: Mes, con tres letras.
  • MMMM: Mes, nombre completo.
  • t: Una letra para indicar AM/PM en formato de 12 horas.
  • tt: Una letra para indicar AM/PM en formato de 12 horas.
  • s: Segundo, con uno o dos dígitos.
  • ss: Segundo, con dos dígitos, se añade un cero inicial si es necesario.
  • yy: Año, con dos dígitos.
  • yyyy: Año, con cuatro dígitos.
    char* cad = "'Fecha: 'hh':'mm':'ss ddd dd'/'MMM'/'yyy";
...
    SendDlgItemMessage(hwnd, ID_DATETIME, DTM_SETFORMAT, 0, (LPARAM)cad);
    DateTime_SetFormat(GetDlgItem(hwnd, ID_DATETIME), cad);

Calcular el tamaño del control

Debido a que existen varios estilos de formato de este tipo de controles, el tamaño puede ser muy diferente en función del estilo asignado. Se puede calcular el tamaño ideal de uno de estos controles mediante el mensaje DTM_GETIDEALSIZE o la macro DateTime_GetIdealSize.

En el caso del mensaje indicaremos en lParam un puntero a una estructura SIZE que recibirá las dimensiones ideales para el control. Si se usa la macro indicaremos en el primer parámetro un manipulador del control y en el segundo un puntero a la estructura.

El tamaño varía según la fuente asignada al control, de modo que antes de calcularlo deberemos asignar la fuente, y después mover el control usando la función MoveWindow.

    HFONT hFont;
    SIZE size;
...
        hFont = CreateFont(-11, 0, 0, 0, 0, FALSE, FALSE, FALSE, 1, 0, 0, 0, 0, ("Ms Shell Dlg"));

        CreateWindowEx(0, DATETIMEPICK_CLASS, NULL,
            WS_BORDER | WS_CHILD | WS_VISIBLE | WS_TABSTOP | DTS_LONGDATEFORMAT | DTS_APPCANPARSE,
            10,30,0,0, /* La anchura y altura no son relevantes */
            hwnd, (HMENU)ID_DATETIME,
            hInstance, NULL);
        SendDlgItemMessage(hwnd, ID_DATETIME, WM_SETFONT, (WPARAM)hFont, MAKELPARAM(FALSE, 0));
        
        SendDlgItemMessage(hwnd, ID_DATETIME, DTM_GETIDEALSIZE, 0, (LPARAM)&size);
        MoveWindow( GetDlgItem(hwnd, ID_DATETIME), 10, 30, size.cx, size.cy, TRUE);

Aunque la documentación afirma que se calcula el tamaño ideal, lo cierto es que he verificado que esto sólo se cumple para la anchura, y que es mejor que la altura se fije a gusto del programador, ya que parece que el su cálculo se ve influenciado por la altura indicada al crear el control.

Obtener información

Se puede obtener información adicional sobre el control usando el mensaje DTM_GETDATETIMEPICKERINFO o la macro DateTime_GetDateTimePickerInfo.

En el mensaje pasaremos un puntero a una estructura DATETIMEPICKERINFO en lParam. En la macro pasaremos en el primer parámetro un manipulador de ventana del control de fecha y hora y como segundo parámetro un puntero a la estructura.

Antes de enviar el mensaje o usar la macro hay que inicializar el miembro cbSize de la estructura con el valor sizeof(DATETIMEPICKERINFO).

La estructura se devolverá con datos relativos al control, el área en pantalla y el estado de la caja de checkbox, el área y estado del botón de despliegue, y los manipuladores de ventana del los controles hijo de edición, up-down y cuadrícula desplegable.

Códigos de notificación

Existen algunos códigos de notificación específicos para este tipo de controles. Como todos los códigos de notificación, se envían a través de un mensaje WM_NOTIFY.

Notificación de cambio de fecha y hora

Cuando se produce un cambio en el valor del control se envía a la aplicación un código de notificación DTN_DATETIMECHANGE. En lParam se recibe un puntero a una estructura NMDATETIMECHANGE que contiene información sobre el cambio que se haya producido.

La estructura contiene una estructura NMHDR, un miembro de banderas que indican el estado del control, siempre que tenga asignado el estilo DTS_SHOWNONE, que indicará si el estado del control es "no date", es decir, si el checkbox está sin marcar con el valor GDT_NONE, o si la fecha es válida, y el checkbox marcado, con el valor GDT_VALID.

El tercer campo es una estructura SYSTEMTIME con el valor actual del control.

Control de calendario mensual desplegado

Cada vez que el control de calendario mensual se despliegue se envía a la aplicación un código de notificación DTN_DROPDOWN, y cuando se cierre un código DTN_CLOSEUP.

En ambos casos recibiremos en lParam un puntero a una estructura NMHDR.

Campos de retrollamada

Cuando definimos un formato de fecha usando el mensaje DTM_SETFORMAT o la macro DateTime_SetFormat, además de los campos con datos de la fecha, y literales entre comillas sencillas, podemos añadir campos personalizables. Estos son los campos de retrollamada (callback fields). Cuando el control tiene que mostrar la cadena y se encuentra con uno de estos campos envía un código de notificación DTN_FORMAT o DTN_FORMATQUERY.

El código de notificación DTN_FORMATQUERY se envía cuando el control tiene que calcular su tamaño ideal. De modo que si el control tiene el estilo DTS_APPCANPARSE, la cadena de formato tiene campos de retrollamada y enviamos el mensaje DTM_GETIDEALSIZE recibiremos uno de estos códigos de notificación por cada campo de retrollamada existente.

El código de notificación recibirá en lParam un puntero a una estructura NMDATETIMEFORMATQUERY, y la aplicación debe devolver en el miembro szMax el tamaño máximo de la cadena que se podrá usar para ese campo de retrollamada en concreto.

Esto hará posible que el control pueda calcular el tamaño máximo ideal para el control.

Para calcular ese tamaño podemos usar la función GetTextExtentPoint32, usando como manipulador de ventana el del control.

El código de notificación DTN_FORMAT se envía cuando el control tiene que mostrar la cadena en un control con el estilo DTS_APPCANPARSE y con campos de retrollamada. Se enviará uno de estos códigos para cada campo de retrollamada.

El programa que procese este código recibirá en lParam un puntero a una estructura NMDATETIMEFORMAT y deberá copiar en el miembro szDisplay el texto que debe mostrar el control.

En pszFormat estará la subcadena del campo de retrollamada, y en st la fecha y hora actualmente almacenadas en el control. Con estos datos deberíamos poder determinar el valor de la cadena.

Los campos de retrollamada se definen mediante uno o más caracteres 'X'. Si necesitamos dos de esos campos podemos usar las subcadenas 'X' y 'XX', o 'XX' y 'XXX', etc.

El usuario puede modificar una fecha en un control de selección de fecha y hora accediendo a uno de los campos individuales del control (año, mes, día, hora, etc). Pero esto también incluye los campos de retrollamada.

El código de notificación DTN_WMKEYDOWN se envía a la aplicación cuando el usuario escribe en uno de los campos de retrollamada.

En lParam se recibe un puntero a una estructura NMDATETIMEWMKEYDOWN.

El miembro nVirtKey contiene el código de la tecla virtual pulsada. El miembro pszFormat identifica el campo de retrollamada mediante su subcadena, y el miembro st contiene el valor actual de la fecha y hora almacenada en el control.

// Este ejemplo muestra una cadena del tipo "Fecha: mi. 01/01/2020 02 noche" para el control
// Dependiendo el valor de la hora, el texto del final será mañana (de 6 a 15), tarde 
// (de 15 a 23), o noche (de 23 a 6).

void CalculaTamanoTexto(HWND hctrl, char* szcad, SIZE* lpsize);
... 
HFONT hFont;
char* cad = "XX ddd dd'/'MM'/'yyy HH XXX";
SIZE size;
NMDATETIMEFORMATQUERY* pnmdtfq;
... 
    // Crear fuente:
    hFont = CreateFont(-11, 0, 0, 0, 0, FALSE, FALSE, FALSE, 1, 0, 0, 0, 0, ("Ms Shell Dlg"));
    // Insertar control:
    CreateWindowEx(0, DATETIMEPICK_CLASS, NULL,
        WS_BORDER | WS_CHILD | WS_VISIBLE | WS_TABSTOP | DTS_LONGDATEFORMAT | DTS_APPCANPARSE,
        10,30,2500,55,
        hwnd, (HMENU)ID_DATETIME,
        hInstance, NULL);
    // Asignar la cadena de formato:
    DateTime_SetFormat(GetDlgItem(hwnd, ID_DATETIME), cad);
    // Calcular tamaño ideal:
    SendDlgItemMessage(hwnd, ID_DATETIME, DTM_GETIDEALSIZE, 0, (LPARAM)&size);
    // Redimensionar control:
    MoveWindow( GetDlgItem(hwnd, ID_DATETIME), 10, 30, size.cx, 30, TRUE);
... 
    case WM_NOTIFY:
        switch(pnmHdr->code) {
            case DTN_FORMAT:
                pnmdtf = (NMDATETIMEFORMAT*)lParam;
                if(!strcmp(pnmdtf->pszFormat, "XX")) {
                    strcpy(pnmdtf->szDisplay,"Fecha:");
                }
                if(!strcmp(pnmdtf->pszFormat, "XXX")) {
                    if(pnmdtf->st.wHour >= 6 && pnmdtf->st.wHour < 15)
                        strcpy(pnmdtf->szDisplay, "mañana");
                    else if(pnmdtf->st.wHour >= 15 && pnmdtf->st.wHour < 23)
                        strcpy(pnmdtf->szDisplay, "tarde");
                    else strcpy(pnmdtf->szDisplay, "noche");

                }
                printf("Format\n");
                return 0;
            case DTN_FORMATQUERY:
                // Obtener tamaños de callback fields: 
                pnmdtfq = (NMDATETIMEFORMATQUERY*)lParam;
                if(!strcmp(pnmdtfq->pszFormat, "XX")) { // Valor fijo:
                    CalculaTamanoTexto(GetDlgItem(hwnd, ID_DATETIME), "Fecha:", &pnmdtfq->szMax);
                }
                if(!strcmp(pnmdtfq->pszFormat, "XXX")) { // Mañana, tarde o noche, en función de hora.
                    CalculaTamanoTexto(GetDlgItem(hwnd, ID_DATETIME), "mañana", &pnmdtfq->szMax);
                }
                return 0;
            case DTN_WMKEYDOWN:
                pnmdtk = (NMDATETIMEWMKEYDOWN*)lParam;
                if(!strcmp(pnmdtk->pszFormat, "XXX")) {
                    if(pnmdtk->nVirtKey == 'M') {
                        pnmdtk->st.wHour=6;
                    }
                    if(pnmdtk->nVirtKey == 'T') {
                        pnmdtk->st.wHour=15;
                    }
                    if(pnmdtk->nVirtKey == 'N') {
                        pnmdtk->st.wHour=23;
                    }
                }
                return 0;
... 
void CalculaTamanoTexto(HWND hctrl, char* szcad, SIZE* lpsize) {
    HDC hdc;

    hdc = GetDC(hctrl);
    GetTextExtentPoint32(hdc, szcad, strlen(szcad), lpsize);
    ReleaseDC(hctrl, hdc);
}

En rigor, para calcular el tamaño del campo 'XXX' tendríamos que haber usar el mayor tamaño para las tres cadenas, 'mañana', 'tarde', y 'noche'. Pero asuminos que la de 'mañana' será la más larga al tener más caracteres.

Cadenas de usuario

Si el control tiene el estilo DTS_APPCANPARSE, el usuario podrá escribir texto en el control de edición asociado al control de selección de fecha y hora. Podrá, por ejemplo, introducir fechas incompletas "12/08", y que la aplicación calcule los campos faltantes usando valores por defecto; podrá introducir fechas como "ayer" o "mañana", etc.

Si el usuario pulsa F2 se seleccionará todo el texto del control y se entrará en edición.

La aplicación deberá procesar estas cadenas y realizar las comprobaciones y cálculos necesarios para obtener una fecha válida.

Para ello, la aplicación recibirá un código de notificación DTN_USERSTRING cuando el usuario finalice la edición del texto. En lParam recibirá un puntero a una estructura NMDATETIMESTRING en la que el miembro pszUserString contendrá la cadena introducida por el usuario, st el valor de fecha y hora actualmente almacenadas en el control, y en dwflags el valor del checkbox si además el control tiene el estilo DTS_SHOWNONE, que será GDT_VALID si el checkbox está marcado o GDT_NONE en caso contrario.

            case DTN_USERSTRING:
                pnmdts = (NMDATETIMESTRING*)lParam;
                if(!strcmp(pnmdts->pszUserString, "hoy"))
                    GetLocalTime(&(pnmdts->st));
                else if(!strcmp(pnmdts->pszUserString, "mañana")) {
                    GetLocalTime(&(pnmdts->st));
                    pnmdts->st.wDay++;
                } else if(!strcmp(pnmdts->pszUserString, "ayer")) {
                    GetLocalTime(&(pnmdts->st));
                    pnmdts->st.wDay--;
                }
                return 0;

La función GetLocalTime sirve para obtener la fecha y hora local actual. Para obtener la fecha y hora en formato (UTC) se usa la función GetSystemTime.

Foco del teclado

Por último, cuando el control pierde el foco del teclado, la aplicación recibe un código de notificación NM_KILLFOCUS.

Cuando el control recibe el foco del teclado, la aplicación recibe un código de notificación NM_SETFOCUS.

Ejemplo 96

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 96 win096.zip 2021-09-16 6558 bytes 25

Ejemplo 97

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 97 win097.zip 2021-09-17 5496 bytes 30