callAfter 例子2

Introduction

The original bounty (whose content is farther down) required a scheduling widget for handling appointments in a medical doctor's office. This widget needed to support initially fixed time slots, dragging and dropping of appointments, etc.

 

What Objects/Features are Involved

  • wx.grid
  • wx.lib.newevent
  • custom dragging source based on mouse click and movement

 

Process Overview

Generally speaking, the scheduling widget starts out in a fairly regular manner by hiding the row and column labels and disabling row/column resizing. It uses a custom EVT_SIZE handler to resize the schedule information column and leave 32 pixels to the right empty. On the platforms tested (Windows and Ubuntu), this resulted in no horizontal scrollbars.

The content is initially populated with either fixed-length time slots and empty schedule information, or user-specified time slots and schedule information. Added to this is a slightly-more-than-minimal set of functionality to allow for the intuitive manipulation of schedule information (API spec due to Dr. Horst Herb), along with 'client data' information, which can be used as row ids for database entries or otherwise.

Included are a handful of event bindings for a cell's contents being modified, a cell being clicked on, a cell dragged out (for deletion from a database), or a cell dragged in (for isnertion into a database). Modifying the content with the API or does not cause events to be posted for handling.

The real trick with this widget was getting the grid to not select cells during drag. Some initial implementations used background color tricks to mask the selections, but the current version captures mouse click and drag events to usurp dragging behavior, disabling the underlying 'select all cells that have been dragged over', and which makes implementing drag and drop behavior fairly easy. The method used with the wx.grid can be used on other widgets to offer drag and drop behavior where previously such events weren't possible.

 

Implementation with documentation

 

切换行号显示
   1 
   2 '''
   3 schedule.py
   4 
   5 Version .6
   6 
   7 A wx.grid.Grid-based scheduling widget written by Josiah Carlson for the
   8 GNUMED project (http://www.gnumed.org) and related projects.  This widget and
   9 is licensed under the GNU GPL v. 2.  If you would like an alternately licensed
  10 version, please contact the author via phone, email, IM, or in writing; it is
  11 likely that you will be able to get the widget in a license that you desire.
  12 
  13 josiah.carlson@gmail.com or http://dr-josiah.blogspot.com/
  14 
  15 The base control
  16 ----------------
  17 
  18 The object that you will find of the most use is:
  19 ScheduleGrid(parent, data=None, from_time="8:00", to_time="17:00",
  20              default_timeslot=15)
  21 
  22 If data is a non-empty sequence of (time, appointment) pairs, then the content
  23 of the grid will become that data, and the size of the cells will be scaled
  24 based on the duration of the time slot.  In this case, the from_time and
  25 default_timeslot arguments are ignored, but the to_time argument is not, and
  26 will become a hidden entry that determines the duration of the final slot.
  27 
  28 If data is None or an empty sequence, then from_time and to_time are taken as
  29 'military' time literals, and sufficient time slots to fill the from_time to
  30 to_time slots, with a default duration of default_timeslot minutes.  If
  31 default_time is None, it will use a time slot of 15 minutes, with a random
  32 extra duration of 0, 15, 30, or 45 minutes (0 occurring with probability 1/2,
  33 and each of the others occurring with probability 1/6).  This random time slot
  34 assignment is for visual testing purposes only.
  35 
  36 
  37 Dragging a scheduled item from a control to another control (or itself) will
  38 move the scheduled item.  If the destination is empty, the information will
  39 fill the empty slot.  If the destination has a scheduled item already, it will
  40 split the destination time slot in half (rounding down to the nearest minute),
  41 then fill the newly created empty slot.
  42 
  43 
  44 Useful methods:
  45 
  46 SetSlot(time, text='', id=None, bkcolor=None, fgcolor=None)
  47     Set the slot at time 'time' with text 'text' and set it's client data
  48     to 'id', background colour is set to 'bkcolor' if specified, text
  49     colour to fgcolor if specified.  If a slot with the specified time
  50     'time' does not exist, create it and redraw the widget if necessary.
  51 
  52 SplitSlot(time, minutes=None, text='', id=None, bkcolor=None, fgcolor=None)
  53     If there is a slot that already exists, and it has content, split the
  54     slot so that the new time slot has duration minutes, or in half if
  55     minutes is None.  All other arguments have the same semantics as in
  56     SetSlot(...) .
  57 
  58     The previously existing slot will result in a TextEntered event,
  59     providing the new time for the squeezed slot.  No other "useful
  60     methods" cause a TextEntered event.
  61 
  62 
  63 GetSlot(time)
  64     Returns a dict with the keys time, text, id, bkcolor, fgcolor or None
  65     if that time slot does not exist.
  66 
  67 GetAllSlots()
  68     Returns a list of dicts as specified in GetSlot() in chronologic order
  69     for all slots of this widget.
  70 
  71 ClearSlot(time)
  72     Text and client data of this slot is erased, but slot remains.
  73 
  74 DeleteSlot(time)
  75     Slot is removed from the grid along with text and client data.
  76 
  77 [Get|Set|Clear]ClientData methods
  78     Gets/Sets/Clears per-time slot specified client data, specified as the
  79     'id' argument in SetSlot(), SplitSlot(), GetSlot(), and GetAllSlots().
  80 
  81 
  82 Usable event bindings
  83 ---------------------
  84 
  85 CellClicked and EVT_CELL_CLICKED
  86 
  87 If you use schedulewidget.Bind(EVT_CELL_CLICKED, fcn), whenever a cell is
  88 clicked, you will recieve a CellClicked event.  You can discover the row and
  89 column of the click with evt.row and evt.col respectively, the time of the
  90 scheduled item evt.time, and the item text itself with evt.text .
  91 
  92 If you have set the menu items with .SetPopup(), you will not recieve this
  93 event when the right mouse button is clicked.
  94 
  95 TextEntered and EVT_TEXT_ENTERED
  96 
  97 If you use schedulewidget.Bind(EVT_TEXT_ENTERED, fcn), whenever the content
  98 of a row's 'appointment' has been changed, either by the user changing the
  99 content by keyboard, or by a squeezed item being cleared for widget to itself
 100 drags, your function will be called with a TextEntered event. You can discover
 101 the row, text, and time of the event with the same attributes as the
 102 CellClicked event.  There is no col attribute.
 103 
 104 DroppedIn and EVT_DROPPED_IN
 105 DroppedOut and EVT_DROPPED_OUT
 106 BadDrop and EVT_BAD_DROP
 107 
 108 Events that are posted when a cell has been dropped into a control, dragged
 109 out of a control, or when a control has gotten bad data from a drop.
 110 
 111 '''
 112 
 113 import random
 114 import time
 115 
 116 import wx
 117 import wx.grid
 118 import wx.lib.newevent
 119 
 120 printevent=0
 121 
 122 dc = wx.DragCopy
 123 dm = wx.DragMove
 124 
 125 TextEntered, EVT_TEXT_ENTERED = wx.lib.newevent.NewEvent()
 126 CellClicked, EVT_CELL_CLICKED = wx.lib.newevent.NewEvent()
 127 DroppedIn, EVT_DROPPED_IN = wx.lib.newevent.NewEvent()
 128 DroppedOut, EVT_DROPPED_OUT = wx.lib.newevent.NewEvent()
 129 BadDrop, EVT_BAD_DROP = wx.lib.newevent.NewEvent()
 130 
 131 tp_to_name = {TextEntered:'Entered',
 132               CellClicked:'Clicked',
 133               DroppedIn:'Dropped In',
 134               DroppedOut:'Dropped Out',
 135               BadDrop:'Bad Drop'
 136               }
 137 
 138 tt = "%02i:%02i"
 139 def gethm(t):
 140     h,m = [int(i.lstrip('0') or '0') for i in t.split(':')]
 141     return h,m
 142 
 143 def cnt():
 144     i = 0
 145     while 1:
 146         yield i
 147         i += 1
 148 
 149 _counter = cnt()
 150 def timeiter(st, en, incr):
 151     if incr is None:
 152         incr = 15
 153         rr = lambda : random.choice((0, 0, 0, 15, 30, 45))
 154         nx = lambda : str(_counter.next())
 155     else:
 156         rr = lambda : 0
 157         nx = lambda : ''
 158     hs, ms = gethm(st)
 159     he, me = gethm(en)
 160     while (hs, ms) < (he, me):
 161         yield tt%(hs, ms), nx()
 162         ms += incr + rr()
 163         hs += ms//60
 164         ms %= 60
 165 
 166 def timediff(t1, t2):
 167     h2, m2 = gethm(t2)
 168     h1, m1 = gethm(t1)
 169     h2 -= h1
 170     m2 -= m1
 171     m2 += 60*h2
 172     return m2
 173 
 174 def addtime(t1, delta):
 175     h1, m1 = gethm(t1)
 176     m1 += delta
 177     h1 += m1//60
 178     m1 %= 60
 179     return tt%(h1, m1)
 180 
 181 def timetoint(t):
 182     h,m=gethm(t)
 183     return h*60+m
 184 
 185 minh = 17
 186 rightborder = 32
 187 dragsource = None
 188 
 189 class ScheduleDrop(wx.TextDropTarget):
 190     def __init__(self, window):
 191         wx.TextDropTarget.__init__(self)
 192         self.window = window
 193         self.d = None
 194 
 195     def OnDropText(self, x, y, text):
 196         try:
 197             data = eval(text)
 198         except:
 199             to = min(max(self.window.YToRow(y), 1), self.window.GetNumberRows()-2)
 200             wx.PostEvent(self.window, BadDrop(text=text, dest=to))
 201         else:
 202             self.window._dropped(y, data)
 203 
 204     def OnDragOver(self, x, y, d):
 205         self.d = d
 206         to = min(max(self.window.YToRow(y), 1), self.window.GetNumberRows()-2)
 207         if self.window[to,1]:
 208             return dc
 209         return dm
 210 
 211 class ScheduleGrid(wx.grid.Grid):
 212     def __init__(self, parent, data=None, from_time="8:00", to_time="17:00", default_timeslot=15):
 213         wx.grid.Grid.__init__(self, parent, -1)
 214         start, end, step = from_time, to_time, default_timeslot
 215         if data is None:
 216             times = list(timeiter(start, end, step))
 217         else:
 218             times = data
 219 
 220         self.SetDropTarget(ScheduleDrop(self))
 221 
 222         self.lasttime = addtime(end, 0)
 223         self.CreateGrid(len(times)+2, 2)
 224         global minh
 225         minh = self.GetRowSize(0)
 226 
 227         self.SetRowMinimalAcceptableHeight(0)
 228         self.SetColLabelSize(0)
 229         self.SetRowLabelSize(0)
 230         self.DisableDragColSize()
 231         self.DisableDragRowSize()
 232         self.Bind(wx.EVT_SIZE, self.OnSize)
 233         self.SetSelectionMode(1)
 234 
 235         self.SetReadOnly(0, 0, 1)
 236         for i,(j,k) in enumerate(times):
 237             i += 1
 238             self.SetCellValue(i, 0, j)
 239             self.SetReadOnly(i, 0, 1)
 240             self.SetCellValue(i, 1, k)
 241             self.SetCellRenderer(i, 1, WrappingRenderer())
 242             self.SetCellEditor(i, 1, WrappingEditor())
 243         i += 1
 244         self.SetReadOnly(i, 0, 1)
 245         self.SetCellValue(i, 0, self.lasttime)
 246 
 247         self.fixh()
 248 
 249         self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self._leftclick)
 250         self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self._rightclick_menu_handler)
 251         self.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN, self.OnShowEdit)
 252         self.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self.OnHideEdit)
 253         self.Bind(wx.grid.EVT_GRID_EDITOR_CREATED, self.OnCreateEdit)
 254         self.GetGridWindow().Bind(wx.EVT_MOTION, self._checkmouse)
 255         self.GetGridWindow().Bind(wx.EVT_LEFT_DOWN, self._checkmouse2)
 256         self.GetGridWindow().Bind(wx.EVT_LEFT_UP, self._checkmouse3)
 257 
 258 
 259         self.Bind(EVT_TEXT_ENTERED, self._handler)
 260         self.Bind(EVT_CELL_CLICKED, self._handler)
 261         self.Bind(EVT_DROPPED_IN, self._handler)
 262         self.Bind(EVT_DROPPED_OUT, self._handler)
 263 
 264         self.selected = 0
 265         self.dragging = 0
 266         self.evtseen = None
 267         self.dragstartok = 0
 268         self.lastpos = None
 269         self.clientdata = {}
 270         self.menu = None
 271         self.ids = []
 272         self.AutoSizeColumn(0, 0)
 273         self.p = wx.Panel(self, -1)
 274         self.p.Hide()
 275 
 276     def YToRow(self, y):
 277         _, y = self.CalcUnscrolledPosition(0, y)
 278         return wx.grid.Grid.YToRow(self, y)
 279 
 280     def _handler(self, evt):
 281         if printevent:
 282             print "[%s] %s"%(time.asctime(), tp_to_name[type(evt)])
 283         evt.Skip()
 284 
 285     def _checkmouse(self, evt):
 286         ## print dir(evt)
 287         if dragsource or self.dragging or not self.dragstartok or not evt.Dragging():
 288             return
 289         self._startdrag(evt.GetY())
 290 
 291     def _checkmouse2(self, evt):
 292         self.dragstartok = 1
 293         evt.Skip()
 294 
 295     def _checkmouse3(self, evt):
 296         self.dragstartok = 0
 297         evt.Skip()
 298 
 299     def _dropped(self, y, data):
 300         to = min(max(self.YToRow(y), 1), self.GetNumberRows()-2)
 301         time = self[to,0]
 302         wx.CallAfter(self.SplitSlot, time, **data)
 303         wx.CallAfter(wx.PostEvent, self, DroppedIn(time=time, **data))
 304 
 305     def _getduration(self, row):
 306         if row < self.GetNumberRows():
 307             return timediff(self[row,0], self[row+1,0])
 308         return timediff(self[row,0], self.lasttime)
 309 
 310     def __getitem__(self, key):
 311         if type(key) is tuple:
 312             row, col = key
 313             if row < 0:
 314                 row += self.GetNumberRows()
 315             if row >= 0:
 316                 return self.GetCellValue(row, col)
 317         raise KeyError("key must be a tuple of length 2")
 318 
 319     def __setitem__(self, key, value):
 320         if type(key) is tuple:
 321             row, col = key
 322             if row < 0:
 323                 row += self.GetNumberRows()
 324             if row >= 0:
 325                 return self.SetCellValue(row, col, value)
 326         raise KeyError("key must be a tuple of length 2")
 327 
 328     def _leftclick(self, evt):
 329         sel = evt.GetRow()
 330 
 331         if not self.GetGridCursorCol():
 332             self.MoveCursorRight(0)
 333         gr = self.GetGridCursorRow()
 334         if gr < sel:
 335             for i in xrange(sel-gr):
 336                 self.MoveCursorDown(0)
 337         else:
 338             for i in xrange(gr-sel):
 339                 self.MoveCursorUp(0)
 340         wx.PostEvent(self, CellClicked(row=sel, col=evt.GetCol(), **self._getdict(sel)))
 341         evt.Skip()
 342 
 343     def _rightclick_menu_handler(self, evt):
 344         sel = evt.GetRow()
 345         if not self.menu:
 346             wx.PostEvent(self, CellClicked(row=sel, col=evt.GetCol(), time=self[sel,0], text=self[sel,1]))
 347             return
 348 
 349         time, text = self[sel,0], self[sel,1]
 350         cdata = self.GetClientData(time)
 351 
 352         evt.Skip()
 353 
 354         menu = wx.Menu()
 355         def item((name, fcn)):
 356             def f(evt):
 357                 return fcn(time, text, cdata)
 358             id = wx.NewId()
 359             it = wx.MenuItem(menu, id, name)
 360             menu.AppendItem(it)
 361             self.Bind(wx.EVT_MENU, f, it)
 362             return id
 363         clear = map(item, self.menu)
 364         self.PopupMenu(menu)
 365         menu.Destroy()
 366 
 367     def SetPopup(self, menulist):
 368         self.menu = menulist
 369 
 370     def OnShowEdit(self, evt):
 371         x = self.GetCellEditor(evt.GetRow(), evt.GetCol()).GetControl()
 372         if x:
 373             wx.CallAfter(x.SetInsertionPointEnd)
 374         evt.Skip()
 375 
 376     def OnCreateEdit(self, evt):
 377         x = evt.GetControl()
 378         wx.CallAfter(x.SetInsertionPointEnd)
 379         evt.Skip()
 380 
 381     def OnHideEdit(self, evt):
 382         row = evt.GetRow()
 383         wx.CallAfter(self.OnDoneEdit, self[row,1], row)
 384         evt.Skip()
 385 
 386     def OnDoneEdit(self, old, row):
 387         check = (row, old, self[row,1])
 388         if old != check[-1] and self.evtseen != check:
 389             wx.PostEvent(self, TextEntered(**self._getdict(row)))
 390             if not self.GetGridCursorCol():
 391                 self.MoveCursorRight(0)
 392             for i in xrange(self.GetGridCursorRow()-row):
 393                 self.MoveCursorUp(0)
 394         self.evtseen = check
 395 
 396     def OnSize(self, evt):
 397         x,y = evt.GetSize()
 398         c1 = self.GetColSize(0)
 399         x -= c1
 400         x -= rightborder #otherwise it creates an unnecessary horizontal scroll bar
 401         if x > 20:
 402             self.SetColSize(1, x)
 403 
 404         evt.Skip()
 405 
 406     def fixh(self):
 407         self.SetRowSize(0, 0)
 408         self.SetRowSize(self.GetNumberRows()-1, 0)
 409         for rown in xrange(1, self.GetNumberRows()-1):
 410             td = max(self._getduration(rown), minh)
 411             self.SetRowSize(rown, td)
 412         self.ForceRefresh()
 413 
 414     def _startdrag(self, y):
 415         global dragsource
 416         if dragsource or self.dragging:
 417             return
 418 
 419         self.dragging = 1
 420         dragsource = self
 421 
 422         try:
 423 
 424             row = self.YToRow(y)
 425             if row < 1 or row >= self.GetNumberRows()-1:
 426                 return
 427 
 428             data = row, self._getduration(row), self[row,1]
 429 
 430             time = self[row,0]
 431             data = self._getdict(row)
 432             dcpy = dict(data)
 433             data.pop('time', None)
 434             datar = repr(data)
 435 
 436             d_data = wx.TextDataObject()
 437             d_data.SetText(datar)
 438 
 439             dropSource = wx.DropSource(self)
 440             dropSource.SetData(d_data)
 441             result = dropSource.DoDragDrop(wx.Drag_AllowMove)
 442             if result in (dc, dm):
 443                 self.ClearSlot(time)
 444                 wx.CallAfter(wx.PostEvent, self, DroppedOut(**dcpy))
 445             wx.CallAfter(self.SelectRow, row)
 446             wx.CallAfter(self.ForceRefresh)
 447         finally:
 448             dragsource = None
 449             self.dragging = 0
 450 
 451     def _findslot(self, time):
 452         t = timetoint(time)
 453         for i in xrange(1, self.GetNumberRows()-1):
 454             tt = timetoint(self[i,0])
 455             if tt >= t:
 456                 break
 457         return i, t==tt
 458 
 459     def SetSlot(self, time, text='', id=None, bkcolor=None, fgcolor=None):
 460         '''
 461         Set the slot at time 'time' with text 'text' and set it's client data
 462         to 'id', background colour is set to 'bkcolor' if specified, text
 463         colour to fgcolor if specified.  If a slot with the specified time
 464         'time' does not exist, create it and redraw the widget if necessary.
 465         '''
 466 
 467         i, exact = self._findslot(time)
 468         if not exact:
 469             self.InsertRows(i, 1)
 470             self[i,0] = time
 471         self[i,1] = text
 472         if id:
 473             self.SetClientData(time, id)
 474         if bkcolor:
 475             self.SetCellBackgroundColour(i, 0, bkcolor)
 476             self.SetCellBackgroundColour(i, 1, bkcolor)
 477         if fgcolor:
 478             self.SetCellTextColour(i, 0, fgcolor)
 479             self.SetCellTextColour(i, 1, fgcolor)
 480         if not exact:
 481             self.fixh()
 482         if not self.GetGridCursorCol():
 483             self.MoveCursorRight(0)
 484         for j in xrange(self.GetGridCursorRow()-i):
 485             self.MoveCursorUp(0)
 486         for j in xrange(i-self.GetGridCursorRow()):
 487             self.MoveCursorDown(0)
 488         wx.CallAfter(self.ClearSelection)
 489         wx.CallAfter(self.SelectRow, i)
 490         wx.CallAfter(self.ForceRefresh)
 491 
 492     def GetSlot(self, time):
 493         '''
 494         Returns a dict with the keys time, text, id, bkcolor, fgcolor or None
 495         if that time slot does not exist.
 496         '''
 497 
 498         i, exact = self._findslot(time)
 499         if not exact:
 500             return None
 501         return self._getdict(i)
 502 
 503     def _getdict(self, i):
 504         return dict(time=self[i,0], text=self[i,1],
 505                     id=self.GetClientData(self[i,0]),
 506                     bkcolor=self.GetCellBackgroundColour(i,0),
 507                     fgcolor=self.GetCellTextColour(i,0))
 508 
 509     def GetAllSlots(self):
 510         '''
 511         Returns a list of dicts as specified in GetSlot() in chronologic order
 512         for all slots of this widget.
 513         '''
 514 
 515         ret = []
 516         for i in xrange(1, self.GetNumberRows()-1):
 517             ret.append(self._getdict(i))
 518         return ret
 519 
 520     def ClearSlot(self, time):
 521         '''
 522         Text and client data of this slot is erased, but slot remains.
 523         '''
 524 
 525         i, exact = self._findslot(time)
 526         if not exact:
 527             return
 528         self[i,1] = ''
 529         self.ClearClientData(self[i,0])
 530         #do we clear background and foreground colors?
 531 
 532     def DeleteSlot(self, time):
 533         '''
 534         Slot is removed from the grid along with text and client data.
 535         '''
 536 
 537         i, exact = self._findslot(time)
 538         if not exact:
 539             return
 540         self.ClearClientData(self[i,0])
 541         self.DeleteRows(i, 1)
 542         self.fixh()
 543 
 544     def SplitSlot(self, time, minutes=None, text='', id=None, bkcolor=None, fgcolor=None):
 545         '''
 546         If there is a slot that already exists, and it has content, split the
 547         slot so that the new time slot has duration minutes, or in half if
 548         minutes is None.  All other arguments have the same semantics as in
 549         SetSlot(...) .
 550 
 551         The previously existing slot will result in a TextEntered event,
 552         providing the new time for the squeezed slot.  No other "useful
 553         methods" cause a TextEntered event.
 554         '''
 555 
 556         i, exact = self._findslot(time)
 557         if exact and self[i,1]:
 558             if not minutes:
 559                 minutes = self._getduration(i)//2
 560             cd = self.GetClientData(self[i,0])
 561             self.ClearClientData(self[i,0])
 562             nt = addtime(self[i,0], minutes)
 563             self[i,0] = nt
 564             self.SetClientData(nt, cd)
 565             wx.PostEvent(self, TextEntered(**self._getdict(i)))
 566         self.SetSlot(time, text, id, bkcolor, fgcolor)
 567 
 568     def GetClientData(self, time):
 569         return self.clientdata.get(timetoint(time), None)
 570 
 571     def SetClientData(self, time, data):
 572         if data != None:
 573             self.clientdata[timetoint(time)] = data
 574 
 575     def ClearClientData(self, time):
 576         self.clientdata.pop(timetoint(time), None)
 577 
 578 WrappingRenderer = wx.grid.GridCellAutoWrapStringRenderer
 579 WrappingEditor = wx.grid.GridCellAutoWrapStringEditor
 580 
 581 def pr(*args):
 582     print args
 583 
 584 if __name__ == '__main__':
 585     printevent = 1
 586     a = wx.App(0)
 587     b = wx.Frame(None)
 588     p = wx.Panel(b)
 589     ## op = wx.Panel(p)
 590     s = wx.BoxSizer(wx.HORIZONTAL)
 591     c = ScheduleGrid(p, default_timeslot=None)
 592     d = ScheduleGrid(p, default_timeslot=None)
 593 
 594     lst = [('print information', pr)]
 595 
 596     c.SetPopup(lst)
 597 
 598     s.Add(c, 1, wx.EXPAND)
 599     ## s.Add(op, 1, wx.EXPAND)
 600     s.Add(d, 1, wx.EXPAND)
 601     p.SetSizer(s)
 602     b.Show(1)
 603     a.MainLoop()

 

The original bounty from Dr. Horst Herb

I need a grid-like GUI element that

  • has a set start end end time
  • has slots in set time increments (e.g. 10 minutes each slot) but allows other time spans for each individual slot programmatically
  • allows to "squeeze in" slots, diminishing the size of the squeezed slot
  • displays the slots in a height proportional to their allocated time span
  • allows to drag slots somewhere else, with (=shifting) or without (=squeezing in) reallocating the times for all later slots

Initialization:

init( ..., from_time='08:00', to_time='19:00', default_timeslot=15, slot_width=150) where slot_width is the initial width in pixels

the widget is painted like this:

 

|^^^^^^^^^^^^^^^^^^^|  <- if the user clicks here, the thing scrolls to earlier times
|08:00| (some text) |
|08:15|             |
|08:30|             |
...
|"""""""""""""""""""| <- if the user clicks here, the thing scrolls to later
  • the minimum height of the cell is determined by the font size
  • if total height of from_time to to_time exceeds displayed height, a scroll bar appears to the right side
  • the space to the right of the displayed time is a text control like widget, allowing immediate text entry when getting the focus, end emitting a "TEXT_ENTERED" event with time and text content if the content was changed and the focus is lost
  • right and left clicking any cell emits an event with both the time of the clicked cell and the cell text content as event parameter

I offer a bounty of $150 for this. If you need more details, please contact me. I'd imagine this won't be too difficult using the fantastic wxGrid widget

 

Comments

If you have any questions, please feel free to contact the author, whose information is available in his profile profile. You may also be able to use the widget soon in GNUmed.

Note for wxPython < 2.9, this code needs to be changed to associate the drop target with windows within the grid, e.g.self.GetGridWindow().SetDropTarget(ScheduleDrop(self))rather than self.SetDropTarget(ScheduleDrop(self)) to be portable (see discussion).

Brian

posted @ 2013-05-22 23:56  dengyigod  阅读(317)  评论(0编辑  收藏  举报