VB, excel macro pause and resume working if possible - vba

I cannot figure out the best way to do the following problem. Basically my macro (excel, VB) is checking several (100+) worksheets for correct values, if wrong value is found, I want it to stop, give me a warning, then I could have a look into the reason why the value is wrong, correct it manually and then I want to resume the macro, or remember the last value checked so if I return, it remembers where to continue (resume).
My current problem is that it finds the wrong value, then I can either make it stop so I check the problem, or it goes through all the sheets and then I have to remember which sheets had the wrong value.
What I thought of is make a list where the name of sheet is added every time a wrong value is found. The problem is that usually there is more than 1 wrong value in the same sheet if there is a wrong value at all and this added the same sheet name several times to the list. Another problem with that is that I cannot correct the values straight away.
I'm very inexperienced with programming and so would appreciate your idea on how to best approach this problem (I don't want to spend a long time on coding something which wouldn't be efficient for such a "simple" problem).

When the error is found (I'm assuming you've already been able to identify this), you can use the Application.InputBox function to prompt you for a new value.
For example, if rng is a Range variable that represents the cell being checked, and you have some logic to determine where the error happens, then you can just do:
rng.Value = Application.InputBox("Please update the value in " & rng.Address, "Error!", rng.Value)
The inputbox function effectively halts execution of the procedure, while waiting for input from the user.
If InputBox isn't robust enough, then you can create a custom UserForm to do the same sort of thing. But for modifying single range values, one at a time, the InputBox is probably the easiest to implement.

I believe you can handle this task by using one or two static local variables in your macro. A variable declared with "static" rather than "dim" will remember its value from the last time that procedure was run. This can hold where you left off so you can resume from there.
One thing that could be a problem with this solution would be if the macro gets recompiled. That would probably cause VBA to clear the value that the static variable was holding. Just doing a data edit in Excel should not cause a recompile, but you will want to watch for this case, just to make sure it doesn't come up. It almost certainly will if you edit any code between executions.

Create a public variable that stores the cell address of the last checked cell and use a conditional statement to see if it's "mid-macro" for want of a better phrase. here is a very crude example...
Public lastCellChecked As String
Sub Check_Someting()
Dim cell As Excel.Range
Dim WS As Excel.Worksheet
If Not lastCellChecked = vbNullString Then Set cell = Evaluate(lastCellChecked)
'// Rest of code...
'// Some loop here I'm assuming...
lastCellChecked = "'" & WS.Name & "'!" & cell.Address
If cell.Value > 10 Then Exit Sub '// Lets assume this is classed as an error
'// Rest of loop here...
lastCellChecked = vbNullString
End Sub

The best way to do this is to create a userform and as mentioned by prior users create a public variable. When the program finds an error store the cell and initiate the userform. Your code will stop on the userform. When you're done checking the problem have a button on the userform that you can click to continue checking. Your loop can be something like the below.
public y as integer
sub temp1 ()
rw1= range("a500000").end(xlup).row 'any method to create a range will do
if y = null then y=1
for x = y to rw1
cells(x,1).select
'check for the problem your looking for
if errorX=true then
userform1.show
y = activecell.row
exit sub
end if
next x
end sub

What about inserting a button (on the sheet or in a menubar) for stopping?
Insert the code below:
'This at the top of the module
Public mStop As Boolean
'This in the module
Sub MyBreak()
mStop = True
End Sub
'This is your macro
Sub YourMacro()
'This at the top of your code
mStop = False
'Your code
'...
'This code where you want to break
DoEvents '<<<< This makes possible the stop
If mStop Then
mCont = MsgBox("Do you want to continue?", vbYesNo)
If mCont = vbNo Then
Exit Sub
Else
mStop = False
End If
End If
'Your code
'...
End Sub
Now you need to create a button and link it to the macro called "MyBreak".

Related

Default values for fields in a new row of a data table

When you have a data table in Excel, part of the standard functionality is that pressing tab in the last cell adds a new row at the bottom of the table. I want to auto-populate that new row with useful default values. In particular I want to put current date-time in one cell, and copy values into some other cells from the previous row of the table.
It is not workable to do that using formulae -- e.g. using =now() for the date-time stamp is inadequate because it will be auto-updated every time the spreadsheet recalculates, whereas I want it to retain the date-time at the moment when the row was added.
So I am trying to write VBA to be triggered by the event of the row being added, and in that code to write values into the cells of the new row. From MS documentation I thought DataTable.TableNewRow would be the appropriate event. But when I try to write any code for that event it is not being executed. When I look up DataTable in the VBA object browser the TableNewRow event is not listed.
Versions:
VBA for Applications 7.1
Excel 2013
So my questions:
Is the direction of my thinking right, or can you suggest a better approach?
Can you offer any working code that does something like this?
Is DataTable.TableNewRow the event I should be working with?
What do I need to do to get that event accessible in my VBA code?
You can try this:
Write this code in Thisworkbook.
Private Sub Workbook_Open()
Set ref_tbl = Sheet1.ListObjects(1).DataBodyRange
End Sub
Then below code in a Worsksheet Object.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
On Error GoTo halt
Application.EnableEvents = False
Dim tbl_rng As Range
Set tbl_rng = Me.ListObjects(1).DataBodyRange
If Not Intersect(Target, tbl_rng) Is Nothing Then
If tbl_rng.Rows.Count > ref_tbl.Rows.Count Then
MsgBox "Table increase in size"
'~~> Do your stuff here
Set ref_tbl = tbl_rng
End If
End If
forward:
Application.EnableEvents = True
Exit Sub
halt:
MsgBox Err.Number & ": " & Err.Description
Resume forward
End Sub
You will also need a Module to declare the public variable
Public ref_tbl As Range
So basically, this will tell you when your table increase in size.
If we're able to capture that, then you can do your stuff when that condition is met.
This works in the situation you describe in your question.
It will not work though when you insert row between entries in the table. Anyways, HTH.

Excel macro select two ranges and compare

This is a question that was asked to me in an interview. I have a excel list. It is copied to another location and then by mistake a row in the new location gets deleted.
Now I need to write a macro to compare the old and new ranges and then provide the missing data as result.
I can perhaps perform the comparison part. But the problem is I don't know how to get the selected range as input in a macro.
For eg. as soon as I select a range, it should be sent as input to the macro, then the macro should wait for another selection. As soon as I select the new range, the macro should compare and find the missing lines in new range.
Regarding the selection per mouse click you could look at the link I sent in the comments of the other answer. Selection_Change is an event which gets triggered when you change the selection of a worksheet (not only mouseclick but move-by-keys as well). The target coming in is the cell which you have selected. You can pass this as a range on to a function.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
showMsg Target
End Sub
Private Function showMsg(r As Range)
MsgBox r.Address
End Function
You can just as well use another event like BeforeDoubleClick or BeforeRightClick. Check out the events of Excel and choose the one you feel fits best.
If you only want the function to be triggered for a certain range you can filter it.
If target.column <> 1 then exit function
If you don't want the event to trigger your function each time you change a selection you can choose one cell to be the switch which gets triggered by the same event.
If target.address = "$A$1" Then Call toggleSearch()
with toggleSearch being the switching function.
This is a classical diff (and a simple one at that), you shouldn't select by hand or anything. Just sort the two lists in an identical way, then run a Sub which loops over the number of rows in the source sheet comparing each row with the same row in the target sheet. The first mismatch you get is the missing line.
This example assumes both sheets are in the same workbook but you can easily adapt it
Public Sub diffThem()
Dim src as Worksheet, trg as Worksheet
Dim r as Range, i as Integer
Set src = ThisWorkbook.Sheets("Source")
Set trg = ThisWorkbook.Sheets("Destination")
Set r = src.Range("A1")
For i = 1 to ThisWorkbook.Sheets("Source").UsedRange.Rows.Count
If r.EntireRow <> trg.Range("A" & r.Row).EntireRow Then
MsgBox("The missing row is " & r.Row)
Exit Sub
End if
Set r = r.Offset(1,0)
Next i
End Sub
If EntireRow cannot be run due to different layouts or whatever then loop the columns at that point.

Get the cell reference of the value found by Excel INDEX function

The Problem
Assume that the active cell contains a formula based on the INDEX function:
=INDEX(myrange, x,y)
I would like to build a macro that locates the value found value by INDEX and moves the focus there, that is a macro changing the active cell to:
Range("myrange").Cells(x,y)
Doing the job without macros (slow but it works)
Apart from trivially moving the selection to myrange and manually counting x rows y and columns, one can:
Copy and paste the formula in another cell as follows:
=CELL("address", INDEX(myrange, x,y))
(that shows the address of the cell matched by INDEX).
Copy the result of the formula above.
Hit F5, Ctrl-V, Enter (paste the copied address in the GoTo dialog).
You are now located on the very cell found by the INDEX function.
Now the challenge is to automate these steps (or similar ones) with a macro.
Tentative macros (not working)
Tentative 1
WorksheetFunction.CELL("address", ActiveCell.Formula)
It doesn't work since CELL for some reason is not part of the members of WorksheetFunction.
Tentative 2
This method involves parsing the INDEX-formula.
Sub GoToIndex()
Dim form As String, rng As String, row As String, col As String
form = ActiveCell.Formula
form = Split(form, "(")(1)
rng = Split(form, ",")(0)
row = Split(form, ",")(1)
col = Split(Split(form, ",")(2), ")")(0)
Range(rng).Cells(row, CInt(col)).Select
End Sub
This method actually works, but only for a simple case, where the main INDEX-formula has no nested subformulas.
Note
Obviously in a real case myrange, x and ycan be both simple values, such as =INDEX(A1:D10, 1,1), or values returned from complex expressions. Typically x, y are the results of a MATCH function.
EDIT
It was discovered that some solutions do not work when myrange is located on a sheet different from that hosting =INDEX(myrange ...).
They are common practice in financial reporting, where some sheets have the main statements whose entries are recalled from others via an INDEX+MATCH formula.
Unfortunately it is just when the found value is located on a "far" report out of sight that you need more the jump-to-the-cell function.
The task could be done in one line much simpler than any other method:
Sub GoToIndex()
Application.Evaluate(ActiveCell.Formula).Select
End Sub
Application.Evaluate(ActiveCell.Formula) returns a range object from which the CELL function gets properties when called from sheets.
EDIT
For navigating from another sheet you should first activate the target sheet:
Option Explicit
Sub GoToIndex()
Dim r As Range
Set r = Application.Evaluate(ActiveCell.Formula)
r.Worksheet.Activate
r.Select
End Sub
Add error handling for a general case:
Option Explicit
Sub GoToIndex()
Dim r As Range
On Error Resume Next ' errors off
Set r = Application.Evaluate(ActiveCell.Formula) ' will work only if the result is a range
On Error GoTo 0 ' errors on
If Not (r Is Nothing) Then
r.Worksheet.Activate
r.Select
End If
End Sub
There are several approaches to select the cell that a formula refers to...
Assume the active cell contains: =INDEX(myrange,x,y).
From the Worksheet, you could try any of these:
Copy the formula from the formula bar and paste into the name box (to the left of the formula bar)
Define the formula as a name, say A. Then type A into the Goto box or (name box)
Insert hyperlink > Existing File or Web page > Address: #INDEX(myrange,x,y)
Adapt the formula to make it a hyperlink: =HYPERLINK("#INDEX(myrange,x,y)")
Or from the VBA editor, either of these should do the trick:
Application.Goto Activecell.FormulaR1C1
Range(Activecell.Formula).Select
Additional Note:
If the cell contains a formula that refers to relative references such as =INDEX(A:A,ROW(),1) the last of these would need some tweaking. (Also see: Excel Evaluate formula error). To allow for this you could try:
Range(Evaluate("cell(""address""," & Mid(ActiveCell.Formula, 2) & ")")).Select
This problem doesn't seem to occur with R1C1 references used in Application.Goto or:
ThisWorkbook.FollowHyperlink "#" & mid(ActiveCell.FormulaR1C1,2)
You could use the MATCH() worksheet function or the VBA FIND() method.
EDIT#1
As you correctly pointed out, INDEX will return a value that may appear many times within the range, but INDEX will always return a value from some fixed spot, say
=INDEX(A1:K100,3,7)
will always give the value in cell G3 so the address is "builtin" to the formula
If, however, we have something like:
=INDEX(A1:K100,Z100,Z101)
Then we would require a macro to parse the formula and evaluate the arguments.
Both #lori_m and #V.B. gave brilliant solutions in their own way almost in parallel.
Very difficult for me to choose the closing answer, but V.B. even created Dropbox test file, so...
Here I just steal the best from parts from them.
'Move to cell found by Index()
Sub GoToIndex()
On Error GoTo ErrorHandler
Application.Goto ActiveCell.FormulaR1C1 ' will work only if the result is a range
Exit Sub
ErrorHandler:
MsgBox ("Active cell does not evaluate to a range")
End Sub
I associated this "jump" macro with CTRL-j and it works like a charm.
If you use balance sheet like worksheets (where INDEX-formulas, selecting entries from other sheets, are very common), I really suggest you to try it.

VBA application.match error 2015

In my Main procedure I want to write a quick if-statement which checks whether the user has made a valid input (user chooses number of project from list of data, see attached screenshot). For that I am checking whether the project number is not part of the list of projects. If that is true, an error message is displayed; if not then a number of other procedures are called.
For some reason though I get error 2015 when I run it, which means that the if-statement is always true, even on correct user entries. Can someone help me understand the error please?
The project number input is a named cell called "IdSelect" and is on a sheet called "Invoice"
The data against which this input is checked is on a sheet called "Input"
The data is stored in column B and called "ProjectList"
Code below (note: I have tried pasting it 5 times but the formatting still won't work this time for some reason - any idea what that could be? The code is properly formatted. Sorry for the messy display; if anyone can tell me what that problem might I would be very grateful!)
Sub Main()
'Turn off screen updating
Application.ScreenUpdating = False
'Define variable for currently active cell to reactivate it afterwards
Dim OldActiveSheet As Object
Dim OldActiveCell As Object
Dim i As Integer
Dim ProjectList As Range
Set OldActiveSheet = ActiveSheet
Set OldActiveCell = ActiveCell
'If-statement to check whether project number is valid or not
Worksheets("Invoice").Activate
'Print to Immediate Window to check value - remove later
Debug.Print Range("IdSelect").Value
If IsError(Application.Match(Range("IdSelect").Value, "ProjectList", 0)) Then
'Print to Immediate Window to check value - remove later
Debug.Print Application.Match(Range("IdSelect").Value, Worksheets("Input").Range("ProjectList"), 0)
MsgBox "Invalid Choice: Project with this number does not exist!"
Exit Sub
Else
'Call procedures to execute
Call SortData
Call Count_Line_Items
Call Count_Total_Rows
Call Write_Services(ServCnt)
Call Write_Expenses(ExpCnt)
End If
'Reactivate previous active cell
OldActiveSheet.Activate
OldActiveCell.Activate
End Sub
Screenshot from "Input" sheet:
The way you refer to range is rather odd.. because you missed out range reference. Oddly enoughbthat you do it correct on the next line at
Debug.Print Application.Match(Range("IdSelect").Value, Worksheets("Input").Range("ProjectList"), 0)
So try this please: (it take me 100 years to format my own post on mobile.....). Make sure to use explicit reference as shown in my sample code below. Set your sheets accordingly.
Dim ws as Worksheet
Set ws = Sheets(1)
IsError(Application.Match(ws.Range("IdSelect").Value, ws.Range("ProjectList"), 0)) Then
And here is for you to read on for error handling on on match.

VBA function to alert when value is changed within a time frame

I have an excel cell where its value is updated every minute.
What I need is an alert message box; while that cell value moves to a particular time.
Suppose a cell has value 10:
if it reaches 7 in one minute then I need a message box to alert that.
if it is not reaching 7 in one minute then I don't need any alert.
Please help write me a macro for this.
Inside VBA editor for ThisWorkBook, you could write the following code
Dim WithEvents SheetToWatch As Worksheet
Private Sub SheetToWatch_Change(ByVal Target As Range)
If Target.Address = "$A$2" Then
If Target.Value = 7 Then
MsgBox "changed to 7"
End If
End If
End Sub
Private Sub Workbook_Open()
Set SheetToWatch = Sheets("Sheet1")
End Sub
Basically the code sets the reference of Sheet1 to the variable SheetToWatch when the workbook opens. Declaring the variable using WithEvents lets one capture the events.
I am using Change event of the worksheet and checking if the cell that was changed is A2. If so, I am checking or comparing the value with 7 (it can be dynamic and depends on how you want to handle it).
See if this helps.
Cannot be done with a function, any way that I can think of.
If you control the mechanism that is updating the cell, then you could it call a VBA subroutine that you wrote, instead, have to send the alert and then update the cell from that routine.
If you do not control the updating mechanism, then the only thing that I can think of that might work is of the Cell, Range or Worksheet classes have a ChangedDate event that you could catch from VBA and do your alerting from there.