A Date and Time Picker

Inspired by the need to set a deadline, where not only were a date and time required but also
the ability to return a timestamp, which is easy to store in a database, easily sorted and
easily converted back into a date.
I’ve endeavoured to make this as friendly as possible, allowing for the retrieving of a wx.DateTime, a datetime, a string or a timestamp.
As usual let me know if there are errors or omissions.

"""
    MiniDateTime.py

    A custom class that allows selection of both date and time, with the ability to customise
    the calendar and the output format.

    Inspired by the need to set deadlines, where not only were a date and time required but also
    the ability to return a timestamp, which is easy to store in a database, easily sorted and
    easily converted back into a date. (Thus GetTimeStamp())

    Works with wx.DateTime or python datetime values
    With or without an activating button
    Uses wx.adv.GenericCalendarCtrl and wx.adv.TimePickerCtrl
    Uses locale to enable different languages for the calendar

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

        @param parent:   Parent window. Must not be None.
        @param id:       identifier. A value of -1 indicates a default value.
        @param pos:      MiniDateTime position. If the position (-1, -1) is specified
                         then a default position is chosen.
        @param size:     If the default size (-1, -1), is specified then a default size is calculated.
                         Size should be able to accomodate the specified formatter string + button
        @param style:    Alignment (Left,Middle,Right).
        @param name:     Widget name.
        @param date:     Initial date - invalid date = now
        @param formatter A date formatting string in the form of a lambda function
                         default lambda dt: dt.FormatISOCombined(sep=b' ')
                          = ISO 8601 format "YYYY-MM-DD HH:MM:SS".
                         or a lambda function with a format string
                         e.g.:
                            format = lambda dt: (f'{dt.Format("%a %d-%m-%Y %H:%M:%S")}')
                            format = lambda dt: (f'{dt.Format("%A %d %B %Y %H:%M:%S")}')
                            format = lambda dt: (f'{dt.Format("%a %d %B %Y %I:%M %p")}')
                         or
                            fmt = "%Y/%m/%d %H:%M:%S"
                            format = lambda dt: (dt.Format(fmt))
                            format = lambda dt: (dt.Format("%Y/%m/%d %H:%M:%S"))
                            format = lambda dt: (dt.FormatISOCombined(sep=b' '))

    TextCtrl Styles: wx.TE_READONLY (Default)
            wx.TE_RIGHT
            wx.TE_LEFT
            wx.TE_CENTRE

            wx.BORDER_NONE is always applied to the internal textctrl
            wx.BORDER_SIMPLE is the default border for the control itself

    Events: EVT_DATE_CHANGED A date change occurred in the control

    Event Functions:
        GetValue()          Returns formatted date in the event as a string

        GetDate()           Returns wxDateTime date in the event

        GetDateTime()       Returns python datetime of date in the event

        GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date

    Functions:
        GetValue()          Returns wxDateTime date in the event as a string

        GetDate()           Returns wxDateTime date in the control

        GetDateTimeValue()  Returns python datetime of date in the control

        GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected datetime

        GetLocale()         Returns tuple of current language code and encoding

        SetValue(date)      Sets the date in the control
                            expects a wx.DateTime, a python datetime datetime or a datetime timestamp
                            Any invalid date defaults to wx.DateTime.Now()
                            Milliseconds are stripped off

        SetFormatter(formatter) Date format in the form of a lambda
            default:    lambda dt: dt.FormatISOCombined(sep=b' ') (see above)

        SetButton(Boolean)  Shows or Hides Ctrl Button

        SetLocale(locale)   Set the locale for Calendar day and month names
                             e.g. 'de_DE.UTF-8' German
                                  'es_ES.UTF-8' Spanish
                             depends on the locale being available on the machine

        SetCalendarStyle(style)
            wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week (not in wxGTK)
            wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week (not in wxGTK)
            wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar (only generic)
            wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
            wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
            wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
            wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
            wx.adv.CAL_SHOW_WEEK_NUMBERS

        SetCalendarHighlights(colFg, colBg) Calendar and TimeCtrl highlight colours

        SetCalendarHeaders(colFg, colBg) Calendar Header colours

        SetCalendarFg(colFg)    Calendar ForegroundColour

        SetCalendarBg(colBg)    Calendar & Ctrl BackgroundColour

    Default Values:
        date    -       Now
        style   -       READ_ONLY

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

Usage example:

import wx
import minidatetime as MDT
class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDateTime Demo")
        format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y %H:%M:%S")}'))
        panel = wx.Panel(self)
        mdp = MDT.MiniDateTime(panel, -1, pos=(50, 50), size=(280,-1), style=0, date=0, formatter=format)
        self.Show()

app = wx.App()
frame = Frame(None)
app.MainLoop()

"""

import wx
import wx.adv
from wx.lib.embeddedimage import PyEmbeddedImage
import datetime
import locale

img = PyEmbeddedImage(
    b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAABhGlDQ1BJQ0MgcHJvZmlsZQAA'
    b'KJF9kT1Iw0AcxV9TiyJVB4uIOGSoThZERRy1CkWoEGqFVh1MLv2CJg1Jiouj4Fpw8GOx6uDi'
    b'rKuDqyAIfoA4OjkpukiJ/0sKLWI8OO7Hu3uPu3eAUC8zzeoYBzTdNlOJuJjJroqdrwgjhF4M'
    b'QJSZZcxJUhK+4+seAb7exXiW/7k/R4+asxgQEIlnmWHaxBvE05u2wXmfOMKKskp8Tjxm0gWJ'
    b'H7muePzGueCywDMjZjo1TxwhFgttrLQxK5oa8RRxVNV0yhcyHquctzhr5Spr3pO/MJzTV5a5'
    b'TnMYCSxiCRJEKKiihDJsxGjVSbGQov24j3/I9UvkUshVAiPHAirQILt+8D/43a2Vn5zwksJx'
    b'IPTiOB8jQOcu0Kg5zvex4zROgOAzcKW3/JU6MPNJeq2lRY+Avm3g4rqlKXvA5Q4w+GTIpuxK'
    b'QZpCPg+8n9E3ZYH+W6B7zeutuY/TByBNXSVvgINDYLRA2es+7+5q7+3fM83+fgBDkHKUu8H2'
    b'wwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAulJREFUOMvNVTFI80AY/VIirVh1qAoVLbWLVYeC'
    b'i/aQiFIFV4WCi4OD4CAIDgXnDioKiludHFxEKNi6KHRQkkZEFKEhDqI/wVwrhLSIBkzJ/cP9'
    b'lLZW7eDwvyEJX969e/e93IUhhMBvwAa/hAqhXC63sLDwmaTr+vT0dLFY/EaIpbf39/etrS1F'
    b'Ufb29lwuVxWpUCjE4/HV1VW/3z8/P19biRBCCLm5uQGAvr4+AOjv76/i0PrAwAAVJbXAlg9Y'
    b'Wlpqb2+/uLiQJKm8vry83Nraend3l8lkvlxbuaPR0dGNjY3PnOHh4d3dXafTWZcjhFAymbQs'
    b'KxwOq6ra2dlJrxhjt9uNMeZ5PhAI/Jway7JOp7OlpUVVVYRQNptFCGGMg8GgqqrBYPDt7e3n'
    b'1CheX191Xff5fDzP9/T08Dzv8/nS6XQgEMAYsywry7Ldbv+hRxzHpVIpABAEAQBEUQSATCZz'
    b'f39/eno6OztbMr65uakoSlWPbFUxUxfU0fX1tcfjicVik5OTg4ODPM/f3t4mEonLy8tQKETn'
    b'q8uRIAjRaNTlcsmyTAiRJCmVShFCTNOk4T4+PpYcVQhhjAVByOVygiDoui7LMtWlnLW1NY7j'
    b'6PPHx8fc3FwkEqm9NEmSEEIPDw8IoaamJlEUJyYmhoaG6FuGYUrMhoaGxcXF9fX1fD5fHb9l'
    b'Wb29vaIoer3edDrNMIwsyzMzMzbbP47D4TAMo/QRdHR0AMDLy0tF/B6P5/n5uaurq6R7dnZm'
    b't9sLhUKp4vV6r66uuru7o9Ho1NSUaZoA0NzcXNFsQkg+n/9TBk3Tjo6O/H6/YRiUYFnW09PT'
    b'wcFBW1vbzs7O8fExPTYqml0TiqI0NjaenJxU1Q3D0DSN47jt7e3q1L7C4eEhACQSiWKxWCpm'
    b's9mVlZXx8XFN0+oVMk0zFosBQDgc3t/fj8fjkUjE7XaPjY2pqlrOZOo5/BVFSSaT5+fndPeG'
    b'QqGRkRGHw1HOYf67v8hfVpeRQVPNMc8AAAAASUVORK5CYII=')

mdtEVT = wx.NewEventType()
EVT_DATETIME_SELECTED = wx.PyEventBinder(mdtEVT, 1)

class mdtEvent(wx.PyCommandEvent):
    def __init__(self, eventType, eventId=1, date=None, value=''):
        """
        Default class constructor.

        :param `eventType`: the event type;
        :param `eventId`: the event identifier.
        """
        wx.PyCommandEvent.__init__(self, eventType, eventId)
        self._eventType = eventType
        self.date = date
        self.value = value

    def GetDate(self):
        """
        Retrieve the date value of the control at the time
        this event was generated, Returning a wx.DateTime object"""
        return self.date

    def GetValue(self):
        """
        Retrieve the formatted date value of the control at the time
        this event was generated, Returning a string"""
        return self.value

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

    def GetDateTime(self):
        """
        Retrieve the date value of the control at the time
        this event was generated, Returning a python datetime object"""
        return wx.wxdate2pydate(self.date)


class MiniDateTime(wx.Control):
    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MiniDateTime", date=0, formatter=''):

        wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
        self.parent = parent
        self._date = date
        if formatter:
            format = formatter
        else:
            format = lambda dt: dt.FormatISOCombined(sep=b' ')
        font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
        #self.SetWindowStyle(wx.BORDER_NONE)
        self._style = style
        self._calendar_style = wx.adv.CAL_MONDAY_FIRST
        self._calendar_headercolours = None
        self._calendar_highlightcolours = None
        self._calendar_Bg = None
        self._calendar_Fg = None
        if size == wx.DefaultSize:
            dc = wx.ScreenDC()
            dc.SetFont(font)
            trialdate = format(wx.DateTime(28,9,2022)) # a Wednesday in September = longest names in English
            w, h = dc.GetTextExtent(trialdate)
            size = (w+54, -1) # Add image width (24) plus a buffer
            del dc
        self._veto = False

        txtstyle = wx.TE_READONLY

        if style & wx.TE_LEFT or style == wx.TE_LEFT:
            txtstyle = txtstyle | wx.TE_LEFT
        elif style & wx.TE_RIGHT:
            txtstyle = txtstyle | wx.TE_RIGHT
        else:
            txtstyle = txtstyle | wx.TE_CENTRE
        if style & wx.TE_READONLY:
            txtstyle = txtstyle | wx.TE_READONLY
        if style & wx.BORDER_NONE:
            txtstyle = txtstyle | wx.BORDER_NONE

        # MiniDateTime Picker

        self.ctl = wx.TextCtrl(self, id, value=str(self._date),
                               pos=pos, size=size, style=txtstyle, name=name)
        self.button = wx.BitmapButton(self, -1, bitmap=img.Bitmap)
        self.MinSize = self.GetBestSize()

        self._formatter = format
        self.button.Bind(wx.EVT_BUTTON, self.OnCalendar)
        self.ctl.Bind(wx.EVT_LEFT_DOWN, self.OnCalendar)
        self.SetValue(date)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.ctl, 1, wx.EXPAND, 0)
        sizer.Add(self.button, 0, wx.ALIGN_CENTER_VERTICAL, 0)
        self.SetSizerAndFit(sizer)
        self.Show()

    def OnCalendar(self, _event=None):
        window = CalendarPopup(
            self, self._date, self.OnDate, self.GetTopLevelParent(), wx.SIMPLE_BORDER)
        pos = self.ClientToScreen((0, 0))
        size = self.GetSize()
        window.Position(pos, (0, size.height))

    def SetFormatter(self, formatter):
        '''formatter will be called with a wx.DateTime'''
        self._formatter = formatter
        self.OnDate(self._date)

    def SetLocale(self, alias):
        try:
            locale.setlocale(locale.LC_TIME, locale=alias)
        except Exception as e:
            locale.setlocale(locale.LC_TIME, locale='')
        self.SetValue(self._date)

    def SetCalendarStyle(self, style=0):
        self._calendar_style = style

    def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
        self._calendar_headercolours = colFg, colBg

    def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
        self._calendar_highlightcolours = colFg, colBg

    def SetCalendarFg(self, colFg=wx.NullColour):
        self._calendar_Fg = colFg

    def SetCalendarBg(self, colBg=wx.NullColour):
        self._calendar_Bg = colBg

    def SetButton(self, button=True):
        if button:
            self.button.Show()
        else:
            self.button.Hide()
        self.Layout()

    def OnDate(self, date):
        self._date = date
        self.ctl.SetValue(self._formatter(date))
        self.MinSize = self.GetBestSize()
        if self._veto:
            self._veto = False
            return
        event = mdtEvent(mdtEVT, self.GetId(), date=date, value=self._formatter(date))
        event.SetEventObject(self)
        self.GetEventHandler().ProcessEvent(event)

    def GetValue(self):
        return self._ctl.GetValue()

    def GetDate(self):
        return self._date

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

    def GetDateTimeValue(self):
        """
        Return a python datetime object"""
        return wx.wxdate2pydate(self._date)

    def GetLocale(self):
        return locale.getlocale(category=locale.LC_TIME)

    def SetValue(self, date):
        if isinstance(date, wx.DateTime):
            pass
        elif isinstance(date, datetime.date):
            date = wx.pydate2wxdate(date)
        elif isinstance(date, int) and date > 0:
            date = wx.DateTime.FromTimeT(date)
        elif isinstance(date, float) and date > 0:
            date = wx.DateTime.FromTimeT(int(date))
        else:  # Invalid date value default to now
            date = wx.DateTime.Now()
            date.SetSecond(0)
        date.millisecond = 0
        self._date = date
        self._veto = True
        self.SetFormatter(self._formatter)


class CalendarPopup(wx.PopupTransientWindow):
    def __init__(self, parent, date, callback, *args, **kwargs):
        '''date is the initial date; callback is called with the chosen date'''
        super().__init__(*args, **kwargs)
        self.callback = callback
        self.calendar = wx.adv.GenericCalendarCtrl(self, pos=(5, 5), style=parent._calendar_style)
        self.calendar.SetDate(date)
        self.tpc = wx.adv.TimePickerCtrl(self, size=(140, -1),
                                style = wx.adv.TP_DEFAULT)
        self.tpc.SetTime(date.hour, date.minute, date.second)
        tpc_textctrl = self.tpc.GetChildren()[0]
        tpc_spinbutton = self.tpc.GetChildren()[1]
        self.select = wx.Button(self, -1, "&Select")
        self.quit = wx.Button(self, -1, "&Quit")

        if parent._calendar_headercolours:
            self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
        if parent._calendar_Bg:
            self.calendar.SetBackgroundColour(parent._calendar_Bg)
            self.tpc.SetBackgroundColour(parent._calendar_Bg)
            self.SetBackgroundColour(parent._calendar_Bg)
        if parent._calendar_highlightcolours:
            self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
            tpc_textctrl.SetForegroundColour(parent._calendar_highlightcolours[0])
            tpc_textctrl.SetBackgroundColour(parent._calendar_highlightcolours[1])
        if parent._calendar_Fg:
            self.calendar.SetForegroundColour(parent._calendar_Fg)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer1 = wx.BoxSizer(wx.VERTICAL)
        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer1.Add(self.calendar, 1, wx.ALL | wx.EXPAND, 5)
        sizer1.Add(self.tpc, 0, wx.ALL | wx.EXPAND, 5)
        sizer2.Add(self.select, 0, wx.ALL, 5)
        sizer2.Add(self.quit, 0, wx.ALL, 5)
        sizer.Add(sizer1)
        sizer.Add(sizer2)
        self.SetSizerAndFit(sizer)

        self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnDateChosen)
        self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnFocus)
        self.select.Bind(wx.EVT_BUTTON, self.OnChosen)
        self.quit.Bind(wx.EVT_BUTTON, self.OnQuit)
        self.calendar.SetToolTip("Arrow keys and PageUp/pageDn\nAdjust the Calendar")
        self.Popup()

    def OnDateChosen(self, _event=None):
        self.tpc.SetFocus()

    def OnFocus(self, _event=None):
        self.calendar.SetFocus()

    def OnChosen(self, _event=None):
        _date = self.calendar.GetDate()
        _hh, _mm, _ss = self.tpc.GetTime()
        _date.SetHour(_hh)
        _date.SetMinute(_mm)
        _date.SetSecond(_ss)
        self.callback(_date)
        self.Dismiss()

    def OnQuit(self, event):
        self.Dismiss()

class DemoFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDateTime Picker Demo")

        #format = (lambda dt:
        #    (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
        #    )

        format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y %H:%M:%S")}'))
        #format = (lambda dt: (f'{dt.Format("%A %d %B %Y %H:%M:%S")}'))

        panel = wx.Panel(self)

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

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


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


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.

"""
    MiniDateTime.py

    A custom class that allows selection of both date and time, with the ability to customise
    the calendar and the output format.

    Inspired by the need to set deadlines, where not only were a date and time required but also
    the ability to return a timestamp, which is easy to store in a database, easily sorted and
    easily converted back into a date. (Thus GetTimeStamp())

    Works with wx.DateTime or python datetime values
    With or without an activating button
    Uses wx.adv.GenericCalendarCtrl and wx.adv.TimePickerCtrl
    Uses locale to enable different languages for the calendar

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

        @param parent:   Parent window. Must not be None.
        @param id:       identifier. A value of -1 indicates a default value.
        @param pos:      MiniDateTime position. If the position (-1, -1) is specified
                         then a default position is chosen.
        @param size:     If the default size (-1, -1), is specified then a default size is calculated.
                         Size should be able to accomodate the specified formatter string + button
        @param style:    Alignment (Left,Middle,Right).
        @param name:     Widget name.
        @param date:     Initial date - invalid date = now
        @param formatter A date formatting string in the form of a lambda function
                         default lambda dt: dt.FormatISOCombined(sep=b' ')
                          = ISO 8601 format "YYYY-MM-DD HH:MM:SS".
                         or a lambda function with a format string
                         e.g.:
                            format = lambda dt: (f'{dt.Format("%a %d-%m-%Y %H:%M:%S")}')
                            format = lambda dt: (f'{dt.Format("%A %d %B %Y %H:%M:%S")}')
                            format = lambda dt: (f'{dt.Format("%a %d %B %Y %I:%M %p")}')
                         or
                            fmt = "%Y/%m/%d %H:%M:%S"
                            format = lambda dt: (dt.Format(fmt))
                            format = lambda dt: (dt.Format("%Y/%m/%d %H:%M:%S"))
                            format = lambda dt: (dt.FormatISOCombined(sep=b' '))

    TextCtrl Styles: wx.TE_READONLY (Default)
            wx.TE_RIGHT
            wx.TE_LEFT
            wx.TE_CENTRE

            wx.BORDER_NONE is always applied to the internal textctrl
            wx.BORDER_SIMPLE is the default border for the control itself

    Events: EVT_DATE_CHANGED A date change occurred in the control

    Event Functions:
        GetValue()          Returns formatted date in the event as a string

        GetDate()           Returns wxDateTime date in the event

        GetDateTime()       Returns python datetime of date in the event

        GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date

    Functions:
        GetValue()          Returns wxDateTime date in the event as a string

        GetDate()           Returns wxDateTime date in the control

        GetDateTimeValue()  Returns python datetime of date in the control

        GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected datetime

        GetLocale()         Returns tuple of current language code and encoding

        SetValue(date)      Sets the date in the control
                            expects a wx.DateTime, a python datetime datetime or a datetime timestamp
                            Any invalid date defaults to wx.DateTime.Now()
                            Milliseconds are stripped off

        SetFormatter(formatter) Date format in the form of a lambda
            default:    lambda dt: dt.FormatISOCombined(sep=b' ') (see above)

        SetButton(Boolean)  Shows or Hides Ctrl Button

        SetLocale(locale)   Set the locale for Calendar day and month names
                             e.g. 'de_DE.UTF-8' German
                                  'es_ES.UTF-8' Spanish
                             depends on the locale being available on the machine

        SetCalendarStyle(style)
            wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week (not in wxGTK)
            wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week (not in wxGTK)
            wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar (only generic)
            wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
            wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
            wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
            wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
            wx.adv.CAL_SHOW_WEEK_NUMBERS

        SetCalendarHighlights(colFg, colBg) Calendar and TimeCtrl highlight colours

        SetCalendarHeaders(colFg, colBg) Calendar Header colours

        SetCalendarFg(colFg)    Calendar ForegroundColour

        SetCalendarBg(colBg)    Calendar & Ctrl BackgroundColour

    Default Values:
        date    -       Now
        style   -       READ_ONLY

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

Usage example:

import wx
import minidatetime as MDT
class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDateTime Demo")
        format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y %H:%M:%S")}'))
        panel = wx.Panel(self)
        mdp = MDT.MiniDateTime(panel, -1, pos=(50, 50), size=(280,-1), style=0, date=0, formatter=format)
        self.Show()

app = wx.App()
frame = Frame(None)
app.MainLoop()

"""

import wx
import wx.adv
from wx.lib.embeddedimage import PyEmbeddedImage
import datetime
import locale

img = PyEmbeddedImage(
    b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAABhGlDQ1BJQ0MgcHJvZmlsZQAA'
    b'KJF9kT1Iw0AcxV9TiyJVB4uIOGSoThZERRy1CkWoEGqFVh1MLv2CJg1Jiouj4Fpw8GOx6uDi'
    b'rKuDqyAIfoA4OjkpukiJ/0sKLWI8OO7Hu3uPu3eAUC8zzeoYBzTdNlOJuJjJroqdrwgjhF4M'
    b'QJSZZcxJUhK+4+seAb7exXiW/7k/R4+asxgQEIlnmWHaxBvE05u2wXmfOMKKskp8Tjxm0gWJ'
    b'H7muePzGueCywDMjZjo1TxwhFgttrLQxK5oa8RRxVNV0yhcyHquctzhr5Spr3pO/MJzTV5a5'
    b'TnMYCSxiCRJEKKiihDJsxGjVSbGQov24j3/I9UvkUshVAiPHAirQILt+8D/43a2Vn5zwksJx'
    b'IPTiOB8jQOcu0Kg5zvex4zROgOAzcKW3/JU6MPNJeq2lRY+Avm3g4rqlKXvA5Q4w+GTIpuxK'
    b'QZpCPg+8n9E3ZYH+W6B7zeutuY/TByBNXSVvgINDYLRA2es+7+5q7+3fM83+fgBDkHKUu8H2'
    b'wwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAulJREFUOMvNVTFI80AY/VIirVh1qAoVLbWLVYeC'
    b'i/aQiFIFV4WCi4OD4CAIDgXnDioKiludHFxEKNi6KHRQkkZEFKEhDqI/wVwrhLSIBkzJ/cP9'
    b'lLZW7eDwvyEJX969e/e93IUhhMBvwAa/hAqhXC63sLDwmaTr+vT0dLFY/EaIpbf39/etrS1F'
    b'Ufb29lwuVxWpUCjE4/HV1VW/3z8/P19biRBCCLm5uQGAvr4+AOjv76/i0PrAwAAVJbXAlg9Y'
    b'Wlpqb2+/uLiQJKm8vry83Nraend3l8lkvlxbuaPR0dGNjY3PnOHh4d3dXafTWZcjhFAymbQs'
    b'KxwOq6ra2dlJrxhjt9uNMeZ5PhAI/Jway7JOp7OlpUVVVYRQNptFCGGMg8GgqqrBYPDt7e3n'
    b'1CheX191Xff5fDzP9/T08Dzv8/nS6XQgEMAYsywry7Ldbv+hRxzHpVIpABAEAQBEUQSATCZz'
    b'f39/eno6OztbMr65uakoSlWPbFUxUxfU0fX1tcfjicVik5OTg4ODPM/f3t4mEonLy8tQKETn'
    b'q8uRIAjRaNTlcsmyTAiRJCmVShFCTNOk4T4+PpYcVQhhjAVByOVygiDoui7LMtWlnLW1NY7j'
    b'6PPHx8fc3FwkEqm9NEmSEEIPDw8IoaamJlEUJyYmhoaG6FuGYUrMhoaGxcXF9fX1fD5fHb9l'
    b'Wb29vaIoer3edDrNMIwsyzMzMzbbP47D4TAMo/QRdHR0AMDLy0tF/B6P5/n5uaurq6R7dnZm'
    b't9sLhUKp4vV6r66uuru7o9Ho1NSUaZoA0NzcXNFsQkg+n/9TBk3Tjo6O/H6/YRiUYFnW09PT'
    b'wcFBW1vbzs7O8fExPTYqml0TiqI0NjaenJxU1Q3D0DSN47jt7e3q1L7C4eEhACQSiWKxWCpm'
    b's9mVlZXx8XFN0+oVMk0zFosBQDgc3t/fj8fjkUjE7XaPjY2pqlrOZOo5/BVFSSaT5+fndPeG'
    b'QqGRkRGHw1HOYf67v8hfVpeRQVPNMc8AAAAASUVORK5CYII=')

mdtEVT = wx.NewEventType()
EVT_DATETIME_SELECTED = wx.PyEventBinder(mdtEVT, 1)

class mdtEvent(wx.PyCommandEvent):
    def __init__(self, eventType, eventId=1, date=None, value=''):
        """
        Default class constructor.

        :param `eventType`: the event type;
        :param `eventId`: the event identifier.
        """
        wx.PyCommandEvent.__init__(self, eventType, eventId)
        self._eventType = eventType
        self.date = date
        self.value = value

    def GetDate(self):
        """
        Retrieve the date value of the control at the time
        this event was generated, Returning a wx.DateTime object"""
        return self.date

    def GetValue(self):
        """
        Retrieve the formatted date value of the control at the time
        this event was generated, Returning a string"""
        return self.value.title()

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

    def GetDateTime(self):
        """
        Retrieve the date value of the control at the time
        this event was generated, Returning a python datetime object"""
        return wx.wxdate2pydate(self.date)


class MiniDateTime(wx.Control):
    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MiniDateTime", date=0, formatter=''):

        wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
        self.parent = parent
        self._date = date
        if formatter:
            format = formatter
        else:
            format = lambda dt: dt.FormatISOCombined(sep=b' ')
        font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
        #self.SetWindowStyle(wx.BORDER_NONE)
        self._style = style
        self._calendar_style = wx.adv.CAL_MONDAY_FIRST
        self._calendar_headercolours = None
        self._calendar_highlightcolours = None
        self._calendar_Bg = None
        self._calendar_Fg = None
        if size == wx.DefaultSize:
            dc = wx.ScreenDC()
            dc.SetFont(font)
            trialdate = format(wx.DateTime(28,9,2022)) # a Wednesday in September = longest names in English
            w, h = dc.GetTextExtent(trialdate)
            size = (w+64, -1) # Add image width (24) plus a buffer
            del dc
        self._veto = False

        txtstyle = wx.TE_READONLY

        if style & wx.TE_LEFT or style == wx.TE_LEFT:
            txtstyle = txtstyle | wx.TE_LEFT
        elif style & wx.TE_RIGHT:
            txtstyle = txtstyle | wx.TE_RIGHT
        else:
            txtstyle = txtstyle | wx.TE_CENTRE
        if style & wx.TE_READONLY:
            txtstyle = txtstyle | wx.TE_READONLY
        if style & wx.BORDER_NONE:
            txtstyle = txtstyle | wx.BORDER_NONE

        # MiniDateTime Picker

        self.ctl = wx.TextCtrl(self, id, value=str(self._date),
                               pos=pos, size=size, style=txtstyle, name=name)
        self.button = wx.BitmapButton(self, -1, bitmap=img.Bitmap)
        self.MinSize = self.GetBestSize()

        self._formatter = format
        self.button.Bind(wx.EVT_BUTTON, self.OnCalendar)
        self.ctl.Bind(wx.EVT_LEFT_DOWN, self.OnCalendar)
        self.SetValue(date)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.ctl, 1, wx.EXPAND, 0)
        sizer.Add(self.button, 0, wx.ALIGN_CENTER_VERTICAL, 0)
        self.SetSizerAndFit(sizer)
        self.Show()

    def OnCalendar(self, _event=None):
        window = CalendarPopup(
            self, self._date, self.OnDate, self.GetTopLevelParent(), wx.SIMPLE_BORDER)
        pos = self.ClientToScreen((0, 0))
        size = self.GetSize()
        window.Position(pos, (0, size.height))

    def SetFormatter(self, formatter):
        '''formatter will be called with a wx.DateTime'''
        self._formatter = formatter
        self.OnDate(self._date)

    def SetLocale(self, alias):
        try:
            locale.setlocale(locale.LC_TIME, locale=alias)
        except Exception as e:
            locale.setlocale(locale.LC_TIME, locale='')
        self.SetValue(self._date)

    def SetCalendarStyle(self, style=0):
        self._calendar_style = style

    def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
        self._calendar_headercolours = colFg, colBg

    def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
        self._calendar_highlightcolours = colFg, colBg

    def SetCalendarFg(self, colFg=wx.NullColour):
        self._calendar_Fg = colFg

    def SetCalendarBg(self, colBg=wx.NullColour):
        self._calendar_Bg = colBg

    def SetButton(self, button=True):
        if button:
            self.button.Show()
        else:
            self.button.Hide()
        self.Layout()

    def OnDate(self, date):
        self._date = date
        self.ctl.SetValue(self._formatter(date).title())
        self.MinSize = self.GetBestSize()
        if self._veto:
            self._veto = False
            return
        event = mdtEvent(mdtEVT, self.GetId(), date=date, value=self._formatter(date))
        event.SetEventObject(self)
        self.GetEventHandler().ProcessEvent(event)

    def GetValue(self):
        return self.ctl.GetValue()

    def GetDate(self):
        return self._date

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

    def GetDateTimeValue(self):
        """
        Return a python datetime object"""
        return wx.wxdate2pydate(self._date)

    def GetLocale(self):
        return locale.getlocale(category=locale.LC_TIME)

    def SetValue(self, date):
        if isinstance(date, wx.DateTime):
            pass
        elif isinstance(date, datetime.date):
            date = wx.pydate2wxdate(date)
        elif isinstance(date, int) and date > 0:
            date = wx.DateTime.FromTimeT(date)
        elif isinstance(date, float) and date > 0:
            date = wx.DateTime.FromTimeT(int(date))
        else:  # Invalid date value default to now
            date = wx.DateTime.Now()
            date.SetSecond(0)
        date.millisecond = 0
        self._date = date
        self._veto = True
        self.SetFormatter(self._formatter)


class CalendarPopup(wx.PopupTransientWindow):
    def __init__(self, parent, date, callback, *args, **kwargs):
        '''date is the initial date; callback is called with the chosen date'''
        super().__init__(*args, **kwargs)
        self.callback = callback
        self.calendar = wx.adv.GenericCalendarCtrl(self, pos=(5, 5), style=parent._calendar_style)
        self.calendar.SetDate(date)
        self.tpc = wx.adv.TimePickerCtrl(self, size=(140, -1),
                                style = wx.adv.TP_DEFAULT)
        self.tpc.SetTime(date.hour, date.minute, date.second)
        # The object returned by GetChildren is not an actual Python list, but rather a C++ object that mimics
        # some of the behavior of a list. (Robin Dunn)
        # https://stackoverflow.com/questions/37996119/slicing-a-wxpython-windowlist
        wins = list(self.tpc.GetChildren())
        try:
            tpc_textctrl = wins[0]
        except Exception:
            tpc_textctrl = self.tpc
        self.select = wx.Button(self, -1, "&Select")
        self.quit = wx.Button(self, -1, "&Quit")

        if parent._calendar_headercolours:
            self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
        if parent._calendar_Bg:
            self.calendar.SetBackgroundColour(parent._calendar_Bg)
            self.tpc.SetBackgroundColour(parent._calendar_Bg)
            self.SetBackgroundColour(parent._calendar_Bg)
        if parent._calendar_highlightcolours:
            self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
            tpc_textctrl.SetForegroundColour(parent._calendar_highlightcolours[0])
            tpc_textctrl.SetBackgroundColour(parent._calendar_highlightcolours[1])
        if parent._calendar_Fg:
            self.calendar.SetForegroundColour(parent._calendar_Fg)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer1 = wx.BoxSizer(wx.VERTICAL)
        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer1.Add(self.calendar, 1, wx.ALL | wx.EXPAND, 5)
        sizer1.Add(self.tpc, 0, wx.ALL | wx.EXPAND, 5)
        sizer2.Add(self.select, 0, wx.ALL, 5)
        sizer2.Add(self.quit, 0, wx.ALL, 5)
        sizer.Add(sizer1)
        sizer.Add(sizer2)
        self.SetSizerAndFit(sizer)

        self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnDateChosen)
        self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnFocus)
        self.select.Bind(wx.EVT_BUTTON, self.OnChosen)
        self.quit.Bind(wx.EVT_BUTTON, self.OnQuit)
        self.calendar.SetToolTip("Arrow keys and PageUp/pageDn\nAdjust the Calendar")
        self.Popup()

    def OnDateChosen(self, _event=None):
        self.tpc.SetFocus()

    def OnFocus(self, _event=None):
        self.calendar.SetFocus()

    def OnChosen(self, _event=None):
        _date = self.calendar.GetDate()
        _hh, _mm, _ss = self.tpc.GetTime()
        _date.SetHour(_hh)
        _date.SetMinute(_mm)
        _date.SetSecond(_ss)
        self.callback(_date)
        self.Dismiss()

    def OnQuit(self, event):
        self.Dismiss()

class DemoFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDateTime Picker Demo")

        #format = (lambda dt:
        #    (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
        #    )

        #format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y %H:%M:%S")}'))
        format = (lambda dt: (f'{dt.Format("%A %d %B %Y %H:%M:%S")}'))

        panel = wx.Panel(self)

        self.mdp = MiniDateTime(panel, -1, pos=(50, 50), style=0, date=0, formatter=format)
        #self.mdp.SetLocale('es_ES.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('lightgreen')
        self.mdp.ctl.SetBackgroundColour('lightgreen')
        self.mdp.SetCalendarHeaders(colFg='red', colBg='lightgreen')
        self.mdp.SetCalendarHighlights(colFg='red', colBg='lightblue')
        self.mdp.SetCalendarBg(colBg='azure')
        self.Bind(EVT_DATETIME_SELECTED, 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()

For those who abhor Cut & Paste, I’ve finally gained enough points to upload :slightly_smiling_face:
minidatetime.py (18.7 KB)

An update (Version 1.2) where the button is given Focus on Creation.
I’m sure how I missed this. :slight_smile:

minidatetime.py (22.9 KB)