Select the next item in a validation list VBA - excel

I'm creating a simple form in excel that will run in a touch device. In some part of the form i have created a dropdown validation list. I need to go to the next and previous item in the validation list with a button.
The only way thay i found is using Application.Sendkeys to acces to the dropdown but is not working. Any thoughts?
sub Next_item click()
ActiveSheet.Range(cell_rng.Text).Select
Application.SendKeys ("%{Down}")
Application.SendKeys ("{Down}{Enter}")
End Sub

You might consider going to the source of the data validation and grabbing the next item rather than trying to manipulate the in-cell dropdown directly. For instance, if you data validation is a comma-separated list
you could use code like this to get the next value:
Public Sub NextItem()
Dim dv As Validation
Dim vaSplit As Variant
Dim i As Long
'Formula1 contains the comma sep list of values
Set dv = ActiveCell.Validation
vaSplit = Split(dv.Formula1, ",")
For i = LBound(vaSplit) To UBound(vaSplit)
'if you're at the currently selected one
If vaSplit(i) = ActiveCell.Value Then
'but not at the last one
If i < UBound(vaSplit) Then
'select the next one
ActiveCell.Value = vaSplit(i + 1)
Exit For
End If
End If
Next i
End Sub
And if your list comes from a simple range
you could use almost the same code
Public Sub NextItemRange()
Dim dv As Validation
Dim vaSplit As Variant
Dim i As Long
Set dv = ActiveCell.Validation
vaSplit = Range(dv.Formula1).Value
For i = LBound(vaSplit, 1) To UBound(vaSplit, 1)
If vaSplit(i, 1) = ActiveCell.Value Then
If i < UBound(vaSplit, 1) Then
ActiveCell.Value = vaSplit(i + 1, 1)
Exit For
End If
End If
Next i
End Sub
Dynamic ranges or tables might throw a wrench into it, but hopefully it's a start.

Related

Excel VBA Userform combobox1 selection filters combobox2 based off of combobox1 selection

So I'm trying to use three Comboboxes to have a selection list for data input. I'm needing to make a selection in this order: Region -> Site -> Maintenance Plant. When a selection is made in the Region Combobox, then the Site Combobox list should filter to the options that pertain to the corresponding Region selection. Im thinking either a pivot table or vLookup needs to be used but I'm at a loss and have no clue how to get this done. Please help and thank you very much in advance.
Private Sub UserForm_Initialize()
Dim CreateBy As Range
Dim Region As Range
Dim Site As Range
Dim MaintPlant As Range
Dim Dept As Range
Dim Act As Range
Dim ImpActTyp As Range
Dim ValCat As Range
Dim ws As Worksheet
Set ws = Worksheets("LookupLists")
For Each CreateBy In ws.Range("RosterList")
With Me.CboCreateBy
.AddItem CreateBy.Value
End With
Next CreateBy
For Each Region In ws.Range("RegionList")
With Me.CboRegion
.AddItem Region.Value
End With
Next Region
For Each Site In ws.Range("SiteList")
With Me.CboSite
.AddItem Site.Value
End With
Next Site
For Each MaintPlant In ws.Range("MaintPlantList")
With Me.CboMntPlant
.AddItem MaintPlant.Value
End With
Next MaintPlant
For Each Dept In ws.Range("DeptList")
With Me.CboDept
.AddItem Dept.Value
End With
Next Dept
For Each Act In ws.Range("ActList")
With Me.CboAct
.AddItem Act.Value
End With
Next Act
For Each ImpActTyp In ws.Range("ImpActTypList")
With Me.CboImpActTyp
.AddItem ImpActTyp.Value
End With
Next ImpActTyp
For Each ValCat In ws.Range("ValCatList")
With Me.CboValCat
.AddItem ValCat.Value
End With
Next ValCat
Me.DateTextBox.Value = Format(Date, "Medium Date")
Me.PLife.Value = 0
Me.CSE.Value = 0
Me.CboRegion.SetFocus
End Sub
Get ready, because I'm about to reimagine your entire code here. I strongly recommend you create a backup of your original code module or workbook just due to the vast differences and if our ideas didn't align properly.
This will perform real-time filtering on your table, so keep this in mind using this method.
I did perform some testing on the following code, but I am human and threw this together in 20 mins or so. I wouldn't implement this in a real setting until you have fully tested the code and are comfortable with it.
And I just wanted to thank you for your use of Named Ranges. This made coding this easier.
You must enable the Microsoft Scripting Runtime library. This is used to grab the unique values from your tables. (Tools > References)
So to get things started, here is the entire code for your userform's code module:
Option Explicit
Private ws As Worksheet
Private tblLO As ListObject
Private Sub combo_region_Change()
Application.EnableEvents = False
Me.combo_maintPlant.Clear
Me.combo_site.Clear
'This is the first filter, so no worries about clearing entire AutoFilter
tblLO.AutoFilter.ShowAllData
Select Case Me.combo_region.Value
Case ""
Me.combo_site.Value = ""
Me.combo_maintPlant.Value = ""
Me.combo_site.Enabled = False
Me.combo_maintPlant.Enabled = False
Case Else
'If data is entered into first combobox, filter the table
tblLO.Range.AutoFilter 1, Me.combo_region.Value
'Populate the site combo box with new data
populateSiteCombo
'Enable the Site Combobox for user input
Me.combo_site.Enabled = True
End Select
Application.EnableEvents = True
End Sub
Private Sub combo_site_Change()
Application.EnableEvents = False
Me.combo_maintPlant.Clear
'Clear the filtering, then readd the Region's filter
tblLO.AutoFilter.ShowAllData
tblLO.Range.AutoFilter 1, Me.combo_region
Select Case Me.combo_site.Value
Case ""
Me.combo_maintPlant.Value = ""
Me.combo_maintPlant.Enabled = False
Case Else
'If data is entered into first combobox, filter the table
tblLO.Range.AutoFilter 2, Me.combo_site.Value
'Populate the Plant combo box with new data
populatePlantCombo
'Enable the Plant Combobox for user input
Me.combo_maintPlant.Enabled = True
End Select
Application.EnableEvents = True
End Sub
Private Sub populatePlantCombo()
'Grab unique values from Region column using Dictionary
Dim i As Long, regionDict As New Scripting.Dictionary
Dim arrReg() As Variant
'If it filters only 1 item, then it's just a single cell and not an arr
With ws.Range("MaintPlantList").SpecialCells(xlCellTypeVisible)
If .Count = 1 Then
Me.combo_maintPlant.AddItem .Value
Exit Sub
Else
arrReg = .Value
End If
End With
With New Scripting.Dictionary
For i = 1 To UBound(arrReg)
If Not .Exists(arrReg(i, 1)) Then
.Add arrReg(i, 1), "" 'We only add to dictionary for tracking
Me.combo_maintPlant.AddItem arrReg(i, 1)
End If
Next
End With
End Sub
Private Sub populateSiteCombo()
'Grab unique values from Region column using Dictionary
Dim i As Long, regionDict As New Scripting.Dictionary
Dim arrReg() As Variant
'If it filters only 1 item, then it's just a single cell and not an arr
With ws.Range("SiteList").SpecialCells(xlCellTypeVisible)
If .Count = 1 Then
Me.combo_site.AddItem .Value
Exit Sub
Else
arrReg = .Value
End If
End With
With New Scripting.Dictionary
For i = 1 To UBound(arrReg)
If Not .Exists(arrReg(i, 1)) Then
.Add arrReg(i, 1), "" 'We only add to dictionary for tracking
Me.combo_site.AddItem arrReg(i, 1)
End If
Next
End With
End Sub
Private Sub populateRegionCombo()
'Grab unique values from Region column using Dictionary
Dim i As Long, regionDict As New Scripting.Dictionary
Dim arrReg() As Variant
arrReg = ws.Range("RegionList").Value
With New Scripting.Dictionary
For i = 1 To UBound(arrReg)
If Not .Exists(arrReg(i, 1)) Then
.Add arrReg(i, 1), "" 'We only add to dictionary for tracking
Me.combo_region.AddItem arrReg(i, 1)
End If
Next
End With
End Sub
Private Sub UserForm_Initialize()
Set ws = ThisWorkbook.Worksheets("LookupLists") 'Module-defined var
Set tblLO = ws.ListObjects("Table1") 'Module-defined var
tblLO.AutoFilter.ShowAllData
Me.combo_maintPlant.Enabled = False
Me.combo_site.Enabled = False
'We only populate this one during init because the others
'will populate once a value is used in this box
populateRegionCombo
End Sub
If you decided to scroll down to understand what's going on here, then great.
Let's start with the initialization:
Private Sub UserForm_Initialize()
Set ws = ThisWorkbook.Worksheets("LookupLists") 'Module-defined var
Set tblLO = ws.ListObjects("Table1") 'Module-defined var
tblLO.AutoFilter.ShowAllData
Me.combo_maintPlant.Enabled = False
Me.combo_site.Enabled = False
'We only populate this one during init because the others
'will populate once a value is used in this box
populateRegionCombo
End Sub
We defined the module variables ws and tblLO. I'm not a huge fan of module-scoped variables, but we can usually get along when they are private vars to a userform module. Now the other functions in the code module can access these.
We reset autofiltering and disabled the two combo boxes that shouldn't be used until a selection is made for the region. Only after the region is selected will the next box be available for selection. We will handle these using Change Events for the comboboxes.
The userform is mostly controlled by the combo_region_change and combo_site_change events. Everytime region_change is fired, it will clear all the other combo boxes to redetermine it's new value. Then it will refilter as appropriately. The combo_site does the same, but it only clears the maintaince box. These event handlers also establish which of the other combox boxes are enabled depending on their values. So if you where to completely clear the site box for example, it will disable access to the Plant box again.
Finally you just have the "populate subs". Their jobs are simply to (re)populate the next combo box once the appropriate event handler is triggered.
Tip: If you feel the need to reset the filtering once you close your userform, you can just place the code to reset it in a UserForm_Terminate() event. It makes no difference to the above code if autofilter is enabled or not prior to it running, so that is preference only.

Link Listbox and sheets in excel for delete - VBA

I create listbox in excel with VBA userform. Its values are obtained from the Sheet in Excel.
How can I delete the values in the sheet "database" while deleting the box list item?
please help me.
Private Sub UserForm_Initialize()
Dim ws As Worksheet
Dim rng As Range
Dim MyArray
Set ws = Sheets("Database")
Set rng = ws.Range("K2:L" & ws.Range("K" & ws.Rows.Count).End(xlUp).Row)
With Me.ListBox1
.Clear
.ColumnHeads = False
.ColumnCount = rng.Columns.Count
MyArray = rng
.List = MyArray
.ColumnWidths = "90;90"
.TopIndex = 0
End With
End Sub
Private Sub CommandButton2_Click()
For lItem = Me.ListBox1.ListCount - 1 To 0 Step -1
If ListBox1.Selected(lItem) Then
ListBox1.RemoveItem lItem
If Me.ListBox1.MultiSelect = fmMultiSelectSingle Then
Exit For
End If
End If
Next
End Sub
How do I delete the values in the sheet "database"?
As you assign database items via the array method (not using ControlSource), you want to know how to synchronize listbox items with your data base after manual deletion.
Approach A) - Write the entire Listbox1.List
If you want a mirror image of the listbox items after the For- Next loop, you could simply write these items back to a given range (of course you should clear 'surplus rows', too) via the following one liner
rng.Resize(Me.ListBox1.ListCount, 2) = Me.ListBox1.List
Instead of reduplicating the data range declaration in CommandButton2_Click, I'd suggest to declare it ONCE in the declaration head of the Userform code module (and omit it in Userform_Initialize):
Thus the complete code would be as follows:
â–ºAdditional notes due to comment
Insert these two code lines on top of your UserForm code module (and before any procedures).
Option Explicit is strictly recommended in any code to force the declaration of variable types (but you can't use this statement within a Sub as you did). The declaration Dim rng As Range OUTSIDE the other procedures (i.e. on top) allows that any procedure within this code module knows the rng variable.
Option Explicit ' declaration head of the UserForm module
Dim rng as Range ' ONE database declaration only!
' << OUTSIDE of following procedures
' << Start of regular procedures
Private Sub UserForm_Initialize()
Dim ws As Worksheet
' Dim rng As Range ' << not needed here, see top declaration
Dim MyArray
Set ws = Sheets("Database")
Set rng = ws.Range("K2:L" & ws.Range("K" & ws.Rows.Count).End(xlUp).Row)
With Me.ListBox1
.Clear
.ColumnHeads = False
.ColumnCount = rng.Columns.Count
MyArray = rng
.List = MyArray
.ColumnWidths = "90;90"
.TopIndex = 0
End With
End Sub
Private Sub CommandButton3_Click()
Dim lItem&
For lItem = Me.ListBox1.ListCount - 1 To 0 Step -1
If ListBox1.Selected(lItem) Then
ListBox1.RemoveItem lItem ' remove item from listbox
If Me.ListBox1.MultiSelect = fmMultiSelectSingle Then
Exit For
End If
End If
Next
rng.Offset(Me.ListBox1.ListCount, 0).Resize(rng.Rows.Count, 2) = "" ' clear rows
rng.Resize(Me.ListBox1.ListCount, 2) = Me.ListBox1.List ' write list back
End Sub
Note that no rows are deleted physically, the resulting listbox items in the two target columns K:L are shifted up only (approach B allows to delete entire rows as well).
Approach B) - Help procedure within main loop
Using the same data range declaration in the declaration head of the UserForm â–º as shown above (i.e. OUTSIDE the procedures as Subs or Functions), you could use a help procedure DelData allowing to distinguish between two principal cases:
[1] Shift up deleted cells in your database
[2] Delete the entire row
Event procedure CommandButton2_Click
Private Sub CommandButton2_Click()
' Purpose: delete items both from database and listbox
Dim lItem&
For lItem = Me.ListBox1.ListCount - 1 To 0 Step -1
If ListBox1.Selected(lItem) Then
DelData lItem, True ' [1] True=delete items and shift up
'DelData lItem, False ' [2] False=delete entire row
ListBox1.RemoveItem lItem ' remove item from listbox
If Me.ListBox1.MultiSelect = fmMultiSelectSingle Then
Exit For ' do it once in single select case
End If
End If
Next
End Sub
Help procedure DelData
Sub DelData(ByVal indx&, Optional ByVal bShiftUp As Boolean = True)
' Purpose: delete indicated row items in database
' Note: data set in OP includes header
If bShiftUp Then ' [1] bShiftUp = True: delete row items and shift up
rng.Offset(indx).Resize(1, rng.Columns.Count).Delete xlShiftUp
Else ' [2] bShiftUp = False: delete entire row of indicated items
rng.Offset(indx).Resize(1, rng.Columns.Count).EntireRow.Delete
End If
End Sub
Side note
It's recommended to fully qualify range references to avoid getting data from wrong workbooks, so I'd suggest the following statement in your UserForm_Initialize procedure:
Set ws = ThisWorkbook.Worksheets("Database")
Enjoy it :-)
Before removing the item from the ListBox you need to use the located value at the ListBox.Selected to find and remove the item from your "database".
Something like this:
Private Sub CommandButton2_Click()
For lItem = Me.ListBox1.ListCount - 1 To 0 Step -1
If ListBox1.Selected(lItem) Then
DeleteItemFromDatabase ListBox1.Selected(lItem).Value
ListBox1.RemoveItem lItem
If Me.ListBox1.MultiSelect = fmMultiSelectSingle Then
Exit For
End If
End If
Next
End Sub
Then your Sub DeleteItemFromDatabase(ByVal itemToDelete As [type]) would find itemToDelete in your "database" and remove it.
As an additional note, you may want to consider using Access as your database since it's actually designed to be one. I realize this isn't always possible, but thought I'd throw it out there as a thought for you.

Persisting content of MSForms ListBox with ListStyle = fmListStyleOption

I have created a UserForm in VBA Excel that has a ListBox with the ListStyleOption selected. The MultiSelectMulti option is activated.
Whenever I close the UserForm or Workbook and then reopen, all the previous selections are gone. Is there a way to retain selections made in listbox?
Thanks.
Yes, it's possible, but you have to save the listbox items and their selected state in the workbook or some data support such as a file or database. When showing your form, you'll just read back the saved items and selected states.
Assuming you can save the list's content in the workbook, you can use something like the following:
Public Sub SaveList(ByVal plstListBox As MSForms.ListBox, ByVal prngSavePoint As Excel.Range)
On Error GoTo errHandler
Dim lRow As Long
Dim bScreenUpdating As Boolean
Dim bEnableEvents As Boolean
bScreenUpdating = Application.ScreenUpdating
bEnableEvents = Application.EnableEvents
Application.ScreenUpdating = False
Application.EnableEvents = False
prngSavePoint.CurrentRegion.Clear
If plstListBox.ListCount > 1 Then
For lRow = 0 To plstListBox.ListCount - 1
prngSavePoint.Cells(lRow + 1, 1).Value = plstListBox.Selected(lRow)
prngSavePoint.Cells(lRow + 1, 2).Value = plstListBox.List(lRow)
Next
End If
Cleanup:
On Error Resume Next
Application.EnableEvents = bEnableEvents
Application.ScreenUpdating = bScreenUpdating
Exit Sub
errHandler:
MsgBox Err.Description, vbExclamation + vbOKOnly, "Error"
Resume 'Cleanup
End Sub
Public Sub LoadList(ByVal plstListBox As MSForms.ListBox, ByVal prngSavePoint As Excel.Range)
Dim lRow As Long
Dim vntSavedList As Variant
plstListBox.Clear
If Not IsEmpty(prngSavePoint.Cells(1, 1).Value) Then
vntSavedList = prngSavePoint.CurrentRegion.Value
For lRow = 1 To UBound(vntSavedList, 1)
plstListBox.AddItem vntSavedList(lRow, 2)
plstListBox.Selected(lRow - 1) = vntSavedList(lRow, 1)
Next
End If
End Sub
To save (e.g. you could have a Save List button on your form), pass a reference to your listbox, and a reference to the top left cell of a free zone somewhere in your workbook. Beware that the code will write from this point down, on 2 columns, and overwrite everything that may be in its path. You must also be sure that this cell is isolated, i.e. not immediately adjacent to other content in any direction.
Example: SaveList ListBox1, Sheet1.Cells(1, 1)
You could have a Load List button on your form. To load back your list: LoadList ListBox1, Sheet1.Cells(1, 1)
The important listbox properties used in this answer are Selected and List, which give the selected state and label of any item in the list. These are zero-based indexed properties.

Loop repeated code to set userform checkbox values

I've a dashboard that uses a userform box field with checkboxes to choose what data to display on a chart.
My code is very copy and pasted.
Private Sub CommandButton21_Click()
UserForm1.Show
If Worksheets("Data Directorate").Range("X4").Value = True Then UserForm1.CheckBox1 = True
If Worksheets("Data Directorate").Range("X5").Value = True Then UserForm1.CheckBox2 = True
If Worksheets("Data Directorate").Range("X6").Value = True Then UserForm1.CheckBox3 = True
Is there a way to use a loop to do this?
I've more repeated code later on:
Private Sub CheckBox1_Click()
Select Case CheckBox1.Value
Case True
Worksheets("Data Directorate").Range("X4").Value = True
Case False
Worksheets("Data Directorate").Range("X4").Value = False
End Select
End Sub
This repeats for 24 check boxes. Would it be possible to loop this?
All great advice posted in this thread, so I'd like to add something that can maybe help to simplify your loops. Controls have a Tag property which, as best I can tell, does nothing other than to store additional information about the control.
Using this to our advantage, we can include the linked cell in the Tag property for each checkbox. (For example, enter X4 into the Tag for linkage to cell X4). This permits for less of the information to be hardcoded, which makes it more adaptable.
Finally, the code would look like this.
Private Sub UserForm_Click()
For Each octrl In Me.Controls
If TypeName(octrl) = "CheckBox" Then
Sheet1.Range(octrl.Tag).Value = octrl.Value
End If
Next octrl
End Sub
My approach in this scenario would be:
Set ControlSource property of the checkboxes to appropriate cells, leave only:
UserForm1.Show
One thing to simplify: instead of using If statements, just make the two sides equal. Like this:
Private Sub CommandButton21_Click()
UserForm1.Show
UserForm1.CheckBox1 = Worksheets("Data Directorate").Range("X4").Value
And the other one:
Private Sub CheckBox1_Click()
Worksheets("Data Directorate").Range("X4").Value = CheckBox1.Value
End Sub
I recommend using the change event instead of the click event. Some user might use Tab and Space, then the click event won't trigger. Like this:
Private Sub CheckBox1_Change()
...
About looping through the checkboxes in the CommandButton21_Click event, that depends on the order of your checkboxes and your table. This might work, but you will have to try it on your table (the order of the checkboxes compared to the order of your cells can ruin the game...)
Dim contr As control
dim i as integer
i=4
For Each contr In UserForm1.Controls
If TypeName(contr) = "CheckBox" Then
contr.Value = cells(i,24).Value 'column 24 is column X
i=i+1
End If
Next
A possible variation to work when CheckBoxes are not in the expected order or get added/removed:
Dim contr As control
Dim J as Integer
Dim Offset as Integer
Offset = 3 'set this to the difference between 1 and the first row containing data
For Each contr In UserForm1.Controls
If TypeName(contr) = "CheckBox" Then
'set j to the checkboxes "number"
J = cInt(right(contr.name, len(contr.name) - len("CheckBox")))
'use the checkbox number + offset to find the row we want
contr.Value = cells(J + Offset,24).Value 'column 24 is column X
End If
Next
Hope this helps.
For the second portion of your question:
Private Sub CheckBox1_Click()
Worksheets("Data Directorate").Range("X4").Value = CheckBox1.Value
End Sub
You can group the checkboxes inside a given frame and try the the following
Sub Test()
Dim i As Long
i = 5
For Each cb In UserForm1.Frame1.Controls
If Worksheets("Data Directorate").Range("X" & i).Value = True Then cb.Value = True
i = i + 1
Next cb
End Sub

VBA is it possible to pass a Dictionary/Collection to an autofilter?

The idea was to create a variable that would save the changes made to it from previous use of the macro. I have a userform that pulls values from a range and populates unique values in a listbox. I then want to be able to add selected values to my dictionary/collection and save the change. Once all necessary changes have been made, the macro should use the dictionary variable as criteria for an autofilter.
My question is two fold, what class should I use to accomplish this? How can a use this variable to autofilter my worksheet? Userform code is below:
The First bit of code is for the "Add" command button. It is supposed to take the selected value(s) in the listbox and add them to the dictionary titled "Market". The code after that pulls the values from a recently opened excel workbook an displays unique values in the listbox. Listbox2 holds all previous values from past uses of the macro. I want to add a "Delete" button to the userform to tidy up the list if necessary. The two public variables below are actually located on the main macro module, this would allow me to store the values in the dictionary after the userform has stopped running.
Private Sub CommandButton1_Click()
Dim i As Long
For i = 0 To ListBox1.ListCount - 1
If ListBox1.Selected(i) = True Then
Market.Add ListBox1.List(i)
Set Market = New Collection
End If
Next
End Sub
Private Sub UserForm_Initialize()
Dim myList As Collection
Dim myRange As Range
Dim ws As Worksheet
Dim myVal As Variant
Dim Col As Integer
Set ws = ActiveWorkbook.Sheets("Daily Unconfirmed")
Col = WorksheetFunction.Match("Marketer", ws.Range("3:3"), 0)
Set myRange = ws.Range(Cells(4, Col), Cells(4, Col).End(xlDown))
Set myList = New Collection
On Error Resume Next
For Each mycell In myRange.Cells
myList.Add mycell.Value, CStr(mycell.Value)
Next mycell
On Error GoTo 0
For Each myVal In myList
Me.ListBox1.AddItem myVal
Next myVal
Public item As Variant
Public Market As Collection
Market.Add "Al D"
Market.Add "B Collins"
Market.Add "B G"
Market.Add "C Huter"
For Each item In Market
Me.ListBox2.AddItem item
Next item
End Sub
Since AutoFilter runs from an array, I would build the array dynamically and use it in a filtering sub:
Dim ary()
Sub MAIN()
Call BuildDynamicArray
Call FilterMyData
End Sub
Sub BuildDynamicArray()
Dim inString As String
i = 1
While 1 = 1
x = Application.InputBox(Prompt:="Enter a value", Type:=2)
If x = False Then GoTo out
ReDim Preserve ary(1 To i)
ary(i) = x
i = i + 1
Wend
out:
End Sub
Sub FilterMyData()
ActiveSheet.Range("$A$1:$A$10").AutoFilter Field:=1, Criteria1:=ary, Operator:=xlFilterValues
End Sub

Resources