Search code examples
pythonwxpythoncopy-paste

Work with ctrl-c and ctrl-v to copy and paste into a wx.Grid in wxPython


I am working on an program with a number of input tables for which I am using wxPython wx.Grid (primarily for Windows). I noticed that ctrl-c and ctrl-v to copy and paste doe snot simply work and I searched for solutions to prevent having to type in all numbers in the tables by hand. I found an old post by Ruben Charles here: http://comments.gmane.org/gmane.comp.python.wxpython/26387

That seems to do more or less what I wanted, so I started working with that and made some, what I hope are improvements. (I added functionality to 'undo' with ctrl-Z, for working with single cells and for pasting if the last row or column falls outside of the grid table.)

Are there better ways to do this, or might you have advice for improvement? Particularly: How to make this work with Python 3.5?

import wx
import wx.grid

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, -1, wx.WANTS_CHARS)
        agrid.CreateGrid(7, 7)
        for count in range(3):
            for count2 in range(3):
                agrid.SetCellValue(count, count2, str(count + count2))

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.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
        print self.GetSelectionBlockBottomRight()
        print self.GetGridCursorRow()
        print self.GetGridCursorCol()
        if self.GetSelectionBlockTopLeft() == []:
            rows = 1
            cols = 1
            iscell = True
        else:
            rows = self.GetSelectionBlockBottomRight()[0][0] - self.GetSelectionBlockTopLeft()[0][0] + 1
            cols = self.GetSelectionBlockBottomRight()[0][1] - self.GetSelectionBlockTopLeft()[0][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 '\t' 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(self.GetSelectionBlockTopLeft()[0][0] + r, self.GetSelectionBlockTopLeft()[0][1] + c))
                if c < cols - 1:
                    data += '\t'
            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):
        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 self.GetSelectionBlockTopLeft() == []:
                rowstart = self.GetGridCursorRow()
                colstart = self.GetGridCursorCol()
            else:
                rowstart = self.GetSelectionBlockTopLeft()[0][0]
                colstart = self.GetSelectionBlockTopLeft()[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('\t')):
                if y + rowstart < self.NumberRows and x + colstart < self.NumberCols :
                    text4undo += str(self.GetCellValue(rowstart + y, colstart + x)) + '\t'
                    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
        if self.GetSelectionBlockTopLeft() == []:
            rows = 1
            cols = 1
        else:
            rows = self.GetSelectionBlockBottomRight()[0][0] - self.GetSelectionBlockTopLeft()[0][0] + 1
            cols = self.GetSelectionBlockBottomRight()[0][1] - self.GetSelectionBlockTopLeft()[0][1] + 1
        # Clear cells contents
        for r in range(rows):
            for c in range(cols):
                if self.GetSelectionBlockTopLeft() == []:
                    self.SetCellValue(self.GetGridCursorRow() + r, self.GetGridCursorCol() + c, '')
                else:
                    self.SetCellValue(self.GetSelectionBlockTopLeft()[0][0] + r, self.GetSelectionBlockTopLeft()[0][1] + c, '')

class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame(None, -1, "Copy and paste enabled only for a single range")
        frame.Show(True)
        self.SetTopWindow(frame)
        return True
def main():
    app = MyApp()
    app.MainLoop()

if __name__ == '__main__':
    main()

Solution

  • 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, '')