Undo/Redo 基于堆栈的编辑控件实现

Undo/Redo implementation for an edit-control, based on stacks

我正在尝试为文本框的某些事件实现一个简单的 undo/redo 机制(基于堆栈)。

在问这个问题之前,我已经看到了很多 undo/redo 实现,例如 these,但或多或​​少它们是不完整的并且显示了一些东西我已经知道(另一方面,从我的理解中使用稀有接口的专业方式,所以我想遵循这种基于堆栈的方式),因为这些例子不仅仅是 undo/redo 编辑控件的例子,是堆栈的 push/pop 示例,但是 undo/redo 比编写弹出“undo stack”的最后一项的方法和另一种方法多一点弹出“redo stack”的最后一项,因为在用户与控件交互的某个时刻,堆栈应该是 cleared/resetted.

我的意思是,在真正的 undo/redo 编辑控件机制中,“redo stack”应该在用户撤消和用户创建文本时清除在“undo stack”仍然包含项目的情况下在控件中进行修改,因此此时没有什么可重做的,因为在撤消时发生了更改。 我没有看到任何完整的撤消-重做机制示例,请记住当控件中发生更改时必须如何操作 undo/redo 堆栈。

我需要帮助来正确实现我的 undo/redo 堆栈的逻辑,我开始自己尝试了几天,经过几次试验和错误,但总是逃避一些细节,因为当我得到一个(撤消或重做)堆栈正常工作,另一个按预期停止工作,撤消不应撤消的内容或重做不应重做的内容, 所以我(再次)丢弃了 all the conditional logic that I written 因为我的逻辑总是错误的,我应该使用适当的条件算法再次从零开始,我的意思是正确的条件来推送或弹出在适当的时候堆叠项目。

那么,除了文字或建议,我需要一个可以用我的算法解决问题的工作代码,我需要在下面的代码中完成AddUndoRedoItem方法的算法逻辑,这是一个关于这个的具体问题。

如果我缺少遵循相同原则(撤消和重做堆栈)的更简单的解决方案,我也会接受该解决方案。

无论是 C# 还是 Vb.Net。

PD: 如果因为我的英语不好,我没有正确解释某些事情,而你不完全确定我要的是哪种 undo/redo,只是我要听起来像 undo/redo,只需在记事本中测试 Ctrl+Z(undo) 和 Ctrl+Y(redo) 键,同时执行撤消或重做的文本更改,看看它是如何工作的,这是一个真正的 undo/redo 实现,我正在尝试用堆栈重现。


这是当前代码:

Public Enum UndoRedoCommand As Integer
    Undo
    Redo
End Enum

Public Enum UndoRedoTextBoxEvent As Integer
    TextChanged
End Enum

Public NotInheritable Class UndoRedoTextBox

    Private ReadOnly undoStack As Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))
    Private ReadOnly redoStack As Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))

    Private lastCommand As UndoRedoCommand
    Private lastText As String

    Public ReadOnly Property Control As TextBox
        Get
            Return Me.controlB
        End Get
    End Property
    Private WithEvents controlB As TextBox

    Public ReadOnly Property CanUndo As Boolean
        Get
            Return (Me.undoStack.Count <> 0)
        End Get
    End Property

    Public ReadOnly Property CanRedo As Boolean
        Get
            Return (Me.redoStack.Count <> 0)
        End Get
    End Property

    Public ReadOnly Property IsUndoing As Boolean
        Get
            Return Me.isUndoingB
        End Get
    End Property
    Private isUndoingB As Boolean

    Public ReadOnly Property IsRedoing As Boolean
        Get
            Return Me.isRedoingB
        End Get
    End Property
    Private isRedoingB As Boolean

    Public Sub New(ByVal tb As TextBox)

        Me.undoStack = New Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))
        Me.redoStack = New Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))

        Me.controlB = tb
        Me.lastText = tb.Text

    End Sub

    Public Sub Undo()

        If (Me.CanUndo) Then
            Me.InternalUndoRedo(UndoRedoCommand.Undo)
        End If

    End Sub

    Public Sub Redo()

        If (Me.CanRedo) Then
            Me.InternalUndoRedo(UndoRedoCommand.Redo)
        End If

    End Sub

    ' Undoes or redoues.
    Private Sub InternalUndoRedo(ByVal command As UndoRedoCommand)

        Dim undoRedoItem As KeyValuePair(Of UndoRedoTextBoxEvent, Object) = Nothing
        Dim undoRedoEvent As UndoRedoTextBoxEvent
        Dim undoRedoValue As Object = Nothing

        Select Case command

            Case UndoRedoCommand.Undo
                Me.isUndoingB = True
                undoRedoItem = Me.undoStack.Pop
                Me.AddUndoRedoItem(UndoRedoCommand.Redo, UndoRedoTextBoxEvent.TextChanged, Me.lastText, undoRedoItem.Value)

            Case UndoRedoCommand.Redo
                Me.isRedoingB = True
                undoRedoItem = Me.redoStack.Pop
                Me.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.TextChanged, undoRedoItem.Value, Me.lastText)

        End Select

        undoRedoEvent = undoRedoItem.Key
        undoRedoValue = undoRedoItem.Value

        Select Case undoRedoEvent

            Case UndoRedoTextBoxEvent.TextChanged
                Me.controlB.Text = CStr(undoRedoValue)

        End Select

        Me.isUndoingB = False
        Me.isRedoingB = False

    End Sub

    Private Sub AddUndoRedoItem(ByVal command As UndoRedoCommand, ByVal [event] As UndoRedoTextBoxEvent,
                                ByVal data As Object, ByVal lastData As Object)

        Console.WriteLine()
        Console.WriteLine("command     :" & command.ToString)
        Console.WriteLine("last command:" & lastCommand.ToString)
        Console.WriteLine("can undo    :" & Me.CanUndo)
        Console.WriteLine("can redo    :" & Me.CanRedo)
        Console.WriteLine("is undoing  :" & Me.isUndoingB)
        Console.WriteLine("is redoing  :" & Me.isRedoingB)
        Console.WriteLine("data        :" & data.ToString)
        Console.WriteLine("last data   :" & lastData.ToString)

        Dim undoRedoData As Object = Nothing
        Me.lastCommand = command

        Select Case command

            Case UndoRedoCommand.Undo

                If (Me.isUndoingB) Then
                    Exit Select
                End If

                undoRedoData = lastData
                Me.undoStack.Push(New KeyValuePair(Of UndoRedoTextBoxEvent, Object)([event], undoRedoData))

            Case UndoRedoCommand.Redo

                If (Me.isRedoingB) Then
                    Exit Select
                End If

                undoRedoData = lastData
                Me.redoStack.Push(New KeyValuePair(Of UndoRedoTextBoxEvent, Object)([event], undoRedoData))

        End Select

    End Sub

    Private Sub TextBox_TextChanged(ByVal sender As Object, ByVal e As EventArgs) _
    Handles controlB.TextChanged

        Dim currentText As String = Me.controlB.Text

        If Not String.Equals(Me.lastText, currentText, StringComparison.Ordinal) Then

            Select Case Me.lastCommand

                Case UndoRedoCommand.Undo

                    Me.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.TextChanged, currentText, Me.lastText)

                Case UndoRedoCommand.Redo
                    Me.AddUndoRedoItem(UndoRedoCommand.Redo, UndoRedoTextBoxEvent.TextChanged, Me.lastText, currentText)

            End Select

            Me.lastText = currentText

        End If

    End Sub

End Class

感谢@Plutonix 我解决了它,我真的不相信一个简单的注释可以帮助我解决逻辑问题,但是是的,我让事情变得比他们真的是。

我仍然需要考虑如何管理一次性对象,但或多或​​少的想法已经完成,下面的代码按预期工作(至少符合我的预期)。

这些是基础 undo/redo class 控件的部分:

Public Enum UndoRedoCommand As Integer
    Undo
    Redo
End Enum

Public Class UndoRedoItem
    Public Property [Event] As Integer
    Public Property LastValue As Object
    Public Property CurrentValue As Object
End Class

Public MustInherit Class UndoRedo(Of T As Control)

#Region " Private Fields "

    Private ReadOnly undoStack As Stack(Of UndoRedoItem)
    Private ReadOnly redoStack As Stack(Of UndoRedoItem)

#End Region

#Region " Properties "

    Public ReadOnly Property Control As T
        Get
            Return Me.controlB
        End Get
    End Property
    Protected WithEvents controlB As T

    Public ReadOnly Property CanUndo As Boolean
        Get
            Return (Me.undoStack.Count <> 0)
        End Get
    End Property

    Public ReadOnly Property CanRedo As Boolean
        Get
            Return (Me.redoStack.Count <> 0)
        End Get
    End Property

    Public ReadOnly Property IsUndoing As Boolean
        Get
            Return Me.isUndoingB
        End Get
    End Property
    Private isUndoingB As Boolean

    Public ReadOnly Property IsRedoing As Boolean
        Get
            Return Me.isRedoingB
        End Get
    End Property
    Private isRedoingB As Boolean

#End Region

#Region " Constructors "

    Private Sub New()
    End Sub

    Public Sub New(ByVal ctrl As T)

        Me.undoStack = New Stack(Of UndoRedoItem)
        Me.redoStack = New Stack(Of UndoRedoItem)

        Me.controlB = ctrl

    End Sub

#End Region

#Region " Public Methods "

    Public Sub Undo()

        If (Me.CanUndo) Then
            Me.InternalUndoRedo(UndoRedoCommand.Undo)
        End If

    End Sub

    Public Sub Redo()

        If (Me.CanRedo) Then
            Me.InternalUndoRedo(UndoRedoCommand.Redo)
        End If

    End Sub

#End Region

#Region " Private Methods "

    Private Sub InternalUndoRedo(ByVal command As UndoRedoCommand)

        Dim undoRedoItem As UndoRedoItem = Nothing

        Select Case command

            Case UndoRedoCommand.Undo
                Me.isUndoingB = True
                undoRedoItem = Me.undoStack.Pop
                Me.AddUndoRedoItem(UndoRedoCommand.Redo, undoRedoItem.Event, undoRedoItem.LastValue, undoRedoItem.CurrentValue)

            Case UndoRedoCommand.Redo
                Me.isRedoingB = True
                undoRedoItem = Me.redoStack.Pop

        End Select

        Me.DoUndo(undoRedoItem.Event, undoRedoItem.CurrentValue)

        Me.isUndoingB = False
        Me.isRedoingB = False

    End Sub

    Protected MustOverride Sub DoUndo(ByVal [event] As Integer, ByVal data As Object)

    Protected Sub AddUndoRedoItem(ByVal command As UndoRedoCommand,
                                  ByVal [event] As Integer,
                                  ByVal currentData As Object,
                                  ByVal lastData As Object)

        Dim undoRedoItem As New UndoRedoItem
        undoRedoItem.Event = [event]

        Select Case command

            Case UndoRedoCommand.Undo

                If (Me.isUndoingB) Then
                    Exit Select
                End If

                If (Me.CanUndo) AndAlso (Me.CanRedo) AndAlso Not (Me.IsRedoing) Then
                    Me.redoStack.Clear()
                End If

                undoRedoItem.CurrentValue = lastData
                undoRedoItem.LastValue = currentData
                Me.undoStack.Push(undoRedoItem)

            Case UndoRedoCommand.Redo

                If (Me.isRedoingB) Then
                    Exit Select
                End If

                undoRedoItem.CurrentValue = currentData
                undoRedoItem.LastValue = lastData
                Me.redoStack.Push(undoRedoItem)

        End Select

    End Sub

#End Region

End Class

这是文本框上 undo/redo 的实现:

Public Enum UndoRedoTextBoxEvent As Integer

    TextChanged
    FontChanged
    BackColorChanged
    ForeColorChanged

End Enum

Public NotInheritable Class UndoRedoTextBox : Inherits UndoRedo(Of TextBox)

    Private lastText As String
    Private lastFont As Font
    Private lastBackColor As Color
    Private lastForeColor As Color

    Public Sub New(ByVal tb As TextBox)
        MyBase.New(tb)
    End Sub

    Protected Overrides Sub DoUndo([event] As Integer, data As Object)

        Select Case DirectCast([event], UndoRedoTextBoxEvent)

            Case UndoRedoTextBoxEvent.TextChanged
                MyBase.controlB.Text = CStr(data)

            Case UndoRedoTextBoxEvent.FontChanged
                MyBase.controlB.Font = DirectCast(data, Font)

            Case UndoRedoTextBoxEvent.BackColorChanged
                MyBase.controlB.BackColor = DirectCast(data, Color)

            Case UndoRedoTextBoxEvent.ForeColorChanged
                MyBase.controlB.ForeColor = DirectCast(data, Color)

        End Select

    End Sub

    Private Sub TextBox_TextChanged(ByVal sender As Object, ByVal e As EventArgs) _
    Handles controlB.TextChanged

        Dim currentText As String = MyBase.controlB.Text

        If Not String.Equals(Me.lastText, currentText, StringComparison.Ordinal) Then

            MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.TextChanged, currentText, Me.lastText)
            Me.lastText = currentText

        End If

    End Sub

    Private Sub TextBox_FontChanged(sender As Object, e As EventArgs) _
    Handles controlB.FontChanged

        Dim currentFont As Font = MyBase.controlB.Font

        If (Me.lastFont IsNot currentFont) Then
            MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.FontChanged, currentFont, Me.lastFont)
            Me.lastFont = currentFont
        End If

    End Sub

    Private Sub TextBox_BackColorChanged(sender As Object, e As EventArgs) _
    Handles controlB.BackColorChanged

        Dim currentBackColor As Color = MyBase.controlB.BackColor

        If (Me.lastBackColor <> currentBackColor) Then
            MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.BackColorChanged, currentBackColor, Me.lastBackColor)
            Me.lastBackColor = currentBackColor
        End If

    End Sub

    Private Sub TextBox_ForeColorChanged(sender As Object, e As EventArgs) _
    Handles controlB.ForeColorChanged

        Dim currentForeColor As Color = MyBase.controlB.ForeColor

        If (Me.lastForeColor <> currentForeColor) Then
            MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.ForeColorChanged, currentForeColor, Me.lastForeColor)
            Me.lastForeColor = currentForeColor
        End If

    End Sub

End Class