SpecialCells causing SheetSelectionChange event in Excel 2010 - excel

I have a test Macro
Sub test()
Dim rSrcMatrix As Range
Set rSrcMatrix = Sheets("Code Matrix").Range("Xfer_To_Xfer_Matrix").Range("A1")
Set rSrcMatrix = rSrcMatrix.Resize(rSrcMatrix.SpecialCells(xlCellTypeLastCell).Row, rSrcMatrix.SpecialCells(xlCellTypeLastCell).Column)
End Sub
I am using this macro to test my COM addin that I have created in VS2010. I have delegated the SheetSelectionChange event in the addin to some function.
Now I notice that whenever I run this macro, Excel fires the SheetSelectionChange event 4 times and my addin calls the associated method for that many times.
Is there anything that I am missing or is this a bug in excel?

I believe and I could be wrong because I couldn't find an MSDN article to prove it but SpecialCells performs a type of selection and triggers the Worksheet_SelectionChange or the Workbook_SheetSelectionChange event and hence you need to switch off events.
Here is a simple way to test it.
Place this code in the Sheet Code Area
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
MsgBox "Damn! The SpecialCells caused me to pop up!!!"
End Sub
Sub test()
Debug.Print ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row
End Sub
Worksheet_SelectionChange and Workbook_SheetSelectionChange do the same job. Worksheet_SelectionChange is used in the sheet code are for a specific sheet. And Workbook_SheetSelectionChange is used when you want the event to fire across all the sheets in that workbook.
YOUR QUESTION FROM THE COMMENT: What if we wanted to associate another event with that line of code. In that case, we cannot suppress the event.
Now, we have two alternatives. Based on your above question we cannot use Alternative One. So you may directly skip to Alternative 2
ALTERNATIVE 1
Switch Off Events
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
On Error GoTo Whoa
Application.EnableEvents = False
'
'~~> YOUR CODE
'
Letscontinue:
Application.EnableEvents = True
Exit Sub
Whoa:
MsgBox Err.Description
Resume Letscontinue
End Sub
ALTERNATIVE 2
Instead of using SpecialCells to find the last row or the last column, we will use .Find.
Sub test()
Dim ws As Worksheet
Dim rSrcMatrix As Range
Dim Lrow As Long, LCol As Long
Set ws = ThisWorkbook.Sheets("Code Matrix")
With ws
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
Lrow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
LCol = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Column
Else
Lrow = 1
End If
Set rSrcMatrix = .Range("Xfer_To_Xfer_Matrix").Range("A1")
Set rSrcMatrix = rSrcMatrix.Resize(Lrow, LCol)
Debug.Print rSrcMatrix.Address
End With
End Sub

Based on #Siddharth Rout technique, here is a method that will return the last cell of a Worksheet.UsedRange (see my comments below his answer)
Function GetLastCellEmpty(rng As Range) As Range
Set m_rngCheck = rng
Set m_rngFound = m_rngCheck.Find(What:="", _
LookIn:=XlFindLookIn.xlFormulas, _
Lookat:=XlLookAt.xlPart, _
SearchDirection:=XlSearchDirection.xlPrevious)
Set GetLastCellEmpty = m_rngFound
End Function
Function GetLastCellUsedRange(ws As Worksheet) As Range
Set GetLastCellUsedRange = GetLastCellEmpty(ws.UsedRange.Offset(1, 1)).Offset(-1, -1)
End Function

Related

How to Select All or (Ctrl + A) dynamic table doesn't matter how big it is

This is my sample data.
And this is the VBA code produced with macro.
Sub Macro1()
'
' Macro1 Macro
'
'
Range("A1:C3").Select
Application.CutCopyMode = False
ActiveSheet.ListObjects.Add(xlSrcRange, Range("$A$1:$C$3"), , xlYes).Name = _
"Table1"
Range("Table1[#All]").Select
ActiveSheet.ListObjects("Table1").TableStyle = "TableStyleMedium9"
End Sub
How do I get the macro code?
Developer > Record Macro
Select all (ctrl + A) inside any cells within A1:C3
Home > Format as Table
Output
The problem is my data is dynamic and not necessarily stay at A1:C3.
It could be bigger or smaller.
E.g., let say I've bigger within range A1:C4 in different Ms Excel file.
The code above won't select all, instead it selects only A1:C3.
Last row A4:C4 won't be affected with this code.
How do I change this doesn't matter how big the table is?
Solution 1
You can use Range("A1").CurrentRegion to get the area of continous data (which is the same as Ctrl + A):
Option Explicit
Public Sub SelectCurrentRegion()
Dim MyData As Range
Set MyData = Worksheets("Sheet1").Range("A1").CurrentRegion
'don't use .select this is just for illustrating
MyData.Select
End Sub
Note that the number in cell D6 is not vertically nor horizontally connected with the other data. Therefore it is not selected by Ctrl + A or .CurrentRegion.
Solution 2
Or Worksheets("Sheet1").UsedRange to get the area of all data:
Option Explicit
Public Sub SelectCurrentRegion()
Dim MyData As Range
Set MyData = Worksheets("Sheet1").UsedRange
'don't use .select this is just for illustrating
MyData.Select
End Sub
You might benefit from reading
How to avoid using Select in Excel VBA.
#Pᴇʜ has already given you 2 solutions. Here is Solution 3. I would prefer finding last row and column over UsedRange and then construct the range. I have explained here why I do not prefer using UsedRange
Solution 3
Find the last row and last column and then create your range
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim LastRow As Long
Dim LastColumn As Long
Dim rng As Range
'~~> Change this to relevant sheet
Set ws = Sheet1
With ws
LastRow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
LastColumn = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Column
Set rng = .Range(.Cells(1, 1), .Cells(LastRow, LastColumn))
Debug.Print rng.Address
End With
End Sub
If you are using Excel tables then you can use DataBodyRange or Range Properties to select what you need to select.
Dim lstObj As ListObject
For Each lstObj In ActiveSheet.ListObjects
lstObj.DataBodyRange.Select ' Will select only data without headers
lstObj.Range.Select ' Will select complete table
Next lstObj

EXCEL VBA Debug: Searching through the whole workbook

I'm working on a VBA Macro for a database I have in Excel. I've got one Worksheet that stores information such as names, emails etc. (sadly those are not consistently placed in the same columns across all worksheets, but the email adresses span from "B:F"), this database is split into multiple worksheets. Except all those worksheets, I have also got one other worksheet ("Sheet2" in the code below) that stores all the email addresses that have assigned to my newsletter. (The only information in this sheet are the email addresses in the "A" column).
The VBA I'm working on should loop through all the email adresses that have subscribed to the newsletter ("Sheet2") and check if they're stored in "the database" - in the other sheets as well. If not, then give a warning - write "NOTFOUND" in the cell next to the email.
For some reason, VBA gives me a run-time error "Object doesn't support this property or method" on the row:
With Sheets(sheetIndex).Range("B:F").
Originally I thought that the reason for that is that I have not activated the Sheets, but I'm still getting the error.
The code I came up with so far:
Sub Search_for_emails()
Dim scanstring As String
Dim foundscan As Range
Dim lastRowIndex As Long
Dim ASheet As Worksheet
Set ASheet = Sheets("Sheet2")
lastRowInteger = ASheet.Range("A1", ASheet.Range("A1").End(xlDown)).Rows.Count
For rowNum = 1 To lastRowInteger
scanstring = Sheets("Sheet2").Cells(rowNum, 1).Value
For sheetIndex = 1 To ThisWorkbook.Sheets.Count
Sheets(sheetIndex).Activate
If Sheets(sheetIndex).Name <> "Sheet2" Then
With Sheets(sheetIndex).Range("B:F")
Set foundscan = .Find(What:=scanstring, LookIn:=xlValues, LookAt:=xlWhole, _
SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False)
End With
If foundscan Is Nothing Then
ASheet.Cells(rowNum, 2).Value = "NOTFOUND"
Else
' ASheet.Cells(rowNum, 2).Value = foundscan.Rows.Count
End If
End If
Next
Next rowNum
End Sub
Some points:
You should avoid Activate - no need for that.
You should always qualify things like
sheet or range, else Excel will use the active workbook /
sheet, and that is not always what you want.
There is a difference between the Sheets and the Worksheets collection. A Chart-sheet, for example, has no cells and therefore no Range.
You are declaring a variable lastRowIndex but uses lastRowInteger. To avoid such errors, always put Option Explicit at the top of your code.
Change your Sub to
Sub Search_for_emails()
Dim scanstring As String
Dim foundscan As Range
Dim lastRowIndex As Long, rowNum As Long
Dim ASheet As Worksheet
Set ASheet = ThisWorkbook.Worksheets("Sheet2")
lastRowIndex = ASheet.Range("A1", ASheet.Range("A1").End(xlDown)).Rows.Count
For rowNum = 1 To lastRowIndex
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
If ws.Name <> "Sheet2" Then
With ws.Range("B:F")
Set foundscan = .Find(What:=scanstring, LookIn:=xlValues, LookAt:=xlWhole, _
SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False)
End With
If foundscan Is Nothing Then
ASheet.Cells(rowNum, 2).Value = "NOTFOUND"
Else
' ASheet.Cells(rowNum, 2).Value = foundscan.Rows.Count
End If
End If
Next
Next rowNum
End Sub

Problems with a 'myrange' loop continuing to process beyond the end of the range

I am having problems with a macro which should search for each mycell of myrange in turn and copy it to another sheet if it is found in the GL sheet. However it continues to run after the cells in myrange (i.e. it continues to run on all the blank rows under myrange). myrange is just 10 rows of data. Here is the code:
Dim myrange As Range
Dim mycell As Range
Set wbProjects = Workbooks("Expense Project Jobs.xlsx")
Set wbGL = Workbooks("GL.xml")
Set wbProjectJournal = Workbooks("Expense Project Journal.xlsx")
wbProjects.Activate
LastrowJob1 = Sheets("Project_Costs").Range("F" & Rows.Count).End(xlUp).Row
Set myrange = Range("F2:F" & LastrowJob1)
'LOOP START
For Each mycell In myrange
If mycell = "" Then
GoTo ErrorHandlerMyCell
End If
mycell.Copy
wbGL.Activate
On Error GoTo ErrorHandlerMyCell
Range("A1").Activate
Cells.Find(What:=mycell, After:=ActiveCell, LookIn:=xlValues, LookAt _
:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:= _
False, SearchFormat:=False).Activate
On Error GoTo 0
ActiveCell.EntireRow.Cut
wbProjectJournal.Activate
Range("A1").Activate
If Range("A2") <> "" Then
GoTo NextCode2
NextCode2:
Selection.End(xlDown).Select
ActiveCell.Offset(1, 0).Select
Activesheet.Paste
wbGL.Activate
ActiveCell.EntireRow.Delete
Else
Range("A2").Select
Activesheet.Paste
End If
NextCode1:
Next mycell
ErrorHandlerMyCell:
Resume NextCode1
End Sub
Do you know that your code will run ErrorHandlerMyCell at the end irregardless of whether there's an error or not? It's not a separate module that is called only when there's error but part of the main program which gets triggered. Perhaps you can add a Exit Sub before ErrorHandlerMyCell
Exit Sub
ErrorHandlerMyCell:
Resume NextCode1
End Sub
The code have plenty of redundancies and it seems to be overwriting records copied in Row 3 when cell A2 in wbProjectJournal is empty.
I also suggest to set the worksheets as objects instead of the workbooks. Actually the code ends up working with whatever is the active sheet in the workbooks after they are activated. It could be working now if there is only one sheet or if the one active is the one required, but it’s just a coincidence, not a good practice.
One point to highlight is the excessive and incorrect use of what is intended to act as Error Handlers (see this page On Error Statement for a better understanding), also to improve use of objects see this With Statement
The code below should solve the issue, (have inserted comments to explain the changes):
Option Explicit
Sub TEST_Solution()
Dim wbProjects As Workbook, wbGL As Workbook, wbProjectJournal As Workbook
Dim rTrg As Range, rCll As Range, rCllTrg As Range
Dim rFnd As Range, vWhat As Variant
Set wbProjects = Workbooks("Expense Project Jobs.xlsx")
Set wbGL = Workbooks("GL.xml")
Set wbProjectJournal = Workbooks("Expense Project Journal.xlsx")
wbProjects.Activate
Rem Set Range from wbProjects\Project_Costs\Column F
'use [With] to perform several statements on the same object
'see https://msdn.microsoft.com/en-us/library/office/gg264723(v=office.15).aspx
With wbProjects.Sheets("Project_Costs").Columns(6)
Set rTrg = Range(.Cells(2), .Cells(Rows.Count).End(xlUp))
End With
Rem Search for the value of each cell in the no-empty cells of
For Each rCll In rTrg
Rem Set & Validate cell value
vWhat = rCll.Value2
If vWhat <> Empty Then
Rem Activate range to apply the FIND method
'Replace [1] with the name of the worksheet where the search is run
With wbGL.Sheets(1)
.Application.Goto .Cells(1), 1
Rem Set cell with found value
Set rFnd = .Cells.Find(What:=vWhat, After:=ActiveCell, _
LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlByColumns, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
If Not (rFnd Is Nothing) Then
Rem Activate range to apply the FIND method
'Replace [1] with the name of the worksheet where the search is performed
With wbProjectJournal.Sheets(1).Cells(2, 1)
If .Value2 = Empty Then
Rem A2 = Blank then Paste in row 2 only
rFnd.EntireRow.Copy
.PasteSpecial
Application.CutCopyMode = False
ElseIf .Offset(1).Value2 = Empty Then
Rem A3 = Blank then Paste in row 3 & delete record found
rFnd.EntireRow.Copy
.Offset(1).PasteSpecial
Application.CutCopyMode = False
rFnd.EntireRow.Delete
Else
Rem Paste below last row & delete record found
rFnd.EntireRow.Copy
.End(xlDown).Offset(1).PasteSpecial
Application.CutCopyMode = False
rFnd.EntireRow.Delete
End If: End With: End If: End With: End If: Next
End Sub

Set VBA Range with Variable End

I'm kind of new to VBA and am struggling to understand some of the syntax.
I have a range from a3:c13, for example, and I'd like to set it as a variable so I can pass it to vlookup later as a the table array. However, the range is defined by user input in terms of its size. It will always start in A3, it will always include columns A:C, but I don't know how far down it would go. In that case, I think I'd set it as:
With range("a3")
table_array = range(.cells(0,0), .End(xlDown).End(xlToRight)).Select
End With
However, that doesn't seem to work. I get a runtime error:
Run-time Error '1004': Method '_Default' of object 'Range' failed.
Assuming cols A, B, and C have the same number of rows:
Sub Macro1()
Set r = Range("A3")
Set table_array = Range(r, r.End(xlDown)).Resize(, 3)
End Sub
You can find the last row in Col A:C and then construct your range?
Sub Sample()
Dim ws As Worksheet
Dim LastRow As Long
Dim Rng As Range
'~~> Change this to the relevant sheet
Set ws = ThisWorkbook.Sheets("Sheet1")
With ws
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
LastRow = .Range("A:C").Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
Else
LastRow = 1
End If
If Not LastRow < 3 Then
Set Rng = .Range("A3:C" & LastRow)
Debug.Print Rng.Address
Else
MsgBox "No Data found beyond A3"
End If
End With
End Sub

Getting the actual usedrange

I have a Excel worksheet that has a button.
When I call the usedRange() function, the range it returns includes the button part.
Is there anyway I can just get actual used range that contains data?
What sort of button, neither a Forms Control nor an ActiveX control should affect the used range.
It is a known problem that excel does not keep track of the used range very well. Any reference to the used range via VBA will reset the value to the current used range. So try running this sub procedure:
Sub ResetUsedRng()
Application.ActiveSheet.UsedRange
End Sub
Failing that you may well have some formatting hanging round. Try clearing/deleting all the cells after your last row.
Regarding the above also see:
Excel Developer Tip
Another method to find the last used cell:
Dim rLastCell As Range
Set rLastCell = ActiveSheet.Cells.Find(What:="*", After:=.Cells(1, 1), LookIn:=xlFormulas, LookAt:= _
xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious, MatchCase:=False)
Change the search direction to find the first used cell.
Readify made a very complete answer. Yet, I wanted to add the End statement, you can use:
Find the last used cell, before a blank in a Column:
Sub LastCellBeforeBlankInColumn()
Range("A1").End(xldown).Select
End Sub
Find the very last used cell in a Column:
Sub LastCellInColumn()
Range("A" & Rows.Count).End(xlup).Select
End Sub
Find the last cell, before a blank in a Row:
Sub LastCellBeforeBlankInRow()
Range("A1").End(xlToRight).Select
End Sub
Find the very last used cell in a Row:
Sub LastCellInRow()
Range("IV1").End(xlToLeft).Select
End Sub
See here for more information (and the explanation why xlCellTypeLastCell is not very reliable).
Here's a pair of functions to return the last row and col of a worksheet, based on Reafidy's solution above.
Function LastRow(ws As Object) As Long
Dim rLastCell As Object
On Error GoTo ErrHan
Set rLastCell = ws.Cells.Find("*", ws.Cells(1, 1), , , xlByRows, _
xlPrevious)
LastRow = rLastCell.Row
ErrExit:
Exit Function
ErrHan:
MsgBox "Error " & Err.Number & ": " & Err.Description, _
vbExclamation, "LastRow()"
Resume ErrExit
End Function
Function LastCol(ws As Object) As Long
Dim rLastCell As Object
On Error GoTo ErrHan
Set rLastCell = ws.Cells.Find("*", ws.Cells(1, 1), , , xlByColumns, _
xlPrevious)
LastCol = rLastCell.Column
ErrExit:
Exit Function
ErrHan:
MsgBox "Error " & Err.Number & ": " & Err.Description, _
vbExclamation, "LastRow()"
Resume ErrExit
End Function
Public Sub FindTrueUsedRange(RowLast As Long, ColLast As Long)
Application.EnableEvents = False
Application.ScreenUpdating = False
RowLast = 0
ColLast = 0
ActiveSheet.UsedRange.Select
Cells(1, 1).Activate
Selection.End(xlDown).Select
Selection.End(xlDown).Select
On Error GoTo -1: On Error GoTo Quit
Cells.Find(What:="*", LookIn:=xlFormulas, LookAt:=xlWhole, SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Activate
On Error GoTo -1: On Error GoTo 0
RowLast = Selection.Row
Cells(1, 1).Activate
Selection.End(xlToRight).Select
Selection.End(xlToRight).Select
Cells.Find(What:="*", LookIn:=xlFormulas, LookAt:=xlWhole, SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Activate
ColLast = Selection.Column
Quit:
Application.ScreenUpdating = True
Application.EnableEvents = True
On Error GoTo -1: On Error GoTo 0
End Sub
This function returns the actual used range to the lower right limit. It returns "Nothing" if the sheet is empty.
'2020-01-26
Function fUsedRange() As Range
Dim lngLastRow As Long
Dim lngLastCol As Long
Dim rngLastCell As Range
On Error Resume Next
Set rngLastCell = ActiveSheet.Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious)
If rngLastCell Is Nothing Then 'look for data backwards in rows
Set fUsedRange = Nothing
Exit Function
Else
lngLastRow = rngLastCell.Row
End If
Set rngLastCell = ActiveSheet.Cells.Find("*", searchorder:=xlByColumns, searchdirection:=xlPrevious)
If rngLastCell Is Nothing Then 'look for data backwards in columns
Set fUsedRange = Nothing
Exit Function
Else
lngLastCol = rngLastCell.Column
End If
Set fUsedRange = ActiveSheet.Range(Cells(1, 1), Cells(lngLastRow, lngLastCol)) 'set up range
End Function
I use the following vba code to determine the entire used rows range for the worksheet to then shorten the selected range of a column:
Set rUsedRowRange = Selection.Worksheet.UsedRange.Columns( _
Selection.Column - Selection.Worksheet.UsedRange.Column + 1)
Also works the other way around:
Set rUsedColumnRange = Selection.Worksheet.UsedRange.Rows( _
Selection.Row - Selection.Worksheet.UsedRange.Row + 1)
This function gives all 4 limits of the used range:
Function FindUsedRangeLimits()
Set Sheet = ActiveSheet
Sheet.UsedRange.Select
' Display the range's rows and columns.
row_min = Sheet.UsedRange.Row
row_max = row_min + Sheet.UsedRange.Rows.Count - 1
col_min = Sheet.UsedRange.Column
col_max = col_min + Sheet.UsedRange.Columns.Count - 1
MsgBox "Rows " & row_min & " - " & row_max & vbCrLf & _
"Columns: " & col_min & " - " & col_max
LastCellBeforeBlankInColumn = True
End Function
Timings on Excel 2013 fairly slow machine with a big bad used range million rows:
26ms Cells.Find xlPrevious method (as above)
0.4ms Sheet.UsedRange (just call it)
0.14ms Counta binary search + 0.4ms Used Range to start search (12 CountA calls)
So the Find xlPrevious is quite slow if that is of concern.
The CountA binary search approach is to first do a Used Range. Then chop the range in half and see if there are any non-empty cells in the bottom half, and then halve again as needed. It is tricky to get right.
Here's another one. It looks for the first and last non empty cell and builds are range from those. This also handles cases where your data is not rectangular and does not start in A1. Furthermore it handles merged cells as well, which .Find skips when executed from a macro, used on .Cells on a worksheet.
Function getUsedRange(ByRef sheet As Worksheet) As Range
' finds used range by looking for non empty cells
' works around bug in .Find that skips merged cells
' by starting at with the UsedRange (that may be too big)
' credit to https://contexturesblog.com/archives/2012/03/01/select-actual-used-range-in-excel-sheet/
' for the .Find commands
Dim excelsUsedRange As Range
Dim lastRow As Long
Dim lastCol As Long
Dim lastCell As Range
Dim firstRow As Long
Dim firstCol As Long
Dim firstCell As Range
Set excelsUsedRange = ActiveSheet.UsedRange
lastRow = excelsUsedRange.Find(What:="*", _
LookIn:=xlValues, SearchOrder:=xlRows, _
SearchDirection:=xlPrevious).Row
lastCol = excelsUsedRange.Find(What:="*", _
LookIn:=xlValues, SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious).Column
Set lastCell = sheet.Cells(lastRow, lastCol)
firstRow = excelsUsedRange.Find(What:="*", After:=lastCell, _
LookIn:=xlValues, SearchOrder:=xlRows, _
SearchDirection:=xlNext).Row
firstCol = excelsUsedRange.Find(What:="*", After:=lastCell, _
LookIn:=xlValues, SearchOrder:=xlByColumns, _
SearchDirection:=xlNext).Row
Set firstCell = sheet.Cells(firstRow, firstCol)
Set getUsedRange = sheet.Range(firstCell, lastCell)
End Function
This is a different approach to the other answers, which will give you all the regions with data - a Region is something enclosed by an empty row and column and or the the edge of the worksheet. Basically it gives all the rectangles of data:
Public Function ContentRange(ByVal ws As Worksheet) As Range
'First, identify any cells with data, whose neighbourhood we will inspect
' to identify contiguous regions of content
'For efficiency, restrict our search to only the UsedRange
' NB. This may be pointless if .SpecialCells does this internally already, it probably does...
With ws.UsedRange 'includes data and cells that have been formatted
Dim cellsWithContent As Range
On Error Resume Next '.specialCells will error if nothing found, we can ignore it though
Set cellsWithContent = .SpecialCells(xlCellTypeConstants)
Set cellsWithContent = Union(cellsWithContent, .SpecialCells(xlCellTypeFormulas))
On Error GoTo 0
End With
'Early exit; return Nothing if there is no Data
If cellsWithContent Is Nothing Then Exit Function
'Next, loop over all the content cells and group their currentRegions
' This allows us to include some blank cells which are interspersed amongst the data
' It is faster to loop over areas rather than cell by cell since we merge all the CurrentRegions either way
Dim item As Range
Dim usedRegions As Range
For Each item In cellsWithContent.Areas
'Debug.Print "adding: "; item.Address, item.CurrentRegion.Address
If usedRegions Is Nothing Then
Set usedRegions = item.CurrentRegion 'expands "item" to include any surrounding non-blank data
Else
Set usedRegions = Union(usedRegions, item.CurrentRegion)
End If
Next item
'Debug.Print cellsWithContent.Address; "->"; usedRegions.Address
Set ContentRange = usedRegions
End Function
Used like:
Debug.Print ContentRange(Sheet1).Address '$A$1:$F$22
Debug.Print ContentRange(Sheet2).Address '$A$1:$F$22,$N$5:$M$7
The result is a Range object containing 1 or more Areas, each of it which will represent a data/formula containing region on the sheet.
It is the same technique as clicking in all the cells in your sheet and pressing Ctrl+T, merging all those areas. I'm using it to find potential tables of data

Resources