1 # 11/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
3 # o Updated for wx namespace
11 class MegaTable(Grid
.PyGridTableBase
):
13 A custom wxGrid Table using user supplied data
15 def __init__(self
, data
, colnames
, plugins
):
16 """data is a list of the form
17 [(rowname, dictionary),
18 dictionary.get(colname, None) returns the data for column
21 # The base class must be initialized *first*
22 Grid
.PyGridTableBase
.__init
__(self
)
24 self
.colnames
= colnames
25 self
.plugins
= plugins
or {}
27 # we need to store the row length and column length to
28 # see if the table has changed size
29 self
._rows
= self
.GetNumberRows()
30 self
._cols
= self
.GetNumberCols()
32 def GetNumberCols(self
):
33 return len(self
.colnames
)
35 def GetNumberRows(self
):
38 def GetColLabelValue(self
, col
):
39 return self
.colnames
[col
]
41 def GetRowLabelValue(self
, row
):
42 return "row %03d" % int(self
.data
[row
][0])
44 def GetValue(self
, row
, col
):
45 return str(self
.data
[row
][1].get(self
.GetColLabelValue(col
), ""))
47 def GetRawValue(self
, row
, col
):
48 return self
.data
[row
][1].get(self
.GetColLabelValue(col
), "")
50 def SetValue(self
, row
, col
, value
):
51 self
.data
[row
][1][self
.GetColLabelValue(col
)] = value
53 def ResetView(self
, grid
):
55 (Grid) -> Reset the grid view. Call this to
56 update the grid if rows and columns have been added or deleted
60 for current
, new
, delmsg
, addmsg
in [
61 (self
._rows
, self
.GetNumberRows(), Grid
.GRIDTABLE_NOTIFY_ROWS_DELETED
, Grid
.GRIDTABLE_NOTIFY_ROWS_APPENDED
),
62 (self
._cols
, self
.GetNumberCols(), Grid
.GRIDTABLE_NOTIFY_COLS_DELETED
, Grid
.GRIDTABLE_NOTIFY_COLS_APPENDED
),
66 msg
= Grid
.GridTableMessage(self
,delmsg
,new
,current
-new
)
67 grid
.ProcessTableMessage(msg
)
69 msg
= Grid
.GridTableMessage(self
,addmsg
,new
-current
)
70 grid
.ProcessTableMessage(msg
)
71 self
.UpdateValues(grid
)
75 self
._rows
= self
.GetNumberRows()
76 self
._cols
= self
.GetNumberCols()
77 # update the column rendering plugins
78 self
._updateColAttrs
(grid
)
80 # update the scrollbars and the displayed part of the grid
81 grid
.AdjustScrollbars()
85 def UpdateValues(self
, grid
):
86 """Update all displayed values"""
87 # This sends an event to the grid table to update all of the values
88 msg
= Grid
.GridTableMessage(self
, Grid
.GRIDTABLE_REQUEST_VIEW_GET_VALUES
)
89 grid
.ProcessTableMessage(msg
)
91 def _updateColAttrs(self
, grid
):
93 wxGrid -> update the column attributes to add the
94 appropriate renderer given the column name. (renderers
95 are stored in the self.plugins dictionary)
97 Otherwise default to the default renderer.
101 for colname
in self
.colnames
:
102 attr
= Grid
.GridCellAttr()
103 if colname
in self
.plugins
:
104 renderer
= self
.plugins
[colname
](self
)
107 grid
.SetColSize(col
, renderer
.colSize
)
110 grid
.SetDefaultRowSize(renderer
.rowSize
)
112 attr
.SetReadOnly(True)
113 attr
.SetRenderer(renderer
)
115 grid
.SetColAttr(col
, attr
)
118 # ------------------------------------------------------
119 # begin the added code to manipulate the table (non wx related)
120 def AppendRow(self
, row
):
124 for name
in self
.colnames
:
125 entry
[name
] = "Appended_%i"%row
128 # entry["A"] can only be between 1..4
129 entry
["A"] = random
.choice(range(4))
130 self
.data
.insert(row
, ["Append_%i"%row
, entry
])
132 def DeleteCols(self
, cols
):
134 cols -> delete the columns from the dataset
135 cols hold the column indices
137 # we'll cheat here and just remove the name from the
138 # list of column names. The data will remain but
145 self
.colnames
.pop(i
-deleteCount
)
146 # we need to advance the delete count
147 # to make sure we delete the right columns
150 if not len(self
.colnames
):
153 def DeleteRows(self
, rows
):
155 rows -> delete the rows from the dataset
156 rows hold the row indices
163 self
.data
.pop(i
-deleteCount
)
164 # we need to advance the delete count
165 # to make sure we delete the right rows
168 def SortColumn(self
, col
):
170 col -> sort the data based on the column indexed by col
172 name
= self
.colnames
[col
]
175 for row
in self
.data
:
177 _data
.append((entry
.get(name
, None), row
))
182 for sortvalue
, row
in _data
:
183 self
.data
.append(row
)
185 # end table manipulation code
186 # ----------------------------------------------------------
189 # --------------------------------------------------------------------
190 # Sample wxGrid renderers
192 class MegaImageRenderer(Grid
.PyGridCellRenderer
):
193 def __init__(self
, table
):
195 Image Renderer Test. This just places an image in a cell
196 based on the row index. There are N choices and the
197 choice is made by choice[row%N]
199 Grid
.PyGridCellRenderer
.__init
__(self
)
201 self
._choices
= [images
.getSmilesBitmap
,
202 images
.getMondrianBitmap
,
203 images
.get_10s_Bitmap
,
204 images
.get_01c_Bitmap
]
209 def Draw(self
, grid
, attr
, dc
, rect
, row
, col
, isSelected
):
210 choice
= self
.table
.GetRawValue(row
, col
)
211 bmp
= self
._choices
[ choice
% len(self
._choices
)]()
212 image
= wx
.MemoryDC()
213 image
.SelectObject(bmp
)
215 # clear the background
216 dc
.SetBackgroundMode(wx
.SOLID
)
219 dc
.SetBrush(wx
.Brush(wx
.BLUE
, wx
.SOLID
))
220 dc
.SetPen(wx
.Pen(wx
.BLUE
, 1, wx
.SOLID
))
222 dc
.SetBrush(wx
.Brush(wx
.WHITE
, wx
.SOLID
))
223 dc
.SetPen(wx
.Pen(wx
.WHITE
, 1, wx
.SOLID
))
224 dc
.DrawRectangleRect(rect
)
226 #dc.DrawRectangle((rect.x, rect.y), (rect.width, rect.height))
228 # copy the image but only to the size of the grid cell
229 width
, height
= bmp
.GetWidth(), bmp
.GetHeight()
231 if width
> rect
.width
-2:
234 if height
> rect
.height
-2:
235 height
= rect
.height
-2
237 dc
.Blit((rect
.x
+1, rect
.y
+1), (width
, height
),
239 (0, 0), wx
.COPY
, True)
242 class MegaFontRenderer(Grid
.PyGridCellRenderer
):
243 def __init__(self
, table
, color
="blue", font
="ARIAL", fontsize
=8):
244 """Render data in the specified color and font and fontsize"""
245 Grid
.PyGridCellRenderer
.__init
__(self
)
248 self
.font
= wx
.Font(fontsize
, wx
.DEFAULT
, wx
.NORMAL
, wx
.NORMAL
, 0, font
)
249 self
.selectedBrush
= wx
.Brush("blue", wx
.SOLID
)
250 self
.normalBrush
= wx
.Brush(wx
.WHITE
, wx
.SOLID
)
254 def Draw(self
, grid
, attr
, dc
, rect
, row
, col
, isSelected
):
255 # Here we draw text in a grid cell using various fonts
256 # and colors. We have to set the clipping region on
257 # the grid's DC, otherwise the text will spill over
259 dc
.SetClippingRect(rect
)
261 # clear the background
262 dc
.SetBackgroundMode(wx
.SOLID
)
265 dc
.SetBrush(wx
.Brush(wx
.BLUE
, wx
.SOLID
))
266 dc
.SetPen(wx
.Pen(wx
.BLUE
, 1, wx
.SOLID
))
268 dc
.SetBrush(wx
.Brush(wx
.WHITE
, wx
.SOLID
))
269 dc
.SetPen(wx
.Pen(wx
.WHITE
, 1, wx
.SOLID
))
270 dc
.DrawRectangleRect(rect
)
272 #dc.DrawRectangle((rect.x, rect.y), (rect.width, rect.height))
274 text
= self
.table
.GetValue(row
, col
)
275 dc
.SetBackgroundMode(wx
.SOLID
)
277 # change the text background based on whether the grid is selected
280 dc
.SetBrush(self
.selectedBrush
)
281 dc
.SetTextBackground("blue")
283 dc
.SetBrush(self
.normalBrush
)
284 dc
.SetTextBackground("white")
286 dc
.SetTextForeground(self
.color
)
287 dc
.SetFont(self
.font
)
288 dc
.DrawText(text
, (rect
.x
+1, rect
.y
+1))
290 # Okay, now for the advanced class :)
291 # Let's add three dots "..."
292 # to indicate that that there is more text to be read
293 # when the text is larger than the grid cell
295 width
, height
= dc
.GetTextExtent(text
)
297 if width
> rect
.width
-2:
298 width
, height
= dc
.GetTextExtent("...")
299 x
= rect
.x
+1 + rect
.width
-2 - width
300 dc
.DrawRectangle((x
, rect
.y
+1), (width
+1, height
))
301 dc
.DrawText("...", (x
, rect
.y
+1))
303 dc
.DestroyClippingRegion()
306 # --------------------------------------------------------------------
307 # Sample Grid using a specialized table and renderers that can
308 # be plugged in based on column names
310 class MegaGrid(Grid
.Grid
):
311 def __init__(self
, parent
, data
, colnames
, plugins
=None):
312 """parent, data, colnames, plugins=None
313 Initialize a grid using the data defined in data and colnames
314 (see MegaTable for a description of the data format)
315 plugins is a dictionary of columnName -> column renderers.
318 # The base class must be initialized *first*
319 Grid
.Grid
.__init
__(self
, parent
, -1)
320 self
._table
= MegaTable(data
, colnames
, plugins
)
321 self
.SetTable(self
._table
)
322 self
._plugins
= plugins
324 self
.Bind(Grid
.EVT_GRID_LABEL_RIGHT_CLICK
, self
.OnLabelRightClicked
)
327 """reset the view based on the data in the table. Call
328 this when rows are added or destroyed"""
329 self
._table
.ResetView(self
)
331 def OnLabelRightClicked(self
, evt
):
332 # Did we click on a row or a column?
333 row
, col
= evt
.GetRow(), evt
.GetCol()
334 if row
== -1: self
.colPopup(col
, evt
)
335 elif col
== -1: self
.rowPopup(row
, evt
)
337 def rowPopup(self
, row
, evt
):
338 """(row, evt) -> display a popup menu when a row label is right clicked"""
339 appendID
= wx
.NewId()
340 deleteID
= wx
.NewId()
341 x
= self
.GetRowSize(row
)/2
343 if not self
.GetSelectedRows():
347 xo
, yo
= evt
.GetPosition()
348 menu
.Append(appendID
, "Append Row")
349 menu
.Append(deleteID
, "Delete Row(s)")
351 def append(event
, self
=self
, row
=row
):
352 self
._table
.AppendRow(row
)
355 def delete(event
, self
=self
, row
=row
):
356 rows
= self
.GetSelectedRows()
357 self
._table
.DeleteRows(rows
)
360 self
.Bind(wx
.EVT_MENU
, append
, id=appendID
)
361 self
.Bind(wx
.EVT_MENU
, delete
, id=deleteID
)
362 self
.PopupMenu(menu
, (x
, yo
))
367 def colPopup(self
, col
, evt
):
368 """(col, evt) -> display a popup menu when a column label is
370 x
= self
.GetColSize(col
)/2
375 xo
, yo
= evt
.GetPosition()
377 cols
= self
.GetSelectedCols()
379 menu
.Append(id1
, "Delete Col(s)")
380 menu
.Append(sortID
, "Sort Column")
382 def delete(event
, self
=self
, col
=col
):
383 cols
= self
.GetSelectedCols()
384 self
._table
.DeleteCols(cols
)
387 def sort(event
, self
=self
, col
=col
):
388 self
._table
.SortColumn(col
)
391 self
.Bind(wx
.EVT_MENU
, delete
, id=id1
)
394 self
.Bind(wx
.EVT_MENU
, sort
, id=sortID
)
396 self
.PopupMenu(menu
, (xo
, 0))
400 # -----------------------------------------------------------------
402 # data is in the form
403 # [rowname, dictionary]
404 # where dictionary.get(colname, None) -> returns the value for the cell
406 # the colname must also be supplied
408 colnames
= ["Row", "This", "Is", "A", "Test"]
412 for row
in range(1000):
414 for name
in ["This", "Test", "Is"]:
415 d
[name
] = random
.random()
419 # the "A" column can only be between one and 4
420 d
["A"] = random
.choice(range(4))
421 data
.append((str(row
), d
))
423 class MegaFontRendererFactory
:
424 def __init__(self
, color
, font
, fontsize
):
426 (color, font, fontsize) -> set of a factory to generate
427 renderers when called.
428 func = MegaFontRenderFactory(color, font, fontsize)
429 renderer = func(table)
433 self
.fontsize
= fontsize
435 def __call__(self
, table
):
436 return MegaFontRenderer(table
, self
.color
, self
.font
, self
.fontsize
)
439 #---------------------------------------------------------------------------
441 class TestFrame(wx
.Frame
):
442 def __init__(self
, parent
, plugins
={"This":MegaFontRendererFactory("red", "ARIAL", 8),
443 "A":MegaImageRenderer
,
444 "Test":MegaFontRendererFactory("orange", "TIMES", 24),}):
445 wx
.Frame
.__init
__(self
, parent
, -1,
446 "Test Frame", size
=(640,480))
448 grid
= MegaGrid(self
, data
, colnames
, plugins
)
452 #---------------------------------------------------------------------------
454 def runTest(frame
, nb
, log
):
455 win
= TestFrame(frame
)
461 overview
= """Mega Grid Example
463 This example attempts to show many examples and tricks of
464 using a virtual grid object. Hopefully the source isn't too jumbled.
468 <li>Uses a virtual grid
469 <li>Columns and rows have popup menus (right click on labels)
470 <li>Columns and rows can be deleted (i.e. table can be
472 <li>Dynamic renderers. Renderers are plugins based on
473 column header name. Shows a simple Font Renderer and
477 Look for 'XXX' in the code to indicate some workarounds for non-obvious
478 behavior and various hacks.
483 if __name__
== '__main__':
486 run
.main(['', os
.path
.basename(sys
.argv
[0])])