Work with ctrl-c and ctrl-v to copy and paste into a wx.Grid in wxPython
I improved the code and added a few things like unlimited undo, deleting colums, rows, mouse right click popup etc. Tested in Python 2 & 3
import wx.grid
import wx
class MyGrid(wx.grid.Grid):
def __init__(self, parent):
wx.grid.Grid.__init__(self, parent, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0)
self.Bind(wx.EVT_KEY_DOWN, self.on_key)
self.Bind(wx.grid.EVT_GRID_CELL_CHANGING, self.on_change)
self.Bind(wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.on_label_right_click)
self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.on_cell_right_click)
self.selected_rows = []
self.selected_cols = []
self.history = []
def get_col_headers(self):
return [self.GetColLabelValue(col) for col in range(self.GetNumberCols())]
def get_table(self):
for row in range(self.GetNumberRows()):
result = {}
for col, header in enumerate(self.get_col_headers()):
result[header] = self.GetCellValue(row, col)
yield result
def add_rows(self, event):
for row in self.selected_rows:
self.InsertRows(row)
self.add_history({"type": "add_rows", "rows": self.selected_rows})
def delete_rows(self, event):
self.cut(event)
rows = []
for row in reversed(self.selected_rows):
rows.append((
row,
{ # More attributes can be added
"label": self.GetRowLabelValue(row),
"size": self.GetRowSize(row)
}
))
self.DeleteRows(row)
self.add_history({"type": "delete_rows", "rows": rows})
def add_cols(self, event):
for col in self.selected_cols:
self.InsertCols(col)
self.add_history({"type": "add_cols", "cols": self.selected_cols})
def delete_cols(self, event):
self.delete(event)
cols = []
for col in reversed(self.selected_cols):
cols.append((
col,
{ # More attributes can be added
"label": self.GetColLabelValue(col),
"size": self.GetColSize(col)
}
))
self.DeleteCols(col)
self.add_history({"type": "delete_cols", "cols": cols})
def on_cell_right_click(self, event):
menus = [(wx.NewId(), "Cut", self.cut),
(wx.NewId(), "Copy", self.copy),
(wx.NewId(), "Paste", self.paste)]
popup_menu = wx.Menu()
for menu in menus:
if menu is None:
popup_menu.AppendSeparator()
continue
popup_menu.Append(menu[0], menu[1])
self.Bind(wx.EVT_MENU, menu[2], id=menu[0])
self.PopupMenu(popup_menu, event.GetPosition())
popup_menu.Destroy()
return
def on_label_right_click(self, event):
menus = [(wx.NewId(), "Cut", self.cut),
(wx.NewId(), "Copy", self.copy),
(wx.NewId(), "Paste", self.paste),
None]
# Select if right clicked row or column is not in selection
if event.GetRow() > -1:
if not self.IsInSelection(row=event.GetRow(), col=1):
self.SelectRow(event.GetRow())
self.selected_rows = self.GetSelectedRows()
menus += [(wx.NewId(), "Add row", self.add_rows)]
menus += [(wx.NewId(), "Delete row", self.delete_rows)]
elif event.GetCol() > -1:
if not self.IsInSelection(row=1, col=event.GetCol()):
self.SelectCol(event.GetCol())
self.selected_cols = self.GetSelectedCols()
menus += [(wx.NewId(), "Add column", self.add_cols)]
menus += [(wx.NewId(), "Delete column", self.delete_cols)]
else:
return
popup_menu = wx.Menu()
for menu in menus:
if menu is None:
popup_menu.AppendSeparator()
continue
popup_menu.Append(menu[0], menu[1])
self.Bind(wx.EVT_MENU, menu[2], id=menu[0])
self.PopupMenu(popup_menu, event.GetPosition())
popup_menu.Destroy()
return
def on_change(self, event):
cell = event.GetEventObject()
row = cell.GetGridCursorRow()
col = cell.GetGridCursorCol()
attribute = {"value": self.GetCellValue(row, col)}
self.add_history({"type": "change", "cells": [(row, col, attribute)]})
def add_history(self, change):
self.history.append(change)
def undo(self):
if not len(self.history):
return
action = self.history.pop()
if action["type"] == "change" or action["type"] == "delete":
for row, col, attribute in action["cells"]:
self.SetCellValue(row, col, attribute["value"])
if action["type"] == "delete":
self.SetCellAlignment(row, col, *attribute["alignment"]) # *attribute["alignment"] > horiz, vert
elif action["type"] == "delete_rows":
for row, attribute in reversed(action["rows"]):
self.InsertRows(row)
self.SetRowLabelValue(row, attribute["label"])
self.SetRowSize(row, attribute["size"])
elif action["type"] == "delete_cols":
for col, attribute in reversed(action["cols"]):
self.InsertCols(col)
self.SetColLabelValue(col, attribute["label"])
self.SetColSize(col, attribute["size"])
elif action["type"] == "add_rows":
for row in reversed(action["rows"]):
self.DeleteRows(row)
elif action["type"] == "add_cols":
for col in reversed(action["cols"]):
self.DeleteCols(col)
else:
return
def on_key(self, event):
"""
Handles all key events.
"""
# print(event.GetKeyCode())
# Ctrl+C or Ctrl+Insert
if event.ControlDown() and event.GetKeyCode() in [67, 322]:
self.copy(event)
# Ctrl+V
elif event.ControlDown() and event.GetKeyCode() == 86:
self.paste(event)
# DEL
elif event.GetKeyCode() == 127:
self.delete(event)
# Ctrl+A
elif event.ControlDown() and event.GetKeyCode() == 65:
self.SelectAll()
# Ctrl+Z
elif event.ControlDown() and event.GetKeyCode() == 90:
self.undo()
# Ctrl+X
elif event.ControlDown() and event.GetKeyCode() == 88:
# Call delete method
self.cut(event)
# Ctrl+V or Shift + Insert
elif (event.ControlDown() and event.GetKeyCode() == 67) \
or (event.ShiftDown() and event.GetKeyCode() == 322):
self.paste(event)
else:
event.Skip()
def get_selection(self):
"""
Returns selected range's start_row, start_col, end_row, end_col
If there is no selection, returns selected cell's start_row=end_row, start_col=end_col
"""
if not len(self.GetSelectionBlockTopLeft()):
selected_columns = self.GetSelectedCols()
selected_rows = self.GetSelectedRows()
if selected_columns:
start_col = selected_columns[0]
end_col = selected_columns[-1]
start_row = 0
end_row = self.GetNumberRows() - 1
elif selected_rows:
start_row = selected_rows[0]
end_row = selected_rows[-1]
start_col = 0
end_col = self.GetNumberCols() - 1
else:
start_row = end_row = self.GetGridCursorRow()
start_col = end_col = self.GetGridCursorCol()
elif len(self.GetSelectionBlockTopLeft()) > 1:
wx.MessageBox("Multiple selections are not supported", "Warning")
return []
else:
start_row, start_col = self.GetSelectionBlockTopLeft()[0]
end_row, end_col = self.GetSelectionBlockBottomRight()[0]
return [start_row, start_col, end_row, end_col]
def get_selected_cells(self):
# returns a list of selected cells
selection = self.get_selection()
if not selection:
return
start_row, start_col, end_row, end_col = selection
for row in range(start_row, end_row + 1):
for col in range(start_col, end_col + 1):
yield [row, col]
def copy(self, event):
"""
Copies range of selected cells to clipboard.
"""
selection = self.get_selection()
if not selection:
return []
start_row, start_col, end_row, end_col = selection
data = u''
rows = range(start_row, end_row + 1)
for row in rows:
columns = range(start_col, end_col + 1)
for idx, column in enumerate(columns, 1):
if idx == len(columns):
# if we are at the last cell of the row, add new line instead
data += self.GetCellValue(row, column) + "\n"
else:
data += self.GetCellValue(row, column) + "\t"
text_data_object = wx.TextDataObject()
text_data_object.SetText(data)
if wx.TheClipboard.Open():
wx.TheClipboard.SetData(text_data_object)
wx.TheClipboard.Close()
else:
wx.MessageBox("Can't open the clipboard", "Warning")
def paste(self, event):
if not wx.TheClipboard.Open():
wx.MessageBox("Can't open the clipboard", "Warning")
return False
clipboard = wx.TextDataObject()
wx.TheClipboard.GetData(clipboard)
wx.TheClipboard.Close()
data = clipboard.GetText()
if data[-1] == "\n":
data = data[:-1]
try:
cells = self.get_selected_cells()
cell = next(cells)
except StopIteration:
return False
start_row = end_row = cell[0]
start_col = end_col = cell[1]
max_row = self.GetNumberRows()
max_col = self.GetNumberCols()
history = []
out_of_range = False
for row, line in enumerate(data.split("\n")):
target_row = start_row + row
if not (0 <= target_row < max_row):
out_of_range = True
break
if target_row > end_row:
end_row = target_row
for col, value in enumerate(line.split("\t")):
target_col = start_col + col
if not (0 <= target_col < max_col):
out_of_range = True
break
if target_col > end_col:
end_col = target_col
# save previous value of the cell for undo
history.append([target_row, target_col, {"value": self.GetCellValue(target_row, target_col)}])
self.SetCellValue(target_row, target_col, value)
self.SelectBlock(start_row, start_col, end_row, end_col) # select pasted range
if out_of_range:
wx.MessageBox("Pasted data is out of Grid range", "Warning")
self.add_history({"type": "change", "cells": history})
def delete(self, event):
cells = []
for row, col in self.get_selected_cells():
attributes = {
"value": self.GetCellValue(row, col),
"alignment": self.GetCellAlignment(row, col)
}
cells.append((row, col, attributes))
self.SetCellValue(row, col, "")
self.add_history({"type": "delete", "cells": cells})
def cut(self, event):
self.copy(event)
self.delete(event)
if __name__ == '__main__':
class MyFrame(wx.Frame):
def __init__(self, parent, ID, title, pos=wx.DefaultPosition, size=wx.Size(800, 400), style=wx.DEFAULT_FRAME_STYLE):
wx.Frame.__init__(self, parent, ID, title, pos, size, style)
agrid = MyGrid(self)
agrid.CreateGrid(7, 7)
for count in range(3):
for count2 in range(3):
agrid.SetCellValue(count, count2, str(count + count2))
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, "A copy and paste grid")
frame.Show(True)
self.SetTopWindow(frame)
return True
app = MyApp()
app.MainLoop()
I adapted the code for class MyGrid to work with Python 2 and 3, see below.
class MyGrid(wx.grid.Grid):
""" A Copy&Paste enabled grid class"""
def __init__(self, parent, id, style):
wx.grid.Grid.__init__(self, parent, id, wx.DefaultPosition, wx.DefaultSize, style)
# wx.EVT_KEY_DOWN(self, self.OnKey)
self.Bind(wx.EVT_KEY_DOWN, self.OnKey)
self.data4undo = [0, 0, '']
def OnKey(self, event):
# If Ctrl+C is pressed...
if event.ControlDown() and event.GetKeyCode() == 67:
self.copy()
# If Ctrl+V is pressed...
if event.ControlDown() and event.GetKeyCode() == 86:
self.paste('clip')
# If Ctrl+Z is pressed...
if event.ControlDown() and event.GetKeyCode() == 90:
if self.data4undo[2] != '':
self.paste('undo')
# If del is pressed...
if event.GetKeyCode() == 127:
# Call delete method
self.delete()
# Skip other Key events
if event.GetKeyCode():
event.Skip()
return
def copy(self):
# Number of rows and cols
topleft = self.GetSelectionBlockTopLeft()
if list(topleft) == []:
topleft = []
else:
topleft = list(topleft[0])
bottomright = self.GetSelectionBlockBottomRight()
if list(bottomright) == []:
bottomright = []
else:
bottomright = list(bottomright[0])
if list(self.GetSelectionBlockTopLeft()) == []:
rows = 1
cols = 1
iscell = True
else:
rows = bottomright[0] - topleft[0] + 1
cols = bottomright[1] - topleft[1] + 1
iscell = False
# data variable contain text that must be set in the clipboard
data = ''
# For each cell in selected range append the cell value in the data variable
# Tabs ' ' for cols and '\r' for rows
for r in range(rows):
for c in range(cols):
if iscell:
data += str(self.GetCellValue(self.GetGridCursorRow() + r, self.GetGridCursorCol() + c))
else:
data += str(self.GetCellValue(topleft[0] + r, topleft[1] + c))
if c < cols - 1:
data += ' '
data += '\n'
# Create text data object
clipboard = wx.TextDataObject()
# Set data object value
clipboard.SetText(data)
# Put the data in the clipboard
if wx.TheClipboard.Open():
wx.TheClipboard.SetData(clipboard)
wx.TheClipboard.Close()
else:
wx.MessageBox("Can't open the clipboard", "Error")
def paste(self, stage):
topleft = list(self.GetSelectionBlockTopLeft())
if stage == 'clip':
clipboard = wx.TextDataObject()
if wx.TheClipboard.Open():
wx.TheClipboard.GetData(clipboard)
wx.TheClipboard.Close()
else:
wx.MessageBox("Can't open the clipboard", "Error")
data = clipboard.GetText()
if topleft == []:
rowstart = self.GetGridCursorRow()
colstart = self.GetGridCursorCol()
else:
rowstart = topleft[0][0]
colstart = topleft[0][1]
elif stage == 'undo':
data = self.data4undo[2]
rowstart = self.data4undo[0]
colstart = self.data4undo[1]
else:
wx.MessageBox("Paste method "+stage+" does not exist", "Error")
text4undo = ''
# Convert text in a array of lines
for y, r in enumerate(data.splitlines()):
# Convert c in a array of text separated by tab
for x, c in enumerate(r.split(' ')):
if y + rowstart < self.NumberRows and x + colstart < self.NumberCols :
text4undo += str(self.GetCellValue(rowstart + y, colstart + x)) + ' '
self.SetCellValue(rowstart + y, colstart + x, c)
text4undo = text4undo[:-1] + '\n'
if stage == 'clip':
self.data4undo = [rowstart, colstart, text4undo]
else:
self.data4undo = [0, 0, '']
def delete(self):
# print "Delete method"
# Number of rows and cols
topleft = list(self.GetSelectionBlockTopLeft())
bottomright = list(self.GetSelectionBlockBottomRight())
if topleft == []:
rows = 1
cols = 1
else:
rows = bottomright[0][0] - topleft[0][0] + 1
cols = bottomright[0][1] - topleft[0][1] + 1
# Clear cells contents
for r in range(rows):
for c in range(cols):
if topleft == []:
self.SetCellValue(self.GetGridCursorRow() + r, self.GetGridCursorCol() + c, '')
else:
self.SetCellValue(topleft[0][0] + r, topleft[0][1] + c, '')