Attachment 'datectrl_1.5.py'
Download 1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import sys
5 import wx
6 import wx.combo
7 import wx.calendar
8 from datetime import date as dt
9
10 class DateCtrl(wx.combo.ComboCtrl):
11 INPUT_FORMAT = 0
12 DISPLAY_FORMAT = 1
13
14 def __init__(self, parent, size, pos, input_format, display_format,
15 title, default_to_today, allow_null):
16 wx.combo.ComboCtrl.__init__(self, parent, size=size, pos=pos)
17
18 self.input_format = input_format
19 self.display_format = display_format
20 self.title = title
21 self.default_to_today = default_to_today
22 self.allow_null = allow_null
23
24 self.TextCtrl.Bind(wx.EVT_SET_FOCUS, self.on_got_focus)
25 self.TextCtrl.Bind(wx.EVT_CHAR, self.on_char)
26 self.Bind(wx.EVT_ENTER_WINDOW, self.on_mouse_enter)
27 self.Bind(wx.EVT_LEAVE_WINDOW, self.on_mouse_leave)
28
29 self.nav = False # force navigation after selecting date
30 self.is_valid = True # unlike IsValid(), a blank date can be valid
31 self.current_format = self.DISPLAY_FORMAT
32 self.date = wx.DateTime()
33 self.setup_button() # create a custom button for popup
34 (self.blank_string, self.yr_pos, self.mth_pos, self.day_pos,
35 self.literal_pos) = self.setup_input_format()
36
37 # set up button coords for mouse hit-test
38 self.b_x1 = self.TextRect[2] - 2
39 self.b_y1 = self.TextRect[1] - 1
40 self.b_x2 = self.b_x1 + self.ButtonSize[0] + 3
41 self.b_y2 = self.b_y1 + self.ButtonSize[1] + 1
42 self.on_button = False
43
44 self.timer = wx.Timer(self)
45 self.Bind(wx.EVT_TIMER, self.show_tooltip)
46
47 def on_mouse_enter(self, evt):
48 if self.b_x1 <= evt.X <= self.b_x2:
49 if self.b_y1 <= evt.Y <= self.b_y2:
50 self.on_button = True
51 self.timer.Start(500, oneShot=True)
52 evt.Skip()
53
54 def on_mouse_leave(self, evt):
55 if self.on_button:
56 self.on_button = False
57 self.timer.Stop()
58 evt.Skip()
59
60 def show_tooltip(self, evt):
61 abs_x, abs_y = self.ScreenPosition
62 rect = wx.Rect(abs_x+self.b_x1, abs_y+self.b_y1,
63 self.b_x2-self.b_x1+1, self.b_y2-self.b_y1+1)
64 tip = wx.TipWindow(self, 'Show calendar\n(F4 or space)')
65 # tip will be destroyed when mouse leaves this rect
66 tip.SetBoundingRect(rect)
67
68 def setup_button(self): # copied directly from demo
69 # make a custom bitmap showing "..."
70 bw, bh = 14, 16
71 bmp = wx.EmptyBitmap(bw, bh)
72 dc = wx.MemoryDC(bmp)
73
74 # clear to a specific background colour
75 bgcolor = wx.Colour(255, 254, 255)
76 dc.SetBackground(wx.Brush(bgcolor))
77 dc.Clear()
78
79 # draw the label onto the bitmap
80 label = u'\u2026' # unicode ellipsis
81 font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
82 font.SetWeight(wx.FONTWEIGHT_BOLD)
83 dc.SetFont(font)
84 tw, th = dc.GetTextExtent(label)
85 dc.DrawText(label, (bw-tw)/2, (bw-tw)/2)
86 del dc
87
88 # now apply a mask using the bgcolor
89 bmp.SetMaskColour(bgcolor)
90
91 # and tell the ComboCtrl to use it
92 self.SetButtonBitmaps(bmp, True)
93
94 def setup_input_format(self):
95 """
96 Modify the defined input format to a string where each character
97 represents one character of the input string.
98 Generate and return a blank string to fill in the control.
99 Return positions within the string of yr, mth, day and literals.
100 """
101 format = self.input_format
102 blank_string = format
103
104 yr_pos = format.find('%y')
105 if yr_pos > -1:
106 blank_string = blank_string[:yr_pos]+' '+blank_string[yr_pos+2:]
107 yr_pos = (yr_pos, yr_pos+2)
108 else:
109 yr_pos = format.find('%Y')
110 if yr_pos > -1:
111 blank_string = blank_string[:yr_pos]+' '+blank_string[yr_pos+2:]
112 format = format[:yr_pos+2]+'YY'+format[yr_pos+2:]
113 yr_pos = (yr_pos, yr_pos+4)
114
115 mth_pos = format.find('%m')
116 if mth_pos > -1:
117 blank_string = blank_string[:mth_pos]+' '+blank_string[mth_pos+2:]
118 mth_pos = (mth_pos, mth_pos+2)
119
120 day_pos = format.find('%d')
121 if day_pos > -1:
122 blank_string = blank_string[:day_pos]+' '+blank_string[day_pos+2:]
123 day_pos = (day_pos, day_pos+2)
124
125 literal_pos = [i for (i, ch) in enumerate(blank_string)
126 if blank_string[i] == format[i]]
127
128 return blank_string, yr_pos, mth_pos, day_pos, literal_pos
129
130 # Overridden from ComboCtrl, called when the combo button is clicked
131 def OnButtonClick(self):
132 self.SetFocus() # in case we do not have focus
133 dlg = CalendarDlg(self)
134 dlg.CentreOnScreen()
135 if dlg.ShowModal() == wx.ID_OK:
136 self.date = dlg.cal.Date
137 self.Value = self.date.Format(self.display_format)
138 self.current_format = self.DISPLAY_FORMAT
139 self.nav = True # force navigation to next control
140 dlg.Destroy()
141
142 # Overridden from ComboCtrl to avoid assert since there is no ComboPopup
143 def DoSetPopupControl(self, popup):
144 pass
145
146 def on_got_focus(self, evt):
147 if self.nav: # user has made a selection, so move on
148 self.nav = False
149 wx.CallAfter(self.Navigate)
150 else:
151 text_ctrl = self.TextCtrl
152 if not self.is_valid: # re-focus after error
153 pass # leave Value alone
154 elif self.date.IsValid():
155 text_ctrl.Value = self.date.Format(self.input_format)
156 elif self.default_to_today:
157 self.date = wx.DateTime.Today()
158 text_ctrl.Value = self.date.Format(self.input_format)
159 else:
160 text_ctrl.Value = self.blank_string
161 self.current_format = self.INPUT_FORMAT
162 text_ctrl.InsertionPoint = 0
163 text_ctrl.SetSelection(-1, -1)
164 text_ctrl.pos = 0
165 evt.Skip()
166
167 def convert_to_wx_date(self): # conversion and validation method
168 self.is_valid = True
169
170 value = self.Value
171 if value in (self.blank_string, ''):
172 if self.default_to_today:
173 self.date = wx.DateTime.Today()
174 self.Value = self.date.Format(self.display_format)
175 elif self.allow_null:
176 self.date = wx.DateTime()
177 self.Value = ''
178 else:
179 wx.CallAfter(self.display_error, 'Date is required')
180 return
181
182 if self.current_format == self.DISPLAY_FORMAT: # no validation reqd
183 self.TextCtrl.SetSelection(0, 0)
184 return
185
186 today = dt.today()
187
188 if self.yr_pos == -1: # 'yr' not an element of input_format
189 year = today.year
190 else:
191 year = value[self.yr_pos[0]:self.yr_pos[1]].strip()
192 if year == '':
193 year = today.year
194 elif len(year) == 2:
195 # assume year is in range (today-90) to (today+10)
196 year = int(year) + int(today.year/100)*100
197 if year - today.year > 10:
198 year -= 100
199 elif year - today.year < -90:
200 year += 100
201 else:
202 year = int(year)
203
204 if self.mth_pos == -1: # 'mth' not an element of input_format
205 month = today.month
206 else:
207 month = value[self.mth_pos[0]:self.mth_pos[1]].strip()
208 if month == '':
209 month = today.month
210 else:
211 month = int(month)
212
213 if self.day_pos == -1: # 'day' not an element of input_format
214 day = today.day
215 else:
216 day = value[self.day_pos[0]:self.day_pos[1]].strip()
217 if day == '':
218 day = today.day
219 else:
220 day = int(day)
221
222 try:
223 date = dt(year, month, day) # validate using python datetime
224 except ValueError as error: # gives a meaningful error message
225 wx.CallAfter(self.display_error, error.args[0])
226 else: # date is valid
227 self.date = wx.DateTimeFromDMY(day, month-1, year)
228 self.Value = self.date.Format(self.display_format)
229 self.current_format = self.DISPLAY_FORMAT
230
231 def display_error(self, errmsg):
232 self.is_valid = False
233 self.SetFocus()
234 dlg = wx.MessageDialog(self, errmsg,
235 self.title, wx.OK | wx.ICON_INFORMATION)
236 dlg.ShowModal()
237 dlg.Destroy()
238
239 def on_char(self, evt):
240 text_ctrl = self.TextCtrl
241 code = evt.KeyCode
242 if code in (wx.WXK_SPACE, wx.WXK_F4) and not evt.AltDown():
243 self.OnButtonClick()
244 return
245 max = len(self.blank_string)
246 if code in (wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_HOME, wx.WXK_END):
247 if text_ctrl.Selection == (0, max):
248 text_ctrl.SetSelection(0, 0)
249 if code == wx.WXK_LEFT:
250 if text_ctrl.pos > 0:
251 text_ctrl.pos -= 1
252 while text_ctrl.pos in self.literal_pos:
253 text_ctrl.pos -= 1
254 elif code == wx.WXK_RIGHT:
255 if text_ctrl.pos < max:
256 text_ctrl.pos += 1
257 while text_ctrl.pos in self.literal_pos:
258 text_ctrl.pos += 1
259 elif code == wx.WXK_HOME:
260 text_ctrl.pos = 0
261 elif code == wx.WXK_END:
262 text_ctrl.pos = max
263 text_ctrl.InsertionPoint = text_ctrl.pos
264 return
265 if code in (wx.WXK_BACK, wx.WXK_DELETE):
266 if text_ctrl.Selection == (0, max):
267 text_ctrl.Value = self.blank_string
268 text_ctrl.SetSelection(0, 0)
269 if code == wx.WXK_BACK:
270 if text_ctrl.pos == 0:
271 return
272 text_ctrl.pos -= 1
273 while text_ctrl.pos in self.literal_pos:
274 text_ctrl.pos -= 1
275 elif code == wx.WXK_DELETE:
276 if text_ctrl.pos == max:
277 return
278 curr_val = text_ctrl.Value
279 text_ctrl.Value = curr_val[:text_ctrl.pos]+' '+curr_val[text_ctrl.pos+1:]
280 text_ctrl.InsertionPoint = text_ctrl.pos
281 return
282 if code in (wx.WXK_TAB, wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER) or code > 255:
283 evt.Skip()
284 return
285 if text_ctrl.pos == max:
286 wx.Bell()
287 return
288 ch = chr(code)
289 if ch not in ('0123456789'):
290 wx.Bell()
291 return
292 if text_ctrl.Selection == (0, max):
293 curr_val = self.blank_string
294 else:
295 curr_val = text_ctrl.Value
296 text_ctrl.Value = curr_val[:text_ctrl.pos]+ch+curr_val[text_ctrl.pos+1:]
297 text_ctrl.pos += 1
298 while text_ctrl.pos in self.literal_pos:
299 text_ctrl.pos += 1
300 text_ctrl.InsertionPoint = text_ctrl.pos
301
302 class CalendarDlg(wx.Dialog):
303 def __init__(self, parent):
304
305 wx.Dialog.__init__(self, parent, title=parent.title)
306 panel = wx.Panel(self, -1)
307
308 sizer = wx.BoxSizer(wx.VERTICAL)
309 panel.SetSizer(sizer)
310
311 cal = wx.calendar.CalendarCtrl(panel, date=parent.date)
312
313 if sys.platform != 'win32':
314 # gtk truncates the year - this fixes it
315 w, h = cal.Size
316 cal.Size = (w+25, h)
317 cal.MinSize = cal.Size
318
319 sizer.Add(cal, 0)
320
321 button_sizer = wx.BoxSizer(wx.HORIZONTAL)
322 button_sizer.Add((0, 0), 1)
323 btn_ok = wx.Button(panel, wx.ID_OK)
324 btn_ok.SetDefault()
325 button_sizer.Add(btn_ok, 0, wx.ALL, 2)
326 button_sizer.Add((0, 0), 1)
327 btn_can = wx.Button(panel, wx.ID_CANCEL)
328 button_sizer.Add(btn_can, 0, wx.ALL, 2)
329 button_sizer.Add((0, 0), 1)
330 sizer.Add(button_sizer, 1, wx.EXPAND | wx.ALL, 10)
331 sizer.Fit(panel)
332 self.ClientSize = panel.Size
333
334 cal.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
335 cal.SetFocus()
336 self.cal = cal
337
338 def on_key_down(self, evt):
339 code = evt.KeyCode
340 if code == wx.WXK_TAB:
341 self.cal.Navigate()
342 elif code in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
343 self.EndModal(wx.ID_OK)
344 elif code == wx.WXK_ESCAPE:
345 self.EndModal(wx.ID_CANCEL)
346 else:
347 evt.Skip()
348
349 class Panel(wx.Panel):
350 def __init__(self, parent):
351 wx.Panel.__init__(self, parent, -1)
352
353 wx.StaticText(self, -1, 'Field1', pos=(50, 30))
354 t1 = wx.TextCtrl(self, -1, '', size=(130, -1), pos=(150, 30))
355
356 input_format = '%d-%m-%Y'
357 display_format = '%a %d %b %Y'
358 # display_format = '%d-%m-%Y'
359
360 wx.StaticText(self, -1, 'Invoice date', pos=(50, 80))
361 self.d = DateCtrl(self, size=(130, -1), pos=(150, 80),
362 input_format=input_format, display_format=display_format,
363 title='Invoice date', default_to_today=False, allow_null=False)
364
365 wx.StaticText(self, -1, 'Field3', pos=(50, 130))
366 t3 = wx.TextCtrl(self, -1, '', size=(130, -1), pos=(150, 130))
367
368 t1.Bind(wx.EVT_SET_FOCUS, self.on_t_got_focus)
369 t3.Bind(wx.EVT_SET_FOCUS, self.on_t_got_focus)
370
371 self.first_time = True # don't validate date first time
372 self.SetFocus()
373
374 def on_t_got_focus(self, evt):
375 if self.first_time:
376 self.first_time = False
377 else:
378 self.d.convert_to_wx_date()
379 evt.Skip()
380
381 class Frame(wx.Frame):
382 def __init__(self):
383 wx.Frame.__init__(self, None, -1, "Date Picker Ctrl test", size=(400, 240))
384 panel = Panel(self)
385 self.CentreOnScreen()
386
387 class App(wx.App):
388 def OnInit(self):
389 frame = Frame()
390 frame.Show(True)
391 return True
392
393 app = App(False)
394 app.MainLoop()
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.