7 #---------------------------------------------------------------------------
9 class MegaTable(Grid
.PyGridTableBase
):
11 A custom wx.Grid Table using user supplied data
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
19 # The base class must be initialized *first*
20 Grid
.PyGridTableBase
.__init
__(self
)
22 self
.colnames
= colnames
23 self
.plugins
= plugins
or {}
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()
30 def GetNumberCols(self
):
31 return len(self
.colnames
)
33 def GetNumberRows(self
):
36 def GetColLabelValue(self
, col
):
37 return self
.colnames
[col
]
39 def GetRowLabelValue(self
, row
):
40 return "row %03d" % int(self
.data
[row
][0])
42 def GetValue(self
, row
, col
):
43 return str(self
.data
[row
][1].get(self
.GetColLabelValue(col
), ""))
45 def GetRawValue(self
, row
, col
):
46 return self
.data
[row
][1].get(self
.GetColLabelValue(col
), "")
48 def SetValue(self
, row
, col
, value
):
49 self
.data
[row
][1][self
.GetColLabelValue(col
)] = value
51 def ResetView(self
, grid
):
53 (Grid) -> Reset the grid view. Call this to
54 update the grid if rows and columns have been added or deleted
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
),
64 msg
= Grid
.GridTableMessage(self
,delmsg
,new
,current
-new
)
65 grid
.ProcessTableMessage(msg
)
67 msg
= Grid
.GridTableMessage(self
,addmsg
,new
-current
)
68 grid
.ProcessTableMessage(msg
)
69 self
.UpdateValues(grid
)
73 self
._rows
= self
.GetNumberRows()
74 self
._cols
= self
.GetNumberCols()
75 # update the column rendering plugins
76 self
._updateColAttrs
(grid
)
78 # update the scrollbars and the displayed part of the grid
79 grid
.AdjustScrollbars()
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
)
89 def _updateColAttrs(self
, grid
):
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)
95 Otherwise default to the default renderer.
99 for colname
in self
.colnames
:
100 attr
= Grid
.GridCellAttr()
101 if colname
in self
.plugins
:
102 renderer
= self
.plugins
[colname
](self
)
105 grid
.SetColSize(col
, renderer
.colSize
)
108 grid
.SetDefaultRowSize(renderer
.rowSize
)
110 attr
.SetReadOnly(True)
111 attr
.SetRenderer(renderer
)
113 grid
.SetColAttr(col
, attr
)
116 # ------------------------------------------------------
117 # begin the added code to manipulate the table (non wx related)
118 def AppendRow(self
, row
):
122 for name
in self
.colnames
:
123 entry
[name
] = "Appended_%i"%row
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
])
130 def DeleteCols(self
, cols
):
132 cols -> delete the columns from the dataset
133 cols hold the column indices
135 # we'll cheat here and just remove the name from the
136 # list of column names. The data will remain but
143 self
.colnames
.pop(i
-deleteCount
)
144 # we need to advance the delete count
145 # to make sure we delete the right columns
148 if not len(self
.colnames
):
151 def DeleteRows(self
, rows
):
153 rows -> delete the rows from the dataset
154 rows hold the row indices
161 self
.data
.pop(i
-deleteCount
)
162 # we need to advance the delete count
163 # to make sure we delete the right rows
166 def SortColumn(self
, col
):
168 col -> sort the data based on the column indexed by col
170 name
= self
.colnames
[col
]
173 for row
in self
.data
:
175 _data
.append((entry
.get(name
, None), row
))
180 for sortvalue
, row
in _data
:
181 self
.data
.append(row
)
183 # end table manipulation code
184 # ----------------------------------------------------------
187 # --------------------------------------------------------------------
188 # Sample wx.Grid renderers
190 class MegaImageRenderer(Grid
.PyGridCellRenderer
):
191 def __init__(self
, table
):
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]
197 Grid
.PyGridCellRenderer
.__init
__(self
)
199 self
._choices
= [images
.getSmilesBitmap
,
200 images
.getMondrianBitmap
,
201 images
.getWXPdemoBitmap
,
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
)
213 # clear the background
214 dc
.SetBackgroundMode(wx
.SOLID
)
217 dc
.SetBrush(wx
.Brush(wx
.BLUE
, wx
.SOLID
))
218 dc
.SetPen(wx
.Pen(wx
.BLUE
, 1, wx
.SOLID
))
220 dc
.SetBrush(wx
.Brush(wx
.WHITE
, wx
.SOLID
))
221 dc
.SetPen(wx
.Pen(wx
.WHITE
, 1, wx
.SOLID
))
222 dc
.DrawRectangleRect(rect
)
225 # copy the image but only to the size of the grid cell
226 width
, height
= bmp
.GetWidth(), bmp
.GetHeight()
228 if width
> rect
.width
-2:
231 if height
> rect
.height
-2:
232 height
= rect
.height
-2
234 dc
.Blit(rect
.x
+1, rect
.y
+1, width
, height
,
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
)
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
)
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
256 dc
.SetClippingRect(rect
)
258 # clear the background
259 dc
.SetBackgroundMode(wx
.SOLID
)
262 dc
.SetBrush(wx
.Brush(wx
.BLUE
, wx
.SOLID
))
263 dc
.SetPen(wx
.Pen(wx
.BLUE
, 1, wx
.SOLID
))
265 dc
.SetBrush(wx
.Brush(wx
.WHITE
, wx
.SOLID
))
266 dc
.SetPen(wx
.Pen(wx
.WHITE
, 1, wx
.SOLID
))
267 dc
.DrawRectangleRect(rect
)
269 text
= self
.table
.GetValue(row
, col
)
270 dc
.SetBackgroundMode(wx
.SOLID
)
272 # change the text background based on whether the grid is selected
275 dc
.SetBrush(self
.selectedBrush
)
276 dc
.SetTextBackground("blue")
278 dc
.SetBrush(self
.normalBrush
)
279 dc
.SetTextBackground("white")
281 dc
.SetTextForeground(self
.color
)
282 dc
.SetFont(self
.font
)
283 dc
.DrawText(text
, rect
.x
+1, rect
.y
+1)
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
290 width
, height
= dc
.GetTextExtent(text
)
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)
298 dc
.DestroyClippingRegion()
301 # --------------------------------------------------------------------
302 # Sample Grid using a specialized table and renderers that can
303 # be plugged in based on column names
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.
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
319 self
.Bind(Grid
.EVT_GRID_LABEL_RIGHT_CLICK
, self
.OnLabelRightClicked
)
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
)
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
)
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
338 if not self
.GetSelectedRows():
342 xo
, yo
= evt
.GetPosition()
343 menu
.Append(appendID
, "Append Row")
344 menu
.Append(deleteID
, "Delete Row(s)")
346 def append(event
, self
=self
, row
=row
):
347 self
._table
.AppendRow(row
)
350 def delete(event
, self
=self
, row
=row
):
351 rows
= self
.GetSelectedRows()
352 self
._table
.DeleteRows(rows
)
355 self
.Bind(wx
.EVT_MENU
, append
, id=appendID
)
356 self
.Bind(wx
.EVT_MENU
, delete
, id=deleteID
)
357 self
.PopupMenu(menu
, (x
, yo
))
362 def colPopup(self
, col
, evt
):
363 """(col, evt) -> display a popup menu when a column label is
365 x
= self
.GetColSize(col
)/2
370 xo
, yo
= evt
.GetPosition()
372 cols
= self
.GetSelectedCols()
374 menu
.Append(id1
, "Delete Col(s)")
375 menu
.Append(sortID
, "Sort Column")
377 def delete(event
, self
=self
, col
=col
):
378 cols
= self
.GetSelectedCols()
379 self
._table
.DeleteCols(cols
)
382 def sort(event
, self
=self
, col
=col
):
383 self
._table
.SortColumn(col
)
386 self
.Bind(wx
.EVT_MENU
, delete
, id=id1
)
389 self
.Bind(wx
.EVT_MENU
, sort
, id=sortID
)
391 self
.PopupMenu(menu
, (xo
, 0))
395 # -----------------------------------------------------------------
397 # data is in the form
398 # [rowname, dictionary]
399 # where dictionary.get(colname, None) -> returns the value for the cell
401 # the colname must also be supplied
403 colnames
= ["Row", "This", "Is", "A", "Test"]
407 for row
in range(1000):
409 for name
in ["This", "Test", "Is"]:
410 d
[name
] = random
.random()
414 # the "A" column can only be between one and 4
415 d
["A"] = random
.choice(range(4))
416 data
.append((str(row
), d
))
418 class MegaFontRendererFactory
:
419 def __init__(self
, color
, font
, fontsize
):
421 (color, font, fontsize) -> set of a factory to generate
422 renderers when called.
423 func = MegaFontRenderFactory(color, font, fontsize)
424 renderer = func(table)
428 self
.fontsize
= fontsize
430 def __call__(self
, table
):
431 return MegaFontRenderer(table
, self
.color
, self
.font
, self
.fontsize
)
434 #---------------------------------------------------------------------------
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))
443 grid
= MegaGrid(self
, data
, colnames
, plugins
)
447 #---------------------------------------------------------------------------
449 class TestPanel(wx
.Panel
):
450 def __init__(self
, parent
, log
):
452 wx
.Panel
.__init
__(self
, parent
, -1)
454 b
= wx
.Button(self
, -1, "Show the MegaGrid", (50,50))
455 self
.Bind(wx
.EVT_BUTTON
, self
.OnButton
, b
)
458 def OnButton(self
, evt
):
459 win
= TestFrame(self
)
462 #---------------------------------------------------------------------------
465 def runTest(frame
, nb
, log
):
466 win
= TestPanel(nb
, log
)
471 overview
= """Mega Grid Example
473 This example attempts to show many examples and tricks of
474 using a virtual grid object. Hopefully the source isn't too jumbled.
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
482 <li>Dynamic renderers. Renderers are plugins based on
483 column header name. Shows a simple Font Renderer and
487 Look for 'XXX' in the code to indicate some workarounds for non-obvious
488 behavior and various hacks.
493 if __name__
== '__main__':
496 run
.main(['', os
.path
.basename(sys
.argv
[0])] + sys
.argv
[1:])