loop through specified sheets in VBA - vba

I am trying to use a bit of code I found here For Each Function, to loop through specifically named worksheets to loop through specified sheets in a workbook, run a small amount of code and move to next sheet.
Sub LoopThroughSheets()
Dim Assets As Worksheet
Dim Asset As Worksheet
Assets = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
For Each Asset In Assets
'my code here
MsgBox ActiveSheet.Name 'test loop
Next Asset
End Sub
This is not looping through the sheets. I tried Dim Assets as Worksheet but this broke the code.
Any help much appreciated,
Cheers

The code you show in your question fails because of:
Assets = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
Assets is a Worksheet which is a type of Object and you must use Set when assigning a value to an Object:
Set Assets = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
This would fail because Array("…") is not a worksheet.
You imply that an earlier version of your code would run but did not loop through the worksheets. The reason is:
MsgBox ActiveSheet.Name
This displays the name of the active worksheet but nothing in this loop changes the active worksheet.
I am not happy with your solution although there is nothing explicitly wrong with it. I have seen too many programs fail because the programmer did too much in a single statement. Firstly, the more complicated a statement, the longer it will take to get right in the first place and the longer it takes to understand during subsequent maintenance. Sometimes the original programmer got the statement slightly wrong; sometimes the maintenance programmer got it wrong when trying to update it. In every case, any saving in runtime was not justified by the extra time spend by the programmers.
Alex K has fixed your code by redefining Assets and Asset as Variants, as required by VBA, and adding Sheets(Asset).Select to change which worksheet is active. I cannot approve of this because Select is a slow statement. In particular, if you do not include Application.ScreenUpdating = False, the duration of your routine can go through the roof as the screen is repainted from each Select.
Before explaining my solutions, some background on Variants.
If I write:
Dim I as Long
I will always be a long integer.
At runtime, the compiler/interpreter does not have to consider what I is when it encounters:
I = I + 5
But suppose I write:
Dim V as Variant
V = 5
V = V + 5
V = "Test"
V = V & " 1"
This is perfectly valid (valid but not sensible) code because a Variant can contain a number, a string or a worksheet. But every time my code accesses V, the interpreter has to check the type of the current contents of V and decide if it is appropriate in the current situation. This is time consuming.
I do not want to discourage you from using Variants when appropriate because they can be incredibly useful but you need to be aware of their overheads.
Next I wish to advocate the use of meaningful and systematic names. I name my variables according to a system that I have used for years. I can look at any of my programs/macros and know what the variables are. This is a real time saver when I need to update a program/macro I wrote 12 or 15 months ago.
I do not like:
Dim Assets As Variant
Assets = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
because "pipe_mat_tables" and so on are not assets; they are the names of worksheets. I would write:
Dim WshtNames As Variant
WshtNames = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
My first offering is:
Option Explicit
Sub Test1()
Dim WshtNames As Variant
Dim WshtNameCrnt As Variant
WshtNames = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
For Each WshtNameCrnt In WshtNames
With Worksheets(WshtNameCrnt)
Debug.Print "Cell B1 of worksheet " & .Name & " contains " & .Range("B1").Value
End With
Next WshtNameCrnt
End Sub
I could have named WshtNameCrnt as WshtName but I was taught that names should differ by at least three characters to avoid using the wrong one and not noticing.
The Array function returns a variant containing an array. The control variable of a For Each statement must be an object or a variant. This is why I have defined WshtNames and WshtNameCrnt as Variants. Note, your solution worked because a worksheet is an object.
I have used With Worksheets(WshtNameCrnt) which means any code before the matching End With can access a component of this worksheet by having a period at the beginning. So .Name and .Range("B1").Value reference Worksheets(WshtNameCrnt) without selecting the worksheet. This is faster and clearer than any alternative.
I have used Debug.Print rather than MsgBox because it is less bother. My code runs without my having to press Return for every worksheet and I have a tidy list in the Immediate Window which I can examine at my leisure. I often have many Debug.Print statements within my code during development which why I have output a sentence rather than just a worksheet name or cell value.
My second offering is:
Sub Test2()
Dim InxW As Long
Dim WshtNames As Variant
WshtNames = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
For InxW = LBound(WshtNames) To UBound(WshtNames)
With Worksheets(WshtNames(InxW))
Debug.Print "Cell B1 of worksheet " & .Name & " contains " & .Range("B1").Value
End With
Next InxW
End Sub
This macro has the same effect as the first. I sometimes find For more convenient than For Each although I can see no advantage either way in this case. Note that I have written LBound(WshtNames) even though the lower bound of WshtNames will always be zero. This is just me being (over? excessively?) precise.
Hope this helps.

Solved it but always happy to hear other methods
Sub loopsheets()
Dim Sh As Worksheet
For Each Sh In Sheets(Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables"))
MsgBox Sh.Range("b1")
Next
End Sub
Cheers

Use variants instead of worksheets.
Array returns a Variant array of string so cant be cast to Worksheet, the Each variable must be a Variant.
Dim Assets As Variant
Dim Asset As Variant
Assets = Array("pipe_mat_tables", "pipe_diam_tables", "pipe_length_tables")
For Each Asset In Assets
'my code here
Sheets(Asset).Select
MsgBox ActiveSheet.Name 'test loop
Next Asset

Related

Fire a macro on formula result change across two workbooks

My problem is quite simple : I have two workbooks (let's say they are wb1 and wb2).
On ws2 of wb2, I have on Range("A1") a formula like ='[wb1.xlsm]ws1'B1 . So, when B1 on ws1 of wb1 changes, A1 on ws2 of wb2 changes too (that's the goal).
My problem is how to fire a macro when the value of A1 on ws2 changes ? Worksheet_Change doesn't fire, and Workbook_SheetChange isn't fitting in this case...
EDIT By the way, Worksheet_Calculate doesn't fit too. Indeed, I don't know where the value on ws2 will change.
Before answering your question I feel compelled to highlight that there are lots of really good reasons to avoid linked workbooks. It always ends in pain, misery, lost data and long wasted hours spent trying to track data back to its source. Rant over, here is how you can ignore my advice.
This code uses the VBA collection object, which is pretty rubbish. VBScript includes the much better dictionary object, which you can use within VBA. I would highly recommend investigating this further...
There are two parts to the code. The first element is run once. It finds and begins tracking every external reference in a given sheet.
' Find all formulas that point to external workbook.
' Store current value.
Sub Initialise()
Dim c As Range ' Used to loop over all cells, looking for external.
' Ready collection for use.
Set ExternalFormula = New Collection
For Each c In [Sheet1].UsedRange
' Check if external, will start: =[
If c.HasFormula And c.Formula Like "=[[]*" Then
' Value added to collection contains key, for later use.
' Collections cannot return keys.
' Dictionaries are better, but require an external reference.
ExternalFormula.Add c.address & "~~~" & c.Value, c.address
End If
Next
End Sub
The next section is included in the Calculate event. Calculate doesn't provide the updated cell address. But using the ExternalFormula collection we can figure out which cell has been updated.
' Check external formula for changes.
Private Sub Worksheet_Calculate()
Dim c As Integer ' Used to loop over forumla.
Dim address As String ' A1 style address of current forumla.
Dim oldValue As String ' Value before any updates.
' Loop over stored values, looking for change.
If ExternalFormula.Count > 0 Then
For c = 1 To ExternalFormula.Count
' Extract address and old value.
address = Split(ExternalFormula.Item(c), "~~~")(0)
oldValue = Split(ExternalFormula.Item(c), "~~~")(1)
' Check for changes.
If [Sheet1].Range(address).Value <> oldValue Then
' Change found.
MsgBox address & " updated", vbInformation
' Update stored value.
ExternalFormula.Remove address
ExternalFormula.Add address & "~~~" & [Sheet1].Range(address).Value, address
End If
Next
End If
End Sub
Using the dictionary object would dramatically reduce the number of lines of code in that function.
Don't forget to declare ExternalFormula at the workbook or worksheet level.
Private ExternalFormula As Collection ' Stores all external forumulas.

VBA EVALUATE() Misbehavior

I have a set of code that seeks out vlookups within formulas within a range of a sheet and calculates them. This worked perfectly and then one morning I came in and it didnt. Offending Code here:
sValue = Application.Evaluate(sVLOOKUP)
If Len(sValue) = 0 Then sValue = "#N/A"
for example
=VLOOKUP(Box,A1:B2,2,0)
in table
A B
Box 1
Car 2
When manually highlighting the VLOOKUP within the in question sheet and calculating all is fine, however, this code always pulls NA because the evaluation always evaluates to ERROR 2042
My question then is what could cause the EVALUATE function to always evaluate to ERROR 2042 (NA) when the vlookup is without a doubt evaluatable?
EDIT
it would seem that the underlying issue is that I am using a form tool to reference the desired sub something to do with
Dim OutputFolder As String, GetBook As String, BookCopy As String, wb As Workbook
OutputFolder = GetFolder("C:\")
GetBook = ActiveWorkbook.Name
BookCopy = OutputFolder & "\Client Copy " & GetBook
ActiveWorkbook.SaveCopyAs BookCopy
Set wb = Workbooks.Open(BookCopy)
Dim wsDataLoad As Worksheet, wsEconomics As Worksheet, wsUI As Worksheet, wsTypeCurves As Worksheet, wsSWM As Worksheet, wsCC As Worksheet, wsSummary As Worksheet
Set wsDataLoad = wb.Sheets("DataLoad")
interaction with the formtool vs running the sub directly alters the desired output of the sub
Application.Evaluate has a counter-part Worksheet.Evaluate - you should use the latter if you want to be sure your string is evaluated in the context of a specific sheet, and doesn't just default to whatever sheet is active when you run your code.
Since you've stated the string sVLOOKUP holds the correct value, this seems to be the likely explanation for what you're seeing.

Using Vlookup to copy and paste data into a separate worksheet using VBA

Alright I'm a beginner with VBA so I need some help. Assuming this is very basic, but here are the steps I am looking at for the code:
-Use Vlookup to find the value "Rec" in column C of Sheet1, and select that row's corresponding value in column D
-Then copy that value from column D in Sheet1 and paste it into the first blank cell in column B of another worksheet titled Sheet2
I've got a basic code that uses Vlookup to find Rec as well as it's corresponding value in column D, then display a msg. The code works fine, and is the following:
Sub BasicFindGSV()
Dim movement_type_code As Variant
Dim total_gsv As Variant
movement_type_code = "Rec"
total_gsv = Application.WorksheetFunction.VLookup(movement_type_code,Sheet1.Range("C2:H25"), 2, False)
MsgBox "GSV is :$" & total_gsv
End Sub
I also have another one that will find the next blank cell in column B Sheet2, it works as well:
Sub SelectFirstBlankCell()
Dim Sheet2 As Worksheet
Set Sheet2 = ActiveSheet
For Each cell In Sheet2.Columns(2).Cells
If IsEmpty(cell) = True Then cell.Select: Exit For
Next cell
End Sub
Not sure how to integrate the two, and I'm not sure how to make the code paste the Vlookup result in Sheet2. Any help would be greatly appreciated, thanks!
So for being a beginner you're off to a good start by designing two separate subroutines that you can confirm work and then integrating. That's the basic approach that will save you headache after headache when things get more complicated. So to answer your direct question on how to integrate the two, I'd recommend doing something like this
Sub BasicFindGSV()
Dim movement_type_code As Variant
Dim total_gsv As Variant
movement_type_code = "Rec"
total_gsv = Application.WorksheetFunction.VLookup(movement_type_code, Sheet1.Range("C2:H25"), 2, False)
AssignValueToBlankCell (total_gsv)
End Sub
Sub AssignValueToBlankCell(ByVal v As Variant)
Dim Sheet2 As Worksheet
Set Sheet2 = ActiveSheet
For Each cell In Sheet2.Columns(2).Cells
If IsEmpty(cell) = True Then cell.Value2 = v
Next cell
End Sub
That being said, as Macro Man points out, you can knock out the exact same functionality your asking for with a one liner. Keeping the operational steps separate (so actually a two liner now) would look like this.
Sub FindGSV()
AssignValueToBlankCell WorksheetFunction.VLookup("Rec", Sheet1.Range("C2:H25"), 2, False)
End Sub
Sub AssignValueToBlankCell(ByVal v As Variant)
Sheet3.Range("B" & Rows.Count).End(xlUp).Offset(1, 0).Value2 = v
End Sub
Like I said, if you plan to continue development with this, it's usually a good idea to design your code with independent operations the way you already have begun to. You can build off of this by passing worksheets, ranges, columns, or other useful parameters as arguments to a predefined task or subroutine.
Also, notice that I use Value2 instead of Value. I notice you're retrieving a currency value, so there's actually a small difference between the two. Value2 gives you the more accurate number behind a currency formatted value (although probably unnecessary) and is also faster (although probably negligible in this case). Just something to be aware of though.
Also, I noticed your use of worksheet objects kind of strange, so I thought it'd help to mentioned that you can select a worksheet object by it's object name, it's name property (with sheets() or worksheets()), index number (with sheets() or worksheets()), or the "Active" prefix. It's important to note that what you're doing in your one subroutine is reassigning the reference of the Sheet2 object to your active sheet, which means it may end up being any sheet. This demonstrates the issue:
Sub SheetSelectDemo()
Dim Sheet2 As Worksheet
Set Sheet2 = Sheets(1)
MsgBox "The sheet object named Sheet2 has a name property equal to " & Worksheets(Sheet2.Name).Name & " and has an index of " & Worksheets(Sheet2.Index).Index & "."
End Sub
You can view and change the name of a sheet object, as well as it's name property (which is different) here...
The name property is what you see and change in the worksheet tab in Excel, but once again this is not the same as the object name. You can also change these things programmatically.
Try this:
Sub MacroMan()
Range("B" & Rows.Count).End(xlUp).Offset(1, 0).Value = _
WorksheetFunction.VLookup("Rec", Sheet1.Range("C2:H25"), 2, False)
End Sub
The Range("B" & Rows.Count).End(xlUp) command is the equivalent of going to the last cell in column B and pressing Ctrl + ↑
We then use .Offset(1, 0) to get the cell after this (the next blank one) and write the value of your vlookup directly into this cell.
If Both work, then good, you have two working subs and you want to integrate them. You probably want to keep them so they might be useful for some other work later. Integrating them means invoking them in some third routine.
For many reasons, it is surely better and advised to avoid as much as possible to use (select, copy, paste) in VBA, and to use rather a direct copying method (range1.copy range2).
You need to make your routines as functions that return ranges objects, then in some third routine, invoke them
Function total_gsv() as range
Dim movement_type_code As Variant: movement_type_code = "Rec"
Set total_gsv = Application.WorksheetFunction.VLookup(movement_type_code,Sheet1.Range("C2:H25"), 2, False)
End Sub
Function FindFirstBlankCell() as Range
Dim Sheet2 As Worksheet: Set Sheet2 = ActiveSheet
For Each cell In Sheet2.Columns(2).Cells
If IsEmpty(cell) Then Set FindFirstBlankCell= cell: exit For
Next cell
End Sub
Sub FindAndMoveGsv()
total_gsv.copy FindFirstBlankCell
... 'some other work
End Sub

How to refer to with object

How can i refer to the object i use inside With if i want the object itself, not its properties / methods?
With ThisWorkbook.Sheets("MySheet")
Call MySub(ThisWorkbook.Sheets("MySheet")) ' works OK, but duplicated
Call MySub(this) ' does not works
.Range(...).Value2 = 1
...
End With
+ what is the correct terminology here? i don't even know how to compose a google query for this and get some usefull results (since with is a common word)...
UPDATE: to clarify, i was thinking in terms of a handle like with ... as handle from python syntax, not about object-oriented this keyword
How about by not using with in the first place? It makes your code much more readable, uses no more memory (as the with statement has to allocate a temporary variable anyway), and is less confusing.
Dim WS as WorkSheet
WS = ThisWorkBook.Sheets("MySheet")
Call vymaz_obrazky(WS)
WS.Range(...).Value2 = 1
In the code above, the total cost is one additional line of code (the DIM statement), and 9 less keystrokes overall. (The DIM statement is 19 keystrokes, changing to WS in the three lines is 6 keystrokes, but you've saved the with (4) and duplication (30), saving about 9 keystrokes.)
Try this
Sub Sample()
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("MySheet")
With ws
MySub ws
'~~> Rest of the code
End With
End Sub
or
Sub Sample()
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("MySheet")
MySub ws
With ws
'~~> Rest of the code
End With
End Sub
Edit:
do you have any info about non-existence of "this"? – deathApril 19 mins ago
this is basically a keyword from C# which refers to the current instance of the class. The equivalent of this in VB is Me.
The Me keyword provides a way to refer to the specific instance of a class or structure in which the code is currently executing. For example in a Userform you can use
Me.textBox1.Text = "Blah Blah"
In VBA, Me can also be used for thisworkbook. For example, if you paste this code in the ThisWorkbook code Area then it will give you the name of the workbook
Sub Sample()
Debug.Print Me.Name
End Sub
Similarly when you run the above code from the Sheet Code Area you will get the Sheet Name.
HTH
Use .Cells.Parent. This only works for worksheets, but there are similar things for some other objects (for a workbook you can use .Sheets.Parent)
With ThisWorkbook.Sheets("MySheet")
Call MySub(.Cells.Parent)
.Range(...).Value2 = 1
...
End With

Iterating through all the cells in Excel VBA or VSTO 2005

I need to simply go through all the cells in a Excel Spreadsheet and check the values in the cells. The cells may contain text, numbers or be blank. I am not very familiar / comfortable working with the concept of 'Range'. Therefore, any sample codes would be greatly appreciated. (I did try to google it, but the code snippets I found didn't quite do what I needed)
Thank you.
If you only need to look at the cells that are in use you can use:
sub IterateCells()
For Each Cell in ActiveSheet.UsedRange.Cells
'do some stuff
Next
End Sub
that will hit everything in the range from A1 to the last cell with data (the bottom right-most cell)
Sub CheckValues1()
Dim rwIndex As Integer
Dim colIndex As Integer
For rwIndex = 1 To 10
For colIndex = 1 To 5
If Cells(rwIndex, colIndex).Value <> 0 Then _
Cells(rwIndex, colIndex).Value = 0
Next colIndex
Next rwIndex
End Sub
Found this snippet on http://www.java2s.com/Code/VBA-Excel-Access-Word/Excel/Checksvaluesinarange10rowsby5columns.htm It seems to be quite useful as a function to illustrate the means to check values in cells in an ordered fashion.
Just imagine it as being a 2d Array of sorts and apply the same logic to loop through cells.
If you're just looking at values of cells you can store the values in an array of variant type. It seems that getting the value of an element in an array can be much faster than interacting with Excel, so you can see some difference in performance using an array of all cell values compared to repeatedly getting single cells.
Dim ValArray as Variant
ValArray = Range("A1:IV" & Rows.Count).Value
Then you can get a cell value just by checking ValArray( row , column )
You can use a For Each to iterate through all the cells in a defined range.
Public Sub IterateThroughRange()
Dim wb As Workbook
Dim ws As Worksheet
Dim rng As Range
Dim cell As Range
Set wb = Application.Workbooks(1)
Set ws = wb.Sheets(1)
Set rng = ws.Range("A1", "C3")
For Each cell In rng.Cells
cell.Value = cell.Address
Next cell
End Sub
For a VB or C# app, one way to do this is by using Office Interop. This depends on which version of Excel you're working with.
For Excel 2003, this MSDN article is a good place to start.
Understanding the Excel Object Model from a Visual Studio 2005 Developer's Perspective
You'll basically need to do the following:
Start the Excel application.
Open the Excel workbook.
Retrieve the worksheet from the workbook by name or index.
Iterate through all the Cells in the worksheet which were retrieved as a range.
Sample (untested) code excerpt below for the last step.
Excel.Range allCellsRng;
string lowerRightCell = "IV65536";
allCellsRng = ws.get_Range("A1", lowerRightCell).Cells;
foreach (Range cell in allCellsRng)
{
if (null == cell.Value2 || isBlank(cell.Value2))
{
// Do something.
}
else if (isText(cell.Value2))
{
// Do something.
}
else if (isNumeric(cell.Value2))
{
// Do something.
}
}
For Excel 2007, try this MSDN reference.
There are several methods to accomplish this, each of which has advantages and disadvantages; First and foremost, you're going to need to have an instance of a Worksheet object, Application.ActiveSheet works if you just want the one the user is looking at.
The Worksheet object has three properties that can be used to access cell data (Cells, Rows, Columns) and a method that can be used to obtain a block of cell data, (get_Range).
Ranges can be resized and such, but you may need to use the properties mentioned above to find out where the boundaries of your data are. The advantage to a Range becomes apparent when you are working with large amounts of data because VSTO add-ins are hosted outside the boundaries of the Excel application itself, so all calls to Excel have to be passed through a layer with overhead; obtaining a Range allows you to get/set all of the data you want in one call which can have huge performance benefits, but it requires you to use explicit details rather than iterating through each entry.
This MSDN forum post shows a VB.Net developer asking a question about getting the results of a Range as an array
You basically can loop over a Range
Get a sheet
myWs = (Worksheet)MyWb.Worksheets[1];
Get the Range you're interested in If you really want to check every cell use Excel's limits
The Excel 2007 "Big Grid" increases
the maximum number of rows per
worksheet from 65,536 to over 1
million, and the number of columns
from 256 (IV) to 16,384 (XFD).
from here http://msdn.microsoft.com/en-us/library/aa730921.aspx#Office2007excelPerf_BigGridIncreasedLimitsExcel
and then loop over the range
Range myBigRange = myWs.get_Range("A1", "A256");
string myValue;
foreach(Range myCell in myBigRange )
{
myValue = myCell.Value2.ToString();
}
In Excel VBA, this function will give you the content of any cell in any worksheet.
Function getCellContent(Byref ws As Worksheet, ByVal rowindex As Integer, ByVal colindex As Integer) as String
getCellContent = CStr(ws.Cells(rowindex, colindex))
End Function
So if you want to check the value of cells, just put the function in a loop, give it the reference to the worksheet you want and the row index and column index of the cell. Row index and column index both start from 1, meaning that cell A1 will be ws.Cells(1,1) and so on.
My VBA skills are a little rusty, but this is the general idea of what I'd do.
The easiest way to do this would be to iterate through a loop for every column:
public sub CellProcessing()
on error goto errHandler
dim MAX_ROW as Integer 'how many rows in the spreadsheet
dim i as Integer
dim cols as String
for i = 1 to MAX_ROW
'perform checks on the cell here
'access the cell with Range("A" & i) to get cell A1 where i = 1
next i
exitHandler:
exit sub
errHandler:
msgbox "Error " & err.Number & ": " & err.Description
resume exitHandler
end sub
it seems that the color syntax highlighting doesn't like vba, but hopefully this will help somewhat (at least give you a starting point to work from).
Brisketeer