Excel crashing on Worksheet_Change, but NOT on Worksheet_SelectionChange - vba

I have a spreadsheet that is a data-entry tool for pulling equipment tags and line numbers from engineering drawings – it’s set up with a table that takes either 3-segment tags (columns A-C), 5 segment line numbers (columns A-E), or a list of complete tags (column F), with column G either concatenating the tag segments or pulling across the complete tag. I had this set up using a formula, but I’d rather avoid using complicated formulas in anything that anyone else is going to use and so I took a stab at converting the formula to VBA and putting in a Worksheet_Change procedure.
The code works fine... until you make a change to a cell on the last row of the table and then hit enter or use the down arrow key, at which point Excel crashes. Moving sideways or upwards is fine, and so is moving sideways off the changed cell before hitting enter. I tried converting the table to a regular range, and it still crashes at the last row of the data. I tried turning Application.EnableEvents to False, and that stops the crashing, but then the updating no longer triggers properly.
If the procedure is changed to Worksheet_SelectionChange, it doesn’t crash.
Just to make it more interesting, in both the Worksheet_Change and Worksheet_SelectionChange procedures, using the up/down arrow keys or the enter key fails to trigger a change, but in the Worksheet_SelectionChange procedure arrowing back down/up to the row off which I just moved triggers the update.
I’m sure there are a million ways to fix this, but I have no idea how to do it, and I haven’t had any luck finding an answer.
What I want is for the code to update column G whenever the active cell changes – regardless of whether I use the enter key, tab key, arrow keys, or the $!## mouse to change my cell selection.
I'm working on a Windows 10 machine, using Excel 2016. When I get to work tomorrow I'll see how it goes on Excel 2013.
Spreadsheet screencap, for reference: https://drive.google.com/file/d/0B_wa8YmM1J2ddjlkOWxERE5TM1k/view?usp=sharing
Any assistance would be hugely appreciated - especially if it comes with a thorough explanation about what is going on here.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim strDelim As String
Dim strConcatTag As String
Dim intActiveRow As Integer
Dim rngTagSegment As Range
Dim rngSingleTag As Range
Dim rng3SegmentTag As Range
Dim rng5SegmentTag As Range
Dim rngTagEntry As Range
Dim rngConcatTag As Range
Dim rngCheck As Range
strDelim = "-"
intActiveRow = ActiveCell.Row
Set rngSingleTag = Cells(intActiveRow, 6)
Set rng3SegmentTag = Range(Cells(intActiveRow, 1), Cells(intActiveRow, 3))
Set rng5SegmentTag = Range(Cells(intActiveRow, 1), Cells(intActiveRow, 5))
Set rngTagEntry = Range(Cells(intActiveRow, 1), Cells(intActiveRow, 6))
Set rngConcatTag = Cells(intActiveRow, 7)
If intActiveRow = 1 Then
Exit Sub
Else
Select Case True
Case WorksheetFunction.CountA(rngTagEntry) = 0
rngConcatTag = ""
Case WorksheetFunction.CountA(rng5SegmentTag) > 0 And WorksheetFunction.CountA(rngSingleTag) > 0
rngConcatTag = "Enter either a complete tag or the individual sections, not both"
Case WorksheetFunction.CountA(rng5SegmentTag) = 0 And WorksheetFunction.CountA(rngSingleTag) <> 0
rngConcatTag = UCase(Trim(rngSingleTag))
Case WorksheetFunction.CountA(rng3SegmentTag) = 3 And WorksheetFunction.CountA(rng5SegmentTag) = 3
For Each rngTagSegment In rng5SegmentTag
strConcatTag = IIf(rngTagSegment = "", Trim(strConcatTag) & "", IIf(strConcatTag = "", _
Trim(rngTagSegment.Text), Trim(strConcatTag) & strDelim & Trim(rngTagSegment.Text)))
Next
rngConcatTag = UCase(Trim(strConcatTag))
Case WorksheetFunction.CountA(rng3SegmentTag) = 3 And WorksheetFunction.CountA(rng5SegmentTag) = 5
For Each rngTagSegment In rng5SegmentTag
strConcatTag = IIf(rngTagSegment = "", Trim(strConcatTag) & "", IIf(strConcatTag = "", _
Trim(rngTagSegment.Text), Trim(strConcatTag) & strDelim & Trim(rngTagSegment.Text)))
Next
rngConcatTag = UCase(strConcatTag)
Case Else
rngConcatTag = "Incomplete Tag"
End Select
End If
End Sub

Something like this should work:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rw As Range, r As Range, dataRange As Range
Dim rngSingleTag As Range
Dim rng3SegmentTag As Range
Dim rng5SegmentTag As Range
Dim rngTagEntry As Range
Dim rngConcatTag As Range
'data entry area only (adjust to suit)...
Set dataRange = Application.Intersect(Target, Me.Range("A2:F10000"))
If dataRange Is Nothing Then Exit Sub 'nothing to do...
'process each changed row
For Each r In dataRange.Rows
Set rw = r.EntireRow
Set rngSingleTag = rw.Cells(6)
Set rng3SegmentTag = rw.Cells(1).Resize(1, 3)
Set rng5SegmentTag = rw.Cells(1).Resize(1, 5)
Set rngTagEntry = rw.Cells(1).Resize(1, 6)
Set rngConcatTag = rw.Cells(7)
Select Case True
Case filled(rngTagEntry) = 0
rngConcatTag = ""
Case filled(rng5SegmentTag) > 0 And filled(rngSingleTag) = 1
rngConcatTag = "Enter either a complete tag or the individual sections, not both"
Case filled(rng5SegmentTag) = 0 And filled(rngSingleTag) = 1
rngConcatTag = UCase(Trim(rngSingleTag))
Case filled(rng3SegmentTag) = 3 And filled(rng5SegmentTag) = 3
rngConcatTag = Tag(rng3SegmentTag)
Case filled(rng5SegmentTag) = 5
rngConcatTag = Tag(rng5SegmentTag)
Case Else
rngConcatTag = "Incomplete Tag"
End Select
Next r
End Sub
Function filled(rng)
filled = Application.CountA(rng)
End Function
Function Tag(rng) As String
Const DELIM As String = "-"
Dim c As Range, rv As String
For Each c In rng.Cells
rv = rv & IIf(Len(rv) > 0, DELIM, "") & Trim(c.Text)
Next c
Tag = rv
End Function

Related

Using VBA to fill a column based on the value in a separate sheet

I need to run a macro between two worksheets where say column C in "Sheet 1" has a "Y", I need column AP in "Sheet 2" to return something along the lines of "covered" or "Y". Just something to indicate that a Y was there in Sheet 1. I am mainly running issues in actually connecting the two worksheets. This code below works fine if I am running it on columns within the same work sheet.
Code:
Private Sub Set_Border_Pattern(Requirements_Selector_Str As String)
Dim strTemp As String
Dim strRange As String
Dim strCellVal As String
If Len(Requirements_Selector_Str) > 2 Then
strTemp = Mid(Requirements_Selector_Str, 4, 1)
Else
strTemp = Requirements_Selector_Str
End If
With Worksheets("test")
For i = 2 To REQUIREMENT_ROW_COUNT
strRange = strTemp & i
strCellVal = .Range(strRange).Value
If strCellVal = "Y" Then
Worksheets("NFR_List").Range(AP & i).Value = "Y"
End If
Next i
End With
The code below does what you describe in a very simple way. I believe that if you understand it you will be able to modify it for your situation. If not, please feel free to ask questions.
Sub test()
Dim sh1 As Worksheet, sh2 As Worksheet
Dim r1 As Range, r2 As Range, i As Long
Set sh1 = Worksheets("test")
Set sh2 = Worksheets("NFR_List")
Set r1 = sh1.Range("C1")
Set r2 = sh2.Range("AP1")
i = 0
While r1.Offset(i, 0) <> ""
If r1.Offset(i, 0) = "Y" Then r2.Offset(i, 0) = "Y"
i = i + 1
Wend
End Sub
I've assumed that Column C has no blank cells until the data is finished, but if this is not true, the code can be easily modified according to your needs.
Another approach would be to just use a formula for this (instead of VBA), such as =IF(test!C1="Y", "Y","") in AP1 (if the "NFR_List" sheet) and then drag the formula down. Or you could also put the formula in using VBA using code like, r2.offset(i,0).formula = ... . There are many ways.

Copying Range After Last Row

I am trying to copy the entire range from "Leads" sheet after the last row of "TempDataNew"
If Sheets("Leads").Range("A1") <> "" Then
Set rngSource = Sheets("Leads").Range("A1").CurrentRegion
lastrowdyn = rngSource.Rows.Count
If lastrowdyn > 0 Then
Sheets("Leads").Range("A:A").Copy
Sheets("TempDataNew").Range ("A" & x)
Set rngSource = Sheets("TempDataNew").Range("A1").CurrentRegion
x = lastrowdyn + 1
End If
End If
I am getting the "Application Defined or Object Defined Error" when the code is trying to paste. Any thoughts?
From what I can tell, you want to copy to the bottom of the used range of TempDataNew, not Leads. So change this
Set rngSource = Sheets("Leads").Range("A1").CurrentRegion
to this
Set rngSourceTempDatNew = Sheets("TempDatNew").Range("A1").CurrentRegion
lastrowdynTempDatNew = rngSourceTempDatNew.Rows.Count
Also, I'm guessing you really don't want or need to copy the entire column A of Leads. So this needs to be changed.
Sheets("Leads").Range("A:A").Copy
or that is exactly what you'll end up doing. May I recommend also finding the last row of the used range in column A of Leads as you want to do for TempDatNew. Maybe something like
Set rngSourceLeads = Sheets("Leads").Range("A1").CurrentRegion
Now just copying from the range in Leads to the right spot in TempDatNew can be done with something like this
rngSourceLeads.Copy _
destination:=Worksheets("TempDatNew").Range("A" & lastrowdynTempDatNew + 1)
I can't exactly tell why you are storing the last row plus one to x, but my current thinking is that it isn't needed for your purposes. Every time this code is ran, it will update the last row of TempDatNew for you and you just simply paste in the row after that.
So, all together you have something more succinct and accurate with
If Sheets("Leads").Range("A1") <> "" Then
Set rngSourceTempDatNew = Sheets("TempDatNew").Range("A1").CurrentRegion
lastrowdynTempDatNew = rngSourceTempDatNew.Rows.Count
Set rngSourceLeads = Sheets("Leads").Range("A1").CurrentRegion
rngSourceLeads.Copy destination:=Worksheets("TempDatNew").Range("A" & lastrowdynTempDatNew + 1)
End If
EDIT
If you only want to copy column A in "Leads" change this
Set rngSourceLeads = Sheets("Leads").Range("A1").CurrentRegion
to this
lastrowdynLeads = Sheets("Leads").Cells(65000, 1).End(xlup).Row
Set rngSourceLeads = Sheets("Leads").Range("A1:A" & lastrowdynLeads)
This assumes you have no data below row 65000 in column A in sheet "Leads".
You have not specified what x is before you begin running the code. I have added a line to give x a value. The way you had it, excel was evaluating x to be 0, thus resulting in a range called Range("A0")... that doesn't exist.
If Sheets("Leads").Range("A1") <> "" Then
Set rngSource = Sheets("Leads").Range("A1").CurrentRegion
lastrowdyn = rngSource.Rows.Count
x = 1 'Whatever integer it is supposed to start at
If lastrowdyn > 0 Then
Sheets("Leads").Range("A:A").Copy
Sheets("TempDataNew").Range ("A" & x)
Set rngSource = Sheets("TempDataNew").Range("A1").CurrentRegion
x = lastrowdyn + 1
End If
End If
I think I found the problem you were originally experiencing in your code... Your first IF statement says If Sheets("Leads").Range("A1") <> "" Then. Your referencing a range object, and comparing it to a value. Your error should go away if you use Sheets("Leads").Range("A1").Value <> "".
I'm not sure why you want to use .CurrentRegion if you're only working with a single column (I'm also not the most versed in VBA logic) but if you're just trying to find the last row, you could use something like this:
Dim Leads, TempDataNew as Worksheet
Set Leads = ThisWorkbook.Sheets("Leads")
Set TempDataNew = ThisWorkbook.Sheets("TempDataNew")
lastrowdyn = Leads.Cells(Leads.Rows.Count, "A").End(xlUp).Row
If Leads.Range("A1").Value <> "" And lastrowdyn > 0 Then
Leads.Range("A:A").Copy Destination:=TempDataNew.Range("A" & x)
x = lastrowdyn + 1
End If
Why not just this?
Sub CopyRange()
Dim wsLeads As Worksheet, wsTemp As Worksheet
Dim lLastRowNew As Long, lRows As Long, iColumns As Integer
Set wsLeads = Worksheets("Leads")
Set wsTemp = Worksheets("TempDataNew")
lLastRowNew = wsTemp.UsedRange.Rows(wsTemp.UsedRange.Rows.Count).Row
lRows = wsLeads.UsedRange.Rows.Count
iColumns = wsLeads.UsedRange.Columns.Count
If wsLeads.Range("A1").Value <> "" And lRows > 0 Then
wsTemp.Cells(lLastRowNew + 1, 1).Resize(lRows, iColumns).Value = wsLeads.UsedRange.Value
End If
End Sub

VBA - Changing row shading when a column value changes even with filtered

I'm trying to write a macro to change the colors of rows when the values in column B change. Column A will be my controlling column using 1's and 0's, i.e. column A will stay a 1 as long as column B stays the same; whenever B changes, A will flip to a 0, and so on.
I can get it to color the rows correctly when the values in column B change, but the problem arises when I filter the data. For example: let's say I have B2-B4 set to "test1", B5-B7 set to "test2", and B8-B10 set to "test3", then I filter column B to not include "test2". Originally, the rows would be colored differently where the column values changed, but rows B2-B4 and B8-B10 are set to the same color and now they're touching since the "test2" rows are hidden.
Here's the code I used to color the rows, but it doesn't work for filtering:
Sub ColorRows()
Dim This As Long
Dim Previous As Long
Dim LastRow As Long
Dim Color As Integer
Dim R As Long
LastRow = ActiveSheet.Range("A" & Rows.Count).End(xlUp).Row
RwColor = Array(15,0)
Color = 0
For R = 2 To LastRow
This = Cells(R, 1).Value
Previous = Cells(R - 1, 1).Value
If This <> Previous Then Color = 1 - Color
Range("A" & R & ":M" & R).Select
Selection.Interior.ColorIndex = RwColor(Color)
Next R
End Sub
How can I fix it so that even after filtering the rows are colored correctly when there is a change in column values?
Here's a way to do this:
1.) Insert the code below as a UDF in a code module.
2.) Then put the formula in A, as A2: =analyseVisible(B2).
This will compare B-cells to the next visible cell above and result in a 'rank'-counter in A.
Now that the counter in A in contiunous (even if rows are hidden), you can use MOD 2 to color it with conditional formatting:
3.) Add a conditional format (from A2 for the whole table): =MOD($A2,2)=1 and set the fill color.
If you use the filter now or change values in B, the rows are re-colored in realtime.
Public Function analyseVisible(r As Range) As Integer
Dim i As Long
If Application.Caller.Row <= 2 Or _
r.Row <> Application.Caller.Row Then
analyseVisible = 1
Exit Function
End If
i = r.Row - 1
While r.Worksheet.Rows(i).Hidden And i > 1
i = i - 1
Wend
If i = 1 Then
analyseVisible = 1
Else
analyseVisible = r.Worksheet.Cells(i, Application.Caller.Column).Value
If r.Worksheet.Cells(i, r.Column).Value <> _
r.Value Then analyseVisible = analyseVisible + 1
End If
End Function
The code below handles the issue by checking only the used & visible rows. It works pretty well, but I was unable to figure out how to fire it when the filter changes. It also does it's comparisons directly on the values that are changing.
Private Sub colorRows()
Dim this As Variant
Dim previous As Variant
Dim currentColor As Long
Dim rng As Range 'visible range
Dim c As Range ' cell
' pick a color to start with
currentColor = vbYellow
' rng = used and visible cells
Set rng = ActiveSheet.UsedRange.SpecialCells(xlCellTypeVisible)
For Each c In rng ' For each cell that is visible and used
If Not c.Row = 1 Then ' skip header row
this = c.Value
'some simple test logic to switch colors
If this <> previous Then
If currentColor = vbBlue Then
currentColor = vbYellow
ElseIf currentColor = vbYellow Then
currentColor = vbBlue
End If
End If
'set interior color
c.Interior.color = currentColor
previous = this
End If
Next c
End Sub
Then, in the module of the worksheet that you want to colorize, call the sub from the Worksheet_Activate() event. (In reality, you probably want a different event. I mostly work with Access, so I don't really know what's available to you. I'm just trying to point you in the right direction to what I'm sure is your next question if you stick with the method you started with.)
Private Sub Worksheet_Activate()
colorRows
End Sub

EXCEL 2010 find max value cell and change its color

I am working on VBA EXCEL 2010.
I need to find the max value in a column and highlight its cell with a color.
Sub findMax_1()
Dim c As Range
Dim max As Double
Dim maxCell As String
max = 0
For Each c In Selection
If c.Value > max Then
max = c.Value
maxCell = c.Address
End If
Next c
ActiveSheet.Range("A10") = max
ActiveSheet.Range(maxCell).Color = vbBlue
End Sub
It does not work. Runtime error 438.
Any help would be appreciated.
As simco mentioned in the comments you would need to change the following line of code:
ActiveSheet.Range(maxCell).Color = vbBlue
To
ActiveSheet.Range(maxCell).Interior.Color = vbBlue
The problem with your current code is that if you have nothing selected you would end up with a 1004 error. One way of overcoming this is as simco mentioned to check if you have any cells selected. The method below is the method I preferr. Lets say you have your data in Column A:
Use the code below:
Sub findMax_1()
Dim c As Range
Dim flag As Boolean
Dim i As Integer
Dim max As Double
Dim maxCell As String
flag = True
i = 1
max = 0
While flag = True
If Cells(i, 1) <> "" Then
If Cells(i, 1) > max Then
max = Cells(i, 1)
maxCell = Range(Cells(i, 1), Cells(i, 1)).Address
End If
i = i + 1
Else
flag = False
End If
Wend
ActiveSheet.Range("A10") = max
ActiveSheet.Range(maxCell).Interior.Color = vbBlue
End Sub
Result:
Also you could look at this article on my blog for more information Excel VBA Formatting Cells and Range
Also as simco mentioned you could use conditional formatting, Select the column with the data:
From the Home Ribbon Select
Conditional Formatting>>Top/Bottom Rules >> Top 10 Items ...>>
Select "1" From the left text box and choose your color from the drop down list on the right:

How to compare two columns in different sheets

I have one excel file with multiple sheets.
I need to compare two sheets (1) TotalList and (2) cList with more than 25 columns, in these two sheets columns are same.
On cList the starting row is 3
On TotalList the starting row is 5
Now, I have to compare the E & F columns from cList, with TotalList E & F columns, if it is not found then add the entire row at the end of TotalList sheet and highlight with Yellow.
Public Function compare()
Dim LoopRang As Range
Dim FoundRang As Range
Dim ColNam
Dim TotRows As Long
LeaData = "Shhet2"
ConsolData = "Sheet1"
TotRows = Worksheets(LeaData).Range("D65536").End(xlUp).Row
TotRows1 = Worksheets(ConsolData).Range("D65536").End(xlUp).Row
'TotRows = ThisWorkbook.Sheets(LeaData).UsedRange.Rows.Count
ColNam = "$F$3:$F" & TotRows
ColNam1 = "$F$5:$F" & TotRows1
For Each LoopRang In Sheets(LeaData).Range(ColNam)
Set FoundRang = Sheets(ConsolData).Range(ColNam1).Find(LoopRang, lookat:=xlWhole)
For Each FoundRang In Sheets(ConsolData).Range(ColNam1)
If FoundRang & FoundRang.Offset(0, -1) <> LoopRang & LoopRang.Offset(0, -1) Then
TotRows = Worksheets(ConsolData).Range("D65536").End(xlUp).Row
ThisWorkbook.Worksheets(LeaData).Rows(LoopRang.Row).Copy ThisWorkbook.Worksheets(ConsolData).Rows(TotRows + 1)
ThisWorkbook.Worksheets(ConsolData).Rows(TotRows + 1).Interior.Color = vbYellow
GoTo NextLine
End If
Next FoundRang
NextLine:
Next LoopRang
End Function
Please help with the VBA code.
Thanks in advance...
First I am going to give some general coding hints:
set Option Explicit ON. This is done through Tools > Options >
Editor (tab) > Require Variable Declaration . Now you HAVE to
declare all variables before you use them.
always declare a variables type when you declare it. If you are unsure about what to sue or if it can take different types (not advisable!!) use Variable.
Use a standard naming convention for all your variables. Mine is a string starts with str and a double with dbl a range with r, etc.. So strTest, dblProfit and rOriginal. Also give your variables MEANINGFUL names!
Give your Excel spreadsheets meanigful names or captions (caption is what you see in excel, name is the name you can directly refer to in VBA). Avoid using the caption, but refer to the name instead, as users can change the caption easily but the name only if they open the VBA window.
Ok so here is how a comparison between two tables can be done with your code as starting point:
Option Explicit
Public Function Compare()
Dim rOriginal As Range 'row records in the lookup sheet (cList = Sheet2)
Dim rFind As Range 'row record in the target sheet (TotalList = Sheet1)
Dim rTableOriginal As Range 'row records in the lookup sheet (cList = Sheet2)
Dim rTableFind As Range 'row record in the target sheet (TotalList = Sheet1)
Dim shOriginal As Worksheet
Dim shFind As Worksheet
Dim booFound As Boolean
'Initiate all used objects and variables
Set shOriginal = ThisWorkbook.Sheets("Sheet2")
Set shFind = ThisWorkbook.Sheets("Sheet1")
Set rTableOriginal = shOriginal.Range(shOriginal.Rows(3), shOriginal.Rows(shOriginal.Rows.Count).End(xlUp))
Set rTableFind = shFind.Range(shFind.Rows(5), shFind.Rows(shFind.Rows.Count).End(xlUp))
booFound = False
For Each rOriginal In rTableOriginal.Rows
booFound = False
For Each rFind In rTableFind.Rows
'Check if the E and F column contain the same information
If rOriginal.Cells(1, 5) = rFind.Cells(1, 5) And rOriginal.Cells(1, 6) = rFind.Cells(1, 6) Then
'The record is found so we can search for the next one
booFound = True
GoTo FindNextOriginal 'Alternatively use Exit For
End If
Next rFind
'In case the code is extended I always use a boolean and an If statement to make sure we cannot
'by accident end up in this copy-paste-apply_yellow part!!
If Not booFound Then
'If not found then copy form the Original sheet ...
rOriginal.Copy
'... paste on the Find sheet and apply the Yellow interior color
With rTableFind.Rows(rTableFind.Rows.Count + 1)
.PasteSpecial
.Interior.Color = vbYellow
End With
'Extend the range so we add another record at the bottom again
Set rTableFind = shFind.Range(rTableFind, rTableFind.Rows(rTableFind.Rows.Count + 1))
End If
FindNextOriginal:
Next rOriginal
End Function