2 # CSheet - A wxPython spreadsheet class.
3 # This is free software. Feel free to adapt it as you like.
4 # Author: Mark F. Russo (russomf@hotmail.com) 2002/01/31
5 #---------------------------------------------------------------------------
6 # 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net)
8 # o 2.5 compatability update.
16 #---------------------------------------------------------------------------
17 class CTextCellEditor(wx
.TextCtrl
):
18 """ Custom text control for cell editing """
19 def __init__(self
, parent
, id, grid
):
20 wx
.TextCtrl
.__init
__(self
, parent
, id, "", style
=wx
.NO_BORDER
)
21 self
._grid
= grid
# Save grid reference
22 self
.Bind(wx
.EVT_CHAR
, self
.OnChar
)
24 def OnChar(self
, evt
): # Hook OnChar for custom behavior
25 """Customizes char events """
26 key
= evt
.GetKeyCode()
27 if key
== wx
.WXK_DOWN
:
28 self
._grid
.DisableCellEditControl() # Commit the edit
29 self
._grid
.MoveCursorDown(False) # Change the current cell
30 elif key
== wx
.WXK_UP
:
31 self
._grid
.DisableCellEditControl() # Commit the edit
32 self
._grid
.MoveCursorUp(False) # Change the current cell
33 elif key
== wx
.WXK_LEFT
:
34 self
._grid
.DisableCellEditControl() # Commit the edit
35 self
._grid
.MoveCursorLeft(False) # Change the current cell
36 elif key
== wx
.WXK_RIGHT
:
37 self
._grid
.DisableCellEditControl() # Commit the edit
38 self
._grid
.MoveCursorRight(False) # Change the current cell
40 evt
.Skip() # Continue event
42 #---------------------------------------------------------------------------
43 class CCellEditor(wx
.grid
.PyGridCellEditor
):
44 """ Custom cell editor """
45 def __init__(self
, grid
):
46 wx
.grid
.PyGridCellEditor
.__init
__(self
)
47 self
._grid
= grid
# Save a reference to the grid
49 def Create(self
, parent
, id, evtHandler
):
50 """ Create the actual edit control. Must derive from wxControl.
53 self
._tc
= CTextCellEditor(parent
, id, self
._grid
)
54 self
._tc
.SetInsertionPoint(0)
55 self
.SetControl(self
._tc
)
57 self
._tc
.PushEventHandler(evtHandler
)
59 def SetSize(self
, rect
):
60 """ Position/size the edit control within the cell rectangle. """
61 # Size text control to exactly overlay in-cell editing
62 self
._tc
.SetDimensions(rect
.x
+3, rect
.y
+3, rect
.width
-2, rect
.height
-2)
64 def Show(self
, show
, attr
):
65 """ Show or hide the edit control. Use the attr (if not None)
66 to set colors or fonts for the control.
68 self
.base_Show(show
, attr
)
70 def PaintBackground(self
, rect
, attr
):
71 """ Draws the part of the cell not occupied by the edit control. The
72 base class version just fills it with background colour from the
75 # Call base class method.
76 self
.base_PaintBackground(self
, rect
, attr
)
78 def BeginEdit(self
, row
, col
, grid
):
79 """ Fetch the value from the table and prepare edit control to begin editing.
80 Set the focus to the edit control. Must Override.
82 self
._startValue
= grid
.GetTable().GetValue(row
, col
)
83 self
._tc
.SetValue(self
._startValue
)
86 # Select the text when initiating an edit so that subsequent typing
87 # replaces the contents.
88 self
._tc
.SetSelection(0, self
._tc
.GetLastPosition())
90 def EndEdit(self
, row
, col
, grid
):
91 """ Commit editing the current cell. Returns True if the value has changed.
92 If necessary, the control may be destroyed. Must Override.
94 changed
= False # Assume value not changed
95 val
= self
._tc
.GetValue() # Get value in edit control
96 if val
!= self
._startValue
: # Compare
97 changed
= True # If different then changed is True
98 grid
.GetTable().SetValue(row
, col
, val
) # Update the table
99 self
._startValue
= '' # Clear the class' start value
100 self
._tc
.SetValue('') # Clear contents of the edit control
105 """ Reset the value in the control back to its starting value. Must Override. """
106 self
._tc
.SetValue(self
._startValue
)
107 self
._tc
.SetInsertionPointEnd()
109 def IsAcceptedKey(self
, evt
):
110 """ Return True to allow the given key to start editing. The base class
111 version only checks that the event has no modifiers. F2 is special
112 and will always start the editor.
114 return (not (evt
.ControlDown() or evt
.AltDown())
115 and evt
.GetKeyCode() != wx
.WXK_SHIFT
)
117 def StartingKey(self
, evt
):
118 """ If the editor is enabled by pressing keys on the grid, this will be
119 called to let the editor react to that first key.
121 key
= evt
.GetKeyCode() # Get the key code
122 ch
= None # Handle num pad keys
123 if key
in [ wx
.WXK_NUMPAD0
, wx
.WXK_NUMPAD1
, wx
.WXK_NUMPAD2
, wx
.WXK_NUMPAD3
,
124 wx
.WXK_NUMPAD4
, wx
.WXK_NUMPAD5
, wx
.WXK_NUMPAD6
, wx
.WXK_NUMPAD7
,
125 wx
.WXK_NUMPAD8
, wx
.WXK_NUMPAD9
]:
126 ch
= chr(ord('0') + key
- wx
.WXK_NUMPAD0
)
128 elif key
== wx
.WXK_BACK
: # Empty text control when init w/ back key
131 elif key
< 256 and key
>= 0 and chr(key
) in string
.printable
:
133 if not evt
.ShiftDown():
136 if ch
is not None: # If are at this point with a key,
137 self
._tc
.SetValue(ch
) # replace the contents of the text control.
138 self
._tc
.SetInsertionPointEnd() # Move to the end so that subsequent keys are appended
142 def StartingClick(self
):
143 """ If the editor is enabled by clicking on the cell, this method will be
144 called to allow the editor to simulate the click on the control.
149 """ Final cleanup """
153 """ Create a new object which is the copy of this one. Must Override. """
156 #---------------------------------------------------------------------------
157 class CSheet(wx
.grid
.Grid
):
158 def __init__(self
, parent
):
159 wx
.grid
.Grid
.__init
__(self
, parent
, -1)
162 self
._lastCol
= -1 # Init last cell column clicked
163 self
._lastRow
= -1 # Init last cell row clicked
164 self
._selected
= None # Init range currently selected
165 # Map string datatype to default renderer/editor
166 self
.RegisterDataType(wx
.grid
.GRID_VALUE_STRING
,
167 wx
.grid
.GridCellStringRenderer(),
170 self
.CreateGrid(4, 3) # By default start with a 4 x 3 grid
171 self
.SetColLabelSize(18) # Default sizes and alignment
172 self
.SetRowLabelSize(50)
173 self
.SetRowLabelAlignment(wx
.ALIGN_RIGHT
, wx
.ALIGN_BOTTOM
)
174 self
.SetColSize(0, 75) # Default column sizes
175 self
.SetColSize(1, 75)
176 self
.SetColSize(2, 75)
179 self
.Bind(wx
.grid
.EVT_GRID_CELL_LEFT_CLICK
, self
.OnLeftClick
)
180 self
.Bind(wx
.grid
.EVT_GRID_CELL_RIGHT_CLICK
, self
.OnRightClick
)
181 self
.Bind(wx
.grid
.EVT_GRID_CELL_LEFT_DCLICK
, self
.OnLeftDoubleClick
)
182 self
.Bind(wx
.grid
.EVT_GRID_RANGE_SELECT
, self
.OnRangeSelect
)
183 self
.Bind(wx
.grid
.EVT_GRID_ROW_SIZE
, self
.OnRowSize
)
184 self
.Bind(wx
.grid
.EVT_GRID_COL_SIZE
, self
.OnColSize
)
185 self
.Bind(wx
.grid
.EVT_GRID_CELL_CHANGE
, self
.OnCellChange
)
186 self
.Bind(wx
.grid
.EVT_GRID_SELECT_CELL
, self
.OnGridSelectCell
)
188 def OnGridSelectCell(self
, event
):
189 """ Track cell selections """
190 # Save the last cell coordinates
191 self
._lastRow
, self
._lastCol
= event
.GetRow(), event
.GetCol()
194 def OnRowSize(self
, event
):
197 def OnColSize(self
, event
):
200 def OnCellChange(self
, event
):
203 def OnLeftClick(self
, event
):
204 """ Override left-click behavior to prevent left-click edit initiation """
205 # Save the cell clicked
206 currCell
= (event
.GetRow(), event
.GetCol())
208 # Suppress event if same cell clicked twice in a row.
209 # This prevents a single-click from initiating an edit.
210 if currCell
!= (self
._lastRow
, self
._lastCol
): event
.Skip()
212 def OnRightClick(self
, event
):
213 """ Move grid cursor when a cell is right-clicked """
214 self
.SetGridCursor( event
.GetRow(), event
.GetCol() )
217 def OnLeftDoubleClick(self
, event
):
218 """ Initiate the cell editor on a double-click """
219 # Move grid cursor to double-clicked cell
220 if self
.CanEnableCellControl():
221 self
.SetGridCursor( event
.GetRow(), event
.GetCol() )
222 self
.EnableCellEditControl(True) # Show the cell editor
225 def OnRangeSelect(self
, event
):
226 """ Track which cells are selected so that copy/paste behavior can be implemented """
227 # If a single cell is selected, then Selecting() returns False (0)
228 # and range coords are entire grid. In this case cancel previous selection.
229 # If more than one cell is selected, then Selecting() is True (1)
230 # and range accurately reflects selected cells. Save them.
231 # If more cells are added to a selection, selecting remains True (1)
232 self
._selected
= None
233 if event
.Selecting():
234 self
._selected
= ((event
.GetTopRow(), event
.GetLeftCol()),
235 (event
.GetBottomRow(), event
.GetRightCol()))
239 """ Copy the currently selected cells to the clipboard """
240 # TODO: raise an error when there are no cells selected?
241 if self
._selected
== None: return
242 ((r1
, c1
), (r2
, c2
)) = self
._selected
244 # Build a string to put on the clipboard
245 # (Is there a faster way to do this in Python?)
246 crlf
= chr(13) + chr(10)
249 for row
in range(r1
, r2
+1):
250 for col
in range(c1
, c2
):
251 s
+= self
.GetCellValue(row
,col
)
253 s
+= self
.GetCellValue(row
, c2
)
256 # Put the string on the clipboard
257 if wx
.TheClipboard
.Open():
258 wx
.TheClipboard
.Clear()
259 wx
.TheClipboard
.SetData(wx
.TextDataObject(s
))
260 wx
.TheClipboard
.Close()
263 """ Paste the contents of the clipboard into the currently selected cells """
264 # (Is there a better way to do this?)
265 if wx
.TheClipboard
.Open():
266 td
= wx
.TextDataObject()
267 success
= wx
.TheClipboard
.GetData(td
)
268 wx
.TheClipboard
.Close()
269 if not success
: return # Exit on failure
270 s
= td
.GetText() # Get the text
272 crlf
= chr(13) + chr(10) # CrLf characters
273 tab
= chr(9) # Tab character
275 rows
= s
.split(crlf
) # split into rows
276 rows
= rows
[0:-1] # leave out last element, which is always empty
277 for i
in range(0, len(rows
)): # split rows into elements
278 rows
[i
] = rows
[i
].split(tab
)
280 # Get the starting and ending cell range to paste into
281 if self
._selected
== None: # If no cells selected...
282 r1
= self
.GetGridCursorRow() # Start the paste at the current location
283 c1
= self
.GetGridCursorCol()
284 r2
= self
.GetNumberRows()-1 # Go to maximum row and col extents
285 c2
= self
.GetNumberCols()-1
286 else: # If cells selected, only paste there
287 ((r1
, c1
), (r2
, c2
)) = self
._selected
289 # Enter data into spreadsheet cells one at a time
290 r
= r1
# Init row and column counters
292 for row
in rows
: # Loop over all rows
293 for element
in row
: # Loop over all row elements
294 self
.SetCellValue(r
, c
, str(element
)) # Set cell value
295 c
+= 1 # Increment the column counter
296 if c
> c2
: break # Do not exceed maximum column
298 if r
> r2
: break # Do not exceed maximum row
302 """ Clear the currently selected cells """
303 if self
._selected
== None: # If no selection...
304 r
= self
.GetGridCursorRow() # clear only current cell
305 c
= self
.GetGridCursorCol()
306 self
.SetCellValue(r
, c
, "")
307 else: # Otherwise clear selected cells
308 ((r1
, c1
), (r2
, c2
)) = self
._selected
309 for r
in range(r1
, r2
+1):
310 for c
in range(c1
, c2
+1):
311 self
.SetCellValue(r
, c
, "")
313 def SetNumberRows(self
, numRows
=1):
314 """ Set the number of rows in the sheet """
315 # Check for non-negative number
316 if numRows
< 0: return False
318 # Adjust number of rows
319 curRows
= self
.GetNumberRows()
320 if curRows
< numRows
:
321 self
.AppendRows(numRows
- curRows
)
322 elif curRows
> numRows
:
323 self
.DeleteRows(numRows
, curRows
- numRows
)
327 def SetNumberCols(self
, numCols
=1):
328 """ Set the number of columns in the sheet """
329 # Check for non-negative number
330 if numCols
< 0: return False
332 # Adjust number of rows
333 curCols
= self
.GetNumberCols()
334 if curCols
< numCols
:
335 self
.AppendCols(numCols
- curCols
)
336 elif curCols
> numCols
:
337 self
.DeleteCols(numCols
, curCols
- numCols
)