Excel detecting and keeping track of (value) changes in any worksheet - excel

I've managed to write a code that detects value changes of particular cells in any worksheet, but I've struggled to construct something that detects and keeps track of ranged (value) changes.
For example, if a user decides to copy and paste some range of data (lets say more than 1 cell), it will not get caught by the macro. Same goes for a user selecting a range and then manually entering values into each cell while range is still selected.
My current code is constructed of 2 macros, the first runs anytime a worksheet selection change occurs and it stores the target.value into a previous value variable. The second macro runs anytime a worksheet change occurs and it tests if the targeted value is different than the previous one, if so it then notifies the user of the change that had occurred.

OK I don't really see anything here which covers the whole thing, so here's a rough attempt.
It will handle single or multi-cell updates (up to some limit you can set beyond which you don't want to go...)
It will not handle multi-area (non-contiguous) range updates, but could be extended to do so.
You likely should add some error handling also.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim Where As String, OldValue As Variant, NewValue As Variant
Dim r As Long, c As Long
Dim rngTrack As Range
Application.EnableEvents = False
Where = Target.Address
NewValue = Target.Value
Application.Undo
OldValue = Target.Value 'get the previous values
Target.Value = NewValue
Application.EnableEvents = True
Set rngTrack = Sheets("Tracking").Cells(Rows.Count, 1).End(xlUp).Offset(1, 0)
'multi-cell ranges are different from single-cell ranges
If Target.Cells.CountLarge > 1 And Target.Cells.CountLarge < 1000 Then
'multi-cell: treat as arrays
For r = 1 To UBound(OldValue, 1)
For c = 1 To UBound(OldValue, 2)
If OldValue(r, c) <> NewValue(r, c) Then
rngTrack.Resize(1, 3).Value = _
Array(Target.Cells(r, c).Address, OldValue(r, c), NewValue(r, c))
Set rngTrack = rngTrack.Offset(1, 0)
End If
Next c
Next r
Else
'single-cell: not an array
If OldValue <> NewValue Then
rngTrack.Resize(1, 3).Value = _
Array(Target.Cells(r, c).Address, OldValue, NewValue)
Set rngTrack = rngTrack.Offset(1, 0)
End If
End If
End Sub
"Undo" part to get the previous values is from Gary's Student's answer here:
Using VBA how do I detect when any value in a worksheet changes?

This subs will work for you but you have just implement codes in every sheet manually. Just need to copy paste. See below screenshot which is for 1 sheet Sheet1
(1) Declare a public variable.
Public ChangeTrac As Variant
(2) Write below codes in Worksheet_SelectionChange event
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
ChangeTrac = Target.Value
End Sub
(3) write below codes in Worksheet_Change event
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Application.Intersect(Target, Cells()) Is Nothing Then
If ChangeTrac <> Target.Value Then
MsgBox "Value changed to Sheet1 " & Target.Address & " cell."
Range(Target.Address).Select
End If
End If
End Sub
Then test by changing data in any cell. It will prompt if any cell value is changed.

Related

Running a Worksheet_change function when closing the workbook

I want to run a Worksheet_change function that will collect the cell references of any changed cells into an array of "Cells" objects but I keep getting the error "Type mismatch". This is what i've got so far:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim arArray(1 To 70) As Range
Dim K As Integer
K = 1
For i = 1 To 70
For j = 2 To 14
If Target.Column = j And Target.Row = i Then
Set arArray(K) = Target.Address
K = K + 1
End If
Next j
Next i
End Sub
Currently the code looks for any changes within the grid B1 to N70 and stores the changed cell if a change has occurred to a cell within that grid.
Any help would be greatly appreciated.
Right now, your code is set to look over many cells every time any cell changes. Based on your initial description, I'm sure that this is not what you really want. In the following code, Worksheet_Change keeps track of each cell that gets changed in B1:N70 by putting its address in a collection named "changed_cells". While "show_changes" prints the address of the cells that got changed to the immediate window.
Option Explicit
Dim changed_cells As New Collection
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Range("B1:N70"), Target) Is Nothing Then
changed_cells.Add Target.Address(False, False)
End If
End Sub
Private Sub show_changes()
Dim x As Long
For x = 1 To changed_cells.Count
Debug.Print changed_cells(x)
Next
End Sub
Note: If the immediate window is not visible, press ctrl+g to see the ouptut
You declared an array of Range objects at the top and the Target.Address property returns a string.
Your line
Set arArray(K) = Target.Address
Should be
Set arArray(K) = Range(Target.Address)

How to change value of the active cell when clicked using VBA?

I am not sure whether this is running, has errors, or whether the variables are correct. I followed steps online to check what my variables are, like typing ?variable in the immediate window, checking the locals window, and hovering my mouse over the variable, but nothing comes up.
Nothing happens regardless when I go back to the workbook.
Here's a screenshot:
Included a screenshot because the problem might not be just with the code.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.address = "C5:I5" Then
Dim row As Integer
row = Application.WorksheetFunction.Match(ActiveCell.Offset(0, -1).Value, Range("$n$1:$n$365"), 0)
Dim address As Long
address = Application.WorksheetFunction.address(row, 15)
Range(address).Value = Range(address).Value + 1
ActiveCell.Value = Range(address).Value
End If
End Sub
The purpose is to add 1 to the value of the active cell when clicked. The cell's value will change based on the date in the cell directly above it; the value needs to be tied to the date. I plan to accomplish this using a hidden array of ascending dates and values, located at n1:o365.
(a) Probably your intention is to check if the target cell is within the range "C5:I5" - what your checking is if target has the address "C5:I5" so the if fails.
Use for example the function Intersect for that
(b) (Minor thing) Declare row as Long
(c) There is no .WorksheetFunction.address function. A Range has an Address property, eg Target.Address. Note that this will return a String, not a Long. But you don't need this anyhow. Use Cells if you know row and column of a cell.
Note that I haven't checked your logic to find the correct row.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Intersect(Target, Range("C5:I5")) Is Nothing Then Exit Sub
Dim row As Long
On Error Resume Next
row = Application.WorksheetFunction.Match(Target.Offset(0, -1).Value, Range("$n$1:$n$365"), 0)
On Error GoTo 0
If row = 0 Then Exit Sub ' Row not found
Dim cell As Range
Set cell = Cells(row, 15)
cell.Value = cell.Value + 1
Target.Value = cell.Value
End Sub

How can I see when a cell had its contents cleared vs. set to 0 in a "Worksheet_Change" sub?

I am writing a code in a "Worksheet_Change" sub where i need to store potential changes in values of cells (always integer). However, when a value is deleted, it appears as "0" in my worksheet, and I need it to show no value at all, since "0" and "nothing" is handled in completely different ways in my file.
So, how can I code so that I can tell the difference between what is being set as zero, and what is being deleted in order to, when I need to delete the value of a cell (so clear its contents), it will appear empty instead of with a zero?
I believe the problem can be somewhere in the following part:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim oldQty As Integer
Dim newQty As Integer
Application.EnableEvents = False
With Target
newQty = .Value
Application.Undo
oldQty = .Value
.Value = newQty
End With
Application.EnableEvents = True
(...)
End Sub
I need this to store the old quantity of the cell.
I hope I was clear enough, any help is appreciated
Put the following line at the very beginning of Worksheet_Change.
If Target.Cells.Count > 1 Then Exit Sub 'prevent error in case of range edits
And change the following part
With Target
newQty = .Value
Application.Undo
oldQty = .Value
.Value = IIf(newQty = 0, vbNullString, newQty)
'if value is zero return an epmty string instead of `0`
End With

Limit the number of times each option in a dropdown can be selected

I have a table with 16 rows.
Each cell in column A has a dropdown list with 10 items.
I want to set a limit for each of these items so that, for example, the first one couldn’t be selected more than 3 times, the second one no more than 2 times and so on.
Is it feasible with or without VBA?
You could use something like this in the sheet module:
Option Explicit
Private OldValue As Variant
Public Sub Worksheet_SelectionChange(ByVal Target As Range)
'Storing the old value of the newly selected cell
OldValue = Target.Value2
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
On Error GoTo ErrHandler
Application.EnableEvents = False
'Check if the cell that was changed is in column A
If Not Intersect(Target, Me.Range("A:A")) Is Nothing Then
If Application.WorksheetFunction.CountIf(Me.Range("A1:A1000"), Target.Value2) > 2 Then
MsgBox "You can't select more than 2 times the value: " & Target.Value2
'Reset to the old value
Target.Value2 = OldValue
End If
End If
Application.EnableEvents = True
Exit Sub
ErrHandler:
Application.EnableEvents = True
End Sub
Private Sub Worksheet_Activate()
'If the worksheet just got activated, make sure we have the value of the active cell stored.
If IsEmpty(OldValue) Then
OldValue = ActiveCell.Value2
End If
End Sub
Basically, you have to use the Worksheet_SelectionChange event to store the previous value of the cell when it is selected. Then after the user tries to make a change to the cell, the Worksheet_Change event will look through the first 1000 cells of column A (you can always customize this amount) with the COUNTIF function.
Then, if the number of occurrences of the newly selected value is too high (>2 in this case), an error message is displayed and the value is set back to the previous value.
For safety measures, I've added the Worksheet_Activate event code to make sure that we have the value of the cell even if the user arrives from another sheet and doesn't change the selected cell.
If you want to have different limits for the number of repetitions allowed, you could add a Select Case that would handle that :
...
If Not Intersect(Target, Me.Range("A:A")) Is Nothing Then
Dim UpperLimit As Long
Select Case Target.Value2
Case Is = "First value": UpperLimit = 3
Case Is = "Second value": UpperLimit = 2
Case Is = "Third value": UpperLimit = 1
Case Else: UpperLimit = 2 'Default limit
End Select
If Application.WorksheetFunction.CountIf(Me.Range("A1:A1000"), Target.Value2) > UpperLimit Then
MsgBox "You can't select more than "UpperLimit & " times the value " & Target.Value2
Target.Value2 = OldValue
End If
...
Additionally, you might want to prevent people to copy paste in that region. If that's the case, this could be useful to you:
https://jkp-ads.com/Articles/CatchPaste.asp

How to return a timestamp when a specific value is entered into a specific cell

I'm creating a report-styled sheet in Excel, and trying to get a timestamp to automatically be entered in cell "P4" if cell "I6" has a value of "Completed"
I've tried using =IF formulas, which worked, but I'm unable to toggle iterative calculation on the machines this sheet will be working on.
I'm fairly new to writing my own VBA, and I'm having some trouble getting my current code to work. Below is what I currently have, which isn't giving me any results.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim r As String
Set r = Cells("I6")
If r.Value Is Nothing Then Exit Sub
If r.Value <> "Completed" Then Exit Sub
If r.Offset(-2, 7).Value <> "" Then Exit Sub
Application.EnableEvents = False
r.Offset(-2, 7) = Now()
Application.EnableEvents = True
End If
End Sub
I expect the code to give me a current timestamp in Cell "P4" once the value "Completed" is entered into cell "I6", but nothing is showing up. How would I correct it in order to get the value based timestamps?
As this sub is called at every cell's change (and you may use it later for other cell-checks also), check by Intersect first, if "your" cell is affected.
The changed range is given as Target (which may be a single cell or a complete range, e. g. when you paste on it). If that is intersected with your monitored cell I6, you can go ...
Private Sub Worksheet_Change(ByVal Target As Range)
Dim RelevantArea As Range
Set RelevantArea = Intersect(Target, Me.Range("I6"))
If Not RelevantArea Is Nothing Then
If Target.Value = "Completed" Then
Application.EnableEvents = False
Me.Range("P4").Value = Now()
Application.EnableEvents = True
End If
End If
End Sub

Resources