Private Sub Worksheet_SelectionChange/Worksheet_Change - vba

Good day, I need to create 2 private macros in one workbook - one which stores content of cell after clicking on it and second one which will store the new value of cell and send and email with body of old text in cell and new text in cell.
Truth to be told, I'm not sure if this is the right way to do it (or if it's even possible) but I don't work with private macros often so I will appreciate any help. Thanks a lot!
That is what i got so far:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.Cells.Count > 1 Then Exit Sub
OldCellValue = ActiveCell.text
old_value = OldCellValue
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
Dim Area As Range
Dim OutlApp As Object
Dim IsCreated As Boolean
Dim cell As String
Dim old_value As String
Dim new_value As String
Set Area = Range("A1:E20")
If Target.Cells.Count > 1 Then Exit Sub
If Not Intersect(Target, Area) Is Nothing Then
cell = ActiveCell.Address
new_value = ActiveCell.text
On Error Resume Next
Set OutlApp = GetObject(, "Outlook.Application")
If Err Then
Set OutlApp = CreateObject("Outlook.Application")
IsCreated = True
End If
OutlApp.Visible = True
On Error GoTo 0
With OutlApp.CreateItem(0)
.Subject = "Change in table"
.to = "someones email"
.HTMLBody = "Change in cell " & "<B>" & cell & "</B><br>" _
& "Old value: " & old_value & "New value: " & new_value
On Error Resume Next
.Send
Application.Visible = True
On Error GoTo 0
End With
If IsCreated Then OutlApp.Quit
Set OutlApp = Nothing
End With
End If
End Sub

Not re-writing all your code, but in essence you have to do this to store the value when the cell is selected and then after it is changed. You don't need the newcellvalue variable as Target captures that.
Dim OldCellValue
Private Sub Worksheet_Change(ByVal Target As Range)
Dim newcellvalue
newcellvalue = Target.Value
MsgBox "Old " & OldCellValue & ", New " & newcellvalue
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
OldCellValue = Target.Value
End Sub

I think you're on the right track. I would use a global variable to track the current/old value so you can compare it in the Worksheet_Change event.
Something like this:
Private old_value As String
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.Cells.Count > 1 Then Exit Sub
old_value = Target.Text
'Debug to check the old_value
'Debug.Print "old_value = " + old_value
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
Dim Area As Range
Dim OutlApp As Object
Dim IsCreated As Boolean
Dim cell As String
Dim new_value As String
Set Area = Range("A1:E20")
If Target.Cells.Count > 1 Then Exit Sub
If Not Intersect(Target, Area) Is Nothing Then
new_value = Target.Text
'Debug to compare values
'Debug.Print "new_value = " + new_value
'Debug.Print "old_value = " + old_value
If new_value <> old_value Then
'Debug to compare
'Debug.Print "new_value and old_value are different"
End If
End If
End Sub
Keep in mind that the Worksheet_SelectionChange event is going to fire every time you navigate between cells. So, if you change the value of a cell and press the Enter key, the value of old_value is going to change because you are resetting the value in the Worksheet_SelectionChange event. You need to perform the comparison and send the email before the selection changes.
Also, you'll probably want to use .Value for the cell instead of .Text. See this post for the differences: What is the difference between .text, .value, and .value2?

Related

VBA Macro triggering too often

My worksheet is set up with data validation dropdowns and I am wanting a macro to ONLY trigger when the value of the cell is changed from another value in the dropdown, not from the default "empty" value.
Here is what I am trying to use:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Column = 5 Then
If IsEmpty(Target.Value) = True Then
MsgBox "Test1"
Else
MsgBox "Test2"
End If
End If
exitHandler:
Application.EnableEvents = True
Exit Sub
End Sub
My problem is that this "IsEmpty" command is reading the cell AFTER the selection not before. I want it to read what the cells value was BEFORE the selection not after.
How can I do this?
Example approach:
Const COL_CHECK As Long = 5
Private oldVal
Private Sub Worksheet_Change(ByVal Target As Range)
Dim c As Range
Set c = Target.Cells(1) '<< in case multiple cells are changed...
If c.Column = COL_CHECK Then
If oldVal <> "" Then
Debug.Print "changed from non-blank"
Else
Debug.Print "changed from blank"
End If
End If
exitHandler:
Application.EnableEvents = True
Exit Sub
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim c As Range
Set c = Target.Cells(1)
oldVal = IIf(c.Column = COL_CHECK, c.Value, "")
Debug.Print "oldVal=" & oldVal
End Sub
Another approach:
This will need one cell per validation-dropdown:
Function ValChange(Cell2Follow As Range) As String
ValChange = ""
If Len(Application.Caller.Text) = 0 Then Exit Function
If Application.Caller.Text = Cell2Follow.Text Then Exit Function
MsgBox "value of the cell is changed from another value in the dropdown" & vbLf & "not from the default 'empty' value"
End Function
in a different cell, assumed the dropdown is in E6:
=E6&ValChange(E6)
application.caller.text will be the old value
(calculation must be automatic)

Capture Cell Value on left mouse single click in Excel VBA

I need help in capturing the cell value on left mouse click. So when a user has done a single left mouse click in a particular cell then I need to capture that particular cell value (Value written in that cell) in the VBA code.
This value will then be passed on to the VBA code and the output will be different for click in different cells. I hope I was able to explain the purpose.
I have total of 10 cells where the left mouse click value is to be captured.
This code will check to see if multiple cells are selected at once then it checks to see if the cell is empty. If it is empty then it exits otherwise it stores the value is cell N1. You can change which cell the value gets stores at. If N1 has a value it goes to the next empty cell in column N.
Dim oval
Dim N As Long
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.Cells.Count < 2 Then
If Target = Empty Then
Exit Sub
End If
oval = Target.Value
MsgBox "The value saved is " + oval + "."
If Range("N1").Value = "" Then
N = 1
Else
N = Cells(Rows.Count, "N").End(xlUp).Row + 1
End If
Cells(N, "N").Value = oval
End If
End Sub
If you would settle for a double mouse click, you could try something like
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
MsgBox Target.Value
Cancel = True
End Sub
How about this? Just wrote it, it will not register a mouse click if you click where you could have reached with the keyboard
Enter Under Worksheet Code:
XXXXXXX
Option Explicit
Private prevTarget As Range
Private Sub Worksheet_Activate()
Set prevTarget = Selection
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim wasMouseClick As Boolean
Dim ch1, ch2, ch3, ch4, ch5, ch6, ch7, ch8, ch9 As String
On Error Resume Next
ch1 = prevTarget.Offset(1, 0).Address
ch2 = prevTarget.Offset(-1, 0).Address
ch3 = prevTarget.Offset(0, 1).Address
ch4 = prevTarget.Offset(0, -1).Address
ch5 = prevTarget.End(xlDown).Address
ch6 = prevTarget.End(xlToLeft).Address
ch7 = prevTarget.End(xlToRight).Address
ch8 = prevTarget.End(xlUp).Address
On Error GoTo error_noPreTarget
If Not (Target.Address = ch1 Or _
Target.Address = ch2 Or _
Target.Address = ch3 Or _
Target.Address = ch4 Or _
Target.Address = ch5 Or _
Target.Address = ch6 Or _
Target.Address = ch7 Or _
Target.Address = ch8) Then
wasMouseClick = True
End If
Set prevTarget = ActiveCell
If wasMouseClick Then
Debug.Print wasMouseClick ' replace with what you want when wasMouseClick = True
End If
Exit Sub
error_noPreTarget:
Set prevTarget = Selection
End Sub

VBA - Change color of modified text

I have this code that changes the color of the text in a cell if it is modified. However I was looking into something that only changes the color of modified text inside the cell. For example I have in cell A1 = "This cell" and when I change it to "This cell - this is new text" I would like just to change the color of "- this is new text"
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A1:A100")) Is Nothing Then
If Target.Font.ColorIndex = 3 Then
Target.Font.ColorIndex = 5
Else
Target.Font.ColorIndex = 3
End If
End If
End Sub
Thanks
Here's what I put together:
Dim oldString$, newString$
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A1:A100")) Is Nothing Then
newString = Target.Value
If Target.Font.ColorIndex = 3 Then
Target.Font.ColorIndex = 5
Else
Target.Font.ColorIndex = 3
End If
End If
Debug.Print "New text: " & newString
color_New_Text oldString, newString, Target
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Excel.Range)
If Not Intersect(Target, Range("A1:A100")) Is Nothing Then
oldString$ = Target.Value
Debug.Print "Original text: " & oldString$
End If
End Sub
Sub color_New_Text(ByVal oldString As String, ByVal newString As String, ByVal theCell As Range)
Dim oldLen&, newLen&, i&, k&
oldLen = Len(oldString)
newLen = Len(newString)
Debug.Print newString & ", " & oldString
For i = 1 To newLen
If Mid(newString, i, 1) <> Mid(oldString, i, 1) Then
Debug.Print "different"
Debug.Print theCell.Characters(i, 1).Text
If theCell.Characters(i, 1).Font.ColorIndex = 3 Then
theCell.Characters(i, 1).Font.ColorIndex = 5
Else
theCell.Characters(i, 1).Font.ColorIndex = 3
End If
End If
Next i
End Sub
It's two global variables, a Worksheet_SelectionChangeand Worksheet_Changeto get the strings.
It is laborious:
detect that a cell has changed in the range of interest
use UnDo to get the original contents
use ReDo to get the new contents
compare them to get the changed characters
use the Characters property of the cell to format the new characters
I would use UnDo to avoid keeping a static copy of each of the 100 cells.
using the tip from Gary's Student, I retain the old value of cell and compare with the new value. Then use the lenght to get the 'difference' and color the 'characters'. Here's the modification:
Option Explicit
Public oldValue As Variant
Public Sub Worksheet_SelectionChange(ByVal Target As Range)
oldValue = Target.Value
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
Dim oldColor
If Not Intersect(Target, Range("A1:A100")) Is Nothing Then
If Target.Value <> oldValue Then
oldColor = Target.Font.ColorIndex
Target.Characters(Len(oldValue) + 1, Len(Target) - Len(oldValue)).Font.ColorIndex = IIf(oldColor = 3, 5, 3)
End If
End If
End Sub
P.S. Sorry my english
This will change the font, but it's not perfect. Seems if you have different font colours in the same cell then Target.Font.ColorIndex returns NULL so it only works on the first change.
Option Explicit
Dim sOldValue As String
Private Sub Worksheet_Change(ByVal Target As Range)
Dim sNewValue As String
Dim sDifference As String
Dim lStart As Long
Dim lLength As Long
Dim lColorIndex As Long
On Error GoTo ERROR_HANDLER
If Not Intersect(Target, Range("A1:A100")) Is Nothing Then
sNewValue = Target.Value
sDifference = Replace(sNewValue, sOldValue, "")
lStart = InStr(sNewValue, sDifference)
lLength = Len(sDifference)
If Target.Font.ColorIndex = 3 Then
lColorIndex = 5
Else
lColorIndex = 3
End If
Target.Characters(Start:=lStart, Length:=lLength).Font.ColorIndex = lColorIndex
End If
On Error GoTo 0
Exit Sub
ERROR_HANDLER:
Select Case Err.Number
'I haven't added error handling - trap any errors here.
Case Else
MsgBox "Error " & Err.Number & vbCr & _
" (" & Err.Description & ") in procedure Sheet1.Worksheet_Change."
End Select
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Not Intersect(Target, Range("A1:A100")) Is Nothing Then
sOldValue = Target.Value
End If
End Sub
Edit: It will only work with a continuous string. Maybe can change to look at each character in sOldValue and sNewValue and change colour as required.
Try with below
Private Sub Worksheet_Change(ByVal Target As Range)
Dim newvalue As String
Dim olvalue As String
Dim content
Application.EnableEvents = False
If Not Intersect(Target, Range("A1:A100")) Is Nothing Then
If Target.Font.ColorIndex <> -4105 Or IsNull(Target.Font.ColorIndex) = True Then
newvalue = Target.Value
Application.Undo
oldvalue = Target.Value
Content = InStr(newvalue, Replace(newvalue, oldvalue, ""))
Target.Value = newvalue
With Target.Characters(Start:=Content, Length:=Len(newvalue)).Font
.Color = 5
End With
Else
Target.Font.ColorIndex = 3
End If
End If
Application.EnableEvents = True
End Sub

How can I run my Excel macro when a specific cell changes? [new twist]

I googled around, and I wrote the following code that I want to run only when a specific cell, D4, changes:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim KeyCells As Range
Static EmailSent As Boolean
Dim Threshold As Integer
Dim Cell As String, Email As String, Msg As String
Cell = "D4"
Threshold = 100
Email = Range("E7").Value
Set KeyCells = Range(Cell)
If Not Application.Intersect(Range(Cell), Range(Target.Address)) Is Nothing Then
Dim x As Integer
x = Range(Cell).Value
If x >= Threshold Then
EmailSent = False
ElseIf x < Threshold And Not EmailSent Then
EmailSent = True
Msg = "You only have " & x & " widgets remaining."
MsgBox Msg
SendMail Email, Msg
End If
End If
End Sub
This works, and I know there are a lot of similar questions here. But here's where I'm having trouble: this only works if I set D4 to an explicit value, say "48". I want it to work even if D4 is a formula: so if D4 is "=SUM(A4:C4)" then an email should send if that sum drops below 100. This code won't send an email in that case :-(
Does anyone know how to fix this?
Private Sub Worksheet_Calculate()
CheckForMail
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
CheckForMail Target
End Sub
Sub CheckForMail(Optional rng As Range = Nothing)
Static EmailSent As Boolean
Dim KeyCells As Range
Dim Threshold As Integer
Dim Cell As String, Email As String, Msg As String
Dim x As Integer
Cell = "D4"
Set KeyCells = Me.Range(Cell)
'if called from worksheet_change, check the range
If Not rng Is Nothing Then
If Application.Intersect(KeyCells, rng) Is Nothing Then
Exit Sub
End If
End If
Threshold = 100
Email = Me.Range("E7").Value
x = KeyCells.Value
If x >= Threshold Then
EmailSent = False
ElseIf x < Threshold And Not EmailSent Then
EmailSent = True
Msg = "You only have " & x & " widgets remaining."
MsgBox Msg
SendMail Email, Msg
End If
End Sub
You need to check your cell when any of the contributing cells change, then. If they're in the same sheet, this will work:
Try this:
Private Sub Worksheet_Change(ByVal Target As Range)
Static varD4_old As Variant ' Static variables retain their value between calls
Dim varD4 As Variant
varD4 = Range("D4").Value
If varD4 &LT;&GT; varD4_old Then
Debug.Print "D4 changed from " & varD4_old & " to " & varD4
varD4_old = varD4
' NOW RUN MY CHANGE CODE
End If
End Sub

How do I get the old value of a changed cell in Excel VBA?

I'm detecting changes in the values of certain cells in an Excel spreadsheet like this...
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
Assuming this isn't too bad a way of coding this, how do I get the value of the cell before the change?
try this
declare a variable say
Dim oval
and in the SelectionChange Event
Public Sub Worksheet_SelectionChange(ByVal Target As Range)
oval = Target.Value
End Sub
and in your Worksheet_Change event set
old_value = oval
You can use an event on the cell change to fire a macro that does the following:
vNew = Range("cellChanged").value
Application.EnableEvents = False
Application.Undo
vOld = Range("cellChanged").value
Range("cellChanged").value = vNew
Application.EnableEvents = True
I had to do it too. I found the solution from "Chris R" really good, but thought it could be more compatible in not adding any references. Chris, you talked about using Collection. So here is another solution using Collection. And it's not that slow, in my case. Also, with this solution, in adding the event "_SelectionChange", it's always working (no need of 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
I have an alternative solution for you. You could create a hidden worksheet to maintain the old values for your range of interest.
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
Delete it when the workbook is closed...
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Application.DisplayAlerts = False
Me.Worksheets("HiddenSheet").Delete
Application.DisplayAlerts = True
End Sub
And modify your Worksheet_Change event like so...
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)
Here's a way I've used in the past. Please note that you have to add a reference to the Microsoft Scripting Runtime so you can use the Dictionary object - if you don't want to add that reference you can do this with Collections but they're slower and there's no elegant way to check .Exists (you have to trap the error).
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
Like any similar method, this has its problems - first off, it won't know the "old" value until the value has actually been changed. To fix this you'd need to trap the Open event on the workbook and go through Sheet.UsedRange populating OldVals. Also, it will lose all its data if you reset the VBA project by stopping the debugger or some such.
an idea ...
write these in the ThisWorkbook module
close and open the workbook
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
Place the following in the CODE MODULE of a WORKSHEET to track the last value for every cell in the used range:
Option Explicit
Private r As Range
Private Const d = "||"
Public Function ValueLast(r As Range)
On Error Resume Next
ValueLast = Split(r.ID, d)(1)
End Function
Private Sub Worksheet_Activate()
For Each r In Me.UsedRange: Record r: Next
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
For Each r In Target: Record r: Next
End Sub
Private Sub Record(r)
r.ID = r.Value & d & Split(r.ID, d)(0)
End Sub
And that's it.
This solution uses the obscure and almost never used
Range.ID property, which allows the old values to persist when the workbook is saved and closed.
At any time you can get at the old value of
a cell and it will indeed be different than a new current value:
With Sheet1
MsgBox .[a1].Value
MsgBox .ValueLast(.[a1])
End With
I've expanded a bit on Matt Roy's solution which is awesome by the way. What I did is handle situations when the user selects the whole row/column, so the macro only record the intersection between selection and ".UsedRange", and also handled situations where selection is not a range (for buttons, shapes, pivot tables)
Sub trackChanges_loadOldValues_toCollection(ByVal Target As Range)
'LOADS SELECTION AND VALUES INTO THE COLLECTION collOldValues
If isErrorHandlingOff = False Then: On Error GoTo endWithError
Dim RngI As Range, newTarget As Range, arrValues, arrFormulas, arrAddress
'DON'T RECORD WHEN SELECTING BUTTONS OR SHAPES, ONLY FOR RANGES
If TypeName(Target) <> "Range" Then: Exit Sub
'RESET OLD VALUES COLLECITON
Set collOldValues = Nothing
'ONLY RECORD CELLS IN USED RANGE, TO AVOID ISSUES WHEN SELECTING WHOLE ROW
Set newTarget = Intersect(Target, Target.Parent.UsedRange)
'newTarget.Select
If Not newTarget Is Nothing Then
For Each RngI In newTarget
'ADD TO COLLECTION
'ITEM, KEY
collOldValues.add Array(RngI.value, RngI.formula), RngI.Address
Next RngI
End If
done:
Exit Sub
endWithError:
DisplayError Err, "trackChanges_loadOldValues_toCollection", Erl
End Sub
try this, it will not work for the first selection, then it will work nice :)
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
On Error GoTo 10
If Target.Count > 1 Then GoTo 10
Target.Value = lastcel(Target.Value)
10
End Sub
Function lastcel(lC_vAl As String) As String
Static vlu
lastcel = vlu
vlu = lC_vAl
End Function
I had a need to capture and compare old values to the new values entered into a complex scheduling spreadsheet. I needed a general solution which worked even when the user changed many rows at the same time. The solution implemented a CLASS and a COLLECTION of that class.
The class: 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
There are three sheets in which i track cells. Each sheet gets its own collection as a global variable in the module named ProjectPlan as follows:
Public prepColl As Collection
Public preColl As Collection
Public postColl As Collection
Public migrColl As Collection
The InitDictionaries SUB is called out of worksheet.open to establish the collections.
Sub InitDictionaries()
Set prepColl = New Collection
Set preColl = New Collection
Set postColl = New Collection
Set migrColl = New Collection
End Sub
There are three modules used to manage each collection of oldValue objects they are Add, Exists, and 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
The heavy lifting is done in the Worksheet_SelectionChange callback. One of the four is shown below. The only difference is the collection used in the ADD and EXIST calls.
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
THe VALUE call is called out of code executed from the Worksheet_Change Callback for example. I need to assign the correct collection based on the sheet name:
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
and then i am free to compute compare the some current value to the original value.
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
Let's first see how to detect and save the value of a single cell of interest. Suppose Worksheets(1).Range("B1") is your cell of interest. In a normal module, use this:
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
Then in the module of 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
This will capture the change of the Worksheets(1).Range("B1"), whether the change is due to the user actively selecting that cell on the Worksheet and changing the value, or due to other VBA codes that change the value of Worksheets(1).Range("B1").
Since we have declared the variable StorageArray as public, you can reference its latest value in other modules in the same VBA project.
To expand our scope to the detection and saving the values of multiple cells of interest, you need to:
Declare the StorageArray as a two-dimensional array, with the number of rows equal to the number of cells you are monitoring.
Modify the Sub SaveToStorageArray procedure to a more general Sub SaveToStorageArray(TargetSingleCell as Range) and change the
relevant codes.
Modify the Private Sub Worksheet_Change procedure to accommodate the monitoring of those multiple cells.
Appendix:
For more information on the lifetime of variables, please refer to: https://msdn.microsoft.com/en-us/library/office/gg278427.aspx
I needed this feature and I did not like all the solutions above after trying most as they are either
Slow
Have complex implications like using application.undo.
Do not capture if they were not selected
Do not captures values if they were not changed before
Too complex
Well I thought very hard about it and I completed a solution for a full UNDO,REDO history.
To capture the old value it is actually very easy and very fast.
My solution is to capture all values once the user open the sheet is open into a variable and it gets updated after each change. this variable will be used to check the old value of the cell. In the solutions above all of them used for loop. Actually there is way easier method.
To capture all the values I used this simple command
SheetStore = sh.UsedRange.Formula
Yeah, just that, Actually excel will return an array if the range is a multiple cells so we do not need to use FOR EACH command and it is very fast
The following sub is the full code which should be called in Workbook_SheetActivate. Another sub should be created to capture the changes. Like, I have a sub called "catchChanges" that runs on Workbook_SheetChange. It will capture the changes then save them on another a change history sheet. then runs UpdateCache to update the cache with the new values
' should be added at the top of the module
Private SheetStore() As Variant
Private SheetStoreName As String ' I use this variable to make sure that the changes I captures are in the same active sheet to prevent overwrite
Sub UpdateCache(sh As Object)
If sh.Name = ActiveSheet.Name Then ' update values only if the changed values are in the activesheet
SheetStoreName = sh.Name
ReDim SheetStore(1 To sh.UsedRange.Rows.count, 1 To sh.UsedRange.Columns.count) ' update the dimension of the array to match used range
SheetStore = sh.UsedRange.Formula
End If
End Sub
now to get the old value it is very easy as the array have the same address of cells
examples if we want cell D12 we can use the following
SheetStore(row_number,column_number)
'example
return = SheetStore(12,4)
' or the following showing how I used it.
set cell = activecell ' the cell that we want to find the old value for
newValue = cell.value ' you can ignore this line, it is just a demonstration
oldValue = SheetStore(cell.Row, cell.Column)
these are snippet explaining the method, I hope everyone like it
In response to Matt Roy answer, I found this option a great response, although I couldn't post as such with my current rating. :(
However, while taking the opportunity to post my thoughts on his response, I thought I would take the opportunity to include a small modification. Just compare code to see.
So thanks to Matt Roy for bringing this code to our attention, and Chris.R for posting original code.
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
I have the same problem like you and luckily I have read the solution from this link:
http://access-excel.tips/value-before-worksheet-change/
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
Note: you must place oldValue variable as a global variable so all subclasses can use it.
Private Sub Worksheet_Change(ByVal Target As Range)
vNEW = Target.Value
aNEW = Target.Address
Application.EnableEvents = False
Application.Undo
vOLD = Target.Value
Target.Value = vNEW
Application.EnableEvents = True
End Sub
Using Static will solve your problem (with some other stuff to initialize old_value properly:
Private Sub Worksheet_Change(ByVal Target As Range)
Static old_value As String
Dim inited as Boolean 'Used to detect first call and fill old_value
Dim new_value As String
If Not Intersect(cell, Range("cell_of_interest")) Is Nothing Then
new_value = Range("cell_of_interest").Value
If Not inited Then
inited = True
Else
Call DoFoo (old_value, new_value)
End If
old_value = new_value
Next cell
End Sub
In workbook code, force call of Worksheet_change to fill old_value:
Private Sub Private Sub Workbook_Open()
SheetX.Worksheet_Change SheetX.Range("cell_of_interest")
End Sub
Note, however, that ANY solution based in VBA variables (including dictionary and another more sophisticate methods) will fail if you stop (Reset) running code (eg. while creating new macros, debugging some code, ...). To avoid such, consider using alternative storage methods (hidden worksheet, for example).
I have read this old post, and I would like to provide another solution.
The problem with running Application.Undo is that Woksheet_Change runs again. We have the same problem when we restore.
To avoid that, I use a piece of code to avoid the second steps through Worksheet_Change.
Before we begin, we must create a Boolean static variable BlnAlreadyBeenHere, to tell Excel not to run Worksheet_Change again
Here you can see it:
Private Sub Worksheet_Change(ByVal Target As Range)
Static blnAlreadyBeenHere As Boolean
'This piece avoid to execute Worksheet_Change again
If blnAlreadyBeenHere Then
blnAlreadyBeenHere = False
Exit Sub
End If
'Now, we will store the old and new value
Dim vOldValue As Variant
Dim vNewValue As Variant
'To store new value
vNewValue = Target.Value
'Undo to retrieve old value
'To avoid new Worksheet_Change execution
blnAlreadyBeenHere = True
Application.Undo
'To store old value
vOldValue = Target.Value
'To rewrite new value
'To avoid new Worksheet_Change execution agein
blnAlreadyBeenHere = True
Target.Value = vNewValue
'Done! I've two vaules stored
Debug.Print vOldValue, vNewValue
End Sub
The advantage of this method is that it is not necessary to run Worksheet_SelectionChange.
If we want the routine to work from another module, we just have to take the declaration of the variable blnAlreadyBeenHere out of the routine, and declare it with Dim.
Same operation with vOldValue and vNewValue, in the header of a module
Dim blnAlreadyBeenHere As Boolean
Dim vOldValue As Variant
Dim vNewValue As Variant
Just a thought, but Have you tried using application.undo
This will set the values back again. You can then simply read the original value. It should not be too difficult to store the new values first, so you change them back again if you like.