The wxGrid Class
Contents
-
The wxGrid Class
- Table Interactions
- Changing Size/Shape of Grid/Table
- Data Types, Renderers and Editors
- Selections and the Cursor
- General Display Configuration/Queries
- Column/Row Sizing
- Attribute Objects (Cell Display Properties)
- Label (Header) Display/Edit Configuration
- Cell Display/Edit Configuration
- DragAndDrop onto Grid
- DragAndDrop From Grid
- Capturing Cell Edits when focus changes
Table Interactions
GetTable() — Return the current table object (depending on your version of wxPython, this may not be the Python object instance you passed to SetTable. For this reason, you will likely want to store a weak reference to your table in your Grid class and provide methods such as those in the example below.
SetTable( wxGridTableBase* table, bool takeOwnership = FALSE, wxGrid::wxGridSelectionModes selmode = wxGrid::wxGridSelectCells) — Set the current table object. You should pass an instance of your wxPyGridTableBase sub-class to this method. However, see the note on GetTable (above), and the workaround implementation below.
This workaround code stores a Python weak reference to your wxPyGridTableBase instance, overriding the GetTable and SetTable methods of the wxGrid.
Note: you can only call SetTable once for any given wxGrid. Because of this, it is generally necessary to have your wxPyGridTableBase class alter the grid's size and shape to reflect any changes in your table's dimensions. See wxGrid Size/Shape Management below for details.
Changing Size/Shape of Grid/Table
The methods described in this section control the grid's overall data structures (for instance the number of grid rows/columns). When using a data table (recommended), is often necessary to alter the grid's size to reflect changes in the data table. Here is a recipe for such changes:
1 class BasePropertyTable( wxPyGridTableBase):
2 def ResetView(self):
3 """Trim/extend the control's rows and update all values"""
4 self.getGrid().BeginBatch()
5 for current, new, delmsg, addmsg in [
6 (self.currentRows, self.GetNumberRows(), wxGRIDTABLE_NOTIFY_ROWS_DELETED, wxGRIDTABLE_NOTIFY_ROWS_APPENDED),
7 (self.currentColumns, self.GetNumberCols(), wxGRIDTABLE_NOTIFY_COLS_DELETED, wxGRIDTABLE_NOTIFY_COLS_APPENDED),
8 ]:
9 if new < current:
10 msg = wxGridTableMessage(
11 self,
12 delmsg,
13 new, # position
14 current-new,
15 )
16 self.getGrid().ProcessTableMessage(msg)
17 elif new > current:
18 msg = wxGridTableMessage(
19 self,
20 addmsg,
21 new-current
22 )
23 self.getGrid().ProcessTableMessage(msg)
24 self.UpdateValues()
25 self.getGrid().EndBatch()
26
27 # The scroll bars aren't resized (at least on windows)
28 # Jiggling the size of the window rescales the scrollbars
29 h,w = grid.GetSize()
30 grid.SetSize((h+1, w))
31 grid.SetSize((h, w))
32 grid.ForceRefresh()
33
34 def UpdateValues( self ):
35 """Update all displayed values"""
36 msg = wxGridTableMessage(self, wxGRIDTABLE_REQUEST_VIEW_GET_VALUES)
37 self.getGrid().ProcessTableMessage(msg)
Note: this code assumes that you record self.currentRows and self.currentCols on your initial table set up to allow for calculating the new size relative to the old size. It should be possible to use wxGrid::GetNumberRows() if you prefer.
Also note: the code uses getGrid() to return a pointer to the wxGrid, depending on your version of wxPython, you may be able to use wxPyGridTableBase::GetGrid()or wxPyGridTableBase::GetView() instead. See: Table Interactions (above) for details on this work-around.
Last note: At least on windows, the grid's scroll bars are not resized when rows and columns are deleted. Jiggling the size of the grid and forcing a refresh fixing this problem. The fix for this has been added to the demo code above.
Other size/shape management methods:
AppendCols(int numCols = 1, bool updateLabels = TRUE) — Add the given number of columns to the grid
AppendRows(int numRows = 1, bool updateLabels = TRUE) — Add the given number of rows to the grid
ClearGrid() — Eliminate all rows and columns from the grid (XXX I'm not sure whether this is even applicable to table-based grids)
DeleteCols(int pos = 0, int numCols = 1, bool updateLabels = TRUE) — Delete numCols columns from position pos
DeleteRows(int pos = 0, int numRows = 1, bool updateLabels = TRUE) — Delete numRows rows from position pos
GetNumberCols( ) — Retrieve the number of columns in the table
GetNumberRows( ) — Retrieve the number of rows in the table
InsertCols(int pos = 0, int numCols = 1, bool updateLabels = TRUE) — Insert a number of columns into the grid (and associated table)
InsertRows(int pos = 0, int numRows = 1, bool updateLabels = TRUE) — Insert a number of rows into the grid (and associated table)
Data Types, Renderers and Editors
Your wxPyGridTableBase's GetTypeName returns data-type specifiers for a given row,col pair, the grid uses that to look up the appropriate renderer and editor for the cell.
RegisterDataType(const wxString& typeName, wxGridCellRenderer* renderer, wxGridCellEditor* editor) — Note (as of 2.3.3pre4): calling this twice for the same data type will result in your new editors not having "Create" called to create their control. You will get a message that "The wxGridCellEditor must be Created first!" if you happen to do this.
Data Type Specifiers
Usage patterns? I automate their generation, not sure if most people do that.
Edit/Render Objects/Controls
CanEnableCellControl()
EnableCellEditControl(bool enable = TRUE) / DisableCellEditControl()
EnableEditing(bool edit) —
GetDefaultEditor() —
GetDefaultRenderer() —
HideCellEditControl() —
IsCellEditControlEnabled() —
IsEditable() —
IsReadOnly(int row, int col) —
SaveEditControlValue() — XXX Note: should be mentioned that you want to call this before closing the grid's parent' window!
SetCellEditor(int row, int col, wxGridCellEditor* editor) — XXX Hmm, is this usable with table-based grids?
SetCellRenderer(int row, int col, wxGridCellRenderer* renderer) — XXX Hmm, is this usable with table-based grids?
SetDefaultEditor(wxGridCellEditor* editor) —
SetDefaultRenderer(wxGridCellRenderer* renderer) —
SetReadOnly(int row, int col, bool isReadOnly = TRUE) — XXX Not really sure where this goes...
ShowCellEditControl() —
GetDefaultEditorForCell(int row, int col) —
GetDefaultRendererForCell(int row, int col) —
GetDefaultEditorForType(const wxString& typeName) —
GetDefaultRendererForType(const wxString& typeName) —
GetCellEditor(int row, int col) — returning a wxPyGridCellEditor registered with the control using the grid's RegisterDataType, selected based on the data type specified by your wxPyGridTableBase's GetTypeName method
GetCellRenderer(int row, int col) — returning a wxPyGridCellRenderer registered with the control using the grid's RegisterDataType, selected based on the data type specified by your wxPyGridTableBase's GetTypeName method
Selections and the Cursor
The grid has both a selection set (which may include disjoint ranges) and a single-cell focus cursor. The selection set can contain arbitrary blocks of cells, while the cursor can only point to a single cell.
Cursor Manipulation
The API for interacting with the focus cursor is fairly simple.
GetGridCursorCol() — Retrieve the current cursor position
GetGridCursorRow() — Retrieve the current cursor position
MoveCursorDown/Left/Right/Up( bool expandSelection) — move a single cell in direction
MoveCursorDown/Left/Right/UpBlock( bool expandSelection) — move a single cell, or multiple cells if the intervening cells are empty.
MovePageDown() — Move cursor down such that the last visible row becomes the first
MovePageUp() — Move cursor up such that the first visible row becomes the last
SetGridCursor( int row, int col ) — set the cursor to the giving coordinate, also makes sure that the coordinate is visible on screen
MakeCellVisible( int row, int col ) — forces the particular cell to be visible, effectively works to scroll the grid to be given cell
Selection Set Management
The grid class does not provide a simple API for retrieving the current selection set. Instead, it is necessary to catch two different events and store their values to determine the current selection set. The following sample code illustrates how this is done. The _OnSelectedRange method is mapped using EVT_GRID_RANGE_SELECT( self.grid, self._OnSelectedRange ), while the _OnSelectedCell method is mapped using EVT_GRID_SELECT_CELL( self.grid, self._OnSelectedCell ). Because these are both command events, they can be mapped from the grid's parent window.
The following code sample shows tracking of currently selected rows in a table:
1 def _OnSelectedRange( self, event ):
2 """Internal update to the selection tracking list"""
3 if event.Selecting():
4 # adding to the list...
5 for index in range( event.GetTopRow(), event.GetBottomRow()+1):
6 if index not in self.currentSelection:
7 self.currentSelection.append( index )
8 else:
9 # removal from list
10 for index in range( event.GetTopRow(), event.GetBottomRow()+1):
11 while index in self.currentSelection:
12 self.currentSelection.remove( index )
13 self.ConfigureForSelection()
14 event.Skip()
15 def _OnSelectedCell( self, event ):
16 """Internal update to the selection tracking list"""
17 self.currentSelection = [ event.GetRow() ]
18 self.ConfigureForSelection()
19 event.Skip()
Note: the selection of a single cell will cause a range selection event with Selecting() == false and the entire grid as the selection set to be sent before the single-cell selection occurs.
Note: the Selecting() == false ranges will be sent if the user is control-clicking to deselect particular cells within the current selection set. Because of this, the "removal from list" sub-block above becomes somewhat complex when you need to store cell-selections, not just row selections. The wxoo project has a partial implementation of a grid-selection tracking class gridselectionset.py, but it merely forces the whole selection set to be represented as individual cells when a deselection is called (which can be spectacularly inefficient). Improved code welcome.
Other selection-set-management methods:
ClearSelection() — Clear all currently selected cell ranges (sends a range-selection event)
IsInSelection(int row, int col) — Query whether a particular cell is currently selected
SelectAll() — Strangely enough, select all cells in the table
SelectBlock( int topRow, int leftCol, int bottomRow, int rightCol, bool addToSelected = FALSE ) — Select a particular block of cells as an inclusive range. If addToSelected is true, add to the current selection set
SelectCol( int col, bool addToSelected = FALSE ) — Select all cells in a column
SelectRow(int row, bool addToSelected = FALSE) — Select all cells in a row
IsSelection() — True if there is a current selection
SetSelectionMode(wxGrid::wxGridSelectionModes selmode) — Determine whether cells, rows or columns are selected
General Display Configuration/Queries
EnableGridLines(bool enable = TRUE) — Enable drawing of the grid cell lines
ForceRefresh() — Force an Refresh of the grid's window
SetMargins(int extraWidth, int extraHeight) — Set padding space around the grid cells within the grid window
Geometry Queries
Feedback about the geometry displayed in the grid window.
BlockToDeviceRect(const wxGridCellCoords & topLeft, const wxGridCellCoords & bottomRight)
SelectionToDeviceRect()
CellToRect(int row, int col)
IsVisible(int row, int col, bool wholeCellVisible = TRUE)
XToCol(int x)
XToEdgeOfCol(int x) —
YToEdgeOfRow(int y) —
YToRow(int y) —
Column/Row Sizing
Default Sizes
The following methods allow for specifying default height and/or width values. These values are used when there is no explicitly set value for column width/height, (and where no auto-sizing is occurring (see below)).
SetDefaultColSize(int width, bool resizeExistingCols = FALSE) —
SetDefaultRowSize(int height, bool resizeExistingRows = FALSE) —
GetDefaultColSize(), GetDefaultRowSize() — return an integer representing the default pixel-width of the column headers or pixel-height of the row headers (respectively).
Auto-sizing of Cells to Contents
The following methods set the grid's cells to automatically size themselves to their content's prefered sizes. The prefered sizes are the values returned by the wxPyGridCellRenderer's GetBestSize method. XXX are these sizes stored (potential memory exhaustion), or re-queried for each redisplay?
Note: there is a bug in the current grid implementation that makes it impossible to edit cells which are wider than the grid's displayed client area (i.e. the size of the grid window minus the labels). For this reason you should ensure that your renderer's return conservative values from GetBestSize or avoid using automatic sizing until the bug is fixed.
AutoSize() — All columns and all rows
AutoSizeColumn(int col, bool setAsMin = TRUE) — A single column only
AutoSizeColumns(bool setAsMin = TRUE) — All columns
AutoSizeRow(int row, bool setAsMin = TRUE) — A single row only
AutoSizeRows(bool setAsMin = TRUE) — All rows
Overflow (Multiple-cell display/edit)
wxPython version 2.4.x has introduced a new (default) functionality which allows renderers and editors displayed in a particular cell to overwrite neighboring cells if their data values would not normally fit in the space allotted to the cell. This will break most renderers written for earlier versions of wxPython, so you may want to disable the functionality by default, or disable it for particular cells/rows/columns.
The overflow state is stored in wxGridCellAttr objects, with functions available on the grid for setting the entire-grid-default, and for setting the mode for individual cells/ranges. See below for discussion of wxGridCellAttr objects and their interactions with the grid.
SetDefaultCellOverflow( bool allow ) — Allows you to set the default "overflow" functionality for the grid, the default value is true, which differs from the operation of earlier wxPython versions.
GetDefaultCellOverflow() — Return a boolean representing the current default overflow state
SetCellOverflow( int row, int col, bool allow ) — Allows you to set the "overflow" functionality for a particular cell in the grid, see notes on wxGridCellAttrfor why you would normally not do this on table-based grid objects.
GetCellOverflow( int row, int col ) — Return a boolean representing the current overflow state for the given (row,col) coordinate
Setting Individual Heights/Widths (Exceptions to Defaults)
The following methods can explicitly query and or set individual column or row width or height values. Note: the grid will set up an array for storing exceptions to default widths, so extremely large grids may consume considerable memory storing these exceptions. I am unsure whether the return values from grid cell renderers are similarly stored (I would think not).
GetColSize(int col) —
GetRowSize(int row) —
SetColSize(int col, int width) —
SetRowSize(int row, int height) —
SetColMinimalWidth(int col, int width) —
SetRowMinimalHeight(int row, int width) —
Interactive Resizing
The following methods allow for querying and changing the grid's ability to resize columns and/or rows by dragging on the borders between columns or rows. The grid can also (and does by default) allow for changing heights/widths by dragging grid lines in the cell area of the grid display.
CanDragColSize() — Return a boolean value indicating whether we can currently change column sizes by dragging
CanDragRowSize() — Return a boolean value indicating whether we can currently change row sizes by dragging
CanDragGridSize() — Return a boolean value indicating whether we can currently change column and row sizes by dragging on grid lines.
EnableDragColSize(bool enable = TRUE) — Enable or disable changing column sizes by dragging on dividers between column labels
EnableDragRowSize(bool enable = TRUE) — Enable or disable changing row sizes by dragging on dividers between row labels
EnableDragGridSize(bool enable = TRUE) — Enable or disable changing row and column sizes by dragging on grid lines
Autosizing labels
wxPython does not (yet) allow auto sizing of row or column labels, but a little fiddling with device contexts can acheive this. This has been tested on Windows only and your mileage may vary.
1 def AutosizeLabels(self):
2 # Common setup.
3
4 devContext = wxScreenDC()
5 devContext.SetFont(self.gridCtrl.GetLabelFont())
6
7 # First do row labels.
8
9 maxWidth = 0
10 curRow = self.gridCtrl.GetNumberRows() - 1
11 while curRow >= 0:
12 curWidth = devContext.GetTextExtent("M%s"%(self.gridCtrl.GetRowLabelValue(curRow)))[0]
13 if curWidth > maxWidth:
14 maxWidth = curWidth
15 curRow = curRow - 1
16 self.gridCtrl.SetRowLabelSize(maxWidth)
17
18 # Then column labels.
19
20 maxHeight = 0
21 curCol = self.gridCtrl.GetNumberCols() - 1
22 while curCol >= 0:
23 (w,h,d,l) = devContext.GetFullTextExtent(self.gridCtrl.GetColLabelValue(curCol))
24 curHeight = h + d + l + 4
25 if curHeight > maxHeight:
26 maxHeight = curHeight
27 curCol = curCol - 1
28 self.gridCtrl.SetColLabelSize(maxHeight)
This code operates on a control with a wxGrid-derived instance named gridCtrl. I haven't bothered to implement this very cleanly (ie. as a class inherited from wxGrid). I leave that as an exercise for the reader :-).
The code basically traverses each row label, calculating its width (including an extraneous "M" for spacing) and storing the widest label. Then it simply sets the row label column to that width.
It also does a similar thing for column label heights, except it calculates the desired height using character height, descender and leading gap and adding a nice 4-pixel buffer for padding.
Update:
According to the docs here, as of wxPython version 2.8.8. the following should auto-size column and row labels. I replaced the code snippet above in my latest project (a Virtual Grid using wx.2.9.3 on Windows 7) with:
self.SetRowLabelSize(wx.grid.GRID_AUTOSIZE) self.SetColLabelSize(wx.grid.GRID_AUTOSIZE)
and it appears to be working well. In my situation, wx.grid.GRID_AUTOSIZE fixed the problem I was having with auto-sizing multi-line column labels. --js
Attribute Objects (Cell Display Properties)
wxGridCellAttr objects provide a mechanism for altering the display of particular rows, columns, or individual cells. In table-based grids, you determine what attribute object will be used for a particular grid cell by overriding the wxPyGridTableBase's GetAttr method to return a grid cell attribute object.
Note: the following methods are not really appropriate for a table-based grid, as this will force the grid to create storage for each column's attribute (which may exhaust memory on large grids). See: wxPyGridTableBase's GetAttr method for the appropriate table-based approach. With that said, there might be some use for them where you want to customise particular rows in the grid and for some reason want to keep that code out of your table.
Note: there are methods to set the default values for the default attribute object used by the grid when no other attributes available. See section "Defaults and Overall Settings" below.
SetColAttr(int col, wxGridCellAttr* attr) — Set an attribute object as default for a particular column in the grid.
SetRowAttr(int row, wxGridCellAttr* attr) — Set an attribute object as default for a particular row in the grid.
CanHaveAttributes() — The purpose of this method is unclear, but it appears to be a query to determine whether the grid has allocated storage for attribute objects. I have no idea whether you could override the method, but I assume not (as the grid is not generally overridable).
Label (Header) Display/Edit Configuration
Each column and row in the table has an associated label (columns having their labels at the top of the grid, rows on the left-hand side).
GetColLabelAlignment(), GetRowLabelAlignment() — Returns a two-tuple of (horizontal, vertical) constants representing the alignment of the text labels within the column and row header spaces (respectively)
horizontal — wxALIGN_LEFT, wxALIGN_CENTRE or wxALIGN_RIGHT
vertical — wxALIGN_TOP, wxALIGN_CENTRE or wxALIGN_BOTTOM
GetColLabelSize(), GetRowLabelSize() — Return an integer representing the current pixel-height of the column headers, or the current pixel width of the row headers (respectively)
GetDefaultColLabelSize(), GetDefaultRowLabelSize() — return an integer representing the default pixel-height of the column headers, or default pixel width of the row headers (respectively)
GetLabelBackgroundColour() — Get the colour used for painting the label background
GetLabelFont() — Get the font used for the label text
GetLabelTextColour() — Get the colour used for the label text
SetColLabelAlignment(int horiz, int vert), SetRowLabelAlignment(int horiz, int vert) — Sets the horizontal and vertical alignment of column or row (respectively) label text.
horizontal — wxALIGN_LEFT, wxALIGN_CENTRE or wxALIGN_RIGHT
vertical — wxALIGN_TOP, wxALIGN_CENTRE or wxALIGN_BOTTOM
SetColLabelSize(int height) — Set the column label height to a particular pixel value
SetRowLabelSize(int width) — Set the row label width to a particular pixel value
SetLabelBackgroundColour(const wxColour& colour) — Set the colour used for painting the label background
SetLabelFont(const wxFont& font) — Set the font used for the label text
SetLabelTextColour(const wxColour& colour) — Set the colour used for the label text
Label Queries Passed to Table
GetColLabelValue(int col) — The value returned by wxPyGridTableBase's GetColLabelValue
GetRowLabelValue(int row) — The value returned by wxPyGridTableBase's GetRowLabelValue
SetColLabelValue(int col, const wxString& value) — wxPyGridTableBase's SetColLabelValue
SetRowLabelValue(int row, const wxString& value) — wxPyGridTableBase's SetRowLabelValue
Cell Display/Edit Configuration
Generic Cell Display (alignment, fonts, colours)
GetCellAlignment(int row, int col, int* horiz, int* vert) —
GetCellBackgroundColour(int row, int col) —
GetCellFont(int row, int col) —
GetCellTextColour(int row, int col) —
SetCellAlignment(int row, int col, int horiz, int vert) —
SetCellFont(int row, int col, const wxFont& font) —
SetCellTextColour(int row, int col, const wxColour& colour) —
SetColFormatBool(int col) —
SetColFormatNumber(int col) —
SetColFormatFloat(int col, int width = -1, int precision = -1) —
SetColFormatCustom(int col, const wxString& typeName) —
GetSelectionBackground() —
GetSelectionForeground() —
SetSelectionBackground(const wxColour& c) —
SetSelectionForeground(const wxColour& c) —
See also:
- Overflow (Multiple-cell display/edit) (above) Attribute Objects (above)
Defaults and Overall Settings
GetGridLineColour() —
GetDefaultCellAlignment() —
GetDefaultCellBackgroundColour() —
GetDefaultCellFont() —
GetDefaultCellTextColour() —
GridLinesEnabled() —
SetDefaultCellAlignment(int horiz, int vert) —
SetDefaultCellBackgroundColour(const wxColour& colour) —
SetDefaultCellFont(const wxFont& font) —
SetGridLineColour(const wxColour&colour) —
Cell-Related Queries Passed to Table
GetCellEditor(int row, int col) — returning a wxPyGridCellEditor registered with the control using the grid's RegisterDataType, selected based on the data type specified by your wxPyGridTableBase's GetTypeName method
GetCellRenderer(int row, int col) — returning a wxPyGridCellRenderer registered with the control using the grid's RegisterDataType, selected based on the data type specified by your wxPyGridTableBase's GetTypeName method
GetCellValue(int row, int col) — wxPyGridTableBase's GetValue Note: this works with string data only!
IsCurrentCellReadOnly() — XXX Not sure about what this does under the covers
SetCellValue(int row, int col, const wxString& s) — wxPyGridTableBase's SetValue==
DragAndDrop onto Grid
wxGrid like most windows can be the target of a DragAndDrop action such as wxFileDropTarget. Note that for wx < 2.9, one should set the drop target to be the appropriate subwidget(s) of the grid [e.g. grid.GetGridWindow().SetDropTarget(drptgt) or GetGridRowLabelWindow(), etc.] Do not use the grid itself or code will not be completely portable (see wxPython-users on drop problem with grid on Mac).
The basic way of to handle the drop is as follows:
1 class GridFileDropTarget(wxFileDropTarget):
2 def __init__(self, grid):
3 wxFileDropTarget.__init__(self)
4 self.grid = grid
5
6 def OnDropFiles(self, x, y, filenames):
7 row, col = self.grid.XYToCell(x, y)
8 if row > -1 and col > -1:
9 self.grid.SetValue(row, col, filenames[0])
10 self.grid.Refresh()
Note: I'm assuming here that the grid has a user supplied method self.SetValue. This method can either just call wxGrid.SetCellValue or do the appropriate logic for virtual grids. self.grid.Refresh() just tells the grid that it needs to be redrawn for the new data.
Virtual grids, however, have a significant problem here. It seems that wxGrid.XYToCell is broken for virtual grids. (A virtual grid is a grid that uses wxGrid.SetTable on a PyGridTableBase instance.) Here is a replacement to compute XYToCell to compute the current cell positions. Simply add this method to your virtual grid and away you go!
1 def XYToCell(self, x, y):
2 # For virtual grids, XYToCell doesn't work properly
3 # For some reason, the width and heights of the labels
4 # are not computed properly and thw row and column
5 # returned are computed as if the window wasn't
6 # scrolled
7 # This function replaces XYToCell for Virtual Grids
8
9 rowwidth = self.GetGridRowLabelWindow().GetRect().width
10 colheight = self.GetGridColLabelWindow().GetRect().height
11 yunit, xunit = self.GetScrollPixelsPerUnit()
12 xoff = self.GetScrollPos(wxHORIZONTAL) * xunit
13 yoff = self.GetScrollPos(wxVERTICAL) * yunit
14
15 # the solution is to offset the x and y values
16 # by the width and height of the label windows
17 # and then adjust by the scroll position
18 # Then just go through the columns and rows
19 # incrementing by the current column and row sizes
20 # until the offset points lie within the computed
21 # bounding boxes.
22 x += xoff - rowwidth
23 xpos = 0
24 for col in range(self.GetNumberCols()):
25 nextx = xpos + self.GetColSize(col)
26 if xpos <= x <= nextx:
27 break
28 xpos = nextx
29
30 y += yoff - colheight
31 ypos = 0
32 for row in range(self.GetNumberRows()):
33 nexty = ypos + self.GetRowSize(row)
34 if ypos <= y <= nexty:
35 break
36 ypos = nexty
37
38 return row, col
DragAndDrop From Grid
For complete functionality, wxGrids require access to most mouse functions. While the main grid window can be used as a source for mouse drags, this can become problematic if you still want to resize cells or allow clicks and double clicks to activate the cell. A good comprimise is to allow dragging from the rows of the grids. The following Mixin enables row labels to be used as drag sources. The drag source activates a TextDrag that contains the string represenation of the selected wells, i.e. "[0,1,2,3]". More complicated DragAndDrop sources can be enabled by overriding the StartDrag method function of the DragGridRowMixin below.
1 '''DragGridRowMixin
2
3 Allows users to drag rows off of grids as Drag and Drop actions.
4 Usage:
5 mixin with a Grid class.
6
7 class MyGrid(Grid, DragGridRowMixin):
8 def __init__(self, parent, id):
9 # must initialize grid before DragGridRowMixin
10 Grid.__init__(self, parent, id)
11 DragGridRowMixin.__init__(self)
12
13 To change the type of data being sent by the Grid, override the method function
14 def StartDrag(self, selectedrows):
15 """start a drag operation using selectedrows"""
16
17 Currently a drag operation is performed with a PyTextDataObject containing
18 the data str(selectedrows)
19 '''
20 import wx
21 from wx.grid import Grid
22
23
24 class DragGridRowMixin:
25 """This mixin allows the use of grid rows as drag sources, you can drag
26 rows off of a grid to a text target. Only row labels are draggable, internal
27 cell contents are not.
28
29 You must Initialize this class *after* the wxGrid initialization.
30 example
31 class MyGrid(Grid, DragGridRowMixin):
32 def __init__(self, parent, id):
33 Grid.__init__(self, parent, id)
34 DragGridRowMixin.__init__(self)
35 """
36 def __init__(self):
37 rowWindow = self.GetGridRowLabelWindow()
38 # various flags to indicate whether we should have a select
39 # event or a drag event
40 self._potentialDrag = False
41 self._startRow = None
42 self._shift, self._ctrl = None, None
43
44 wx.EVT_LEFT_DOWN(rowWindow, self.OnDragGridLeftDown)
45 wx.EVT_LEFT_UP(rowWindow, self.OnDragGridLeftUp)
46 wx.EVT_LEFT_UP(self, self.OnDragGridLeftUp)
47 wx.EVT_MOTION(rowWindow, self.OnDragGridMotion)
48 wx.EVT_KEY_DOWN(self, self.OnDragGridKeyDown)
49 wx.EVT_KEY_UP(self, self.OnDragGridKeyUp)
50
51 def OnDragGridKeyDown(self, evt):
52 """Set the states of the shift and ctrl keys"""
53 self._shift, self._ctrl = (evt.ShiftDown(), evt.ControlDown())
54
55 def OnDragGridKeyUp(self, evt):
56 """unset the states of the shift and ctrl keys"""
57 self._shift, self._ctrl = None, None
58
59 def OnDragGridLeftDown(self, evt):
60 """The left button is down so see if we are selecting rows or not
61 and do the appropriate thing. We need to do this because we
62 are blocking the rowlabels so rows won't be selected."""
63 x,y = evt.GetX(), evt.GetY()
64 row, col = self.DragGridRowXYToCell(x,y, colheight=0)
65
66 if not self._shift and not self._ctrl:
67 self._startRow = row
68 self.SelectRow(row)
69 elif self._shift and not self._ctrl:
70 if self._startRow > row:
71 start, end = row, self._startRow
72 else:
73 start, end = self._startRow, row
74 for row in range(start, end+1):
75 self.SelectRow(row, True)
76 elif self._ctrl:
77 self.SelectRow(row, True)
78 self._potentialDrag = True
79
80 def OnDragGridLeftUp(self, evt):
81 """We are not dragging anymore, so unset the potentialDrag flag"""
82 self._potentialDrag = False
83 evt.Skip()
84
85 def OnDragGridMotion(self, evt):
86 """We are moving so see whether this should be a drag event or not"""
87 if not self._potentialDrag:
88 evt.Skip()
89 return
90
91 x,y = evt.GetX(), evt.GetY()
92 row, col = self.DragGridRowXYToCell(x,y, colheight=0)
93 rows = self.GetSelectedRows()
94 if not rows or row not in rows:
95 evt.Skip()
96 return
97 self.StartDrag(rows)
98
99 def DragGridRowXYToCell(self, x, y, colheight=None, rowwidth=None):
100 # For virtual grids, XYToCell doesn't work properly
101 # For some reason, the width and heights of the labels
102 # are not computed properly and thw row and column
103 # returned are computed as if the window wasn't
104 # scrolled
105 # This function replaces XYToCell for Virtual Grids
106
107 if rowwidth is None:
108 rowwidth = self.GetGridRowLabelWindow().GetRect().width
109 if colheight is None:
110 colheight = self.GetGridColLabelWindow().GetRect().height
111 yunit, xunit = self.GetScrollPixelsPerUnit()
112 xoff = self.GetScrollPos(wx.HORIZONTAL) * xunit
113 yoff = self.GetScrollPos(wx.VERTICAL) * yunit
114
115 # the solution is to offset the x and y values
116 # by the width and height of the label windows
117 # and then adjust by the scroll position
118 # Then just go through the columns and rows
119 # incrementing by the current column and row sizes
120 # until the offset points lie within the computed
121 # bounding boxes.
122 x += xoff - rowwidth
123 xpos = 0
124 for col in range(self.GetNumberCols()):
125 nextx = xpos + self.GetColSize(col)
126 if xpos <= x <= nextx:
127 break
128 xpos = nextx
129
130 y += yoff - colheight
131 ypos = 0
132 for row in range(self.GetNumberRows()):
133 nexty = ypos + self.GetRowSize(row)
134 if ypos <= y <= nexty:
135 break
136 ypos = nexty
137
138 return row, col
139
140 def StartDrag(self, selectedrows):
141 """This starts the drag event, override this to send different drag
142 types."""
143 tdo = wx.PyTextDataObject(str(selectedrows))
144 # Create a Drop Source Object, which enables the Drag operation
145 tds = wx.DropSource(self.GetGridRowLabelWindow())
146 # Associate the Data to be dragged with the Drop Source Object
147 tds.SetData(tdo)
148 # Intiate the Drag Operation
149 tds.DoDragDrop(wx.true)
150
151 if __name__ == "__main__":
152 # -----------------------------------------------------------------------
153 # Testing
154 class GridTextDropTarget(wx.TextDropTarget):
155 def __init__(self, grid):
156 wx.TextDropTarget.__init__(self)
157 self.grid = grid
158
159 def OnDropText(self, x, y, text):
160 # XYToCell doesn't behave quite right, so we'll just
161 # grab the DragGridRowXYToCell that we fixed,
162 # see the wx.Grid wiki for a better explanation
163 # http://wiki.wxpython.org/index.cgi/wxGrid
164 row, col = self.grid.DragGridRowXYToCell(x, y)
165 if row > -1 and col > -1:
166 self.grid.SetCellValue(row, col, text)
167 self.grid.Refresh()
168
169 class T(Grid, DragGridRowMixin):
170 def __init__(self, parent, id, title):
171 Grid.__init__(self, parent, id)
172 self.CreateGrid(25,25)
173 DragGridRowMixin.__init__(self)
174 self.SetDropTarget(GridTextDropTarget(self))
175
176 app = wx.PySimpleApp()
177 frame = wx.Frame(None, -1, "hello")
178 foo = T(frame, -1, "hello")
179 frame.Show()
180 app.MainLoop()
Capturing Cell Edits when focus changes
The default behavior of the grid control is to revert to the old value if the user changes focus before hitting enter. This can frustrate users when their edits are gone because they have clicked on another region of the GUI.
A wxGrid is actually a control made of several wxWindow components: The main grid, the table of cells, and a cell editor when a value is being modified.
To make the grid save its cell data, it's necessary to first bind to the EVT_GRID_EDITOR_CREATED event. Inside that event, then bind to the EVT_KILL_FOCUS event for the newly created control.
Note: This code is written from the perspective of the Frame containing the grid. It may of course be rewritten to embed the event handling within a class inheriting from wxGrid.
1 def __init__(self):
2 self.grid = wx.grid.Grid(self, -1)
3 self.Bind(wx.EVT_GRID_EDITOR_CREATED, self.OnGridEditorCreated)
4
5 def OnGridEditorCreated(self, event):
6 """ Bind the kill focus event to the newly instantiated cell editor """
7 editor = event.GetControl()
8 editor.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
9 event.Skip()
10
11 def OnKillFocus(self, event):
12 # Cell editor's grandparent, the grid GridWindow's parent, is the grid.
13 grid = event.GetEventObject().GetGrandParent()
14 grid.SaveEditControlValue()
15 grid.HideCellEditControl()
16 event.Skip()
The above code snippet handles the case where a user changes focus by clicking on another control in your interface. However, clicking on the menu of a frame does not remove focus from the edit control, so the above code won't be triggered. If your users are like mine and fond of choosing Save from the menus without closing the cell editor, you can modify the above to compensate.
1 def __init__(self):
2 self.grid = wx.grid.Grid(self, -1)
3 self.Bind(wx.EVT_GRID_EDITOR_CREATED, self.OnGridEditorCreated)
4 self.Bind(wx.EVT_MENU_OPEN, self.OnMenuOpen)
5
6 def OnGridEditorCreated(self, event):
7 """ Bind the kill focus event to the newly instantiated cell editor """
8 editor = event.GetControl()
9 editor.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
10 event.Skip()
11
12 def OnKillFocus(self, event):
13 # Cell editor's grandparent, the grid GridWindow's parent, is the grid.
14 grid = event.GetEventObject().GetGrandParent()
15 grid.SaveEditControlValue()
16 grid.HideCellEditControl()
17 event.Skip()
18
19 def OnMenuOpen(self, event):
20 self.grid.HideCellEditControl()
The EVT_MENU_OPEN handler above will generate an EVT_KILL_FOCUS event, saving the contents of the editor. Neither call to hide generates an EVT_GRID_EDITOR_HIDDEN event, but the call to Save does generate an EVT_GRID_CELL_CHANGE event, so you only want to call it once, in the EVT_KILL_FOCUS handler.
Back to the wxGrid Manual
- When dealing with large grids the RANGE_SELECT method described in 1.4.2 can be quite slow when selecting whole columns/rows. The following code treats that special case seperately and is much faster:
1 def _OnRangeSelect:
2 top,bottom = event.GetTopRow(),event.GetBottomRow()+1
3 left,right = event.GetLeftCol(),event.GetRightCol()+1
4
5 if event.Selecting():
6 # adding to the list...
7 if bottom-top == self.GetNumberRows():
8 self.currentSelection[0] = range(top, bottom)
9 for index in range(left,right):
10 if index not in self.currentSelection[1]:
11 self.currentSelection[1].append(index)
12 elif right-left == self.GetNumberCols():
13 self.currentSelection[1] = range(left, right)
14 for index in range(top,bottom):
15 if index not in self.currentSelection[0]:
16 self.currentSelection[0].append(index)
17 else:
18 for index in range(top, bottom):
19 if index not in self.currentSelection[0]:
20 self.currentSelection[0].append(index)
21 for index in range(left, right):
22 if index not in self.currentSelection[1]:
23 self.currentSelection[1].append(index)
24 else:
25 # removal from list
26 if bottom-top == self.GetNumberRows():
27 for index in range(left,right):
28 while index in self.currentSelection[1]:
29 self.currentSelection[1].remove(index)
30 elif right-left == self.GetNumberCols():
31 for index in range(top,bottom):
32 while index in self.currentSelection[0]:
33 self.currentSelection[0].remove(index)
34 else:
35 for index in range(top, bottom):
36 while index in self.currentSelection[0]:
37 self.currentSelection[0].remove(index)
38 for index in range(left, right):
39 while index in self.currentSelection[1]:
40 self.currentSelection[1].remove(index)
Additionally one has to catch the mouse click on the row/col labels to reset the current selection:
--Christian