基于堆栈的编辑控件撤销/重做实现

3
我正在尝试为文本框的某些事件实现一个基于堆栈的简单撤销/重做机制。
在提问之前,我看到了很多像这些的撤销/重做实现,但它们或多或少都是不完整的,并且展示了已经知道的内容(另一方面,使用罕见接口的专业方式超出了我的理解能力,因此我想遵循这种基于堆栈的方法)。因为那些例子不仅是编辑控件的撤销/重做示例,而且还是堆栈的推入/弹出示例,但撤销/重做更多地是编写一种方法以弹出“撤销堆栈”的最后一项和另一种方法以弹出“重做堆栈”的最后一项,因为在用户与控件交互时,在某个点上,堆栈应该被清除/重置。
我是指在真正的编辑控件的撤销/重做机制中,“重做堆栈”应在用户撤消并且用户在控件中进行文本修改时被清除,而“撤消堆栈”仍然包含项,因此在这一点上没有可重做的内容,因为在撤消时发生了更改。 我没有看到任何完整的以这种方式实现撤销/重做机制的示例,考虑到当控件发生更改时必须如何操作撤销/重做堆栈。
我需要帮助来正确实现我的撤销/重做堆栈逻辑,我开始自己尝试了几天并进行了几次试错,但总有一些细节逃脱我的注意,因为当我让一个(撤消或重做)堆栈正常工作时,另一个就停止按预期工作,撤消了不该撤消的东西或重做了不该重做的东西, 所以我再次放弃了我编写的所有条件逻辑,因为我的逻辑总是错误的,我应该从零开始使用适当的条件算法,即在合适的时刻推送或弹出堆栈项的适当条件。

我需要的不仅仅是建议或文字,而是能够解决我的算法问题的可行代码。我需要完成以下代码中AddUndoRedoItem方法的算法逻辑,这是一个具体的问题。

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

使用C#或Vb.Net编写,无所谓。

附注: 如果因为我的英语不好而没有正确解释某些事情,并且您并不完全确定我在寻求何种撤销/重做操作,请尝试在文本中执行撤销或重做操作时测试记事本中的 Ctrl+Z (撤销)和 Ctrl+Y (重做)键,看看它的行为如何,那就是真正的撤销/重做实现,我正试图通过堆栈来复制它。


这是当前的代码:

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

这将如何处理多个文本框?您会为每个文本控件都有一个这样的吗? - Ňɏssa Pøngjǣrdenlarp
@Plutonix 是的,我将为每个文本框创建一个实例,该类不打算处理多个控件,就像某种“撤消/重做管理器”一样可以处理各种不同的控件。 - ElektroStudios
@Plutonix 你几小时前的评论很有用,请重新写下至少最重要的部分好吗?(我希望我没有忘记你在那些评论中说的任何事情)。我现在无法遵循你之前给我的建议,但是一旦我能进入VS,我会这样做,而且你所说的关于仅在撤消时添加重做项并且不打算对重做栈进行任何更多操作似乎相当合理,但是我认为在明天尝试之前,我无法完全搞清楚。感谢并祝圣诞快乐。 - ElektroStudios
看起来你只是想要让每个TB的撤销池“更深”? 因为每个控件一个堆栈不会像普通文档撤销一样工作。 你不会知道哪个堆栈有“下一个”项目,因此大多数弹出/撤销将导致从未存在或毫无意义的文档状态:Genie in a Bottle; 1958;(by)Pink Floyd - Ňɏssa Pøngjǣrdenlarp
@Plutonix,我不确定我是否完全理解了您的最后一条评论,为什么它不能工作?如果一个类实例正在处理一个控件及其撤消/重做堆栈,则它将按预期工作,似乎您正在谈论具有各种控件堆栈的一个类,但正如我所说,我将为每个控件拥有一个类,而不是任何类型的多个控件的撤消/重做管理器。 - ElektroStudios
显示剩余2条评论
1个回答

0

感谢@Plutonix,我解决了问题。我真的不敢相信一个简单的评论可以帮助我解决逻辑问题,但是事实证明,我把事情想得比它们复杂。

我仍然需要考虑如何管理可处理对象,但是基本思路已经完成,下面的代码按照我的期望工作(至少是我期望的)。

这些是控件的基本撤销/重做类的部分:

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

这是一个针对文本框实现撤销/重做的代码:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接