Detect whether a cell has been changed by user or macro - vba

I have a spreadsheet where users paste numbers, due to the length of these numbers (and the fact that we don't need to spreadsheet to carry out any computations with them) we want them to be formatted as text, otherwise they appear in scientific format i.e. 1.12345E+13 instead of 12345678912345
It is not possible to adjust/modify the data source the numbers are being copied from.
I'm using Private Sub Workbook_SheetChange do detect if a cell in the relevant range has been changed, and I then format the range to text with
ThisWorkbook.Sheets("Sheet1").Columns("B").NumberFormat = "#"
Unfortunately on Excel 2007 whether you do this manually in Excel or via a marco, the number still appears as 1.12345E+13 unless you click into the cell and press enter.
I can get round this by applying:
With rng
.Value = Evaluate("IF(ISTEXT(" & .Address & "),TRIM(" & .Address & "),REPT(" & .Address & ",1))")
End With
but when I do this I end up with an infinite loop, as the Private Sub Workbook_SheetChange detects the cell has been changed and goes round the loop again.
If I could somehow work out whether the cell has been changed manually by the user or by the macro, this would be easily fixed. The macro is in ThisWorkbook. I've tried using Application.Activesheet instead of ThisWorkbook.Sheets but it didn't make any difference.
If alternatively there's an easier/better way to fix numbers being displayed as 1.12345E+13 even after I've re-formatted the cell I'd love to know about it.
Thank you.

but when I do this I end up with an infinite loop, as the Private Sub Workbook_SheetChange detects the cell has been changed and goes round the loop again.
That's because you need to disable application events from automatically firing.
Private Sub Workbook_SheetChange(ByVal Sh As Worksheet, ByVal Target As Range)
Application.EnableEvents = False '// Stop events automatically firing
With rng
.Value = Evaluate("IF(ISTEXT(" & .Address & "),TRIM(" & .Address & "),REPT(" & .Address & ",1))")
End With
Application.EnableEvents = True '// Re-enable events for next time
End Sub
Because you've disabled the events, it can't trigger itself again when you change the value of the cell. Once the code has completed you can re-enable the events to ensure that it fires the next time it is required.
For what it's worth, don't beat yourself up about it - this is an extremely common pitfall when people start working with event procedures in excel-vba.

Here's a full example, including handling Target ranges of >1 cell:
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
Dim c As Range
Application.EnableEvents = False
On Error GoTo haveError
For Each c In Application.Intersect(Target, Sh.UsedRange).Cells
With c
If IsNumeric(.Value) Then
.NumberFormat = "#"
.Value = CStr(.Value)
End If
End With
Next c
haveError:
'Make sure to re-enable events!
Application.EnableEvents = True
End Sub

Related

Create date stamp when change detected

This script ends up being a runtime error when more than one cell in the target is modified.
I basically need to be able to make multiple changes at once and still have the date stamp work.
I'm still new to these sorts of scripts, any help will be appreciated.
Thanks.
Private Sub Worksheet_Change(ByVal Target As Range)
' Auto Date
Dim cell As Range
'Unprotecting Text Submission tool tab
wstextsubmissiontool.Unprotect "Abc123"
For Each cell In Target
If cell.Column = Range("E:E").Column Then
If cell.Value <> "" Then
Cells(cell.Row, "C").Value = Now
Else
Cells(cell.Row, "C").Value = ""
End If
End If
Next cell
'protecting Text Submission tool tab
wstextsubmissiontool.Protect "Abc123"
End Sub
The issue is that, by changing the cell that contains the time, you are changing the worksheet, so Excel wants to run your code to change the cell that contains the time... so basically the error is to prevent an infinite loop.
The way around it is to disable events at the start of your Worksheet_Change procedure with Application.EnableEvents = False. Just be sure to re-enable events at the End of the procedure (or also if you Exit the procedure early for some reason).
A simplified example (excluding your password protection) is:
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
Sheets("sheet1").Range("a1") = Now()
Application.EnableEvents = True
End Sub
More Info:
Microsoft : Application.EnableEvents property
Microsoft : Worksheet.Change event
Wikipedia : Infinite Loop
"Infinite Loop" 😜 (source)

Excel VBA - How to alter the value of multiple cells inputted by the user?

I have a worksheet on Excel in which the user must input some registration numbers in column C. These numbers are usually inserted by the user by copying a pasting several of them at a time. The format of the copy source is often in the format "000.000.000-00", but I want it to be just "00000000000" in the worksheet, without the dots and dash.
I am trying to develop a code in Excel VBA to automatically remove these dots and dash after the user types or paste the registration number. I need it to be in the same cell. The code is:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim SkipEvents As Boolean
If SkipEvents Then Exit Sub
With ActiveWorkbook.Sheets("Staff")
If Not Application.Intersect(Target, Me.Range("C:C")) Is Nothing Then
SkipEvents = True
li = Intersect(Target, Range("C:C")).Row
nl = Intersect(Target, Range("C:C")).Rows.Count
lf = li + nl - 1
For i = li To lf
RegNumb = Range("C" & i).Value
If Len(RegNumb) = 14 Then
Range("C" & i).Value = Left(RegNumb, 3) & Mid(RegNumb, 5, 3) & Mid(RegNumb, 9, 3) & Right(RegNumb, 2)
End If
Next i
SkipEvents = False
End If
End With
End Sub
So far, the code is able to remove the dots and dash if the user types or copy+paste one registration number at a time. However, when the user copy+paste 2 or more registraton numbers at a time, only the first cell from the range have its dots and dash removed, and the others stay as they are. Can someone help me with this problem?
Only the first cell in the range is being changed by your code because you are not iterating over the range. You could instead do this:
For each c in Target
if c.column = 3 then
c.value = Replace(Replace(c.value, ".", ""), "-", "")
end if
Next c
inside your Worksheet_Change.
You can replace directly on the entire desired range. You'll also need to Disable Events from firing since the changes you make will again fire the code. Also, since it's sheet level code, no need to refer to the sheet.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rCheck as Range
Set rCheck = Application.Intersect(Target, Me.Range("C:C"))
If Not rCheck is Nothing
Application.EnableEvents = False
rCheck.Replace(".","",lookat:=xlPart)
rCheck.Replace("-","",lookat:=xlPart)
Application.EnableEvents = True
End If
End Sub
I will also warn that if the data set being checked is very large, this replace method can have poor performance. However, if copy / paste is relatively small in terms of cell counts, it should be fine.

error '1004': Select method of Range class failed

I am sure that every person who reads the title would say "oh no, not again". But before posting this, I have read about 7-8 previous questions with similar title and no-one seemed to work. So this is the deal:
My workbook uses the Workbook_Open event to show a userform:
Sub Workbook_Open()
UserForm1.Show
End Sub
Userform1 includes a textbox and a listbox. The listbox is populated with data dynamically, according to user's typing in the textbox. So far everything ok.
When user clicks at a value inside the listbox, I want a specific cell to be selected. So I used this:
Private Sub ListBox1_Click()
Dim Cell As Range
With ThisWorkbook.Worksheets(1)
Set Cell = .Range("C3", .Cells(.Rows.Count, 3).End(xlUp)).Find(UserForm1.ListBox1.Text, LookIn:=xlValues)
.Range(Cell, Cell.Offset(0, 2)).Select
End With
End Sub
But as many others before me, I cannot make Select work properly and this code gave an error like the title.
I tried several things to make this code work.
-Tried Application.GoTo, using it instead of Select,as someone suggested.
-Tried to move the code of the ListBox1_Click event to a sub inside a standard module and call this sub with the ListBox1_Click event.
-Tried to select the worksheet first as others suggested.
-Tried Worksheets(1).Visible = True as someone else suggested.
-Tried to activate the main window of the application first with AppActivate Application.Caption.
-Tried to scroll to the desired cell instead of selecting:
Private Sub ListBox1_Click()
Dim foundRow As Integer
With ThisWorkbook.Worksheets(1)
foundRow = .Range("C3", .Cells(.Rows.Count, 3).End(xlUp)).Find(UserForm1.ListBox1.Text, LookIn:=xlValues).Row
End With
ActiveWindow.ScrollRow = foundRow - 1
End Sub
I also tried other less important changes but I'm stuck with it. The funny thing is that before using the Workbook_Open event, I had UserForm1 shown via a macro assigned to a button/shape in the worksheet and everything worked fine then.
Does anybody know how am I going to make Select work in my case?
On the other hand, it's well understood that Select causes a great deal of problems, so I'm not stuck with it. If anyone has a another way to have the same result I'm all ears.
I was able to replicate the issue, first of all I followed what you described, made a list and picked from it to see if would scroll into view, and it worked.
If I closed the form, changed the active sheet. and then run the form it failed with the error message you are seeing.
Trying a number of things I discovered you can't select a cell if it is not the active sheet (which is logical, a user can not click on a cell that is not on the sheet they are viewing)
The below Activate line should fix it.
Private Sub ListBox1_Click()
Dim Cell As Range
With ThisWorkbook.Worksheets(1)
Set Cell = .Range("C3", .Cells(.Rows.Count, 3).End(xlUp)).Find(UserForm1.ListBox1.Text, LookIn:=xlValues)
ThisWorkbook.Worksheets(1).Activate
.Range(Cell, Cell.Offset(0, 2)).Select
End With
End Sub
As noted by #ScotCraner and as shown in the sample code on MSDN you should first check if something has been found before using / showing it.
The following should work flawlessly:
Private Sub ListBox1_Click()
Dim Cell As Range
Application.DisplayStatusBar = True
With ThisWorkbook.Worksheets(1)
Set Cell = .Range("C3", .Cells(.Rows.Count, 3).End(xlUp)).Find(UserForm1.ListBox1.Text, LookIn:=xlValues, LookAt:=xlWhole)
If Not Cell Is Nothing Then
Application.StatusBar = "Found " & UserForm1.ListBox1.Text
.Activate
.Range(Cell, Cell.Offset(0, 2)).Select
Else
Application.StatusBar = "Couldn't find " & UserForm1.ListBox1.Text
End If
End With
End Sub
Note, that you are using UserForm1.ListBox1.Text which does not consider UserForm1.ListBox1.MultiSelect.

How to speed up this code VBA

This code is very straightforward if any cell in the range is changed it should put date and time in the col A of that row.
This takes too long for larger range I tried to exit sub after the IF statement but it gets slow and EXCEL stops responding until it has finished with the code
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim Cel As Range
Dim m As Long
For Each Cel In Range("B2:Z104857")
If Not Intersect(Target, Cel) Is Nothing Then
m = Cel.Row
With ActiveSheet.Range("A" & m)
.Value = Date & " " & Time
.NumberFormat = "dd/mm/yyyy hh:mm AM/PM "
End With
Exit Sub
End If
Next Cel
End Sub
My guess is that Target is smaller than Range("B2:Z104857") (which is a huge range)
So if you replace the line:
For Each Cel In Range("B2:Z104857")
by
For Each Cel In Target
you should get a dramatic speedup.
Also -- it can't hurt to put
Application.ScreenUpdating = False
at the beginning of your sub and
Application.ScreenUpdating = True
at the end.
On Edit: #CharlesWilliams points out that it is also a good idea to add the line Application.EnableEvents=False at the start of the sub and Appication.EnableEvents=True at the end. This is because event handlers can sometimes lead to cascading behavior whereby event handlers make changes which trigger other event handlers (which maybe trigger still other event handlers ...).
Can you just check target.column within your constraints, the same with rows, then cells(target.Row,1).value=now?
If (target.column>=2 and target.column<=26) and (target.row>=2 and target.row<=10000) then cells(target.Row,1).value =now

Program excel to return a specific value

I have a spreadsheet that is going to be used in a survey.
How can I make the cells only to return "x" regardless of what the survey taker type in.
For instance, if I write a "w" in the cell, it should turn into an "x".
I have come to a point where I think there is an option when I protect the workbook or sheet. Because I can tell from another spreadsheet (which has this function) that it only works if the workbook is protected.
I tried to google it, but it seems as if I don't know the right keywords to find the answer.
Also, I have found a set of Vba code that I fiddle with, but I'm not sure this is correct. I don't want to attach the code as I don't want to confuse any response here.
Thank you for any help provided.
Put this code in the worksheet module and test it out, when you change a cell in column A (1) it will activate,
Where is the worksheet Module?
Copy and paste the code ,
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Count > 1 Then Exit Sub
If Intersect(Target, Range("A1,B1,C1,A4,B4,C4")) Is Nothing Then Exit Sub
Application.EnableEvents = 0
If Target <> "" Then Target = "X"
Application.EnableEvents = 1
End Sub
This should work for you (just change the range to the one you need) :
Option Explicit
Sub worksheet_Change(ByVal Target As Range)
On Error GoTo errorbutler 'error handler - important to have this in case your macro does a booboo
Application.Calculation = xlCalculationManual 'turn off automatic calculation to speed up the macro
Application.EnableEvents = False 'turn off events in order to prevent an endless loop
Dim LR, cell As Range 'Declare your variables
Set LR = Range("A1:b3") ' Select range of cells this macro would apply to
For Each cell In Target 'Loops through the cells that user have changed
If Union(cell, LR).Address = LR.Address Then 'Checks if the changed cell is part of your range
cell.Value="x" 'changes the value of that cell to x
End if
Next cell
Errorexit: 'part of error handling procedure
Application.EnableEvents = True 'Turn autocalc back on
Application.Calculation = xlCalculationAutomatic 'turn events back on
Exit Sub
Errorbutler: 'error handling procedure
Debug.Print Err.Number & vbNewLine & Err.Description
Resume Errorexit
End Sub
Oh yes, and this code should be put into the worksheet module - the same way as Dave has shown you