Accessing Collection causes Subscript out of range error? - excel

I've got an UserForm, which upon an incorrect user input calls the following procedure, which highlights the field and disables the "save changes" button.
Private disabledElems As New Collection
Private Sub disable(ByRef controlName As String)
UserForm1.Controls(controlName).BackColor = &H8080FF
Me.save_button.Enabled = False
Dim i As Byte
If disabledElems.Count <> 0 Then
For i = 1 To disabledElems.Count
If disabledElems(i) = controlName Then
Exit Sub ' we dont want to add duplicates to collection
End If
Next i
End If
disabledElems.Add controlName ' otherwise add to collection
End Sub
If the input is corrected, it calls the enable procedure, which looks like this:
Private Sub enable(ByRef controlName As String)
Me.Controls(controlName).BackColor = &H80000005
Dim i As Byte
For i = 1 To disabledElems.Count
If disabledElems(i) = controlName Then
disabledElems.Remove i ' remove the enabled element upon match
End If
Next i
If disabledElems.Count = 0 Then
save_button.Enabled = True
End If
End Sub
This seems to work just fine when I try this with one Textbox
However, as soon I have multiple incorrect entries, my enable procedure seems to throw a Subscript out of range error seemingly for no reason.
The highlighted line in the debugger is:
If disabledElems(i) = controlName Then
I can't comprehend what could be causing this. Any ideas?

Ah alright, it's one of those classical "when removing a row, loop
from end to beginning"
Basically, the reason why the Subscript out of range was thrown - once the element was removed from the collection via the
disabledElems.Remove i
It reduced the size of the Collection from Collection.Count to Collection.Count - 1, however during the for loop declaration, the i was already hard-set to the previous Collection.Count
In an practical example:
Let's say my Collection looks like this
disabledElems = "button1", "button2"
Upon doing this
controlName = "button1"
For i = 1 to disabledElems.Count ' <= 2
If disabledElems(i) = controlName ' < True for i = 1
disabledElems.Remove i ' < button1 was removed from collection, however it still loops
End If
' will loop to i = 2. However disabledElems(2) no longer exists, because upon removal _
the button2 was shifted to disabledElems(1) - hence Subscript out of range
Next i
A clear case of trying to access an element, which has shifted its position in the queue.
There are two possible fixes (that I can think of):
1. Enforce Exit Sub upon removal
For i = 1 to disabledElems.Count
If disabledElems(i) = controlName
disabledElems.Remove i
Exit Sub
End If
Next i
2. Loop from end to start
Dim i as Integer ' needs to be redeclared to int, because Byte can't -1
For i = disabledElems.Count to 1 Step -1
If disabledElems(i) = controlName
disabledElems.Remove i
End If
Next i

Related

Handle an array to avoid errors

In userform1, I have the following code
Private Sub cmdOK_Click()
Dim i As Long
With Me.ListBox2
If .ListCount = 0 Then MsgBox "You Have To Select At Least One Column", vbExclamation: GoTo Skipper
ReDim aCols(0 To .ListCount - 1)
For i = 0 To .ListCount - 1
aCols(i) = "[" & ListBox2.List(i, 0) & "]"
Next i
End With
Skipper:
Unload Me
End Sub
and in standard module I declared aCols as public
Public aCols
if listbox2 has no items then aCols became Empty while if there are items the aCols became an array ..
Then in another code I am confused of how to avoid errors
If UBound(aCols) > -1 Then
This works fine if aCols is not empty but I encountered errors if aCols is Empty .. How to deal with both cases
Simply I need to avoid the errors and deal with aCols either it is empty or either it is an array.
I would use Function safeUBound() which looks ugly due to OERN but works fine:
Function safeUBound(a)
safeUBound = -1
On Error Resume Next
safeUBound = UBound(a)
End Function
Another solution is to assign empty array or empty 2d array to the variable aCols either at the very beginning of the code or at userform initialize.

Searching in listview while typing in textbox using vba

I used OnKeyUp so while user is typing, it is searching in the list view. I am getting an error with my code.
"Wrong number of arguments or invalid property assignment"
Here is my code below:
Private Sub txtEmailGenSearch_KeyUp(ByVal KeyCode As
MSForms.ReturnInteger, ByVal Shift As Integer)
Dim strText As String
Dim i As Long
strText = LCase(txtSearch.value)
With MainForm.lstMailGen
For i = 0 To .ListItems.count - 1
If LCase(Left(.ListItems(i, 0), Len(strText))) = strText Then
Exit For
Next i
If i = .ListItems.count Then
' No matching item was found, select nothing
.ListIndex = -1
Else
' A match was found, select it
.ListIndex = i
End If
End With
End Sub
A ListView is quite different from a ListBox. Each row in a ListView is a ListItem. If the ListView has multiple columns then each ListItem will contain ListSubItems. This applies to a Microsoft Windows Common Controls 6.0 Listview, 5.0 works a little different. ListViews do not return a 2D array like a ListBox does have have a ListIndex property.
Recommended Reading: Excel VBA ListView Control Examples
Use ListItem.Find() to locate a matching ListItem
With MainForm.lstMailGen
Dim item As ListItem
Set item = .FindItem(sz:=txtSearch.value, fPartial:=lvwPartial)
If Not item Is Nothing Then
item.Selected = True
End If
End With
In for the ListItem to be hightlighted make sure that HideSelection = False
MainForm.lstMailGen.HideSelection = False
The Listitems first index is 1 not 0.
For i = 1 To .ListItems.Count
If LCase(Left(.ListItems(i), Len(strText))) = strText Then
Exit For
Next i
If the last item contained the string than If i = .ListItems.count Then would skip the selection. If i > .ListItems.count Then is the right way to do this. If the For loop completes then i will be incremented an extra time. In the above case i would = .ListItems.Count + 1` if the loop completed.

Endless VBA Loop UNLESS I step through the code

I have a userform with 6 list objects. All of the list objects have named range rowsources. Clicking any one item in any one list will reference a chart on a spreadsheet and clear contents of any item's cell that does not belong with what was selected (explained better at the bottom of this, if you're interested). All of my list objects only have "After Update" triggers, everything else is handled by private subs.
Anyway, there's a lot of looping and jumping from list to list. If I run the userform normally, it endlessly loops. It seems to run through once, and then acts as though the user has again clicked the same item in the list, over and over again.
The odd thing is, if I step through the code (F8), it ends perfectly, when it's supposed to and control is returned to the user.
Does anyone have any thoughts on why that might be?
EDIT: I didn't originally post the code because all of it is basically a loop, and there's 150+ lines of it. I don't understand how it can be the code if stepping through makes it work perfectly, but allowing it to run regular makes it endless loop. Anyway, here's the code:
Option Explicit
Dim arySelected(6) As String
Dim intHoldCol As Integer, intHoldRow As Integer
Dim strHold As String
Dim rngStyleFind As Range, rngStyleList As Range
Private Sub UserForm_Activate()
Set rngStyleList = Range("Lists_W_Style")
Set rngStyleFind = Range("CABI_FindStyle")
End Sub
Private Sub lstStyle_AfterUpdate()
If lstStyle.ListIndex >= 0 Then
arySelected(0) = lstStyle.Value
Call FilterCabinetOptions(Range("Lists_W_Style"), Range("CABI_FindStyle"), 0)
End If
End Sub
Private Sub lstWood_AfterUpdate()
If lstWood.ListIndex >= 0 Then
arySelected(1) = lstWood.Value
Call FilterCabinetOptions(Range("Lists_W_Wood"), Range("CABI_FindWood"), 1)
' lstWood.RowSource = "Lists_W_Wood"
End If
End Sub
Private Sub cmdReset_Click()
Range("Lists_S_Style").Copy Destination:=Range("Lists_W_Style")
Call RemoveXes(Range("Lists_W_Style"))
Range("Lists_S_Wood").Copy Destination:=Range("Lists_W_Wood")
Call RemoveXes(Range("Lists_W_Wood"))
Range("Lists_S_Door").Copy Destination:=Range("Lists_W_Door")
Call RemoveXes(Range("Lists_W_Door"))
Range("Lists_S_Color").Copy Destination:=Range("Lists_W_Color")
Call RemoveXes(Range("Lists_W_Color"))
Range("Lists_S_Glaze").Copy Destination:=Range("Lists_W_Glaze")
Call RemoveXes(Range("Lists_W_Glaze"))
Range("Lists_S_Const").Copy Destination:=Range("Lists_W_Const")
Call RemoveXes(Range("Lists_W_Const"))
Range("Lists_S_DrawFrontConst").Copy Destination:=Range("Lists_W_DrawFrontConst")
Call RemoveXes(Range("Lists_W_DrawFrontConst"))
End Sub
Private Sub FilterCabinetOptions(rngList As Range, rngFind As Range, intAry As Integer)
Dim intListCntr As Integer, intFindCntr As Integer, intStyleCntr As Integer
If intAry = 0 Then
Call FindStyle(arySelected(intAry))
Else
'Save the List item.
For intListCntr = 1 To rngList.Rows.Count
If rngList.Cells(intListCntr, 1) = arySelected(intAry) Then
rngList.Cells(intListCntr, 3) = "X"
' Call RemoveNonXes(rngList)
Exit For
End If
Next intListCntr
'Save the column of the Find List.
For intFindCntr = 1 To rngFind.Columns.Count
If rngFind.Cells(1, intFindCntr) = arySelected(intAry) Then
'Minus 2 to allow for columns A and B when using Offset in the below loop.
intHoldCol = rngFind.Cells(1, intFindCntr).Column - 2
Exit For
End If
Next intFindCntr
'Find appliciple styles.
For intStyleCntr = 1 To rngStyleFind.Rows.Count
If Len(rngStyleFind.Cells(intStyleCntr, intHoldCol)) > 0 Then
Call FindStyle(rngStyleFind.Cells(intStyleCntr, 1))
End If
Next intStyleCntr
End If
Call RemoveNonXes(rngStyleList)
Call RemoveNonXes(Range("Lists_W_Wood"))
Call RemoveNonXes(Range("Lists_W_Door"))
Call RemoveNonXes(Range("Lists_W_Color"))
Call RemoveNonXes(Range("Lists_W_Glaze"))
Call RemoveNonXes(Range("Lists_W_Const"))
Call RemoveNonXes(Range("Lists_W_DrawFrontConst"))
End Sub
Private Sub FindStyle(strFindCode As String)
Dim intListCntr As Integer, intFindCntr As Integer
For intListCntr = 1 To rngStyleList.Rows.Count
If rngStyleList.Cells(intListCntr, 1) = strFindCode Then
rngStyleList.Range("C" & intListCntr) = "X"
Exit For
End If
Next intListCntr
For intFindCntr = 1 To rngStyleFind.Rows.Count
If rngStyleFind.Cells(intFindCntr, 1) = strFindCode Then
intHoldRow = rngStyleFind.Cells(intFindCntr).Row
Exit For
End If
Next intFindCntr
If Len(arySelected(1)) = 0 Then Call FindStyleOptions(Range("CABI_FindWood"), Range("Lists_W_Wood"))
If Len(arySelected(2)) = 0 Then Call FindStyleOptions(Range("CABI_FindDoor"), Range("Lists_W_Door"))
If Len(arySelected(3)) = 0 Then Call FindStyleOptions(Range("CABI_FindColor"), Range("Lists_W_Color"), Range("Lists_W_Wood"))
If Len(arySelected(4)) = 0 Then Call FindStyleOptions(Range("CABI_FindGlaze"), Range("Lists_W_Glaze"), Range("Lists_W_Wood"))
If Len(arySelected(5)) = 0 Then Call FindStyleOptions(Range("CABI_FindConst"), Range("Lists_W_Const"))
If Len(arySelected(6)) = 0 Then Call FindStyleOptions(Range("CABI_FindDrawFrontConst"), Range("Lists_W_DrawFrontConst"))
End Sub
Private Sub FindStyleOptions(rngFind As Range, rngList As Range, Optional rngCheckList As Range)
Dim intListCntr As Integer, intFindCntr As Integer
Dim intStrFinder As Integer, intCheckCntr As Integer
Dim strHoldCheck As String
Dim strHoldFound As String, strHoldOption As String
'Go through the appropriate find list (across the top of CABI)
For intFindCntr = 1 To rngFind.Columns.Count
strHoldOption = rngFind.Cells(1, intFindCntr)
strHoldFound = rngFind.Cells(1, intFindCntr).Offset((intHoldRow - 1), 0)
If Len(strHoldFound) > 0 Then
If rngCheckList Is Nothing Then
For intListCntr = 1 To rngList.Rows.Count
If rngList.Cells(intListCntr, 1) = strHoldFound Then
Call AddXes(rngList, strHoldFound, "X")
Exit For
End If
Next intListCntr
Else
intStrFinder = 1
Do While intStrFinder < Len(rngFind.Cells(1, intFindCntr).Offset((intHoldRow - 1), 0))
strHoldCheck = Mid(rngFind.Cells(1, intFindCntr).Offset((intHoldRow - 1), 0), intStrFinder, 2)
intStrFinder = intStrFinder + 3
For intCheckCntr = 1 To rngCheckList.Rows.Count
If strHoldCheck = rngCheckList(intCheckCntr, 1) And Len(rngCheckList(intCheckCntr, 3)) > 0 Then
Call AddXes(rngList, strHoldOption, "X")
intStrFinder = 99
Exit For
End If
Next intCheckCntr
Loop
End If
End If
Next intFindCntr
End Sub
Private Sub AddXes(rngList As Range, strToFind As String, strX As String)
Dim intXcntr As Integer
For intXcntr = 1 To rngList.Rows.Count
If rngList.Cells(intXcntr, 1) = strToFind Then
rngList.Cells(intXcntr, 3) = strX
Exit For
End If
Next intXcntr
End Sub
Private Sub RemoveNonXes(rngList As Range)
Dim intXcntr As Integer
For intXcntr = 1 To rngList.Rows.Count
If Len(rngList(intXcntr, 3)) = 0 Then
rngList.Range("A" & intXcntr & ":B" & intXcntr) = ""
Else
rngList.Range("C" & intXcntr) = ""
End If
Next intXcntr
End Sub
Private Sub RemoveXes(rngList As Range)
rngList.Range("C1:C" & rngList.Rows.Count) = ""
End Sub
Explanation:
Imagine you had 6 lists with different automobile conditions. So Make would be one list with Chevy, Ford, Honda... Model would be another with Malibu, Focus, Civic... But you'd also have Color Blue, Red, Green... So if your user wants a Green car, the program references an inventory list and gets rid of any Makes, Models, etc... not available in green. Likewise the user could click on Civic from the Model list and it would elminate all but Honda from the Make and so on. That's what I'm trying to do anyway.
Without seeing the code it's tough to tell. When you run the script, the 'AfterUpdate' event may be getting triggered over and over, causing the endless loop. Try using a counter to limit the update to one change and have it exit the loop once the counter is greater than 0.

Excel UDF detecting page breaks?

I'm trying to write a UDF that returns whether the cell is at a page break.
So far I have this:
Function pbreak() As Boolean
' Application.Volatile
pbreak = False
Dim ra As Range
Set ra = Application.Caller
With ra
For i = 1 To .Worksheet.HPageBreaks.Count
If .Worksheet.HPageBreaks(i).Location.Row = .Row Then
pbreak = True
End If
Next
End With
End Function
This returns a #VALUE error. I've tried debugging it, HPageBreaks.Count returns 3 (and there are 3 page breaks), but HPageBreaks(i) yields an "index out of range"-error for all pagebreaks that are below the current cell .
Is this a bug (ie .Count is wrong), or is there some special behavior with page breaks that I am missing?
Is there a way to fix this (preferably without resorting to on error resume next)?
Thanks
Martin
Option Explicit
Function pbreak() As Boolean
' Application.Volatile
Dim i As Integer 'the missing line
pbreak = False
Dim ra As Range
Set ra = Application.Caller
With ra
For i = 1 To .Worksheet.HPageBreaks.Count
If .Worksheet.HPageBreaks(i).Location.Row <= .Row Then
If .Worksheet.HPageBreaks(i).Location.Row = .Row Then
pbreak = True
'exit the function once a page break is found.
Exit Function
End If
Else
Exit Function
End If
Next
End With
End Function
EDIT: Always use Option Explicit & compile the code before using it.
Use of Exit Function inside the loop is to prevent the code from running it further, once the result is known.

VBA-Sorting the data in a listbox, sort works but data in listbox not changed

A listbox is passed, the data placed in an array, the array is sort and then the data is placed back in the listbox. The part that does work is putting the data back in the listbox. Its like the listbox is being passed by value instead of by ref.
Here's the sub that does the sort and the line of code that calls the sort sub.
Private Sub SortListBox(ByRef LB As MSForms.ListBox)
Dim First As Integer
Dim Last As Integer
Dim NumItems As Integer
Dim i As Integer
Dim j As Integer
Dim Temp As String
Dim TempArray() As Variant
ReDim TempArray(LB.ListCount)
First = LBound(TempArray) ' this works correctly
Last = UBound(TempArray) - 1 ' this works correctly
For i = First To Last
TempArray(i) = LB.List(i) ' this works correctly
Next i
For i = First To Last
For j = i + 1 To Last
If TempArray(i) > TempArray(j) Then
Temp = TempArray(j)
TempArray(j) = TempArray(i)
TempArray(i) = Temp
End If
Next j
Next i ! data is now sorted
LB.Clear ! this doesn't clear the items in the listbox
For i = First To Last
LB.AddItem TempArray(i) ! this doesn't work either
Next i
End Sub
Private Sub InitializeForm()
' There's code here to put data in the list box
Call SortListBox(FieldSelect.CompleteList)
End Sub
Thanks for your help.
This works for me on Excel 2003 on a very basic UserForm with a single ListBox called ListBox1:
Private Sub UserForm_Initialize()
ListBox1.AddItem "john"
ListBox1.AddItem "paul"
ListBox1.AddItem "george"
ListBox1.AddItem "ringo"
SortListBox ListBox1
End Sub
and then your SortListBox as written apart from fixing the three comments which start with ! rather than '
The only difference to your initializer is the name (UserForm_Initialize vs InitializeForm). Make sure to use the object and event selectors at the top of the code page for the userform to ensure that the event handlers get named correctly
You can't pass objects by value. Since you're not going to return another instance of listbox to the caller, you should declare LP as ByVal. That does not affect the code though. It works and the list gets sorted. I think you omitted some importand details.
Here is how I used this, for example, w/ a relational Dictionary and two columns:
Private Sub UserForm_Initialize()
Call HideTitleBar(Me)
Set ExtraFiltersDic = CreateObject("scripting.dictionary")
ExtraFiltersDic.CompareMode = 1
Set ExtraFiltersDic = GetExtraFiltersDic()
Dim k
For Each k In ExtraFiltersDic.Keys
ListBox1.AddItem k
Next
Call SortListBox(ListBox1, ListBox2, ExtraFiltersDic)
End Sub
Public Sub SortListBox(ByRef ListBox As MSForms.ListBox, Optional ByRef ListBox2 As MSForms.ListBox, Optional ByRef RelationalDic As Object)
Dim First As Integer, Last As Integer, NumItems As Integer
Dim i As Integer, j As Integer
Dim TempArray() As Variant, Temp As String
ReDim TempArray(ListBox.ListCount)
First = LBound(TempArray)
Last = UBound(TempArray) - 1
For i = First To Last
TempArray(i) = ListBox.List(i)
Next i
For i = First To Last
For j = i + 1 To Last
If TempArray(i) > TempArray(j) Then
Temp = TempArray(j)
TempArray(j) = TempArray(i)
TempArray(i) = Temp
End If
Next j
Next i
ListBox.Clear
If Not ListBox2 Is Nothing And Not RelationalDic Is Nothing Then
Set KeyValDic = CreateObject("scripting.dictionary")
Set KeyValDic = RelationalDic
End If
For i = First To Last
ListBox.AddItem TempArray(i)
If Not ListBox2 Is Nothing And Not RelationalDic Is Nothing Then
ListBox2.AddItem KeyValDic(TempArray(i))
End If
Next i
End Sub
I dont know if this would work for you but try it this way.
First, make an array of all the items in the list box
Pass that array to your function
Sort that array
return the array to the main program
clear the listbox
overwrite the listbox items with the new array

Resources