How can I test if a selection is completely within a range? - vba

So i have found this which is similar:
VBA test if cell is in a range
but this seems to be testing (as I understand it) if the cells selected intersect the range at all. However I need to find a way to confirm if the selected range is COMPLETELY within the range so that I can restrict the macro to only work inside a specified range of cells.
here is what I've got so far....I name the selected cells as a range (sel_rng) and set them as a variable....then I name the acceptable range as a named range (okay_rng)....then (hopefully....but this is the part I'm still unclear how to pull off) if "sel_rng" lies completely within "okay_rng" I want to grab "sel_rng" and merge it, otherwise throw up an error"
Sub Merge_Cells()
'
' Merge_Cells Macro
Dim selcells As Range
Selection.Name = "sel_rng"
selcells = Range("sel_rng")
Dim okayrng As Integer
okayrng = Range("itemrows").Value + 28
ActiveSheet.Range("C29:C" & okayrng).Select
Selection.Name = "okay_rng"
Range("sel_rng").Select
Selection.Merge
Thoughts anyone?

The intersection of the two ranges will determine if one range is completely within another range.
dim rng1 as range, rng2 as range
set rng1 = range("b2:c3")
set rng2 = range("a1:d4")
'if rng1 is completely within rng2, the intersection's address will be the same as rng1's address
if application.intersect(rng1, rng2).address = rng1.address then
debug.print rng1.address(0, 0) & " is within " & rng2.address(0, 0)
end if
btw, there is the possibility that the intersect could be nothing. You should add error handling for that.

Related

Strange cell addresses behaviour for non-contiguous ranges: VBA

I was trying to answer this question when I came across some bizarre VBA behaviour in Excel. I have written a very simple sub to demonstrate the issue:
Sub debugAddresses(rng As Range)
Debug.Print "Whole range: " & rng.Address
Dim i As Long
For i = 1 To rng.Cells.Count
Debug.Print rng.Cells(i).Address
Next i
End Sub
I loop over each cell in a range object and print its address, simple right?
debugAddresses Range("B2:B3")
' Result as expected:
' >> Whole range: $B$2:$B$3
' >> $B$2
' >> $B$3
However, for non-contiguous ranges I get some strange behaviour:
debugAddresses Range("A1,B2")
' Strange behaviour when getting addresses of individual cells:
' >> Whole range: $A$1,$B$2
' >> $A$1
' >> $A$2
Can anyone shed any light on this please? Specifically why the Cells objects, which can be used for indexing of a contiguous range, seem to just extend the first selected Area.
Edit: It might be worth noting that using a For Each loop through the actual cell range objects gives the expected result*
Sub debugAddresses2(rng As Range)
Debug.Print "Whole range: " & rng.Address
Dim c As Range
For Each c In rng
Debug.Print c.Address
Next c
End Sub
*See my answer for a comment on a more robust solution, as this (apparently) may not always give the expected result
Try using the modified Sub debugAddresses code below:
Sub debugAddresses(rng As Range)
Dim RngA As Range
Dim C As Range
For Each RngA In rng.Areas
For Each C In RngA.Cells
Debug.Print C.Address
Next C
Next RngA
End Sub
Here is your code "fixed". by just adding one more For Loop
Sub debugAddresses(rng As Range)
Debug.Print "Whole range: " & rng.Address
For Each r In rng ' this loops through the range even if separated cells
Dim i As Long
For i = 1 To r.Cells.Count 'changed to r instead of rng
Debug.Print r.Cells(i).Address 'changed to r instead of rng
Next i
Next r
End Sub
So .Range works by entering the cell address ex. "B1", or by using R1C1 meaning row column ex. 1,2.
But you cant use just one R1C1 inside of .Range, since the range here is a span of cells. So to properly use R1C1 in .Range, you have to specify 2 of them.
So .Range("B5:B10") is equal to Range(Cells(5,2),Cells(10,2))
What you did was Specify a Range, then from that created another range using Cells. Very much like offset.
So Range("A1,B2") then adding Cells(1) then Cells(2) adds rows to the first range that is "A1" or offsets.
Sub selector()
Set Rng = Range("A1")
Rng.Select
Rng.Cells(4, 4).Select
End Sub
This offsets 4 colums and 4 rows from A1
It appears that Florent's comment was in the correct direction, and that this method is extending the first Area within the range object.
In a contiguous range (e.g. "A1:B5", "C10:C100") the following method loops over each cell in the given range, rng.
Dim j As Long
For j = 1 To rng.Cells.Count
Debug.Print rng.Cells(j).Address
Next j
However, in non-contiguous ranges it appears that this is equivalent (or shorthand for)
For j = 1 To rng.Cells.Count
Debug.Print rng.Areas(1).Cells(j).Address
Next j
There doesn't appear to be any direct mention of this in the documentation but it is a sensible conclusion to draw by looking in the Locals browser of the VBA editor.
In the range object rng, there is a Cells property which only contains one "Item", which is the first Area. So it's reasonable to assume this one item is what .Cells(j) has access to.
In rng we can also see the Areas property, which contains 2 items (in this example) equal to the number of Areas in my non-contiguous range.
So rng.Cells(j) is accessing the jth element within the first area of rng. Because .Cells() can extend past the original size of rng, we see the addresses listed of cells outside rng.
The solution(s):
Either ensure you directly loop through the range objects within rng using a For Each loop as shown in the question.
Or loop over each area, and then each cell within that area.
The first option is more concise, but Shai points out that to be completely sure, the most robust method is to do the two For Each loops as there may be more complicated edge cases which aren't captured with the single loop.

Setting variables VBA

complete novice here
I started some VBA a few days ago, I have simple question but cant seem to find what I am doing wrong.
I am trying to make a button which will take the coordinates of the active cell and compare them to another worksheet to retrieve a specific value from another table.
I set variables to the active cell column and row, I want to do this so I can later compare these locations to another worksheet and get the value at a specified position on another worksheet.
So far I have written simply what I could find on the internet as I have no formal training.
The msgbox at the end is just to test whether or not it actually picks up the reference.
Sub CommandButton1_Click()
Dim Arow As Range
Dim Acol As Range
Set Arow = Worksheets("Sheet1").Range(ActiveCell.Row)
Set Acol = Worksheets("Sheet1").Range(ActiveCell.Column)
MsgBox (Arow)
End Sub
So far I have error run-time error '1004' Application defined or object defined error highlighting the 4th Row. If anyone could help me solve this or redirect me to some help it would be much appreciated.
I think this won't work, you should put there
Set arow = Worksheets("Sheet1").Range(ActiveCell.Row & ":" & ActiveCell.Row)
Putting there simply number won't work. For the column, you should put there somethong like C:C. For getting letter of column, see this qestion: Function to convert column number to letter?
For more information about Range property, please see official documentation https://msdn.microsoft.com/en-us/library/office/ff836512.aspx.
The thing is, that you have to supply either the address in so called A1 reference, which is "A1", or "$A$1" or name of cell, etc, or you have to supply two Range objects, such as two cells Worksheets("Sheet1").Range(Worksheets("Sheet1").Cells(1,1), Worksheets("Sheet1").Cells(2,2)), which defines area starting with upper-left corner in first parameter and lower right in second parameter.
ActiveCell.Row and ActiveCell.Column returns you some Integer value representing number of row and column, i.e. if you point cell B4, ActiveCell.Row would return 4, and ActiveCell.Column gonna return 2. An Range() property need as an argument whole adress for some range, i.e. Range("C6") or Range("G3:J8").
When you have your column as a number, you can use Cells() property for pointing first and last cell in your range, i.e. Range(Cells(2, 4), Cells(6, 8) would be the same range as Range("D2:H6").
Following this, one of the ways that you can do what you have described is:
Sub CommandButton1_Click()
Dim Rng As Range
Set Rng = Worksheets("Sheet1").Cells(ActiveCell.Row, ActiveCell.Column)
End Sub
Now you have under variable Rng an Range of the same coordinates as ActiveCell, but in Sheet1. You can pass some value into i.e Rng.Value = "Hello World", paste something with Rng.PasteSpecial xlPasteAll etc.
if you want the value from other sheet at the same location as activeCell, use this code,
Private Sub CommandButton1_Click()
valueFromOtherSheet = Sheets("Sheet2").Range(ActiveCell.Address)
MsgBox (valueFromOtherSheet)
End Sub
Like the others have said, it's just about knowing your variable types. This is another way you could achieve what you want
Sub CommandButton1_Click()
Dim Acell As Range
Set Acell = Worksheets("Sheet2").Range(ActiveCell.Address)
MsgBox "Value on ActiveSheet: " & ActiveCell.Value & vbNewLine & _
"Value on Sheet2: " & Acell.Value
End Sub
Thank you everyone for the help and clarification, In the end I was able to come up with some code that seems to do what I need it to.
Private Sub CommandButton1_Click()
Dim cabDate As Range
Dim searchCol As Integer
Dim newindex As Range
Set cabDate = WorksheetFunction.Index(Range("A1:O9999"), ActiveCell.Row, 2)
searchCol = ActiveCell.Column
Set newindex = WorksheetFunction.Index(Worksheets("Deadlines").Range("A1:O9999"), cabDate.Row, searchCol)
MsgBox (newindex)
End Sub
I wasn't aware about conflicting data types so thank you all for the assistance.

Select & Copy Only Non Blank Cells in Excel VBA, Don't Overwrite

I cant seem to find a solution for my application after endless searching. This is what I want to do:
I have cells in one excel sheet that can contain a mixture of dates and empty cells in one column. I want to then select the cells that have only dates and then copy them to a corresponding column in another sheet. They must be pasted in exactly the same order as in the first sheet because there are titles attached to each row. I do get it right with this code:
'Dim i As Long
'For i = 5 To 25
'If Not IsEmpty(Sheets("RMDA").Range("D" & i)) Then _
Sheets("Overview").Range("D" & i) = Sheets("RMDA").Range("D" & i)
'Next i
However, the dates in the first sheet are being updated on a daily basis and it can be that one title has not been updated (on another day) on the first sheet because the user has not checked it yet. If I leave it blank and If I follow the same procedure then it will "overwrite" the date in the second sheet and make the cell blank, which I do not want. I hope I was clear. Can someone please help me?
Regards
You can accomplish this very easily (and with little code) utilizing Excel's built-in AutoFilter and SpecialCells methods.
With Sheets("RMDA").Range("D4:D25")
.AutoFilter 1, "<>"
Dim cel as Range
For Each cel In .SpecialCells(xlCellTypeVisible)
Sheets("Overview").Range("D" & cel.Row).Value = cel.Value
Next
.AutoFilter
End With
you could try something like. This will give you the non blanks from the range, there may be an easier way... hope it helps
Sub x()
Dim rStart As Excel.Range
Dim rBlanks As Excel.Range
Set rStart = ActiveSheet.Range("d1:d30")
Set rBlanks = rStart.SpecialCells(xlCellTypeBlanks)
Dim rFind As Excel.Range
Dim i As Integer
Dim rNonBlanks As Excel.Range
For i = 1 To rStart.Cells.Count
Set rFind = Intersect(rStart.Cells(i), rBlanks)
If Not rFind Is Nothing Then
If rNonBlanks Is Nothing Then
Set rNonBlanks = rFind
Else
Set rNonBlanks = Union(rNonBlanks, rFind)
End If
End If
Next i
End Sub
Just because a cell is blank does not mean that it is actually empty.
Based on your description of the problem I would guess that the cells are not actually empty and that is why blank cells are being copied into the second sheet.
Rather than using the "IsEmpty" function I would count the length of the cell and only copy those which have a length greater than zero
Dim i As Long
For i = 5 To 25
If Len(Trim((Sheets("RMDA").Range("A" & i)))) > 0 Then _
Sheets("Overview").Range("D" & i) = Sheets("RMDA").Range("D" & i)
Next i
Trim removes all spaces from the cell and then Len counts the length of the string in the cell. If this value is greater than zero it is not a blank cell and therefore should be copied.

.End(xlDown) selecting last nonblank cell incorrectly

Writing VBA code to copy a dynamic range to a new worksheet. The code is supposed to first define a range which is the range to be copied. It does this by starting in the upper left hand corner of where the range will begin, then using Range.End(xlDown) to find the last entry. The offset then finds the bottom right hand corner of the range, and the range is set to span from the upper left hand corner to the lower right hand corner. That's how it is supposed to work, and how it does work for a verbatim sub, where the only changes are in the variable names for clarity.
Here's where it goes south. The Range.End(xlDown) is indicating that the last non-blank cell in the column is the very, very bottom cell on the worksheet (Like row 40,000 something). This is clearly not true, as the last non-blank cell is three rows down from the range I am looking at. Thus, rather than getting a 4x5 size range, I get one that spans nearly the entire height of the sheet. I have also tried clearing all the formatting of the column in case something was lingering, but to no avail. The code is below.
Sub Copy_Starters_ToMaster()
Dim MasterIO As Worksheet, IOws As Worksheet
Set MasterIO = Worksheets("Master IO Worksheet")
Set IOws = Worksheets("IO Worksheet")
'Sets a range to cover all VFDs entered for an enclosure.
Dim EnclosureStarters As Range, BottomLine As Range
Set BottomLine = IOws.Range("$Z$6").End(xlDown).Offset(0, 3)
Set EnclosureStarters = IOws.Range("$Z$6", BottomLine)
'Finds first blank line in Master VFD table
Dim myBlankLine As Range
Set myBlankLine = MasterIO.Range("$AB$6")
Do While myBlankLine <> vbNullString
Set myBlankLine = myBlankLine.Offset(1, 0)
Loop
'Copies over enclosure list of VFDs, pastes into the master at the bottom of the list.
EnclosureStarters.Copy
myBlankLine.PasteSpecial
Dim BottomInEnclosure As Range, currentStarterRange As Range, EnclosureNumber As Range
'Indicates which enclosure each VFD copied in belongs to, formats appropriately.
Set BottomInEnclosure = myBlankLine.End(xlDown)
Set currentStarterRange = Range(myBlankLine, BottomInEnclosure).Offset(0, -1)
For Each EnclosureNumber In currentStarterRange
With EnclosureNumber
.Value = Worksheets("Math Sheet").Range("$A$11").Value
.BorderAround _
ColorIndex:=1, Weight:=xlThin
.HorizontalAlignment = xlCenter
End With
Next EnclosureNumber
End Sub
Any advice on this would be much appreciated, it is endlessly frustrating. Please let me know if I need to post photos of the errors, or any further code, etc.
I think the answer here is to use xlUp and a generic lastrow formula such as:
Set BottomLine = IOws.Cells(Rows.Count, "Z").End(xlUp).Offset(0, 3)
That gives the last used cell in column Z, offset by 3
Cleaner again to use Find rather than rely on the xlUp type shortcuts, something like this (which also caters for the column being empty, which is a common error when setting ranges):
Dim rng1 As Range
Set rng1 = IOws.Range("Z:Z").Find("*", IOws.[z1], xlFormulas, , xlPrevious)
If Not rng1 Is Nothing Then Set Bottomline = rng1.Offset(0, 3)
`if rng1 is Nothing then the area searched is blank

Copy a range to a virtual range

Is it possible to copy a range to a virtual range or does it require me to sloppily paste it in another range in the workbook?
dim x as range
x = copy of Range("A1:A4")
obviously I usually use the following code
dim x as range
set x = Range("A1:A4")
but in the above example it only makes x a "shortcut" to that range rather than a copy of the range object itself. Which is usually what I want but lately I have been finding it would be quite useful to totally save a range and all it's properties in memory rather than in the workbook somewhere.
I think this is what you are trying to do:
'Set reference to range
Dim r As Range
Set r = Range("A1:A4")
'Load range contents to an array (in memory)
Dim v As Variant
v = r.Value
'Do stuff with the data just loaded, e.g.
'Add 123 to value of cell in 1st row, 3rd column of range
v(1,3) = v(1,3) + 123
'Write modified data back to some other range
Range("B1:B4").Value = v
Is it possible to copy a range to a virtual range?
No it is not possible. Range allways represents some existing instance(s) of cells on a worksheet in a workbook.
Does it require me to sloppily paste it in another range in the
workbook?
It depends on what you want to do. You can paste everithing from one range to another, you can paste only something like e.g. formulas to another range.
dim x as range
set x = Range("A1:A4")
But in the above example it only makes x a "shortcut" to that range
rather than a copy of the range object itself.
Variable x holds a reference to that specific range. It is not possible to made any standalone copy of a range. It is possible to create references to a range and to copy everithing / something from one range to another range.
Lately I have been finding it would be quite useful to totally save a
range and all it's properties in memory rather than in the workbook
somewhere.
Again, it is not possible to save all range properties to some virtual, standalone copy of specific Range because Range allways represents an existing, concrete set of cells. What you could do is to create your own class with some properties of a Range or even all properties ... but it will be some extra work to do.
Here some examples how to use range as parameter and copy it to another range. HTH.
Option Explicit
Sub Main()
Dim primaryRange As Range
Set primaryRange = Worksheets(1).Range("A1:D3")
CopyRangeAll someRange:=primaryRange
CopyRangeFormat someRange:=primaryRange
' Value property of a range represents and 2D array of values
' So it is usefull if only values are important and all the other properties do not matter.
Dim primaryRangeValues As Variant
primaryRangeValues = primaryRange.value
Debug.Print "primaryRangeValues (" & _
LBound(primaryRangeValues, 1) & " To " & UBound(primaryRangeValues, 1) & ", " & _
LBound(primaryRangeValues, 2) & " To " & UBound(primaryRangeValues, 2) & ")"
' Prints primaryRangeValues (1 To 3, 1 To 4)
Dim value As Variant
For Each value In primaryRangeValues
' This loop throught values is much quicker then to iterate through primaryRange.Cells itself.
' Use it to iterate through range when other properties except value does not matter.
Debug.Print value
Next value
End Sub
Private Sub CopyRangeAll(ByVal someRange As Range)
' Here all properties of someRange which can be copied are copied to another range.
' So the function gets a reference to specific range and uses all its properties for another range.
Dim secondaryRange As Range
Set secondaryRange = Worksheets(2).Range("D4:G6")
someRange.Copy secondaryRange
End Sub
Private Sub CopyRangeFormat(ByVal someRange As Range)
' Here only formats are copied.
' Function receives reference to specific range but uses only one special property of it in that another range.
Dim secondaryRange As Range
Set secondaryRange = Worksheets(3).Range("G7:J9")
someRange.Copy
secondaryRange.PasteSpecial xlPasteFormats ' and many more e.g. xlPasteFormulas, xlPasteValues etc.
End Sub