A wx.DatePickerCtrl with a customisable format - Part 2 (Phoenix)
Keywords : DatePickerCtrl.
Demonstrating :
Tested py3.x, wx4.x and Linux/Win10.
Are you ready to use some samples ?
Test, modify, correct, complete, improve and share your discoveries !
Introduction :
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.
Sample one
Last version here : https://discuss.wxpython.org/t/a-wx-datepickerctrl-with-a-customisable-format/36295
1 """
2 MiniDatePickerButton.py
3
4 A custom class that looks like the wx.DatePickerCtrl but with the ability to customise
5 the calendar and the output format. (DatePickerCtrl is seemingly stuck with MM/DD/YYYY format)
6 Works with wx.DateTime or python datetime values
7 With or without a fixed calendar image
8 If the image is included, it goes on the opposite side of the text style. wx.LEFT image on the right.
9 (If the text_style is wx.TE_CENTRE it defaults left)
10 Uses wx.adv.GenericCalendarCtrl
11 Uses locale to enable different languages for the calendar
12
13 An attempt has been made to allow days marked with attributes denoting Holiday, Marked, Restricted to live with each other
14
15 Dates can be marked, restricted, defined as holidays and have ToolTip notes
16 Marked and Restricted dates can be defined as a simple date or use more advanced rules for example the 3rd Friday of the month
17 or every Tuesday of the month. Note: they can be year specific or every year
18
19 Marked dates are marked with a Border, either Square or Oval
20 Holidays are normally highlighted with a different Foreground/Background colour
21 Restricted dates are marked using an Italic StrikeThrough font
22
23 Defined Holidays can be year specific or every year on that Month/Day
24
25 Official Holidays rely on the python 'holidays' package being available (pip install --upgrade holidays)
26 official holidays are automatically entered into the Notes for you
27 You may add in more than one region's official holidays and they will be denoted by the country code
28 and region code if appropriate.
29
30 Notes are date specific or every year and can follow the rules for Marked and Restricted dates, namely:
31 All of a specified week day in a month;
32 The specified occurrence of a weekday in a month e.g. 3rd Tuesday or last Friday of the month
33 The last day of the month
34 The last weekday of the month
35
36 Navigation:
37 The Escape key will exit the Calendar
38 The Arrow keys will navigate the calendar
39 The PageUp/PageDown keys will retreat and advance and the month respectively, as will MouseScrollUp and MouseScrollDown
40 The Home and End keys jump to the First and Last day of the month, respectively.
41 A right click on the calendar on a blank day, will display All the notes for the month.
42 Date ToolTips will be displayed as and when appropriate, depending of the position in the calendar and settings
43
44 MiniDatePicker(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
45 style=wx.BORDER_SIMPLE, name="MiniDatePicker", date=0, formatter='', image=True):
46
47 @param parent: Parent window. Must not be None.
48 @param id: identifier. A value of -1 indicates a default value.
49 @param pos: MiniDatePicker position. If the position (-1, -1) is specified
50 then a default position is chosen.
51 @param size: If the default size (-1, -1) is specified then a default size is calculated.
52 Size should be able to accomodate the specified formatter string + image
53 @param style: Alignment (Left,Middle,Right).
54 @param name: Widget name.
55 @param date: Initial date (an invalid date = today)
56 @param formatter A date formatting string in the form of a lambda function
57 The formatter will be called with a wx.DateTime thus we can use .Format()
58 the wxPython version of the standard ANSI C strftime
59 default lambda dt: dt.FormatISODate()
60 = ISO 8601 format "YYYY-MM-DD".
61 or a lambda function with a format string e.g.:
62 lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
63 e.g.:
64 format = lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
65 format = lambda dt: (f'{dt.Format("%A %d %B %Y")}')
66 or
67 fmt = "%Y/%m/%d"
68 format = lambda dt: (dt.Format(fmt))
69 format = lambda dt: (dt.Format("%Y/%m/%d"))
70 format = lambda dt: (dt.FormatISODate())
71 for those who prefer strftime formatting:
72 format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))
73 @param image Show image or Not (True or False) Default True
74
75 TextCtrl Styles:
76 wx.RIGHT
77 wx.LEFT
78 wx.TE_CENTRE
79
80 wx.BORDER_NONE is always applied to the internal textctrl
81 wx.BORDER_SIMPLE is the default border for the control itself
82
83 Events: EVT_DATE_CHANGED A date change occurred in the control
84
85 Event Functions:
86 GetValue() Returns formatted date in the event as a string
87
88 GetDate() Returns wxDateTime date in the event, with all of its attendant functions
89
90 GetDateTime() Returns python datetime of date in the event
91
92 GetTimeStamp() Returns seconds since Jan 1, 1970 UTC for selected date
93
94 Functions:
95 GetValue() Returns formatted date in the event as a string
96
97 GetDate() Returns wxDateTime date in the control
98
99 GetDateTimeValue() Returns python datetime of date in the control
100
101 GetTimeStamp() Returns seconds since Jan 1, 1970 UTC for selected date
102
103 GetLocale() Returns tuple of current language code and encoding
104
105 SetValue(date) Sets the date in the control
106 expects a wx.DateTime, a python datetime datetime or a timestamp
107 Any invalid date defaults to wx.DateTime.Today()
108
109 SetButtonBitmap(bitmap=None) Set a specified image to used for the Button
110 This is also used for the Focus image unless overridden (see below)
111 You may use a file name or a wx.Bitmap
112
113 SetButtonBitmapFocus(bitmap=None) Set a specified image to used for the Button focus
114 You may use a file name or a wx.Bitmap
115
116 SetFormatter(formatter) Date format in the form of a lambda
117 default: lambda dt: dt.FormatISODate()
118
119
120 SetLocale(locale) Set the locale for Calendar day and month names
121 e.g. 'de_DE.UTF-8' German
122 'es_ES.UTF-8' Spanish
123 depends on the locale being available on the machine
124
125 SetCalendarStyle(style)
126 wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week
127 wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week
128 wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar
129 wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
130 wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
131 wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
132 wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
133 wx.adv.CAL_SHOW_WEEK_NUMBERS
134
135 SetCalendarHighlights(colFg, colBg) Colours to mark the currently selected date
136
137 SetCalendarHolidayColours(colFg, colBg) Colours to mark Holidays
138
139 SetCalendarHeaders(colFg, colBg)
140
141 SetCalendarFg(colFg) Calendar ForegroundColour
142
143 SetCalendarBg(colBg) Calendar & Ctrl BackgroundColour
144
145 SetCalendarFont(font=None) Set font of the calendar to a wx.Font
146 Alter the font family, weight, size, etc
147
148 SetCalendarMarkDates(markdates = {}) Mark dates with a Border
149 A dictionary containing year and month tuple as the key and a list of days for the values to be marked
150 e.g.
151 {
152 (2023, 7) : [2,5,7,11,30],
153 (2023, 8) : [7,12,13,20,27],
154 (2023, 9) : [1,27]
155 }
156
157 Values of less than 0 indicate not a specific date but a day: -1 Monday, -2 Tuesday, ... -7 Sunday
158 allowing you to mark all Mondays and Fridays in the month of January e.g {(2023, 1) : [-1, -5]}
159 You may include a mixture of negative and positive numbers (days and specific dates)
160
161 Negative values beyond that indicate the nth weekday, (the indexing is a bit confusing because it's off by 1
162 the first digit represents the day and the 2nd digit represents the occurrence i.e.
163
164 -11, 1st Monday | -12, 2nd Monday | -13, 3rd Monday | -14, 4th Monday | -15, 5th or Last Monday
165 -21, 1st Tuesday | -22, 2nd Tuesday | -23, 3rd Tuesday | -24, 4th Tuesday | -25, 5th or Last Tuesday
166 ..............................................................................................................
167 -71, 1st Sunday | -72, 2nd Sunday | -73, 3rd Sunday | -74, 4th Sunday | -75, 5th or Last Sunday
168
169 If the 5th occurrence of a weekday doesn't exist, the last occurrence of the weekday is substituted.
170
171 -99 Stands for the last day of the month
172 -98 is for the last weekday of the month
173
174 SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour)
175 Defines the border type to mark dates wx.adv.CAL_BORDER_SQUARE (default)
176 and a border colour e.g. wx.NullColour (Default), wx.RED or a hex value '#800080' etc
177 Valid border values are:
178 wx.adv.CAL_BORDER_NONE - 0
179 wx.adv.CAL_BORDER_SQUARE - 1
180 wx.adv.CAL_BORDER_ROUND - 2
181
182
183 SetCalendarHolidays(holidays = {})
184 A dictionary containing year and month tuple as the key and a list of days for the values e.g.
185 {
186 (2023, 1) : [1,],
187 (2023, 7) : [1,30],
188 (2023, 8) : [7,15,27],
189 (2023, 12) : [25,26]
190 }
191
192 Holidays can also be 'fixed' Holidays occurring every year on the same day by setting the year to zero in the key
193 e.g.
194 {
195 (0, 1) : [1,], # January 1st is a Holiday every year
196 (2023, 7) : [1,30],
197 (2023, 8) : [7,15,27],
198 (0, 12) : [25,26] # Christmas Day and Boxing Day are Holidays every year
199 }
200
201 SetCalendarNotes(notes = {})
202 A dictionary containing a year, month, day tuple as the key and a string for the note e.g.
203 {
204 (2023, 1, 1) : "New Year's Day",
205 (2023, 12, 25) : "Christmas Day"
206 }
207
208 Like Holidays, Notes can be assigned to a specific day every year
209 {
210 (0, 1, 1) : "New Year's Day",
211 (0, 12, 25) : "Christmas Day"
212 }
213
214 To compliment Marked Dates and Restricted Dates, notes can also be assigned a negative day following the
215 the same pattern as Marked Dates and Restricted Dates.
216 Allowing you to match Notes with Marked Dates and Restricted Dates.
217
218 {
219 (0, 1, -11) : "The 1st Monday of January/the year",
220 (0, 1, -35) : "The last Wednesday of January",
221 (0, 2, -5) : "Every Friday in February"
222 }
223
224 If you set Official Holidays, they are enter automatically into the notes, marked with a leading asterix (*).
225
226 Notes are displayed as a ToolTip, when the day is hovered over or Right clicked
227 or if the mouse is over the calendar and the Arrow keys are used to navigate the calendar to that day.
228
229 A right click on the calendar on a blank day, will display All the notes for the month.
230
231 SetCalendarRestrictDates(rdates = {})
232 A dictionary containing a year and month tuple as the key and a list of days, for the days that
233 are Not selectable within that year/month i.e. the reverse of Marked Dates
234 e.g.
235 {
236 (2023, 1) : [1,15],
237 (2023, 3) : [1,15],
238 (2023, 5) : [1,15],
239 (2023, 7) : [1,15,23],
240 (2023, 9) : [1,15],
241 (2023, 11) : [1,15]
242 }
243
244 All dates in the 'restricted' dictionary use an Italic StruckThrough font and cannot be selected
245
246 See SetCalendarMarkDates for the ability to use negative values to calculate dictionary values to restrict
247 more complicated entries like All Mondays or the 2nd and 4th Tuesday for example, by using negative values.
248
249 SetCalendarDateRange(lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime)
250 Either 2 wx.DateTime values to restrict the selectable dates
251 or just a lower date or just an upper date
252 (The oddity of wx.DateTime months from 0 is catered for)
253 Returns False if the dates are not wx.DateTime objects
254 wx.DefaultDateTime equals no date selected.
255
256 Dates outside of the range will display an "Out of Range" ToolTip, with the defined range.
257
258 SetCalendarOnlyWeekDays(boolean) Default False
259 If set only weekdays are selectable. weekends and holidays use an Italic StruckThrough font and cannot be selected
260 Holidays are treated as Not a weekday i.e. no work
261
262 AddOfficialHolidays(country='', subdiv='', language='') Default blank, blank, blank
263 Only available if the python 'holidays' module was successfully imported
264 Currently supports 134 country codes using country ISO 3166-1 alpha-2 codes and the optional subdivision
265 (state, region etc) using ISO 3166-2 codes.
266 Language must be an ISO 639-1 (2-letter) language code. If the language translation is not supported
267 the original holiday names are returned.
268
269 For details: https://python-holidays.readthedocs.io/en/latest/
270 (or the file 'python-holidays — holidays documentation.html' supplied with this program)
271 e.g.
272 country='ES' Spain
273 country='ES' and subdiv='AN' Spain, Andalucia
274 country='UK' and subdiv='ENG' United Kingdom, England
275 country='US' and subdiv='SC' USA, South Carolina
276
277 function returns True if successful, an existing country and subdivision (if supplied)
278 or False if there was an error
279
280 This function can be called multiple times, once for each country or region in a country
281 that you wish marked on the calendar.
282 The first call sets the primary holiday region.
283 May be useful if you are operating in more than one geographical area, with differing holidays
284
285 Default Values:
286 date - Today
287 style - READ_ONLY
288
289 Author: J Healey
290 Created: 04/12/2022
291 Copyright: J Healey - 2022-2023
292 License: GPL 3 or any later version
293 Email: <rolfofsaxony@gmx.com>
294 Version 1.5
295
296 A thank you to Richard Townsend (RichardT) for the inspiration of the date dictionaries for some of the functions.
297
298 Changelog:
299 1.5 Add optional holidays package
300 A fast, efficient Python library for generating country and subdivision- (e.g. state or province) specific
301 sets of government-designated holidays on the fly.
302 Alter the font_family, weight, size etc of the calendar popup
303 New functions:
304 AddOfficialHolidays - to add Official holiday zones using 'python holidays package'
305 SetCalendarFont() - Change calendar font_family, weight, style, size etc
306 Fix for MSW, added style wx.PU_CONTAINS_CONTROLS to the Popup, which allows the month choice drop down
307 to function
308
309 1.4 SetCalendarDateRange
310 allows the restriction of dates to a range, that can be selected
311 SetCalendarNotes
312 Allows for a note to be assigned to individual days in the calendar.
313 If notes exist for a day, when hovered over the ToolTip will display the note.
314 Envisaged to be used in conjunction with Holidays and Marked days to provide detail
315 SetCalendarRestrictDates
316 Set a dictionary of dates that are Not selectable
317 SetCalendarOnlyWeekDays
318 Only weekdays are selectable i.e. weekends and holidays are not
319
320 1.3 New Functions
321 SetCalendarHolidayColours
322 SetCalendarHolidays
323 SetCalendarMarkBorder
324 SetCalendarMarkDates
325 Permit the definition of Holidays and key dates to be highlighted and the method of highlighting;
326 by colour for holidays and border for marked days
327
328 1.2 New function SetButtonBitmap()
329 Allows a specified image to be used for the button
330 (also allows bitmap from wx.ArtProvider to be used)
331
332 New function SetButtonBitmapFocus()
333 Allow a specified image to be used for the button focus
334
335 1.1 subclass changes from wx.Control to wx.Panel to handle tab traversal
336 The image will indicate Focus
337 Demonstration colours set to Hex
338
339 Usage example:
340
341 import wx
342 import minidatepickerbutton as MDB
343 class Frame(wx.Frame):
344 def __init__(self, parent):
345 wx.Frame.__init__(self, parent, -1, "MiniDatePicker Demo")
346 format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y")}'))
347 panel = wx.Panel(self)
348 mdb = MDB.MiniDatePickerButton(panel, -1, pos=(50, 50), style=0, date=0, formatter=format)
349 self.Show()
350
351 app = wx.App()
352 frame = Frame(None)
353 app.MainLoop()
354
355 """
356
357 import wx
358 import wx.adv
359 from wx.lib.embeddedimage import PyEmbeddedImage
360 import datetime
361 import calendar
362 import locale
363 try:
364 import holidays
365 holidays_available = True
366 except ModuleNotFoundError:
367 holidays_available = False
368
369 img = PyEmbeddedImage(
370 b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABg2lDQ1BJQ0MgcHJvZmlsZQAA'
371 b'KJF9kT1Iw0AcxV9TS0UiDu0g4pChOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOL'
372 b's64OroIg+AHi6OSk6CIl/i8ptIj14Lgf7+497t4BQqPCdLtnHNANx0onE1I2tyqFXyEihAhi'
373 b'EBVmm3OynELX8XWPAF/v4jyr+7k/R7+WtxkQkIhnmWk5xBvE05uOyXmfOMpKikZ8Tjxm0QWJ'
374 b'H7mu+vzGueixwDOjViY9TxwlloodrHYwK1k68RRxTNMNyheyPmuctzjrlRpr3ZO/UMwbK8tc'
375 b'pzmMJBaxBBkSVNRQRgUO4rQapNhI036ii3/I88vkUslVBiPHAqrQoXh+8D/43a1dmJzwk8QE'
376 b'EHpx3Y8RILwLNOuu+33sus0TIPgMXBltf7UBzHySXm9rsSNgYBu4uG5r6h5wuQMMPpmKpXhS'
377 b'kKZQKADvZ/RNOSByC/St+b219nH6AGSoq9QNcHAIjBYpe73Lu3s7e/v3TKu/H0FDcpMpQj/v'
378 b'AAAACXBIWXMAAAsTAAALEwEAmpwYAAACk0lEQVRIx43W3+unQxQH8Nc83w9tVutHNj9v5M4q'
379 b'tty7oC2u2Fy58KOV9g/AlRJygQvlRrIlpbhSW7uJorCKlKQoJand7AVJYm3Z7zMu9v18zWfM'
380 b'J6aeZs7MmTNnznm/zzwFE6p/WunkLczNXEnfyhO2Rza2rLfpP4xPG4zPm2xsNR6NPN/uNuq8'
381 b'nAa3W4tGaRQrrsZFkX/FIbyO3dG7PHq/RP4d9+NIs/YHTi+OrzKYcS2OZtNpvIqbcQfuyqFv'
382 b'pH80e45hP26JM1fhYtyDHzGvUGuttZSyB3/hWdyAn3P4KXwYg+dywAfx9mTmf8JH+A5P45Ls'
383 b'Ox/XUkpJzM/kJofxJ17JjR7GTfgGX2EfHsHZ3PRM5OsynvvEtTCrGR+Ih3fmJvsTgmsSEtE5'
384 b'lb5N7g7qVgO0LIk/FM9r5N14uUPM3TiY/bVbK5hbDPdEaudq8/VQnBu4Lzo7XJiWJA8Y2reK'
385 b'5/F4jN6L9/EaHsQFsVewHZtWSfLUeTBqBbfitxi6HTfiisjn1pTPA2daNZ7PDengh8R9b9Cz'
386 b'L0g6G73bcGUguSv7/1UypkFYlnA9iZfwPR4K+V7EM/H2cIh2JGG7sCszFXW1oZDNeA57Ujre'
387 b'CkSfSClYpYRcH3Ie7EJU2ySPilnBCXwW4rwdKH4axlYcDwe+xnsbSvlO1vt6Lsa/TGH7OP0X'
388 b'+Dy6J/BtdD5pbK1BfjVg8aL4QlMIj2btscaBN5s9D3RkW4hWpwaztXmAapfwTdAdEbOF/BoP'
389 b'JlyamD/VvU6tV/OgrCxzu3BZq7NqFE/iXdzXkW5UOspAZznonaVUY6t9zeRdKMu4YeVUa507'
390 b'lg51lrXlPS+dh6MwTJibw6dBSdn4J1K6R39ofPDH8L9/c/4GBa36v+mJzSMAAAAASUVORK5C'
391 b'YII=')
392
393 imgf = PyEmbeddedImage(
394 b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAA'
395 b'CXBIWXMAAAsTAAALEwEAmpwYAAADf0lEQVRIx+2V3yusWxjHP2u9o8Y7k92e6DQ1U3KUGiXp'
396 b'iDZGUepEtF1RdlImuVEucCGk1K7hD9BEfuz2nXBzqIkLdS7VODXDldygEfmVkTBr7at3mjFj'
397 b'37k7z9X7Pms96/v8+H7XEt/3vms+0CQfbB8OYEv/+fbnNx4fH1FKsXGxgfpX0dvbSyKRQGvN'
398 b'9fU1QghcLhdaa5xOJ6urq4g6wdc/viKEwOFw8OP4RzZAT0kP7e3tOJ1O3G43gUCA0H8hdnZ2'
399 b'2NraQilFT08PSinm5uaQUtLW1kYkEiHwV4CpqSni8TgPDw9sbGzw8+RnJsD9/T15eXmMj49z'
400 b'fHxMYWEhWms8Hg+NjY1IKbHZbCilaGpqQgiB1+tFSklRURF+v5/S0lImJye5u7vLnkEymcQ0'
401 b'Tc7Ozpifnyc/P5+BgQGcTicLCwtEo1F8Ph8VFRXEYjFCoRB2u51AIIBpmoRCIU5PTzFNEyll'
402 b'NoBhGAAIITAMg3A4jNfrZXt7G601kUiEeDzO+fk5BwcHAITDYTweD+FwGCFE6tD0b9vbqWut'
403 b'0VqzuLjIwsICQgi01iQSCQYHB1PBWms2NzdZX1/HMAyEEBlrWRXkWrR8VrAFlp6hlJJkMpny'
404 b'CyFQSmUDWAenB6ebEILR0VGCwSBSStbW1mhubqavr4/l5WVeXl5QSqG1TrU7o0XpGeQyrTX7'
405 b'+/sUFBSglGJ3d5fDw0Ourq5QSmGz2X4vNCklWutUecXFxSQSCS4vL/H5fMRiMaLRKHa7Ha01'
406 b'e3t7XFxccHd3x9PTE1LKDPa8e1VYfZ6enmZoaIiSkhKWlpZwu90MDw8zMTGBzWZjfn6etrY2'
407 b'+vv7CQaDPD8/k075rAqszIUQSCkZGxvj/v6ex8dHurq6iMfjzMzM4HA4eH19pbe3l5OTE/Ly'
408 b'8lhfX89oUQYJ3gJY/a6vr6empgbTNOns7MQwDGpra/H7/QghaG1txePxUF5eTktLy7tEyRKa'
409 b'ZTU1NVRWVuJyuWhoaMDlclFVVUV1dTWGYVBfX09ZWRmVlZXU1dWlGPRboVkc1lozMjKC1hop'
410 b'JR0dHQghmJ2dTe3t7u5OxaysrGSI7V2hWcNJF9hbYeWib64Ecw759vYW0zSZmppK+dKzsip6'
411 b'e61YvqenJ25ubjL2iPQ3+eafG46OjjJEl+vqsA7OBe7z+fj096fcM/jc+pkvrV/+f/Qz7BdH'
412 b'Sp/4DuCblwAAAABJRU5ErkJggg==')
413
414 __version__ = 1.5
415
416
417 mdbEVT = wx.NewEventType()
418 EVT_DATE_CHANGED = wx.PyEventBinder(mdbEVT, 1)
419
420
421 class mdbEvent(wx.PyCommandEvent):
422 def __init__(self, eventType, eventId=1, date=None, value=''):
423 """
424 Default class constructor.
425
426 :param `eventType`: the event type;
427 :param `eventId`: the event identifier.
428 """
429 wx.PyCommandEvent.__init__(self, eventType, eventId)
430 self._eventType = eventType
431 self.date = date
432 self.value = value
433
434 def GetDate(self):
435 """
436 Retrieve the date value of the control at the time
437 this event was generated, Returning a wx.DateTime object"""
438 return self.date
439
440 def GetValue(self):
441 """
442 Retrieve the formatted date value of the control at the time
443 this event was generated, Returning a string"""
444 return self.value.title()
445
446 def GetDateTime(self):
447 """
448 Retrieve the date value of the control at the time
449 this event was generated, Returning a python datetime object"""
450 return wx.wxdate2pydate(self.date)
451
452 def GetTimeStamp(self):
453 """
454 Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
455 Returning a integer
456 """
457 return int(self.date.GetValue()/1000)
458
459 class MiniDatePickerButton(wx.Panel):
460 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
461 style=wx.BORDER_SIMPLE, name="MDPickerButton", date=0, formatter='', image=True):
462
463 wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
464 self.parent = parent
465 self._date = date
466 if formatter:
467 format = formatter
468 else:
469 format = lambda dt: dt.FormatISODate()
470 font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
471 #self.SetWindowStyle(wx.BORDER_NONE)
472 self._style = style
473 self._calendar_style = wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SHOW_HOLIDAYS
474 self._calendar_headercolours = None
475 self._calendar_highlightcolours = None
476 self._calendar_holidaycolours = None
477 self._calendar_Bg = None
478 self._calendar_Fg = None
479 self._calendar_Font = None
480 self._calendar_MarkDates = {}
481 self._calendar_MarkBorder = (wx.adv.CAL_BORDER_SQUARE, wx.NullColour)
482 self._calendar_Holidays = {}
483 self._calendar_RestrictDates = {}
484 self._calendar_daterange = (wx.DefaultDateTime, wx.DefaultDateTime)
485 self._calendar_Notes = {}
486 self._calendar_SetOnlyWeekDays = False
487 self._calendar_OfficialHolidays = False
488 self._calendar_AddOfficialHolidays = []
489
490 self._image = image
491 self._veto = False
492 self._pop = False
493 if size == wx.DefaultSize:
494 dc = wx.ScreenDC()
495 dc.SetFont(font)
496 trialdate = format(wx.DateTime(28,9,2022)) # a Wednesday in September = longest names in English
497 w, h = dc.GetTextExtent(trialdate)
498 if self._image:
499 size = (w+94, -1) # Add image width (24) plus a buffer
500 else:
501 size = (w+60, -1) # Add a buffer
502 del dc
503 self.txtstyle = 0
504
505 if style & wx.LEFT or style == wx.LEFT:
506 self.txtstyle = wx.RIGHT
507 elif style & wx.RIGHT or style == wx.RIGHT:
508 self.txtstyle = wx.LEFT
509
510 # MiniDatePickerButton
511
512 self.ctl = wx.Button(self, id, label=str(self._date),
513 pos=pos, size=size, style=style, name=name)
514 if self._image:
515 if self.txtstyle:
516 self.ctl.SetBitmap(img.Bitmap, self.txtstyle)
517 self.ctl.SetBitmapFocus(imgf.Bitmap)
518 else:
519 self.ctl.SetBitmap(img.Bitmap)
520 self.ctl.SetBitmapFocus(imgf.Bitmap)
521 self.MinSize = self.GetBestSize()
522 # End
523
524 # Bind the events
525 self._formatter = format
526 self.ctl.Bind(wx.EVT_BUTTON, self.OnCalendar)
527 self.SetValue(date)
528
529 sizer = wx.BoxSizer(wx.HORIZONTAL)
530 sizer.Add(self.ctl, 1, wx.EXPAND, 0)
531 self.SetSizerAndFit(sizer)
532 self.Show()
533
534 def OnCalendar(self, _event=None):
535 if self._pop:
536 return
537 self._pop = True # controls only one popup at any one time
538 self.calendar = CalendarPopup(
539 self, self._date, self.OnDate, self.GetTopLevelParent(), wx.PU_CONTAINS_CONTROLS|wx.SIMPLE_BORDER)
540 pos = self.ClientToScreen((0, 0))
541 size = self.GetSize()
542 self.calendar.Position(pos, (0, size.height))
543
544 def SetFormatter(self, formatter):
545 '''formatter will be called with a wx.DateTime'''
546 self._formatter = formatter
547 self.OnDate(self._date)
548
549 def SetLocale(self, alias):
550 try:
551 locale.setlocale(locale.LC_TIME, locale=alias)
552 except Exception as e:
553 locale.setlocale(locale.LC_TIME, locale='')
554 self.SetValue(self._date)
555
556 def SetCalendarStyle(self, style=0):
557 self._calendar_style = style
558
559 def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
560 self._calendar_headercolours = colFg, colBg
561
562 def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
563 self._calendar_highlightcolours = colFg, colBg
564
565 def SetCalendarHolidayColours(self, colFg=wx.NullColour, colBg=wx.NullColour):
566 self._calendar_holidaycolours = colFg, colBg
567
568 def SetCalendarFg(self, colFg=wx.NullColour):
569 self._calendar_Fg = colFg
570
571 def SetCalendarBg(self, colBg=wx.NullColour):
572 self._calendar_Bg = colBg
573
574 def SetCalendarFont(self, font=None):
575 self._calendar_Font = font
576
577 def SetCalendarMarkDates(self, markdates = {}):
578 self._calendar_MarkDates = markdates
579
580 def SetCalendarMarkBorder(self, border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour):
581 self._calendar_MarkBorder = (border, bcolour)
582
583 def SetCalendarHolidays(self, holidays = {}):
584 self._calendar_Holidays = holidays
585
586 def SetCalendarRestrictDates(self, rdates = {}):
587 self._calendar_RestrictDates = rdates
588
589 def AddOfficialHolidays(self, country='', subdiv='', language=''):
590 if not holidays_available:
591 return False
592 country = country.upper()
593 subdiv = subdiv.upper()
594 try:
595 supported = holidays.country_holidays(country=country).subdivisions
596 except Exception as e:
597 return False
598 if subdiv:
599 if subdiv not in supported:
600 return False
601 if not self._calendar_OfficialHolidays:
602 self._calendar_OfficialHolidays = (country, subdiv, language)
603 else:
604 self._calendar_AddOfficialHolidays.append([country, subdiv, language])
605 return True
606
607 def SetCalendarDateRange(self, lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime):
608 if not isinstance(lowerdate, wx.DateTime):
609 return False
610 if not isinstance(upperdate, wx.DateTime):
611 return False
612 self._calendar_daterange = (lowerdate, upperdate)
613 return True
614
615 def SetCalendarNotes(self, notes = {}):
616 self._calendar_Notes = notes
617
618 def SetCalendarOnlyWeekDays(self, wds = False):
619 self._calendar_SetOnlyWeekDays = wds
620 if wds:
621 self._calendar_style = self._calendar_style | wx.adv.CAL_SHOW_HOLIDAYS
622
623 def OnDate(self, date):
624 self._date = date
625 self.ctl.SetLabel(self._formatter(date).title())
626 self.MinSize = self.GetBestSize()
627 if self._veto:
628 self._veto = False
629 return
630 event = mdbEvent(mdbEVT, self.GetId(), date=date, value=self._formatter(date))
631 event.SetEventObject(self)
632 self.GetEventHandler().ProcessEvent(event)
633
634 def GetValue(self):
635 return self.ctl.GetLabel()
636
637 def GetLabel(self):
638 return self.ctl.GetLabel()
639
640 def GetDate(self):
641 return self._date
642
643 def GetDateTimeValue(self):
644 """
645 Return a python datetime object"""
646 return wx.wxdate2pydate(self._date)
647
648 def GetTimeStamp(self):
649 """
650 Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
651 Returning a integer
652 """
653 return int(self._date.GetValue()/1000)
654
655 def GetLocale(self):
656 return locale.getlocale(category=locale.LC_TIME)
657
658 def SetValue(self, date):
659 if isinstance(date, wx.DateTime):
660 pass
661 elif isinstance(date, datetime.date):
662 date = wx.pydate2wxdate(date)
663 elif isinstance(date, int) and date > 0:
664 date = wx.DateTime.FromTimeT(date)
665 elif isinstance(date, float) and date > 0:
666 date = wx.DateTime.FromTimeT(int(date))
667 else: # Invalid date value default to today's date
668 date = wx.DateTime.Today()
669 self._date = date.ResetTime()
670 self._veto = True
671 self.SetFormatter(self._formatter)
672
673 def SetButtonBitmap(self, bitmap=None):
674 if not bitmap:
675 return
676 bitmap = wx.Bitmap(bitmap)
677 if self.txtstyle:
678 self.ctl.SetBitmap(bitmap, self.txtstyle)
679 self.ctl.SetBitmapFocus(bitmap)
680 else:
681 self.ctl.SetBitmap(bitmap)
682 self.ctl.SetBitmapFocus(bitmap)
683
684 def SetButtonBitmapFocus(self, bitmap=None):
685 if not bitmap:
686 return
687 bitmap = wx.Bitmap(bitmap)
688 self.ctl.SetBitmapFocus(bitmap)
689
690
691 class CalendarPopup(wx.PopupTransientWindow):
692 def __init__(self, parent, date, callback, *args, **kwargs):
693 '''date is the initial date; callback is called with the chosen date'''
694 super().__init__(*args, **kwargs)
695 self.parent = parent
696 self.callback = callback
697 self.calendar = wx.adv.GenericCalendarCtrl(self, pos=(5, 5), style=parent._calendar_style)
698 self.calendar.SetDate(date)
699 self.Holidays = {}
700 self.OfficialHolidays = {}
701 self.RestrictedDates = {}
702 self.MarkDates = {}
703 self.Notes = {}
704
705 if parent._calendar_headercolours:
706 self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
707 if parent._calendar_highlightcolours:
708 self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
709 if parent._calendar_holidaycolours:
710 self.calendar.SetHolidayColours(parent._calendar_holidaycolours[0],parent._calendar_holidaycolours[1])
711 if parent._calendar_Bg:
712 self.calendar.SetBackgroundColour(parent._calendar_Bg)
713 self.SetBackgroundColour(parent._calendar_Bg)
714 if parent._calendar_Fg:
715 self.calendar.SetForegroundColour(parent._calendar_Fg)
716 if parent._calendar_Font:
717 self.calendar.SetFont(parent._calendar_Font)
718 self.markborder = parent._calendar_MarkBorder
719 if parent._calendar_MarkDates:
720 self.SetMarkDates(parent._calendar_MarkDates)
721 if parent._calendar_Holidays:
722 self.SetHolidays(parent._calendar_Holidays)
723 if parent._calendar_OfficialHolidays:
724 self.SetOfficialHolidays(parent._calendar_OfficialHolidays)
725 if parent._calendar_daterange[0].IsValid() or parent._calendar_daterange[1].IsValid():
726 self.SetDateRange(parent._calendar_daterange[0], parent._calendar_daterange[1])
727 if parent._calendar_RestrictDates:
728 self.SetRestrictDates(parent._calendar_RestrictDates)
729 if parent._calendar_Notes:
730 self.SetNotes(parent._calendar_Notes)
731 sizer = wx.BoxSizer(wx.VERTICAL)
732 sizer.Add(self.calendar, 1, wx.ALL | wx.EXPAND)
733 self.SetSizerAndFit(sizer)
734 self.calendar.Bind(wx.adv.EVT_CALENDAR_MONTH, self.OnChange)
735 self.calendar.Bind(wx.adv.EVT_CALENDAR_YEAR, self.OnChange)
736 self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnChosen)
737 self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnToolTip)
738 self.calendar.Bind(wx.EVT_MOTION, self.OnToolTip)
739 self.calendar.Bind(wx.EVT_RIGHT_DOWN, self.OnToolTip)
740 self.calendar.Bind(wx.EVT_KEY_DOWN, self.OnKey)
741 self.Popup()
742
743 def OnChosen(self, _event=None):
744 ''' Test chosen date for inclusion in restricted dates if set
745 Test if set to only allow weekdays, test if it is a weekday or a holiday, whicj is treated as not a weekday
746 '''
747 d = self.calendar.GetDate()
748 if self.RestrictedDates:
749 test = (d.year, d.month+1)
750 days = self.RestrictedDates.get(test, ())
751 if not days or d.day not in days:
752 pass
753 else:
754 return
755
756 if self.parent._calendar_SetOnlyWeekDays and not d.IsWorkDay(): # Weekend
757 return
758 if self.parent._calendar_SetOnlyWeekDays: # Holiday
759 attr = self.calendar.GetAttr(d.day)
760 if attr.IsHoliday():
761 return
762
763 self.callback(self.calendar.GetDate())
764 self.parent._pop = False
765 self.Dismiss()
766
767 def OnChange(self, event):
768 # If the year changed, recalculate the dictionaries for Marked, Restricted, Official Holidays and Note dates
769 if event.GetEventType() == wx.adv.EVT_CALENDAR_YEAR.typeId:
770 self.MarkDates = self.GenerateDates(self.parent._calendar_MarkDates)
771 self.RestrictedDates = self.GenerateDates(self.parent._calendar_RestrictDates)
772 self.SetOfficialHolidays(self.parent._calendar_OfficialHolidays)
773 self.Notes = self.GenerateNotes(self.parent._calendar_Notes)
774
775 date = event.GetDate()
776 self.OnMonthChange()
777
778 def OnDismiss(self, event=None):
779 self.parent._pop = False
780
781 def OnKey(self, event):
782 keycode = event.GetKeyCode()
783 if keycode == wx.WXK_ESCAPE:
784 self.parent._pop = False
785 self.Dismiss()
786 event.Skip()
787
788 def SetMarkDates(self, markdates):
789 self.MarkDates = self.GenerateDates(markdates)
790 self.OnMonthChange()
791
792 def OnMonthChange(self):
793 font = self.calendar.GetFont()
794 font.SetStrikethrough(True)
795 font.MakeItalic()
796 date = self.calendar.GetDate()
797 days_in_month = date.GetLastMonthDay().day
798 mark_days = self.MarkDates.get((date.year, date.month+1), []) # get dict values or an empty list if none
799 h_days = self.Holidays.get((date.year, date.month+1), [])
800 fixed_h_days = self.Holidays.get((0, date.month+1), [])
801 r_days = self.RestrictedDates.get((date.year, date.month+1), [])
802 oh_days = self.OfficialHolidays.get((date.year, date.month+1), [])
803
804 if isinstance(mark_days, int): # Allow for people forgetting it must be a tuple, when entering a single day
805 mark_days = tuple((mark_days,))
806 if isinstance(h_days, int):
807 h_days = tuple((h_days,))
808 if isinstance(fixed_h_days, int):
809 fixed_h_days = tuple((fixed_h_days,))
810 if isinstance(r_days, int):
811 r_days = tuple((r_days,))
812
813 for d in range(1, days_in_month+1):
814 attr = self.calendar.GetAttr(d)
815 highlight_attr = wx.adv.CalendarDateAttr()
816 if d in mark_days: # Marked Day
817 highlight_attr.SetBorder(self.markborder[0])
818 highlight_attr.SetBorderColour(self.markborder[1])
819 if d in h_days: # Holiday
820 highlight_attr.SetHoliday(True)
821 if d in fixed_h_days: # Fixed Holiday
822 highlight_attr.SetHoliday(True)
823 if d in oh_days: # Official Holidays
824 highlight_attr.SetHoliday(True)
825 if not wx.DateTime(d, date.month, date.year).IsWorkDay(): # Weekend
826 highlight_attr.SetHoliday(True)
827 if d in r_days: # Resticted Day (override holiday)
828 highlight_attr.SetFont(font)
829 if highlight_attr.IsHoliday():
830 if self.parent._calendar_SetOnlyWeekDays:
831 highlight_attr.SetFont(font)
832 if highlight_attr is not None:
833 self.calendar.SetAttr(d, highlight_attr)
834 else:
835 self.calendar.ResetAttr(d)
836
837 self.calendar.Refresh()
838
839 def SetHolidays(self, holidays):
840 self.Holidays = holidays
841 self.OnMonthChange()
842
843 def SetOfficialHolidays(self, holiday_codes):
844 self.OfficialHolidays = {}
845 if not holiday_codes: # holiday codes not set
846 return
847 country, subdiv, language = holiday_codes
848 self.country_name = country
849 for c in holidays.registry.COUNTRIES.values():
850 if country in c:
851 self.country_name = c[0]
852
853 d = self.calendar.GetDate()
854 for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
855 existing = self.OfficialHolidays.get((k.year, k.month), [])
856 if k.day not in existing:
857 self.OfficialHolidays[(k.year, k.month)] = existing + [k.day]
858
859 for item in self.parent._calendar_AddOfficialHolidays:
860 country, subdiv, language = item
861 for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
862 existing = self.OfficialHolidays.get((k.year, k.month), [])
863 if k.day not in existing:
864 self.OfficialHolidays[(k.year, k.month)] = existing + [k.day]
865
866 self.OnMonthChange()
867
868 def SetDateRange(self, lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime):
869 if lowerdate.IsValid() or upperdate.IsValid():
870 if lowerdate.IsValid():
871 lowerdate = wx.DateTime(lowerdate.day, lowerdate.month-1, lowerdate.year)
872 if upperdate.IsValid():
873 upperdate = wx.DateTime(upperdate.day, upperdate.month-1, upperdate.year)
874 self.calendar.SetDateRange(lowerdate, upperdate)
875
876 def SetNotes(self, notes):
877 self.Notes = self.GenerateNotes(notes)
878
879 def SetRestrictDates(self, rdates):
880 self.RestrictedDates = self.GenerateDates(rdates)
881 self.OnMonthChange()
882
883 def restricted_date_range(self, start, end):
884 '''
885 Generate dates between a start and end date
886 '''
887 for i in range((end - start).days + 1):
888 yield start + datetime.timedelta(days = i)
889
890 def day_in_range(self, start, end, day):
891 '''
892 Test if date is the required day of the week
893 '''
894 for d in self.restricted_date_range(start, end):
895 if d.isoweekday() == day:
896 yield d
897
898 def GenerateDates(self, date_dict):
899 ''' Generated on start and when the year changes (Marked and Restricted dictionaries)
900 This routine generates a new dictionary from the one passed in and returns the generated dictionary.
901 This because the original passed in dictionary may include date codes e.g. -99 for the last day of a month
902 or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
903 An added complication is that the year may be set to zero, denoting all years, so if the calendar year is
904 changed, this routine is run again, to ensure that the dates are relevant to the current year.
905 '''
906 generated_dict = {}
907
908 for year, month in date_dict:
909 gen_year = year
910 if gen_year == 0: # Zero entry = All years, so generate dates for the currently selected year
911 d = self.calendar.GetDate()
912 gen_year = d.year
913 day_map = calendar.monthcalendar(gen_year, month)
914 for neg in list(date_dict.get((year, month))):
915 if neg >= 0:
916 existing = generated_dict.get((gen_year, month), [])
917 if neg not in existing:
918 generated_dict[(gen_year, month)] = existing + [neg]
919 continue
920 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
921 d1 = datetime.datetime(gen_year, month, 1)
922 d2 = datetime.datetime(gen_year, month, last_day_no)
923 if neg < 0 and neg >= -7: # Every specified weekday
924 for i in self.day_in_range(d1, d2, abs(neg)):
925 existing = generated_dict.get((gen_year, month), [])
926 if i.day not in existing:
927 generated_dict[(gen_year, month)] = existing + [i.day]
928 continue
929 if neg == -99: # Last day of the month
930 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
931 existing = generated_dict.get((gen_year, month), [])
932 if last_day_no not in existing:
933 generated_dict[(gen_year, month)] = existing + [last_day_no]
934 continue
935 if neg == -98: # Last weekday of the month
936 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
937 ld = datetime.date(gen_year, month, last_day_no)
938 while ld.isoweekday() > 5: # Last day of month is not a weekday
939 ld -= datetime.timedelta(days=1) # deduct days to get to Friday
940 existing = generated_dict.get((gen_year, month), [])
941 if ld.day not in existing:
942 generated_dict[(gen_year, month)] = existing + [ld.day]
943 continue
944 if neg <= -11 and neg >= -75: # Occurrence of a weekday
945 if neg <= -11 and neg >= -15: # Monday 1-5
946 map_idx = 0
947 occ = neg + 11
948 elif neg <= -21 and neg >= -25: # Tuesday 1-5
949 map_idx = 1
950 occ = neg + 21
951 elif neg <= -31 and neg >= -35: # Wednesday 1-5
952 map_idx = 2
953 occ = neg + 31
954 elif neg <= -41 and neg >= -45: # Thursday 1-5
955 map_idx = 3
956 occ = neg + 41
957 elif neg <= -51 and neg >= -55: # Friday 1-5
958 map_idx = 4
959 occ = neg + 51
960 elif neg <= -61 and neg >= -65: # Saturday 1-5
961 map_idx = 5
962 occ = neg + 61
963 elif neg <= -71 and neg >= -75: # Sunday 1-5
964 map_idx = 6
965 occ = neg + 71
966 else: # Undefined
967 continue
968 week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
969 if abs(occ) >= len(week_map):
970 occ = len(week_map) - 1
971 week_idx = week_map[abs(occ)]
972 map_day = day_map[week_idx][map_idx]
973 existing = generated_dict.get((gen_year, month), [])
974 if map_day not in existing:
975 generated_dict[(gen_year, month)] = existing + [map_day]
976 return generated_dict
977
978 def GenerateNotes(self, date_dict):
979 ''' Generated on start and when the year changes
980 This routine generates a new dictionary of Notes from the one passed in and returns the generated dictionary.
981 This because the original passed in dictionary may include date codes e.g. -99 for the last of a month
982 or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
983 An added complication is that the year may be set to zero, denoting all years, so if the calendar year is changed,
984 this routine is run again, to ensure that the dates are relevant to the current year.
985 Because some of the notes are calculated, a date may have muliple notes, so the notes are accumulated, to form
986 a single note entry, seperated by a + sign
987 If Official Holidays are included, these too are recalculated for the current year.
988 '''
989 generated_dict = {}
990 for year, month, day in date_dict:
991 gen_year = year
992 if gen_year == 0: # Zero entry = All years, so generate dates for the currently selected year
993 d = self.calendar.GetDate()
994 gen_year = d.year
995 day_map = calendar.monthcalendar(gen_year, month)
996 note = date_dict.get((year, month, day))
997 if day >= 0:
998 use_note = generated_dict.get((gen_year, month, day), '')
999 if use_note:
1000 use_note = use_note+"\n + "+note
1001 else:
1002 use_note = note
1003 generated_dict[(gen_year, month, day)] = use_note
1004 continue
1005 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1006 d1 = datetime.datetime(gen_year, month, 1)
1007 d2 = datetime.datetime(gen_year, month, last_day_no)
1008 if day < 0 and day >= -7: # Every specified weekday
1009 for i in self.day_in_range(d1, d2, abs(day)):
1010 use_note = generated_dict.get((gen_year, month, i.day), '')
1011 if use_note:
1012 use_note = use_note+"\n + "+note
1013 else:
1014 use_note = note
1015 generated_dict[(gen_year, month, i.day)] = use_note
1016 continue
1017 if day == -99: # Last day of the month
1018 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1019 use_note = generated_dict.get((gen_year, month, last_day_no), '')
1020 if use_note:
1021 use_note = use_note+"\n + "+note
1022 else:
1023 use_note = note
1024 generated_dict[(gen_year, month, last_day_no)] = use_note
1025 continue
1026 if day == -98: # Last weekday of the month
1027 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1028 ld = datetime.date(gen_year, month, last_day_no)
1029 while ld.isoweekday() > 5: # Last day of month is not a weekday
1030 ld -= datetime.timedelta(days=1) # deduct days to get to Friday
1031 use_note = generated_dict.get((gen_year, month, ld.day), '')
1032 if use_note:
1033 use_note = use_note+"\n + "+note
1034 else:
1035 use_note = note
1036 generated_dict[(gen_year, month, ld.day)] = use_note
1037 continue
1038 if day <= -11 and day >= -75: # Occurrence of a weekday
1039 if day <= -11 and day >= -15: # Monday 1-5
1040 map_idx = 0
1041 occ = day + 11
1042 elif day <= -21 and day >= -25: # Tuesday 1-5
1043 map_idx = 1
1044 occ = day + 21
1045 elif day <= -31 and day >= -35: # Wednesday 1-5
1046 map_idx = 2
1047 occ = day + 31
1048 elif day <= -41 and day >= -45: # Thursday 1-5
1049 map_idx = 3
1050 occ = day + 41
1051 elif day <= -51 and day >= -55: # Friday 1-5
1052 map_idx = 4
1053 occ = day + 51
1054 elif day <= -61 and day >= -65: # Saturday 1-5
1055 map_idx = 5
1056 occ = day + 61
1057 elif day <= -71 and day >= -75: # Sunday 1-5
1058 map_idx = 6
1059 occ = day + 71
1060 else: # Undefined
1061 continue
1062 week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
1063 if abs(occ) >= len(week_map):
1064 occ = len(week_map) - 1
1065 week_idx = week_map[abs(occ)]
1066 map_day = day_map[week_idx][map_idx]
1067 use_note = generated_dict.get((gen_year, month, map_day), '')
1068 if use_note:
1069 use_note = use_note+"\n + "+note
1070 else:
1071 use_note = note
1072 generated_dict[(gen_year, month, map_day)] = use_note
1073
1074 # If official holidays are available write them into the notes
1075
1076 if holidays_available and self.parent._calendar_OfficialHolidays:
1077 country, subdiv, language = self.parent._calendar_OfficialHolidays
1078 d = self.calendar.GetDate()
1079 for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
1080 use_note = generated_dict.get((k.year, k.month, k.day), '')
1081 if use_note:
1082 use_note = use_note+"\n + * "+v
1083 else:
1084 use_note = " * "+v
1085 generated_dict[(k.year, k.month, k.day)] = use_note
1086
1087 for item in self.parent._calendar_AddOfficialHolidays:
1088 country, subdiv, language = item
1089 for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
1090 use_note = generated_dict.get((k.year, k.month, k.day), '')
1091 if use_note:
1092 use_note = use_note+"\n + *"+' '.join(item)+v
1093 else:
1094 use_note = " *"+' '.join(item)+v
1095 generated_dict[(k.year, k.month, k.day)] = use_note
1096
1097
1098 return generated_dict
1099
1100 def OnToolTip(self, event):
1101 '''
1102 If Right click on a non date area, displays all Notes for the month
1103 Test for date range restrictions.
1104 Generate and display tooltips for each day based on position, if there are:
1105 Notes or Restricted entries for the day
1106 '''
1107 try:
1108 pos = event.GetPosition()
1109 click_code, click_date, click_day = self.calendar.HitTest(pos)
1110
1111 # Show all holidays for the month in popup
1112 # if not a valid date position or surrounding week of previous/next month (if shown).
1113 if click_code == 0 or click_code == 5:
1114 if event.GetEventType() == wx.EVT_RIGHT_DOWN.typeId:
1115 click_date = self.calendar.GetDate()
1116 if holidays_available and self.parent._calendar_OfficialHolidays:
1117 country, subdiv, language = self.parent._calendar_OfficialHolidays
1118 else:
1119 country = subdiv = language = ''
1120 hdr = msg = ''
1121 if country:
1122 hdr = "Inc holidays for "+self.country_name
1123 if subdiv:
1124 hdr += " region "+subdiv
1125 hdr += "\n"
1126 else:
1127 hdr = ''
1128 vmax = 200
1129 for k, v in sorted(self.Notes.items()):
1130 if k[0] == click_date.year and k[1] == click_date.month + 1:
1131 msg += "\n"+str(k[2]).zfill(2)+ " "+ v
1132 vmax = max(vmax, self.GetTextExtent(v)[0]+50)
1133 vmax = max(vmax, self.GetTextExtent(hdr)[0])
1134 if msg:
1135 msg = 'Notes for '+click_date.Format('%B') + '\n' + hdr + msg
1136 wx.TipWindow(self,msg,maxLength=vmax)
1137 return
1138 elif click_code != 2: # Something other than a valid date or a blank date
1139 self.calendar.SetToolTip('')
1140 return
1141 except Exception:
1142 click_date = self.calendar.GetDate()
1143
1144 self.calendar.SetToolTip('')
1145 range_check, lower, upper = self.calendar.GetDateRange()
1146 if range_check:
1147 if (lower != wx.DefaultDateTime and click_date.IsEarlierThan(lower)) or \
1148 (upper != wx.DefaultDateTime and click_date.IsLaterThan(upper)):
1149 msg = str(self.parent._formatter(click_date)).title()+'\n'+"Out of Range\n"
1150 if lower != wx.DefaultDateTime:
1151 msg += str(lower.Format("%d-%b-%Y")).title()+' > '
1152 else:
1153 msg += "Any date > "
1154 if upper != wx.DefaultDateTime:
1155 msg += str(upper.Format("%d-%b-%Y")).title()
1156 else:
1157 msg += "Any date"
1158 self.calendar.SetToolTip(msg)
1159 return
1160
1161 restricted = self.RestrictedDates.get((click_date.year, click_date.month + 1), [])
1162 restricted_set = False
1163 if click_date.day in restricted:
1164 restricted_set = True
1165 if self.parent._calendar_SetOnlyWeekDays and not click_date.IsWorkDay():
1166 restricted_set = True
1167 d = (click_date.year, click_date.month + 1, click_date.day)
1168 note = self.Notes.get(d, '') # Year/Month/Day specific Note or blank
1169 if restricted_set:
1170 note = "** Restricted **\n\n"+ note
1171 if not note:
1172 return
1173 self.calendar.SetToolTip(str(self.parent._formatter(click_date)).title()+'\n'+note)
1174
1175
1176 class DemoFrame(wx.Frame):
1177 '''
1178 This demonstration code attempts to provide at least one example of every option, even if it's commented out
1179 It may offer examples of various options for the same thing, which explains its rather messy look
1180 The bulk of the marked dates, holidays, restrictions and notes are set around August 2023, when the testing
1181 was performed, so feel free to navigate to that month or change the values.
1182 '''
1183 def __init__(self, parent):
1184 wx.Frame.__init__(self, parent, -1, "MiniDatePicker Button")
1185
1186 #format = (lambda dt:
1187 # (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
1188 # )
1189
1190 #format = (lambda dt: (f'{dt.Format("%a %d-%m-%Y")}'))
1191
1192 #Using a strftime format converting wx.DateTime to datetime.datetime
1193 #format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))
1194
1195 format = (lambda dt: (f'{dt.Format("%A %d %B %Y")}'))
1196
1197 panel = wx.Panel(self)
1198
1199 self.mdp = MiniDatePickerButton(panel, -1, pos=(50, 50), style=wx.RIGHT,
1200 date=0, formatter=format, image=True)
1201
1202 #self.mdp.SetLocale('es_ES.UTF-8') # Set Locale for Language
1203 #self.mdp.SetFormatter(format)
1204 x=datetime.datetime.now()
1205 #self.mdp.SetValue(x.timestamp()) # Set Date
1206 self.mdp.SetValue(0)
1207
1208 #font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
1209 #font.SetFractionalPointSize(16)
1210 #self.mdp.SetCalendarFont(font) # Set Calendar Font
1211
1212 self.mdp.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST)
1213 self.mdp.ctl.SetBackgroundColour('#e1ffe1') # background lightgreen
1214 #self.mdp.SetButtonBitmap('./Off.png') # Specify button bitmap
1215 #self.mdp.SetButtonBitmapFocus('./On.png') # Specify button focus bitmap
1216 # Another option for the bitmap is to use wx.ArtProvider e.g.
1217 # bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, client=wx.ART_BUTTON)
1218 # self.mdp.SetButtonBitmap(bmp)
1219 self.mdp.SetCalendarHeaders(colFg='#ff0000', colBg='#90ee90') # red/lightgreen
1220 self.mdp.SetCalendarHighlights(colFg='#ffff00', colBg='#0000ff') # yellow/blue
1221 self.mdp.SetCalendarHolidayColours(colFg='#ff0000', colBg='#ffff00') # Holidays red/yellow
1222 self.mdp.SetCalendarBg(colBg='#f0ffff') # azure
1223 self.Bind(EVT_DATE_CHANGED, self.OnEvent)
1224 self.mdp.SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour='#800080') # Mark Border Square or Round, + Colour
1225 #self.mdp.SetCalendarOnlyWeekDays(True) # Only non holiday weekdays are selectable
1226 self.mdp.SetToolTip('Struck Through dates are not selectable')
1227
1228 self.mdp.SetCalendarMarkDates({
1229 (0, 1) : [-11,-23,1], # 1st Monday, 3rd Tuesday and the 1st January
1230 (0, 2) : [-99,], # last day of February holiday, marked, with note
1231 (x.year, 7) : [2,5,7,11,30],
1232 (x.year, 8) : [7,12,13,20,27],
1233 (0, 9) : [1,27,-98] # 1st, 27th and the last weekday of September
1234 })
1235 self.mdp.SetCalendarHolidays({
1236 (0, 1) : [1,], # January 1st every year
1237 (0, 2) : [-99,], # last day of February holiday, marked, with note
1238 (x.year, 8) : [7,15],
1239 (0, 12) : [25,26] # 25th & 26th December every year
1240 })
1241 self.mdp.SetCalendarNotes({
1242 (0, 1, 1) : "New Year's Day", # January 1st every year
1243 (0, 1, -11) : "First Monday of the year",
1244 (0, 1, -1) : "A Monday in January",
1245 (0, 1, -23) : "3rd Tuesday of the year",
1246 (0, 1, -99) : "Last day of January",
1247 (0, 1, -35) : "The last Wednesday of January",
1248 (0, 2, -5) : "A Friday in February",
1249 (0, 2, -99) : "Note for last day of February", # last day of February holiday, marked, with note
1250 (x.year, 8, 7) : "Marked for no reason whatsoever", # This year only
1251 (x.year, 8, 20) : "Marked for reason X", # this year only
1252 (0, 9, -98) : "Last weekday of September",
1253 (0, 12, 25) : "Merry Christmas!",
1254 (0, 2, -99) : "Last day of February"
1255 })
1256
1257 self.mdp.SetCalendarRestrictDates({
1258 (0, 1) : [-1, -2, 5], # exclude Mondays and Tuesdays, 5th of January All years
1259 (0, 2) : [-99,] # exclude last day of February - All years
1260 })
1261
1262 # Restrict Calendar to a date range (define a lowerdate or an upperdate or both
1263 #self.mdp.SetCalendarDateRange(lowerdate=wx.DateTime(23,8,2023), upperdate=wx.DateTime(23,9,2023))
1264
1265 # Official Holidays requires the python holidays module (pip install --upgrade holidays)
1266 #self.mdp.AddOfficialHolidays(country="GB", subdiv="ENG", language="") # Primary region England
1267 #self.mdp.AddOfficialHolidays(country="ES", subdiv="AN", language="") # Additional region Spain, Andalucia
1268
1269 #------------------------------ 2nd Calendar ------------------------------#
1270
1271 self.mdp2 = MiniDatePickerButton(panel, -1, pos=(50, 150), style=wx.LEFT, date=0, formatter=None)
1272
1273 self.mdp2.SetCalendarRestrictDates({
1274 (0, 1) : [-1, -2], # exclude Mondays and Tuesdays - All years
1275 (x.year, 2) : [-1, -2], # exclude Mondays and Tuesdays - This year
1276 (0, 3) : [-99, -1, -2, 5], # exclude Mondays and Tuesdays + last day + 5th - All years
1277 (x.year, 4) : [-1, -2], # exclude Mondays and Tuesdays - This year
1278 (x.year, 5) : [-1, -2], # exclude Mondays and Tuesdays - This year
1279 (x.year, 6) : [-1, -2] # exclude Mondays and Tuesdays - This year
1280 })
1281 self.mdp2.SetToolTip("Struck Through dates are not selectable")
1282 self.mdp2.SetCalendarOnlyWeekDays(False) # Weekends and holidays selectable
1283 self.mdp2.SetCalendarStyle(wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION)
1284 font = wx.Font(16, wx.FONTFAMILY_ROMAN,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_SEMIBOLD)
1285 self.mdp2.SetCalendarFont(font) # Set Calendar Font
1286
1287 y = x + datetime.timedelta(weeks=+4)
1288 self.mdp2.SetValue(y.timestamp())
1289
1290 self.Bind(EVT_DATE_CHANGED, self.OnEvent)
1291 self.Centre()
1292
1293 def OnEvent(self, event):
1294 obj = event.GetEventObject()
1295 print("\nevt", event.GetValue())
1296
1297 x = event.GetDate() # wx.DateTime object
1298 print("evt", x)
1299 print("Day", x.GetDayOfYear(),"Week", x.GetWeekOfYear(), "Name", x.GetWeekDayName(x.GetWeekDay()))
1300
1301 print("evt", event.GetDateTime()) # datetime.datime object
1302 print("evt", event.GetTimeStamp())
1303 print("func", obj.GetValue())
1304 print("func", obj.GetDate())
1305 print("func", obj.GetDateTimeValue())
1306 print("func", obj.GetTimeStamp())
1307 print("func", obj.GetLocale())
1308
1309 if __name__ == '__main__':
1310 app = wx.App()
1311 frame = DemoFrame(None)
1312 frame.Show()
1313 app.MainLoop()
Download source
Additional Information
Link :
- - - - -
https://wiki.wxpython.org/TitleIndex
https://discuss.wxpython.org/t/a-wx-datepickerctrl-with-a-customisable-format/36295
https://discuss.wxpython.org/t/datepickerctrlgeneric/36477/5
Thanks to
J. Healey (aka RolfofSaxony), the wxPython community...
About this page
Date(d/m/y) Person (bot) Comments :
26/11/23 - Ecco (Created page for wxPython Phoenix).
Comments
- blah, blah, blah....