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 recipe shows a simple way of using a mixin to make at least two of the more commonly used wxPython controls data-aware.
It also shows how to decorate a COM object representing a Microsoft Access Recordset to co-operate with these decorated controls.
What Objects are Involved
The recipe involves no unusual Python objects, only wxPython controls such as wxTextCtrl and wxButton.
Process Overview
Two main pieces are involved, DerivedDBControl and DBMixin.
DerivedDBControl is a procedure that accepts a wxPython control class such as wxTextCtrl, and arranges to create a class which derives from the wxPython class and class DBMixin, as a default.
DBMixin provides storage for the identity of the source field upon whose contents the control is to operate, along with the data source that persists that field. The data source will usually be a database cursor or similar item. In this example, however, for the sake of simplicity, the data source is just an MS Access table.
In the code sample line,
DBwxTextCtrl=DerivedDBControl(wxTextCtrl)
the class DBwxTextCtrl is derived from wxTextCtrl and DBMixin. This implies that this class has all of the attributes of the original wxPython class. In this code, nameEdit is an instance of the derived class that is created in exactly the same way that one would create an instance of wxTextCtrl. To connect the derived class to a datasourc its two properties, source and fieldName, are set appropriately.
The Update button directs the datasource to interrogate its listeners, ie, the decorated controls, to provide their contents, so that these data can be used to update a database row, which is then put back into the database.
The Next button directs the datasource to select the next row of the database, and to ask each of its listeners to update its contents from the newly selected record.
Concerns
- To find a better abstraction for a "datasource".
- The datasource gives incomplete access to ADO features, and it probably differs from other Python implementations of wrappers for this standard.
Code Sample
As the reader will see, the following is a complete script that illustrates how to use this idea to connect a wxPython control with a simple data "source".
1 from wxPython . wx import *
2
3 class DBMixin :
4
5 def __init__ ( self ):
6
7 self . _source = None
8 self . _fieldName = None
9
10 def __getattr__ ( self, attr ) :
11
12 if attr == 'source' :
13 return self . _getSource ( )
14 elif attr == 'fieldName' :
15 return self . _getFieldName ( )
16 elif attr == 'value':
17 return self . GetValue ( )
18 else :
19 raise AttributeError, attr
20
21 def __setattr__ ( self, attr, value ) :
22
23 if attr == 'source' :
24 self . _setSource ( value )
25 elif attr == 'fieldName' :
26 self . _setFieldName ( value )
27 elif attr == 'value' :
28 self . SetValue ( value )
29 else :
30 self . __dict__ [ attr ] = value
31
32 def _setSource ( self, _source ):
33
34 self . _source = _source
35 _source . addListener ( self )
36 self . tell ( )
37
38 def _getSource ( self ):
39
40 return self . _source
41
42 def _setFieldName ( self, _fieldName ):
43
44 self . _fieldName = _fieldName
45 self . tell ( )
46
47 def _getFieldName ( self ):
48
49 return self . _fieldName
50
51 def tell ( self ):
52
53 if self . _fieldName and self . _source :
54 self . value = self . _source . getFieldValue ( self . _fieldName )
55
56 def changed ( self ):
57
58 return self . value != self . _source . getFieldValue ( self . _fieldName )
59
60 def DerivedDBControl ( wx_control, mixin = DBMixin, tell = None ):
61
62 class result ( wx_control, mixin ):
63
64 def __init__ ( self, *args, **kwargs ):
65
66 wx_control . __init__ ( self, * args, ** kwargs )
67 mixin . __init__ ( self )
68 self . id = self . GetId ( )
69 self . wx_control = wx_control
70
71 if tell:
72 setattr ( result, 'tell', tell )
73 return result
74
75 DBwxTextCtrl = DerivedDBControl ( wxTextCtrl )
76 DBwxCheckBox = DerivedDBControl ( wxCheckBox )
77
78 class DataSource :
79
80 def __init__ ( self, table ) :
81
82 self . __dict__ [ 'table'] = table
83 self . __dict__ [ 'listeners' ] = [ ]
84 self . __dict__ [ 'fieldNames' ] = [ Field . Name for Field in table . Fields ]
85
86 def __getattr__ ( self, attr ) : return getattr ( self . table, attr )
87
88 def __setattr__ ( self, attr, value ) : return setattr ( self . table, attr, value )
89
90 def addListener ( self, listener ):
91
92 self . listeners . append ( listener )
93
94 def anyChanged ( self ) :
95
96 return any ( listener . changed ( ) for listener in self . listeners )
97
98 def updateFieldValues ( self ) :
99
100 for listener in self . listeners :
101 self . setFieldValue ( listener . fieldName, listener . value )
102
103 def tellListenersUpdate ( self ) :
104
105 for listener in self . listeners :
106 listener . tell ( )
107
108 def setFieldValue ( self, fieldSelector, value ) :
109
110 if type ( fieldSelector ) is types . IntType :
111 self . Fields [ fieldSelector ] . Value = value
112 elif type ( fieldSelector ) is types . StringType :
113 self . Fields [ self . fieldNames . index ( fieldSelector ) ] . Value = value
114 else :
115 raise AttributeError, fieldSelector
116
117 def getFieldValue ( self, fieldSelector ):
118
119 if type ( fieldSelector ) is types . IntType :
120 return self . Fields [ fieldSelector ] . Value
121 elif type ( fieldSelector ) is types . StringType :
122 return self . Fields [ self . fieldNames . index ( fieldSelector ) ] . Value
123 else :
124 raise AttributeError, fieldSelector
125
126 def MoveFirst ( self ) :
127
128 self . table . MoveFirst ( )
129 self . tellListenersUpdate ( )
130
131 def MoveLast ( self ) :
132
133 self . table . MoveLast ( )
134 self . tellListenersUpdate ( )
135
136 def MoveNext ( self ) :
137
138 self . table . MoveNext ( )
139 self . tellListenersUpdate ( )
140
141 if __name__ == "__main__":
142
143 class mainWindow ( wxFrame ):
144
145 def __init__ ( self, parent, id, title, datasource ):
146
147 wxFrame . __init__ ( self, parent, -1, title, size = ( 300, 400 ) , style = wxDEFAULT_FRAME_STYLE )
148
149 self . mainPanel = wxPanel ( self, -1 )
150 self . mainPanel . SetAutoLayout ( True )
151
152 self . datasource = datasource
153
154 self . nameEdit = DBwxTextCtrl ( self . mainPanel, -1, "", ( 10, 10 ) )
155 self . nameEdit . source = self . datasource
156 self . nameEdit . fieldName = 'NAME'
157
158 self . addressEdit = DBwxTextCtrl ( self . mainPanel, -1, "", ( 10, 40 ) )
159 self . addressEdit . source = self . datasource
160 self . addressEdit . fieldName = 'ADDRESS'
161
162 self . frenchCB = DBwxCheckBox ( self . mainPanel, -1, "French?", ( 10, 70 ) )
163 self . frenchCB . source = self . datasource
164 self . frenchCB . fieldName = 'FRENCH'
165
166 self . saveButton = wxButton ( self . mainPanel, -1, "Update", ( 210, 10 ) )
167 EVT_BUTTON ( self, self . saveButton . GetId ( ), self . handleUpdate )
168
169 self . changeButton = wxButton ( self . mainPanel, -1, "Next", ( 210, 40 ) )
170 EVT_BUTTON ( self, self . changeButton . GetId ( ), self . handleNext )
171
172 self . Show ( True )
173
174 def handleUpdate ( self, event ):
175
176 if self . datasource . anyChanged ( ):
177 self . datasource . updateFieldValues ( )
178 self . datasource . Update ( )
179
180 def handleNext ( self, event ) :
181
182 if self . datasource . anyChanged ( ) :
183 # ask here whether the user wishes to save the altered record before proceeding
184 # and act accordingly
185 pass
186 if self . datasource . AbsolutePosition < self . datasource . RecordCount :
187 self . datasource . MoveNext ( )
188 else :
189 self . datasource . MoveFirst ( )
190
191 from win32com . client import Dispatch
192 conn = Dispatch ( r'ADODB.Connection' )
193 connString = r'PROVIDER=Microsoft.Jet.OLEDB.4.0;DATA SOURCE=%s' % r'N:\FWOTS\ZODB\vss.mdb'
194 conn.Open ( connString )
195 rs = Dispatch ( r'ADODB.Recordset' )
196 rs.Open ( "[Table]", conn, 1, 3 )
197
198 datasource = DataSource ( rs )
199
200 app = wxPySimpleApp ( )
201
202 frame = mainWindow ( None, -1, "Recipe", datasource )
203 frame . Show ( )
204 app . MainLoop ( )
Comments
Any comments, please contact Bill Bell.