Range() vs Cells() vs Range.Cells() - vba

I know that the Range() and Cells() properties are equivalent methods of accessing cells on a worksheet. However, when is it wise to use Range.Cells() in this combination?
I came across an example where they used Range("A1").Resize(2,3).cells.value.
Would this be equivalent to Range("A1").Resize(2,3).value?
If not, what is the advantage of the former?

Technically, Range and Range.Cells are not equivalent. There is a small but important difference.
However in your particular case, where you
Construct the range with Range("something"), and
Are only interested in the .Value of that range,
it makes no difference at all.
There is a handy clause in VB, For Each, that enumerates all elements in a collection. In the Excel object model, there are convenient properties such as Columns, Rows, or Cells, that return collections of respective cell spans: a collection of columns, a collection of rows, or a collection of cells.
From how the language flows, you would naturally expect that For Each c In SomeRange.Columns would enumerate columns, one at a time, and that For Each r In SomeRange.Rows would enumerate rows, one at a time. And indeed they do just that.
But you can notice that the Columns property returns a Range, and the Rows property also returns a Range. Yet, the former Range would tell the For Each that it's a "collection of columns", and the latter Range would introduce itself as a "collection of rows".
This works because apparently there is a hidden flag inside each instance of the Range class, that decides how this instance of Range will behave inside a For Each.
Querying Cells off a Range makes sure that you get an instance of Range that has the For Each enumeration mode set to "cells". If you are not going to For Each the range to begin with, that difference makes no difference to you.
And even if you did care about the For Each mode, in your particular case Range("A1").Resize(2,3) and Range("A1").Resize(2,3).Cells are the same too, because by default the Range is constructed with enumeration mode of "cells", and Resize does not change the enumeration mode of the range it resizes.
So the only case that I can think of where querying Cells from an already existing Range would make a difference, is when you have a function that accepts a Range as a parameter, you don't know how that Range was constructed, you want to enumerate individual cells in that range, and you want to be sure that it's cells For Each is going to enumerate, not rows or columns:
function DoSomething(byval r as range)
dim c as range
'for each c in r ' Wrong - we don't know what we are going to enumerate
for each c in r.cells ' Make sure we enumerate cells and not rows or columns (or cells sometimes)
...
next
end function

Both
Dim b As Variant
b = Range("A1").Resize(2, 3).Cells.Value
and
Dim c As Variant
c = Range("A1").Resize(2, 3).Value
will return the same array of values. So they are equivalent. No advantage which one you use (one is shorter).
But you can use
Range("A1").Resize(2, 3).Cells(1, 2).Value
to get a specific cell out of that range. So this is the most likely where you need the .Cells on .Range.
Note that the row/column numbers in Cells(1, 2) are relative to the range not the absolute numbers of the worksheet.
So the differences are:
Range("A1:A2") 'range can return multiple cells …
Range("A1") '… or one cell.
Cells(1, 2) 'cells can return one cell or …
Cells '… all cells of the sheet
Range("A1:A2").Cells 'returns all cells of that range and therefore is the same as …
Range("A1:A2") '… which also returns all cells of that range.
Range("C5:C10").Cells(2, 1) 'returns the second row cell of that range which is C6, but …
Cells(2, 1) 'returns the second row cell of the sheet which is A2

Related

Excel VBA Rows.Count reference for a loop

I need to pull a range of data from a sheet where the top 16 rows will always be the same but the data below will vary. I can find the starting cell with this
Sheets("AA").Next.Select
Range("A17").Select
Selection.End(xlDown).Offset(2, 2).Select
and then I want to count the cells using (starting cell selected above) to end of cells containing data. I have tried this
Range(Selection, Range(Selection.End(xlDown)).Rows.Count
and all kinds of variations on it but cannot seem to make it work. I need to be able refer back to both the start cell and the number of rows to pull data from that cell down to the last cell using a loop (another topic I will probably ask questions on when I get that far)
Can someone help?
Your explanation as to why you are trying to do this makes it appear you are placing more work on yourself than you need to.
Let's create a couple functions.
The first function we will call nextBlankCell, which will automatically grab the last cell before an empty range. In your case, we can even use your selection to determine this - which we will do in our next function.
nextBlankCell Function
Function nextBlankCell(ByVal startRng As Range) As Range
Set nextBlankCell = startRng.End(xlDown)
End Function
Next, let's create a function that will automatically set your entire range for you. In this case, it will be the range from your current selection to the last row containing data that we get from using the above function.
getRngDownwards Function
Function getRngDownwards() As Range
Dim celStart As Range, celEnd As Range, ws As Worksheet
Set celStart = Selection
Set ws = celStart.Parent
Set celEnd = nextBlankCell(celStart)
Set getRngDownwards = ws.Range(celStart, celEnd)
End Function
In the above function, we have two ranges celStart and celEnd. celStart is simply your current selection. I always prefer to immediately set your selection to a static range if you must use Selection (most cases it’s not necessary).
celEnd is the range that will contain the last used cell in your column.
We also determine the worksheet ws by using the selection's parent object. Protip: We avoided ActiveSheet!
Now you can put it to the test:
Sub test()
' This test shows you the address of the range
MsgBox getRngDownwards.Address
' This test visually shows you the range
getRngDownwards.Select
End Sub

Excel Selecting a Row

I am Trying to create a macro that will take the 1st Cell of a selection and merge it with the 6th Cell. I am getting error 400.
Sub SignPage()
Dim ws As Worksheet, BasePoint As Range, row As Long, singlecell As Long,
singleCol As Long, sLong As Long, sShort As String
Set BasePoint = Selection
row = BasePoint.row
Range(Cells(row, BasePoint.Columns(1)), Cells(row, BasePoint.Columns(6))).Merge
End Sub
This should be enough:
Selection(1).Resize(, 6).Merge
Explanation: Selection can span several cells, so Selection(1) takes the top left cell - thus, we always refer to one cell.
The point is that you should refer to the first column of Basepoint and take its column. This could be achieved, if you amend your line like to this:
Range(Cells(row,BasePoint.Columns(1).Column),Cells(row,BasePoint.Columns(6).Column)).Merge
As an advice:
do not use names like row, column, Cells, Range etc. as a variable, the Excel object module library inside VBA uses them as well. In your case, you have BasePoint.row. If you did not have the variable row, then the VBEditor would have written BasePoint.Row automatically.
try to avoid Selection, although it depends on the context - How to avoid using Select in Excel VBA
as #Mat's Mug mentioned in the comments, it is a good habit to show the "parent" of Cells and Ranges, when you are using them.
Thus in your case, something like this:
With Worksheets(1)
.Range(.Cells(row, BP.Columns(1).Column), .Cells(row, BP.Columns(6).Column)).Merge
End With
Then it would always refer to the first worksheet (or whichever you need to) and not to the ActiveSheet, which would be referred, if the Parent in not mentioned. (I have written BP instead of BasePoint to make sure it goes on one line.)

In VBA the Rows property has a weird behavior

I am trying to figure out how to work on a specific row among a big range. However it appears that a range created with the rows property does not behave the same as a simple range. Check the following code, the first time the variable SpecificRow is defined, it is not possible to select a specific cell. However with a weird workaround that redefines the range, it works fine. Do you have an idea why and how could I define the range with a more elegant way?
'The following shows the weird behavior of Rows property
Dim SourceRng As Range
Dim SpecificRow As Range
Dim i As Long
i = 3
Set SourceRng = Range("A1:D20")
Set SpecificRow = SourceRng.Rows(i)
'This will show the address of the selected row ("A3:D3")
MsgBox SpecificRow.Address
'Unexplicable behavior of the range when trying to select a specific cell
'where it will instead consider the whole row (within the limits of SourceRng)
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
'This would send an error
'MsgBox SpecificRow(1, 1).Address
'Workaround
Set SpecificRow = Intersect(SpecificRow, SpecificRow)
'The following will select the same address than before
MsgBox SpecificRow.Address
'However, now it has a correct behavior when selecting a specific cell
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
You should expect weird behavior if you're passing indexed properties the incorrect parameters. As demonstrated by your code, the Range returned by SourceRng.Rows(i) is actually correct. It just isn't doing what you think it's doing. The Rows property of a Range just returns a pointer to the exact same Range object that it was called on. You can see that in its typelib definition:
HRESULT _stdcall Rows([out, retval] Range** RHS);
Note that it doesn't take any parameters. The returned Range object is what you're providing the indexing for, and you're indexing it based on it's default property of Item (technically it's _Default, but the 2 are interchangeable). The first parameter (which is the only one you're passing with Rows(i), is RowIndex. So Rows(i) is exactly the same thing as Rows.Item(RowIndex:=i). You can actually see this in the IntelliSense tooltip that pops up when you provide a Row index:
Excel handles the indexing differently on this call though, because providing any value parameter for the second parameter is a Run-time error '1004'. Note that a similar property call is going on when you call SpecificRow(1).Address. Again, the default property of Range is Range.Item(), so you're specifying a row again - not a column. SpecificRow(1).Address is exactly the same thing as SpecificRow.Item(RowIndex:=1).Address.
The oddity in Excel appears to be that the Range returned by Range.Rows "forgets" the fact that it was called within the context of a Rows call and doesn't suppress the column indexer anymore. Remember from the typelib definition above that the object returned is just a pointer back to the original Range object. That means SpecificRow(2) "leaks" out of the narrowed context.
All things considered, I'd say the Excel Rows implementation is somewhat of a hack. Application.Intersect(SpecificRow, SpecificRow) is apparently giving you back a new "hard" Range object, but the last 2 lines of code are not what you should consider "correct" behavior. Again, when you provide only the first parameter to Range.Items, it is declared as the RowIndex:
What appears to happen is that Excel determines that there is only one row in the Range at this point and just assumes that the single parameter passed is a ColumnIndex.
As pointed out by #CallumDA, you can avoid all of this squirrelly behavior by not relying on default properties at all and explicitly providing all of the indexes that you need, i.e.:
Debug.Print SpecificRow.Item(1, 1).Address
'...or...
Debug.Print SpecificRow.Cells(1, 1).Address
This is how I would work with rows and specific cells within those rows. The only real difference is the use of .Cells():
Sub WorkingWithRows()
Dim rng As Range, rngRow As Range
Set rng = Sheet1.Range("A1:C3")
For Each rngRow In rng.Rows
Debug.Print rngRow.Cells(1, 1).Address
Debug.Print rngRow.Cells(1, 2).Address
Debug.Print rngRow.Cells(1, 3).Address
Next rngRow
End Sub
which returns:
$A$1
$B$1
$C$1
$A$2
$B$2
$C$2
$A$3
$B$3
$C$3
As you would expect
I cannot find any proper documentation on this, but this observed behaviour actually appears to be very logical.
The Range class in Excel has two important properties:
A single instance of Range is enough to represent any possible range on a sheet
It is iterable (can be used in a For Each loop)
I believe that in order to achieve logically looking iterability and yet avoid creating unnecessary entities (i.e. separate classes like CellsCollection, RowsCollection and ColumnsCollection), the Excel developers came up with a design where each instance of Range holds a private property that tells it in which units it is going to count itself (so that one range could be "a collection of rows" and another range could be "a collection of cells").
This property is set to (say) "rows" when you create a range via the Rows property, to (say) "columns" when you create a range via the Columns property, and to (say) "cells" when you create a range in any other way.
This allows you to do this and not become unnecessarily surprised:
For Each r In SomeRange.Rows
' will iterate through rows
Next
For Each c In SomeRange.Columns
' will iterate through columns
Next
Both Rows and Columns here return the same type, Range, that refers to the exactly same sheet area, and yet the For Each loop iterates via rows in the first case and via columns in the second, as if Rows and Columns returned two different types (RowsCollection and ColumnsCollection).
It makes sense that it was designed this way, because the important property of a For Each loop is that it cannot provide multiple parameters to a Range object in order to fetch the next item (cell, row, or column). In fact, For Each cannot provide any parameters at all, it can only ask "Next one please."
To support that, the Range class had to be able to give the next "something" without parameters, even though a range is two-dimensional and needs two coordinates to fetch the "something." Which is why each instance of Range has to remember in what units it will be counting itself.
A side effect of that design is that it is perfectly fine to look up "somethings" in a Range providing only one coordinate. This is exactly what the For Each mechanism would do, we are just directly jumping to the ith item.
When iterating over (or indexing into) a range returned by Rows, we're going to get the ith row, from top to bottom; for a range returned by Columns we're getting the ith column, from left to right; and for a range returned by Cells or by any other method we're going to get the ith cell, counting from top left corner to the right and then to the bottom.
Another side effect of this design is that can "step out" of a range in a meaningful way. That is, if you have a range of three cells, and you ask for the 4th cell, you still get it, and it will be the cell dictated by the shape of the range and the units it's counting itself in:
Dim r As Range
Set r = Range("A1:C3") ' Contains 9 cells
Debug.Print r.Cells(12).Address ' $C$4 - goes outside of the range but maintains its shape
So your workaround of Set SpecificRow = Intersect(SpecificRow, SpecificRow) resets the internal counting mode of that specific Range instance from (say) "rows" to (say) "cells".
You could have achieved the same with
Set SpecificRow = SpecificRow.Cells
MsgBox SpecificRow(1).Address
But it's better to keep the Cells close to the point of usage rather than the point of range creation:
MsgBox SpecificRow.Cells(1).Address

Pick one cell in range need range.cell when set inside a UDF

I have created a UDF which works pretty well as it is. However, if the second range is not submitted (it is optional) it will be set to a range inside the UDF.
If i now try to return the range of one cell directly i need to add .Cells to the range to select the cell with (x, y).
If no range is submitted it does something like that:
Set optional_range = required_range.Columns(2)
Set required_range = required_range.Columns(1)
If I later in the UDF want to output a cell from optional_range i get this behavior:
Set MyFunction = optional_range(x, y) 'cell shows #VALUE
Set MyFunction = optional_range.Cells(x, y) 'shows correct value
But as said: if optional_range gets a range from the formula directly, it shows the correct value also without the use of .Cells.
I can't find a reason for this behavior at all. Can someone tell me why this happens?
The full code can be found here.
Your error is occurring due to setting optional_range to the Range.Columns property and then attempting to piece it out into individual Range.Cells objects. The Range.Columns and Range.Rows property properties share many methods with a Range object but this is not one of them.
The solution is to simply explicitly set optional_range to the .Cells of .Columns(2).
Set optional_range = required_range.Columns(2).Cells
'optional truncation to the worksheet's .UsedRange
Set optional_range = Intersect(required_range.Parent.UsedRange, required_range.Columns(2))
I have added a way to truncate .Columns(2) down to the Worksheet.UsedRange property. Think of it as SUMIF vs. SUMPRODUCT with full column references; particularly helpful if you plan to loop through the cells in optional_range. No need to add .Cells to this; the Intersect method returns the cells in the intersection as a true Range object.

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