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