How to create a wx.DatePickerCtrl with a customisable format (Phoenix)

Keywords : wx.DatePickerCtrl, wx.adv.CAL_MONDAY_FIRST, wx.DateTime, wx.pydate2wxdate, wx.adv.GenericCalendarCtrl, wx.PopupTransientWindow, wx.adv.CAL_SHOW_WEEK_NUMBERS.


Introduction :

DatePickerCtrl is seemingly stuck with a MM/DD/YYYY format, great for our North American chums but which makes it virtually useless for the rest of us. Hopefully others will find this useful as you can decide the date format you wish to use and elect to use a calendar in your own language too.


Demonstrating :

Tested py3.8.10, wx4.x and Linux.

Tested py3.10.5, wx4.2.1 and Win11.

Are you ready to use some samples ? ;)

Test, modify, correct, complete, improve and share your discoveries ! (!)


First example

img_sample_one.png

   1 """
   2     MiniDatePicker.py
   3 
   4     A custom class that looks like the wx.DatePickerCtrl but with the ability to customise
   5     the calendar and the output format. (DatePickerCtrl is seemingly stuck with MM/DD/YYYY format)
   6     Works with wx.DateTime or python datetime values
   7     With or without an activating button
   8     Uses wx.adv.GenericCalendarCtrl
   9     Uses locale to enable different languages for the calendar
  10 
  11     MiniDatePicker(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
  12                  style=wx.BORDER_SIMPLE, name="MiniDatePicker", date=0, formatter=''):
  13 
  14         @param parent:   Parent window. Must not be None.
  15         @param id:       identifier. A value of -1 indicates a default value.
  16         @param pos:      MiniDatePicker position. If the position (-1, -1) is specified
  17                          then a default position is chosen.
  18         @param size:     If the default size (-1, -1) is specified then a default size is calculated.
  19                          Size should be able to accomodate the specified formatter string + button
  20         @param style:    Alignment (Left,Middle,Right).
  21         @param name:     Widget name.
  22         @param date:     Initial date (an invalid date = today)
  23         @param formatter A date formatting string in the form of a lambda function
  24                          default lambda dt: dt.FormatISODate()
  25                           = ISO 8601 format "YYYY-MM-DD".
  26                          or a lambda function with a format string e.g.:
  27                             lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
  28                          e.g.:
  29                             format = lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
  30                             format = lambda dt: (f'{dt.Format("%A %d %B %Y")}')
  31                          or
  32                             fmt = "%Y/%m/%d"
  33                             format = lambda dt: (dt.Format(fmt))
  34                             format = lambda dt: (dt.Format("%Y/%m/%d"))
  35                             format = lambda dt: (dt.FormatISODate())
  36 
  37     TextCtrl Styles: wx.TE_READONLY (Default)
  38             wx.TE_RIGHT
  39             wx.TE_LEFT
  40             wx.TE_CENTRE
  41 
  42             wx.BORDER_NONE is always applied to the internal textctrl
  43             wx.BORDER_SIMPLE is the default border for the control itself
  44 
  45     Events: EVT_DATE_CHANGED A date change occurred in the control
  46 
  47     Event Functions:
  48         GetValue()          Returns formatted date in the event as a string
  49 
  50         GetDate()           Returns wxDateTime date in the event
  51 
  52         GetDateTime()       Returns python datetime of date in the event
  53 
  54         GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for current date
  55 
  56     Functions:
  57         GetValue()          Returns formatted date in the event as a string
  58 
  59         GetDate()           Returns wxDateTime date in the control
  60 
  61         GetDateTimeValue()  Returns python datetime of date in the control
  62 
  63         GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date
  64 
  65         GetLocale()         Returns tuple of current language code and encoding
  66 
  67         SetValue(date)      Sets the date in the control
  68                             expects a wx.DateTime, a python datetime datetime or a timestamp
  69                             Any invalid date defaults to wx.DateTime.Today()
  70 
  71         SetFormatter(formatter) Date format in the form of a lambda
  72             default:    lambda dt: dt.FormatISODate()
  73 
  74         SetButton(Boolean)  Shows or Hides Ctrl Button
  75 
  76         SetLocale(locale)   Set the locale for Calendar day and month names
  77                              e.g. 'de_DE.UTF-8' German
  78                                   'es_ES.UTF-8' Spanish
  79                              depends on the locale being available on the machine
  80 
  81         SetCalendarStyle(style)
  82             wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week (not in wxGTK)
  83             wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week (not in wxGTK)
  84             wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar (only generic)
  85             wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
  86             wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
  87             wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
  88             wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
  89             wx.adv.CAL_SHOW_WEEK_NUMBERS
  90 
  91         SetCalendarHighlights(colFg, colBg)
  92 
  93         SetCalendarHeaders(colFg, colBg)
  94 
  95         SetCalendarFg(colFg)    Calendar ForegroundColour
  96 
  97         SetCalendarBg(colBg)    Calendar & Ctrl BackgroundColour
  98 
  99     Default Values:
 100         date    -       Today
 101         style   -       READ_ONLY
 102 
 103 Author:     J Healey
 104 Created:    04/12/2022
 105 Copyright:  J Healey - 2022
 106 License:    GPL 3 or any later version
 107 Email:      <rolfofsaxony@gmx.com>
 108 
 109 Usage example:
 110 
 111 import wx
 112 import minidatepicker as MDP
 113 class Frame(wx.Frame):
 114     def __init__(self, parent):
 115         wx.Frame.__init__(self, parent, -1, "MiniDatePicker Demo")
 116         format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y")}'))
 117         panel = wx.Panel(self)
 118         mdp = MDP.MiniDatePicker(panel, -1, pos=(50, 50), size=(-1,-1), style=0, date=0, formatter=format)
 119         self.Show()
 120 
 121 app = wx.App()
 122 frame = Frame(None)
 123 app.MainLoop()
 124 
 125 """
 126 
 127 import wx
 128 import wx.adv
 129 from wx.lib.embeddedimage import PyEmbeddedImage
 130 import datetime
 131 import locale
 132 
 133 img = PyEmbeddedImage(
 134     b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABg2lDQ1BJQ0MgcHJvZmlsZQAA'
 135     b'KJF9kT1Iw0AcxV9TS0UiDu0g4pChOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOL'
 136     b's64OroIg+AHi6OSk6CIl/i8ptIj14Lgf7+497t4BQqPCdLtnHNANx0onE1I2tyqFXyEihAhi'
 137     b'EBVmm3OynELX8XWPAF/v4jyr+7k/R7+WtxkQkIhnmWk5xBvE05uOyXmfOMpKikZ8Tjxm0QWJ'
 138     b'H7mu+vzGueixwDOjViY9TxwlloodrHYwK1k68RRxTNMNyheyPmuctzjrlRpr3ZO/UMwbK8tc'
 139     b'pzmMJBaxBBkSVNRQRgUO4rQapNhI036ii3/I88vkUslVBiPHAqrQoXh+8D/43a1dmJzwk8QE'
 140     b'EHpx3Y8RILwLNOuu+33sus0TIPgMXBltf7UBzHySXm9rsSNgYBu4uG5r6h5wuQMMPpmKpXhS'
 141     b'kKZQKADvZ/RNOSByC/St+b219nH6AGSoq9QNcHAIjBYpe73Lu3s7e/v3TKu/H0FDcpMpQj/v'
 142     b'AAAACXBIWXMAAAsTAAALEwEAmpwYAAACk0lEQVRIx43W3+unQxQH8Nc83w9tVutHNj9v5M4q'
 143     b'tty7oC2u2Fy58KOV9g/AlRJygQvlRrIlpbhSW7uJorCKlKQoJand7AVJYm3Z7zMu9v18zWfM'
 144     b'J6aeZs7MmTNnznm/zzwFE6p/WunkLczNXEnfyhO2Rza2rLfpP4xPG4zPm2xsNR6NPN/uNuq8'
 145     b'nAa3W4tGaRQrrsZFkX/FIbyO3dG7PHq/RP4d9+NIs/YHTi+OrzKYcS2OZtNpvIqbcQfuyqFv'
 146     b'pH80e45hP26JM1fhYtyDHzGvUGuttZSyB3/hWdyAn3P4KXwYg+dywAfx9mTmf8JH+A5P45Ls'
 147     b'Ox/XUkpJzM/kJofxJ17JjR7GTfgGX2EfHsHZ3PRM5OsynvvEtTCrGR+Ih3fmJvsTgmsSEtE5'
 148     b'lb5N7g7qVgO0LIk/FM9r5N14uUPM3TiY/bVbK5hbDPdEaudq8/VQnBu4Lzo7XJiWJA8Y2reK'
 149     b'5/F4jN6L9/EaHsQFsVewHZtWSfLUeTBqBbfitxi6HTfiisjn1pTPA2daNZ7PDengh8R9b9Cz'
 150     b'L0g6G73bcGUguSv7/1UypkFYlnA9iZfwPR4K+V7EM/H2cIh2JGG7sCszFXW1oZDNeA57Ujre'
 151     b'CkSfSClYpYRcH3Ie7EJU2ySPilnBCXwW4rwdKH4axlYcDwe+xnsbSvlO1vt6Lsa/TGH7OP0X'
 152     b'+Dy6J/BtdD5pbK1BfjVg8aL4QlMIj2btscaBN5s9D3RkW4hWpwaztXmAapfwTdAdEbOF/BoP'
 153     b'JlyamD/VvU6tV/OgrCxzu3BZq7NqFE/iXdzXkW5UOspAZznonaVUY6t9zeRdKMu4YeVUa507'
 154     b'lg51lrXlPS+dh6MwTJibw6dBSdn4J1K6R39ofPDH8L9/c/4GBa36v+mJzSMAAAAASUVORK5C'
 155     b'YII=')
 156 
 157 mdpEVT = wx.NewEventType()
 158 EVT_DATE_CHANGED = wx.PyEventBinder(mdpEVT, 1)
 159 
 160 
 161 class mdpEvent(wx.PyCommandEvent):
 162     def __init__(self, eventType, eventId=1, date=None, value=''):
 163         """
 164         Default class constructor.
 165 
 166         :param `eventType`: the event type;
 167         :param `eventId`: the event identifier.
 168         """
 169         wx.PyCommandEvent.__init__(self, eventType, eventId)
 170         self._eventType = eventType
 171         self.date = date
 172         self.value = value
 173 
 174     def GetDate(self):
 175         """
 176         Retrieve the date value of the control at the time
 177         this event was generated, Returning a wx.DateTime object"""
 178         return self.date
 179 
 180     def GetValue(self):
 181         """
 182         Retrieve the formatted date value of the control at the time
 183         this event was generated, Returning a string"""
 184         return self.value.title()
 185 
 186     def GetDateTime(self):
 187         """
 188         Retrieve the date value of the control at the time
 189         this event was generated, Returning a python datetime object"""
 190         return wx.wxdate2pydate(self.date)
 191 
 192     def GetTimeStamp(self):
 193         """
 194         Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
 195         Returning a integer
 196         """
 197         return int(self.date.GetValue()/1000)
 198 
 199 class MiniDatePicker(wx.Control):
 200     def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
 201                  style=wx.BORDER_SIMPLE, name="MiniDatePicker", date=0, formatter=''):
 202 
 203         wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
 204         self.parent = parent
 205         self._date = date
 206         if formatter:
 207             format = formatter
 208         else:
 209             format = lambda dt: dt.FormatISODate()
 210         font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
 211         self.SetWindowStyle(wx.BORDER_NONE)
 212         self._style = style
 213         self._calendar_style = wx.adv.CAL_MONDAY_FIRST
 214         self._calendar_headercolours = None
 215         self._calendar_highlightcolours = None
 216         self._calendar_Bg = None
 217         self._calendar_Fg = None
 218         if size == wx.DefaultSize:
 219             dc = wx.ScreenDC()
 220             dc.SetFont(font)
 221             trialdate = format(wx.DateTime(28,9,2022)) # a Wednesday in September = longest names in English
 222             w, h = dc.GetTextExtent(trialdate)
 223             size = (w+64, -1) # Add image width (24) plus a buffer
 224             del dc
 225         self._veto = False
 226         #try:
 227         #    locale.setlocale(locale.LC_TIME, ".".join(locale.getlocale()))
 228         #except Exception as e:
 229         #    pass
 230         txtstyle = wx.TE_READONLY
 231 
 232         if style & wx.TE_LEFT or style == wx.TE_LEFT:
 233             txtstyle = txtstyle | wx.TE_LEFT
 234         elif style & wx.TE_RIGHT:
 235             txtstyle = txtstyle | wx.TE_RIGHT
 236         else:
 237             txtstyle = txtstyle | wx.TE_CENTRE
 238         if style & wx.TE_READONLY:
 239             txtstyle = txtstyle | wx.TE_READONLY
 240         if style & wx.BORDER_NONE:
 241             txtstyle = txtstyle | wx.BORDER_NONE
 242 
 243         # MiniDatePicker
 244 
 245         self.ctl = wx.TextCtrl(self, id, value=str(self._date),
 246                                pos=pos, size=size, style=txtstyle, name=name)
 247         self.button = wx.BitmapButton(self, -1, bitmap=img.Bitmap)
 248         self.MinSize = self.GetBestSize()
 249         # End
 250 
 251         # Bind the events
 252         self._formatter = format
 253         self.button.Bind(wx.EVT_BUTTON, self.OnCalendar)
 254         self.ctl.Bind(wx.EVT_LEFT_DOWN, self.OnCalendar)
 255         self.SetValue(date)
 256 
 257         sizer = wx.BoxSizer(wx.HORIZONTAL)
 258         sizer.Add(self.ctl, 1, wx.EXPAND, 0)
 259         sizer.Add(self.button, 0, wx.ALIGN_CENTER_VERTICAL, 0)
 260         self.SetSizerAndFit(sizer)
 261         self.Show()
 262 
 263     def OnCalendar(self, _event=None):
 264         window = CalendarPopup(
 265             self, self._date, self.OnDate, self.GetTopLevelParent(), wx.SIMPLE_BORDER)
 266         pos = self.ClientToScreen((0, 0))
 267         size = self.GetSize()
 268         window.Position(pos, (0, size.height))
 269 
 270     def SetFormatter(self, formatter):
 271         '''formatter will be called with a wx.DateTime'''
 272         self._formatter = formatter
 273         self.OnDate(self._date)
 274 
 275     def SetLocale(self, alias):
 276         try:
 277             locale.setlocale(locale.LC_TIME, locale=alias)
 278         except Exception as e:
 279             locale.setlocale(locale.LC_TIME, locale='')
 280         self.SetValue(self._date)
 281 
 282     def SetCalendarStyle(self, style=0):
 283         self._calendar_style = style
 284 
 285     def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
 286         self._calendar_headercolours = colFg, colBg
 287 
 288     def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
 289         self._calendar_highlightcolours = colFg, colBg
 290 
 291     def SetCalendarFg(self, colFg=wx.NullColour):
 292         self._calendar_Fg = colFg
 293 
 294     def SetCalendarBg(self, colBg=wx.NullColour):
 295         self._calendar_Bg = colBg
 296 
 297     def SetButton(self, button=True):
 298         if button:
 299             self.button.Show()
 300         else:
 301             self.button.Hide()
 302         self.Layout()
 303 
 304     def OnDate(self, date):
 305         self._date = date
 306         self.ctl.SetValue(self._formatter(date).title())
 307         self.MinSize = self.GetBestSize()
 308         if self._veto:
 309             self._veto = False
 310             return
 311         event = mdpEvent(mdpEVT, self.GetId(), date=date, value=self._formatter(date))
 312         event.SetEventObject(self)
 313         self.GetEventHandler().ProcessEvent(event)
 314 
 315     def GetValue(self):
 316         return self.ctl.GetValue()
 317 
 318     def GetDate(self):
 319         return self._date
 320 
 321     def GetDateTimeValue(self):
 322         """
 323         Return a python datetime object"""
 324         return wx.wxdate2pydate(self._date)
 325 
 326     def GetTimeStamp(self):
 327         """
 328         Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
 329         Returning a integer
 330         """
 331         return int(self._date.GetValue()/1000)
 332 
 333     def GetLocale(self):
 334         return locale.getlocale(category=locale.LC_TIME)
 335 
 336     def SetValue(self, date):
 337         if isinstance(date, wx.DateTime):
 338             pass
 339         elif isinstance(date, datetime.date):
 340             date = wx.pydate2wxdate(date)
 341         elif isinstance(date, int) and date > 0:
 342             date = wx.DateTime.FromTimeT(date)
 343         elif isinstance(date, float) and date > 0:
 344             date = wx.DateTime.FromTimeT(int(date))
 345         else:  # Invalid date value default to today's date
 346             date = wx.DateTime.Today()
 347         self._date = date.ResetTime()
 348         self._veto = True
 349         self.SetFormatter(self._formatter)
 350 
 351 
 352 class CalendarPopup(wx.PopupTransientWindow):
 353     def __init__(self, parent, date, callback, *args, **kwargs):
 354         '''date is the initial date; callback is called with the chosen date'''
 355         super().__init__(*args, **kwargs)
 356         self.callback = callback
 357         self.calendar = wx.adv.GenericCalendarCtrl(self, pos=(5, 5), style=parent._calendar_style)
 358         self.calendar.SetDate(date)
 359         if parent._calendar_headercolours:
 360             self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
 361         if parent._calendar_highlightcolours:
 362             self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
 363         if parent._calendar_Bg:
 364             self.calendar.SetBackgroundColour(parent._calendar_Bg)
 365             self.SetBackgroundColour(parent._calendar_Bg)
 366         if parent._calendar_Fg:
 367             self.calendar.SetForegroundColour(parent._calendar_Fg)
 368         sizer = wx.BoxSizer(wx.VERTICAL)
 369         sizer.Add(self.calendar, 1, wx.ALL | wx.EXPAND)
 370         self.SetSizerAndFit(sizer)
 371         self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnChosen)
 372         self.calendar.SetToolTip("Arrow keys and PageUp/pageDn\nAdjust the Calendar")
 373         self.Popup()
 374 
 375     def OnChosen(self, _event=None):
 376         self.callback(self.calendar.GetDate())
 377         self.Dismiss()
 378 
 379 
 380 class DemoFrame(wx.Frame):
 381     def __init__(self, parent):
 382         wx.Frame.__init__(self, parent, -1, "MiniDatePicker Demo")
 383 
 384         #format = (lambda dt:
 385         #    (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
 386         #    )
 387 
 388         #format = (lambda dt: (f'{dt.Format("%a %d-%m-%Y")}'))
 389         format = (lambda dt: (f'{dt.Format("%A %d %B %Y")}'))
 390 
 391         panel = wx.Panel(self)
 392 
 393         self.mdp = MiniDatePicker(panel, -1, pos=(50, 50), style=wx.TE_CENTRE, date=0, formatter=format)
 394         self.mdp.SetLocale('fr_FR.UTF-8')
 395         #self.mdp.SetFormatter(format)
 396         x=datetime.datetime.now()
 397         self.mdp.SetValue(x.timestamp())
 398         #self.mdp.SetValue(0)
 399         #self.mdp.SetButton(False)
 400         self.mdp.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST)
 401         self.mdp.button.SetBackgroundColour('white')
 402         self.mdp.ctl.SetBackgroundColour('lightgreen')
 403         self.mdp.SetCalendarHeaders(colFg='red', colBg='lightgreen')
 404         self.mdp.SetCalendarHighlights(colFg='red', colBg='lightgreen')
 405         self.mdp.SetCalendarBg(colBg='azure')
 406         self.Bind(EVT_DATE_CHANGED, self.OnEvent)
 407 
 408     def OnEvent(self, event):
 409         print("\nevt", event.GetValue())
 410         print("evt", event.GetDate())
 411         print("evt", event.GetDateTime())
 412         print("evt", event.GetTimeStamp())
 413         print("func", self.mdp.GetValue())
 414         print("func", self.mdp.GetDate())
 415         print("func", self.mdp.GetDateTimeValue())
 416         print("func", self.mdp.GetTimeStamp())
 417         print("func", self.mdp.GetLocale())
 418 
 419 
 420 if __name__ == '__main__':
 421     app = wx.App()
 422     frame = DemoFrame(None)
 423     frame.Show()
 424     app.MainLoop()


Download source

source.zip


Additional Information

Link :

https://discuss.wxpython.org/t/a-wx-datepickerctrl-with-a-customisable-format/36295

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/


Thanks to

J Healey (MiniDatePicker.py coding), the wxPython community...


About this page

Date(d/m/y) Person (bot) Comments :

16/01/23 - Ecco (Created page for wxPython Phoenix).


Comments

- blah, blah, blah...

How to create a wx.DatePickerCtrl with a customisable format (Phoenix) (last edited 2023-01-16 12:54:05 by Ecco)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.