Introduction

wxPython provides an excellent and growing collection of controls, with means for sizing, aligning and otherwise manipulating them.

When one builds an application that populates controls with data from a database one needs to make the controls data-aware, which is to say, one needs to make the controls capable of responding to database state changes and also of responding to requests from the database about whether a database field value should be changed, for instance. At the same time, the database needs to be able to update its content from the controls, at the discretion of the user.

This topic shows:

What Objects are Involved

wxTextCtrl, wxCheckBox, wxDialog, wxLayoutConstraints

Process Overview

* In essence the control mixin associates a datasource with a control, and makes the control a "listener" to the datasource "publisher."

* Datasources mediate between databases and fields.

* The validators generated by the factory provide both TransferToWindow and TransferFromWindow methods that are compatible with the other parts of this little system. The default factory-generates validators include a Validate method that returns True by default, to make it easier to build dialogs that do not insist on validation. In other words, data transfer to and from controls is handled automatically by the default factory-generated validator; true validation can be added later in a development process.

* FieldsInfo is a class that mediates between mappings of information about available fields and other parts of the system. In particular this class returns default field names and validators, etc. Fields can be marked for non-display, or as read-only.

Special Concerns

None of which I can think! :o)

The original version of this recipe was unable to handle controls such as drop-down lists that require additional parameters. See Data-aware Controls with Validators, Note 1

Some controls, such as CalendarCtrl lack the ability to accept a validator parameter. This recipe, together with Data-aware Controls with Validators, Note 2, shows how to make use of some of these controls under this scheme. Rather than using extensions of wxPyValidator, the derived control shown here includes validation code within itself. However, transfer of (what might be edited) data from the control window must be handled specially since these controls lack the ability to honour messages indicating the need to do this.

Code Sample

   1 from wxPython.wx import *
   2 import types
   3 
   4 class DBMixin:
   5     def __init__(self):
   6         self._source = None
   7         self._fieldName = None
   8 
   9     def _setSource(self, _source):
  10         self._source = _source
  11         _source.addListener(self)
  12         self.tell()
  13 
  14     def _getSource(self):
  15         return self._source
  16 
  17     source = property(_getSource, _setSource)
  18 
  19     def _setFieldName(self, _fieldName):
  20         self._fieldName = _fieldName
  21         self.tell()
  22 
  23     def _getFieldName(self):
  24         return self._fieldName
  25 
  26     def _getValue(self):
  27         return self.GetValue()
  28 
  29     def _setValue(self, value):
  30         if value is None:
  31             self.SetValue('')
  32         else:
  33             try:
  34                 self.SetValue(value)
  35             except TypeError:
  36                 self.SetValue(str(value))
  37 
  38     fieldName = property(_getFieldName, _setFieldName)
  39     value = property(_getValue, _setValue)
  40 
  41     def tell(self):
  42         if self._fieldName and self._source:
  43             self.value = self._source.getFieldValue(self._fieldName)
  44 
  45     def changed(self):
  46         return self.value  != self._source.getFieldValue(self._fieldName)
  47 
  48 def DerivedDBControl(wx_control, mixin = DBMixin, tell = None, kwargs = {}):
  49     class result(wx_control, mixin):
  50         def __init__(self, * __args, ** __kwargs):
  51             __kwargs.update(kwargs)
  52             wx_control.__init__(self, * __args, ** __kwargs)
  53             mixin.__init__(self)
  54             self.id = self.GetId()
  55             self.wx_control = wx_control
  56 
  57     if tell:
  58         result.tell = tell
  59     return  result
  60 
  61 DBwxTextCtrl = DerivedDBControl(wxTextCtrl)
  62 DBwxCheckBox = DerivedDBControl(wxCheckBox)
  63 
  64 from wx.calendar import  *
  65 
  66 class CalendarCtrlForDB(wxPanel):
  67     def __init__(self, parent, * args, ** kwargs):
  68         wxPanel.__init__(self, parent, -1)
  69         self.validator = kwargs['validator']
  70         del kwargs['validator']
  71         self.calendar = CalendarCtrl(self, * args, ** kwargs)
  72         self.stateButton = wxButton(self, -1, '')
  73 
  74         lc = wxLayoutConstraints()
  75         lc.left.SameAs(self, wxLeft)
  76         lc.width.AsIs()
  77         lc.height.AsIs()
  78         lc.top.SameAs(self, wxTop)
  79         self.calendar.SetConstraints(lc)
  80 
  81         lc = wxLayoutConstraints()
  82         lc.left.SameAs(self, wxLeft)
  83         lc.width.SameAs(self.calendar, wxWidth)
  84         lc.height.AsIs()
  85         lc.top.SameAs(self.calendar, wxBottom)
  86         self.stateButton.SetConstraints(lc)
  87 
  88         self.SetAutoLayout(True)
  89 
  90         EVT_BUTTON(self, self.stateButton.GetId(), self.OnStateButton)
  91 
  92         self.SetValue(None)
  93         self.SetSize(( -1, self.calendar.GetClientSize()[1] + self.stateButton.GetClientSize()[1]))
  94 
  95     def OnStateButton(self, event = None):
  96         if self.stateButton.GetLabel() == 'Nullify':
  97             self.__value = None
  98         elif self.__value is None:
  99             dateValue = wxDateTime()
 100             dateValue.ParseDate('today')
 101             self.calendar.SetDate(dateValue)
 102             self.__value = dateValue
 103         else:
 104             self.calendar.SetDate(self.__value)
 105         self.UpdateView()
 106         if event : event.Skip()
 107 
 108     def UpdateView(self):
 109         if self.__value is None:
 110             self.calendar.Hide()
 111             self.stateButton.SetLabel('Set')
 112         else:
 113             self.calendar.Show()
 114             self.stateButton.SetLabel('Nullify')
 115 
 116     def SetValue(self, value):
 117         self.__value = None
 118         if value not in ('', None):
 119             dateValue = wxDateTime()
 120             if dateValue.ParseDate(value) is not None:
 121                 self.calendar.SetDate(dateValue)
 122                 self.__value = dateValue
 123         self.UpdateView()
 124 
 125     def GetValue(self):
 126         if self.__value is None:
 127             return ''
 128         else:
 129             return self.calendar.GetDate().FormatISODate()
 130 
 131 
 132     def TransferFromWindow(self):
 133         control = wxPyTypeCast(self.GetWindow(), "wxControl")
 134         control.source.TransferFromWindow(control.fieldName, control.GetValue())
 135         return True
 136 
 137 class MSAccessDataSourceAuxiliary(object):
 138     def MoveFirst(self):
 139         self.table.MoveFirst()
 140         self.tellListenersUpdate()
 141 
 142     def MoveNext(self):
 143         self.table.MoveNext()
 144         self.tellListenersUpdate()
 145 
 146 import copy
 147 
 148 class MySQLDataSourceAuxiliary(object):
 149     class Field:
 150         def __init__(self, Value):
 151             self.Value = Value
 152 
 153     def __init__(self, table, db, keyField, tableName):
 154         self.table = table
 155         self.table.Fields = []
 156         for f in range( len(table.fields)):
 157             if self.table.FetchField(f)[1] == 253:
 158                 self.table.Fields.append(self.Field(''))
 159             else:
 160                 self.table.Fields.append(self.Field(0))
 161         self.db = db
 162         self.keyField = keyField
 163         self.tableName = tableName
 164         cursorValues = []
 165         for r in range(table.RecordCount()):
 166             cursorValues.append(copy.copy(table))
 167             if r + 1 < table.RecordCount():
 168                 table.MoveNext()
 169         self.cursorValues = cursorValues
 170         self.cursorIndex = 0
 171 
 172     def GetNextCircular(self):
 173         row = copy.copy(self.cursorValues[self.cursorIndex]).FetchRow()
 174         newIndex =(self.cursorIndex + 1)% len(self.cursorValues)
 175         self.cursorIndex = newIndex
 176         self.table.Fields = [self.Field(item) for item in row]
 177         self.tellListenersUpdate()
 178 
 179     def Update(self):
 180         valuePairs = []
 181         for f, (fieldName, field) in enumerate(zip(self.FieldNames, self.table.Fields)):
 182             if fieldName == self.keyField:
 183                 keyValue = field.Value
 184                 continue
 185             if field.Value is None : continue
 186             if self.table.FetchField(f)[1] == 253:
 187                 valuePairs.append('%s = "%s"' %(fieldName, field.Value))
 188             else:
 189                 valuePairs.append('%s = %s' %(fieldName, field.Value))
 190         setList = ', '.join(valuePairs)
 191         where_definition = '%s = %s' %(self.keyField, self.table.Fields[self.FieldNames.index(self.keyField)].Value,)
 192         updateCommand = "UPDATE %s SET %s WHERE %s" %(self.tableName, setList, where_definition)
 193         self.db.Execute(updateCommand)
 194         newCursor = db.Execute('select * from %s where %s = %s' %(self.tableName, self.keyField, keyValue))
 195         self.cursorValues [(self.cursorIndex - 1)% len(self.cursorValues)] = newCursor
 196 
 197 def DataSource(mixin = None):
 198     from inspect import isclass
 199 
 200     if mixin is None or not isclass(mixin):
 201         raise "Woops, we need a mixin class, Sport!"
 202 
 203     class DataSourceResult(mixin):
 204         def __init__(self, table, * args):
 205             mixin.__init__(self, table, *args)
 206             self.table = table
 207             self.listeners = []
 208 
 209         def __getattr__(self, attr):
 210             return getattr(self.table, attr)
 211 
 212         def __setattr__(self, attr, value):
 213             if attr == "FieldNames":
 214                 setattr(self, attr, value)
 215                 return
 216             return setattr(self.table, attr, value)
 217 
 218         def addListener(self, listener):
 219             self.listeners.append(listener)
 220 
 221         def anyChanged(self):
 222             return any(listener.changed() for listener in self.listeners)
 223 
 224         def updateFieldValues(self):
 225             for listener in self.listeners:
 226                 self.setFieldValue(listener.fieldName, listener.value)
 227 
 228         def tellListenersUpdate(self):
 229             for listener in self.listeners:
 230                 listener.tell()
 231 
 232         def setFieldValue(self, fieldSelector, value):
 233             if type(fieldSelector) is types.IntType:
 234                 self.Fields[fieldSelector].Value = value
 235             elif type(fieldSelector) is types.StringType:
 236                 self.Fields[self.FieldNames.index(fieldSelector)].Value = value
 237             else:
 238                 raise AttributeError, fieldSelector
 239 
 240         def getFieldValue(self, fieldSelector):
 241             if type(fieldSelector) is types.IntType:
 242                 fieldIndex = fieldSelector
 243             elif type(fieldSelector) is types.StringType:
 244                 fieldIndex = self.FieldNames.index(fieldSelector)
 245             else:
 246                 raise AttributeError, fieldSelector
 247             fieldValue = self.Fields[fieldIndex].Value
 248             return fieldValue
 249 
 250     return DataSourceResult
 251 
 252 class AlwaysTrueValidator(wxPyValidator):
 253     def Validate(self, win = None):
 254         return True
 255 
 256 def DBValidator(validator = AlwaysTrueValidator):
 257     class resultClass(validator):
 258         def Clone(self):
 259             return DBValidator(validator)
 260 
 261         def TransferToWindow(self):
 262             control = wxPyTypeCast(self.GetWindow(), "wxControl")
 263             control.tell()
 264             return True
 265 
 266         def TransferFromWindow(self):
 267             control = wxPyTypeCast(self.GetWindow(), "wxControl")
 268             control.source.TransferFromWindow(control.fieldName, control.GetValue())
 269             return True
 270 
 271     return resultClass()
 272 
 273 class AllDigits(wxPyValidator):
 274     def Validate(self, win):
 275         controlValue = wxPyTypeCast(self.GetWindow(), "wxControl").GetValue()
 276         if controlValue == '' : return True
 277         try:
 278             int(controlValue)
 279             return True
 280         except:
 281             return False
 282 
 283 class PostalCode(wxPyValidator):
 284     def __init__(self):
 285         wxPyValidator.__init__(self)
 286         from re import compile, IGNORECASE
 287         self.regexp = compile(r'^[A-Z]\d[A-Z] ?\d[A-Z]\d$', IGNORECASE)
 288 
 289     def Validate(self, win):
 290         controlValue = wxPyTypeCast(self.GetWindow(), "wxControl").GetValue()
 291         return (controlValue == '') or (self.regexp.match(controlValue) is not None)
 292 
 293 class FieldsInfo:
 294     def display(self, actualFieldName):
 295         return not (actualFieldName in self.rawInfo and
 296                     'NoDisplay' in self.rawInfo[actualFieldName] and
 297                     self.rawInfo[actualFieldName]['NoDisplay'])
 298 
 299     def displayName(self, actualFieldName):
 300         if actualFieldName in self.rawInfo and 'DisplayName' in self.rawInfo[actualFieldName]:
 301             return self.rawInfo[actualFieldName]['DisplayName']
 302         return actualFieldName
 303 
 304     def validator(self, actualFieldName):
 305         result = None
 306         if actualFieldName in self.rawInfo and 'validator' in self.rawInfo[actualFieldName]:
 307             if self.rawInfo[actualFieldName]['validator'] is None:
 308                 return None
 309             else:
 310                 result = DBValidator(self.rawInfo[actualFieldName]['validator'])
 311         if result is None:
 312             result = DBValidator()
 313         return result
 314 
 315     def control(self, actualFieldName):
 316         if actualFieldName in self.rawInfo and 'control' in self.rawInfo[actualFieldName]:
 317             return self.rawInfo[actualFieldName]['control']
 318         return DBwxTextCtrl
 319 
 320     def readOnly(self, actualFieldName):
 321         return actualFieldName in self.rawInfo and \
 322                'readOnly' in self.rawInfo[actualFieldName] and \
 323                self.rawInfo[actualFieldName]['readOnly']
 324 
 325     def fieldsAvailableForDisplay(self, available):
 326         try:
 327             if not self.order:
 328                 return available
 329         except AttributeError:
 330             return available
 331         return [fieldName for fieldName in self.order
 332                 if fieldName in available and self.display(fieldName)]
 333 
 334 class NoFieldsInfo(FieldsInfo):
 335     order = []
 336     rawInfo = {}
 337 
 338 nofieldsinfo = NoFieldsInfo()
 339 
 340 class RecordDialog(wxDialog):
 341     def __init__(self, parent, title, datasource, availableFields, fieldsInfo = nofieldsinfo, size = None, ** kwargs):
 342         if not size:
 343             size =(wxSystemSettings_GetMetric(wxSYS_SCREEN_X)/ 2, -20 + wxSystemSettings_GetMetric(wxSYS_SCREEN_Y))
 344 
 345         wxDialog.__init__(self, parent, -1, title, size = size, ** kwargs)
 346 
 347         self.datasource = datasource
 348         self.fieldsAvailableForDisplay = fieldsInfo.fieldsAvailableForDisplay(availableFields)
 349         self.fieldsInfo = fieldsInfo
 350 
 351         OKbutton = wxButton(self, wxID_OK, '&OK', pos = wxPoint(200, 130))
 352         Cancelbutton = wxButton(self, wxID_CANCEL, '&Cancel', pos = wxPoint(200, 130))
 353 
 354         lc = wxLayoutConstraints()
 355         lc.right.SameAs(self, wxRight, 5)
 356         lc.width.AsIs()
 357         lc.height.AsIs()
 358         lc.top.SameAs(self, wxTop, 5)
 359         OKbutton.SetConstraints(lc)
 360 
 361         lc = wxLayoutConstraints()
 362         lc.right.SameAs(self, wxRight, 5)
 363         lc.width.AsIs()
 364         lc.height.AsIs()
 365         lc.top.SameAs(OKbutton, wxBottom, 5)
 366         Cancelbutton.SetConstraints(lc)
 367 
 368         TC  = wxTextCtrl(self, -1, '')
 369         TCSize = TC.GetSize()
 370         TCDC = wxClientDC(TC)
 371         maxExtent = 0
 372         displayedFieldsCount = 0
 373         for fieldName in self.fieldsAvailableForDisplay:
 374             label = self.fieldsInfo.displayName(fieldName)
 375             if self.fieldsInfo.display(fieldName):
 376                 displayedFieldsCount += 1
 377                 maxExtent = max(maxExtent, TCDC.GetTextExtent(label)[0])
 378         TC.Destroy()
 379 
 380         previousControl = self
 381         self.children = {}
 382         for fieldName in self.fieldsAvailableForDisplay:
 383             TC = self.fieldsInfo.control(fieldName)(self, -1, validator = self.fieldsInfo.validator(fieldName))
 384             self.children[fieldName] = TC
 385             if self.fieldsInfo.readOnly(fieldName): TC.Enable(False)
 386             TC.source = self.datasource
 387             TC.fieldName = fieldName
 388             lc = wxLayoutConstraints()
 389             if previousControl == self:
 390                 lc.top.SameAs(self, wxTop, 5)
 391             else:
 392                 lc.top.SameAs(previousControl, wxBottom, 5)
 393             lc.right.SameAs(Cancelbutton, wxLeft, 5)
 394             lc.left.SameAs(self, wxLeft, 10 + maxExtent)
 395             lc.height.AsIs()
 396             TC.SetConstraints(lc)
 397             labelST = wxStaticText(self, -1, self.fieldsInfo.displayName(fieldName),(-1, -1),(-1, -1), wxALIGN_RIGHT)
 398             lc = wxLayoutConstraints()
 399             lc.centreY.SameAs(TC, wxCentreY)
 400             lc.left.SameAs(self, wxLeft, 5)
 401             lc.right.SameAs(TC, wxLeft, 5)
 402             lc.height.AsIs()
 403             labelST.SetConstraints(lc)
 404             previousControl = TC
 405 
 406 
 407 if __name__ == "__main__":
 408     import adodb
 409     from FieldsInfo import workorderFieldsInfo
 410 
 411     db = adodb.NewADOConnection('mysql')
 412     db.Connect('', '', '', 'FWOTS')
 413     rs = db.Execute('select * from workorders where identifier = 2')
 414 
 415     datasource = DataSource(MySQLDataSourceAuxiliary)(rs, db, 'IDENTIFIER', 'workorders')
 416     datasource.FieldNames = [rs.FetchField(f)[0] for f in range(len(rs.fields))]
 417     datasource.GetNextCircular()
 418 
 419     def TransferFromWindow(fieldName, fieldValue):
 420         newFieldValues[fieldName] = fieldValue
 421     datasource.TransferFromWindow = TransferFromWindow
 422 
 423     app = wxPySimpleApp()
 424 
 425     #~ dialog = RecordDialog(None, "Recipe", datasource, datasource.FieldNames)
 426     #~ newFieldValues = {}
 427     #~ dialog.ShowModal()
 428     #~ print newFieldValues
 429     #~ dialog.Destroy()
 430 
 431     dialog = RecordDialog(None, "Recipe", datasource, datasource.FieldNames, workorderFieldsInfo)
 432     newFieldValues = {}
 433     dialog.ShowModal()
 434 
 435     for fieldName in workorderFieldsInfo.fieldsAvailableForDisplay(datasource.FieldNames):
 436         if workorderFieldsInfo.validator(fieldName) is None:
 437             newFieldValues[fieldName] = dialog.children[fieldName].GetValue()
 438 
 439     for field in newFieldValues:
 440         print field, newFieldValues[field]
 441     dialog.Destroy()
 442 
 443     app.MainLoop()

Comments

I would welcome any comments.--Bill Bell


Data-aware Controls with Validators, demonstrated in a dialog (last edited 2011-04-04 10:24:44 by c-98-210-210-193)

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