14 Controles de fecha y hora

En este capítulo veremos los controles que podemos usar para obtener fechas y horas introducidas por el usuario.
wxWidgets nos proporciona dos controles para capturar fechas: wxCalendarCtrl y wxDatePickerCtrl, y uno para capturar horas wxTimePickerCtrl.
Control de calendario
Para empezar, comentar algo que no está claramente explicado en la documentación: disponemos de dos clases diferentes de controles de calendario, la genérica y la nativa.
La nativa es wxCalendarCtrl, usará la versión nativa del control del sistema operativo. Para usarla tendremos que incluir el fichero de cabecera "wx/calctrl.h".
La genérica es wxGenericCalendarCtrl, usará una versión propia de wxWidgets, que tendrá el mismo aspecto en todas las plataformas, y para usarla hay que incluir los ficheros de cabecera "wx/calctrl.h" y "wx/generic/calctrl.h".
Las versiones nativas pueden tener algunas limitaciones, por ejemplo, en Windows, el rango de fechas está limitado a años a partir de 1601.
Además, algunos estilos y eventos sólo estarán disponibles en la versión genérica, de modo que esa será la versión que usaremos en los ejemplos.
En cualquiera de las dos versiones, el control calendario permite capturar una fecha. Para ello, como se puede ver en la imagen de la derecha, se muestra el mes completo al que pertenece la fecha actualmente seleccionada. Los días festivos se resaltan, la forma de hacerlo en la versión nativa depende de la plataforma. La fecha actualmente seleccionada se marca con un fondo diferente y la fecha actual, si está visible, y dependiendo de la plataforma, se rodea con un marco.
Los atributos de colores para resaltar las fechas festivas se pueden modificar usando el método SetHolidayColours.
El usuario puede seleccionar el mes y año a mostrar, y puede seleccionar un día concreto haciendo clic sobre él.
Para crear un control de calendario usaremos el constructor con parámetros, o bien el constructor por defecto y el método Create.
Estos controles no admiten validadores, por lo que tendremos que procesar el botón de "Aceptar" o algún evento como el doble clic o la selección de fecha.
El constructor no difiere mucho de los que hemos visto hasta ahora, y requiere los parámetros habituales:
- La ventana padre.
- El identificador del control.
- Una referencia al valor inicial del control, que puede ser wxDefaultDateTime para usar la fecha actual.
- La posición.
- El tamaño. Es recomendable usar el valor por defecto, wxDefaultSize.
- El estilo.
- El nombre.
Si optamos por procesar el botón de "Aceptar", deberemos procesar el evento correspondiente, y obtener el valor actual del control mediante el método GetDate:
class datosCalendar { public: datosCalendar(wxDateTime dt=wxDefaultDateTime) : valor(dt) {} wxDateTime valor; }; ... wxBEGIN_EVENT_TABLE(calendar, wxDialog) EVT_CLOSE(calendar::OnClose) ... EVT_BUTTON(wxID_OK, calendar::OnOk) wxEND_EVENT_TABLE() ... void calendar::OnOk(wxCommandEvent& event) { data.valor = m_calendar->GetDate(); Destroy(); }
Estilos
Por defecto se establece el estilo wxCAL_SHOW_HOLIDAYS, que resalta los días festivos, y también por defecto, se carga la autoridad wxDateTimeWorkDays, que marca como festivos los sábados y domingos.
Los estilos wxCAL_SUNDAY_FIRST y wxCAL_MONDAY_FIRST hacen que se muestre como primer día de la semana el domingo o el lunes.
Los estilos wxCAL_NO_YEAR_CHANGE y wxCAL_NO_MONTH_CHANGE impiden que el usuario pueda cambiar de año, o de mes, respectivamente.
El estilo wxCAL_SHOW_SURROUNDING_WEEKS muestra los días del mes anterior y posterior para completar la semanas, siempre se mostrarán seis semanas, aunque el mes actual sólo tenga cuatro o cinco.El estilo wxCAL_SHOW_WEEK_NUMBERS añade el número de la semana del año a cada semana.

Por defecto se puede seleccionar el mes desde una lista desplegable y el año desde un control spin, como en el ejemplo de arriba a la derecha. El estilo wxCAL_SEQUENTIAL_MONTH_SELECTION, permite sustituir estos controles por uno más compacto que permite seleccionar el mes y año de forma secuencial, como se ve en la imagen de la izquierda. Este estilo no es muy útil si el rango de fechas a seleccionar es muy grande, ya que sólo permite seleccionar los meses uno a uno, y retroceder o avanzar años puede resultar tedioso.
Eventos
Estos controles pueden generar varios eventos diferentes. Por ejemplo, EVT_CALENDAR cuando se hace doble clic sobre el calendario o EVT_CALENDAR_PAGE_CHANGED cuando se cambia de mes o año.
Cuando se cambia la fecha seleccionada se genera un evento EVT_CALENDAR_SEL_CHANGED, y además también se generan eventos EVT_CALENDAR_DAY, EVT_CALENDAR_MONTH o EVT_CALENDAR_YEAR si se cambia el día, el mes o el año seleccionado. Esto puede ser útil si queremos resaltar algunas fechas al cambiar de mes o de año, como veremos en un ejemplo más adelante.
Para detectar clics sobre zonas especiales como el día de la semana o el número de la semana, también se generan eventos EVT_CALENDAR_WEEKDAY_CLICKED y EVT_CALENDAR_WEEK_CLICKED, respectivamente.
Para procesar el doble clic tenemos que capturar el evento EVT_CALENDAR:
wxBEGIN_EVENT_TABLE(calendar, wxDialog) EVT_CLOSE(calendar::OnClose) EVT_CALENDAR(idCalendar, calendar::OnCalendar) EVT_BUTTON(wxID_OK, calendar::OnOk) wxEND_EVENT_TABLE() ... void calendar::OnCalendar(wxCalendarEvent& event) { data.valor = event.GetDate(); Destroy(); }
En principio, el parámetro de la clase wxCalendarEvent no nos será muy útil, ya que el único método de que nos proporciona información es GetWeekDay, y sólo tiene sentido para el evento EVT_CALENDAR_WEEKDAY_CLICKED.
Sin embargo, esta clase se deriva de wxDateEvent, que sí tiene un método para recuperar la fecha, GetDate().
Festivos
Hay al menos dos formas de marcar días como festivos. Se puede usar el método SetHoliday, indicando el día. Esto se aplica al mes actual, y la información se pierde cada vez que el usuario cambia de mes. Podemos usar el evento EVT_CALENDAR_MONTH para actualizar el calendario cada vez que sea necesario.
La otra alternativa consiste en añadir nuevas autoridades de festivos, como comentamos en el capítulo anterior, de modo que se resalten los festivos que nos interese. Podemos crear autoridades para cada mes o año, es más complicado crear autoridades generarles, que se puedan aplicar a todos los años, aunque en ciertos casos es posible calcular varias festividades "fijas", en la práctica no es tan simple, ya que (al menos en España), ciertas fiestas se cambian de fecha en función de si caen en domingo, y otras dependen de comunidades autónomas o poblaciones locales.
Una aplicación que deba tener en cuenta los días festivos debería proporcionar herramientas para mantener una base de datos de esos días.
Hay muchas maneras de mantener un control de calendario actualizado con una lista de festivos.
Sin embargo hay que tener en cuenta que una vez creado el control, cualquier autoridad que sea añada no actualizará en control para el mes actualmente mostrado. Por ejemplo, si optamos por añadir una autoridad para cada año, procesando el evento EVT_CALENDAR_YEAR, y al cambiar de diciembre a enero, cargamos la nueva autoridad para el año siguiente, los festivos de enero no se mostrarán automáticamente, ya que el control ya ha sido dibujado cuando recibimos la notificación.
Podemos evitar esto de varias formas. Por ejemplo, cambiando la fecha a un año diferente después de cargar la nueva autoridad y volviendo a la fecha previa, de modo que obligamos a redibujar el control dos veces. Es poco elegante, pero funciona casi siempre.
void calendar::OnCambioAnno(wxCalendarEvent& event) { AsignarAutoridad(event.GetDate().GetYear()); // Asignar los festivos m_calendar->SetDate(wxDateTime(1, wxDateTime::Jan, 800)); // Cambiar a una fecha diferente (1/01/0800) m_calendar->SetDate(event.GetDate()); // Volver a asignar la fecha actual
El problema es si se ha establecido un rango de fechas, en cuyo caso, la fecha que usemos para provocar la recarga probablemente esté fuera del rango, en cuyo caso el cambio será ignorado.
Una alternativa mejor es usar el método EnableHolidayDisplay para ocultar los festivos y volver a mostrarlos, de modo que obligamos a actualizar el control:
void calendar::OnCambioAnno(wxCalendarEvent& event) { AsignarAutoridad(event.GetDate().GetYear()); m_calendar->EnableHolidayDisplay(false); m_calendar->EnableHolidayDisplay(true); }
También podemos cargar las autoridades del año anterior, actual y posterior al actual, de modo que siempre estén cargadas las festividades de tres periodos consecutivos.
O podemos crear autoridades para meses en lugar de años, procesar el evento EVT_CALENDAR_MONTH, y mantener cargadas las del mes anterior, actual y siguiente. O sencillamente, cargar todas las autoridades disponibles.
Empecemos creando una clase de autoridades de festivos propia, derivada de wxDateTimeHolidayAuthority. En nuestro caso queremos que se marquen los festivos que leeremos desde un fichero, para lo que añadiremos un método InsertarFestivo y un conjunto o set donde almacenaremos las fechas.
También consideraremos como festivos los domingos, pero no los sábados, de modo que no podemos usar la clase wxDateTimeWorkDays, así que crearemos otra clase para esa tarea:
#include <set> class wxDateTimeFestivosAnuales : public wxDateTimeHolidayAuthority { public: void InsertarFestivo(const wxDateTime& dt); protected: virtual bool DoIsHoliday(const wxDateTime& dt) const wxOVERRIDE; virtual size_t DoGetHolidaysInRange(const wxDateTime& dtStart, const wxDateTime& dtEnd, wxDateTimeArray& holidays) const wxOVERRIDE; private: std::set<wxDateTime> festivo; }; class wxDateTimeDomingos : public wxDateTimeHolidayAuthority { protected: virtual bool DoIsHoliday(const wxDateTime& dt) const wxOVERRIDE; virtual size_t DoGetHolidaysInRange(const wxDateTime& dtStart, const wxDateTime& dtEnd, wxDateTimeArray& holidays) const wxOVERRIDE; };
La implementación queda:
bool wxDateTimeFestivosAnuales::DoIsHoliday(const wxDateTime& dt) const { auto busca = festivo.find(dt); return busca != festivo.end(); } size_t wxDateTimeFestivosAnuales::DoGetHolidaysInRange(const wxDateTime& dtStart, const wxDateTime& dtEnd, wxDateTimeArray& holidays) const { if( dtStart > dtEnd ) { wxFAIL_MSG( wxT("rango de fechas inválido en GetHolidaysInRange") ); return 0u; } holidays.Empty(); for(wxDateTime dt : festivo) { if(dt >= dtStart && dt <= dtEnd) holidays.Add(dt); } return holidays.GetCount(); } void wxDateTimeFestivosAnuales::InsertarFestivo(const wxDateTime& dt) { festivo.insert(dt); } bool wxDateTimeDomingos::DoIsHoliday(const wxDateTime& dt) const { return (dt.GetWeekDay() == wxDateTime::Sun); } size_t wxDateTimeDomingos::DoGetHolidaysInRange(const wxDateTime& dtStart, const wxDateTime& dtEnd, wxDateTimeArray& holidays) const { if( dtStart > dtEnd ) { wxFAIL_MSG( wxT("rango de fechas inválido en GetHolidaysInRange") ); return 0u; } holidays.Empty(); wxDateTime dtPrimerDomingo = dtStart.GetNextWeekDay(wxDateTime::Sun); wxDateTime dtUltimoDomingo = dtEnd.GetPrevWeekDay(wxDateTime::Sun); for(wxDateTime dt = dtPrimerDomingo; dt <= dtUltimoDomingo; dt += wxDateSpan::Week()) { holidays.Add(dt); } return holidays.GetCount(); }
Estas clase son lo suficientemente flexibles como para almacenar los festivos en el rango de tiempo que queramos, ya sean uno o más años o meses, todo depende del modo en que añadamos los festivos.
Por ejemplo, para asignar todos los festivos de un año podemos usar este método:
void calendar::AsignarAutoridad(int year) { wxFileStream fe("fiestas.txt"); if(!fe.IsOk()) return; wxDateTime dt; wxString cad; char buffer[20]; wxString::const_iterator end; wxDateTimeHolidayAuthority::ClearAllAuthorities(); festivos = new wxDateTimeFestivosAnuales; while(!fe.Eof()) { if(!fe.Read(buffer, 12).Eof()) { cad = wxString(buffer); if(dt.ParseFormat(cad, "%d/%m/%Y", &end) && year==dt.GetYear()) { festivos->InsertarFestivo(dt); } } } wxDateTimeHolidayAuthority::AddAuthority(festivos); }
Donde las fechas correspondientes a los días festivos se almacenan en un fichero de texto "fiestas.txt", una fecha en cada línea, con el formato "dd/mm/aaaa". Eso son ocho caracteres más los dos correspondientes al cambio de línea en Windows "\n\r", en otros sistemas probablemente sean once caracteres.
Hemos usado la clase wxFileStream, dedicaremos un capítulo a las clases para ficheros más adelante.
Resaltar fechas
Podemos resaltar algunas fechas concretas usando el método Mark, indicando el día y un booleano para activar o desactivar la marca. La marca se mostrará como un rectángulo alrededor del día indicado, y ese día permanecerá marcado aunque se cambie de mes, es decir, si queremos que sólo se marque el 12 de marzo de 2025, deberemos marcarlo cuando se muestre ese mes, y desmarcarlo para el resto de meses.
Podemos usar este método para marcar el día actual, y procesar los eventos de cambio de mes o año o el evento EVT_CALENDAR_PAGE_CHANGED
void calendar::OnCambioMes(wxCalendarEvent& event) { MarcarHoy(event.GetDate()); } void calendar::MarcarHoy(const wxDateTime& dt) { if(dt.GetYear() == wxDateTime::Today().GetYear() && dt.GetMonth() == wxDateTime::Today().GetMonth()) { m_calendar->Mark(wxDateTime::Today().GetDay(), true); } else { m_calendar->Mark(wxDateTime::Today().GetDay(), false); } }

Limitar rangos de fechas
Es posible establecer límites al rango de fechas que el usuario puede seleccionar. Se puede establecer un límite inferior, uno superior o ambos. Para ello disponemos del método SetDateRange, indicando como parámetros las fechas de inicio y final del rango. Si no queremos establecer alguno de los extremos podemos usar el valor wxDefaultDateTime.
m_calendar->SetDateRange(wxDateTime(15, wxDateTime::Nov, 2024), wxDateTime(18, wxDateTime::Feb, 2025));

Control wxDatePicker
Otra forma de capturar fechas introducidas por el usuario es el control wxDatePickerCtrl. O en su formato genérico wxDatePickerCtrlGeneric.
Estos controles son más sencillos de usar, son más pequeños y permiten introducir fechas usando el teclado, por lo que pueden ser más rápidos de utilizar si el rango de fechas admitidas es muy grande o el espacio disponible en el control es pequeño.
Estilos
Disponemos de varios estilos, pero algunos sólo están soportados en su forma nativa para ciertas plataformas, por ejemplo, el estilo wxDP_DROPDOWN, que permite desplegar un control calendario, en Windows utiliza la forma nativa. El estilo wxDP_SPIN que permite usar controles spin para cambiar de día, mes o año, no está soportado en la versión genérica.
El resultado es que los estilos están muy limitados para ciertas combinaciones, y probablemente es mejor usar el estilo wxDP_DEFAULT con la versión no genérica del control. Este estilo usa wxDP_SPIN en Windows y wxDP_DROPDOWN en macOS, sobre todo si se está escribiendo código portable.
El estilo wxDP_ALLOWNONE permite que el usuario no introduzca ninguna fecha válida, y wxDP_SHOWCENTURY muestra cuatro dígitos para los años.
En el programa de ejemplo usaremos el control no genérico y los estilos por defecto: wxDP_DEFAULT y wxDP_SHOWCENTURY.
Si se quiere usar la versión genérica hay que incluir los ficheros de cabecera "wx/datectrl.h" y "wx/generic/datectrl.h".
Al contrario que el control de calendario, estos controles sí admiten un validador, que en su forma más sencilla validará el contenido al cerrar el diálogo, devolviendo el valor al proceso que lo invocó.
class dpValidator : public wxValidator { public: dpValidator(wxDateTime *v = nullptr) : m_valor(v) {} virtual wxObject* Clone() const { return new dpValidator(*this); } virtual bool TransferFromWindow(); virtual bool TransferToWindow() { return true; } virtual bool Validate(wxWindow * parent) { return true; } private: wxDateTime* m_valor; DECLARE_DYNAMIC_CLASS( dpValidator ) DECLARE_EVENT_TABLE() };
Y la implementación es sencilla:
IMPLEMENT_DYNAMIC_CLASS( dpValidator, wxValidator ) BEGIN_EVENT_TABLE( dpValidator, wxValidator ) END_EVENT_TABLE() bool dpValidator::TransferFromWindow() { try { wxDatePickerCtrl *dp = dynamic_cast<wxDatePickerCtrl*>(m_validatorWindow); *m_valor = dp->GetValue(); } catch (...) { wxFAIL_MSG( _T("dpValidator sólo funciona con wxDatePicker")); return false; } return true; }
El constructor no difiere mucho de los que hemos visto hasta ahora, y requiere los parámetros habituales:
- La ventana padre.
- El identificador del control.
- Una referencia al valor inicial del control, que puede ser wxDefaultDateTime para usar la fecha actual.
- La posición.
- El tamaño. Es recomendable usar el valor por defecto, wxDefaultSize.
- El estilo.
- Un validador.
- El nombre.
Ahora podemos crear el control:
m_datepicker = new wxDatePickerCtrl(this, iddatepicker, datos.valor, wxDefaultPosition, wxDefaultSize, wxDP_SPIN | wxDP_SHOWCENTURY, dpValidator(&datos.valor));
Eventos
Sólo existe un evento para este tipo de controles. EVT_DATE_CHANGED, que se genera cada vez que el usuario cambia la fecha seleccionada.
Rangos
Al igual que los controles de calendario, en estos también podemos limitar el rango de fechas que el usuario puede introducir. En este caso usando el método SetRange.
m_datepicker->SetRange(wxDateTime(15, wxDateTime::Nov, 2024), wxDateTime(18, wxDateTime::Feb, 2025));

Control wxTimePicker
Por último, wxWidgets nos proporciona un control para introducir horas, wxTimePickerCtrl.
Funciona de forma similar a wxDatePickerCtrl, con la diferencia de que permite introducir horas, minutos y segundos.
Como wxWidgets no dispone de una clase para almacenar horas, se usa wxDateTime, y la parte de la fecha se ignora.
Esta clase de controles no tiene estilos específicos.
Al igual que con el control wxDatePickerCtrl, estos controles también admiten un validador, que en su forma más sencilla validará el contenido al cerrar el diálogo, devolviendo el valor de la hora.
class tpValidator : public wxValidator { public: tpValidator(wxDateTime *v = nullptr) : m_valor(v) {} virtual wxObject* Clone() const { return new tpValidator(*this); } virtual bool TransferFromWindow(); virtual bool TransferToWindow() { return true; } virtual bool Validate(wxWindow * parent) { return true; } private: wxDateTime* m_valor; DECLARE_DYNAMIC_CLASS( tpValidator ) DECLARE_EVENT_TABLE() };
Y la implementación es sencilla:
IMPLEMENT_DYNAMIC_CLASS( tpValidator, wxValidator ) BEGIN_EVENT_TABLE( tpValidator, wxValidator ) END_EVENT_TABLE() bool tpValidator::TransferFromWindow() { try { wxTimePickerCtrl *dp = dynamic_cast<wxTimePickerCtrl*>(m_validatorWindow); *m_valor = dp->GetValue(); } catch (...) { wxFAIL_MSG( _T("tpValidator sólo funciona con wxTimePicker")); return false; } return true; }
El constructor también es muy parecido a los que ya hemos visto, y requiere los siguientes parámetros:
- La ventana padre.
- El identificador del control.
- Una referencia al valor inicial del control, que puede ser wxDefaultDateTime para usar la fecha actual.
- La posición.
- El tamaño. Es recomendable usar el valor por defecto, wxDefaultSize.
- El estilo.
- Un validador.
- El nombre.
Ahora podemos crear el control:
m_timepicker = new wxTimePickerCtrl(this, idtimepicker, datos.valor, wxDefaultPosition, wxDefaultSize, 0, tpValidator(&datos.valor));
Eventos
Sólo existe un evento para este tipo de controles. wxEVT_TIME_CHANGED, que se genera cada vez que el usuario cambia la hora seleccionada.
Ejemplo 14
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 14 | wx014.zip | 2025-01-01 | 11232 bytes | 7 |