Introduction
Undo and Redo commands are very common in most applications. This article demonstrates how to implement undo and redo in wxPython.
What Objects are Involved
wx.App - wxPython application object
CSheet - An enhanced grid widget
wx.Frame - Frame container
wx.BoxSizer - sizer object used for layout
Process Overview
I have been working on an application that needed undo and redo commands. I had only a slight idea of how to do it. So I looked at the source code of KSpread. After I understood the implementation, I quickly programmed a solution in python.
First of all, we need to figure out which actions we would like to implement. In our example, we want to undo and redo cell text changes and column and row size changes. So we must bind relevant events to our methods. These bindings are not visible in our sample code. You can find them in sheet.py file, in CSheet class:
self.Bind(wx.grid.EVT_GRID_ROW_SIZE, self.OnRowSize) self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.OnColSize) self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange)
In these three methods we create Undo objects. These objects are UndoText, UndoColSize and UndoRowSize. Each object has two methods. undo() and redo(). They are responsible for bringing to the state of the application before the change was done and vice versa. The objects are then appended to stockUndo list. This way we ensure, that all necessary changes are stored.
Finally, when we press undo, redo buttons, we call OnUndo() and OnRedo() methods. The following method calls actually do the job:
a.undo() a.redo()
The objects move between stockUndo and stockRedo lists accordingly. Also when there are no objects left, we disable a button with the EnableTool() method.
Please keep in mind, that this script is only demonstrational. It should only give you the way. Hope that it was helpful.
Code Sample
import wx import wx.lib.sheet stockUndo = [] stockRedo = [] class UndoText: def __init__( self, sheet, text1, text2, row, column ): self.RedoText = text2 self.row = row self.col = column self.UndoText = text1 self.sheet = sheet def undo( self ): self.RedoText = self.sheet.GetCellValue( self.row, self.col ) if self.UndoText == None: self.sheetSetCellValue( '' ) else: self.sheet.SetCellValue( self.row, self.col, self.UndoText ) def redo( self ): if self.RedoText == None: self.sheet.SetCellValue( '' ) else: self.sheet.SetCellValue( self.row, self.col, self.RedoText ) class UndoColSize: def __init__( self, sheet, position, size ): self.sheet = sheet self.pos = position self.RedoSize = size self.UndoSize = 80 def undo( self ): self.RedoSize = self.sheet.GetColSize( self.pos ) self.sheet.SetColSize( self.pos, self.UndoSize ) self.sheet.ForceRefresh() def redo( self ): self.UndoSize = 80 self.sheet.SetColSize( self.pos, self.RedoSize ) self.sheet.ForceRefresh() class UndoRowSize: def __init__( self, sheet, position, size ): self.sheet = sheet self.pos = position self.RedoSize = size self.UndoSize = 20 def undo( self ): self.RedoSize = self.sheet.GetRowSize( self.pos ) self.sheet.SetRowSize( self.pos, self.UndoSize ) self.sheet.ForceRefresh() def redo( self ): self.UndoSize = 20 self.sheet.SetRowSize( self.pos, self.RedoSize ) self.sheet.ForceRefresh() class MySheet( wx.lib.sheet.CSheet ): def __init__( self, parent ): wx.lib.sheet.CSheet.__init__( self, parent ) self.SetLabelBackgroundColour( '#DBD4D4' ) self.SetRowLabelAlignment( wx.ALIGN_CENTRE, wx.ALIGN_CENTRE ) self.text = '' def OnCellChange( self, event ): toolbar = self.GetParent().toolbar1 if ( toolbar.GetToolEnabled( 808 ) == False ): toolbar.EnableTool( 808, True ) r = event.GetRow() c = event.GetCol() text = self.GetCellValue( r, c ) # self.text - text before change # text - text after change undo = UndoText( self, self.text, text, r, c ) stockUndo.append( undo ) if stockRedo: del stockRedo[:] # this might be surprising, but it is a standard behaviour in all spreadsheets toolbar.EnableTool( 809, False ) def OnColSize( self, event ): toolbar = self.GetParent().toolbar1 if ( toolbar.GetToolEnabled( 808 ) == False ): toolbar.EnableTool( 808, True ) pos = event.GetRowOrCol() size = self.GetColSize( pos ) undo = UndoColSize( self, pos, size ) stockUndo.append( undo ) if stockRedo: del stockRedo[:] toolbar.EnableTool( 809, False ) def OnRowSize( self, event ): toolbar = self.GetParent().toolbar1 if ( toolbar.GetToolEnabled( 808 ) == False ): toolbar.EnableTool( 808, True ) pos = event.GetRowOrCol() size = self.GetRowSize( pos ) undo = UndoRowSize( self, pos, size ) stockUndo.append( undo ) if stockRedo: del stockRedo[:] toolbar.EnableTool( 809, False ) class Newt( wx.Frame ): def __init__( self, parent, id, title ): wx.Frame.__init__( self, parent, -4, title, size = ( 550, 500 ), style = wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE ) box = wx.BoxSizer( wx.VERTICAL ) menuBar = wx.MenuBar() menu1 = wx.Menu() quit = wx.MenuItem( menu1, 105, "&Quit\tCtrl+Q", "Quits Newt" ) quit.SetBitmap( wx.ArtProvider_GetBitmap( wx.ART_QUIT, wx.ART_OTHER, wx.Size( 16, 16 ) ) ) menu1.AppendItem( quit ) menuBar.Append( menu1, "&File" ) wx.EVT_MENU( self, 105, self.OnQuitNewt ) self.SetMenuBar( menuBar ) # Setting up Toolbar self.toolbar1 = wx.ToolBar( self, -1, style = wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT ) self.toolbar1.AddSimpleTool( 808, wx.ArtProvider_GetBitmap( wx.ART_UNDO, wx.ART_OTHER, wx.Size( 16, 16 ) ), 'Undo', '' ) self.toolbar1.AddSimpleTool( 809, wx.ArtProvider_GetBitmap( wx.ART_REDO, wx.ART_OTHER, wx.Size( 16, 16 ) ), 'Redo', '' ) self.toolbar1.EnableTool( 808, False ) self.toolbar1.EnableTool( 809, False ) self.toolbar1.AddSeparator() self.toolbar1.AddSimpleTool( 813, wx.ArtProvider_GetBitmap( wx.ART_QUIT, wx.ART_OTHER, wx.Size( 16, 16 ) ), 'Quit', '' ) self.toolbar1.Realize() wx.EVT_TOOL( self.toolbar1, 808, self.OnUndo ) wx.EVT_TOOL( self.toolbar1, 809, self.OnRedo ) wx.EVT_TOOL( self.toolbar1, 813, self.OnQuitNewt ) box.Add( self.toolbar1, border = 5 ) box.Add( ( 5, 10 ), 0 ) self.SetSizer( box ) self.sheet1 = MySheet( self ) self.sheet1.SetNumberRows( 55 ) self.sheet1.SetNumberCols( 25 ) for i in range( self.sheet1.GetNumberRows() ): self.sheet1.SetRowSize( i, 20 ) self.sheet1.SetFocus() box.Add( self.sheet1, 1, wx.EXPAND ) self.CreateStatusBar() self.Centre() self.Show( True ) def OnUndo( self, event ): if len( stockUndo ) == 0: return a = stockUndo.pop() if len( stockUndo ) == 0: self.toolbar1.EnableTool( 808, False ) a.undo() stockRedo.append( a ) self.toolbar1.EnableTool( 809, True ) def OnRedo( self, event ): if len( stockRedo ) == 0: return a = stockRedo.pop() if len( stockRedo ) == 0: self.toolbar1.EnableTool( 809, False ) a.redo() stockUndo.append( a ) self.toolbar1.EnableTool( 808, True ) def OnQuitNewt( self, event ): self.Close( True ) app = wx.App( redirect = None ) newt = Newt( None, -1, "Newt" ) app.MainLoop()
Comments
- -Zack Buhman -jan bodnar