Selecting multiple cells - vba

I have this code that check if the attachment size of the attachment is greater than 10MB. Now, if the attachment is greater than 10MB, it displays the file names on a msgbox then I want to select or highlight the cells that has this attachment greater than 10 MB but dunno how to do it.
Here's what I've tried:
Function checkAttSize()
Application.ScreenUpdating = False
Dim attach As Object
Dim attSize() As String
Dim loc() As String
Dim num As Long
Dim rng As Range
Set objOutlook = CreateObject("Outlook.Application")
Set objMail = objOutlook.CreateItem(0)
Set main = ThisWorkbook.Sheets("Main")
lRow = Cells(Rows.count, 15).End(xlUp).Row
efCount = 0
num = 0
With objMail
If lRow > 22 Then
On Error GoTo errHandler
For i = 23 To lRow
'attach.Add main.Range("O" & i).value
'totalSize = totalSize +
If (FileLen(main.Cells(i, "O").value) / 1000000) > 10 Then
ReDim Preserve attSize(efCount)
ReDim Preserve loc(num)
'store file names
attSize(efCount) = Dir(main.Range("O" & i))
'store cell address
loc(num) = i
efCount = efCount + 1
num = num + 1
found = True
End If
Next i
End If
End With
If found = True Then
MsgBox "Following File(s) Exceeds 10MB Attachment Size Limit:" & vbCrLf & vbCrLf & Join(attSize, vbCrLf) _
& vbCrLf & vbCrLf & "Please try removing the file(s) and try again.", vbCritical, "File Size Exceed"
'trying to select the cell addresses
For i = 1 To num
rng = rng + main.Range("O" & loc(i)).Select ' Ive also tried &
Next i
checkAttSize = True
Exit Function
End If
Exit Function
errHandler:
MsgBox "Unexpected Error Occured.", vbCritical, "Error"
checkAttSize = True
End Function
Thanks for the help.

No need to select the range. A single miss click by the user take take the focus away from the range. Also using .Select recklessly may cause run time errors. Color them instead.
After this line
If (FileLen(main.Cells(i, "O").value) / 1000000) > 10 Then
Add this line
main.Cells(i, "O").Interior.ColorIndex = 3
The cells now will be colored in red.
And in the end, alert the user with the message
If found = True Then
MsgBox "File(s) Exceeding 10MB Attachment Size Limit has been colored in red:"
End If

Related

Excel VBA crashing due to size

I made a script in VBA that should read a very long Pivot Table with over 190,000 entries in the "Data" sheet, and according with the value in the column "J", it should write the info from that row in a sheet called "Temp".
When the value from column "A" changes, it should read from sheet "Regioner" a list of over 600 entries and check if each value is presented in the previous arrays of values.
The code I wrote works, but it takes forever to write down the expected 220,000 entries in the "Temp" sheet. In my laptop, i5 6th generation with 8Gb RAM, it simply crashes.
The current code is as per below.
Many thanks to all!
Public Sub FindWithoutOrder()
Dim DataRowCounter As Long
Dim TempRowCounter As Long
Dim RegiRowCounter As Long
Dim DataOldCounter As Long
Dim DataNewCounter As Long
Dim loopCounter As Long
Dim DataOldProd As Range
Dim DataNewProd As Range
Dim DataPurchase As Range
Dim RegiButikk As Range
Dim ButikkFlag As Boolean
'Code optimization to run faster.
Application.ScreenUpdating = False
Application.DisplayAlerts = False
'Initialize variables.
'----------------------------------------------------------------------------------------------------------
DataRowCounter = 11
TempRowCounter = 1
DataOldCounter = 11
DataNewCounter = 11
Set DataOldProd = ActiveWorkbook.Sheets("Data").Range("A" & DataRowCounter)
Set DataNewProd = ActiveWorkbook.Sheets("Data").Range("A" & DataRowCounter)
Set DataPurchase = ActiveWorkbook.Sheets("Data").Range("J" & DataRowCounter)
'Start of loop that verifies all values inside "Data" sheet.
'----------------------------------------------------------------------------------------------------------
Do Until (IsEmpty(DataOldProd) And IsEmpty(DataNewProd))
'Verify if the product of new line is still the same or different.
'------------------------------------------------------------------------------------------------------
If DataNewProd.Value = DataOldProd.Value Then
DataNewCounter = DataNewCounter + 1
Else
'Initialize variables from "Regioner" sheet.
'------------------------------------------------------------------------------------------
ButikkFlag = False
RegiRowCounter = 11
Set RegiButikk = ActiveWorkbook.Sheets("Regioner").Range("C" & RegiRowCounter)
'Verify list of supermarkets and match them with purchases list.
'--------------------------------------------------------------------------------------------------
Do Until IsEmpty(RegiButikk)
'Check all supermarkets in the product range.
'----------------------------------------------------------------------------------------------
For loopCounter = DataOldCounter To DataNewCounter - 1
'Compare both entries and register them if it doesn't exist in the product list.
'------------------------------------------------------------------------------------------
If RegiButikk.Value = ActiveWorkbook.Sheets("Data").Range("D" & loopCounter).Value Then
ButikkFlag = True
RegiRowCounter = RegiRowCounter + 1
Set RegiButikk = ActiveWorkbook.Sheets("Regioner").Range("C" & RegiRowCounter)
Exit For
Else
ButikkFlag = False
End If
Next loopCounter
'Add to list supermarkets not present in the purchases list.
'------------------------------------------------------------------------------------------
If ButikkFlag = False Then
ActiveWorkbook.Sheets("Temp").Range("B" & TempRowCounter & ":D" & TempRowCounter).Value = ActiveWorkbook.Sheets("Regioner").Range("A" & RegiRowCounter & ":C" & RegiRowCounter).Value
ActiveWorkbook.Sheets("Temp").Range("A" & TempRowCounter).Value = ActiveWorkbook.Sheets("Data").Range("A" & DataRowCounter - 1).Value
TempRowCounter = TempRowCounter + 1
RegiRowCounter = RegiRowCounter + 1
Set RegiButikk = ActiveWorkbook.Sheets("Regioner").Range("C" & RegiRowCounter)
End If
Loop
'Reset the product range.
'--------------------------------------------------------------------------------------------------
DataOldCounter = DataNewCounter
DataNewCounter = DataNewCounter + 1
End If
'Validate if item was purchased in the defined period and copy it.
'------------------------------------------------------------------------------------------------------
If DataPurchase.Value = 0 Then
ActiveWorkbook.Sheets("Temp").Range("A" & TempRowCounter & ":D" & TempRowCounter).Value = ActiveWorkbook.Sheets("Data").Range("A" & DataRowCounter & ":D" & DataRowCounter).Value
TempRowCounter = TempRowCounter + 1
End If
'Update row counter and values for previous and new product readed.
'------------------------------------------------------------------------------------------------------
Set DataOldProd = ActiveWorkbook.Sheets("Data").Range("A" & DataRowCounter)
DataRowCounter = DataRowCounter + 1
Set DataNewProd = ActiveWorkbook.Sheets("Data").Range("A" & DataRowCounter)
Set DataPurchase = ActiveWorkbook.Sheets("Data").Range("J" & DataRowCounter)
Loop
'Code optimization to run faster.
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub
Instead of having this code scattered all over the place:
'Code optimization to run faster.
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Use this procedure:
Public Sub ToggleWaitMode(ByVal wait As Boolean)
Application.Cursor = IIf(wait, XlMousePointer.xlWait, XlMousePointer.xlDefault)
Application.StatusBar = IIf(wait, "Working...", False)
Application.Calculation = IIf(wait, XlCalculation.xlCalculationManual, XlCalculation.xlCalculationAutomatic)
Application.ScreenUpdating = Not wait
Application.EnableEvents = Not wait
End Sub
Like this:
Public Sub DoSomething()
ToggleWaitMode True
On Error GoTo CleanFail
'do stuff
CleanExit:
ToggleWaitMode False
Exit Sub
CleanFail:
'handle errors
Resume CleanExit
End Sub
Disabling automatic calculation and worksheet events should already help quite a lot... but it's by no means "optimizing" anything. It simply makes Excel work [much] less, whenever a cell is modified.
If your code works but is just slow, take it to Code Review Stack Exchange and present it to the VBA reviewers: they'll go out of their ways to help you actually optimize your code. I know, I'm one of them =)

Run macro on row most recently added

I'm using a macro to generate an Outlook template based on data that has been entered into a workbook.
In the workbook I have 100 rows of data and 7 sheets.
I need to run the macro (on click of a button) on the most recent row's data and generate the template.
My rows contain time data (example 13:37, next row 14:02 etc) so I think that could be a good way to identify the latest row.
I'm using this code. I'm selecting the row using A203:G203
Sub NonConformanceGenerator()
ActiveSheet.Range("A203:G203").Select
Const HEADER_ROW As Long = 202 '<< the row with column headers
Const NUM_COLS As Long = 7 '<< how many columns of data
Const olMailItem = 0
Const olFolderInbox = 6
Dim ol As Object, fldr, ns, msg
Dim html As String, c As Range, colReq As Long, hdr As Range
Dim rw As Range
On Error Resume Next
Set ol = GetObject(, "outlook.application")
On Error GoTo 0
If ol Is Nothing Then
On Error Resume Next
Set ol = CreateObject("outlook.application")
Set ns = ol.GetNamespace("MAPI")
Set fldr = ns.GetDefaultFolder(olFolderInbox)
fldr.display
On Error GoTo 0
End If
If ol Is Nothing Then
MsgBox "Couldn't start Outlook to compose mail!", vbExclamation
Exit Sub
End If
Set msg = ol.CreateItem(olMailItem)
Set rw = Selection.Cells(1).EntireRow
msg.Subject = ""
html = "<style type='text/css'>"
html = html & "body, p {font:11pt calibri;padding:40px;}"
html = html & "table {border-collapse:collapse}"
html = html & "td {border:1px solid #000;padding:8px;}"
html = html & "</style>"
html = html & "<p>Hello,</p>"
html = html & "<table>"
For Each c In rw.Cells(1).Resize(1, NUM_COLS).Cells
If c.Column <> 0 Then '<<< This removes the 4th column if you type number 4 after the <> symbols
Set hdr = rw.Parent.Cells(HEADER_ROW, c.Column) '<< get the header text for this cell
html = html & "<tr><td style='background-color:#FFF;width:200px;'>" & _
hdr.Value & _
"</td><td style='width:400px;'>" & Trim(c.Value) & "</td></tr>"
End If 'we want this cell
Next c
html = html & "</table>"
msg.HTMLBody = html
msg.display
ActiveSheet.Range("A15").Select
End Sub
Is the newest row always at the bottom of the spreadsheet? If so, you can use Cells(Rows.Count, "A").End(xlUp).Row to return the last row with data in Column "A" for instance.
You could do something like this for use in your example.
With ActiveSheet
.Range("A" & .Cells(.Rows.Count, "A").End(xlUp).Row).Resize(1, 7).Select
End With

Check if a value is present in a range or not with VBA

I'm looking to check if a value is present in a range or not. If it's not there then I want it to jump to WriteProcess else I want it to give a message box saying it's present and exit the sub.
This is code,
'Write the Selected Value in the Range - Next Available row in the Column of Source
For i = TableStartingRow + 1 To AddNewEntrow
If Range(EntryColLett & i).Value = wb21Tool.Sheets("Home").ComboBox1.Value Then
MsgBox "The data exists in the Table"
GoTo StopSub
Else
GoTo WriteProcess
End If
Next
WriteProcess:
wbTool.Sheets("Home").Range(EntryColLett & AddNewEntrow).Value = wb21Tool.Sheets("Home").ComboBox1.Value
StopSub:
'Turn on the ScreenUpdate
Application.ScreenUpdating = True
Please share your thoughts. Thanks.
Your problem is that if the loop expires (exhausts all of the iterations) there is no control to prevent it from entering the WriteProcess.
This is one problem with using GoTo statements. Preferably keep these to a minimum. For example, although this doesn't check every row, just an example of how you might avoid the extra GoTo.
'Write the Selected Value in the Range - Next Available row in the Column of Source
For i = TableStartingRow + 1 To AddNewEntrow
If Range(EntryColLett & i).Value = wb21Tool.Sheets("Home").ComboBox1.Value Then
MsgBox "The data exists in the Table"
GoTo StopSub
Else
wbTool.Sheets("Home").Range(EntryColLett & AddNewEntrow).Value = wb21Tool.Sheets("Home").ComboBox1.Value
End If
Next
StopSub:
'Turn on the ScreenUpdate
Application.ScreenUpdating = True
However, a brute-force iteration over the table data seems unnecessary and if you need to check all rows int he table it's probably better to just use the Find method.
Assuming EntryColLet is a string representing the column letter:
Dim tblRange as Range
Dim foundRow as Range
Set tblRange = Range(EntryColLet & (TableStartingRow+1) & ":" & EntryColLet & AddNewEntRow)
Set foundRow = tblRange.Find(wb21Tool.Sheets("Home").ComboBox1.Value)
If foundRow Is Nothing Then
'The value doesn't exist in the table, so do something
'
Else
'The value exists already
MsgBox "The data exists in the Table"
GoTo StopSub
End If
'More code, if you have any...
StopSub:
Application.ScreenUpdating = True
And regarding the remaining GoTo -- if there's no more code that executes after the condition If foundRow Is Nothing then you can remove the entire Else clause and the GoTo label:
Dim tblRange as Range
Dim foundRow as Range
Set tblRange = Range(EntryColLet & (TableStartingRow+1) & ":" & EntryColLet & AddNewEntRow)
Set foundRow = tblRange.Find(wb21Tool.Sheets("Home").ComboBox1.Value)
If foundRow Is Nothing Then
'The value doesn't exist in the table, so do something
End If
Application.ScreenUpdating = True
End Sub
Alternate solution if you need to check every row before performing the "WriteProcess":
Dim bExists As Boolean
bExists = False
'Write the Selected Value in the Range - Next Available row in the Column of Source
For i = TableStartingRow + 1 To AddNewEntrow
If Range(EntryColLett & i).Value = wb21Tool.Sheets("Home").ComboBox1.Value Then
bExists = True
MsgBox "The data exists in the Table"
Exit For
End If
Next
If Not bExists Then wbTool.Sheets("Home").Range(EntryColLett & AddNewEntrow).Value = wb21Tool.Sheets("Home").ComboBox1.Value
'Turn on the ScreenUpdate
Application.ScreenUpdating = True

VBA: error 91 Object variable or with block variable not set

I faced a weird issue of VBA error 91. I saw many other people have this problem because they didn't use keyword "Set" for object, whereas that is not my case.
Following is my code:
Dim eventWS As Worksheet
Set eventWS = Worksheets("Event Sheet")
Dim eventRange As Range
Set eventRange = eventWS.Columns("A:A").Find(240, , xlValues, xlWhole)
If Not eventRange Is Nothing Then
Dim eventFirstAddress As String
eventFirstAddress = eventRange.Address
Do
If eventWS.Range("L" & eventRange.Row).Value = busId Then
If commuter = True Then
Count = Count + Affected(eventWS.Range("Q" & eventRange.Row).Value)
Else
Count = Count + 1
End If
End If
MsgBox("Before call move next: " & eventRange.Row )
Set eventRange = eventWS.Columns("A:A").FindNext(eventRange)
MsgBox("After call move next: " & eventRange.Row )
Loop While Not eventRange Is Nothing And eventRange.Address <> eventFirstAddress
End If
Affected() is a function I can call to do internal processing. And if I removed this "Count = Count + Affected(....)", the code was working fine. If I added it, "Loop While " would throw error 91. If I added a message box to print out the row number before and after moving eventRange, it turned out that "MsgBox("After call move next: " & eventRange.Row)" would throw error 91.
Hence, I'm confuse whether the issue is caused by the internal function or the eventRange now. Hope someone can point my mistakes out. Thank you very much.
Following are the codes of internal function:
Function Affected(markerId As Integer) As Integer
'initialized return value'
AffectedCoummters = 0
'get total financial sheets'
Dim totalFinancial As Integer
totalFinancial = 0
For Each ws In Worksheets
If InStr(ws.Name, "Financial") > 0 Then
totalFinancial = totalFinancial + 1
End If
Next
Dim i As Integer
'run through all financial sheets'
For i = 1 To totalFinancial
'get current financial sheet'
Dim financialWS As Worksheet
Set financialWS = Worksheets("Financial Sheet" & i)
'get total rows of current operation sheet'
Dim rowSize As Long
rowSize = financialWS.Range("A" & financialWS.Rows.Count).End(xlUp).Row
'if reach the maximum number of rows, the value will be 1'
'reInitialize rowSize based on version of Excel'
If rowSize = 1 Then
If Application.Version = "12.0" Then
'MsgBox ("You are using Excel 2007")'
If InStr(ThisWorkbook.Name, ".xlsx") > 0 Then
rowSize = 1048576
Else
'compatible mode'
rowSize = 65536
End If
ElseIf Application.Version = "11.0" Then
'MsgBox ("You are using Excel 2003")'
rowSize = 65536
End If
End If
'filter by marker id first inside current financial sheet'
Dim findMarker As Range
Set findMarker = financialWS.Columns("K:K").Find(markerId, , xlValues, xlWhole)
'if found any given marker id'
If Not findMarker Is Nothing Then
Dim firstAddress As String
firstAddress = findMarker.Address
'check all matched marker id'
Do
AffectedCommuters = AffectedCommuters + financialWS.Range("O" & findMarker.Row).Value
'move to next'
Set findMarker = financialWS.Columns("K:K").FindNext(findMarker)
Loop While Not findMarker Is Nothing And findMarker.Address <> firstAddress
End If
Next i
End Function
Sorry I dont have enough rep to comment so I have to answer here :(
Just want to say that although it is standard procedure to use
Loop While Not eventRange Is Nothing And eventRange.Address <> eventFirstAddress
in this type of procedure, if eventRange is actually Nothing, the line will throw Error 91, because eventRange.address does not exists. What this means is that once you have found something, you can't modify the row in such a way that it will not be found again using .findnext.
After you exit the do...loop, you can modifiy the range to suit...
Perhaps you want to use an array to hold all the rows from your .find...findnext results, and then manipulate them after the Do...loop

Store ActiveWindow.SelectedSheets as an object to refer to later

I'm trying to write a macro that will create a table of contents, listing the name of each of the worksheets currently selected by the user, together with the number of the page on which it starts when printed. I've taken the code from this page and adapted it a little as below.
However, when the new worksheet ("Contents") is created, that becomes the active, selected sheet, such that I can no longer use ActiveWindow.SelectedSheets to refer back to the collection of worksheets selected by the user. So I would like to store that information before creating the new sheet. How can I do this?
I have tried assigning it to a variable of type Worksheets as you can see, but this generates an error message. (I also tried Collection but to no avail.)
Sub CreateTableOfContents()
' Determine if there is already a Table of Contents
' Assume it is there, and if it is not, it will raise an error
' if the Err system variable is > 0, you know the sheet is not there
Dim WST As Worksheet
Dim SelSheets As Worksheets
Set SelSheets = ActiveWindow.SelectedSheets
On Error Resume Next
Set WST = Worksheets("Contents")
If Not Err = 0 Then
' The Table of contents doesn't exist. Add it
Set WST = Worksheets.Add(Before:=Worksheets("blankMagnitude"))
WST.Name = "Contents"
End If
On Error GoTo 0
' Set up the table of contents page
WST.[A2] = "Table of Contents"
With WST.[A6]
.CurrentRegion.Clear
.Value = "Subject"
End With
WST.[B6] = "Page(s)"
WST.Range("A1:B1").ColumnWidth = Array(36, 12)
TOCRow = 7
PageCount = 0
' Do a print preview on all sheets so Excel calcs page breaks
' The user must manually close the PrintPreview window
Msg = "Excel needs to do a print preview to calculate the number of pages." & vbCrLf & "Please dismiss the print preview by clicking close."
MsgBox Msg
SelSheets.PrintPreview
' Loop through each sheet, collecting TOC information
For Each S In SelSheets
If S.Visible = -1 Then
S.Select
ThisName = ActiveSheet.Name
HPages = ActiveSheet.HPageBreaks.Count + 1
VPages = ActiveSheet.VPageBreaks.Count + 1
ThisPages = HPages * VPages
' Enter info about this sheet on TOC
WST.Select
Range("A" & TOCRow).Value = ThisName
Range("B" & TOCRow).NumberFormat = "#"
If ThisPages = 1 Then
Range("B" & TOCRow).Value = PageCount + 1 & " "
Else
Range("B" & TOCRow).Value = PageCount + 1 & " " ' & - " & PageCount + ThisPages
End If
PageCount = PageCount + ThisPages
TOCRow = TOCRow + 1
End If
Next S
End Sub
I just amended your code. Is this what you are trying? Honestly all you had to do was
Change Dim SelSheets As Worksheets to Dim SelSheets and your original code would have worked :)
Option Explicit
Sub CreateTableOfContents()
Dim WST As Worksheet, S As Worksheet
Dim SelSheets
Dim msg As String
Dim TOCRow As Long, PageCount As Long, ThisPages As Long
Dim HPages As Long, VPages As Long
Set SelSheets = ActiveWindow.SelectedSheets
On Error Resume Next
Application.DisplayAlerts = False
Worksheets("Contents").Delete
Application.DisplayAlerts = True
On Error GoTo 0
Set WST = Worksheets.Add(Before:=Worksheets("blankMagnitude"))
With WST
.Name = "Contents"
.[A2] = "Table of Contents"
.[A6] = "Subject"
.[B6] = "Page(s)"
.Range("A1:B1").ColumnWidth = Array(36, 12)
End With
TOCRow = 7: PageCount = 0
msg = "Excel needs to do a print preview to calculate the number of pages." & vbCrLf & "Please dismiss the print preview by clicking close."
MsgBox msg
SelSheets.PrintPreview
For Each S In SelSheets
With S
HPages = .HPageBreaks.Count + 1
VPages = .VPageBreaks.Count + 1
ThisPages = HPages * VPages
WST.Range("A" & TOCRow).Value = .Name
WST.Range("B" & TOCRow).NumberFormat = "#"
If ThisPages = 1 Then
WST.Range("B" & TOCRow).Value = PageCount + 1 & " "
Else
WST.Range("B" & TOCRow).Value = PageCount + 1 & " " ' & - " & PageCount + ThisPages
End If
PageCount = PageCount + ThisPages
TOCRow = TOCRow + 1
End With
Next S
End Sub
EDIT: One important thing. It's always good to use OPTION EXPLICIT :)
You could store references to each sheet;
function getSheetsSnapshot() as Worksheet()
dim shts() As Worksheet, i As long
redim shts(ActiveWindow.SelectedSheets.Count - 1)
for i = 0 to ActiveWindow.SelectedSheets.Count - 1
set shts(i) = ActiveWindow.SelectedSheets(i + 1)
next
getSheetsSnapshot = shts
end function
fetch & store them:
dim oldsel() as Worksheet: oldsel = getSheetsSnapshot()
do your stuff then refer back to the original selected sheets;
for i = 0 to ubound(oldsel)
msgbox oldsel(i).name
next
Dim wks as Worksheet, strName as String
For each wks in SelSheets
strName = strName & wks.Name & ","
Next
strName = Left(strName, Len(strName) -1)
Dim arrWks() as String
arrWks = Split(strName,",")
End Sub
Your will have all the selected sheets, by name, in an arrWks, which you can then process through. You could also add each sheet name to a collection as well in the loop making it smoother.
It's best to stay away from ActiveSheet as much as possible. In this way you can loop through array with a counter and process
So:
Dim intCnt as Ingeter
For intCnt = Lbound(arrWks) to UBound(arrWks)
Worksheets(arrWks(intCnt)).Activate
.... rest of code ....
Next
replaces
For Each S In SelSheets