A wx.DatePickerCtrl with a customisable format

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.
Let me know if there are errors and omissions.
Apparently I’m still not permitted to upload a file, so I’ll have to paste it!

"""
    MiniDatePicker.py

    A custom class that looks like the wx.DatePickerCtrl but with the ability to customise
    the calendar and the output format. (DatePickerCtrl is seemingly stuck with MM/DD/YYYY format)
    Works with wx.DateTime or python datetime values
    With or without an activating button
    Uses wx.adv.GenericCalendarCtrl
    Uses locale to enable different languages for the calendar

    MiniDatePicker(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MiniDatePicker", date=0, formatter=''):

        @param parent:   Parent window. Must not be None.
        @param id:       identifier. A value of -1 indicates a default value.
        @param pos:      MiniDatePicker 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 (an invalid date = today)
        @param formatter A date formatting string in the form of a lambda function
                         default lambda dt: dt.FormatISODate()
                          = ISO 8601 format "YYYY-MM-DD".
                         or a lambda function with a format string e.g.:
                            lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
                         e.g.:
                            format = lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
                            format = lambda dt: (f'{dt.Format("%A %d %B %Y")}')
                         or
                            fmt = "%Y/%m/%d"
                            format = lambda dt: (dt.Format(fmt))
                            format = lambda dt: (dt.Format("%Y/%m/%d"))
                            format = lambda dt: (dt.FormatISODate())

    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 current date

    Functions:
        GetValue()          Returns formatted 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 date

        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 timestamp
                            Any invalid date defaults to wx.DateTime.Today()

        SetFormatter(formatter) Date format in the form of a lambda
            default:    lambda dt: dt.FormatISODate()

        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)

        SetCalendarHeaders(colFg, colBg)

        SetCalendarFg(colFg)    Calendar ForegroundColour

        SetCalendarBg(colBg)    Calendar & Ctrl BackgroundColour

    Default Values:
        date    -       Today
        style   -       READ_ONLY

Author:     J Healey
Created:    04/12/2022
Copyright:  J Healey - 2022
License:    GPL 3 or any later version
Email:      <rolfofsaxony@gmx.com>

Usage example:

import wx
import minidatepicker as MDP
class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDatePicker Demo")
        format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y")}'))
        panel = wx.Panel(self)
        mdp = MDP.MiniDatePicker(panel, -1, pos=(50, 50), size=(-1,-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'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABg2lDQ1BJQ0MgcHJvZmlsZQAA'
    b'KJF9kT1Iw0AcxV9TS0UiDu0g4pChOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOL'
    b's64OroIg+AHi6OSk6CIl/i8ptIj14Lgf7+497t4BQqPCdLtnHNANx0onE1I2tyqFXyEihAhi'
    b'EBVmm3OynELX8XWPAF/v4jyr+7k/R7+WtxkQkIhnmWk5xBvE05uOyXmfOMpKikZ8Tjxm0QWJ'
    b'H7mu+vzGueixwDOjViY9TxwlloodrHYwK1k68RRxTNMNyheyPmuctzjrlRpr3ZO/UMwbK8tc'
    b'pzmMJBaxBBkSVNRQRgUO4rQapNhI036ii3/I88vkUslVBiPHAqrQoXh+8D/43a1dmJzwk8QE'
    b'EHpx3Y8RILwLNOuu+33sus0TIPgMXBltf7UBzHySXm9rsSNgYBu4uG5r6h5wuQMMPpmKpXhS'
    b'kKZQKADvZ/RNOSByC/St+b219nH6AGSoq9QNcHAIjBYpe73Lu3s7e/v3TKu/H0FDcpMpQj/v'
    b'AAAACXBIWXMAAAsTAAALEwEAmpwYAAACk0lEQVRIx43W3+unQxQH8Nc83w9tVutHNj9v5M4q'
    b'tty7oC2u2Fy58KOV9g/AlRJygQvlRrIlpbhSW7uJorCKlKQoJand7AVJYm3Z7zMu9v18zWfM'
    b'J6aeZs7MmTNnznm/zzwFE6p/WunkLczNXEnfyhO2Rza2rLfpP4xPG4zPm2xsNR6NPN/uNuq8'
    b'nAa3W4tGaRQrrsZFkX/FIbyO3dG7PHq/RP4d9+NIs/YHTi+OrzKYcS2OZtNpvIqbcQfuyqFv'
    b'pH80e45hP26JM1fhYtyDHzGvUGuttZSyB3/hWdyAn3P4KXwYg+dywAfx9mTmf8JH+A5P45Ls'
    b'Ox/XUkpJzM/kJofxJ17JjR7GTfgGX2EfHsHZ3PRM5OsynvvEtTCrGR+Ih3fmJvsTgmsSEtE5'
    b'lb5N7g7qVgO0LIk/FM9r5N14uUPM3TiY/bVbK5hbDPdEaudq8/VQnBu4Lzo7XJiWJA8Y2reK'
    b'5/F4jN6L9/EaHsQFsVewHZtWSfLUeTBqBbfitxi6HTfiisjn1pTPA2daNZ7PDengh8R9b9Cz'
    b'L0g6G73bcGUguSv7/1UypkFYlnA9iZfwPR4K+V7EM/H2cIh2JGG7sCszFXW1oZDNeA57Ujre'
    b'CkSfSClYpYRcH3Ie7EJU2ySPilnBCXwW4rwdKH4axlYcDwe+xnsbSvlO1vt6Lsa/TGH7OP0X'
    b'+Dy6J/BtdD5pbK1BfjVg8aL4QlMIj2btscaBN5s9D3RkW4hWpwaztXmAapfwTdAdEbOF/BoP'
    b'JlyamD/VvU6tV/OgrCxzu3BZq7NqFE/iXdzXkW5UOspAZznonaVUY6t9zeRdKMu4YeVUa507'
    b'lg51lrXlPS+dh6MwTJibw6dBSdn4J1K6R39ofPDH8L9/c/4GBa36v+mJzSMAAAAASUVORK5C'
    b'YII=')

mdpEVT = wx.NewEventType()
EVT_DATE_CHANGED = wx.PyEventBinder(mdpEVT, 1)


class mdpEvent(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 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)

    def GetTimeStamp(self):
        """
        Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
        Returning a integer
        """
        return int(self.date.GetValue()/1000)

class MiniDatePicker(wx.Control):
    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MiniDatePicker", 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.FormatISODate()
        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+64, -1) # Add image width (24) plus a buffer
            del dc
        self._veto = False
        #try:
        #    locale.setlocale(locale.LC_TIME, ".".join(locale.getlocale()))
        #except Exception as e:
        #    pass
        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

        # MiniDatePicker

        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()
        # End

        # Bind the events
        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 = mdpEvent(mdpEVT, 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 GetDateTimeValue(self):
        """
        Return a python datetime object"""
        return wx.wxdate2pydate(self._date)

    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 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 today's date
            date = wx.DateTime.Today()
        self._date = date.ResetTime()
        print(self._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)
        if parent._calendar_headercolours:
            self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
        if parent._calendar_highlightcolours:
            self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
        if parent._calendar_Bg:
            self.calendar.SetBackgroundColour(parent._calendar_Bg)
            self.SetBackgroundColour(parent._calendar_Bg)
        if parent._calendar_Fg:
            self.calendar.SetForegroundColour(parent._calendar_Fg)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.calendar, 1, wx.ALL | wx.EXPAND)
        self.SetSizerAndFit(sizer)
        self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnChosen)
        self.calendar.SetToolTip("Arrow keys and PageUp/pageDn\nAdjust the Calendar")
        self.Popup()

    def OnChosen(self, _event=None):
        self.callback(self.calendar.GetDate())
        self.Dismiss()


class DemoFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDatePicker 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")}'))
        #format = (lambda dt: (f'{dt.Format("%A %d %B %Y")}'))

        panel = wx.Panel(self)

        mdp = MiniDatePicker(panel, -1, pos=(50, 50), style=wx.TE_CENTRE, 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('white')
        mdp.ctl.SetBackgroundColour('lightgreen')
        mdp.SetCalendarHeaders(colFg='red', colBg='lightgreen')
        mdp.SetCalendarHighlights(colFg='red', colBg='lightgreen')
        mdp.SetCalendarBg(colBg='azure')
        self.Bind(EVT_DATE_CHANGED, self.OnEvent)

    def OnEvent(self, event):
        print(event.GetValue())
        print(event.GetDate())
        print(event.GetTimeStamp())


if __name__ == '__main__':
    app = wx.App()
    frame = DemoFrame(None)
    frame.Show()
    app.MainLoop()

Thanks to Zig Zag for testing and finding an issue with this code.
A few other amendments have been made to the original post, the new code is posted as a reply because this forum has the strangest set of rules, where I the original poster, am denied access to either edit or delete the original. Bizarre!

In addition to a fix or two, the date string is now returned with day name and month name, capitalised if appropriate.

"""
    MiniDatePicker.py

    A custom class that looks like the wx.DatePickerCtrl but with the ability to customise
    the calendar and the output format. (DatePickerCtrl is seemingly stuck with MM/DD/YYYY format)
    Works with wx.DateTime or python datetime values
    With or without an activating button
    Uses wx.adv.GenericCalendarCtrl
    Uses locale to enable different languages for the calendar

    MiniDatePicker(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MiniDatePicker", date=0, formatter=''):

        @param parent:   Parent window. Must not be None.
        @param id:       identifier. A value of -1 indicates a default value.
        @param pos:      MiniDatePicker 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 (an invalid date = today)
        @param formatter A date formatting string in the form of a lambda function
                         default lambda dt: dt.FormatISODate()
                          = ISO 8601 format "YYYY-MM-DD".
                         or a lambda function with a format string e.g.:
                            lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
                         e.g.:
                            format = lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
                            format = lambda dt: (f'{dt.Format("%A %d %B %Y")}')
                         or
                            fmt = "%Y/%m/%d"
                            format = lambda dt: (dt.Format(fmt))
                            format = lambda dt: (dt.Format("%Y/%m/%d"))
                            format = lambda dt: (dt.FormatISODate())

    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 current date

    Functions:
        GetValue()          Returns formatted 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 date

        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 timestamp
                            Any invalid date defaults to wx.DateTime.Today()

        SetFormatter(formatter) Date format in the form of a lambda
            default:    lambda dt: dt.FormatISODate()

        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)

        SetCalendarHeaders(colFg, colBg)

        SetCalendarFg(colFg)    Calendar ForegroundColour

        SetCalendarBg(colBg)    Calendar & Ctrl BackgroundColour

    Default Values:
        date    -       Today
        style   -       READ_ONLY

Author:     J Healey
Created:    04/12/2022
Copyright:  J Healey - 2022
License:    GPL 3 or any later version
Email:      <rolfofsaxony@gmx.com>

Usage example:

import wx
import minidatepicker as MDP
class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDatePicker Demo")
        format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y")}'))
        panel = wx.Panel(self)
        mdp = MDP.MiniDatePicker(panel, -1, pos=(50, 50), size=(-1,-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'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABg2lDQ1BJQ0MgcHJvZmlsZQAA'
    b'KJF9kT1Iw0AcxV9TS0UiDu0g4pChOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOL'
    b's64OroIg+AHi6OSk6CIl/i8ptIj14Lgf7+497t4BQqPCdLtnHNANx0onE1I2tyqFXyEihAhi'
    b'EBVmm3OynELX8XWPAF/v4jyr+7k/R7+WtxkQkIhnmWk5xBvE05uOyXmfOMpKikZ8Tjxm0QWJ'
    b'H7mu+vzGueixwDOjViY9TxwlloodrHYwK1k68RRxTNMNyheyPmuctzjrlRpr3ZO/UMwbK8tc'
    b'pzmMJBaxBBkSVNRQRgUO4rQapNhI036ii3/I88vkUslVBiPHAqrQoXh+8D/43a1dmJzwk8QE'
    b'EHpx3Y8RILwLNOuu+33sus0TIPgMXBltf7UBzHySXm9rsSNgYBu4uG5r6h5wuQMMPpmKpXhS'
    b'kKZQKADvZ/RNOSByC/St+b219nH6AGSoq9QNcHAIjBYpe73Lu3s7e/v3TKu/H0FDcpMpQj/v'
    b'AAAACXBIWXMAAAsTAAALEwEAmpwYAAACk0lEQVRIx43W3+unQxQH8Nc83w9tVutHNj9v5M4q'
    b'tty7oC2u2Fy58KOV9g/AlRJygQvlRrIlpbhSW7uJorCKlKQoJand7AVJYm3Z7zMu9v18zWfM'
    b'J6aeZs7MmTNnznm/zzwFE6p/WunkLczNXEnfyhO2Rza2rLfpP4xPG4zPm2xsNR6NPN/uNuq8'
    b'nAa3W4tGaRQrrsZFkX/FIbyO3dG7PHq/RP4d9+NIs/YHTi+OrzKYcS2OZtNpvIqbcQfuyqFv'
    b'pH80e45hP26JM1fhYtyDHzGvUGuttZSyB3/hWdyAn3P4KXwYg+dywAfx9mTmf8JH+A5P45Ls'
    b'Ox/XUkpJzM/kJofxJ17JjR7GTfgGX2EfHsHZ3PRM5OsynvvEtTCrGR+Ih3fmJvsTgmsSEtE5'
    b'lb5N7g7qVgO0LIk/FM9r5N14uUPM3TiY/bVbK5hbDPdEaudq8/VQnBu4Lzo7XJiWJA8Y2reK'
    b'5/F4jN6L9/EaHsQFsVewHZtWSfLUeTBqBbfitxi6HTfiisjn1pTPA2daNZ7PDengh8R9b9Cz'
    b'L0g6G73bcGUguSv7/1UypkFYlnA9iZfwPR4K+V7EM/H2cIh2JGG7sCszFXW1oZDNeA57Ujre'
    b'CkSfSClYpYRcH3Ie7EJU2ySPilnBCXwW4rwdKH4axlYcDwe+xnsbSvlO1vt6Lsa/TGH7OP0X'
    b'+Dy6J/BtdD5pbK1BfjVg8aL4QlMIj2btscaBN5s9D3RkW4hWpwaztXmAapfwTdAdEbOF/BoP'
    b'JlyamD/VvU6tV/OgrCxzu3BZq7NqFE/iXdzXkW5UOspAZznonaVUY6t9zeRdKMu4YeVUa507'
    b'lg51lrXlPS+dh6MwTJibw6dBSdn4J1K6R39ofPDH8L9/c/4GBa36v+mJzSMAAAAASUVORK5C'
    b'YII=')

mdpEVT = wx.NewEventType()
EVT_DATE_CHANGED = wx.PyEventBinder(mdpEVT, 1)


class mdpEvent(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.title()

    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)

    def GetTimeStamp(self):
        """
        Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
        Returning a integer
        """
        return int(self.date.GetValue()/1000)

class MiniDatePicker(wx.Control):
    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MiniDatePicker", 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.FormatISODate()
        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+64, -1) # Add image width (24) plus a buffer
            del dc
        self._veto = False
        #try:
        #    locale.setlocale(locale.LC_TIME, ".".join(locale.getlocale()))
        #except Exception as e:
        #    pass
        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

        # MiniDatePicker

        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()
        # End

        # Bind the events
        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).title())
        self.MinSize = self.GetBestSize()
        if self._veto:
            self._veto = False
            return
        event = mdpEvent(mdpEVT, 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 GetDateTimeValue(self):
        """
        Return a python datetime object"""
        return wx.wxdate2pydate(self._date)

    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 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 today's date
            date = wx.DateTime.Today()
        self._date = date.ResetTime()
        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)
        if parent._calendar_headercolours:
            self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
        if parent._calendar_highlightcolours:
            self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
        if parent._calendar_Bg:
            self.calendar.SetBackgroundColour(parent._calendar_Bg)
            self.SetBackgroundColour(parent._calendar_Bg)
        if parent._calendar_Fg:
            self.calendar.SetForegroundColour(parent._calendar_Fg)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.calendar, 1, wx.ALL | wx.EXPAND)
        self.SetSizerAndFit(sizer)
        self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnChosen)
        self.calendar.SetToolTip("Arrow keys and PageUp/pageDn\nAdjust the Calendar")
        self.Popup()

    def OnChosen(self, _event=None):
        self.callback(self.calendar.GetDate())
        self.Dismiss()


class DemoFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDatePicker 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")}'))
        format = (lambda dt: (f'{dt.Format("%A %d %B %Y")}'))

        panel = wx.Panel(self)

        self.mdp = MiniDatePicker(panel, -1, pos=(50, 50), style=wx.TE_CENTRE, date=0, formatter=format)
        #self.mdp.SetLocale('fr_FR.UTF-8')
        #self.mdp.SetFormatter(format)
        x=datetime.datetime.now()
        self.mdp.SetValue(x.timestamp())
        #self.mdp.SetValue(0)
        #self.mdp.SetButton(False)
        self.mdp.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST)
        self.mdp.button.SetBackgroundColour('white')
        self.mdp.ctl.SetBackgroundColour('lightgreen')
        self.mdp.SetCalendarHeaders(colFg='red', colBg='lightgreen')
        self.mdp.SetCalendarHighlights(colFg='red', colBg='lightgreen')
        self.mdp.SetCalendarBg(colBg='azure')
        self.Bind(EVT_DATE_CHANGED, self.OnEvent)

    def OnEvent(self, event):
        print("\nevt", event.GetValue())
        print("evt", event.GetDate())
        print("evt", event.GetDateTime())
        print("evt", event.GetTimeStamp())
        print("func", self.mdp.GetValue())
        print("func", self.mdp.GetDate())
        print("func", self.mdp.GetDateTimeValue())
        print("func", self.mdp.GetTimeStamp())
        print("func", self.mdp.GetLocale())


if __name__ == '__main__':
    app = wx.App()
    frame = DemoFrame(None)
    frame.Show()
    app.MainLoop()
1 Like

well, that looks like an iconic piece of art from days gone by!
what about something more context driven: entering number + separator gives pop up with suggestions including branch to legacy calendar (there will always be people around who can’t do without)
could be used world-wide and beyond :crossed_fingers:

:slight_smile: Call me old fashioned but given the current calendar layout seems to have worked for more than a few centuries, I thought I’d stick with it.
However map out out your less staid concept and I’ll have a look at it.

For those who abhor Cut & Paste, I’ve finally gained enough points to upload :slight_smile:

minidatepicker.py (16.6 KB)

Here’s minidatepicker Version 1.1

Changelog:
1.1    subclass changes from wx.Control to wx.Panel to handle tab traversal
        The image will indicate Focus
        All demonstration colours set to Hex

minidatepicker.py (18.9 KB) minidatepickerbutton.py (18.6 KB)

Thanks, I have been wondering why there was not such a control in standard widget library.

An update to Version 1.2 where the button is given Focus on creation.
Something obvious, which I missed :slight_smile:

minidatepicker.py (19.5 KB)

1 Like