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

New versions of:
minidatepicker.py
minidatepickerbutton.py

Changelog:
New function SetButtonBitmap()
Allows a specified image to be used for the button
(also allows bitmap from wx.ArtProvider to be used)

New function SetButtonBitmapFocus()
Allow a specified image to be used for the button focus

minidatepicker.py (20.9 KB) minidatepickerbutton.py (20.2 KB)

Submit bugs, comments and insults on a postcard please :wink:

1 Like

Hi,

I think there is a bug in minidatepickerbutton. The month drop down list doesn’t show up when you click on the month.

calend

Hi Steve,
can you state your operating system and version of wx.Python.
Also, are you running minidatepickerbutton.py direct or calling it in your own code?
If your own code, can you create a mini version of it, to demonstrate the problem.
The issue doesn’t show up under Linux wx.Python 4.2.1 gtk3 and to be fair, once you are at that position in the code, it will be in wx.adv.GenericCalendarCtrl

Hi,

I use Windows 10, python 3.11.4, wxpython 4.2.1.

Whether I run minidatepickerbutton.py directly or I call it in my own code, it behaves the same, the months drop down list doesn’t show up.

I really do think, this is an issue in wx.adv.GenericCalendarCtrl, of course I could be horribly wrong.
Could you lift the 2nd piece of code written by RichardT at:

and tell me if that works, just to test it using code written by some one else. (Cheers Richard)
I don’t have access to MS Windows, so I can’t replicate your problem.

The code is identical, I think, to minidatepicker.py which also calls wx.adv.GenericCalendarCtrl in a wx.PopupTransientWindow, so I would expect that to fail as well, if there’s a bug.

For the record I code on Linux Mint 20.3 - Python 3.8.10 - wxPython 4.2.1 gtk3 (phoenix) wxWidgets 3.2.2.1

Regards,
Rolf

Hi,
I will try it, but let me tell you, the wx.adv.GenericCalendarCtrl works correctly in wxdemo in my system.

Hello Rolf,
Excellent bit of code. I am also experiencing the same issue as steve2, unfortunately. Any insight?

Good evening Rushin,

As I explained, I don’t have access to the Windows operating system and currently. I’m convinced it’s an issue with wx.adv.GenericCalendarCtrl or how I’m handling it, which doesn’t reveal itself on Linux.
My reasoning: that month selection process is within wx.adv.GenericCalendarCtrl not my code.

So I’m rather hoping that someone who does run Windows can get to the bottom of it or at least, report back if the 2 pieces of code I mentioned above, also fail or work without a glitch.

That would at least allow me to go through the code looking for any form of difference in the way I utilise wx.adv.GenericCalendarCtrl, within minidatepickerbutton.py

1 Like

Hi Rolf,

wx.adv.GenericCalendarCtrl works fine in wxpython demo (Windows 10, python 3.11.4, wxpython 4.2.1). Therefore I don’t think that wx.adv.GenericCalendarCtrl is buggy.

Morning Steve,
That isn’t really what I proposed, as I’d expect the demo to function, but it is what it is.

Try changing the method of month/year selection in the DemoFrame class to:

self.mdb.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION)

See if it behaves itself.

Screenshot at 2023-08-10 10-38-17

Rolf, this seems to work great in the demo. I’m going to implement it into my code and see if it cooperates.

Thanks for coming back to me Rushin.
I have dug into this issue with the month choice, in the choice drop down being blank, as reported by both yourself and Steve2 and I remain convinced it’s a glitch in the wxWidget itself.

The code https://github.com/wxWidgets/wxWidgets/blob/3ae3a4e1f519741132c51e5aa8f83d6f05174501/src/generic/calctrlg.cpp builds a wx.Choice of the month names and I see no way that I can influence that.

void wxGenericCalendarCtrl::CreateMonthChoice()
{
    m_choiceMonth = new wxChoice(GetParent(), wxID_ANY,
                                  wxDefaultPosition,
                                  wxDefaultSize,
                                  0, nullptr);

    wxDateTime::Month m;
    for ( m = wxDateTime::Jan; m < wxDateTime::Inv_Month; wxNextMonth(m) )
    {
        m_choiceMonth->Append(wxDateTime::GetMonthName(m, wxDateTime::NameForm().Full().Standalone()));
    }

    m_choiceMonth->SetSelection(GetDate().GetMonth());
    m_choiceMonth->SetSize(wxDefaultCoord,
                          wxDefaultCoord,
                          wxDefaultCoord,
                          wxDefaultCoord,
                          wxSIZE_AUTO_WIDTH|wxSIZE_AUTO_HEIGHT);

    m_choiceMonth->Bind(wxEVT_CHOICE, &wxGenericCalendarCtrl::OnMonthChange, this);
}

In the above for ... loop my understanding is that Jan is 0 and Inv_Month is 12 (as defined in wx.DateTime), so I fail to see how that can go wrong.

So quirks aside, on Windows OS, it seems that for the moment, running with the style wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION is the best option.