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