1 from wxPython
.wx
import *
2 from wxPython
.grid
import *
5 class MegaTable(wxPyGridTableBase
):
7 A custom wxGrid Table using user supplied data
9 def __init__(self
, data
, colnames
, plugins
):
10 """data is a list of the form
11 [(rowname, dictionary),
12 dictionary.get(colname, None) returns the data for column
15 # The base class must be initialized *first*
16 wxPyGridTableBase
.__init
__(self
)
18 self
.colnames
= colnames
19 self
.plugins
= plugins
or {}
21 # we need to store the row length and collength to
22 # see if the table has changed size
23 self
._rows
= self
.GetNumberRows()
24 self
._cols
= self
.GetNumberCols()
26 def GetNumberCols(self
):
27 return len(self
.colnames
)
29 def GetNumberRows(self
):
32 def GetColLabelValue(self
, col
):
33 return self
.colnames
[col
]
35 def GetRowLabelValues(self
, row
):
36 return self
.data
[row
][0]
38 def GetValue(self
, row
, col
):
39 return str(self
.data
[row
][1].get(self
.GetColLabelValue(col
), ""))
41 def GetRawValue(self
, row
, col
):
42 return self
.data
[row
][1].get(self
.GetColLabelValue(col
), "")
44 def SetValue(self
, row
, col
, value
):
45 self
.data
[row
][1][self
.GetColLabelValue(col
)] = value
47 def ResetView(self
, grid
):
49 (wxGrid) -> Reset the grid view. Call this to
50 update the grid if rows and columns have been added or deleted
53 for current
, new
, delmsg
, addmsg
in [
54 (self
._rows
, self
.GetNumberRows(), wxGRIDTABLE_NOTIFY_ROWS_DELETED
, wxGRIDTABLE_NOTIFY_ROWS_APPENDED
),
55 (self
._cols
, self
.GetNumberCols(), wxGRIDTABLE_NOTIFY_COLS_DELETED
, wxGRIDTABLE_NOTIFY_COLS_APPENDED
),
58 msg
= wxGridTableMessage(self
,delmsg
,new
,current
-new
)
59 grid
.ProcessTableMessage(msg
)
61 msg
= wxGridTableMessage(self
,addmsg
,new
-current
)
62 grid
.ProcessTableMessage(msg
)
63 self
.UpdateValues(grid
)
66 self
._rows
= self
.GetNumberRows()
67 self
._cols
= self
.GetNumberCols()
68 # update the column rendering plugins
69 self
._updateColAttrs
(grid
)
71 # update the scrollbars and the displayed part of the grid
72 grid
.AdjustScrollbars()
76 def UpdateValues(self
, grid
):
77 """Update all displayed values"""
78 # This sends an event to the grid table to update all of the values
79 msg
= wxGridTableMessage(self
, wxGRIDTABLE_REQUEST_VIEW_GET_VALUES
)
80 grid
.ProcessTableMessage(msg
)
82 def _updateColAttrs(self
, grid
):
84 wxGrid -> update the column attributes to add the
85 appropriate renderer given the column name. (renderers
86 are stored in the self.plugins dictionary)
88 Otherwise default to the default renderer.
91 for colname
in self
.colnames
:
92 attr
= wxGridCellAttr()
93 if colname
in self
.plugins
:
94 renderer
= self
.plugins
[colname
](self
)
96 grid
.SetColSize(col
, renderer
.colSize
)
98 grid
.SetDefaultRowSize(renderer
.rowSize
)
99 attr
.SetReadOnly(True)
100 attr
.SetRenderer(renderer
)
101 grid
.SetColAttr(col
, attr
)
104 # ------------------------------------------------------
105 # begin the added code to manipulate the table (non wx related)
106 def AppendRow(self
, row
):
108 for name
in self
.colnames
:
109 entry
[name
] = "Appended_%i"%row
111 # entry["A"] can only be between 1..4
112 entry
["A"] = random
.choice(range(4))
113 self
.data
.insert(row
, ["Append_%i"%row
, entry
])
115 def DeleteCols(self
, cols
):
117 cols -> delete the columns from the dataset
118 cols hold the column indices
120 # we'll cheat here and just remove the name from the
121 # list of column names. The data will remain but
127 self
.colnames
.pop(i
-deleteCount
)
128 # we need to advance the delete count
129 # to make sure we delete the right columns
131 if not len(self
.colnames
):
134 def DeleteRows(self
, rows
):
136 rows -> delete the rows from the dataset
137 rows hold the row indices
143 self
.data
.pop(i
-deleteCount
)
144 # we need to advance the delete count
145 # to make sure we delete the right rows
148 def SortColumn(self
, col
):
150 col -> sort the data based on the column indexed by col
152 name
= self
.colnames
[col
]
154 for row
in self
.data
:
156 _data
.append((entry
.get(name
, None), row
))
160 for sortvalue
, row
in _data
:
161 self
.data
.append(row
)
163 # end table manipulation code
164 # ----------------------------------------------------------
167 # --------------------------------------------------------------------
168 # Sample wxGrid renderers
170 class MegaImageRenderer(wxPyGridCellRenderer
):
171 def __init__(self
, table
):
173 Image Renderer Test. This just places an image in a cell
174 based on the row index. There are N choices and the
175 choice is made by choice[row%N]
177 wxPyGridCellRenderer
.__init
__(self
)
179 self
._choices
= [images
.getSmilesBitmap
,
180 images
.getMondrianBitmap
,
181 images
.get_10s_Bitmap
,
182 images
.get_01c_Bitmap
]
188 def Draw(self
, grid
, attr
, dc
, rect
, row
, col
, isSelected
):
189 choice
= self
.table
.GetRawValue(row
, col
)
190 bmp
= self
._choices
[ choice
% len(self
._choices
)]()
192 image
.SelectObject(bmp
)
194 # clear the background
195 dc
.SetBackgroundMode(wxSOLID
)
197 dc
.SetBrush(wxBrush(wxBLUE
, wxSOLID
))
198 dc
.SetPen(wxPen(wxBLUE
, 1, wxSOLID
))
200 dc
.SetBrush(wxBrush(wxWHITE
, wxSOLID
))
201 dc
.SetPen(wxPen(wxWHITE
, 1, wxSOLID
))
202 dc
.DrawRectangleRect(rect
)
204 # copy the image but only to the size of the grid cell
205 width
, height
= bmp
.GetWidth(), bmp
.GetHeight()
206 if width
> rect
.width
-2:
209 if height
> rect
.height
-2:
210 height
= rect
.height
-2
212 dc
.Blit((rect
.x
+1, rect
.y
+1), (width
, height
),
214 (0, 0), wxCOPY
, True)
217 class MegaFontRenderer(wxPyGridCellRenderer
):
218 def __init__(self
, table
, color
="blue", font
="ARIAL", fontsize
=8):
219 """Render data in the specified color and font and fontsize"""
220 wxPyGridCellRenderer
.__init
__(self
)
223 self
.font
= wxFont(fontsize
, wxDEFAULT
, wxNORMAL
, wxNORMAL
,
225 self
.selectedBrush
= wxBrush("blue",
227 self
.normalBrush
= wxBrush(wxWHITE
, wxSOLID
)
231 def Draw(self
, grid
, attr
, dc
, rect
, row
, col
, isSelected
):
232 # Here we draw text in a grid cell using various fonts
233 # and colors. We have to set the clipping region on
234 # the grid's DC, otherwise the text will spill over
236 dc
.SetClippingRect(rect
)
238 # clear the background
239 dc
.SetBackgroundMode(wxSOLID
)
241 dc
.SetBrush(wxBrush(wxBLUE
, wxSOLID
))
242 dc
.SetPen(wxPen(wxBLUE
, 1, wxSOLID
))
244 dc
.SetBrush(wxBrush(wxWHITE
, wxSOLID
))
245 dc
.SetPen(wxPen(wxWHITE
, 1, wxSOLID
))
246 dc
.DrawRectangleRect(rect
)
248 text
= self
.table
.GetValue(row
, col
)
249 dc
.SetBackgroundMode(wxSOLID
)
251 # change the text background based on whether the grid is selected
254 dc
.SetBrush(self
.selectedBrush
)
255 dc
.SetTextBackground("blue")
257 dc
.SetBrush(self
.normalBrush
)
258 dc
.SetTextBackground("white")
260 dc
.SetTextForeground(self
.color
)
261 dc
.SetFont(self
.font
)
262 dc
.DrawText(text
, (rect
.x
+1, rect
.y
+1))
264 # Okay, now for the advanced class :)
265 # Let's add three dots "..."
266 # to indicate that that there is more text to be read
267 # when the text is larger than the grid cell
269 width
, height
= dc
.GetTextExtent(text
)
270 if width
> rect
.width
-2:
271 width
, height
= dc
.GetTextExtent("...")
272 x
= rect
.x
+1 + rect
.width
-2 - width
273 dc
.DrawRectangle((x
, rect
.y
+1), (width
+1, height
))
274 dc
.DrawText("...", (x
, rect
.y
+1))
276 dc
.DestroyClippingRegion()
279 # --------------------------------------------------------------------
280 # Sample Grid using a specialized table and renderers that can
281 # be plugged in based on column names
283 class MegaGrid(wxGrid
):
284 def __init__(self
, parent
, data
, colnames
, plugins
=None):
285 """parent, data, colnames, plugins=None
286 Initialize a grid using the data defined in data and colnames
287 (see MegaTable for a description of the data format)
288 plugins is a dictionary of columnName -> column renderers.
291 # The base class must be initialized *first*
292 wxGrid
.__init
__(self
, parent
, -1)
293 self
._table
= MegaTable(data
, colnames
, plugins
)
294 self
.SetTable(self
._table
)
295 self
._plugins
= plugins
297 EVT_GRID_LABEL_RIGHT_CLICK(self
, self
.OnLabelRightClicked
)
300 """reset the view based on the data in the table. Call
301 this when rows are added or destroyed"""
302 self
._table
.ResetView(self
)
304 def OnLabelRightClicked(self
, evt
):
305 # Did we click on a row or a column?
306 row
, col
= evt
.GetRow(), evt
.GetCol()
307 if row
== -1: self
.colPopup(col
, evt
)
308 elif col
== -1: self
.rowPopup(row
, evt
)
310 def rowPopup(self
, row
, evt
):
311 """(row, evt) -> display a popup menu when a row label is right clicked"""
314 x
= self
.GetRowSize(row
)/2
315 if not self
.GetSelectedRows():
318 xo
, yo
= evt
.GetPosition()
319 menu
.Append(appendID
, "Append Row")
320 menu
.Append(deleteID
, "Delete Row(s)")
322 def append(event
, self
=self
, row
=row
):
323 self
._table
.AppendRow(row
)
326 def delete(event
, self
=self
, row
=row
):
327 rows
= self
.GetSelectedRows()
328 self
._table
.DeleteRows(rows
)
331 EVT_MENU(self
, appendID
, append
)
332 EVT_MENU(self
, deleteID
, delete
)
333 self
.PopupMenu(menu
, wxPoint(x
, yo
))
336 def colPopup(self
, col
, evt
):
337 """(col, evt) -> display a popup menu when a column label is
339 x
= self
.GetColSize(col
)/2
344 xo
, yo
= evt
.GetPosition()
346 cols
= self
.GetSelectedCols()
348 menu
.Append(id1
, "Delete Col(s)")
349 menu
.Append(sortID
, "Sort Column")
351 def delete(event
, self
=self
, col
=col
):
352 cols
= self
.GetSelectedCols()
353 self
._table
.DeleteCols(cols
)
356 def sort(event
, self
=self
, col
=col
):
357 self
._table
.SortColumn(col
)
360 EVT_MENU(self
, id1
, delete
)
362 EVT_MENU(self
, sortID
, sort
)
363 self
.PopupMenu(menu
, wxPoint(xo
, 0))
366 # -----------------------------------------------------------------
368 # data is in the form
369 # [rowname, dictionary]
370 # where dictionary.get(colname, None) -> returns the value for the cell
372 # the colname must also be supplied
374 colnames
= ["Row", "This", "Is", "A", "Test"]
377 for row
in range(1000):
379 for name
in ["This", "Test", "Is"]:
380 d
[name
] = random
.random()
383 # the "A" column can only be between one and 4
384 d
["A"] = random
.choice(range(4))
385 data
.append((str(row
), d
))
387 class MegaFontRendererFactory
:
388 def __init__(self
, color
, font
, fontsize
):
390 (color, font, fontsize) -> set of a factory to generate
391 renderers when called.
392 func = MegaFontRenderFactory(color, font, fontsize)
393 renderer = func(table)
397 self
.fontsize
= fontsize
399 def __call__(self
, table
):
400 return MegaFontRenderer(table
, self
.color
, self
.font
, self
.fontsize
)
403 #---------------------------------------------------------------------------
405 class TestFrame(wxFrame
):
406 def __init__(self
, parent
, plugins
={"This":MegaFontRendererFactory("red", "ARIAL", 8),
407 "A":MegaImageRenderer
,
408 "Test":MegaFontRendererFactory("orange", "TIMES", 24),}):
409 wxFrame
.__init
__(self
, parent
, -1,
410 "Test Frame", size
=(640,480))
412 grid
= MegaGrid(self
, data
, colnames
, plugins
)
416 #---------------------------------------------------------------------------
418 def runTest(frame
, nb
, log
):
419 win
= TestFrame(frame
)
425 overview
= """Mega Grid Example
427 This example attempts to show many examples and tricks of
428 using a virtual grid object. Hopefully the source isn't too jumbled.
431 1) Uses a virtual grid
432 2) Columns and rows have popup menus (right click on labels)
433 3) Columns and rows can be deleted (i.e. table can be
435 4) Dynamic renderers. Renderers are plugins based on
436 column header name. Shows a simple Font Renderer and
439 Look for XXX in the code to indicate some workarounds for non-obvious
440 behavior and various hacks.
447 if __name__
== '__main__':
450 run
.main(['', os
.path
.basename(sys
.argv
[0])])