Iterating through all the cells in Excel VBA or VSTO 2005 - vba

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

Related

Excel VBA Get Count of Non-Empty Rows in Range

I know this has been asked at least a dozen times, but of all those questions the solutions seemed to be tailored or simply created an error in my code. For some reason I get an error when I try and reference a range from a different worksheet. Below is my code. I have two worksheets (tabs). One has a button to launch my code MENU, the other is the data I am trying to read and write to RAW. All I am trying to do is find out how many rows of data the sheet has so I can loop through the rows and make changes. For some reason I can't wrap my head around it or something.
Private Sub CommandButton21_Click()
Dim wbCurrent As Workbook
Dim wsCurrent As Worksheet
Set wbCurrent = ThisWorkbook
Set wsCurrent = wbCurrent.Worksheets("RAW")
Dim i As Long
Dim siteCol As Long
siteCol = Range("I" & Rows.Count).End(xlUp).Row
For i = 1 To siteCol
wsCurrent.Range("I" & i) = "MARKED"
Next i
Range("I1") = siteCol
End Sub
1- Always use Long for your variables, not Integer unless you have a specific reason. The maximum value of an Integer is 32767, which might be not enough to hold your count of rows.
2- Dont get the number of rows with COUNTA, this has many drawbacks. use this instead:
siteCol = wsCurrent.Range("I" & wsCurrent.Rows.count).End(xlUp).Row
This finds the Row number of the last occupied cell in wsCurrent, assuming that wsCurrent is at the top of the document (it starts on Row 1). Please note that if wsCurrent is completely empty, this will find the row number of the first occupied cell above wsCurrent, or the first row of the document.
3- When you want to assign a whole range to the same value, you can do it at once, which is much faster and simpler, like this (no loop needed):
wsCurrent.Range("I1:I" & siteCol) = "MARKED"
4- No need to Activate a worksheet to work with it. Drop the statement wsCurrent.Activate and try to always work with qualified ranges.

Not Understanding why I am receiving 'Subscript Out Of Range'

I have been working on this code in which I have a userform that has a mashup of listboxes and comboboxes. So far I have populated the listboxes but for some reason I am having trouble with the comboboxes (combobox1 and combobox2).
I have managed to populate the drop-down list for combobox1, and from that list I want to 'filter' through a named range that is already called out through the 'name manager'. The named range is called Range_Books.
Range_Books references two columns and a variable number of rows in table48 on sheet BOOKS or in VBA code Sheet7. The code below is my latest iteration of attempting to accomplish what I have explained but it still has failed.
I originally was attempting to call out the range directly without the Worksheets("Sheet7"). since the named range is not on a specific sheet, but I am still not sure which is the best way to call out the range and if that is the root of my problem. I have called out the range directly without the worksheets(" ") before which is why I am so perplexed by this.
It may be important to note that when the userform is initialized, it opens a secondary workbook in order to populate the listboxes. After initialization, various actions may be done before a value is chosen for combobox1, and thus activating the function I am trying to create. This secondary workbook stays open until the userform is closed. I mention this because I am unsure if the secondary workbook is causing issues with the range object. I have been receiving trouble from VBA since adding the opening of a secondary workbook functionality to the userform.
Private Sub ComboBox1_Change()
Dim count As Integer
Dim i As Integer
count = Worksheets("Sheet7").Range("Range_Books").Rows.count
For i = 0 To count
If Worksheets("Sheet7").Range("Range_Books").Cells(i, 1) = ComboBox1.Value Then
ComboBox2.AddItem (Worksheets("Sheet7").Range("Range_Books").Cells(i, 2))
End If
Next i
End Sub
You need to either start with For i = 1 to count, or change the ranges to .Cells(i+1,1)...
Also, make sure you're referring to the correct sheet. I think this is where the crux of your issue is.
If your named range is in a worksheet with the tab name "Books", then you need to instead use count = Worksheets("Books").Range("Range_Books").Rows.count
If you want to use the "Sheet7" reference instead, you could use count = Sheet7.Range("Range_Books").Rows.count
For i = 0 To count
...
Cells(i, 1)
at this point, i = 0. Row 0 Doesn't exist.
Change i = 0 to i = 1
Use this
Private Sub ComboBox1_Change()
Dim count As Integer
Dim i As Integer
Dim ws As WorkSheet
Set ws = Sheets("Sheet7")
count = ws.Range("Range_Books").Rows.count
For i = 1 To count
If Worksheets("Sheet7").Range("Range_Books").Cells(i, 1) = ComboBox1.Value Then
ComboBox2.AddItem (Worksheets("Sheet7").Range("Range_Books").Cells(i, 2))
End If
Next i
End Sub
Thank you everyone for the help, it is very much appreciated! My final working code is below. I changed all instances of Worksheets("Sheet7") to just Sheet7. I attached a picture of the Excel Objects folder tree, as you can see I have Sheet7 which I named "Books". My confusion was that Worksheets(" ") calls out the name I assign rather than the VBA assigned name for the sheet. I also added ComboBox2.Clear that way whenever ComboBox1 changes it resets the values rather then stacking them. I hope this helps somebody in the future and thanks again to the commentors who helped me!
enter image description here
Private Sub ComboBox1_Change()
ComboBox2.Clear
Dim count As Integer
Dim i As Integer
count = Sheet7.Range("Range_Books").Rows.count
For i = 1 To count
If Sheet7.Range("Range_Books").Cells(i, 1) = ComboBox1.Value Then
ComboBox2.AddItem (Sheet7.Range("Range_Books").Cells(i, 2))
End If
Next i
End Sub

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

VBA- How to change compare cells to compare rows

I would like to change the following code to compare entire rows instead of individual cells. I'm a beginner at vba so please explain in simple terms.
Sub RunCompare()
Call compareSheets("Sheet1", "Sheet2")
End Sub
Sub compareSheets(shtBefore As String, shtAfter As String)
'Compares sheets by cells and highlight difference
Dim MyCell As Range
Dim mydiffs As Integer
For Each MyCell In ActiveWorkbook.Worksheets(shtAfter).UsedRange
If Not MyCell.Value = ActiveWorkbook.Worksheets(shtBefore).Cells(MyCell.Row, MyCell.Column).Value Then
MyCell.Interior.Color = vbYellow
mydiffs = mydiffs + 1
End If
Next
MsgBox mydiffs & " differences found", vbInformation
ActiveWorkbook.Sheets(shtAfter).Select
End Sub
The Range object is a strange beast in Excel and it can take some getting used to its various characteristics.
The phrase
ActiveWorkbook.Worksheets(shtAfter).UsedRange
delivers a Range object and when you use the loop
For Each MyCell In ActiveWorkbook.Worksheets(shtAfter).UsedRange
what you are actually doing is implicitly relying on the Cells property of the Range object to deliver an object that contains all the cells in that range. Excel's help system (in version Office 2010, at least) also indicates this latter object is a Range object and I suspect this is a source of confusion amongst beginners, because each of the cells is also a Range object in its own right (so the Cells property of a Range delivers an object which is also a Range though different from its parent and which has "elements" each of which is a Range)
The loop above is really a shorthand form of
For Each MyCell In ActiveWorkbook.Worksheets(shtAfter).UsedRange.Cells
The Range object has many properties, one of which is the Rows property. The phrase
ActiveWorkbook.Worksheets(shtAfter).UsedRange.Rows
delivers an object that contains the separate rows of your UsedRange and you can then use a loop such as
For each myRow in ActiveWorkbook.Worksheets(shtAfter).UsedRange.Rows
to look at each row in turn. Here myRow is also a Range object. Again, perhaps confusingly, the Rows property also delivers a Range object which contains "elements", each of which is also a Range object.
Unfortunately, you cannot implicitly rely on the Cells property with the myRow object to loop over the individual cells within each row. So
For each myCell in myRow
doesn't work as you'd hope but by explicitly adding the Cells property
For each myCell in myRow.Cells
does.
In summary, you can achieve your row by row comparison by using two loops: the first for the rows (based on the Rows property) and a second, nested inside the first, for the cells within a row (based on the Cells property).
As an aside, you can do much of what you want without using VBA at all. Array formulas in Excel (the ones that require CTL+SHIFT+ENTER when entered from the formula bar) can compare two arrays. For example, the array formula
{=AND(Sheet1!A1:Z1=Sheet2!A1:Z1)}
tells whether the range A1:Z1 is the same on two different worksheets and there are other array formulas which can be used to count the number of differences between two ranges.
If you want to highlight the differences between cells in two worksheets use conditional formatting. The trick here is to set the conditional formatting using a formula on the first cell and then to copy this formatting to the other cells. So set the conditional formatting for cell Sheet2!A1 using the formula
=Sheet1!A1<>Sheet2!A1
Make sure that the formula uses relative A1 rather than absolute $A$1 cell addresses (editting the formula if necessary) and then Copy the format (using Paste Special Format) from cell Sheet2!A1 to the rest of the cells on Sheet2.
There is no way to compare row. You can improve your current method.
1. Set mydiffs to long type (vba initial value with long, so no need convert to integer)
2. Add Application.ScreenUpdating = False to enhance the script performance.
Sub RunCompare()
Call compareSheets("Sheet1", "Sheet2")
End Sub
Sub compareSheets(shtBefore As String, shtAfter As String)
'Compares sheets by cells and highlight difference
Dim MyCell As Range
Dim mydiffs As Long
Application.ScreenUpdating = False
For Each MyCell In ActiveWorkbook.Worksheets(shtAfter).UsedRange
If Not MyCell.Value = ActiveWorkbook.Worksheets(shtBefore).Cells(MyCell.Row, MyCell.Column).Value Then
MyCell.Interior.Color = vbYellow
mydiffs = mydiffs + 1
End If
Next
MsgBox mydiffs & " differences found", vbInformation
ActiveWorkbook.Sheets(shtAfter).Select
Application.ScreenUpdating = True
End Sub

Determine number of columns in printed page

In Excel 2007, I have a worksheet that only contains data in a only few cells (well within one page wide/tall). For illustration, say the worksheet only contains data in cell A1. How can I use VBA to determine the number of columns that fit in a single printed page? Said another way, how can I determine the column furthest to the right in which I could add data and NOT cause an additional sheet to print. A couple of additional comments:
I am not setting a print area. If I were, then I'd just use the same
range...but I'm not.
I can't use UsedRange, because the used range is much smaller than
what actually fits in the width/height of a printed page.
I can't use ActiveWindow.VisibleRange because it isn't limited to a
single page width/height.
I've searched and searched, but cannot find a solution to accomplish this seemingly simple task. I mostly found scenarios that involved UsedRange, VisibleRange, and the print area, but those don't help me.
EDIT
Here's the final version of the function I'm using, which is a slight tweak of the selected answer.
Public Function GetLastColumnBeforeVPageBreak( _
ByRef ws As Worksheet, _
ByVal aVPageBreakNum As Long) As Long
Dim isMod As Boolean
isMod = False
On Error GoTo ErrorHandler
GetLastColumnBeforeVPageBreak = ws.VPageBreaks(aVPageBreakNum).Location.Column - 1
' If necessary, delete the last column with dummy data and reset UsedRange.
If isMod Then
ws.Cells(ws.Rows.Count, ws.Columns.Count).EntireColumn.Delete
r = ws.UsedRange.Rows.Count
End If
Exit Function
ErrorHandler:
If Err.Number = 9 Then
' Subscript out of range.
' Ensure there is more than one page by putting something in last cell.
isMod = True
ws.Cells(ws.Rows.Count, ws.Columns.Count).Value = 1
Err.Clear
Resume
Else
Err.Raise Err.Number
End If
End Function
I was sure there was a worksheet property around page breaks so I hit F2 in the IDE to open the object browser and searched on pagebreak. A little bit of F1'ing showed there is a Worksheets(1).VPageBreaks(1).Location property that returns a range object. The left side of the range aligns with the 1st vertical page break so:
LastColOnP1 = Worksheets(1).VPageBreaks(1).Location.Column - 1
will give you a variable containing the number of the last column that will print on page 1 of your 1st sheet.
Or within a procedure:
Sub FindFirstVPageBreak()
Dim LastColOnP1 As Long
With ActiveSheet
'Ensure there is more than one page by puting something in last column
.Cells(1, .Columns.Count) = 1
LastColOnP1 = .VPageBreaks(1).Location.Column - 1
'Delete the last column to allow UsedRange to be reset
.Cells(1, .Columns.Count).EntireColumn.Delete
End With
'Save to workbook to commit the reset UsedRange
If Not ActiveWorkbook.ReadOnly Then
ActiveWorkbook.Save 'assumes workbook has been saved previously.
End If
End Sub
You can use Columns(x).ColumnWidth to calculate (iif column contains data). See http://EzineArticles.com/7305778 for a much more detailed solution.