What is the difference between 'Range.Parent' and 'Range.Worksheet' in VBA? - vba

The .Parent and .Worksheet properties, when used with a range, seem to reference to the same worksheet object where Range is located.
For example, both of these lines return the same value:
Debug.Print Selection.Parent.Name
Debug.Print Selection.Worksheet.Name
Is there a difference between the two? Are there advantages/disadvantages to each method?

Unless you can guarantee Selection is always part of a Worksheet, it's not said that Selection.Parent.Name will yield the same result as Selection.Worksheet.Name. If it's "in" other types of objects (charts or graphics, for example) the result could be quite different - you'd need to do some testing.
So, Selection.Worksheet.Name is more exact (and, as someone has pointed out in Comments, potentially faster in execution). But if you can't guarantee Selection is going to reference a Worksheet it can trigger an error or yield an unexpected result.

Related

Application.Cells VS Application.ActiveSheet.Cells

The Macro Recorder generated the following statement:
Cells.Select
Now I understand that without the object qualifier this will return all the cells as a Range object.
However, I am wondering what the fully qualified version of this statement is?
Is it:
Application.Cells.Select
Application.ActiveSheet.Cells
Application.ActiveWorkbook.ActiveSheet.Cells
In other words, which one of those fully qualified statements is actually executed by VBE when it runs Cells.Select?
What is the difference between all of these??? As all of these access the same object in the end - is it just personal preference as to which statement I would use if I wanted to explicitly qualify all the objects?
Thank you so much!
It's complicated :)
As all of these access the same object in the end
True. Keywords "in the end". The difference is how many steps it takes to get there...
Unqualified Cells (or Range, Rows, Columns, Names, etc.) aren't magic, they're member calls (Property Get) against a hidden, global-scope object cleverly named Global:
You can validate that this hidden object is involved, by blowing up in a standard module:
Sub GoesBoom()
'throws error 1004 "Method 'Range' of object '_Global' failed"
Debug.Print Range(Sheet2.Cells(1, 1), Sheet3.Cells(1, 1))
End Sub
_Global and Global are closely related - without diving deep into COM, you can consider Global the class, and _Global its interface (it's not really quite like that though - look into "COM coClasses" for more information).
But Cells is a property of the Range class:
I think it's reasonable to presume that Global calls are pretty much all redirected to Application, which exposes all members of Global, and then some.
Now as you noted, Application also have a Cells property - but Cells belong on a Worksheet, so no matter what we do, we need to end up with a Worksheet qualifier... and then any worksheet belongs in a Worksheets collection, which belongs in a Workbook object - so we can infer that an unqualified Cells call would be, in fully-explicit notation, equivalent to... (drumroll):
Application.ActiveWorkbook.ActiveSheet.Cells
But you don't need to be that explicit, because ActiveSheet has a Parent that is always going to be the ActiveWorkbook, so this is also explicit, without going overboard:
ActiveSheet.Cells
But that's all assuming global context. This answer explains everything about it - the gist of it, is that if you're in a worksheet's code-behind, then an unqualified Cells member call isn't Global.Cells, but Me.Cells.
Now, note that Cells returns a Range. Thus, whenever you invoke it against a Range without providing parameters, you're making a redundant member call:
ActiveSheet.Range("A1:B10").Cells ' parameterless Range.Cells is redundant
Let's take the post apart:
Cells.Select
Now I understand that without the object qualifier this will return
all the cells as a Range object.
That's actually somewhat incorrect. While it is true that .Cells returns a Range.Cells object which returns all the cells, Cells.Select is actually a method of the Range object which - as you may have guessed - Selects the range (in our case, all the cells)
The Select method, as per MSDN actually returns a Variant and not a Range object.
That it is a pretty important distinction to make, especially if you plan on passing that value to anything. So if we pretended to be a compiler
Cells -> ActiveWorkbook.ActiveSheet.Range.Cells returns Range of all the cells
Range.Cells.Select -> first we take our returned Range, we then select the cells in Worksheet and actually return a Variant (not Range)
As to the other part of the question. It depends where your module is placed. By default, Cells is shorthand for the following statement:
Application.ActiveWorkbook.ActiveSheet.Range.Cells
This however is subject to change depending on where your module is placed and if Application, workbook or sheet has been modified.
In general, it is a good coding practice to always specify at least a specific Worksheet object whenever you're referencing a Range, eg.
Sheets("Sheet1").Range.Cells
This is explicit and therefore less error prone and clearer to comprehend, be it for you or anyone working with your code.. You always know what exactly you're working with and not leave it to guesswork.
Obviously, the moment you start working with multiple workbooks, it's a good idea to incorporate Workbook objects statements before the Sheet. You get my point.
Last but not least, whatever you're trying to do, it's probably for the best you avoid using Select. It's generally not worth it and prone to unexpected behaviour.
Check this question here: How to avoid using Select in Excel VBA
If you just type Cells - in and of itself it does nothing. It is the same as Range.Cells. The only advantage of Cells is that it can accept numeric value for column (second argument). It's very handy when you do complex manipulations.
Range.Cells just returns Range object. When you have Range object, think of it as a small Excel worksheet. Say, you have range Range("F3:J10"). Then following ranges all refer to H3 cell:
Range("F3:J10").Cells(3)
Range("F3:J10")(3)
Range("F3:J10").Cells(1, 3)
Range("F3:J10")(1, 3)
Range("F3:J10").Cells(1, "C")
Range("F3:J10")(1, "C")
Range("F3:J10").Range("C1")

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

Need help refining my excel macro for deleting blank rows or performing another action

Basically what I'm trying to accomplish is to search the document for blank rows and delete them, if any. This works great if there are blank rows to delete; however, if there are no blank rows, the macro ends with an error. I'd be eternally grateful if someone could advise me how to make this into an "if blank rows then this, if none then that"
Sheets ("xml") .Select
Cells.Select
Selection.SpecialCells(x1CellTypeBlanks).Select
Selection.EntireRow.Delete
Enter my second macro (this part works fine)
Regards
Let me point you to the canonical:
How to avoid using Select/Activate in Excel VBA macros
So you can start to understand why your current code fails or performs undesired operation. What happens when there are no blank cells in your selection? You'll get an error. Why?
Because in that circumstance, Selection.SpecialCells(xlCellTypeBlanks) evaluates to Nothing. (You can verify this using some debug statements) And because Nothing does not have any properties or methods, you'll get an error, because you're really saying:
Nothing.Select
Which is a null program, does not grok, does not compute, etc.
So, you need to test for nothingness with something like this:
Sheets("xml").Select
Cells.Select
If Not Selection.SpecialCells(x1CellTypeBlanks) Is Nothing Then
Selection.SpecialCells(x1CellTypeBlanks).EntireRow.Delete
End If
I still suggest avoiding Select at all costs (it is superfluous about 99% of the time and makes for sloppy code which is difficult to debug and maintain).
So you could do something more complete following that line of thought:
Dim blankCells as Range '## Use a range variable.
'## Assign to your variable:
Set blankCells = Sheets("xml").Cells.SpecialCells(xlCellTypeBlanks)
'## check for nothingness, delete if needed:
If Not blankCells Is Nothing then blankCells.EntireRow.Delete
Follow-up from comments
So in VBA we are able to declare variables which represent objects or data/values, much like a maths variable in an equation.
A Range is a type of object part of the Excel object model, which consists of the Workbook/Worksheets/Cells/Ranges/etc. (far more than I could hope to convey to you, here)
http://msdn.microsoft.com/en-us/library/office/ff846392(v=office.14).aspx
A good example of why to use variables might be here if you scroll down to the "Why Use Variables" section.
http://www.ozgrid.com/VBA/variables.htm
This is of course very simple... but the reader's digest version is that variables allow us to repeatedly refer to the same object (or value for sipmle data types) without explicitly referring to it each time.
THen there is the handy side-effect that the code bcomes more easy to read, maintain and debug, when we use variables instead of absolute references:
Dim rng as Range
Set rng = Sheets(1).Range("A1:Q543").Resize(Application.WorksheetFunction.CountA(Sheets(1).Range("A:A"),))
Imagine that fairly (but not ridiculously) complicated range construct. If you needed to refer to that range more than once in your code, it would be silly not to assign it to a variable, if for no other reason than to save your own sanity from typing (and possibly mistyping a part of it). It is also easy to maintain, since you need only modify the one assignment statement and all subsequent references to rng would reflect that change.

Sort Range and Store Result in VBA Excel

I want to create a VBA function that accept a range, a key, and index and return a value based on it's index position.
The code look like this.
Function SortRange(datarange As Range, mycolumn As Range, position)
Dim theResult As Range
Set theResult = hasil.Sort(Key1:=kolom, order1:=xlAscending, Header:=xlNo)
SortRange= position.Cells(nomorurut, 1)
End Function
However, I always got #VALUE result. What's wrong with the code?
There appears to be a lot wrong here, or at least a lot of potentially wrong things.
First and most importantly, if you are calling this as a worksheet function, it will never work. UDFs cannot manipulate the worksheet object (the .Sort method manipulates the sheet).
Also while you indicate the function accepts a range, a key (string?) and index (integer/long?), your function is accepting two range arguments and a third un-typed variant which appears to be a worksheet Object based on the later call to position.Cells. So, the function does not appear to be accepting the arguments you expect it to accept, which may cause mismatch errors.
The following are also potential errors:
If position is not a worksheet object, you'll get an error (object does not support this property or method)
If hasil is not a global variable, or has not been instantiated, you'll get an error (object variable or with block not set)
It is not entirely clear what you are trying to do, but probably the VLOOKUP, INDEX and/or MATCH functions already will accomplish what you are attempting.

Breaking down evaluate in vba

I've been searching online trying to figure out what the use of evaluate is. What I get from msdn is: " An expression that returns an object in the Applies To list." I have no clue what this means.
The reason I ask is because I've been given a piece of code and I'm trying to make some logical sense out of it. Why is it written this way? What is the advantage of using evaluate instead of a more traditional approach? What is the correct syntax of a long line of nested functions?
Here is my code example:
With Range("B1", Cells(Rows.Count, "B").End(xlUp))
.Value = Evaluate("Index(If(Left(Trim(" & .Address & "),1)=""."",Replace(Trim(" & .Address & "),1,1,""""),Trim(" & .Address & ")),)")
End With
Can someone help me break this down and make some sense out of it? It is supposed to remove leading periods and get rid of excess spaces in all cells in column b. My problem is that it only works if I run it twice. If I could make some sense out of this then I may be able to manipulate it to make it function correctly.
For extra credit, How would I build a statement like this if I wanted to go through the same range and remove all dashes ("-")?
I really want to learn. Any help appreciated.
OK here goes:
Evaluate tells the application to evaluate a function in this context.
The function is a string concatenation of "Index(If(Left(... which includes some dynamic components (.Address), because it's being applied to the entire range:
Range("B1", Cells(Rows.Count, "B").End(xlUp))
What this does, effectively, is to evaluate the formula for each cell in that range, but only writes the formula's evaluated value to each cell.
Equivalent would be to fill the range with the formula, and then do a copy + paste-special (values only) to the range. This is obviously more expensive in terms of memory and time consuming processes, especially for a larger range object.
I personally don't favor this approach, but that's a matter of my personal preference primarily.
Advantages in this case is that it's filling in an entire range of cells -- which could be 10 cells or 10,000 cells or 1,048,576 cells (in Excel 2007+) in one statement.
Alternative methods would be to do a For/Next loop (which is expensive in terms of memory and therefore slow on large ranges), even if you're doing a loop over an array in memory and writing the resulting array to the worksheet, I think there is a certain elegance to using a single statement like this code.