0

I have a worksheet with about 50 cells (containing formulas) that changes depending on cells in an external workbook.

I want to trigger a certain macro when ANY of these cells changes it's value.

Worksheet_change event doesn't work and Worksheet_Calculate doesn't refer to the target cell that changes.

I found this code but it won't help since it tests if only one cell value is changed ("A1").

Private Sub Worksheet_Calculate()
   Static OldVal As Variant

   If Range("A1").Value <> OldVal Then
      OldVal = Range("A1").Value
      Call Macro
   End If
End Sub

So I would really appreciate your help about finding a solution for this problem.

Note: All cells containing formulas are named cells.

Community
  • 1
  • 1
user3286479
  • 415
  • 3
  • 15
  • 26

2 Answers2

1

You could keep the values of the sheet in memory, and upon each recalculation check which have changed while at the same time updating that array.

Here is some code, to place in the ThisWorkbook module, that would have such a detection set up for the first sheet (change Sheet1 to whichever sheet you want to monitor):

Dim cache As Variant

Private Sub Workbook_Open()
    cache = getSheetValues(Sheet1)
End Sub

Private Function getSheetValues(sheet As Worksheet) As Variant
    Dim arr As Variant
    Dim cell As Range

    ' Get last cell in the used range
    Set cell = sheet.Cells.SpecialCells(xlCellTypeLastCell)
    ' Get all values in the range between A1 and that cell
    arr = sheet.Cells.Resize(cell.Row, cell.Column)
    If IsEmpty(arr) Then ReDim arr(0, 0) ' Default if no data at all
    getSheetValues = arr
End Function

Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
    Dim current As Variant
    Dim previous As Variant
    Dim i As Long
    Dim j As Long
    Dim prevVal As Variant
    Dim currVal As Variant

    If Sh.CodeName <> Sheet1.CodeName Then Exit Sub
    ' Get the values of the sheet and from the cache
    previous = cache
    current = getSheetValues(Sh)
    For i = 1 To WorksheetFunction.Max(UBound(previous), UBound(current))
        For j = 1 To WorksheetFunction.Max(UBound(previous, 2), UBound(current, 2))
            prevVal = ""
            currVal = ""
            On Error Resume Next ' Ignore errors when out of array bounds
                prevVal = previous(i, j)
                currVal = current(i, j)
            On Error GoTo 0
            If prevVal <> currVal Then
                ' Change detected: call the function that will treat this
                CellChanged Sheet1.Cells(i, j), prevVal
            End If
        Next
    Next
    ' Update cache
    cache = current
ext:
End Sub

Private Sub CellChanged(cell As Range, oldValue As Variant)
    ' This is the place where you would put your logic
    Debug.Print cell.Address & " changed from '" & oldValue & "' to '" & cell.Value & "'"
End Sub

You could use some If statement(s) in the last routine to filter out only those ranges you are really interested in.

For All Sheets

If you need to monitor changes in multiple sheets, you could build your cache to be a collection of 2D arrays, one collection entry per sheet, keyed by its name.

Dim cache As Collection

Private Sub Workbook_Open()
    Dim sheet As Worksheet

    Set cache = New Collection
    ' Initialise the cache when the workbook opens
    For Each sheet In ActiveWorkbook.Sheets
        cache.Add getSheetValues(sheet), sheet.CodeName
    Next
End Sub

Private Function getSheetValues(sheet As Worksheet) As Variant
    Dim arr As Variant
    Dim cell As Range

    ' Get last cell in the used range
    Set cell = sheet.Cells.SpecialCells(xlCellTypeLastCell)
    ' Get all values in the range between A1 and that cell
    arr = sheet.Cells.Resize(cell.Row, cell.Column)
    If IsEmpty(arr) Then ReDim arr(0, 0) ' Default if no data at all
    getSheetValues = arr
End Function

Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
    Dim current As Variant
    Dim previous As Variant
    Dim i As Long
    Dim j As Long
    Dim prevVal As Variant
    Dim currVal As Variant

    ' Get the values of the sheet and from the cache
    previous = cache(Sh.CodeName)
    current = getSheetValues(Sh)
    For i = 1 To WorksheetFunction.Max(UBound(previous), UBound(current))
        For j = 1 To WorksheetFunction.Max(UBound(previous, 2), UBound(current, 2))
            prevVal = ""
            currVal = ""
            On Error Resume Next ' Ignore errors when out of array bounds
                prevVal = previous(i, j)
                currVal = current(i, j)
            On Error GoTo 0
            If prevVal <> currVal Then
                ' Change detected: call the function that will treat this
                CellChanged Sheet1.Cells(i, j), prevVal
            End If
        Next
    Next
    ' Update cache
    cache.Remove Sh.CodeName
    cache.Add current, Sh.CodeName
ext:
End Sub

Private Sub CellChanged(cell As Range, oldValue As Variant)
    ' This is the place where you would put your logic
    Debug.Print cell.Address & " changed from '" & oldValue & "' to '" & cell.Value & "'"
End Sub

This would work for sheets that exist from the start, not sheets that are added. Of course, that also could be made to work, but you'll get the idea.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Thank you so much trincot .. that was perfect :) .. One more thing.. Could you please explain to me what does the "2" in UBound(cache, 2) mean? – user3286479 May 23 '17 at 08:32
  • 1
    You're welcome. `cache` is a 2-dimensional array. To know how many rows it has you would do `UBound(cache, 1)` (but 1 is the default, so it's not needed to provide it). To know how many *columns* it has, you provide as second argument a 2, i.e. you ask: "what is the array's size in its second dimension?" – trincot May 23 '17 at 09:48
  • Sorry for bothering you.. But I want to know what if my cells exists in more that one sheet? Is there a way other that declaring a variant variable for each sheet? cashe1,cashe2, etc... – user3286479 May 23 '17 at 13:29
  • You can create one variant variable that contains the information for all sheets. If you want, I can look into this and adapt the answer. – trincot May 23 '17 at 13:35
  • Thank you so much.. I would really appreciate that – user3286479 May 23 '17 at 14:06
  • Updated my answer. – trincot May 23 '17 at 14:17
  • Dear trincot.. What if my range does not begin from row 1 or column 1.. How to find the first used cell in UsedRange?! – user3286479 May 28 '17 at 10:37
  • Good question. I updated both code blocks to always store the range from cell A1 up until the last used cell. I don't have access to Excel today, so I could not verify. If there still is an issue, let me know. – trincot May 28 '17 at 17:46
  • Perfect. Thank u again. – user3286479 Jun 01 '17 at 11:15
0

Maybe you can start with this code.

Private Sub Worksheet_Change(ByVal Target As Range)
  Dim rIntersect As Range
  Set rIntersect = Intersect(Target, Application.names("NameOfRange").RefersToRange)
  If Not rIntersect Is Nothing Then
    MsgBox "found" '<~ change to your liking
  End If
End Sub
Romcel Geluz
  • 583
  • 2
  • 10
  • Thank you for your reply Romcel .. Unfortunately Worksheet_Change event is not triggered when a cell containig formula changes its value. – user3286479 May 23 '17 at 06:36
  • In the line `Set rIntersect = Intersect(Target, Application.names("NameOfRange").RefersToRange)` change the "NameOfRange" to their respective name. – Romcel Geluz May 23 '17 at 06:48
  • Thanks again for your reply .. But again, Worksheet_Change event is not triggered when a cell containig formula changes its value. Nothing happens at all regardless what the code is. It's triggered only if I change the value manually, but not through a formula. – user3286479 May 23 '17 at 06:54
  • Opps! I badly misunderstood you. I will look deeper into this. – Romcel Geluz May 23 '17 at 06:55
  • Thank you so much. – user3286479 May 23 '17 at 07:07