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

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

The original bounty from Dr. Horst Herb

I need a grid-like GUI element that

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

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

AppointmentsSchedulingWidget (last edited 2011-12-29 18:05:03 by rrcs-76-79-191-19)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.