【问题标题】:Undo/Redo implementation for an edit-control, based on stacks基于堆栈的编辑控件的撤消/重做实现
【发布时间】:2016-03-31 02:57:33
【问题描述】:

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

在问这个问题之前,我已经看到了很多 undo/redo 实现,例如 these,但它们或多或少是不完整的,并且显示了我已经知道的东西(另一方面,使用稀有接口的专业方式不符合我的理解,所以我想遵循这种基于堆栈的方式),因为这些示例不仅仅是用于编辑控件的撤消/重做示例,而是堆栈的推送/弹出示例,但撤消/redo 不仅仅是编写一个弹出“undo stack”最后一项的方法和另一个弹出“redo stack”最后一项的方法,因为在用户与控件交互的某个时刻,堆栈应该被清除/重置。

我的意思是,在编辑控件的真正撤消/重做机制中,“重做堆栈”应该在用户撤消并且用户在控件中进行文本修改时清除,而“ 撤消堆栈”仍然包含项目,因此此时无需重做,因为撤消时发生了更改。 我没有看到任何完整的撤消重做机制示例,请记住当控件发生更改时必须如何操作撤消/重做堆栈。

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

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

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

在 C# 或 Vb.Net 中,无所谓。

PD: 如果因为我的英语不好,我没有正确解释某些事情,并且您不完全确定我要求什么样的撤消/重做,只是我要求撤消/重做听起来,只需测试 Ctrl+Z(撤消)和Ctrl+Y(重做)键,同时在文本中执行更改撤消或重做,看看它的作用,这是一个真正的撤消/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 是的,每个文本框都有一个实例,该类不打算处理多个控件,例如可以处理一种不同控件的某种“撤消/重做管理器”,不是。
  • @Plutonix 您几小时前的最后一个 cmets 很有用,请您至少重写最重要的部分吗? (我希望我不会忘记您在那些 cmets 中所说的任何内容)。我现在无法遵循您以前对我所做的建议,但是一旦我可以进入VS,我就会这样做,而且您所说的似乎很合逻辑,仅在撤消时才添加重做项目,并且不再打算对重做堆栈进行任何操作,但是我想在我明天尝试之前,我根本无法弄清楚。谢谢,圣诞快乐。
  • 看起来您只是想让每个 TB 的撤消池“更深”?因为每个控件一个堆栈不会像普通文档撤消一样。您不会知道哪个堆栈具有“下一个”项目,因此 most pop/Undo 将导致文档状态从来没有或没有意义:Genie in a Bottle; 1958; (by) 平克·弗洛伊德
  • @Plutonix 我不确定是否完全理解您的最后一条评论,为什么它不应该工作?,如果一个类实例正在处理一个带有其撤消/重做堆栈的控件,那么它将按预期工作,似乎您正在谈论一个具有各种控件堆栈的类,但正如我所说,每个控件我将有一个类,而不是用于各种控件的任何类型的撤消/重做管理器。

标签: c# .net vb.net algorithm undo-redo


【解决方案1】:

感谢@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

【讨论】:

    猜你喜欢
    • 2021-06-04
    • 2012-12-21
    • 2017-03-16
    • 2023-03-10
    • 2019-08-22
    • 1970-01-01
    • 1970-01-01
    • 2011-10-15
    • 2020-05-12
    相关资源
    最近更新 更多