Как получить старое значение измененной ячейки в Excel VBA? - PullRequest
42 голосов
/ 12 января 2011

Я обнаруживаю изменения в значениях определенных ячеек в электронной таблице Excel, как это ...

Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range
Dim old_value As String
Dim new_value As String

For Each cell In Target

    If Not (Intersect(cell, Range("cell_of_interest")) Is Nothing) Then
        new_value = cell.Value
        old_value = ' what here?
        Call DoFoo (old_value, new_value)
    End If

Next cell

End Sub

Если предположить, что это не слишком плохой способ кодирования, как мне получитьзначение ячейки до изменения?

Ответы [ 15 ]

52 голосов
/ 12 января 2011

попробуйте

объявить переменную скажем

Dim oval

и в SelectionChange событии

Public Sub Worksheet_SelectionChange(ByVal Target As Range)
oval = Target.Value
End Sub

и в вашем наборе событий Worksheet_Change

old_value = oval
30 голосов
/ 12 августа 2011

Вы можете использовать событие изменения ячейки для запуска макроса, который выполняет следующие действия:

vNew = Range("cellChanged").value
Application.EnableEvents = False
Application.Undo
vOld = Range("cellChanged").value
Range("cellChanged").value = vNew
Application.EnableEvents = True 
10 голосов
/ 13 января 2011

У меня есть альтернативное решение для вас. Вы можете создать скрытый рабочий лист, чтобы сохранить старые значения для вашего диапазона интересов.

Private Sub Workbook_Open()

Dim hiddenSheet As Worksheet

Set hiddenSheet = Me.Worksheets.Add
hiddenSheet.Visible = xlSheetVeryHidden
hiddenSheet.Name = "HiddenSheet"

'Change Sheet1 to whatever sheet you're working with
Sheet1.UsedRange.Copy ThisWorkbook.Worksheets("HiddenSheet").Range(Sheet1.UsedRange.Address)

End Sub

Удалить, когда рабочая книга закрыта ...

Private Sub Workbook_BeforeClose(Cancel As Boolean)

Application.DisplayAlerts = False
Me.Worksheets("HiddenSheet").Delete
Application.DisplayAlerts = True

End Sub

И измените ваше событие Worksheet_Change следующим образом ...

For Each cell In Target

    If Not (Intersect(cell, Range("cell_of_interest")) Is Nothing) Then
        new_value = cell.Value
        ' here's your "old" value...
        old_value = ThisWorkbook.Worksheets("HiddenSheet").Range(cell.Address).Value
        Call DoFoo(old_value, new_value)
    End If

Next cell

' Update your "old" values...
ThisWorkbook.Worksheets("HiddenSheet").UsedRange.Clear
Me.UsedRange.Copy ThisWorkbook.Worksheets("HiddenSheet").Range(Me.UsedRange.Address)
8 голосов
/ 03 октября 2012

Я тоже должен был это сделать.Я нашел решение от "Chris R" действительно хорошим, но подумал, что оно может быть более совместимым, если не добавлять ссылки.Крис, вы говорили об использовании Коллекции.Итак, вот еще одно решение с использованием коллекции.И это не так медленно, в моем случае.Кроме того, благодаря этому решению при добавлении события «_SelectionChange» оно всегда работает (нет необходимости в workbook_open).

Dim OldValues As New Collection

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    'Copy old values
    Set OldValues = Nothing
    Dim c As Range
    For Each c In Target
        OldValues.Add c.Value, c.Address
    Next c
End Sub

Private Sub Worksheet_Change(ByVal Target As Range)
    On Local Error Resume Next  ' To avoid error if the old value of the cell address you're looking for has not been copied
    Dim c As Range
    For Each c In Target
        Debug.Print "New value of " & c.Address & " is " & c.Value & "; old value was " & OldValues(c.Address)
    Next c
    'Copy old values (in case you made any changes in previous lines of code)
    Set OldValues = Nothing
    For Each c In Target
        OldValues.Add c.Value, c.Address
    Next c
End Sub
8 голосов
/ 12 января 2011

Вот способ, которым я пользовался в прошлом. Обратите внимание, что вам нужно добавить ссылку на Microsoft Scripting Runtime, чтобы вы могли использовать объект Dictionary - если вы не хотите добавлять эту ссылку, вы можете сделать это с коллекциями, но они медленнее и нет элегантного способа проверить .Exists (вы должны перехватить ошибку).

Dim OldVals As New Dictionary
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range
    For Each cell In Target
        If OldVals.Exists(cell.Address) Then
            Debug.Print "New value of " & cell.Address & " is " & cell.Value & "; old value was " & OldVals(cell.Address)
        Else
            Debug.Print "No old value for " + cell.Address
        End If
        OldVals(cell.Address) = cell.Value
    Next
End Sub

Как и у любого подобного метода, у него есть свои проблемы - во-первых, он не будет знать «старое» значение, пока значение не будет фактически изменено. Чтобы это исправить, вам нужно перехватить событие Open в книге и пройти через Sheet.UsedRange, заполнив OldVals. Кроме того, он потеряет все свои данные, если вы сбросите проект VBA, остановив отладчик или что-то подобное.

3 голосов
/ 19 апреля 2013

идея ...

  • запишите их в ThisWorkbook модуле
  • закройте и откройте рабочую книгу
    Public LastCell As Range

    Private Sub Workbook_Open()

        Set LastCell = ActiveCell

    End Sub

    Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)

        Set oa = LastCell.Comment

        If Not oa Is Nothing Then
        LastCell.Comment.Delete
        End If

        Target.AddComment Target.Address
        Target.Comment.Visible = True
        Set LastCell = ActiveCell

    End Sub
1 голос
/ 09 ноября 2018

У меня та же проблема, что и у вас, и, к счастью, я прочитал решение по этой ссылке: http://access -excel.tips / стоимость перед тем-лист-изменение /

Dim oldValue As Variant
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    oldValue = Target.Value
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
    'do something with oldValue...
End Sub

Примечание: вы должны поместить переменную oldValue в качестве глобальной переменной, чтобы все подклассы могли использовать ее.

1 голос
/ 16 мая 2017

В ответ на ответ Мэтта Роя я нашел этот вариант отличным ответом, хотя я не мог публиковать сообщения с моим текущим рейтингом. (

Однако, пользуясь возможностью опубликовать свои мысли о его ответе, я подумал, что воспользуюсь возможностью включить небольшую модификацию. Просто сравните код, чтобы увидеть.

Так что спасибо Мэтту Рою за доведение этого кода до нашего внимания и Chris.R за публикацию оригинального кода.

Dim OldValues As New Collection

Private Sub Worksheet_SelectionChange(ByVal Target As Range)

'>> Prevent user from multiple selection before any changes:

 If Selection.Cells.Count > 1 Then
        MsgBox "Sorry, multiple selections are not allowed.", vbCritical
        ActiveCell.Select
        Exit Sub
    End If
 'Copy old values
    Set OldValues = Nothing
    Dim c As Range
    For Each c In Target
        OldValues.Add c.Value, c.Address
    Next c
End Sub


Private Sub Worksheet_Change(ByVal Target As Range)
On Error Resume Next

 On Local Error Resume Next  ' To avoid error if the old value of the cell address you're looking for has not been copied

Dim c As Range

    For Each c In Target
        If OldValues(c.Address) <> "" And c.Value <> "" Then 'both Oldvalue and NewValue are Not Empty
                    Debug.Print "New value of " & c.Address & " is " & c.Value & "; old value was " & OldValues(c.Address)
        ElseIf OldValues(c.Address) = "" And c.Value = "" Then 'both Oldvalue and NewValue are  Empty
                    Debug.Print "New value of " & c.Address & " is Empty " & c.Value & "; old value is Empty" & OldValues(c.Address)

        ElseIf OldValues(c.Address) <> "" And c.Value = "" Then 'Oldvalue is Empty and NewValue is Not Empty
                    Debug.Print "New value of " & c.Address & " is Empty" & c.Value & "; old value was " & OldValues(c.Address)
        ElseIf OldValues(c.Address) = "" And c.Value <> "" Then 'Oldvalue is Not Empty and NewValue is Empty
                    Debug.Print "New value of " & c.Address & " is " & c.Value & "; old value is Empty" & OldValues(c.Address)
        End If
    Next c

    'Copy old values (in case you made any changes in previous lines of code)
    Set OldValues = Nothing
    For Each c In Target
        OldValues.Add c.Value, c.Address
    Next c
1 голос
/ 24 января 2017

Давайте сначала посмотрим, как обнаружить и сохранить значение одной интересующей ячейки. Предположим, что Worksheets(1).Range("B1") является вашей клеткой интереса. В обычном модуле используйте это:

Option Explicit

Public StorageArray(0 to 1) As Variant 
    ' Declare a module-level variable, which will not lose its scope as 
      ' long as the codes are running, thus performing as a storage place.
    ' This is a one-dimensional array. 
      ' The first element stores the "old value", and 
      ' the second element stores the "new value"

Sub SaveToStorageArray()
' ACTION
    StorageArray(0) = StorageArray(1)
        ' Transfer the previous new value to the "old value"

    StorageArray(1) = Worksheets(1).Range("B1").value 
        ' Store the latest new value in Range("B1") to the "new value"

' OUTPUT DEMONSTRATION (Optional)
    ' Results are presented in the Immediate Window.
    Debug.Print "Old value:" & vbTab & StorageArray(0)
    Debug.Print "New value:" & vbTab & StorageArray(1) & vbCrLf

End Sub

Затем в модуле Worksheets (1):

Option Explicit

Private HasBeenActivatedBefore as Boolean
    ' Boolean variables have the default value of False.
    ' This is a module-level variable, which will not lose its scope as 
      ' long as the codes are running.

Private Sub Worksheet_Activate()        
    If HasBeenActivatedBefore = False then
        ' If the Worksheet has not been activated before, initialize the
          ' StorageArray as follows.

        StorageArray(1) = Me.Range("B1")
            ' When the Worksheets(1) is activated, store the current value
              ' of Range("B1") to the "new value", before the 
              ' Worksheet_Change event occurs.

        HasBeenActivatedBefore = True
            ' Set this parameter to True, so that the contents
              ' of this if block won't be evaluated again. Therefore, 
              ' the initialization process above will only be executed 
              ' once.
    End If
End Sub

Private Sub Worksheet_Change(ByVal Target As Range)
    If Not Intersect(Target, Me.Range("B1")) Is Nothing then
        Call SaveToStorageArray
            ' Only perform the transfer of old and new values when 
              ' the cell of interest is being changed.
    End If
End Sub

Это будет фиксировать изменение Worksheets(1).Range("B1"), независимо от того, происходит ли это из-за того, что пользователь активно выбирает эту ячейку на рабочем листе и изменяет значение, или из-за других кодов VBA, которые изменяют значение Worksheets(1).Range("B1").

Поскольку мы объявили переменную StorageArray как общедоступную, вы можете ссылаться на ее последнее значение в других модулях того же проекта VBA.

Чтобы расширить наши возможности обнаружения и сохранения значений нескольких представляющих интерес ячеек, вам необходимо:

  • Объявите StorageArray как двумерный массив с количеством строк, равным количеству отслеживаемых ячеек.
  • Измените процедуру Sub SaveToStorageArray на более общую Sub SaveToStorageArray(TargetSingleCell as Range) и измените соответствующие коды.
  • Измените процедуру Private Sub Worksheet_Change, чтобы обеспечить мониторинг этих нескольких ячеек.

Приложение: Для получения дополнительной информации о времени жизни переменных, пожалуйста, обратитесь к: https://msdn.microsoft.com/en-us/library/office/gg278427.aspx

1 голос
/ 03 июля 2015

Мне нужно было захватить и сравнить старые значения с новыми значениями, введенными в электронную таблицу сложного планирования.Мне нужно было общее решение, которое работало даже тогда, когда пользователь изменил много строк одновременно.В решении реализованы класс и коллекция этого класса.

Класс: oldValue

Private pVal As Variant
Private pAdr As String
Public Property Get Adr() As String
   Adr = pAdr
End Property
Public Property Let Adr(Value As String)
    pAdr = Value
End Property
Public Property Get Val() As Variant
   Val = pVal
End Property
Public Property Let Val(Value As Variant)
   pVal = Value
End Property

Существует три листа, в которых я отслеживаю ячейки.Каждый лист получает свою собственную коллекцию в виде глобальной переменной в модуле с именем ProjectPlan следующим образом:

Public prepColl As Collection
Public preColl As Collection
Public postColl As Collection
Public migrColl As Collection

SUB InitDictionaries вызывается из worksheet.open для создания коллекций.

Sub InitDictionaries()
    Set prepColl = New Collection
    Set preColl = New Collection
    Set postColl = New Collection
    Set migrColl = New Collection
End Sub

Для управления каждой коллекцией объектов oldValue, таких как Add, Exists и Value, используются три модуля.

Public Sub Add(ByRef rColl As Collection, ByVal sAdr As String, ByVal sVal As Variant)
    Dim oval As oldValue
    Set oval = New oldValue
    oval.Adr = sAdr
    oval.Val = sVal
    rColl.Add oval, sAdr
End Sub

Public Function Exists(ByRef rColl As Collection, ByVal sAdr As String) As Boolean
   Dim oReq As oldValue
   On Error Resume Next
   Set oReq = rColl(sAdr)
   On Error GoTo 0

   If oReq Is Nothing Then
      Exists = False
   Else
      Exists = True
   End If
End Function
Public Function Value(ByRef rColl As Collection, ByVal sAdr) As Variant
   Dim oReq As oldValue
   If Exists(rColl, sAdr) Then
      Set oReq = rColl(sAdr)
      Value = oReq.Val
   Else
      Value = ""
   End If
End Function

Тяжелая работа выполняется в обратном вызове Worksheet_SelectionChange.Один из четырех показан ниже.Единственным отличием является коллекция, используемая в вызовах ADD и EXIST.

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
   Dim mode As Range
   Set mode = Worksheets("schedule").Range("PlanExecFlag")
   If mode.Value = 2 Then
      Dim c As Range
      For Each c In Target
          If Not ProjectPlan.Exists(prepColl, c.Address) Then
             Call ProjectPlan.Add(prepColl, c.Address, c.Value)
          End If
      Next c
   End If
End Sub

Вызов VALUE вызывается из кода, выполняемого, например, из обратного вызова Worksheet_Change.Мне нужно назначить правильную коллекцию на основе имени листа:

   Dim rColl As Collection
   If sheetName = "Preparations" Then
       Set rColl = prepColl
   ElseIf sheetName = "Pre-Tasks" Then
       Set rColl = preColl
   ElseIf sheetName = "Migr-Tasks" Then
       Set rColl = migrColl
   ElseIf sheetName = "post-Tasks" Then
       Set rColl = postColl
   Else
   End If

, а затем я могу свободно вычислять сравнение текущего значения с исходным значением.

If Exists(rColl, Cell.Offset(0, 0).Address) Then
   tsk_delay = Cell.Offset(0, 0).Value - Value(rColl, Cell.Offset(0, 0).Address)
Else
   tsk_delay = 0
End If

Mark

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...