2

I would like my Macro to launch whenever a value in a cell containing a formula changes. i.e. the user is modifying another cell thus changing the value of the cell in question.

I have noticed that using the statement (found herein), only works if the user modifies the cell itself but not if the cell changes automatically - due to a formula as specified above.

Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A20")) Is Nothing Then ...

Any thoughts??

I tried to follow the answers from this question "automatically execute an Excel macro on a cell change" but it did not work...

Thanks in advance :)

Community
  • 1
  • 1
Cynthia Kreidy
  • 27
  • 1
  • 3
  • 7

4 Answers4

1

A possible work-around comes from the fact that, to change a value, the user needs to change the selection first. So I would:

1) Declare a global variable called "oldValue" on top of the WS source code module:

Dim oldValue As Variant

2) Register the old value of your formula before the user types anything (let's say it's in Range("A4"), I let you adapt with the others):

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    oldValue = Range("A4")
End Sub

3) Check if the change has affected the formula in the Change event:

Private Sub Worksheet_Change(ByVal Target As Range)
    If Range("A4") <> oldValue Then
        MsgBox "User action has affected your formula"
    End If
End Sub

I've tested with a simple sum, I'm able to write cells that are not involved without any prompt but if I touch one of the cells involved in the sum the MsgBox will show up. I let you adapt for multiple cases, for user adding/removing rows (in that case I suggest to name the ranges containing the formulas you want to track) and the worksheet references.

EDIT I'd like to do it at once, not by going through 2 processes, is it possible? The problem is my macro involves a range containing more than one cell so it will be hard to store old values for 10 cells.

If ranges are next to each other, then instead of using a variable you can use a collection:

Dim oldValues As New Collection
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    For j = 1 To 10
        oldValues.Add Range("A" & j).Value
    Next j
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
    For j = 1 To 10
        If Range("A" & j).Value <> oldValues(j) Then
            MsgBox "The value of Range(A" & j & ") has changed"
        End If
    Next j
End Sub

Of course, if ranges are not close to each other, you can just store them anyway in the SelectionChange event like this:

oldValues.Add Range("A1").Value
oldValues.Add Range("B7").Value
'...

and if you done this ONCE, with 10 ranges only, it should be a reasonable solution to your problem.

Matteo NNZ
  • 11,930
  • 12
  • 52
  • 89
  • I'd like to do it at once, not by going through 2 processes, is it possible? The problem is my macro involves a range containing more than one cell so it will be hard to store old values for 10 cells... – Cynthia Kreidy Dec 18 '14 at 17:47
  • Are the ranges next to each other? For example, Range("A1:A10")? P.s. 10 (ten) cells sounds hard?? :) – Matteo NNZ Dec 18 '14 at 17:51
  • P.s. you need to use two macros, if you want to register the values of the ranges BEFORE the change occurs. They are two separate events, they cannot be performed in one. – Matteo NNZ Dec 18 '14 at 17:52
  • yes they're next to each other, and I've figured out a way to do it but it's lengthy, what if you have a much larger range of cells? And by the way, no need to get grumpy, I'm new to VBA and I'm not a programmer, if I was an expert I wouldn't ask questions. Anyway thanks for your help. – Cynthia Kreidy Dec 18 '14 at 18:00
  • @CynthiaKreidy, I'm not getting grumpy, I was being sarcastic cause you might find yourself needing to store much more than 10 variables ;) I've made an edit to my answer, now it should be fine. As soon as the ranges are consecutive, you can even record 100000 of them with 3 lines of code. – Matteo NNZ Dec 18 '14 at 18:02
  • @cynthia, just to make clear: i dont know why you get offended by what i have said, im sorry if you thought i meant to kid you really. I just wanted to point you out that defining 10 variables is not a big deal, if you have to write the code only once. However, the new correction I made (as long as ranges are close to each other) will allow you to track thousands of values with three lines of code. Let me know if you need further info, but unfortunately you cannot split the process because the two events are separate by mechanics. – Matteo NNZ Dec 18 '14 at 22:31
1

You said, "I would like my Macro to launch whenever a value in a cell containing a formula changes..."

If having your code run whenever a cell containing a formula is recalculated (which is not exactly what you asked for), one solution might be to create a VBA function that simply returns that value passed to it, plus does whatever else you want to do when the formula is recalculated...

Public Function Hook(ByVal vValue As Variant) As Variant
    Hook = vValue

    ' Add your code here...
End Function

...then "wrap" your formula in a call to this function. For example, if the formula you are interested in is =A1+1, you would change this to =Hook(A1+1), and the Hook function would be called whenever A1+1 is recalculated (for example, when the value in A1 changes). However, it is possible that recalculating A1+1 will yield the same result and still call the Hook function (for example, if the user re-enters the same value in A1).

Brian Camire
  • 4,685
  • 2
  • 26
  • 23
  • It's not a bad idea Brian! +1. However, this would be called at any "recalculate" event (e.g. Open workbook, calculate action etc...). Do you have something in mind to manage this and complete your answer? – Matteo NNZ Dec 18 '14 at 22:26
1

Declaring a module -level variable like Matteo describes is definitely one good way to go.

Brian 's answer is on the right track with regards to keeping all is the code in the same place, but it's missing one critical part : Application.Caller

When used in function that is called by a single cell, Application.Caller will return the Range object of that cell. This way you can store the old value within the function itself when it is called, then once you're done with calculating the new value you can compare it with the old and run more code as required.

Edit: The advantage with Application.Caller is that the solution scales in and of itself, and does not change no matter how the target cells are arranged (I.e. Continuous or not).

  • Your answer intrigued me, so I tried it out. However, I found that referencing the the current value of the cell (via `Application.Caller.Value`) within the function created a circular reference, which makes sense. I found discussion of some possible alternatives [here](https://fastexcel.wordpress.com/2012/01/08/writing-efficient-vba-udfs-part-8-getting-the-previously-calculated-value-from-the-calling-cells/), but they're either brittle or complex. The cost of the "false positives" of the simple approach I suggested would need to be high in order to justify these alternatives. – Brian Camire Dec 19 '14 at 14:25
  • Ha! I didn't know that: you can refer to Application.Caller without problem, but getting its value with `Application.Caller.Value` is what triggers the circular reference. Indeed it seems there are no good workaround for that one, so my answer does not stand. In this case I would go with Matteo's answer and declare a Module-level variable. Maybe not as "simple" as you would have liked, but we have to work with the limitations of the system :) – Vincent Courtemanche Dec 19 '14 at 15:48
1

You can have a go at this:

First, in a Module Code declare a Public Variable.

Public r As Range, myVal '<~~ Place it in Module

Second, initialize your variables in Workbook_Open event.

Private Sub Workbook_Open()
    Set r = Sheet1.Range("C2:C3") '<~~ Change to your actual sheet and range
    myVal = Application.Transpose(r)
End Sub

Finally, set up your Worksheet_Calculate event.

Private Sub Worksheet_Calculate()
    On Error GoTo halt
    With Application
        .EnableEvents = False
        If Join(myVal) <> Join(.Transpose(r)) Then
            MsgBox "Something changed in your range"
            '~~> You put your cool stuff here
        End If
        myVal = .Transpose(r)
forward:
        .EnableEvents = True
    End With
    Exit Sub
halt:
    MsgBox "Error " & Err.Number & ": " & Err.Description
    Resume forward
End Sub

Above will trigger the event when values in C2:C3 changes.
Not really very neat but works in detecting changes in your target range. HTH.

L42
  • 19,427
  • 11
  • 44
  • 68