Reliably get Last Column in Excel with or without Merged Cells - excel

I recently ran into an issue where my get_lcol function returned A1 as the cells in A1:D1 were merged. I adapted my function to account for this, but then I had some other data with cells merged in A1:D1 but another column in G and my function returned D1 so I adjusted it again. The problem is I don't trust it still to work with all data types as its only checking merged cells in row 1.
Take a look at the below data, how can I reliably get the function to return D or 4 regardless of where I move the merged row and/or any other issues I haven't foreseen?
Current Function:
Public Sub Test_LCol()
Debug.Print Get_lCol(ActiveSheet)
End Sub
Public Function Get_lCol(WS As Worksheet) As Integer
Dim sEmpty As Boolean
On Error Resume Next
sEmpty = IsWorksheetEmpty(Worksheets(WS.Name))
If sEmpty = False Then
Get_lCol = WS.Cells.Find(What:="*", after:=[A1], SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Column
If IsMerged(Cells(1, Get_lCol)) = True Then
If Get_lCol < Cells(1, Get_lCol).MergeArea.Columns.Count Then
Get_lCol = Cells(1, Get_lCol).MergeArea.Columns.Count
End If
End If
Else
Get_lCol = 1
End If
End Function
Update:
Try this data w/ function:

This is a twist on the classic "Find Last Cell" problem
To state the aim:
find the column number of the right most cell containing data
consider merged cell areas that extend beyond other cells containing data. Return the right most column of a merged area should that extend beyond other data.
exclude formatted but empty cells and merged areas
The approach:
Use Range.Find to locate the last data cell
If the last column of the Used Range = Found last data cell column, return that
Else, loop from the last column of the Used Range back to the found data cell column
test for data in that column (.Count > 0), if true return that
test for merged cells in that column (IsNull(.MergeCells))
if found, loop to find the merged area
test the left most cell of the merged area for data
if found return the search column
Note
this may still be vulnerable to other "Last data" issues, eg Autofilter, Hidden rows/columns etc. I haven't tested those cases.
Has the advantage of limiting the search for merged cells to the relavent right most columns
Function MyLastCol(ws As Worksheet) As Long
Dim ur As Range
Dim lastcell As Range
Dim col As Long
Dim urCol As Range
Dim urCell As Range
Set ur = ws.UsedRange
Set lastcell = ws.Cells.Find("*", ws.Cells(1, 1), xlFormulas, , xlByColumns, xlPrevious)
For col = ur.Columns.Count To lastcell.Column - ur.Column + 2 Step -1
Set urCol = ur.Columns(col)
If Application.CountA(urCol) > 0 Then
MyLastCol = urCol.Column
Exit Function
End If
If IsNull(urCol.MergeCells) Then
For Each urCell In urCol.Cells
If urCell.MergeCells Then
If Not IsEmpty(urCell.MergeArea.Cells(1, 1)) Then
MyLastCol = urCol.Column
Exit Function
End If
End If
Next
End If
Next
MyLastCol = lastcell.Column
End Function

#Toddleson got me on the right track, here is what I ended with:
Public Sub Test_LCol()
Debug.Print Get_lCol(ActiveSheet)
End Sub
Public Function Get_lCol(WS As Worksheet) As Integer
On Error Resume Next
If Not IsWorksheetEmpty(WS) Then
Get_lCol = WS.Cells.Find(What:="*", after:=[A1], SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Column
Dim Cell As Range
For Each Cell In WS.UsedRange
If Cell.MergeCells Then
With Cell.MergeArea
If .Cells(.Cells.Count).Column > Get_lCol Then Get_lCol = .Cells(.Cells.Count).Column
End With
End If
Next Cell
Else
Get_lCol = 1
End If
End Function

The Find Method Backed Up by the UsedRange Property: What?
Talking about wasting time...
Option Explicit
Function GetLastColumn( _
ByVal ws As Worksheet) _
As Long
If ws Is Nothing Then Exit Function
' Using the 'Find' method:
'If ws.AutoFilterMode Then ws.AutoFilterMode = False ' (total paranoia)
Dim lcCell As Range
Set lcCell = ws.Cells.Find("*", , xlFormulas, , xlByColumns, xlPrevious)
If Not lcCell Is Nothing Then
GetLastColumn = lcCell.Column
End If
Debug.Print "After 'Find': " & GetLastColumn
' Using the 'UsedRange' property (paranoia):
Dim rg As Range: Set rg = ws.UsedRange
Dim clColumn As Long: clColumn = rg.Columns.Count + rg.Column - 1
If clColumn > GetLastColumn Then
If rg.Address(0, 0) = "A1" Then
If IsEmpty(rg) Then
Exit Function
End If
End If
GetLastColumn = clColumn
'Else ' clColumn is not gt GetLastColumn
End If
Debug.Print "Final (if not 0): " & GetLastColumn
End Function
Sub GetLastColumnTEST()
Debug.Print "Sub Result: " & GetLastColumn(Sheet1)
Debug.Print Sheet1.UsedRange.Address(0, 0)
End Sub
' It works for a few (?) cells, otherwise it returns 'Null'.
Sub TestMergeCells() ' Useless?! Could someone confirm.
Debug.Print Sheet1.Cells.MergeCells ' Null for sure
Debug.Print Sheet1.UsedRange.MergeCells
End Sub

Related

Defined Range Not producing a Value for VBA

There's probably a simple fix here, but my defined range is not being picked up.
I have an evolving set of data that will refresh. When I run the macro, I want to set a range for the values in the last 5 rows of the table to check win / loss (in column H) for a Count If function. But when I run the VBA trouble shoot command, a range value never gets set and my formula fails for Run-time error 1004. I've tried with both Selection.Offset and ActiveCell.Offset.
I feel like I'm making a basic mistake, but can narrow it down or easily find examples here to replicate
Dim fivegame As Range
Range("H1").Select
Selection.End(xlDown).Select
Set fivegame = Range(Selection.Offset(-4, 0), Selection)
Where do you get the error? Does this work?
Dim fivegame As Range
Set fivegame = Range("H" & Rows.Count).End(xlUp).Offset(-4).Resize(5)
Create a Reference to the Last Cells in a Column
The function refLastCellsInColumn will return a reference to the range consisting of the last cells in a column i.e. the number of consecutive cells above the last non-empty cell (incl.) defined by the parameter (number) supplied to the NumberOfCells argument. The function will return Nothing if the reference cannot be created (e.g. the range is supposed to start with a cell above the first cell...).
The Code
Option Explicit
Sub HowToUseItInYourProcedure()
Dim fivegame As Range
Set fivegame = refLastCellsInColumn(Range("H1"), 5)
End Sub
Function refLastCellsInColumn( _
ByVal FirstCell As Range, _
Optional ByVal NumberOfCells As Long = 1, _
Optional ByVal allowLessCells As Boolean = False) _
As Range
If Not FirstCell Is Nothing And NumberOfCells > 0 Then
With FirstCell
Dim rg As Range
Set rg = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If Not rg Is Nothing Then
If rg.Row - .Row >= NumberOfCells - 1 Then
Set refLastCellsInColumn = _
rg.Offset(1 - NumberOfCells).Resize(NumberOfCells)
Else
If allowLessCells Then
Set refLastCellsInColumn = .Resize(rg.Row - .Row + 1)
End If
End If
End If
End With
End If
End Function
Sub refLastCellsInColumnTEST()
Const FirstCellAddress As String = "H1"
Const NumberOfCells As Long = 2000000
' Define First Cell Range.
Dim cel As Range: Set cel = Range("H1")
' Define Last Cells in column.
Dim rg As Range: Set rg = refLastCellsInColumn(cel, NumberOfCells, True)
' Test.
If Not rg Is Nothing Then
Debug.Print rg.Address
Else
Debug.Print "Nope"
End If
End Sub

Deleting Preceding Blank Columns and Rows

I am trying to write a VBA code to cycle through each sheet in the active workbook and delete all blank columns and rows leading up to the first cell with data. For example, if the first cell with data is D5, columns A-C and Rows 1-4 would be deleted leaving the data starting in A1. I have the code below which works for the active sheet but I can't figure out how to get it to loop through the other sheets.
Sub DeleteRowsColumns()
' This will delete all Blank Columns and Rows before any data
Dim ColCounter As Long
Dim RowCounter As Long
Dim SafeCount As Integer
Dim ws As Worksheet
SafeCount = 0
' Check Column A is empty if Yes then Delete till A is populated
For Each ws In ActiveWorkbook.Worksheets
Do While ColCounter = 0
SafeCount = SafeCount + 1
ColCounter = Application.CountA(Columns(1).EntireColumn)
If ColCounter = 0 Then
Columns(1).EntireColumn.Delete
End If
If SafeCount = 50 Then
Exit Do
End If
Loop
Next ws
' Check Row 1 is empty if Yes then Delete till 1 is populated
For Each ws In ActiveWorkbook.Worksheets
SafeCount = 0
Do While RowCounter = 0
SafeCount = SafeCount + 1
RowCounter = Application.CountA(Rows(1).EntireRow)
If RowCounter = 0 Then
Rows(1).EntireRow.Delete
End If
If SafeCount = 50 Then
Exit Do
End If
'Loop
Next ws
MsgBox "Removed Preceding Blank Rows and Columns"
End Sub
Within each loop you need to specify which worksheet you are performing the operations on. Just looping through doesn't solve the problem. For instance:
ColCounter = Application.CountA(ws.Columns(1).EntireColumn)
If ColCounter = 0 Then
ws.Columns(1).EntireColumn.Delete
This ensures you are working in the correct worksheet.
Add it to a loop.
For X = 1 To 50
For i = 1 To 50
ColCounter = Application.CountA(ws.Columns(i).EntireColumn)
If ColCounter = 0 Then
ws.Columns(i).EntireColumn.Delete
End If
rowCounter = Application.CountA(ws.Rows(i).EntireRow)
If rowCounter = 0 Then
ws.Rows(i).EntireRow.Delete
End If
Next i
Next X
You could avoid any looping by first finding where the content starts (by row and then by column)
Sub RemoveEmpties()
Dim f As Range, f2 As Range, ws As Worksheet
For Each ws In ActiveWorkbook.Worksheets
'first occupied cell on sheet (by row)
Set f = ws.Cells.Find(What:="*", After:=ws.Cells(ws.Rows.Count, ws.Columns.Count), _
LookAt:=xlPart, LookIn:=xlFormulas, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False)
If Not f Is Nothing Then
'have content, so find first-occupied column
Set f2 = ws.Cells.Find(What:="*", After:=ws.Cells(ws.Rows.Count, ws.Columns.Count), _
LookAt:=xlPart, LookIn:=xlFormulas, SearchOrder:=xlByColumns, _
SearchDirection:=xlNext, MatchCase:=False)
'remove rows/columns as required
If f.Row > 1 Then ws.Cells(1, 1).Resize(f.Row - 1).EntireRow.Delete
If f2.Column > 1 Then ws.Cells(1, 1).Resize(, f2.Column - 1).EntireColumn.Delete
End If
Next ws
End Sub
Alternatively (again only max of two deletes):
Sub RemoveEmpties2()
Dim ws As Worksheet, r As Long, c As Long
For Each ws In ActiveWorkbook.Worksheets
'first make sure there's some content on the sheet...
If Application.CountA(ws.Cells) > 0 Then
r = 1: c = 1
Do While Application.CountA(ws.Rows(r)) = 0
r = r + 1
Loop
If r > 1 Then ws.Rows(1).Resize(r - 1).Delete
Do While Application.CountA(ws.Columns(c)) = 0
c = c + 1
Loop
If c > 1 Then ws.Columns(1).Resize(, c - 1).Delete
End If
Next ws
End Sub
Using the Find Method
The Flow
In the procedure delFirstBlank the workbook is defined. A worksheet variable is declared. In the following For Each Next loop, for each worksheet in the workbook, the procedure deleteFirstBlank is called. When the loop exits, by a message box, the user is informed that the code has finished.
In the deleteFirstBlank procedure, the result of the function getFirstRow is written to a variable. The variable is then tested if it is equal to 0 i.e. the worksheet is blank. If so, then the procedure is exited. If not, the variable is tested if it is greater than 1 i.e. if at least the first row is empty. If so, the rows from the first row to the row defined by the variable decreased by one are deleted. Then the result of the function getFirstRow is written to a variable which is tested if it is greater than 1 i.e. if at least the first column is empty. If so, the columns from the first column to the column defined by the variable decreased by one are deleted.
In the getFirstRow procedure (function) a range variable is declared. Using the Find method, searching by rows, the first found non-blank cell (range) in the supplied worksheet, is assigned to the range variable. If the result of the Find method was a cell range, its row is written as the result of the function. If not, 0 is written as the result i.e. the worksheet is blank.
In the getFirstColumn procedure (function) a range variable is declared. Using the Find method, searching by columns, the first found non-blank cell (range) in the supplied worksheet, is assigned to the range variable. If the result of the Find method was a cell range, its column is written as the result of the function. If not, 0 is written as the result i.e. the worksheet is blank (the latter will never happen, because the worksheet was already tested if it is blank in the 'getFirstRow' procedure).
The Code
Option Explicit
Sub delFirstBlank()
Dim wb As Workbook
Set wb = ActiveWorkbook
Dim ws As Worksheet
For Each ws In wb.Worksheets
deleteFirstBlank ws
Next ws
MsgBox "Removed first blank rows and columns.", vbInformation, "Success"
End Sub
Sub deleteFirstBlank(Sheet As Worksheet)
Dim Current As Long
Current = getFirstRow(Sheet)
If Current = 0 Then GoTo ProcExit ' Blank sheet.
If Current > 1 Then
Sheet.Range(Sheet.Rows(1), Sheet.Rows(CLng(Current) - 1)).Delete
End If
Current = getFirstColumn(Sheet)
If Current > 1 Then
Sheet.Range(Sheet.Columns(1), Sheet.Columns(CLng(Current) - 1)).Delete
End If
ProcExit:
End Sub
Function getFirstRow(Sheet As Worksheet) As Long
Dim rng As Range
Set rng = Sheet.Cells.Find(What:="*", _
After:=Sheet.Cells(Sheet.Rows.Count, _
Sheet.Columns.Count), _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows)
If Not rng Is Nothing Then
getFirstRow = rng.Row
Else
getFirstRow = 0 ' Blank Sheet
End If
End Function
Function getFirstColumn(Sheet As Worksheet) As Long
Dim rng As Range
Set rng = Sheet.Cells.Find(What:="*", _
After:=Sheet.Cells(Sheet.Rows.Count, _
Sheet.Columns.Count), _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns)
If Not rng Is Nothing Then
getFirstColumn = rng.Column
Else
getFirstColumn = 0 ' Blank Sheet
End If
End Function

Trying to delete a row if no data in row A:J

I am trying to delete a row if there is no data from A:J
I have found this code and been trying to edit it, but this is deleting the whole sheet's data eventually.
Any help would be greatly appreciated
Sub DeleteRows()
Dim rngBlanks As Range
Dim i As Integer
For i = 1 To 10
On Error Resume Next
Set rngBlanks = Columns(i).SpecialCells(xlCellTypeBlanks)
On Error GoTo 0
If Not rngBlanks Is Nothing Then
rngBlanks.EntireRow.Delete
End If
Next
End Sub
Trying to delete a row if no data in row A:J
What code is doing is individually checking the columns and not the range A:J as your title suggests. It is very much possible that your entire data is getting deleted because of this. Lets say A1 has some data but B1 doesn't. So your code will delete Row 1. What you have to do is to check if say A1:J1 is blank.
I think this is what you are trying?
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim rngBlanks As Range
Dim i As Long, lRow As Long, Ret As Long
'~~> Set this to the relevant worksheet
Set ws = ThisWorkbook.Sheets("Sheet2")
With ws
'~~> Get the last row in that sheet
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
Else
lRow = 1
End If
'~~> Loop through the rows to find which range is blank
For i = 1 To lRow
Ret = Application.Evaluate("=COUNTA(A" & i & ":J" & i & ")")
If Ret = 0 Then
If rngBlanks Is Nothing Then
Set rngBlanks = .Rows(i)
Else
Set rngBlanks = Union(rngBlanks, .Rows(i))
End If
End If
Next i
End With
'~~~> Delete the range
If Not rngBlanks Is Nothing Then rngBlanks.Delete
End Sub
Another way would be to use Autofilter to delete those ranges
I stepped through your code with a sheet having some non-blank cells in columns A:J down to row 15. Rows 16:18 were entirely blank and D19=1. You want to delete rows that have blanks in every cell from A:J.
On the first iteration of your For..Next loop rngBlanks was not Nothing because typing
?rngBlanks.address
returned $A$1,$A$5:$A$19. A2:A4 were not blank. When you execute
Set rngBlanks = Columns(i).SpecialCells(xlCellTypeBlanks)
it looks for any blanks in column A which is not what you wanted to test. You want to test each row, probably within your ActiveSheet.UsedRange to see if columns A:J are all blank. So you need to define a variable
Dim Rw as Range
and iterate through each Rw in UsedRange
For Each Rw in ActiveSheet.UsedRange
If WorksheetFunction.CountBlank(range(cells(Rw,1),cells(Rw,10))) =0 Then
Rw.EntireRow.Delete
I could post the entire code here but what I've given should put you on the right track.

Inserting a blank row after a string in Excel

I am trying to create a macro in excel 2010 that finds every cell in a sheet with a value of "All Customers." Every time that value is found I need a blank row inserted below it. Thought it would be pretty simple but I have searched I many forums and tried to use some sample code and I can't get it to work properly. I am a complete newb when it comes to VBA stuff. Thought I would post here and go do some light reading on basics of VBA.
If anyone has any good training resources, please post those as well.
Thanks in advance!
EDIT: In my OP, I neglected to mention that any row that contains a value of "All Customers" would ideally be highlighted and put in bold, increased size font.
These actions are something that an old Crystal Report viewing/formatting program used to handle automatically when pulling the report. After we upgraded the program I learned that this type of formatting ability had been removed with the release of the newer version of the program, according to the software manufacturer's tech support. Had this been defined in the release notes I would have not performed the upgrade. Regardless, that is how I found myself in this macro disaster.
Something like this code adpated from an article of mine here is efficient and avoids looping
It bolds and increase the font size where the text is found (in the entire row, as Tim points out you should specify whether you meant by cell only)
It adds a blank row below the matches
code
Option Explicit
Const strText As String = "All Customers"
Sub ColSearch_DelRows()
Dim rng1 As Range
Dim rng2 As Range
Dim rng3 As Range
Dim cel1 As Range
Dim cel2 As Range
Dim strFirstAddress As String
Dim lAppCalc As Long
Dim bParseString As Boolean
'Get working range from user
On Error Resume Next
Set rng1 = Application.InputBox("Please select range to search for " & strText, "User range selection", ActiveSheet.UsedRange.Address(0, 0), , , , , 8)
On Error GoTo 0
If rng1 Is Nothing Then Exit Sub
'Further processing of matches
bParseString = True
With Application
lAppCalc = .Calculation
.ScreenUpdating = False
.Calculation = xlCalculationManual
End With
'a) match string to entire cell, case insensitive
'Set cel1 = rng1.Find(strText, , xlValues, xlWhole, xlByRows, , False)
'b) match string to entire cell, case sensitive
'Set cel1 = rng1.Find(strText, , xlValues, xlWhole, xlByRows, , True)
'c)match string to part of cell, case insensititive
Set cel1 = rng1.Find(strText, , xlValues, xlPart, xlByRows, , False)
'd)match string to part of cell, case sensititive
' Set cel1 = rng1.Find(strText, , xlValues, xlPart, xlByRows, , True)
'A range variable - rng2 - is used to store the range of cells that contain the string being searched for
If Not cel1 Is Nothing Then
Set rng2 = cel1
strFirstAddress = cel1.Address
Do
Set cel1 = rng1.FindNext(cel1)
Set rng2 = Union(rng2.EntireRow, cel1)
Loop While strFirstAddress <> cel1.Address
End If
'Further processing of found range if required
If bParseString Then
If Not rng2 Is Nothing Then
With rng2
.Font.Bold = True
.Font.Size = 20
.Offset(1, 0).EntireRow.Insert
End With
End If
End If
With Application
.ScreenUpdating = True
.Calculation = lAppCalc
End With
End Sub
Public Sub InsertRowAfterCellFound()
Dim foundRange As Range
Set foundRange = Cells.Find(What:="yourStringOrVariant", After:=ActiveCell) 'Find the range with the occurance of the required variant
Rows(foundRange.Row + 1 & ":" & foundRange.Row + 1).Insert 'Insert a new row below the row of the foundRange row
foundRange.Activate 'Set the found range to be the ActiveCell, this is a quick and easy way of ensuring you aren't repeating find from the top
End Sub
You may need to add error handling to the code as you will get an error if no cell with the specified value is found.
Assuming this is on the first sheet ("sheet 1"), here is a slow answer:
Sub InsertRowsBelowAllCustomers()
'Set your worksheet to a variable
Dim sheetOne as Worksheet
Set sheetOne = Worksheets("Sheet1")
'Find the total number of used rows and columns in the sheet (where "All Customers" could be)
Dim totalRows, totalCols as Integer
totalRows = sheetOne.UsedRange.Rows.Count
totalCols = sheetOne.UsedRange.Columns.Count
'Loop through all used rows/columns and find your desired "All Customers"
Dim row, col as Integer
For row = 1 to totalRows
For col = 1 to totalCols
If sheetOne.Cells(row,col).Value = "All Customers" Then
Range(sheetOne.Cells(row,col)).Select
ActiveCell.Offset(1).EntireRow.Insert
totalRows = totalRows + 1 'increment totalRows because you added a new row
Exit For
End If
Next col
Next row
End Sub
This function starts from the last row and goes back up to the first row, inserting an empty row after each cell containing "All Customers" on column A:
Sub InsertRowsBelowAllCustomers()
Dim R As Integer
For R = UsedRange.Rows.Count To 1 Step -1
If Cells(R, 1) = "All Customers" Then Rows(R + 1).Insert
Next R
End Sub
The error is because the worksheet was not specified in used range.
I have slightly altered the code with my text being in column AJ and inserting a row above the cell.
Dim R As Integer
For R = ActiveSheet.UsedRange.Rows.Count To 1 Step -1
If Range("AJ" & R) = "Combo" Then Rows(R).Insert
Next R

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