Related
As a single-column listbox, this works and brings back all the data I need, however, upon making this a multi-column listbox, it throws me the error "Run-time error '381': Could not set the List property. Invalid property array index" - see code line ListBox1.List(1, 0) = MyArray(i, 1)
Columns are correctly shown when I add the incorrect List Property and I've tried adding the List Property where I think it should be, but it shows the whole list again and replaces the 2nd row with one of the correct search results, the 1st row never changes.
Main code is from How to filter listbox values based on a Textbox value and I've used the comment by #xris23 to amend into the multi-column listbox thus far.
Please help!
Sub CommandButton1_Click()
Dim i As Long, j As Long, rw As Long
Dim xlApp As Excel.Application
Dim xlBook As Excel.Workbook
Dim ws As Excel.Worksheet
Dim rng As Range
Dim MyArray As Variant
Dim sPath As String
sPath = "U:\GroupEmailDataCut.xlsx"
On Error Resume Next
Set xlApp = GetObject(, "Excel.Application")
On Error GoTo 0
If xlApp Is Nothing Then
Set xlApp = CreateObject("Excel.Application")
xlApp.Visible = True
End If
Set xlBook = xlApp.Workbooks.Open(sPath)
Set ws = xlBook.Sheets("GroupEmailDataCut")
Set rng = ws.Range("A2:D" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row)
MyArray = rng
With UserForm1.ListBox1
.Clear
.ColumnHeads = False
.ColumnCount = rng.Columns.Count
If ws.Range("A" & ws.Rows.Count).End(xlUp).Row > 1 And Trim(UserForm1.TextBox1.Value) <> vbNullString Then
MyArray = ws.Range("A2:D" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row).Value2
For i = LBound(MyArray) To UBound(MyArray)
For j = LBound(MyArray, 1) To UBound(MyArray, 1)
If InStr(1, MyArray(i, 1), Trim(UserForm1.TextBox1.Value), vbTextCompare) Then
UserForm1.ListBox1.AddItem
ListBox1.List(1, 0) = MyArray(i, 1) '<----Error is across ListBox1.List (MyArray is working as intended)
ListBox1.List(1, 1) = MyArray(i, 2)
ListBox1.List(1, 2) = MyArray(i, 3)
ListBox1.List(1, 3) = MyArray(i, 4)
End If
Next
Next
End If
.List = MyArray
If UserForm1.ListBox1.ListCount = 1 Then UserForm1.ListBox1.Selected(0) = True
End With
End Sub
There are several points to observe to make the command button event readable or even runnable:
Don't address controls by referencing the Userform's default instance (returning the public members of that object),
reference instead the current instance starting either with the Me. qualifier or simply by naming the userform control alone. Understand the Userform (class) code behind as sort of pattern which is blue-printed to the current instance(s).
You are using .AddItem to fill a listbox with items (which is okay for e.g. 4 columns); consider, however that this method automatically builds 10 list columns, but not less or more; this can be avoided .. a) by assigning an entire 2-dim array to the .List (or .Column) property or .. b) by a workaround assigning an empty array plus immediate clearing which allows .AddItem also for greater column numbers (integrated in section 2a)
.List indices are zero-based, i.e. they start with 0 whereas the datafield array with its row and column indices is one-based: .List(cnt, c - 1) = data(r, c) - See section 2b)
Furthermore I separated the check for findings into a help function IsFound(), integrated list assignments into loops and tried to make code more readable by some other intermediate variable declarations.
Private Sub CommandButton1_Click()
'~~~~~~~~~~~~~~
'1) Define data
'~~~~~~~~~~~~~~
'1a)Define search string
Dim srch As String: srch = Trim(Me.TextBox1.Value)
'1b)Define data range
Dim ws As Worksheet: Set ws = Tabelle1
Dim lastRow As Long: lastRow = ws.Range("A" & ws.Rows.Count).End(xlUp).Row
Dim rng As Range: Set rng = ws.Range("A2:D" & lastRow)
'1c)Provide for possible exit
If rng.Row < 1 Or srch = vbNullString Then Exit Sub
'1d)Get 1-based 2-dim datafield array
Dim data() As Variant: data = rng.Value
'~~~~~~~~~~~~~~
'2) Fill ListBox
'~~~~~~~~~~~~~~
With Me.ListBox1
'2a)Set listbox properties & clear list
.ColumnHeads = False
.MultiSelect = fmMultiSelectMulti
'Provide for ColumnCount > 10
Dim ColCnt As Long: ColCnt = rng.Columns.Count
.ColumnCount = ColCnt
If ColCnt > 10 Then
Dim tmp(): ReDim tmp(1 To 1, 1 To ColCnt)
.List = tmp
End If
.Clear ' clear the ListBox
'2b)Loop through rows & add findings to list
Dim r As Long, c As Long, cnt As Long
For r = LBound(data) To UBound(data) ' 1 ..
If IsFound(srch, data, r) Then ' << help function IsFound()
.AddItem ' add item
For c = LBound(data, 2) To UBound(data, 2)
.List(cnt, c - 1) = data(r, c) '<< .List is zero-based!
Next
cnt = cnt + 1 ' increment counter
End If
Next
If .ListCount = 1 Then .Selected(0) = True
End With
End Sub
Help function IsFound()
Function IsFound(srch As String, arr, ByVal r As Long) As Boolean
'Purp.: check if search string is found in given 2-dim array row
Dim c As Long ' column index
For c = LBound(arr, 2) To UBound(arr, 2)
If InStr(1, arr(r, c), srch, vbTextCompare) Then
IsFound = True ' positive function result
Exit For ' one finding suffices
End If
Next
End Function
Further links
You'll find an outstanding article giving you some insight at UserForm1.Show?
Treating the different show modes you might be interested also in Destroy a modeless Userform instance properly
This code creates an array off a range.
When I try to see if the array is saving the elements, by using Debug.Print, nothing is shown in the Immediate Window. It displays blank spaces. There are no errors.
This only happens in that part of the code, the first Debug.Print line works. I do have data in the columns.
Dim myArray() As Variant
Dim iCountLI As Long
Dim iElementLI As Long
If IsEmpty(Range("B3").Value) = True Then
ReDim myArray(0, 0)
Else
iCountLI = Sheets("Sheet1").Range("B3").End(xlDown).Row
iCountLI = (Range("B3").End(xlDown).Row) - 2
Debug.Print iCountLI
ReDim myArray(iCountLI)
For iElementLI = 1 To iCountLI
myArray(iElementLI - 1) = Cells(iElementLI + 2, 2).Value
Debug.Print myArray(iElementLI)
Next iElementLI
End If
Immediate problem: ReDim myArray(iCountLI) creates an array with empty values. In the For Loop, myArray(iElementLI - 1) = Cells(iElementLI + 2, 2).Value overwrites the first, second, etc. values, yet your Debug.Print myArray(iElementLI) is printing the second, third, etc. values. These, of course, haven't yet been overwritten, so they are still empty. The easiest fix, then, is to use Debug.Print myArray(iElementLI - 1).
More generally, I think you might be misunderstanding the meaning of ReDim myArray(iCountLI). Let's assume we have values in B3:B7. This would lead to ReDim myArray(5) in your code, but this is an array with 6 empty values, at location 0,1,2,3,4,5. This means you will keep an empty value trailing in the array after your loop, which is probably not what you want.
Here's a suggested rewrite with some comments:
Sub FillArray()
Dim myArray() As Variant
Dim iCountLI As Long
Dim iElementLI As Long
'let's assume: B3:B7 with values: 1,2,3,4,5
If IsEmpty(Range("B3").Value) = True Then
ReDim myArray(0, 0)
Else
'iCountLI = Sheets("Sheet1").Range("B3").End(xlDown).Row
'this line serves no purpose: you are immediately reassigning the value in the next line
'iCountLI = (Range("B3").End(xlDown).Row) - 2 'This would be 5,
'but myArray(5) would have SIX elements
iCountLI = (Range("B3").End(xlDown).Row) - 3
Debug.Print iCountLI '4
ReDim myArray(iCountLI)
'For iElementLI = 1 To iCountLI
For iElementLI = 0 To iCountLI '0 to 4
myArray(iElementLI) = Cells(iElementLI + 3, 2).Value 'starting at 0, so "+3", not "+2"
'myArray(iElementLI - 1) = Cells(iElementLI + 2, 2).Value
Debug.Print myArray(iElementLI) 'in succession: 1,2,3,4,5
Next iElementLI
End If
End Sub
Finally, it is worth pointing out that you don't actually need a For Loop to populate an array with values from a range. You could use something as follows:
Sub FillArrayAlt()
Dim myArray() As Variant
Dim iCountLI As Long
Dim iElementLI As Long
Dim myRange As Range
'let's assume: B3:B7 with values: 1,2,3,4,5
If IsEmpty(Range("B3").Value) = True Then
ReDim myArray(0, 0)
Else
Set myRange = Range("B3:" & Range("B3").End(xlDown).Address)
myArray = Application.Transpose(myRange.Value)
'N.B. Confusingly, used in this way, your array WILL start at 1!
For i = LBound(myArray) To UBound(myArray)
Debug.Print i; ":"; myArray(i)
' 1 : 1
' 2 : 2
' 3 : 3
' 4 : 4
' 5 : 5
Next i
End If
End Sub
You are valuing myArray(iElementLI -1) and printing myArray(iElementLI), which is still empty.
my code copies all the values of a table in excel on an array an filter them and fill a combobox with it, but I keep geting this error on my code and after debuging it's seems that the error is due to Redim Preserve ... can you check it please ?
' FIll CB2()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("D1")
Dim LC As Long
Dim i As Long
Dim PN As Long
Dim myArray() As String
Dim j As Long
Dim k As Long
Dim temp As String
LC = ws.Cells(ws.Rows.Count, 4).End(xlUp).Row
For i = 1 To LC
If StrComp(CB1.List(CB1.ListIndex, 0), ws.Cells(i, 4), vbTextCompare) = 0 Then
'Set you array with the right dimension
ReDim Preserve myArray(0 To PN, 0 To 1)
myArray(PN, 0) = ws.Cells(i, 2)
myArray(PN, 1) = ws.Cells(i, 3)
PN = PN + 1
End If
Next i
End Sub
There is nothing to "Preserve" when the Redim statement is called for the first time in your loop. Call Redim without "Preserve" when you dimension the array for the first time.
If the line of code that dimensions variables is real code it is surprising that it doesn't call an error. I suggest to place each Dim statement in a line by itself, for better readability of the code if for no other reason, and avoid the use of the colon quite generally but especially for the purpose of mixing declarations with value assignment.
I use the code hereunder to calculate max values as described in this post (vba max value of group of values). The code works great but once I have more than 65k lines I get a data type mismatch when trying to pase the array:
sht.Range(Cells(1, lColumn), Cells(last.Row, lColumn)).Value = Application.Index(groupsArray, , lColumn)
Could somebody help me to slice the array in chunks. I have tried to get it working myself but without any luck.
Sub FillGroupsMax()
Dim lColumn As Long
Dim sht As Worksheet
Dim groupsArray As Variant 'array with all group infomation
Dim groupsSeen As Variant 'array with group infomation already seen
Application.ScreenUpdating = False 'stop screen updating makes vba perform better
Set sht = ThisWorkbook.Worksheets("import")
Set last = sht.Range("A:A").Find("*", Cells(1, 1), searchdirection:=xlPrevious) 'last cell with value in column A
lColumn = sht.Cells(1, Columns.Count).End(xlToLeft).Column
groupsArray = sht.Range(Cells(1, 1), Cells(last.Row, lColumn))
'collect all the information on the Sheet into an array
'Improves performance by not visiting the sheet
For dRow = 2 To last.Row 'for each of the rows skipping header
'check if group as already been seen
If inArrayValue(Cells(dRow, 1).Value, groupsSeen) > 0 Then
'if it has been seen/calculated attribute value
'Cells(dRow, 4).Value = inArrayValue(Cells(dRow, 1).Value, groupsSeen)
groupsArray(dRow, lColumn) = inArrayValue(Cells(dRow, 1).Value, groupsSeen)
Else
'if it hasn't been seen then find max
'Cells(dRow, 4).Value = getMax(Cells(dRow, 1).Value, groupsArray)
groupsArray(dRow, lColumn) = getMax(Cells(dRow, 1).Value, groupsArray, lColumn)
'array construction from empty
If IsEmpty(groupsSeen) Then
ReDim groupsSeen(0)
'groupsSeen(0) = Array(Cells(dRow, 1).Value, Cells(dRow, 4).Value)
groupsSeen(0) = Array(groupsArray(dRow, 1), groupsArray(dRow, lColumn))
'attribute value to array
Else
ReDim Preserve groupsSeen(0 To UBound(groupsSeen) + 1)
groupsSeen(UBound(groupsSeen)) = Array(groupsArray(dRow, 1), groupsArray(dRow, lColumn))
End If
End If
Next
sht.Range(Cells(1, lColumn), Cells(last.Row, lColumn)).Value = Application.Index(groupsArray, , lColumn)
'reactivate Screen updating
Application.ScreenUpdating = True
End Sub
Function getMax(group As String, groupsArray As Variant, lColumn As Long) As Double
'for each in array
For n = 1 To UBound(groupsArray)
'if its the same group the Max we seen so far the record
If groupsArray(n, 1) = group And groupsArray(n, lColumn - 1) > maxSoFar Then
maxSoFar = groupsArray(n, lColumn - 1)
End If
Next
'set function value
getMax = maxSoFar
End Function
Function inArrayValue(group As String, groupsSeen As Variant) As Double
'set function value
inArrayValue = 0
'if array is empty then exit
If IsEmpty(groupsSeen) Then Exit Function
'for each in array
For n = 0 To UBound(groupsSeen)
'if we find the group
If groupsSeen(n)(0) = group Then
'set function value to the Max value already seen
inArrayValue = groupsSeen(n)(1)
'exit function earlier
Exit Function
End If
Next
End Function
You can write a helper function to use instead of Application.Index
Bonus - it will be much faster than using Index (>5x)
Sub Tester()
Dim arr, arrCol
arr = Range("A2:J80000").Value
arrCol = GetColumn(arr, 5) '<< get the fifth column
Range("L2").Resize(UBound(arrCol, 1), 1).Value = arrCol
End Sub
'extract a single column from a 1-based 2-D array
Function GetColumn(arr, colNumber)
Dim arrRet, i As Long
ReDim arrRet(1 To UBound(arr, 1), 1 To 1)
For i = 1 To UBound(arr, 1)
arrRet(i, 1) = arr(i, colNumber)
Next i
GetColumn = arrRet
End Function
EDIT - since QHarr asked about timing here's a basic example
Sub Tester()
Dim arr, arrCol, t, i as long
arr = Range("A2:J80000").Value
t = Timer
For i = 1 to 100
arrCol = GetColumn(arr, 5) '<< get the fifth column
Next i
Debug.print Timer - t '<<# of seconds for execution
End Sub
Below, whilst not as tidy as could be, is a way to process an array in chunks and Index to access a column and write out to the sheet.
I populated two columns (A:B) with data. Both had 132,000 rows, populated incrementally, with values from 1 to 132,000 in each column for my test run.
You can fiddle with cutOff to get the chunk size just below the point where the fail happens.
The code below is simply to demonstrate the principle of looping in batches, upto the set cutoff in each batch, until all rows have been processed.
Option Explicit
Public Sub WriteArrayToSheet()
Dim wb As Workbook
Dim ws As Worksheet
Set wb = ThisWorkbook
Set sht = wb.Worksheets("Sheet1") 'change as appropriate
Dim myArr() 'dynamic array
myArr = sht.Range("A1").CurrentRegion.Value 'you may want a more robust method
Dim cutOff As Long 'the max value - what ever it is before error occurs
cutOff = 1000
Dim totalRows As Long 'total rows in array read in from sheet
totalRows = UBound(myArr, 1)
Dim totalArraysNeeded As Long
'Determine how many lots of cutOff chunks there are in the total number of array rows
totalArraysNeeded = Application.WorksheetFunction.Ceiling(totalRows / cutOff, 1)
Dim rotations As Long 'number of times to loop original array to handle all rows
Dim rowCountTotal As Long
Dim rowCount As Long
Dim tempArr() 'this will hold the chunk of the original array
Dim rowCounter As Long
Dim lastRow As Long
Dim nextRow As Long
Dim i As Long
Dim j As Long
Dim numRows As Long
rotations = 1
Do While rotations < totalArraysNeeded
If rotations < totalArraysNeeded - 1 Then
ReDim tempArr(1 To cutOff, 1 To UBound(myArr, 2)) 'size chunk array
numRows = cutOff
Else
numRows = totalRows - rowCountTotal
ReDim tempArr(1 To numRows, 1 To UBound(myArr, 2)) 'size chunk array
End If
For i = 1 To numRows
rowCount = 1 'rows in this chunk looped
rowCountTotal = rowCountTotal + 1 'rows in original array looped
For j = LBound(myArr, 2) To UBound(myArr, 2)
tempArr(i, j) = myArr(rowCountTotal, j)
Next j
rowCount = rowCount + 1
Next i
With sht
lastRow = .Cells(.Rows.Count, "E").End(xlUp).Row 'Column where I am writing the sliced column out to
End With
If lastRow = 1 Then
nextRow = 1
Else
nextRow = lastRow + 1
End If
sht.Range("E" & nextRow).Resize(UBound(tempArr, 1), 1) = Application.Index(tempArr, , 1) 'write out to sheet
rotations = rotations + 1
Loop
End Sub
As #Tim suggested, the best way to slice a large array is use a loop to copy the column.
Though in your case, most of the processing time is spent on computing the maximum since your code is using a nested loop.
If you want to reduce significantly the processing time, then use a dictionary:
Sub Usage
GetMaxByGroupTo _
sourceGroups := ThisWorkbook.Range("Sheet1!A2:A100"), _
sourceValues := ThisWorkbook.Range("Sheet1!B2:B100"), _
target := ThisWorkbook.Range("Sheet1!C2")
End Sub
Sub GetMaxByGroupTo(sourceGroups As Range, sourceValues As Range, target As Range)
Dim dict As Object, groups(), values(), r As Long, max
Set dict = CreateObject("Scripting.Dictionary")
groups = sourceGroups.Value2
values = sourceValues.Value2
' store the maximum value of each group in a dictionary for an efficient lookup '
For r = Lbound(groups) to Ubound(groups)
max = dict(groups(r, 1))
If VarType(max) And values(r, 1) <= max Then Else dict(groups(r, 1)) = values(r, 1)
Next
' build and copy the result array to the sheet '
For r = Lbound(groups) to Ubound(groups)
values(r, 1) = dict(groups(r, 1))
Next
target.Resize(Ubound(groups), 1).Value2 = values
End Sub
I'm having troubles getting my Error array to print to a range. I'm fairly sure I'm resizing it incorrectly, but I'm not sure how to fix it. I created a test add which just added garbage data from columns A and B, but normally AddPartError would be call from within various Subs/Functions, and then at the end of the main script process the array should be dumped onto a sheet. Here are the relevant functions:
Sub testadd()
For Each i In ActiveSheet.Range("A1:A10")
Call AddPartError(i.value, i.Offset(0, 1))
Next i
tmp = PartErrors
PrintArray PartErrors, ActiveWorkbook.Worksheets("Sheet1").[D1]
Erase PartErrors
tmp1 = PartErrors
PartErrorsDefined = 0
End Sub
Sub PrintArray(Data As Variant, Cl As Range)
Cl.Resize(UBound(Data, 1), 2) = Data
End Sub
Private Sub AddPartError(part As String, errType As String)
If Not PartErrorsDefined = 1 Then
ReDim PartErrors(1 To 1) As Variant
PartErrorsDefined = 1
End If
PartErrors(UBound(PartErrors)) = Array(part, errType)
ReDim Preserve PartErrors(1 To UBound(PartErrors) + 1) As Variant
End Sub
Ok. I did a bit of checking and the reason this doesn't work is because of your array structure of PartErrors
PartErrors is a 1 dimensional array and you are adding arrays to it, so instead of multi dimentional array you end up with a jagged array, (or array of arrays) when you actually want a 2d array
So to fix this, I think you need to look at changing your array to 2d. Something like the below
Private Sub AddPartError(part As String, errType As String)
If Not PartErrorsDefined = 1 Then
ReDim PartErrors(1 To 2, 1 To 1) As Variant
PartErrorsDefined = 1
End If
PartErrors(1, UBound(PartErrors, 2)) = part 'Array(part, errType)
PartErrors(2, UBound(PartErrors, 2)) = errType
ReDim Preserve PartErrors(1 To 2, 1 To UBound(PartErrors, 2) + 1) As Variant
End Sub
and
Sub PrintArray(Data As Variant, Cl As Range)
Cl.Resize(UBound(Data, 2), 2) = Application.Transpose(Data)
End Sub
NB. You also need to Transpose your array to fit in the range you specified.
You code is a little hard to follow, but redim clears the data that is in the array, so I think you need to use the "Preserve" keyword.
Below is some example code you can work through to give you the idea of how it works, but you will need to spend some time working out how to fit this into your code.
Good luck!
Sub asda()
'declare an array
Dim MyArray() As String
'First time we size the array I do not need the "Preserve keyword
'there is not data in the array to start with!!!
'Here we size it too 2 by 5
ReDim MyArray(1, 4)
'Fill Array with Stuff
For i = 0 To 4
MyArray(0, i) = "Item at 0," & i
MyArray(1, i) = "Item at 1," & i
Next
' "Print" data to worksheet
Dim Destination1 As Range
Set Destination1 = Range("a1")
Destination1.Resize(UBound(MyArray, 1) + 1, UBound(MyArray, 2) + 1).Value = MyArray
'Now lets resize that arrray
'YOU CAN ONLY RESIZE THE LAST SIZE OF THE ARRAY - in this case 4 to 6...
ReDim Preserve MyArray(1, 6)
For i = 5 To 6
MyArray(0, i) = "New Item at 0," & i
MyArray(1, i) = "New Item at 1," & i
Next
'and let put that next to our first list
' "Print" data to worksheet
Dim Destination2 As Range
Set Destination2 = Range("A4")
Destination2.Resize(UBound(MyArray, 1) + 1, UBound(MyArray, 2) + 1).Value = MyArray
End Sub