vba excel If condition error on final iteration - vba

I'm having the code takes the input from my checkboxes and grab data from the related worksheet. I ran it line by line and found out that it always gets a runtime error at the If statement on the final loop. Is there something wrong in my code?
Option Explicit
Private Sub UserForm_Initialize()
Dim counter As Long
Dim chkBox As MSForms.CheckBox
''''Add checkboxes based on total sheet count
For counter = 1 To Sheets.count - 2
Set chkBox = Me.Frame1.Controls.Add("Forms.CheckBox.1", "CheckBox" & counter)
chkBox.Caption = Sheets(counter + 2).Name
chkBox.Left = 10
chkBox.Top = 5 + ((counter - 1) * 20)
Next
End Sub
Private Sub cmdContinue_Click()
Dim Series As Object
Dim counter As Long
'''Clear old series
For Each Series In Sheets(2).SeriesCollection
Sheets(2).SeriesCollection(1).Delete
Next
''Cycle through checkboxes
For counter = 1 To Sheets.count - 2
''If the box is checked then
If Me.Frame1.Controls(counter).Value = True Then ''Error here on 4th iteration
''Add new series
With Sheets(2).SeriesCollection.NewSeries
.Name = Sheets(counter + 2).Range("$A$1")
.XValues = Sheets(counter + 2).Range("$A$12:$A$25")
.Values = Sheets(counter + 2).Range("$B$12:$B$25")
End With
End If
Next counter
Me.Hide
End Sub
Also, a second problem is it always run on the wrong loop. If i check box 2 it'll run data for the box 1 sheet, 3 run for 2, 4 run for 3, and 1 run for 4. Can anyone explain the reason behind this?
EDIT: So as VincentG point out below, adding an explicit name "checkbox" in there did the trick (i didn't know you could do that). Index 1 was probably taken by one of the buttons or the frame in the user form, causing it to get off set.

I guess your main problem comes from the fact that the controls have to be accessed starting from index 0. So to loop over all controls, you would do something like
For counter = 0 To Me.Frame1.Controls.Count - 1
Debug.Print counter; Me.Frame1.Controls(counter).Name
Next counter
So, when you stick to you code, I assume you have to change the if-statement to
If Me.Frame1.Controls(counter-1).Value = True Then

Related

How do I loop through a DataGridView where RowCount / Rows.Count changes

I have a VB.Net Form with a DataGridView where I wish to change between a Detailed View and a General View.
The DataGridView shows the Distance and Estimated Time between places - called the Route Leg Data and is termed the General View
I have implemented a CheckBox to select between the general and detailed view. When the CheckBox is Checked, I am trying to loop through all of the Route Leg entries and insert the Route Steps which is the Detailed information about the Leg entries.
I have tried various loop options: For..Next, For Each...Next, While...End While, and only the first row (Route Leg) gets processed, even if I have 5 more Route Leg entries.
Important: Keep in mind that when the Detail View is selected, the DataGridView row count increments for every new Route Step entry that gets inserted.
I tried to use both dgv.RowCount and dgv.Rows.Count but I keep getting the same result.
I have added some code to show what I am trying to achieve. Any help or guidance will be much appreciated.
'Show / Hide Route Step Data
Private Sub chkShowRouteStep_CheckedChanged(sender As Object, e As EventArgs) Handles chkShowRouteStep.CheckedChanged
Try
If chkShowRouteStep.Checked Then
'Show Route Steps
For i As Integer = 0 To dgvQuote.RowCount - 1
txtRowCount.Text = i
If dgvQuote.Rows(i).Cells(0).Value.ToString <> "" Then
For j As Integer = 1 To 5
i += 1
dgvQuote.Rows.Insert(i, "Step")
'dgvQuote.Rows.Insert(j + i, "Step")
Next
End If
Next
Else
'Hide Route Steps - WORKS GREAT
For i As Integer = dgvQuote.RowCount - 1 To 0 Step -1
If dgvQuote.Rows(i).Cells(0).Value.ToString = "Step" Then
dgvQuote.Rows.RemoveAt(i)
End If
Next
End If
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
My impatience, as well as a refocus, provided me with the answer.
My focus was always on the DataGridView and the feedback from it with regards to the number of rows contained in the control.
However, the execution of a loop took a snapshot of the number of rows at the start of the loop execution and did NOT update the number of rows every time rows were added to the DataGridView.
I took a different approach where I compared my current loop number (i += 1) with the current number of rows in the DataGridView (dgv.Rows.Count - 1) thereby forcing a recheck on the number of rows. This means that any new rows added to the DataGridView will now be counted in.
I added a trigger (True/False) to be set to true if the last row in the DataGridView was reach and the While...End While loop was exited.
'Show / Hide Route Step Data
Private Sub chkShowRouteStep_CheckedChanged(sender As Object, e As EventArgs) Handles chkShowRouteStep.CheckedChanged
Try
Dim lastRow As Boolean = False 'This will get set when we have reached the last row in the DGV
Dim i As Integer = 0
If chkShowRouteStep.Checked Then
'Show Route Steps
While lastRow <> True
txtRowCount.Text = i
If dgvQuote.Rows(i).Cells(0).Value.ToString <> "" Then
For j As Integer = 1 To 2
'i += 1
dgvQuote.Rows.Insert(i + j, "", "Step")
Next
End If
'Check to see if we have reached the last row, set lastRow to TRUE if it is the last row
If i = dgvQuote.Rows.Count - 1 Then
lastRow = True
End If
i += 1
End While
Else
'Hide Route Steps - WORKS GREAT
For x As Integer = dgvQuote.RowCount - 1 To 0 Step -1
If dgvQuote.Rows(x).Cells(1).Value.ToString = "Step" Then
dgvQuote.Rows.RemoveAt(x)
End If
Next
End If
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
The result is as follows and error free:
Before
After
Since the comment in the code says you want to hide route steps, why not do just that? Instead of removing and inserting rows, populate the grid with everything and then use the checkbox to set the rows .Visible property?

VBA Runtime Error 35600 when removing items from ListView

I would like to ask for help regarding a Runtime Error 35600 "Index out of bounds".
I am trying to delete all Items from a Multicolumn-ListView that do not match a Combobox-Value.
However, it seems that during the deletion-process, my code reaches a Point where the listitems-index is smaller than the index of the selected item.
Does anyone know how I can solve that? Here is my take on it:
Private Sub ComboBox1_Change()
Dim i As Integer
Dim strSearch As String
strSearch = Me.ComboBox1
For i = 1 To ListView1.listItems.Count
If Me.ListView1.listItems(i).SubItems(3) = strSearch Then
Me.ListView1.listItems(i).Checked = True
End If
Next i
For i = 1 To ListView1.listItems.Count
If ListView1.listItems(i).Checked = False Then
Me.ListView1.listItems.Remove (ListView1.selectedItem.Index)
End If
Next i
End Sub
You could try remove them in reverse order (so only for the second loop); I think in basic it would look like:
For i = ListView1.listItems.Count To 0 Step -1
Probably the counter is not re-evaluated after every loop and thus will be higher than the number of elements causing a too high number (more than the number of list items present resulting in an index out of bounds exception).
'Removing part
With ListView1
For i = .ListItems.Count To 1 Step -1
If Not .ListItems(i).Checked Then
.ListItems.Remove i
End If
Next
End With

Excel multi-select, multi-column listboxes

I am having trouble coding a userform that takes the selected data from one multi-column Listbox and adds it to another Listbox on the same userfrom. after adding, the data is removed from the source Listbox
"ListBox" is where the data is located, and "listbox1" is where it is being added to.
Private Sub add_Click()
For i = 0 To ListBox.ListCount - 1
If ListBox.Selected(i) = True Then
NextEmpty = ListBox1.ListCount
ListBox1.List(NextEmpty, 0) = ListBox.List(i, 0)
ListBox1.List(NextEmpty, 1) = ListBox.List(i, 1)
ListBox1.List(NextEmpty, 2) = ListBox.List(i, 2)
ListBox.RemoveItem (i)
End If
Next
End Sub
This code gives me a Run-time error '381'
"Could not set the list property. Invalid property array index."
I have done some looking around but can't seem to pinpoint how to use these properties correctly. Any help is greatly appreciated.
In order to do this, like Daniel said, we need to use an add function.
In the code below you can see how I used the .additem function in my with-block.
To remove the selection after moving it to a new Listbox, I run a backwards loop.
For i = MainListBox.ListCount - 1 To 0 Step -1
Private Sub add_Click()
Dim i As Integer
For i = 0 To MainListBox.ListCount - 1
If MainListBox.Selected(i) Then
With ListBox1
.AddItem
.List(.ListCount - 1, 0) = MainListBox.List(i, 0)
.List(.ListCount - 1, 1) = MainListBox.List(i, 1)
.List(.ListCount - 1, 2) = MainListBox.List(i, 2)
End With
End If
Next i
For i = MainListBox.ListCount - 1 To 0 Step -1
If MainListBox.Selected(i) Then
MainListBox.RemoveItem (i)
End If
Next i
End Sub
You cannot set a value to an index greater than the maximum in the list (and the maximum is exactly ListCount (Or ListCount-1 if zero based).
So, you must add the values with ListBox1.Add

How to use SpinButton in a condition

I have a spin button and a two list boxes. i have set the min and max of spin button. I already have a button which moves data from list box1 to listbox2.But i want to set a limit for moving the data based on spin button.
example:
if value in spin button is 2, then only 2 items can be added to listbox2
note:i have linked the spin button to a textbox
Declare a variable and then every time you move an item, increment the value of the variable by 1. So next time when you click the button to move the item, simply compare the value of the variable with the SpinButton's Value. For example (Untested)
Dim nMoved As Long
Private Sub CommandButton1_Click()
If nMoved < (VAL(TextBox1.Text) + 1)
'
'~~> Code to move items from LB1 to LB2
'
nMoved = nMoved + 1
Else
MsgBox "Max items that can be moved from LB1 to LB2 reached."
End If
End Sub
FollowUp from Comments
You need to use .ListCount in your case only if you are matching the items count in the LB against the TB. What I understood from your post is a user shouldn't be able to move more items into the LB than what is specified in the TB. Anyways, it seems that your query is sorted.
Regarding your 2nd comment.
You need to loop through the LB items using the code below and then exit the loop if the counter is equal to the TB value. For example
For i = 0 To (ListBox1.ListCount - 1)
If i = (Val(TextBox1.Text) - 1) Then Exit For
'
'~~> Code to move items from LB1 to LB2
'
End If
Next i
Thanks you all.. finally i got the answer
Dim ct As Integer
Dim ictr As Long
Dim jctr As Integer
jctr = CInt(Me.TextBox1.Value)
If jctr = ListBox2.ListCount Then
MsgBox "Maximum limit has been reached. You cannot add more players"
Exit Sub
End If
For ictr = jctr To 1 Step -1
Me.ListBox2.AddItem Me.ListBox1.List(ictr)
Me.ListBox1.RemoveItem ictr
Next ictr

fast way to copy formatting in excel

I have two bits of code. First a standard copy paste from cell A to cell B
Sheets(sheet_).Cells(x, 1).Copy Destination:=Sheets("Output").Cells(startrow, 2)
I can do almost the same using
Sheets("Output").Cells(startrow, 2) = Sheets(sheet_).Cells(x, 1)
Now this second method is much faster, avoiding copying to clipboard and pasting again. However it does not copy across the formatting as the first method does. The Second version is almost instant to copy 500 lines, while the first method adds about 5 seconds to the time. And the final version could be upwards of 5000 cells.
So my question can the second line be altered to included the cell formatting (mainly font colour) while still staying fast.
Ideally I would like to be able to copy the cell values to a array/list along with the font formatting so I can do further sorting and operations on them before I "paste" them back on to the worksheet..
So my ideal solution would be some thing like
for x = 0 to 5000
array(x) = Sheets(sheet_).Cells(x, 1) 'including formatting
next
for x = 0 to 5000
Sheets("Output").Cells(x, 1)
next
is it possible to use RTF strings in VBA or is that only possible in vb.net, etc.
Answer*
Just to see how my origianl method and new method compar, here are the results or before and after
New code = 65msec
Sheets("Output").Cells(startrow, 2) = Sheets(sheet_).Cells(x, 1)
Sheets("Output").Range("B" & startrow).Font.ColorIndex = Sheets(sheet_).Range("A" & x).Font.ColorIndex 'copy font colour as well
Old code = 1296msec
'Sheets("Output").Cells(startrow, 2).Value = Sheets(sheet_).Cells(x, 1)
'Sheets(sheet_).Cells(x, 1).Copy
'Sheets("Output").Cells(startrow, 2).PasteSpecial (xlPasteFormats)
'Application.CutCopyMode = False
You could have simply used Range("x1").value(11)
something like below:
Sheets("Output").Range("$A$1:$A$500").value(11) = Sheets(sheet_).Range("$A$1:$A$500").value(11)
range has default property "Value" plus value can have 3 optional orguments 10,11,12.
11 is what you need to tansfer both value and formats. It doesn't use clipboard so it is faster.- Durgesh
For me, you can't. But if that suits your needs, you could have speed and formatting by copying the whole range at once, instead of looping:
range("B2:B5002").Copy Destination:=Sheets("Output").Cells(startrow, 2)
And, by the way, you can build a custom range string, like Range("B2:B4, B6, B11:B18")
edit: if your source is "sparse", can't you just format the destination at once when the copy is finished ?
Remember that when you write:
MyArray = Range("A1:A5000")
you are really writing
MyArray = Range("A1:A5000").Value
You can also use names:
MyArray = Names("MyWSTable").RefersToRange.Value
But Value is not the only property of Range. I have used:
MyArray = Range("A1:A5000").NumberFormat
I doubt
MyArray = Range("A1:A5000").Font
would work but I would expect
MyArray = Range("A1:A5000").Font.Bold
to work.
I do not know what formats you want to copy so you will have to try.
However, I must add that when you copy and paste a large range, it is not as much slower than doing it via an array as we all thought.
Post Edit information
Having posted the above I tried by own advice. My experiments with copying Font.Color and Font.Bold to an array have failed.
Of the following statements, the second would fail with a type mismatch:
ValueArray = .Range("A1:T5000").Value
ColourArray = .Range("A1:T5000").Font.Color
ValueArray must be of type variant. I tried both variant and long for ColourArray without success.
I filled ColourArray with values and tried the following statement:
.Range("A1:T5000").Font.Color = ColourArray
The entire range would be coloured according to the first element of ColourArray and then Excel looped consuming about 45% of the processor time until I terminated it with the Task Manager.
There is a time penalty associated with switching between worksheets but recent questions about macro duration have caused everyone to review our belief that working via arrays was substantially quicker.
I constructed an experiment that broadly reflects your requirement. I filled worksheet Time1 with 5000 rows of 20 cells which were selectively formatted as: bold, italic, underline, subscript, bordered, red, green, blue, brown, yellow and gray-80%.
With version 1, I copied every 7th cells from worksheet "Time1" to worksheet "Time2" using copy.
With version 2, I copied every 7th cells from worksheet "Time1" to worksheet "Time2" by copying the value and the colour via an array.
With version 3, I copied every 7th cells from worksheet "Time1" to worksheet "Time2" by copying the formula and the colour via an array.
Version 1 took an average of 12.43 seconds, version 2 took an average of 1.47 seconds while version 3 took an average of 1.83 seconds. Version 1 copied formulae and all formatting, version 2 copied values and colour while version 3 copied formulae and colour. With versions 1 and 2 you could add bold and italic, say, and still have some time in hand. However, I am not sure it would be worth the bother given that copying 21,300 values only takes 12 seconds.
** Code for Version 1**
I do not think this code includes anything that needs an explanation. Respond with a comment if I am wrong and I will fix.
Sub SelectionCopyAndPaste()
Dim ColDestCrnt As Integer
Dim ColSrcCrnt As Integer
Dim NumSelect As Long
Dim RowDestCrnt As Integer
Dim RowSrcCrnt As Integer
Dim StartTime As Single
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
NumSelect = 1
ColDestCrnt = 1
RowDestCrnt = 1
With Sheets("Time2")
.Range("A1:T715").EntireRow.Delete
End With
StartTime = Timer
Do While True
ColSrcCrnt = (NumSelect Mod 20) + 1
RowSrcCrnt = (NumSelect - ColSrcCrnt) / 20 + 1
If RowSrcCrnt > 5000 Then
Exit Do
End If
Sheets("Time1").Cells(RowSrcCrnt, ColSrcCrnt).Copy _
Destination:=Sheets("Time2").Cells(RowDestCrnt, ColDestCrnt)
If ColDestCrnt = 20 Then
ColDestCrnt = 1
RowDestCrnt = RowDestCrnt + 1
Else
ColDestCrnt = ColDestCrnt + 1
End If
NumSelect = NumSelect + 7
Loop
Debug.Print Timer - StartTime
' Average 12.43 secs
Application.Calculation = xlCalculationAutomatic
End Sub
** Code for Versions 2 and 3**
The User type definition must be placed before any subroutine in the module. The code works through the source worksheet copying values or formulae and colours to the next element of the array. Once selection has been completed, it copies the collected information to the destination worksheet. This avoids switching between worksheets more than is essential.
Type ValueDtl
Value As String
Colour As Long
End Type
Sub SelectionViaArray()
Dim ColDestCrnt As Integer
Dim ColSrcCrnt As Integer
Dim InxVLCrnt As Integer
Dim InxVLCrntMax As Integer
Dim NumSelect As Long
Dim RowDestCrnt As Integer
Dim RowSrcCrnt As Integer
Dim StartTime As Single
Dim ValueList() As ValueDtl
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
' I have sized the array to more than I expect to require because ReDim
' Preserve is expensive. However, I will resize if I fill the array.
' For my experiment I know exactly how many elements I need but that
' might not be true for you.
ReDim ValueList(1 To 25000)
NumSelect = 1
ColDestCrnt = 1
RowDestCrnt = 1
InxVLCrntMax = 0 ' Last used element in ValueList.
With Sheets("Time2")
.Range("A1:T715").EntireRow.Delete
End With
StartTime = Timer
With Sheets("Time1")
Do While True
ColSrcCrnt = (NumSelect Mod 20) + 1
RowSrcCrnt = (NumSelect - ColSrcCrnt) / 20 + 1
If RowSrcCrnt > 5000 Then
Exit Do
End If
InxVLCrntMax = InxVLCrntMax + 1
If InxVLCrntMax > UBound(ValueList) Then
' Resize array if it has been filled
ReDim Preserve ValueList(1 To UBound(ValueList) + 1000)
End If
With .Cells(RowSrcCrnt, ColSrcCrnt)
ValueList(InxVLCrntMax).Value = .Value ' Version 2
ValueList(InxVLCrntMax).Value = .Formula ' Version 3
ValueList(InxVLCrntMax).Colour = .Font.Color
End With
NumSelect = NumSelect + 7
Loop
End With
With Sheets("Time2")
For InxVLCrnt = 1 To InxVLCrntMax
With .Cells(RowDestCrnt, ColDestCrnt)
.Value = ValueList(InxVLCrnt).Value ' Version 2
.Formula = ValueList(InxVLCrnt).Value ' Version 3
.Font.Color = ValueList(InxVLCrnt).Colour
End With
If ColDestCrnt = 20 Then
ColDestCrnt = 1
RowDestCrnt = RowDestCrnt + 1
Else
ColDestCrnt = ColDestCrnt + 1
End If
Next
End With
Debug.Print Timer - StartTime
' Version 2 average 1.47 secs
' Version 3 average 1.83 secs
Application.Calculation = xlCalculationAutomatic
End Sub
Just use the NumberFormat property after the Value property:
In this example the Ranges are defined using variables called ColLetter and SheetRow and this comes from a for-next loop using the integer i, but they might be ordinary defined ranges of course.
TransferSheet.Range(ColLetter & SheetRow).Value = Range(ColLetter & i).Value
TransferSheet.Range(ColLetter & SheetRow).NumberFormat = Range(ColLetter & i).NumberFormat
Does:
Set Sheets("Output").Range("$A$1:$A$500") = Sheets(sheet_).Range("$A$1:$A$500")
...work? (I don't have Excel in front of me, so can't test.)