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:
- a mixin class that makes single controls such as edit boxes data-aware; a factory that creates data-aware controls from wxPython controls using the mixin.
- a factory that generates datasource classes capable of mediating between the data-aware controls and databases.
- sample datasources created using this factory for use with MS Access and MySQL.
- a factory that generates validators that can be used with other elements of this system, and with the state machine-based validators discussed in other recipes in this collection.
- sample validators created using this factory for use in checking Canadian postal codes and integers.
- a class that assigns friendly names to database fields, as well as other information, in co-operation with the other machinery described here.
- a sample wxDialog subclass that illustrates applications of the above.
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
- This looks like a very useful recipe, but the coding style actually makes it very hard for me to read. Having whitespace all over the place breaks up the code too much for me. In case other people are the same way, I've reproduced the same code below with what I find to be a more reasonable whitespace style.
I've replaced the original version with yours. --RonanLamy