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