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 \
'readOnly' in self.rawInfo[actualFieldName] and \
self.rawInfo[actualFieldName]['readOnly']
322
323 def fieldsAvailableForDisplay(self, available):
324 try:
325 if not self.order:
326 return available
327 except AttributeError:
328 return available
329 return [fieldName for fieldName in self.order
330 if fieldName in available and self.display(fieldName)]
331
332 class NoFieldsInfo(FieldsInfo):
333 order = []
334 rawInfo = {}
335
336 nofieldsinfo = NoFieldsInfo()
337
338 class RecordDialog(wxDialog):
339 def __init__(self, parent, title, datasource, availableFields, fieldsInfo = nofieldsinfo, size = None, ** kwargs):
340 if not size:
341 size =(wxSystemSettings_GetMetric(wxSYS_SCREEN_X)/ 2, -20 + wxSystemSettings_GetMetric(wxSYS_SCREEN_Y))
342
343 wxDialog.__init__(self, parent, -1, title, size = size, ** kwargs)
344
345 self.datasource = datasource
346 self.fieldsAvailableForDisplay = fieldsInfo.fieldsAvailableForDisplay(availableFields)
347 self.fieldsInfo = fieldsInfo
348
349 OKbutton = wxButton(self, wxID_OK, '&OK', pos = wxPoint(200, 130))
350 Cancelbutton = wxButton(self, wxID_CANCEL, '&Cancel', pos = wxPoint(200, 130))
351
352 lc = wxLayoutConstraints()
353 lc.right.SameAs(self, wxRight, 5)
354 lc.width.AsIs()
355 lc.height.AsIs()
356 lc.top.SameAs(self, wxTop, 5)
357 OKbutton.SetConstraints(lc)
358
359 lc = wxLayoutConstraints()
360 lc.right.SameAs(self, wxRight, 5)
361 lc.width.AsIs()
362 lc.height.AsIs()
363 lc.top.SameAs(OKbutton, wxBottom, 5)
364 Cancelbutton.SetConstraints(lc)
365
366 TC = wxTextCtrl(self, -1, '')
367 TCSize = TC.GetSize()
368 TCDC = wxClientDC(TC)
369 maxExtent = 0
370 displayedFieldsCount = 0
371 for fieldName in self.fieldsAvailableForDisplay:
372 label = self.fieldsInfo.displayName(fieldName)
373 if self.fieldsInfo.display(fieldName):
374 displayedFieldsCount += 1
375 maxExtent = max(maxExtent, TCDC.GetTextExtent(label)[0])
376 TC.Destroy()
377
378 previousControl = self
379 self.children = {}
380 for fieldName in self.fieldsAvailableForDisplay:
381 TC = self.fieldsInfo.control(fieldName)(self, -1, validator = self.fieldsInfo.validator(fieldName))
382 self.children[fieldName] = TC
383 if self.fieldsInfo.readOnly(fieldName): TC.Enable(False)
384 TC.source = self.datasource
385 TC.fieldName = fieldName
386 lc = wxLayoutConstraints()
387 if previousControl == self:
388 lc.top.SameAs(self, wxTop, 5)
389 else:
390 lc.top.SameAs(previousControl, wxBottom, 5)
391 lc.right.SameAs(Cancelbutton, wxLeft, 5)
392 lc.left.SameAs(self, wxLeft, 10 + maxExtent)
393 lc.height.AsIs()
394 TC.SetConstraints(lc)
395 labelST = wxStaticText(self, -1, self.fieldsInfo.displayName(fieldName),(-1, -1),(-1, -1), wxALIGN_RIGHT)
396 lc = wxLayoutConstraints()
397 lc.centreY.SameAs(TC, wxCentreY)
398 lc.left.SameAs(self, wxLeft, 5)
399 lc.right.SameAs(TC, wxLeft, 5)
400 lc.height.AsIs()
401 labelST.SetConstraints(lc)
402 previousControl = TC
403
404
405 if __name__ == "__main__":
406 import adodb
407 from FieldsInfo import workorderFieldsInfo
408
409 db = adodb.NewADOConnection('mysql')
410 db.Connect('', '', '', 'FWOTS')
411 rs = db.Execute('select * from workorders where identifier = 2')
412
413 datasource = DataSource(MySQLDataSourceAuxiliary)(rs, db, 'IDENTIFIER', 'workorders')
414 datasource.FieldNames = [rs.FetchField(f)[0] for f in range(len(rs.fields))]
415 datasource.GetNextCircular()
416
417 def TransferFromWindow(fieldName, fieldValue):
418 newFieldValues[fieldName] = fieldValue
419 datasource.TransferFromWindow = TransferFromWindow
420
421 app = wxPySimpleApp()
422
423 #~ dialog = RecordDialog(None, "Recipe", datasource, datasource.FieldNames)
424 #~ newFieldValues = {}
425 #~ dialog.ShowModal()
426 #~ print newFieldValues
427 #~ dialog.Destroy()
428
429 dialog = RecordDialog(None, "Recipe", datasource, datasource.FieldNames, workorderFieldsInfo)
430 newFieldValues = {}
431 dialog.ShowModal()
432
433 for fieldName in workorderFieldsInfo.fieldsAvailableForDisplay(datasource.FieldNames):
434 if workorderFieldsInfo.validator(fieldName) is None:
435 newFieldValues[fieldName] = dialog.children[fieldName].GetValue()
436
437 for field in newFieldValues:
438 print field, newFieldValues[field]
439 dialog.Destroy()
440
441 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
