Inspired by the need to set a deadline, where not only were a date and time required but also
the ability to return a timestamp, which is easy to store in a database, easily sorted and
easily converted back into a date.
I’ve endeavoured to make this as friendly as possible, allowing for the retrieving of a wx.DateTime, a datetime, a string or a timestamp.
As usual let me know if there are errors or omissions.
"""
MiniDateTime.py
A custom class that allows selection of both date and time, with the ability to customise
the calendar and the output format.
Inspired by the need to set deadlines, where not only were a date and time required but also
the ability to return a timestamp, which is easy to store in a database, easily sorted and
easily converted back into a date. (Thus GetTimeStamp())
Works with wx.DateTime or python datetime values
With or without an activating button
Uses wx.adv.GenericCalendarCtrl and wx.adv.TimePickerCtrl
Uses locale to enable different languages for the calendar
MiniDateTime(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
style=wx.BORDER_SIMPLE, name="MiniDateTime", date=0, formatter=''):
@param parent: Parent window. Must not be None.
@param id: identifier. A value of -1 indicates a default value.
@param pos: MiniDateTime position. If the position (-1, -1) is specified
then a default position is chosen.
@param size: If the default size (-1, -1), is specified then a default size is calculated.
Size should be able to accomodate the specified formatter string + button
@param style: Alignment (Left,Middle,Right).
@param name: Widget name.
@param date: Initial date - invalid date = now
@param formatter A date formatting string in the form of a lambda function
default lambda dt: dt.FormatISOCombined(sep=b' ')
= ISO 8601 format "YYYY-MM-DD HH:MM:SS".
or a lambda function with a format string
e.g.:
format = lambda dt: (f'{dt.Format("%a %d-%m-%Y %H:%M:%S")}')
format = lambda dt: (f'{dt.Format("%A %d %B %Y %H:%M:%S")}')
format = lambda dt: (f'{dt.Format("%a %d %B %Y %I:%M %p")}')
or
fmt = "%Y/%m/%d %H:%M:%S"
format = lambda dt: (dt.Format(fmt))
format = lambda dt: (dt.Format("%Y/%m/%d %H:%M:%S"))
format = lambda dt: (dt.FormatISOCombined(sep=b' '))
TextCtrl Styles: wx.TE_READONLY (Default)
wx.TE_RIGHT
wx.TE_LEFT
wx.TE_CENTRE
wx.BORDER_NONE is always applied to the internal textctrl
wx.BORDER_SIMPLE is the default border for the control itself
Events: EVT_DATE_CHANGED A date change occurred in the control
Event Functions:
GetValue() Returns formatted date in the event as a string
GetDate() Returns wxDateTime date in the event
GetDateTime() Returns python datetime of date in the event
GetTimeStamp() Returns seconds since Jan 1, 1970 UTC for selected date
Functions:
GetValue() Returns wxDateTime date in the event as a string
GetDate() Returns wxDateTime date in the control
GetDateTimeValue() Returns python datetime of date in the control
GetTimeStamp() Returns seconds since Jan 1, 1970 UTC for selected datetime
GetLocale() Returns tuple of current language code and encoding
SetValue(date) Sets the date in the control
expects a wx.DateTime, a python datetime datetime or a datetime timestamp
Any invalid date defaults to wx.DateTime.Now()
Milliseconds are stripped off
SetFormatter(formatter) Date format in the form of a lambda
default: lambda dt: dt.FormatISOCombined(sep=b' ') (see above)
SetButton(Boolean) Shows or Hides Ctrl Button
SetLocale(locale) Set the locale for Calendar day and month names
e.g. 'de_DE.UTF-8' German
'es_ES.UTF-8' Spanish
depends on the locale being available on the machine
SetCalendarStyle(style)
wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week (not in wxGTK)
wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week (not in wxGTK)
wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar (only generic)
wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
wx.adv.CAL_SHOW_WEEK_NUMBERS
SetCalendarHighlights(colFg, colBg) Calendar and TimeCtrl highlight colours
SetCalendarHeaders(colFg, colBg) Calendar Header colours
SetCalendarFg(colFg) Calendar ForegroundColour
SetCalendarBg(colBg) Calendar & Ctrl BackgroundColour
Default Values:
date - Now
style - READ_ONLY
Author: J Healey
Created: 04/12/2022
Copyright: J Healey
License: GPL 3 or any later version
Email: <rolfofsaxony@gmx.com>
Usage example:
import wx
import minidatetime as MDT
class Frame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "MiniDateTime Demo")
format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y %H:%M:%S")}'))
panel = wx.Panel(self)
mdp = MDT.MiniDateTime(panel, -1, pos=(50, 50), size=(280,-1), style=0, date=0, formatter=format)
self.Show()
app = wx.App()
frame = Frame(None)
app.MainLoop()
"""
import wx
import wx.adv
from wx.lib.embeddedimage import PyEmbeddedImage
import datetime
import locale
img = PyEmbeddedImage(
b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAABhGlDQ1BJQ0MgcHJvZmlsZQAA'
b'KJF9kT1Iw0AcxV9TiyJVB4uIOGSoThZERRy1CkWoEGqFVh1MLv2CJg1Jiouj4Fpw8GOx6uDi'
b'rKuDqyAIfoA4OjkpukiJ/0sKLWI8OO7Hu3uPu3eAUC8zzeoYBzTdNlOJuJjJroqdrwgjhF4M'
b'QJSZZcxJUhK+4+seAb7exXiW/7k/R4+asxgQEIlnmWHaxBvE05u2wXmfOMKKskp8Tjxm0gWJ'
b'H7muePzGueCywDMjZjo1TxwhFgttrLQxK5oa8RRxVNV0yhcyHquctzhr5Spr3pO/MJzTV5a5'
b'TnMYCSxiCRJEKKiihDJsxGjVSbGQov24j3/I9UvkUshVAiPHAirQILt+8D/43a2Vn5zwksJx'
b'IPTiOB8jQOcu0Kg5zvex4zROgOAzcKW3/JU6MPNJeq2lRY+Avm3g4rqlKXvA5Q4w+GTIpuxK'
b'QZpCPg+8n9E3ZYH+W6B7zeutuY/TByBNXSVvgINDYLRA2es+7+5q7+3fM83+fgBDkHKUu8H2'
b'wwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAulJREFUOMvNVTFI80AY/VIirVh1qAoVLbWLVYeC'
b'i/aQiFIFV4WCi4OD4CAIDgXnDioKiludHFxEKNi6KHRQkkZEFKEhDqI/wVwrhLSIBkzJ/cP9'
b'lLZW7eDwvyEJX969e/e93IUhhMBvwAa/hAqhXC63sLDwmaTr+vT0dLFY/EaIpbf39/etrS1F'
b'Ufb29lwuVxWpUCjE4/HV1VW/3z8/P19biRBCCLm5uQGAvr4+AOjv76/i0PrAwAAVJbXAlg9Y'
b'Wlpqb2+/uLiQJKm8vry83Nraend3l8lkvlxbuaPR0dGNjY3PnOHh4d3dXafTWZcjhFAymbQs'
b'KxwOq6ra2dlJrxhjt9uNMeZ5PhAI/Jway7JOp7OlpUVVVYRQNptFCGGMg8GgqqrBYPDt7e3n'
b'1CheX191Xff5fDzP9/T08Dzv8/nS6XQgEMAYsywry7Ldbv+hRxzHpVIpABAEAQBEUQSATCZz'
b'f39/eno6OztbMr65uakoSlWPbFUxUxfU0fX1tcfjicVik5OTg4ODPM/f3t4mEonLy8tQKETn'
b'q8uRIAjRaNTlcsmyTAiRJCmVShFCTNOk4T4+PpYcVQhhjAVByOVygiDoui7LMtWlnLW1NY7j'
b'6PPHx8fc3FwkEqm9NEmSEEIPDw8IoaamJlEUJyYmhoaG6FuGYUrMhoaGxcXF9fX1fD5fHb9l'
b'Wb29vaIoer3edDrNMIwsyzMzMzbbP47D4TAMo/QRdHR0AMDLy0tF/B6P5/n5uaurq6R7dnZm'
b't9sLhUKp4vV6r66uuru7o9Ho1NSUaZoA0NzcXNFsQkg+n/9TBk3Tjo6O/H6/YRiUYFnW09PT'
b'wcFBW1vbzs7O8fExPTYqml0TiqI0NjaenJxU1Q3D0DSN47jt7e3q1L7C4eEhACQSiWKxWCpm'
b's9mVlZXx8XFN0+oVMk0zFosBQDgc3t/fj8fjkUjE7XaPjY2pqlrOZOo5/BVFSSaT5+fndPeG'
b'QqGRkRGHw1HOYf67v8hfVpeRQVPNMc8AAAAASUVORK5CYII=')
mdtEVT = wx.NewEventType()
EVT_DATETIME_SELECTED = wx.PyEventBinder(mdtEVT, 1)
class mdtEvent(wx.PyCommandEvent):
def __init__(self, eventType, eventId=1, date=None, value=''):
"""
Default class constructor.
:param `eventType`: the event type;
:param `eventId`: the event identifier.
"""
wx.PyCommandEvent.__init__(self, eventType, eventId)
self._eventType = eventType
self.date = date
self.value = value
def GetDate(self):
"""
Retrieve the date value of the control at the time
this event was generated, Returning a wx.DateTime object"""
return self.date
def GetValue(self):
"""
Retrieve the formatted date value of the control at the time
this event was generated, Returning a string"""
return self.value
def GetTimeStamp(self):
"""
Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
Returning a integer
"""
return int(self.date.GetValue()/1000)
def GetDateTime(self):
"""
Retrieve the date value of the control at the time
this event was generated, Returning a python datetime object"""
return wx.wxdate2pydate(self.date)
class MiniDateTime(wx.Control):
def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
style=wx.BORDER_SIMPLE, name="MiniDateTime", date=0, formatter=''):
wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
self.parent = parent
self._date = date
if formatter:
format = formatter
else:
format = lambda dt: dt.FormatISOCombined(sep=b' ')
font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
#self.SetWindowStyle(wx.BORDER_NONE)
self._style = style
self._calendar_style = wx.adv.CAL_MONDAY_FIRST
self._calendar_headercolours = None
self._calendar_highlightcolours = None
self._calendar_Bg = None
self._calendar_Fg = None
if size == wx.DefaultSize:
dc = wx.ScreenDC()
dc.SetFont(font)
trialdate = format(wx.DateTime(28,9,2022)) # a Wednesday in September = longest names in English
w, h = dc.GetTextExtent(trialdate)
size = (w+54, -1) # Add image width (24) plus a buffer
del dc
self._veto = False
txtstyle = wx.TE_READONLY
if style & wx.TE_LEFT or style == wx.TE_LEFT:
txtstyle = txtstyle | wx.TE_LEFT
elif style & wx.TE_RIGHT:
txtstyle = txtstyle | wx.TE_RIGHT
else:
txtstyle = txtstyle | wx.TE_CENTRE
if style & wx.TE_READONLY:
txtstyle = txtstyle | wx.TE_READONLY
if style & wx.BORDER_NONE:
txtstyle = txtstyle | wx.BORDER_NONE
# MiniDateTime Picker
self.ctl = wx.TextCtrl(self, id, value=str(self._date),
pos=pos, size=size, style=txtstyle, name=name)
self.button = wx.BitmapButton(self, -1, bitmap=img.Bitmap)
self.MinSize = self.GetBestSize()
self._formatter = format
self.button.Bind(wx.EVT_BUTTON, self.OnCalendar)
self.ctl.Bind(wx.EVT_LEFT_DOWN, self.OnCalendar)
self.SetValue(date)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.ctl, 1, wx.EXPAND, 0)
sizer.Add(self.button, 0, wx.ALIGN_CENTER_VERTICAL, 0)
self.SetSizerAndFit(sizer)
self.Show()
def OnCalendar(self, _event=None):
window = CalendarPopup(
self, self._date, self.OnDate, self.GetTopLevelParent(), wx.SIMPLE_BORDER)
pos = self.ClientToScreen((0, 0))
size = self.GetSize()
window.Position(pos, (0, size.height))
def SetFormatter(self, formatter):
'''formatter will be called with a wx.DateTime'''
self._formatter = formatter
self.OnDate(self._date)
def SetLocale(self, alias):
try:
locale.setlocale(locale.LC_TIME, locale=alias)
except Exception as e:
locale.setlocale(locale.LC_TIME, locale='')
self.SetValue(self._date)
def SetCalendarStyle(self, style=0):
self._calendar_style = style
def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
self._calendar_headercolours = colFg, colBg
def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
self._calendar_highlightcolours = colFg, colBg
def SetCalendarFg(self, colFg=wx.NullColour):
self._calendar_Fg = colFg
def SetCalendarBg(self, colBg=wx.NullColour):
self._calendar_Bg = colBg
def SetButton(self, button=True):
if button:
self.button.Show()
else:
self.button.Hide()
self.Layout()
def OnDate(self, date):
self._date = date
self.ctl.SetValue(self._formatter(date))
self.MinSize = self.GetBestSize()
if self._veto:
self._veto = False
return
event = mdtEvent(mdtEVT, self.GetId(), date=date, value=self._formatter(date))
event.SetEventObject(self)
self.GetEventHandler().ProcessEvent(event)
def GetValue(self):
return self._ctl.GetValue()
def GetDate(self):
return self._date
def GetTimeStamp(self):
"""
Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
"""
return self._date.GetValue()/1000
def GetDateTimeValue(self):
"""
Return a python datetime object"""
return wx.wxdate2pydate(self._date)
def GetLocale(self):
return locale.getlocale(category=locale.LC_TIME)
def SetValue(self, date):
if isinstance(date, wx.DateTime):
pass
elif isinstance(date, datetime.date):
date = wx.pydate2wxdate(date)
elif isinstance(date, int) and date > 0:
date = wx.DateTime.FromTimeT(date)
elif isinstance(date, float) and date > 0:
date = wx.DateTime.FromTimeT(int(date))
else: # Invalid date value default to now
date = wx.DateTime.Now()
date.SetSecond(0)
date.millisecond = 0
self._date = date
self._veto = True
self.SetFormatter(self._formatter)
class CalendarPopup(wx.PopupTransientWindow):
def __init__(self, parent, date, callback, *args, **kwargs):
'''date is the initial date; callback is called with the chosen date'''
super().__init__(*args, **kwargs)
self.callback = callback
self.calendar = wx.adv.GenericCalendarCtrl(self, pos=(5, 5), style=parent._calendar_style)
self.calendar.SetDate(date)
self.tpc = wx.adv.TimePickerCtrl(self, size=(140, -1),
style = wx.adv.TP_DEFAULT)
self.tpc.SetTime(date.hour, date.minute, date.second)
tpc_textctrl = self.tpc.GetChildren()[0]
tpc_spinbutton = self.tpc.GetChildren()[1]
self.select = wx.Button(self, -1, "&Select")
self.quit = wx.Button(self, -1, "&Quit")
if parent._calendar_headercolours:
self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
if parent._calendar_Bg:
self.calendar.SetBackgroundColour(parent._calendar_Bg)
self.tpc.SetBackgroundColour(parent._calendar_Bg)
self.SetBackgroundColour(parent._calendar_Bg)
if parent._calendar_highlightcolours:
self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
tpc_textctrl.SetForegroundColour(parent._calendar_highlightcolours[0])
tpc_textctrl.SetBackgroundColour(parent._calendar_highlightcolours[1])
if parent._calendar_Fg:
self.calendar.SetForegroundColour(parent._calendar_Fg)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer1 = wx.BoxSizer(wx.VERTICAL)
sizer2 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(self.calendar, 1, wx.ALL | wx.EXPAND, 5)
sizer1.Add(self.tpc, 0, wx.ALL | wx.EXPAND, 5)
sizer2.Add(self.select, 0, wx.ALL, 5)
sizer2.Add(self.quit, 0, wx.ALL, 5)
sizer.Add(sizer1)
sizer.Add(sizer2)
self.SetSizerAndFit(sizer)
self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnDateChosen)
self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnFocus)
self.select.Bind(wx.EVT_BUTTON, self.OnChosen)
self.quit.Bind(wx.EVT_BUTTON, self.OnQuit)
self.calendar.SetToolTip("Arrow keys and PageUp/pageDn\nAdjust the Calendar")
self.Popup()
def OnDateChosen(self, _event=None):
self.tpc.SetFocus()
def OnFocus(self, _event=None):
self.calendar.SetFocus()
def OnChosen(self, _event=None):
_date = self.calendar.GetDate()
_hh, _mm, _ss = self.tpc.GetTime()
_date.SetHour(_hh)
_date.SetMinute(_mm)
_date.SetSecond(_ss)
self.callback(_date)
self.Dismiss()
def OnQuit(self, event):
self.Dismiss()
class DemoFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "MiniDateTime Picker Demo")
#format = (lambda dt:
# (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
# )
format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y %H:%M:%S")}'))
#format = (lambda dt: (f'{dt.Format("%A %d %B %Y %H:%M:%S")}'))
panel = wx.Panel(self)
mdp = MiniDateTime(panel, -1, pos=(50, 50), style=0, date=0, formatter=format)
#mdp.SetLocale('es_ES.UTF-8')
#mdp.SetFormatter(format)
x=datetime.datetime.now()
mdp.SetValue(x.timestamp())
#mdp.SetValue(0)
#mdp.SetButton(False)
mdp.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST)
#mdp.button.SetBackgroundColour('lightgreen')
mdp.ctl.SetBackgroundColour('lightgreen')
mdp.SetCalendarHeaders(colFg='red', colBg='lightgreen')
mdp.SetCalendarHighlights(colFg='red', colBg='lightblue')
mdp.SetCalendarBg(colBg='azure')
self.Bind(EVT_DATETIME_SELECTED, self.OnEvent)
def OnEvent(self, event):
print(event.GetValue())
print(event.GetDate())
print(event.GetDateTime())
print(event.GetTimeStamp())
if __name__ == '__main__':
app = wx.App()
frame = DemoFrame(None)
frame.Show()
app.MainLoop()