]> git.saurik.com Git - wxWidgets.git/blob - wxPython/demo/Grid_MegaExample.py
Use Rob O'Connor's icons, provided under the wxWindows Licence
[wxWidgets.git] / wxPython / demo / Grid_MegaExample.py
1
2 import wx
3 import wx.grid as Grid
4
5 import images
6
7 #---------------------------------------------------------------------------
8
9 class MegaTable(Grid.PyGridTableBase):
10 """
11 A custom wx.Grid Table using user supplied data
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*
20 Grid.PyGridTableBase.__init__(self)
21 self.data = data
22 self.colnames = colnames
23 self.plugins = plugins or {}
24 # XXX
25 # we need to store the row length and column length to
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
39 def GetRowLabelValue(self, row):
40 return "row %03d" % int(self.data[row][0])
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 """
53 (Grid) -> Reset the grid view. Call this to
54 update the grid if rows and columns have been added or deleted
55 """
56 grid.BeginBatch()
57
58 for current, new, delmsg, addmsg in [
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),
61 ]:
62
63 if new < current:
64 msg = Grid.GridTableMessage(self,delmsg,new,current-new)
65 grid.ProcessTableMessage(msg)
66 elif new > current:
67 msg = Grid.GridTableMessage(self,addmsg,new-current)
68 grid.ProcessTableMessage(msg)
69 self.UpdateValues(grid)
70
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
86 msg = Grid.GridTableMessage(self, Grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
87 grid.ProcessTableMessage(msg)
88
89 def _updateColAttrs(self, grid):
90 """
91 wx.Grid -> update the column attributes to add the
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
98
99 for colname in self.colnames:
100 attr = Grid.GridCellAttr()
101 if colname in self.plugins:
102 renderer = self.plugins[colname](self)
103
104 if renderer.colSize:
105 grid.SetColSize(col, renderer.colSize)
106
107 if renderer.rowSize:
108 grid.SetDefaultRowSize(renderer.rowSize)
109
110 attr.SetReadOnly(True)
111 attr.SetRenderer(renderer)
112
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):
119 #print 'append'
120 entry = {}
121
122 for name in self.colnames:
123 entry[name] = "Appended_%i"%row
124
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()
141
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
147
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()
159
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 = []
172
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 = []
179
180 for sortvalue, row in _data:
181 self.data.append(row)
182
183 # end table manipulation code
184 # ----------------------------------------------------------
185
186
187 # --------------------------------------------------------------------
188 # Sample wx.Grid renderers
189
190 class MegaImageRenderer(Grid.PyGridCellRenderer):
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 """
197 Grid.PyGridCellRenderer.__init__(self)
198 self.table = table
199 self._choices = [images.getSmilesBitmap,
200 images.getMondrianBitmap,
201 images.get_10s_Bitmap,
202 images.get_01c_Bitmap]
203
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)]()
210 image = wx.MemoryDC()
211 image.SelectObject(bmp)
212
213 # clear the background
214 dc.SetBackgroundMode(wx.SOLID)
215
216 if isSelected:
217 dc.SetBrush(wx.Brush(wx.BLUE, wx.SOLID))
218 dc.SetPen(wx.Pen(wx.BLUE, 1, wx.SOLID))
219 else:
220 dc.SetBrush(wx.Brush(wx.WHITE, wx.SOLID))
221 dc.SetPen(wx.Pen(wx.WHITE, 1, wx.SOLID))
222 dc.DrawRectangleRect(rect)
223
224
225 # copy the image but only to the size of the grid cell
226 width, height = bmp.GetWidth(), bmp.GetHeight()
227
228 if width > rect.width-2:
229 width = rect.width-2
230
231 if height > rect.height-2:
232 height = rect.height-2
233
234 dc.Blit(rect.x+1, rect.y+1, width, height,
235 image,
236 0, 0, wx.COPY, True)
237
238
239 class MegaFontRenderer(Grid.PyGridCellRenderer):
240 def __init__(self, table, color="blue", font="ARIAL", fontsize=8):
241 """Render data in the specified color and font and fontsize"""
242 Grid.PyGridCellRenderer.__init__(self)
243 self.table = table
244 self.color = color
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)
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
259 dc.SetBackgroundMode(wx.SOLID)
260
261 if isSelected:
262 dc.SetBrush(wx.Brush(wx.BLUE, wx.SOLID))
263 dc.SetPen(wx.Pen(wx.BLUE, 1, wx.SOLID))
264 else:
265 dc.SetBrush(wx.Brush(wx.WHITE, wx.SOLID))
266 dc.SetPen(wx.Pen(wx.WHITE, 1, wx.SOLID))
267 dc.DrawRectangleRect(rect)
268
269 text = self.table.GetValue(row, col)
270 dc.SetBackgroundMode(wx.SOLID)
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)
283 dc.DrawText(text, rect.x+1, rect.y+1)
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)
291
292 if width > rect.width-2:
293 width, height = dc.GetTextExtent("...")
294 x = rect.x+1 + rect.width-2 - width
295 dc.DrawRectangle(x, rect.y+1, width+1, height)
296 dc.DrawText("...", x, rect.y+1)
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
305 class MegaGrid(Grid.Grid):
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*
314 Grid.Grid.__init__(self, parent, -1)
315 self._table = MegaTable(data, colnames, plugins)
316 self.SetTable(self._table)
317 self._plugins = plugins
318
319 self.Bind(Grid.EVT_GRID_LABEL_RIGHT_CLICK, self.OnLabelRightClicked)
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"""
334 appendID = wx.NewId()
335 deleteID = wx.NewId()
336 x = self.GetRowSize(row)/2
337
338 if not self.GetSelectedRows():
339 self.SelectRow(row)
340
341 menu = wx.Menu()
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
355 self.Bind(wx.EVT_MENU, append, id=appendID)
356 self.Bind(wx.EVT_MENU, delete, id=deleteID)
357 self.PopupMenu(menu, (x, yo))
358 menu.Destroy()
359 return
360
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
366 menu = wx.Menu()
367 id1 = wx.NewId()
368 sortID = wx.NewId()
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
386 self.Bind(wx.EVT_MENU, delete, id=id1)
387
388 if len(cols) == 1:
389 self.Bind(wx.EVT_MENU, sort, id=sortID)
390
391 self.PopupMenu(menu, (xo, 0))
392 menu.Destroy()
393 return
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 = []
406
407 for row in range(1000):
408 d = {}
409 for name in ["This", "Test", "Is"]:
410 d[name] = random.random()
411
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
436 class TestFrame(wx.Frame):
437 def __init__(self, parent, plugins={"This":MegaFontRendererFactory("red", "ARIAL", 8),
438 "A":MegaImageRenderer,
439 "Test":MegaFontRendererFactory("orange", "TIMES", 24),}):
440 wx.Frame.__init__(self, parent, -1,
441 "Test Frame", size=(640,480))
442
443 grid = MegaGrid(self, data, colnames, plugins)
444 grid.Reset()
445
446
447 #---------------------------------------------------------------------------
448
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
465 def runTest(frame, nb, log):
466 win = TestPanel(nb, log)
467 return win
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:
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
481 resized)
482 <li>Dynamic renderers. Renderers are plugins based on
483 column header name. Shows a simple Font Renderer and
484 an Image Renderer.
485 </ol>
486
487 Look for 'XXX' in the code to indicate some workarounds for non-obvious
488 behavior and various hacks.
489
490 """
491
492
493 if __name__ == '__main__':
494 import sys,os
495 import run
496 run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])
497