Copy & paste inadvertently triggers Worksheet_Change sub - vba

I am having problems with a "Worksheet_Change" sub that copies and pastes the whole row into a second worksheet ("Completed") when the column "P" takes on the value "x". It reads like this:
Private Sub Worksheet_Change(ByVal Target As Range)
On Error Resume Next
Application.EnableEvents = False
'If Cell that is edited is in column P and the value is x then
If Target.Column = 16 And Target.Value = "x" Then
'Define last row on completed worksheet to know where to place the row of data
LrowCompleted = Sheets("Completed").Cells(Rows.Count, "A").End(xlUp).Row
'Copy and paste data
Range("A" & Target.Row & ":P" & Target.Row).Copy Sheets("Completed").Range("A" & LrowCompleted + 1)
'Delete Row from Project List
Range("A" & Target.Row & ":P" & Target.Row).Delete xlShiftUp
End If
Application.EnableEvents = True
End Sub
The sub itself works fine but if I copy and paste anywhere in the worksheet, the sub is activated and the row into which I paste is send to my "Completed" sheet.
I have played around with the "if-clause" without any luck so far. E.g.:
If Not Target.Column = 16 And Target.Value = "x" Is Nothing Then
I fear I am missing the obvious and I am grateful for any help.
Thanks and regards
PMHD

If you are concerned with muliple targets, deal with them; don't discard them.
Private Sub Worksheet_Change(ByVal Target As Range)
If not intersect(target, range("p:p")) is nothing then
on error goto meh
Application.EnableEvents = False
dim t as range, lrc as long
lrc = workSheets("Completed").Cells(Rows.Count, "A").End(xlUp).Row + 1
for each t in intersect(target, range("p:p"))
if lcase(t.Value2) = "x" Then
intersect(columns("A:P"), t.rows(t.row)).Copy _
destination:=workSheets("Completed").cells(lrc , "A")
lrc = lrc+1
'Delete Row from Project List
intersect(columns("A:P"), t.rows(t.row)).Delete xlShiftUp
end if
next t
End if
meh:
Application.EnableEvents = true
end sub

Thanks, Jeeped.
The problem arose due to Target referring to multiple cells. It was fixed by excluding cases where Target.Count > 1.
Private Sub Worksheet_Change(ByVal Target As Range)
On Error Resume Next
Application.EnableEvents = False
'Exclude all cases where more than one cell is Target
If Target.Count > 1 Then
'If Cell that is edited is in column P and the value is x then
ElseIf Target.Column = 16 And Target.Value = "x" Then
'Define last row on completed worksheet to know where to place the row of data
LrowCompleted = Sheets("Completed").Cells(Rows.Count, "A").End(xlUp).Row
'Copy and paste data
Range("A" & Target.Row & ":P" & Target.Row).Copy Sheets("Completed").Range("A" & LrowCompleted + 1)
'Delete Row from Project List
Range("A" & Target.Row & ":P" & Target.Row).Delete xlShiftUp
End If
Application.EnableEvents = True
End Sub

Related

Merging 2 private sub VBA codes

i have 2 Private Sub Worksheet_change(ByVal Target As Range) codes that work on their own. i need them to work in the same sheet. whenever i do this the 2nd code does not run. how do i merge these please!!?
Code 1:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng As Range, cel As Range
Set rng = Intersect(Target, Range([H2], Cells(Rows.Count,
"H").End(xlUp)))
If rng Is Nothing Then Exit Sub
Application.EnableEvents = False
rng.Offset(, 1).FormulaR1C1 = "=IF(RC[-1]<>"""",R1C[6] & ""-"" &" &
"TEXT(COUNTA(R2C[-1]:RC[-1]),""0000"") & ""-"" & R1C[7],"""")"
Application.EnableEvents = True
End Sub
Code 1 is using P1 and O1 to populate an automatic number in column I if information is provided in H
Code 2:
Private Sub Move_blanks_To_Bottom(ByVal Target As Range)
If Target.CountLarge > 1 Then Exit Sub
If Target.Column <> 9 Then Exit Sub
Range("A1", Range("A" & Rows.Count).End(xlUp)).Resize(, 11).Sort
key1:=Range("I1"), order1:=xlAscending, Header:=xlYes
End Sub
Code 2 is using column I and sorting the values so if there is a value in I it move the row to the next available line where column I is completed effectively if cell I is blank the row is moved to the bottom.
as i understand you cannot run 2 private sub codes so how would i run both of these on the same sheet at the same time?
thanks!
Because your first code exits (Exit Sub) when it fails that Intersect, then you have to call your second subroutine above that If statement. You'll have to pass it the Target as well like:
Call Move_blanks_To_Bottom(Target)
However, I think a rewrite might be best. Instead of exiting the subroutine all over the place, instead place relevant bits of code inside If statements so your routine can run to completion and exit gracefully:
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
'Do logic for this first range
Dim rng As Range
rng = Range([H2], Cells(Rows.Count, "H").End(xlUp)))
If Not Intersect(rng, Target) Is Nothing Then
rng.Offset(, 1).FormulaR1C1 = "=IF(RC[-1]<>"""",R1C[6] & ""-"" &" & "TEXT(COUNTA(R2C[-1]:RC[-1]),""0000"") & ""-"" & R1C[7],"""")"
End If
'now do logic for the second range (move_blanks_to_bottom)
If Target.CountLarge = 1 And Target.Column = 9 Then
Range("A1", Range("A" & Rows.Count).End(xlUp)).Resize(, 11).Sort key1:=Range("I1"), order1:=xlAscending, Header:=xlYes
End If
Application.EnableEvents = True
End Sub

Defining a range for 2 private subs that have been merged

I have 2 private subs that have been merged into the code below. the 2nd part of the code i need to define the logic for the range. The issue i am facing is my second code does not specify a range so i am unsure on how to define the range? and my VBA knowledge is not that great! can someone help me enter this information?
The Codes before merging are:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng As Range, cel As Range
Set rng = Intersect(Target, Range([H2], Cells(Rows.Count,
"H").End(xlUp)))
If rng Is Nothing Then Exit Sub
Application.EnableEvents = False
rng.Offset(, 1).FormulaR1C1 = "=IF(RC[-1]<>"""",R1C[6] & ""-"" &" &
"TEXT(COUNTA(R2C[-1]:RC[-1]),""0000"") & ""-"" & R1C[7],"""")"
Application.EnableEvents = True
End Sub
Code 1 is using P1 and O1 to populate an automatic number in column I if information is provided in H Code 2:
Private Sub Move_blanks_To_Bottom(ByVal Target As Range)
If Target.CountLarge > 1 Then Exit Sub
If Target.Column <> 9 Then Exit Sub
Range("A1", Range("A" & Rows.Count).End(xlUp)).Resize(, 11).Sort
key1:=Range("I1"), order1:=xlAscending, Header:=xlYes
End Sub
Code 2 is using column I and sorting the values so if there is a value in I it moves the row to the next available line where column I is completed effectively if cell I is blank the row is moved to the bottom.
The merged code is:
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
'Do logic for this first range
Dim rng As Range, cel As Range
Set rng = Intersect(Target, Range([H2], Cells(Rows.Count,
"H").End(xlUp)))
If Not Intersect(rng, Target) Is Nothing Then
rng.Offset(, 1).FormulaR1C1 = "=IF(RC[-1]<>"""",R1C[6] & ""-"" &" &
"TEXT(COUNTA(R2C[-1]:RC[-1]),""0000"") & ""-"" & R1C[7],"""")"
End If
'now do logic for the second range (move_blanks_to_bottom)
'2nd LOGIC HERE
If Target.CountLarge = 1 And Target.Column = 9 Then
Range("A1", Range("A" & Rows.Count).End(xlUp)).Resize(, 11).Sort
key1:=Range("I1"), order1:=xlAscending, Header:=xlYes
End If
Application.EnableEvents = True
End Sub
Thank you!
Your sorting command is fine though it is pretty complicated because it compresses a series of operations in one command. With this command you sort a range from column A to column K (=11) and from row 1 to last used row, with header. You could split this command with these simpler ones:
Dim lastrow As Long
Dim r As Range
lastrow = Range("A" & Rows.Count).End(xlUp).Row ' find row of last non-blank cell in column A
Set r = Range("A1").Resize(lastrow, 11) ' set the exact data range
r.Sort key1:=Range("I1"), order1:=xlAscending, Header:=xlYes
but this will do the same as in your code 2.
You might need to know that if you break the command into 2 lines you need to terminate the line with an _ like this:
Range("A1", Range("A" & Rows.Count).End(xlUp)).Resize(, 11).Sort _
key1:=Range("I1"), order1:=xlAscending, Header:=xlYes
otherwise the compiler will treat them as separate commands and will drop a Syntax error message.

Need to Delete data correctly in column F if met empty cells on G

My code is limited to work fine only with first empty cell found, the problem starts is that if finds the next two or more empty cells because it loops a bit (I can handle with that), but if it finds empty cells and next finds cells with data again, it totally fails.
Private Sub Worksheet_Change(ByVal Target As Range)
firstRow = 7
lastrow = Sheets("Datos del Proyecto").Range("F" & Rows.Count).End(xlUp).row
i = firstRow
Do Until i > lastrow
If Sheets("Datos del Proyecto").Range("G" & i).Value Like "" Then
Sheets("Datos del Proyecto").Range("F" & i).ClearContents
End If
i = i + 1
Loop
Screenshot:
Since the code is placed inside "Datos del Proyecto" sheet, in Worksheet_Change event, there is no need to reference it in the code all the time, as it is the default sheet.
Using Application.EnableEvents = False will prevent the code to exit and re-enter the Sub as you ClearContents each iteration inside the For loop.
Code
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim LastRow As Long, i As Long
' disable worksheet events >> will prevent the endless loop you got
Application.EnableEvents = False
' find last row in Column F
LastRow = Cells(Rows.Count, "F").End(xlUp).Row
' loop through all rows from row 7 until last row
For i = 7 To LastRow
If IsEmpty(Range("G" & i)) Or Range("G" & i).Value = "" Then
Range("F" & i).ClearContents
End If
Next i
Application.EnableEvents = True
End Sub
You can ass an option to your Sub , to make it run only if the change occurred in certain Range by adding these 3 lines in the beginning of the code:
Dim WatchRange As Range
' check only if cells changed are in Column G
Set WatchRange = Columns("G:G")
If Not Intersect(Target, WatchRange) Is Nothing Then
#Shai_Rado answer:
'Option Explicit <-- I needed to disable to make it work.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim LastRow As Long, i As Long
'Dim WatchRange As Range <-- I needed to disable to make it work.
'Set WatchRange = Columns("G:G") <-- I needed to disable to make it work.
'If Not Intersect(Target, WatchRange) Is Nothing Then <-- I needed to disable to make it work.
Application.EnableEvents = False
LastRow = Cells(Rows.Count, "F").End(xlUp).row
For i = 7 To LastRow
If IsEmpty(Range("G" & i)) Or Range("G" & i).Value = "" Then
Range("F" & i).ClearContents
End If
Next i
Application.EnableEvents = True
End Sub
The one I proposed with the help lines:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim i As Long
Application.EnableEvents = False
firstRow = 7
lastrow = Sheets("Datos del Proyecto").Range("F" & Rows.Count).End(xlUp).row
i = firstRow
Do Until i > lastrow
If Sheets("Datos del Proyecto").Range("G" & i).Value Like "" Then
Sheets("Datos del Proyecto").Range("F" & i).ClearContents
End If
i = i + 1
Loop
Application.EnableEvents = True
End Sub

Forced ranking macro excel vba

I have the setup as shown in the image above.
Logic of the macro is if I enter a number 1 in cell B5 or in empty cell in Range("B2:B26") then the output would be in this format:
B2 3
B3 4
B4 2
B5 1
Now it gives me that output but there are certain drawbacks e.g.
if I provide input 8 to the same cell then it will still increment the ranks. I incorporated a match check to see if that value is there or not but it doesn't seem to work Any help would be appreciated.
Private Sub Worksheet_Change(ByVal Target As Range)
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim KeyCells As Range
Dim i As Long, Cel As Range, sht1 As Worksheet, j As Long, found As Boolean
Set sht1 = Sheet1
Set KeyCells = sht1.Range("B2:C26")
If Not Application.Intersect(KeyCells, Range(Target.Address)) Is Nothing Then
If Target.Column = 2 Then
For i = 2 To 26
If sht1.Range("B" & i) <> Empty And sht1.Range("B" & i).Value >= Target.Value And i <> Target.Row Then
sht1.Range("B" & i).Value = sht1.Range("B" & i).Value + 1
Else: End If
Next i
Else: End If
If Target.Column = 3 Then
For i = 2 To 26
If sht1.Range("C" & i) <> Empty And sht1.Range("C" & i).Value >= Target.Value And i <> Target.Row Then
sht1.Range("C" & i).Value = sht1.Range("C" & i).Value + 1
Else: End If
Next i
Else: End If
Else: End If
Call CreateDataLabels
Target.Select
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
Is this what you are trying? I have not extensively tested it
Option Explicit
Dim rng As Range
Private Sub Worksheet_Change(ByVal Target As Range)
Dim oldVal As Long, i as Long
On Error GoTo Whoa
Application.EnableEvents = False
Set rng = Range("B2:B26")
If Not Intersect(Target, rng) Is Nothing Then
oldVal = Target.Value
If NumExists(oldVal, Target.Row) = True Then
For i = 2 To 26
If i <> Target.Row And Range("B" & i).Value >= oldVal Then _
Range("B" & i).Value = Range("B" & i) + 1
Next i
End If
End If
Letscontinue:
Application.EnableEvents = True
Exit Sub
Whoa:
MsgBox Err.Description
Resume Letscontinue
End Sub
Function NumExists(n As Long, r As Long) As Boolean
Dim i As Long
For i = 2 To 26
If Range("B" & i) = n And r <> i Then
NumExists = True
Exit Function
End If
Next i
End Function
edited to remove "helper" values
edited to add functionality for column C as well
Being Siddharth Rout's answer the solution, and having the OP's not asked for anything more, I'd propose the following as an alternative option to possibly be discussed if worth considering
Option Explicit
Private Sub Worksheet_Change(ByVal target As Range)
Dim oldVal As Long
Dim wrkRng As Range
Application.EnableEvents = False
On Error GoTo EndThis
If Continue(target, Range("B2:C26").Cells, oldVal, wrkRng) Then '<== here you set "B2:C26" as the "sensitive" range
With wrkRng
.Offset(, 2).Value = .Value
.FormulaR1C1 = "=IF(RC[2]<>"""",RC[2]+IF(and(RC[2]>=" & oldVal & ",ROW(RC)<>" & target.Row & "),1,0),"""")"
.Value = .Value
.Offset(, 2).ClearContents
End With
End If
EndThis:
If Err Then MsgBox Err.Description
Application.EnableEvents = True
Exit Sub
End Sub
Function Continue(target As Range, rng As Range, oldVal As Long, wrkRng As Range) As Boolean
If target.Cells.Count = 1 Then
If Not IsEmpty(target) Then ' if cell has not been cancelled
Set wrkRng = Intersect(target.EntireColumn, rng)
If Not wrkRng Is Nothing Then
oldVal = target.Value
Continue = Application.WorksheetFunction.CountIf(wrkRng, oldVal) > 1
End If
End If
End If
End Function
as compared to Siddharth Rout's solution, it enhances the following:
more (complete?) testing as if to go on with rng processing
in previous solution
if you cancelled a cell in rng it'd add 1's in all rng cells
if you pasted values in more then one rng cells it'd throw an error
no use of cells iteration, both for oldVal counting purposes and for ranking updating

Detect on worksheet change if user is deleting

I'd like to know how to detect if the user is deleting or inserting content into a range. If they are deleting a range say D14:D18. I'd like to then perform a macro that also deletes content in E14:E18. I just wouldn't want to delete E14:E18 if they are entering content into D14:D18.
I've tried:
If Selection.ClearContents Then
MsgBox Target.Offset(0, 3).Style
End If
But this get's me stuck in an infinite loop.
A bit more context:
I have a few hundred cells in D:D for entering quantities for services. Not everything in D:D should be touched. Only cells in D:D with .Style = "UnitInput". In E:E I have data validation that lets the user only enter contractor 1 or contractor 2 But, when content is entered in D:D I run a macro to assign the default contractor (housed in F:F) to E:E. So when the user enters quantities into D:D it correctly assigns the default contractor. And when they delete singular items from D:D I have it handling proper removal of contractors. It's only when they delete a range of items from D:D.
Full code:
Private Sub Worksheet_Change(ByVal Target As Range)
Application.ScreenUpdating = False
On Error GoTo ErrHandler:
If Selection.Rows.Count * Selection.Columns.Count = 1 Then
If Target.Offset(0, 3).Style = "Contractor" Then
If Target.Value < 1 Then
Target.Offset(0, 3).Value = ""
Else
Target.Offset(0, 3).Value = Target.Offset(0, 2).Value
End If
End If
If Target.Offset(0, 5).Style = "Markup" Then
If Target.Value = "" Then
Target.Offset(0, 5).Value = ""
ElseIf Target.Value <= Target.Offset(0, 14).Value Then
Target.Offset(0, 5).Value = "Redact 1"
ElseIf Target.Value >= Target.Offset(0, 15).Value Then
Target.Offset(0, 5).Value = "Redact 2"
Else
Target.Offset(0, 5).Value = "Redact 3"
End If
End If
Else
'!!!!!! this is where I need to handle multiple deletions. !!!!!!!
End If
Application.ScreenUpdating = True
ErrHandler:
Application.ScreenUpdating = True
Resume Next
End Sub
Based on your comments in chat, here is what I propose
UNTESTED
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng As Range, aCell As Range
Dim lRow As Long
'~~> Error handling, Switching off events and Intersect
'~~> As described in
'~~> http://stackoverflow.com/questions/13860894/ms-excel-crashes-when-vba-code-runs
On Error GoTo Whoa
Application.EnableEvents = False
With ActiveSheet
'~~> Find Last Row since data is dynamic
'~~> For further reading see
' http://stackoverflow.com/questions/11169445/error-in-finding-last-used-cell-in-vba
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
lRow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
End If
If lRow > 12 Then
'~~> Set your range
Set rng = Range("D13:D" & lRow)
If Not Intersect(Target, rng) Is Nothing Then
For Each aCell In rng
If Len(Trim(aCell.Value)) = 0 Then
Select Case Target.Offset(0, 3).Style
Case "Contractor"
'~~> Do Your Stuff
Case "Markup"
'~~> Do Your Stuff
'
'~~> And so on
'
End Select
End If
Next aCell
End If
End If
End With
Letscontinue:
Application.EnableEvents = True
Exit Sub
Whoa:
MsgBox Err.Description
Resume Letscontinue
End Sub
Here is an idea -- you have to first select a region to clear its contents. Use selection change to record the number of non-blank cells and then worksheet change to see if it drops to zero. Something like:
Dim NumVals As Long
Private Sub Worksheet_Change(ByVal Target As Range)
Dim NewCount As Long
NewCount = Application.WorksheetFunction.CountA(Target)
If NewCount = 0 And NumVals > 0 Then MsgBox Target.Address & " was cleared"
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
NumVals = Application.WorksheetFunction.CountA(Target)
End Sub
I have this code is Sheet1 and it seems to capture when I highlight a group of cells (which contains at least one value) and then hit the delete key.
You can use the CommandBars Undo Control to determine if the user has actually deleted something.
Bear in mind this will fire if the user any or all of the contents of the Range D14:D18, but can be adjusted in many ways to suit your exact needs. After seeing your edit, this basically means you can adjust the ranges and need be and which cells in column E it affects as well. If you need more guidance on this, let me know.
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Me.Range("D14:D18")) Is Nothing Then
Dim sLastAction As String
sLastAction = Application.CommandBars("Standard").Controls("&Undo").List(1)
Debug.Print sLastAction
'manual delete 'right-click delete 'backspace delete
If sLastAction = "Clear" Or sLastAction = "Delete" Or Left(sLastAction, 9) = "Typing ''" Then
Application.EnableEvents = False
Me.Range("E14:E18").ClearContents
Application.EnableEvents = True
End If
End If
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
Dim ChangedRange As Range
Dim Area As Range
Dim Cell As Range
Set ChangedRange = Application.Intersect(Target, Range("D:D"))
If Not ChangedRange Is Nothing Then
Application.EnableEvents = False
For Each Area In ChangedRange.Areas
For Each Cell In Area
If IsEmpty(Cell) Then
Cell.Offset(0, 1).ClearContents
End If
Next
Next
Application.EnableEvents = True
End If
End Sub