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
