]>
Commit | Line | Data |
---|---|---|
1fded56b RD |
1 | from wxPython.wx import * |
2 | from wxPython.grid import * | |
3 | import images | |
4 | ||
5 | class MegaTable(wxPyGridTableBase): | |
6 | """ | |
7 | A custom wxGrid Table using user supplied data | |
8 | """ | |
9 | def __init__(self, data, colnames, plugins): | |
10 | """data is a list of the form | |
11 | [(rowname, dictionary), | |
12 | dictionary.get(colname, None) returns the data for column | |
13 | colname | |
14 | """ | |
15 | # The base class must be initialized *first* | |
16 | wxPyGridTableBase.__init__(self) | |
17 | self.data = data | |
18 | self.colnames = colnames | |
19 | self.plugins = plugins or {} | |
20 | # XXX | |
21 | # we need to store the row length and collength to | |
22 | # see if the table has changed size | |
23 | self._rows = self.GetNumberRows() | |
24 | self._cols = self.GetNumberCols() | |
25 | ||
26 | def GetNumberCols(self): | |
27 | return len(self.colnames) | |
28 | ||
29 | def GetNumberRows(self): | |
30 | return len(self.data) | |
31 | ||
32 | def GetColLabelValue(self, col): | |
33 | return self.colnames[col] | |
34 | ||
35 | def GetRowLabelValues(self, row): | |
36 | return self.data[row][0] | |
37 | ||
38 | def GetValue(self, row, col): | |
39 | return str(self.data[row][1].get(self.GetColLabelValue(col), "")) | |
40 | ||
41 | def GetRawValue(self, row, col): | |
42 | return self.data[row][1].get(self.GetColLabelValue(col), "") | |
43 | ||
44 | def SetValue(self, row, col, value): | |
45 | self.data[row][1][self.GetColLabelValue(col)] = value | |
46 | ||
47 | def ResetView(self, grid): | |
48 | """ | |
49 | (wxGrid) -> Reset the grid view. Call this to | |
50 | update the grid if rows and columns have been added or deleted | |
51 | """ | |
52 | grid.BeginBatch() | |
53 | for current, new, delmsg, addmsg in [ | |
54 | (self._rows, self.GetNumberRows(), wxGRIDTABLE_NOTIFY_ROWS_DELETED, wxGRIDTABLE_NOTIFY_ROWS_APPENDED), | |
55 | (self._cols, self.GetNumberCols(), wxGRIDTABLE_NOTIFY_COLS_DELETED, wxGRIDTABLE_NOTIFY_COLS_APPENDED), | |
56 | ]: | |
57 | if new < current: | |
58 | msg = wxGridTableMessage(self,delmsg,new,current-new) | |
59 | grid.ProcessTableMessage(msg) | |
60 | elif new > current: | |
61 | msg = wxGridTableMessage(self,addmsg,new-current) | |
62 | grid.ProcessTableMessage(msg) | |
63 | self.UpdateValues(grid) | |
64 | grid.EndBatch() | |
65 | ||
66 | self._rows = self.GetNumberRows() | |
67 | self._cols = self.GetNumberCols() | |
68 | # update the column rendering plugins | |
69 | self._updateColAttrs(grid) | |
70 | ||
71 | # update the scrollbars and the displayed part of the grid | |
72 | grid.AdjustScrollbars() | |
73 | grid.ForceRefresh() | |
74 | ||
75 | ||
76 | def UpdateValues(self, grid): | |
77 | """Update all displayed values""" | |
78 | # This sends an event to the grid table to update all of the values | |
79 | msg = wxGridTableMessage(self, wxGRIDTABLE_REQUEST_VIEW_GET_VALUES) | |
80 | grid.ProcessTableMessage(msg) | |
81 | ||
82 | def _updateColAttrs(self, grid): | |
83 | """ | |
84 | wxGrid -> update the column attributes to add the | |
85 | appropriate renderer given the column name. (renderers | |
86 | are stored in the self.plugins dictionary) | |
87 | ||
88 | Otherwise default to the default renderer. | |
89 | """ | |
90 | col = 0 | |
91 | for colname in self.colnames: | |
92 | attr = wxGridCellAttr() | |
93 | if colname in self.plugins: | |
94 | renderer = self.plugins[colname](self) | |
95 | if renderer.colSize: | |
96 | grid.SetColSize(col, renderer.colSize) | |
97 | if renderer.rowSize: | |
98 | grid.SetDefaultRowSize(renderer.rowSize) | |
d14a1e28 | 99 | attr.SetReadOnly(True) |
1fded56b RD |
100 | attr.SetRenderer(renderer) |
101 | grid.SetColAttr(col, attr) | |
102 | col += 1 | |
103 | ||
104 | # ------------------------------------------------------ | |
105 | # begin the added code to manipulate the table (non wx related) | |
106 | def AppendRow(self, row): | |
107 | entry = {} | |
108 | for name in self.colnames: | |
109 | entry[name] = "Appended_%i"%row | |
110 | # XXX Hack | |
111 | # entry["A"] can only be between 1..4 | |
112 | entry["A"] = random.choice(range(4)) | |
113 | self.data.insert(row, ["Append_%i"%row, entry]) | |
114 | ||
115 | def DeleteCols(self, cols): | |
116 | """ | |
117 | cols -> delete the columns from the dataset | |
118 | cols hold the column indices | |
119 | """ | |
120 | # we'll cheat here and just remove the name from the | |
121 | # list of column names. The data will remain but | |
122 | # it won't be shown | |
123 | deleteCount = 0 | |
124 | cols = cols[:] | |
125 | cols.sort() | |
126 | for i in cols: | |
127 | self.colnames.pop(i-deleteCount) | |
128 | # we need to advance the delete count | |
129 | # to make sure we delete the right columns | |
130 | deleteCount += 1 | |
131 | if not len(self.colnames): | |
132 | self.data = [] | |
133 | ||
134 | def DeleteRows(self, rows): | |
135 | """ | |
136 | rows -> delete the rows from the dataset | |
137 | rows hold the row indices | |
138 | """ | |
139 | deleteCount = 0 | |
140 | rows = rows[:] | |
141 | rows.sort() | |
142 | for i in rows: | |
143 | self.data.pop(i-deleteCount) | |
144 | # we need to advance the delete count | |
145 | # to make sure we delete the right rows | |
146 | deleteCount += 1 | |
147 | ||
148 | def SortColumn(self, col): | |
149 | """ | |
150 | col -> sort the data based on the column indexed by col | |
151 | """ | |
152 | name = self.colnames[col] | |
153 | _data = [] | |
154 | for row in self.data: | |
155 | rowname, entry = row | |
156 | _data.append((entry.get(name, None), row)) | |
157 | ||
158 | _data.sort() | |
159 | self.data = [] | |
160 | for sortvalue, row in _data: | |
161 | self.data.append(row) | |
162 | ||
163 | # end table manipulation code | |
164 | # ---------------------------------------------------------- | |
165 | ||
166 | ||
167 | # -------------------------------------------------------------------- | |
168 | # Sample wxGrid renderers | |
169 | ||
170 | class MegaImageRenderer(wxPyGridCellRenderer): | |
171 | def __init__(self, table): | |
172 | """ | |
173 | Image Renderer Test. This just places an image in a cell | |
174 | based on the row index. There are N choices and the | |
175 | choice is made by choice[row%N] | |
176 | """ | |
177 | wxPyGridCellRenderer.__init__(self) | |
178 | self.table = table | |
179 | self._choices = [images.getSmilesBitmap, | |
180 | images.getMondrianBitmap, | |
181 | images.get_10s_Bitmap, | |
182 | images.get_01c_Bitmap] | |
183 | ||
184 | ||
185 | self.colSize = None | |
186 | self.rowSize = None | |
187 | ||
188 | def Draw(self, grid, attr, dc, rect, row, col, isSelected): | |
189 | choice = self.table.GetRawValue(row, col) | |
190 | bmp = self._choices[ choice % len(self._choices)]() | |
191 | image = wxMemoryDC() | |
192 | image.SelectObject(bmp) | |
193 | ||
194 | # clear the background | |
195 | dc.SetBackgroundMode(wxSOLID) | |
196 | if isSelected: | |
197 | dc.SetBrush(wxBrush(wxBLUE, wxSOLID)) | |
198 | dc.SetPen(wxPen(wxBLUE, 1, wxSOLID)) | |
199 | else: | |
200 | dc.SetBrush(wxBrush(wxWHITE, wxSOLID)) | |
201 | dc.SetPen(wxPen(wxWHITE, 1, wxSOLID)) | |
202 | dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) | |
203 | ||
204 | # copy the image but only to the size of the grid cell | |
205 | width, height = bmp.GetWidth(), bmp.GetHeight() | |
206 | if width > rect.width-2: | |
207 | width = rect.width-2 | |
208 | ||
209 | if height > rect.height-2: | |
210 | height = rect.height-2 | |
211 | ||
212 | dc.Blit(rect.x+1, rect.y+1, width, height, | |
213 | image, | |
214 | 0, 0, wxCOPY, True) | |
215 | ||
216 | ||
217 | class MegaFontRenderer(wxPyGridCellRenderer): | |
218 | def __init__(self, table, color="blue", font="ARIAL", fontsize=8): | |
219 | """Render data in the specified color and font and fontsize""" | |
220 | wxPyGridCellRenderer.__init__(self) | |
221 | self.table = table | |
222 | self.color = color | |
223 | self.font = wxFont(fontsize, wxDEFAULT, wxNORMAL, wxNORMAL, | |
224 | 0, font) | |
225 | self.selectedBrush = wxBrush("blue", | |
226 | wxSOLID) | |
227 | self.normalBrush = wxBrush(wxWHITE, wxSOLID) | |
228 | self.colSize = None | |
229 | self.rowSize = 50 | |
230 | ||
231 | def Draw(self, grid, attr, dc, rect, row, col, isSelected): | |
232 | # Here we draw text in a grid cell using various fonts | |
233 | # and colors. We have to set the clipping region on | |
234 | # the grid's DC, otherwise the text will spill over | |
235 | # to the next cell | |
236 | dc.SetClippingRect(rect) | |
237 | ||
238 | # clear the background | |
239 | dc.SetBackgroundMode(wxSOLID) | |
240 | if isSelected: | |
241 | dc.SetBrush(wxBrush(wxBLUE, wxSOLID)) | |
242 | dc.SetPen(wxPen(wxBLUE, 1, wxSOLID)) | |
243 | else: | |
244 | dc.SetBrush(wxBrush(wxWHITE, wxSOLID)) | |
245 | dc.SetPen(wxPen(wxWHITE, 1, wxSOLID)) | |
246 | dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) | |
247 | ||
248 | text = self.table.GetValue(row, col) | |
249 | dc.SetBackgroundMode(wxSOLID) | |
250 | ||
251 | # change the text background based on whether the grid is selected | |
252 | # or not | |
253 | if isSelected: | |
254 | dc.SetBrush(self.selectedBrush) | |
255 | dc.SetTextBackground("blue") | |
256 | else: | |
257 | dc.SetBrush(self.normalBrush) | |
258 | dc.SetTextBackground("white") | |
259 | ||
260 | dc.SetTextForeground(self.color) | |
261 | dc.SetFont(self.font) | |
262 | dc.DrawText(text, rect.x+1, rect.y+1) | |
263 | ||
264 | # Okay, now for the advanced class :) | |
265 | # Let's add three dots "..." | |
266 | # to indicate that that there is more text to be read | |
267 | # when the text is larger than the grid cell | |
268 | ||
269 | width, height = dc.GetTextExtent(text) | |
270 | if width > rect.width-2: | |
271 | width, height = dc.GetTextExtent("...") | |
272 | x = rect.x+1 + rect.width-2 - width | |
273 | dc.DrawRectangle(x, rect.y+1, width+1, height) | |
274 | dc.DrawText("...", x, rect.y+1) | |
275 | ||
276 | dc.DestroyClippingRegion() | |
277 | ||
278 | ||
279 | # -------------------------------------------------------------------- | |
280 | # Sample Grid using a specialized table and renderers that can | |
281 | # be plugged in based on column names | |
282 | ||
283 | class MegaGrid(wxGrid): | |
284 | def __init__(self, parent, data, colnames, plugins=None): | |
285 | """parent, data, colnames, plugins=None | |
286 | Initialize a grid using the data defined in data and colnames | |
287 | (see MegaTable for a description of the data format) | |
288 | plugins is a dictionary of columnName -> column renderers. | |
289 | """ | |
290 | ||
291 | # The base class must be initialized *first* | |
292 | wxGrid.__init__(self, parent, -1) | |
293 | self._table = MegaTable(data, colnames, plugins) | |
294 | self.SetTable(self._table) | |
295 | self._plugins = plugins | |
296 | ||
297 | EVT_GRID_LABEL_RIGHT_CLICK(self, self.OnLabelRightClicked) | |
298 | ||
299 | def Reset(self): | |
300 | """reset the view based on the data in the table. Call | |
301 | this when rows are added or destroyed""" | |
302 | self._table.ResetView(self) | |
303 | ||
304 | def OnLabelRightClicked(self, evt): | |
305 | # Did we click on a row or a column? | |
306 | row, col = evt.GetRow(), evt.GetCol() | |
307 | if row == -1: self.colPopup(col, evt) | |
308 | elif col == -1: self.rowPopup(row, evt) | |
309 | ||
310 | def rowPopup(self, row, evt): | |
311 | """(row, evt) -> display a popup menu when a row label is right clicked""" | |
312 | appendID = wxNewId() | |
313 | deleteID = wxNewId() | |
314 | x = self.GetRowSize(row)/2 | |
315 | if not self.GetSelectedRows(): | |
316 | self.SelectRow(row) | |
317 | menu = wxMenu() | |
318 | xo, yo = evt.GetPosition() | |
319 | menu.Append(appendID, "Append Row") | |
320 | menu.Append(deleteID, "Delete Row(s)") | |
321 | ||
322 | def append(event, self=self, row=row): | |
323 | self._table.AppendRow(row) | |
324 | self.Reset() | |
325 | ||
326 | def delete(event, self=self, row=row): | |
327 | rows = self.GetSelectedRows() | |
328 | self._table.DeleteRows(rows) | |
329 | self.Reset() | |
330 | ||
331 | EVT_MENU(self, appendID, append) | |
332 | EVT_MENU(self, deleteID, delete) | |
333 | self.PopupMenu(menu, wxPoint(x, yo)) | |
334 | menu.Destroy() | |
335 | ||
336 | def colPopup(self, col, evt): | |
337 | """(col, evt) -> display a popup menu when a column label is | |
338 | right clicked""" | |
339 | x = self.GetColSize(col)/2 | |
340 | menu = wxMenu() | |
341 | id1 = wxNewId() | |
342 | sortID = wxNewId() | |
343 | ||
344 | xo, yo = evt.GetPosition() | |
345 | self.SelectCol(col) | |
346 | cols = self.GetSelectedCols() | |
347 | self.Refresh() | |
348 | menu.Append(id1, "Delete Col(s)") | |
349 | menu.Append(sortID, "Sort Column") | |
350 | ||
351 | def delete(event, self=self, col=col): | |
352 | cols = self.GetSelectedCols() | |
353 | self._table.DeleteCols(cols) | |
354 | self.Reset() | |
355 | ||
356 | def sort(event, self=self, col=col): | |
357 | self._table.SortColumn(col) | |
358 | self.Reset() | |
359 | ||
360 | EVT_MENU(self, id1, delete) | |
361 | if len(cols) == 1: | |
362 | EVT_MENU(self, sortID, sort) | |
363 | self.PopupMenu(menu, wxPoint(xo, 0)) | |
364 | menu.Destroy() | |
365 | ||
366 | # ----------------------------------------------------------------- | |
367 | # Test data | |
368 | # data is in the form | |
369 | # [rowname, dictionary] | |
370 | # where dictionary.get(colname, None) -> returns the value for the cell | |
371 | # | |
372 | # the colname must also be supplied | |
373 | import random | |
374 | colnames = ["Row", "This", "Is", "A", "Test"] | |
375 | ||
376 | data = [] | |
377 | for row in range(1000): | |
378 | d = {} | |
379 | for name in ["This", "Test", "Is"]: | |
380 | d[name] = random.random() | |
381 | d["Row"] = len(data) | |
382 | # XXX | |
383 | # the "A" column can only be between one and 4 | |
384 | d["A"] = random.choice(range(4)) | |
385 | data.append((str(row), d)) | |
386 | ||
387 | class MegaFontRendererFactory: | |
388 | def __init__(self, color, font, fontsize): | |
389 | """ | |
390 | (color, font, fontsize) -> set of a factory to generate | |
391 | renderers when called. | |
392 | func = MegaFontRenderFactory(color, font, fontsize) | |
393 | renderer = func(table) | |
394 | """ | |
395 | self.color = color | |
396 | self.font = font | |
397 | self.fontsize = fontsize | |
398 | ||
399 | def __call__(self, table): | |
400 | return MegaFontRenderer(table, self.color, self.font, self.fontsize) | |
401 | ||
402 | ||
403 | #--------------------------------------------------------------------------- | |
404 | ||
405 | class TestFrame(wxFrame): | |
406 | def __init__(self, parent, plugins={"This":MegaFontRendererFactory("red", "ARIAL", 8), | |
407 | "A":MegaImageRenderer, | |
408 | "Test":MegaFontRendererFactory("orange", "TIMES", 24),}): | |
409 | wxFrame.__init__(self, parent, -1, | |
410 | "Test Frame", size=(640,480)) | |
411 | ||
412 | grid = MegaGrid(self, data, colnames, plugins) | |
413 | grid.Reset() | |
414 | ||
415 | ||
416 | #--------------------------------------------------------------------------- | |
417 | ||
418 | def runTest(frame, nb, log): | |
419 | win = TestFrame(frame) | |
420 | frame.otherWin = win | |
421 | win.Show(True) | |
422 | ||
423 | ||
424 | ||
425 | overview = """Mega Grid Example | |
426 | ||
427 | This example attempts to show many examples and tricks of | |
428 | using a virtual grid object. Hopefully the source isn't too jumbled. | |
429 | ||
430 | Features: | |
431 | 1) Uses a virtual grid | |
432 | 2) Columns and rows have popup menus (right click on labels) | |
433 | 3) Columns and rows can be deleted (i.e. table can be | |
434 | resized) | |
435 | 4) Dynamic renderers. Renderers are plugins based on | |
436 | column header name. Shows a simple Font Renderer and | |
437 | an Image Renderer. | |
438 | ||
439 | Look for XXX in the code to indicate some workarounds for non-obvious | |
440 | behavior and various hacks. | |
441 | ||
442 | """ | |
443 | ||
444 | ||
445 | ||
446 | ||
447 | if __name__ == '__main__': | |
448 | import sys,os | |
449 | import run | |
450 | run.main(['', os.path.basename(sys.argv[0])]) | |
451 |