Conditional highlighting: How to optimise? - excel

I have code which achieves exactly what I want. The basis of my code comes from the generous help of Tim Williams in a previous question. Building on his help, I have added slightly to the functionality (larger font size, and returning formatting to original if nothing in the column is selected), and extended the code across several columns, as shown in the code.
The problem is my spreadsheet is now intolerably slow. Is there a way to speed things up?
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim r As Range, c As Range
'Set target for all columns that have this functionality
Set r = Intersect(Me.Range("N:Q"), Target)
'The functionality is repeated for several columns and is identical each time (except for N which maps to two columns)
'Column N maps to columns H & I
If Not Application.Intersect(Target, Range("N:N")) Is Nothing Then
If r Is Nothing Or Target.Cells.CountLarge > 960 Then Exit Sub
Application.ScreenUpdating = False
HighlightIt Application.Intersect(Me.Range("H:I"), Me.UsedRange), False
For Each c In r.Cells
HighlightIt Me.Cells(c.Row, "H").Resize(1, 2)
Next c
Else
With Application.Intersect(Me.Range("H:I"), Me.UsedRange)
.Font.Bold = False
.Font.Color = vbBlack
.Font.Size = 14
End With
End If
'Column O maps to columns J
If Not Application.Intersect(Target, Range("O:O")) Is Nothing Then
If r Is Nothing Or Target.Cells.CountLarge > 960 Then Exit Sub
Application.ScreenUpdating = False
HighlightIt Application.Intersect(Me.Range("J:J"), Me.UsedRange), False
For Each c In r.Cells
HighlightIt Me.Cells(c.Row, "J")
Next c
Else
With Application.Intersect(Me.Range("J:J"), Me.UsedRange)
.Font.Bold = False
.Font.Color = vbBlack
.Font.Size = 14
End With
End If
'Column P maps to columns K
If Not Application.Intersect(Target, Range("P:P")) Is Nothing Then
If r Is Nothing Or Target.Cells.CountLarge > 960 Then Exit Sub
Application.ScreenUpdating = False
HighlightIt Application.Intersect(Me.Range("K:K"), Me.UsedRange), False
For Each c In r.Cells
HighlightIt Me.Cells(c.Row, "K")
Next c
Else
With Application.Intersect(Me.Range("K:K"), Me.UsedRange)
.Font.Bold = False
.Font.Color = vbBlack
.Font.Size = 14
End With
End If
'Column Q maps to columns L
If Not Application.Intersect(Target, Range("Q:Q")) Is Nothing Then
If r Is Nothing Or Target.Cells.CountLarge > 960 Then Exit Sub
Application.ScreenUpdating = False
HighlightIt Application.Intersect(Me.Range("L:L"), Me.UsedRange), False
For Each c In r.Cells
HighlightIt Me.Cells(c.Row, "L")
Next c
Else
With Application.Intersect(Me.Range("L:L"), Me.UsedRange)
.Font.Bold = False
.Font.Color = vbBlack
.Font.Size = 14
End With
End If
End Sub
'utility sub for highlighting/unhighlighting
Sub HighlightIt(rng As Range, Optional hilite As Boolean = True)
With rng
.Font.Color = IIf(hilite, vbWhite, vbBlack)
.Font.Bold = hilite
.Font.Size = IIf(hilite, 20, 14)
End With
End Sub

Darren Bartrup is right. Code Review is an excellent site to obtain help on making your code more efficient.
I'm providing an answer as I'm not sure you've quite got the spirit of Tim Williams' answer. As well as not needing to iterate the cells, you should also be able to do without identical code for each tested column. You can do this by creating some form of selected column to highlight column map. Below is skeleton code to get you started.
The code you've supplied shouldn't be as slow as you are describing, so I wonder if you're handling other events (or there's more code in your _Select event). If there is, then make sure you include it in your question on Code Review or here.
Option Explicit
Private mColumnMap As Collection
Private mOldRange As Range
Private mOldCellColour As Long
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim mappedRange As Range
Dim mappedCells As Range
'Define the column map.
If mColumnMap Is Nothing Then
Set mColumnMap = New Collection
mColumnMap.Add Me.Range("H:I"), "14" 'N
mColumnMap.Add Me.Range("J:J"), "15" 'O
mColumnMap.Add Me.Range("K:K"), "16" 'P
mColumnMap.Add Me.Range("L:L"), "17" 'Q
End If
'If there is a highlighted range, change it back.
If Not mOldRange Is Nothing Then
With mOldRange
.Interior.Color = mOldCellColour
.Font.Bold = False
End With
Set mOldRange = Nothing
End If
'Ignore any selections that are more than one column.
If Target.Columns.Count <> 1 Then Exit Sub
'Ignore any selections outside of a specified range.
'Note: I've just used the 'UsedRange'.
If Intersect(Target, Me.UsedRange) Is Nothing Then Exit Sub
'Acquire the appropriate column map.
On Error Resume Next
Set mappedRange = mColumnMap(CStr(Target.Column))
On Error GoTo 0
'Exit if not a target column.
If mappedRange Is Nothing Then Exit Sub
'Define the cells to be changed.
Set mappedCells = Intersect(mappedRange, Target.EntireRow)
'Store the original values.
Set mOldRange = mappedCells
mOldCellColour = mappedCells(1).Interior.Color
'Change the values.
Application.ScreenUpdating = False
With mappedCells
.Interior.Color = vbWhite
.Font.Bold = True
End With
Application.ScreenUpdating = True
End Sub

Related

Excel VBA dual Worksheet_change events not working

Having trouble executing both Worksheet_Change events correctly. Image below show my results, when modifying column B, column M does nothing. When modifying column L, column N updates as expected but only on row 2. Every other subsequent change to B or M results in N:2 updating to the current time again.
My desired outcome is that when Col B is updated I record a time stamp in Col M and the same when Col L updates that I get a time stamp in Col N.
Example of Excel Error
My current code is here:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
Dim rng As Range
Dim rng2 As Range
If Not Intersect(Target, Columns("B"), Target.Parent.UsedRange) Is Nothing Then
On Error GoTo Safe_Exit
For Each rng In Intersect(Target, Columns("B"), Target.Parent.UsedRange)
If CBool(Len(rng.Value2)) And Not CBool(Len(rng.Offset(0, 11).Value2)) Then
rng.Offset(0, 11) = Now
ElseIf Not CBool(Len(rng.Value2)) And CBool(Len(rng.Offset(0, 11).Value2)) Then
rng.Offset(0, 11) = vbNullString
End If
Next rng
Application.EnableEvents = True
End If
ElseIf Not Intersect(Target, Columns("L"), Target.Parent.UsedRange) Is Nothing Then
On Error GoTo Safe_Exit
For Each rng2 In Intersect(Target, Columns("L"), Target.Parent.UsedRange)
If CBool(Len(rng2.Value2)) And Not CBool(Len(rng2.Offset(0, 2).Value2)) Then
rng2.Offset(0, 2) = Now
ElseIf Not CBool(Len(rng2.Value2)) And CBool(Len(rng2.Offset(0, 2).Value2)) Then
rng2.Offset(0, 2) = vbNullString
End If
Next rng2
Application.EnableEvents = True
End If
Safe_Exit:
End Sub
Mock-up, untested, change of code to simplify as you're doing the same actions in two spots:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
Dim columnLetter as String
Select Case Target.Column
Case 2 'B
columnLetter = "M"
Case 12 'L
columnLetter = "N"
Case Else
Goto Safe_Exit
End Select
Dim loopRng as Range
For Each loopRng In Range(Cells(Target.Row, Target.Column),Cells(Target.End(xlDown).Row,Target.Column)
If IsEmpty(loopRng) = True And IsEmpty(Cells(loopRng.Row,columnLetter)) = False Then
Cells(loopRng.Row,columnLetter) = Now
ElseIf IsEmpty(loopRng) = False And IsEmpty(Cells(loopRng.Row,columnLetter)) = True Then
Cells(loopRng.Row,columnLetter) = vbNullString
End If
Next loopRng
'Columns(columnLetter).NumberFormat = "yyyy/mm/dd"
Application.EnableEvents = True
Safe_Exit:
Application.EnableEvents = True
End Sub
Note that the IsEmpty() = True is important... when using an If case, you need to specify for each condition, otherwise the implicit detection will fail.
Edit1: Removed Intersect from loop, whereas the range i've listed will need corrected... it at least references a specific range, now.
Edit2: Removing .Offset and working with specific column references in cells().
I tried this version of my original code and it started to work for some reason.
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Columns("B"), Target.Parent.UsedRange) Is Nothing Then
On Error GoTo Safe_Exit
Application.EnableEvents = False
Dim rng As Range
For Each rng In Intersect(Target, Columns("B"), Target.Parent.UsedRange)
If CBool(Len(rng.Value2)) And Not CBool(Len(rng.Offset(0, 11).Value2)) Then
rng.Offset(0, 11) = Now
ElseIf Not CBool(Len(rng.Value2)) And CBool(Len(rng.Offset(0, 11).Value2)) Then
rng.Offset(0, 11) = vbNullString
End If
Next rng
End If
If Not Intersect(Target, Columns("L"), Target.Parent.UsedRange) Is Nothing Then
On Error GoTo Safe_Exit
Application.EnableEvents = False
For Each rng In Intersect(Target, Columns("L"), Target.Parent.UsedRange)
If CBool(Len(rng.Value2)) And Not CBool(Len(rng.Offset(0, 2).Value2)) Then
rng.Offset(0, 2) = Now
ElseIf Not CBool(Len(rng.Value2)) And CBool(Len(rng.Offset(0, 2).Value2)) Then
rng.Offset(0, 2) = vbNullString
End If
Next rng
End If
Safe_Exit:
Application.EnableEvents = True
End Sub

VBA Worksheet_Change Only Working For One Cell

I'd like to preface by saying I am a novice to VBA, so hopefully this is an easy fix.
I am trying to get the following VBA code to work for multiple cells with formulas. The effect is that there is a ghost value in the cell a user can overwrite then see again if they delete their value. I can get one cell to work how I want it to, but the second (and third and fourth etc.) do not work. How can I repeat this same line of code so that the effect repeats itself in multiple cells with different formulas?
Working:
Private Sub Worksheet_Change(ByVal Target As Excel.Range)
With Target
If .Address(False, False) = "F7" Then
If IsEmpty(.Value) Then
Application.EnableEvents = False
.Formula = "=IFERROR(INDEX(DATABASE!$D$2:$AG$3222,MATCH('Pricing Grid'!$B$11,DATABASE!$E$2:$E$3222,0),10),0)"
Application.EnableEvents = True
End If
End If
End With
End Sub
My attempt (Top working, bottom not):
Private Sub Worksheet_Change(ByVal Target As Excel.Range)
With Target
If .Address(False, False) = "F7" Then
If IsEmpty(.Value) Then
Application.EnableEvents = False
.Formula = "=IFERROR(INDEX(DATABASE!$D$2:$AG$3222,MATCH('Pricing Grid'!$B$11,DATABASE!$E$2:$E$3222,0),10),0)"
Application.EnableEvents = True
End If
End If
End With
End Sub
Private Sub Worksheet_Change1(ByVal Target As Excel.Range)
With Target
If .Address(False, False) = "F8" Then
If IsEmpty(.Value) Then
Application.EnableEvents = False
.Formula = "=IFERROR(INDEX(DATABASE!$D$2:$AG$3222,MATCH('Pricing Grid'!$B$11,DATABASE!$E$2:$E$3222,0),9),0)"
Application.EnableEvents = True
End If
End If
End With
End Sub
Try this...
Private Sub Worksheet_Change(ByVal Target As Range)
Dim i&, j&, v, t
v = Target.Value2
If Not IsArray(v) Then t = v: ReDim v(1 To 1, 1 To 1): v(1, 1) = t
Application.EnableEvents = False
For i = 1 To UBound(v)
For j = 1 To UBound(v, 2)
If Len(v(i, j)) = 0 Then
With Target(i, j)
Select Case .Address(0, 0)
Case "A1": .Formula = "=""Excel"""
Case "A2": .Formula = "=""Hero"""
End Select
End With
End If
Next
Next
Application.EnableEvents = True
End Sub
Use your formulas and ranges instead of mine, of course.
Update
The above works well, but this is faster/better...
Private Sub Worksheet_Change(ByVal Target As Range)
Dim i&, v
DoEvents
ReDim v(1 To 3, 1 To 2)
v(1, 1) = "A1": v(1, 2) = "=""This"""
v(2, 1) = "A2": v(2, 2) = "=""Works"""
v(3, 1) = "A2": v(3, 2) = "=""Great!"""
Application.EnableEvents = False
For i = 1 To UBound(v)
With Range(v(i, 1))
If Not Intersect(Target, .Cells) Is Nothing Then
If Len(.Value2) = 0 Then
.Formula = v(i, 2)
End If
End If
End With
Next
Application.EnableEvents = True
End Sub
Both of the above methods work for single-cell deletes AND also for clearing and deleting large ranges, including whole columns and whole rows and the second method is particularly quick in all these scenarios.
You can do something like this:
Private Sub Worksheet_Change(ByVal Target As Excel.Range)
'only handle single cells
If Target.Cells.CountLarge > 1 Then Exit Sub
If IsError(Target.Value) Then Exit Sub '<< edit: added
'only handle empty cells
If Len(Target.Value) > 0 Or Len(Target.Formula) > 0 Then Exit Sub
On Error Goto haveError
Application.EnableEvents = False
Select Case Target.Address(False, False)
Case "F7": Target.Formula = "=IFERROR(INDEX(DATABASE!$D$2:$AG$3222,MATCH('Pricing Grid'!$B$11,DATABASE!$E$2:$E$3222,0),10),0)"
Case "F8": Target.Formula = "=IFERROR(INDEX(DATABASE!$D$2:$AG$3222,MATCH('Pricing Grid'!$B$11,DATABASE!$E$2:$E$3222,0),9),0)"
End Select
haveError:
'ensure events are re-enabled
Application.EnableEvents = True
End Sub

Combining Not Intersect, Target.Parent.Range and Worksheets.Cells

I am aiming to add VBA that hides or shows rows depending on whether a user clicks on a specific cell that needs to loop many times.
I was wondering how to possibly combine Target.Parent.Range with Worksheet.Cells so that I can write a loop for it rather than repeating the code multiple times. The below code works fine but seems pretty inefficient:
'Hide1
If (ActiveSheet.Name = "Dashboard") And Not Intersect(Target, Target.Parent.Range("G38")) Is Nothing Then
If Rows("40:47").EntireRow.Hidden = True Then
Rows("40:47").EntireRow.Hidden = False
Range("G38").Value = "Hide"
ActiveSheet.Range("A1").Select
Else
Rows("40:47").EntireRow.Hidden = True
Range("G38").Value = "Show"
ActiveSheet.Range("A1").Select
End If
End If
'Hide2
If (ActiveSheet.Name = "Dashboard") And Not Intersect(Target, Target.Parent.Range("G48")) Is Nothing Then
If Rows("50:57").EntireRow.Hidden = True Then
Rows("50:57").EntireRow.Hidden = False
Range("G48").Value = "Hide"
ActiveSheet.Range("A1").Select
Else
Rows("50:57").EntireRow.Hidden = True
Range("G48").Value = "Show"
ActiveSheet.Range("A1").Select
End If
End If
This will need to be repeated 10's of times as buttons are located at similar intervals down the sheet, so looping makes the most sense. Any help would be of great help as my attempts to combine the two functions have failed thus far.
Your code could be shortened to this
Private Sub Worksheet_Change(ByVal Target As Range)
Dim buttonRng As Range, hideRng As Range
Application.EnableEvents = False
Set buttonRng = Target
' Add in your ranges into this If statement
If Not Intersect(Target, Me.Range("G38")) Is Nothing Then
Set hideRng = Me.Rows("40:47")
ElseIf Not Intersect(Target, Me.Range("G48")) Is Nothing Then
Set hideRng = Me.Rows("50:57")
Else
Set hideRng = Nothing
End If
If Not hideRng Is Nothing Then
With hideRng
.Hidden = Not .Hidden
End With
buttonRng.Value2 = IIf(buttonRng.Value2 = "Show", "Hide", "Show")
End If
Application.EnableEvents = True
End Sub
You could add an additional sheet in with a list of the button location addresses and the range for them to hide.
You will need to set column B to text
and then use the following code
Private Sub Worksheet_Change(ByVal Target As Range)
Dim buttonRng As Range, hideRng As Range
Application.EnableEvents = False
Set buttonRng = Target
With Sheets("Button Hide Range").Columns(1)
Set hideRng = .Find(Target.Address(False, False))
End With
If Not hideRng Is Nothing Then
With Me.Rows(hideRng.Offset(0, 1).Value2)
.Hidden = Not .Hidden
End With
buttonRng.Value2 = IIf(buttonRng.Value2 = "Show", "Hide", "Show")
End If
Application.EnableEvents = True
End Sub
This sheet can then be hidden or set to xlVeryHidden if desired so it is not viewable by the end user.
Or if all of the rows to be hidden are the same offset away from the buttons you could use
Private Sub Worksheet_Change(ByVal Target As Range)
Dim buttonRng As Range
Dim i As Long
Application.EnableEvents = False
' i = row of first button to row of last button. Assuming each button is 10 rows apart from the previous
For i = 38 To 78 Step 10
If buttonRng Is Nothing Then
Set buttonRng = Me.Range("G" & i)
Else
Set buttonRng = Union(buttonRng, Me.Range("G" & i))
End If
Next i
If Not Intersect(Target, buttonRng) Is Nothing Then
' Assuming rows to be hidden are starts 2 rows away from button and ends 9 rows away
With Me.Rows(Target.Offset(2).Row & ":" & Target.Offset(9).Row)
.Hidden = Not .Hidden
End With
Target.Value2 = IIf(Target.Value = "Show", "Hide", "Show")
End If
Application.EnableEvents = True
End Sub

Excel VBA script for finding specific text

So I'm adding a picture so you can see exactly what I need it to do -Excel sheet picture
So I have this script:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim A As Range, G, L, N As Range, Inte As Range, r As Range
Set A = Range("F:F,K:K,M:M")
Set Inte = Intersect(A, Target)
If Inte Is Nothing Then Exit Sub
Application.EnableEvents = False
For Each r In Inte
If r.Offset(0, 1).Value = "" Then
r.Offset(0, 1).Value = Date
End If
Next r
Application.EnableEvents = True
End Sub
We will talk about only what we see in the picture: so now when I write anything in column F it gives the date in column G, I want that it will give the date only if I write "Ja" (Yes in German) or "Yes"
Simple is that. I tried to find any "if" commands for it but none of mine worked.
Hope you can help me with that
Thanks!
Daniel
Here is a better practice to achieve that also you need to set Application.EnableEvents back to True when you are existing the method for the next time the event will be raised, also its not bad to make some "house keeping" and use error catch if something goes wrong:
Private Sub Worksheet_Change(ByVal Target As Range)
On Error GoTo ende
Application.EnableEvents = False
Set A = Range("F:F,K:K,M:M")
Set Inte = Intersect(A, Target)
If Inte Is Nothing Then
Application.EnableEvents = True
Exit Sub
End If
For Each cel In Target
r = cel.Row
c = cel.Column
If Trim(LCase(Cells(r, c))) = "yes" Or Trim(LCase(Cells(r, c))) = "ja" Then
Cells(r, c + 1) = Format(Date, "dd.MM.yyyy")
Else
' do something else
End If
Next
ende:
Application.EnableEvents = True
End Sub
What about this?
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range
Dim rng As Range
Set rng = Range("F:F,K:K,M:M")
On Error GoTo SkipError
Application.EnableEvents = False
If Not Intersect(Target, rng) Is Nothing Then
For Each cell In Target
If LCase(VBA.Trim(cell.Value)) = "yes" Then
cell.Offset(0, 1) = Date
End If
Next cell
End If
SkipError:
Application.EnableEvents = True
End Sub

Clear the contents of columns B to F if cell A is empty

I have a worksheet with values depending on Cell A. If a row in column A contains a value then cells from Columns B through H will be changed accordingly.
If Cell of Column A is empty I want to reset the cells from columns D through F.
I wrote down the following VBA Code
Private Sub Worksheet_Change(ByVal Target As Range)
Dim n As Integer
For n = 5 To 75
Application.EnableEvents = False
If VarType(Cells(n, 1)) = vbEmpty Then
Cells(n, 4).ClearContents
Cells(n, 5).ClearContents
Cells(n, 6).ClearContents
Application.EnableEvents = True
End If
Next n
End Sub
The "FOR" Loop is annoying, and making the Excel to pause for 1 second or more after any entry to any Cell, can anyone help me correct the above code to do what I need to do without the "FOR" loop.
You are using a Worksheet_Change event and you iterating through 70 rows each time something changes.. this is a bad approach for this kind of problem and that's why there is a delay.
Instead, try
Private Sub Worksheet_Change(ByVal Target As Range)
Dim n As Long
If Target.Column = 1 Then
If IsEmpty(Cells(Target.Row, 1)) Then
Range("B" & Target.Row & ":F" & Target.Row).ClearContents
End If
End If
End Sub
this will only clear the cells if you remove a value from column A => when cell in column A is empty
Try this:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim i As Integer
If Mid(Target.Address(1, 1), 1, 2) = "$A" Then
If Target.Cells(1, 1).Value = "" Then
For i = 4 To 6
Target.Cells(1, i).Value = ""
Next i
End If
End If
End Sub
Give this a try:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rLook As Range, r As Range, Intr As Range
Set rLook = Range("A5:A75")
Set Intr = Intersect(rLook, Target)
If Intr Is Nothing Then Exit Sub
Application.EnableEvents = False
For Each r In Intr
If r.Value = "" Then
rw = r.Row
Range("D" & rw & ":F" & rw).ClearContents
End If
Next r
Application.EnableEvents = True
End Sub
It should have minimal impact on timing.
Use a range object.
The following line of code will print the address of the Range we'll use to clear the contents. The first cells call gets the upper left corner of the range, the second cells call gets the lower right corner of the range.
Private Sub test()
Debug.Print Range(Cells(5, 4), Cells(75, 6)).Address
End Sub
We apply this to your code like this:
Private Sub Worksheet_Change(ByVal Target As Range)
If VarType(Cells(Target.Row, 1)) = vbEmpty Then
Application.EnableEvents = False
Range(Cells(Target.Row, 4), Cells(Target.Row, 6)).ClearContents
Application.EnableEvents = True
End If
End Sub
One final sidenote: You should use an error handler to make sure events are always enabled when the sub exits, even if an error occurs.
Private Sub Worksheet_Change(ByVal Target As Range)
On Error GoTo ErrHandler
If VarType(Cells(Target.Row, 1)) = vbEmpty Then
Application.EnableEvents = False
Range(Cells(Target.Row, 4), Cells(Target.Row, 6)).ClearContents
End If
ExitSub:
Application.EnableEvents = True
Exit Sub
ErrHandler:
MsgBox "Oh Noes!", vbCritical
Resume ExitSub
End Sub
You should disable events and cater for multiple cells when using the Change event.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng1 As Range
Dim rng2 As Range
Set rng1 = Intersect(Columns("A"), Target)
If rng1 Is Nothing Then Exit Sub
With Application
.EnableEvents = False
.ScreenUpdating = False
End With
For Each rng2 In rng1.Cells
If IsEmpty(rng2.Value) Then rng2.Offset(0, 1).Resize(1, 5).ClearContents
Next
With Application
.EnableEvents = True
.ScreenUpdating = True
End With
End Sub
For those that need to have data entered in one cell cleared (in a column) when there's a change in another column use this, which is a modification of Gary's Student.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rLook As Range, r As Range, Intr As Range
Set rLook = Range("D:D")
Set Intr = Intersect(rLook, Target)
If Intr Is Nothing Then Exit Sub
Application.EnableEvents = False
For Each r In Intr
If r.Value = "" Then
rw = r.Row
Range("L:L").ClearContents
End If
Next r
Application.EnableEvents = True
End Sub

Resources