Excel VBA, find the next empty cell - excel

What I am trying to do should be easy but the noob in me is showing. I am creating a database in excel using a dataentry form in another worksheet. When the “enter” button is clicked, it runs a macro that places the info from the dataentry sheet to an Excel database. The problem is, of course, that the next record overwrites the previous record. What I want to do is copy the data from a field in the dataentry form, then go to the database and find the next empty row and paste the info there. Ideally, the procedure would continue and copy the next field in the dataentry form, find the empty cell to the right of the data previously pasted and then repeat until all 6 fields have been copied and pasted to the excel database. I want to do this all in excel rather than into an access database. Any ideas?

You could try
Sub LastRow()
Dim rngLast As Range
With ThisWorkbook.Worksheets("Sheet1")
If .Range("A1").Value = "" Then
Set rngLast = .Range("A1")
Else
Set rngLast = .Cells(.Rows.Count, "A").End(xlUp).Offset(1, 0)
End If
MsgBox "Next blank row is " & rngLast.Address
End With
End Sub

This will find the very last cell (last column & last row) used in the workbook & sheet provided. Adapt to taste (e.g you don't need targetwkb if you only work with one workbook). In case you don't need to find the last column, or just want the specific last row of a given column, you could just define different the "With" part (w.e. "With Worksheets(targetSheet).Range("A:A") or something similar:
Function FindLastCell(targetWbk As String, targetSheet As String) As Range
Dim LastColumn As Integer, lastRow As Integer
'Finds the last used cell in target wbk/sheet
With Workbooks(targetWbk).Worksheets(targetSheet)
lastRow = .Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByRows, SearchDirection:=xlPrevious).row
LastColumn = .Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Column
Set FindLastCell = Range(.Cells(1, 1), .Cells(lastRow, LastColumn))
End With
End Function
The above is a better approach overall because it searches from the last cell to the top. The otherway around, if you have an empty cell then Excel will stop searching, and you could overwrite all the data that follows just because a blank cell was left somewhere.
If you have lots of data (like 10s of thousands) you may want to change integer for long, so you don't bump on the integer limit. It also doesn't need to be a function, could be a sub but doesn't matter much...

Related

Last Row Returns 1 - incorrect value

situation is following:
I have 32 columns with data (various number of rows in columns) and need to delete cells with .value "downloaded" (always last cell in a column).
I have a code looping from column 32 to 1 and searching last_row for "downloaded" value. For 30 columns code seems to be working flawlessly but 2 columns return last_row value 1 even though there are multiple values (in fact hundreds of them) but they are non existent for VBA code.
Code:
Last_Col = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
last_row = ws.Cells(Rows.Count & Last_Col).End(xlUp).Row
For R = Last_Col To 1 Step -1
With ws
Last_Col = R
last_row = ws.Cells(.Rows.Count & Last_Col).End(xlUp).Row
If Cells(last_row, Last_Col).Value Like "*Downloaded*" Then
Cells(last_row, Last_Col).ClearContents
End If
End With
Next R
Data is being drained from another worksheets. For 2 columns where I experience an error, I manually deleted values and inserted another, random batch of values and code worked as intended.
Checked columns formatting, worksheets from which data is taken but I struggle to find a solution.
Thank you for your help.
Clear Last Cell If Criteria Is Met
The main mistake was using Cells(.Rows.Count & Last_Col), where .Rows.Count & Last_Col would have resulted in a 8 or 9-digit string, while it should have been ws.Cells(ws.Rows.Count, Last_Col).End(xlUp).Row which was pointed out by chris neilsen in the comments.
Another important issue is using ws. in front of .cells, .rows, .columns, .range, aka qualifying objects. If you don't do it and e.g. the wrong worksheet is active, you may get unexpected results.
There is no need for looping backwards unless you are deleting.
Although it allows wild characters (*, ?), the Like operator is case-sensitive (a<>A) unless you use Option Compare Text.
The first solution, using the End property, will fail if a number of last columns is hidden or if you insert a new first row e.g. for a title.
The second solution, using the Find method (and the first solution), may fail if the data is filtered.
The Code
Option Explicit
Sub clearLastEnd()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim LastCol As Long
LastCol = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
Dim LastRow As Long
Dim c As Long
For c = 1 To LastCol
LastRow = ws.Cells(ws.Rows.Count, c).End(xlUp).Row
With ws.Cells(LastRow, c)
If InStr(1, .Value, "Downloaded", vbTextCompare) > 0 Then
.ClearContents
End If
End With
Next c
End Sub
Sub clearLastFind()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim cel As Range
Set cel = ws.Cells.Find(What:="*", _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious)
If Not cel Is Nothing Then
Dim c As Long
For c = 1 To cel.Column
Set cel = Nothing
Set cel = ws.Columns(c).Find(What:="*", _
SearchDirection:=xlPrevious)
If Not cel Is Nothing Then
If InStr(1, cel.Value, "Downloaded", vbTextCompare) > 0 Then
cel.ClearContents
Else
' The current last non-empty cell does not contain criteria.
End If
Else
' Column is empty.
End If
Next c
Else
' Worksheet is empty.
End If
End Sub
EDIT:
So you are curious why it worked at all. The following should shed a light on it:
Sub test()
Dim i As Long
Debug.Print "Right", "Wrong", "Rows.Count & i"
For i = 1 To 32
Debug.Print Cells(Rows.Count, i).Address, _
Cells(Rows.Count & i).Address, Rows.Count & i
Next i
End Sub
In a nutshell, Cells can have 1 or 2 arguments. When 1 argument is used, it refers to the n-th cell of a range, and it 'counts' by row. The more common usage is with 2 arguments: rows, columns. For example:
Cells(5, 10) ' refers to cell `J5`.
Using one argument is inconvenient here:
Cells(16384 * (5-1) + 10)
i.e.
Cells(65546)
It may be convenient when processing a one-column or a one-row range.
Well , let me see if i understand you have a table in worksheet table have 32 columns and X rows (because you only put WS and i can know if is WS=worksheet or WS= Table-range)
for this i am going to say is selection (if you put worksheet only hace to change for it)
in your code put:
Last_Col = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
but in this you always wil obtein 1st cell so i dont understand why exist?
WS.columns.count
return number of columns you selection have
.End(xlToLeft)
return last cell if star to move to left (like Ctrl - left key)
so
Last_Col ---first go to cells (1,ws.Columns.Count) then go to left (End(xlToLeft)) and the end return number of column where finish (.Column) in this case you always get cell (1,"first column of your table")
NOTE: because you wrote that you have allways value in your cells (I have 32 columns with data (various number of rows in columns)
And for Row you have same question
Then you Wrote you want "Delete" but in your code you put Erase value (.ClearContents) so what do you want? because both are no equal
BUT if you have a table and want to search in any cells that have "Download" and only want to "clear content" you just may to use ".find" instead; or if you want to do all at same time you can use .replace (need to check before if .find return "nothing" or no , because if return nothing you get error)
If you have a table with 32 columns and each row have one cell where you put "Donloaded" and want to "delete" all row your code only need select column where appear "downloaded" (example Column "status").
If you have a table where any cell can take value "downloaded" and want to "delete" that cell you need to take care to resize your table and "move to" (when you delete cells you need to say where you want to move yor data remain "letf, "rigth", "up", down).
However if you say that "Downloaded" always appear in last row you can use For to change for all columns and use .end(xlDown)
For i=1 to 32
if cells(1,i).end(xlDown).value="downloaded" then cells(1,i).end(xlDown).ClearContents
next
BUT you need put more information because if you cant garantize that all cells have values and exist cells with "nothing" you will need

How can I improve my Current Region selector?

I have made a simple macro assigned to a hotkey to select the current region and then remove the header row. The problem is that the ranges we work with are often full of blank cells which prevent the selector from capturing the entire table dependant on the activecell.
I thought about maybe simply creating a loop, offsetting the ActiveCell and trying again until it hits an illegal range, but I have a bad feeling about this approach.
Sub multieditSelect()
Dim tbl As Range
If ActiveCell.Value = "" Then
MsgBox "Select a cell with something in it, you bastard"
Exit Sub
End If
Call startNoUpdates
Set tbl = ActiveCell.CurrentRegion
tbl.Offset(1, 0).Resize(tbl.Rows.Count - 1, _
tbl.Columns.Count).Select
Call endNoUpdates
Selection.Copy
End Sub
Is there a way to make this more reliable?
Edit: Let me add further complication/detail to this problem...
We work with a database and editing records en masse requires exporting them into excel, and the copy/pasting them back into the web interface, so it is common for us to be working with numerous tables of different size, using a worksheet like a notepad to store and modify them.
I want to create a sub that will select the current region irrespective of where it lies on the worksheet, quite possibly this is the third or fourth table to have been pasted onto the same sheet.
This makes going by the last column or last row too inflexible. CurrentRegion is ideal were it not for it's occasional failure to detect the table... so I suppose I need to build my own version of CurrentRegion that will overcome it's shortcomings.
Edit2: I've come up with a lazy solution.
Since these tables will always have a header, I'll just have the activecell offset up till it hits something, and hopefully that will be the header if an empty column is the starting point.
I think this will still be unreliable should there be a pocket of cells surrounded by empty cells in the middle of the table.
Sub multieditSelect2()
Dim tbl As Range
On Error GoTo errmsg
startNoUpdates
Do While ActiveCell.Value = ""
ActiveCell.Offset(-1, 0).Activate
Loop
startNoUpdates
Set tbl = ActiveCell.CurrentRegion
tbl.Offset(1, 0).Resize(tbl.Rows.Count - 1, _
tbl.Columns.Count).Select
endNoUpdates
Selection.Copy
Exit Sub
errmsg:
endNoUpdates
errMsgBox = MsgBox("Couldn't find a table!", vbCritical, "Error!")
End Sub
Edit3: Here is an example of where my code calls down:
I would like it to be able to capture the table even in this scenario where a cell in the test region is the activecell... but how?
Additional to my comment, see if this helps improve your logic (see comments in code for more details):
Sub multieditSelect()
Dim ws As Worksheet
Set ws = ActiveWorkbook.Sheets("Sheet1") 'use a variable for the sheet you want to use
Dim tbl As Range
Dim lRow As Long, lCol As Long
lRow = ws.Cells(Rows.Count, 1).End(xlUp).Row 'last row at column 1
lCol = ws.Cells(1, Columns.Count).End(xlToLeft).Column 'last column at row 1
Set tbl = ws.Range(ws.Cells(2, l), ws.Cells(lRow, lCol)) 'Set the range starting at row 2, column 1, until last row, last col
Call endNoUpdates(tbl) 'pass your range as a parameter if you require this specific range in your other sub
tbl.Copy Destination:=tbl.Offset(0, 20) 'copy 20 columns to the right
'Alternative
ws.Range("W1").Resize(tbl.Rows.Count, tbl.Columns.Count).Value = tbl.Value 'copy values to specific range
End Sub
Sub endNoUpdates(tbl As Range)
'do something with this range, i.e.:
Debug.Print tbl.address
End Sub

Excel.Application.Cells.SpecialCells(xlCellTypeLastCell) returning bottom of worksheet, not last data cell

I'm writing a method in VBA in Excel 2013 to loop through the rows in two worksheets and compare the text in a column from each. When I run my code, I find that the code loops through the entire worksheet, not just the rows with data.
Excel.Sheets("Sheet1").Cells.SpecialCells(xlCellTypeLastCell) returns a cell in the correct column, but the row is the last row in the sheet (1048576), rather than the last row with data (1951).
I had written a check for empty cells (since I can't be sure that every row in the valid range is used), so it doesn't cause any errors, but as this method is called from inside Worksheet_Change, it really slows things down.
Normally when the "last" cell is reported incorrectly a save usually fixes it, and if that doesn't, then deleting the rows (not just the contents, but the entire rows) from the mis-reported last cell back to the actual last cell and then saving works. However, in this instance, it is not helping.
I searched with Google without success. I'd like to not have to copy all the data and code out of this workbook and into a new one. Any ideas?
(Too much info to use a comment here.)
VBA mastermind Ron de Bruin wrote a little snipped about why xlCellTypeLastCell as well as UsedRange might be failing here: http://www.rondebruin.nl/win/s9/win005.htm.
(In the post I linked to in my initial comment, Error in finding last used cell in VBA, the pitfalls of UsedRange are described in the same way.)
Here's the direct quote:
Possible problems with xlCellTypeLastCell and UsedRange are:
The last cell will only re-set when you save (or save/close/reopen the
file). If cell formatting is changed it will not reset the last cell,
clearing the data is not enough, you must delete the rows or columns
then, See: http://www.contextures.com/xlfaqApp.html#Unused
To make a long story short, the logic for finding the last row on a sheet belongs inside a global function. You will use this function all the time. Here's an example for finding the last row on a sheet:
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'INPUT : Sheet, the worksheet we'll search to find the last row
'OUTPUT : Long, the last occupied row
'SPECIAL CASE: if Sheet is empty, return 1
'EXAMPLES :
'
'assume that there is a single
'entry on MySheet in cell A5:
'
'LastRowNum(MySheet)
'>> 5
'
'assume that EmptySheet is totally empty:
'
'LastRowNum(EmptySheet)
'>> 1
'
Public Function LastRowNum(Sheet As Worksheet) As Long
If Application.WorksheetFunction.CountA(Sheet.Cells) <> 0 Then
LastRowNum = Sheet.Cells.Find(What:="*", _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious).Row
Else
LastRowNum = 1
End If
End Function
As far as the downvote goes -- no idea. I've actually never downvoted here haha.
Try using the following code to determine the last row & column instead
Dim LastCol As Long
Dim LastRow As Long
LastCol = ActiveSheet.Cells(1, Columns.Count).End(xlToLeft).Column
LastRow = ActiveSheet.Cells(Rows.Count, "A").End(xlUp).Row
You can try this:
Dim xlastcel As Range
On Error Resume Next
ActiveSheet.ShowAllData: Err.Clear ' Show All
ActiveSheet.UsedRange ' Ajust Vertical Scrool
With ActiveSheet: Set xlastcell = .Cells.Find(What:="*", After:=.[A1], SearchDirection:=xlPrevious): End With
If Err.Number > 0 Then MsgBox "Sheet without data", vbOKOnly
On Error GoTo 0

How to copy only rows with data from one worksheet to another in a different workbook?

I can pull together a decent macro that does what I need but I forgot that the range will change everyday.
To be specific the row count will get higher.
Right now my macro goes through and hides any row that doesn't have today's date and then copies a set range to a worksheet in a different workbook.
The only problem I have is that range will change everyday, so I figure I need a way to copy only rows with data in them once the rest are hidden and then paste them to the other workbook.
Sub automate()
Dim cell As Range
For Each cell In Range("AB2:AB30000")
If cell.Value < Date And cell.Value <> Empty Then cell.EntireRow.Hidden = True
Next
Range("K28336:K28388,O28336:O28388,P28336:P28388,Q28336:Q28388,R28336:R28388,S28336:S28388,T28336:T28388,U28336:U28388,V28336:V28388,Y28336:Y28388,AA28336:AA28388,AB28336:AB28388").Select
Selection.Copy
Workbooks.Open ("\\gvwac09\Public\Parts\Test\2014 IPU.xlsx")
Sheets("Historical Data").Activate
ActiveSheet.Range("c1").End(xlDown).Offset(1, 0).Select
Selection.PasteSpecial Paste:=xlPasteFormats
ActiveSheet.Paste
This is my macro so far. I'm sorry if I didn't format this post correctly, new to this.
I do not understand exacting what you are attempting but I believe I can give you some useful pointers.
I do not explain the statements I use in the code below. Look them up in the Visual Basic Editor's Help or try searching the web for "Excel VBA xxxxx". Come back with questions if necessary but the more you can discover for yourself, the quicker your skills will develop.
Firstly you need to find the last row containing data. Examining every row down to AB30000 just wastes time. Macro Demo1 below demonstrates two techniques. There are more techniques for finding the last row, none of which are appropriate in every situation. Search StackOverflow for "[excel-vba] find last row". There are lots of relevant questions and answers although the first technique I use is far and away the most popular.
General advice: If you can break your requirement down to a sequence of single issues (such as "find last row"), you will find it easier to search StackOverflow for an answer.
Always include Application.ScreenUpdating = False at the start of your macros if you are going to amend a worksheet. Without this statement, everytime you hide a row, Excel repaints the screen.
I have created some test data which I hope is representative of your data. I have two worksheets Source and Dest. Source contains the full set of data. I copy the selected rows to Dest.
I have used Auto Filter which will be much faster than your technique if it will give you the effect you seek. Play with Auto Filter from the keyboard. If you can get the effect you seek, turn on the Macro Recorder, use Auto Filter to get the selection you seek and switch the Macro Recorder off. Adjust the Macro Recorder's statements to remove Selection and replace the corresponding statements in Demo2.
The secret of Demo2 is Set Rng = .AutoFilter.Range.SpecialCells(xlCellTypeVisible) which sets Rng to the visible rows. If you cannot get Auto Filter to work as you wish and you decide to use your current technique to set uninteresting rows invisible, keep this statement to get the remaining rows. However, I think macro Demo3 uses a better technique.
Option Explicit
Sub demo1()
Dim ColLast As Long
Dim Rng As Range
Dim RowLast As Long
Application.ScreenUpdating = False
With Worksheets("Source")
' This searches up from the bottom of column AB for a cell with a value.
' It is the VBA equivalent of placing the cursor at the bottom of column AB
' and clicking Ctrl+Up.
RowLast = .Cells(Rows.Count, "AB").End(xlUp).Row
Debug.Print "Last row with value in column AB: " & RowLast
' This searches for the last cell with a value.
Set Rng = .Cells.Find(What:="*", After:=.Range("A1"), SearchDirection:=xlPrevious)
If Rng Is Nothing Then
' Worksheet is empty
Else
RowLast = Rng.Row
ColLast = Rng.Column
Debug.Print "Last cell with value is: (" & RowLast & ", " & ColLast & _
") = " & Replace(Rng.Address, "$", "")
End If
End With
End Sub
Sub Demo2()
Dim Rng As Range
Dim SearchDate As String
SearchDate = "14-May-14"
Application.ScreenUpdating = False
With Sheets("Source")
.Cells.AutoFilter
.Cells.AutoFilter Field:=28, Criteria1:=SearchDate
Set Rng = .AutoFilter.Range.SpecialCells(xlCellTypeVisible)
End With
' Rng.Address has a maximum length of a little under 256 characters.
' Rng holds the addresses of all the visible rows but you cannot display
' all those addresses in an easy manner. However, this is only to give
' you an idea of what is in Rng; the Copy statement below uses the full
' set of addresses.
Debug.Print "Visible rows: " & Rng.Address
Rng.Copy Worksheets("Dest").Range("A1")
End Sub
Sub Demo3()
Dim RngToBeCopied As Range
Dim RowCrnt As Long
Dim RowLast As Long
Dim SearchDate As Long
' Excel holds dates as integers and times as fractions.
SearchDate = CLng(DateValue("20 May 2014"))
With Worksheets("Source")
RowLast = .Cells(Rows.Count, "AB").End(xlUp).Row
' Include header row in range to be copied
Set RngToBeCopied = .Rows(1)
For RowCrnt = 2 To RowLast
If .Cells(RowCrnt, "AB").Value = SearchDate Then
Set RngToBeCopied = Union(RngToBeCopied, .Rows(RowCrnt))
End If
Next
End With
Debug.Print RngToBeCopied.Address
RngToBeCopied.Copy Worksheets("Dest").Range("A1")
End Sub

Set printing area In Excel 2013 using macro

In Excel 2013, having sheet named "Tags", I am trying to set a printing area from A2 till end of page, ending with column L.
Worksheets("Tags").PageSetup.PrintArea = Worksheets("Tags").Range( _
Cells(2, 1), Cells(Worksheets("Tags").Range("A65536").End(xlUp).Row, 12))
My code compiles okay, but it does not seems to work - no printing area has been set.
What should be a correct macro to set printing area?
It's easier to see what is happening if you declare a few variables and decompose your statement.
Try this:
Sub SetPrintArea()
Dim ws As Worksheet
Dim lastRow As Long
Set ws = ThisWorkbook.Sheets("Tags")
' find the last row with formatting, to be included in print range
lastRow = ws.UsedRange.SpecialCells(xlCellTypeLastCell).Row
ws.PageSetup.PrintArea = ws.Range("A2:L" & lastRow).Address
End Sub
Alternatively, if you want to find the lastRow with data, you can find the lastrow like this:
lastRow = ws.Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
Note that the 65536 value you're using as the starting point to find the last row is obsolete (although it will frequently still work) as of Excel 2007, which has over a million rows per sheet.
A few things to note about your approach:
Cells(2,1) is A2. The syntax is Cells([row], [column])
You want the last populated row in column L, but are looking in column A instead. Range("A65536").End(xlUp).Row
This results in a print area (once you've added the .Address to your range) of A1:L2. Why A1? Because the column A is empty, and the lastrow is therefore row 1. You have set the range to be A2:L1, which becomes A1:L2.
You need to add .Address at the end of your code.
Worksheets("Tags").PageSetup.PrintArea = Worksheets("Tags").Range( _
Cells(2, 1), Cells(Worksheets("Tags").Range("A65536").End(xlUp).Row, 12)).Address
PageSetup.PrintArea Property
Returns or sets the range to be printed, as a string using A1-style references in the language of the macro. Read/write String.

Resources