VB DropDown Read Only - vb.net

I have a DropDown and a DropDownList on my form. I am aware that a DropDown can hold a placeholder text and a DropDownList cannot, however; I would like some code or a work around to allow either:
DropDown as read-only, therefore not allowing a user to type
But preferably a DropDownList with placeholder text (context menu option, or not)
Is this possible?
Thanks.

But preferably a DropDownList with placeholder text (context menu option, or not)
By definition, the text displayed in this control is always the text of the selected item. You can add a "fake" item to the list if you want (e.g. "Select Property Code"), but you will have to check that this item isn't selected later.
To display one of the items (fake or not), simply set the SelectedIndex to the appropriate value once the list is loaded.
DropDown as read-only, therefore not allowing a user to type
This will actively make sure that the text is either in the list or a default value, effectively making it read only. (Written for a ComboBox, but the behavior should be identical with a DropDown.)
Private Sub ComboBox1_TextChanged(sender As Object, e As EventArgs) Handles ComboBox1.TextChanged
Static recursion As Boolean = False
Dim defaultText As String = "My Default Text"
If recursion Then
recursion = False
Else
If ComboBox1.Items.Count > 0 Then
For i As Integer = 0 To ComboBox1.Items.Count - 1
If ComboBox1.Items(i).ToString = ComboBox1.Text Then
Exit Sub
End If
Next
recursion = True
ComboBox1.Text = defaultText
End If
End Sub
Alternatively, here is a sub I call whenever a "strict" ComboBox looses focus to accomplish the same thing. The difference is that doing it this way allows you to keep the AutoComplete functionality:
Public Sub EnforceList(ByRef box As ComboBox) 'FORCES .TEXT TO BEST (OR FIRST) MATCH IN .ITEMS
'If list contains item whose name begins another item's, the shorter must be listed first, e.g. "sew" must preceed "sewer"
If box.Items.Count = 0 Then Exit Sub 'Can't enforce a list that doesn't exist
Dim txt As String = box.Text
Do
For i As Integer = 0 To box.Items.Count - 1
If box.Items(i).ToString Like txt & "*" Then
box.Text = box.Items(i).ToString
Exit Sub
End If
Next
txt = Left(txt, Len(txt) - 1)
Loop
End Sub

Related

Question on multiple checkboxes launching code

I have a user form and a frame with 35 checkboxes in it, numbered 1 to 35. They represent 35 Named Ranges. I test to see if any of the name ranges are not set, if set correctly the checkbox value is set to TRUE.
I found some code that would allow me to trigger a sub if one of the checkboxes is clicked. That code seems to work, but my check code above also triggers the checkbox events, which I do not want. I only want the sub to run when the checkbox is clicked with the mouse? I can post the code I'm using, but though I'd first ask the question to see if what I would like to do is possible.
Thanks,
Jim
Code in class module:
Public WithEvents ChkBox As MSForms.CheckBox
Public Sub AssignClicks(ctrl As Control)
Set ChkBox = ctrl
End Sub
Public Sub chkBox_Click()
If chkBoxProcess = "Y" Then
'ThisWorkbook.Worksheets("Sheet1").Range(ChkBox.Name).Value = Format(Now, "dd.mm.yyyy")
'MsgBox ("check box number = " & ChkBox.Name & " " & ChkBox.Value)
' Else
End If
End Sub
Code in Forms:
Public Sub UserForm_Initialize()
Dim SheetCount, i As Integer
Dim sh As Worksheet
'Public SheetName, SheetName2, StartOldNewTimeA, OldNewTimeAdd As String
'Initialize the form frmChgNameRng
'Set array values of the day options
'Set array values for 12:00 timeframes
'Set array values for 12:30 timeframes
'Set colors used in Checkboxes
'Set array for Checkboxes (boxes are numbered accross the page, 1 corressponds to Mon_1200/Mon_1230, 8 corresponds to Mon_200/Mon_230, etc.)
'Formulas are placed in the time cells on the left of the page, the macro will add the appropriate value into the Mon_1200 time slot and all other cells update off that cell
chkBoxProcess = "N"
Dim ChkBoxes As cls_ChkBox
Dim ctrl As Control
Set colTickBoxes = New Collection
For Each ctrl In Me.Controls
If TypeName(ctrl) = "CheckBox" Then
Set ChkBoxes = New cls_ChkBox
ChkBoxes.AssignClicks ctrl
colTickBoxes.Add ChkBoxes
End If
Next ctrl
'..... lots of code for Range Name Checks, etc.
End Sub
Your code is conflating control state with model data, and so the only way to tell it "named range 32 is ON", or "named range 13 is OFF", is to alter a checkbox' state, which fires that control's Change event.
There's no way around that, it's just how controls work: they fire a Change event whenever their value changes, regardless of how that's done.
Instead of having controls' state be the data, make the controls' state alter the data.
This requires conceptualizing this data, first: looks like you need to associate a number/index to some Boolean value. An array can do this.
Private namedRangeStates(1 To 35) As Boolean
Note that depending on what you're doing, initializing the state should be feasible by iterating the workbook's Names collection in the UserForm_Initialize handler. Or better, the form could expose a method that takes an array of Boolean values, and copies that state into namedRangeStates.
Now, when a checkbox is modified, make it alter the state:
Private Sub Checkbox31_Change()
namedRangeStates(31) = Checkbox31.Value
End Sub
Your form can expose that state as a property:
Public Property Get NamedRangeState(ByVal index As Long) As Boolean
NamedRangeState = namedRangeStates(index)
End Property
Public Property Let NamedRangeState(ByVal index As Long, ByVal value As Boolean)
namedRangeStates(index) = value
End Property
And now you can modify the enapsulated state independently of the combobox values.

How do I efficiently pair Toggle Buttons and Textboxes in Access Form?

I know the title is a bit confusing so let me make myself as clear as possible.
In an Access form (2010), I have a set of text fields meant to contain dates. Those fields are all optional, the user can fill in any number of dates.
To make it more user-friendly, I want to associate each field to a toggle button. Then I want 2 things to happen :
On CURRENT event :
If a text field has a value, then it is visible and the toggle button
associated with it is pressed.
If a text field is empty, then it is not visible and the toggle button isn't pressed.
On CLICK of a toggle button :
If the text field associated with it has a value, then this field gets cleared (and invisible) and the toggle button gets de-pressed ;
If the text field associated with it is empty, then the focus is set on it and the toggle button gets pressed (and stay that way if a value is entered in the text field, otherwise everything gets back the way it was, invisible and unpressed).
So far I've achieved the first step by setting two collections of controls based on some examples I found online.
So, when the form is loaded, I call this :
Private mcolGroupTxToggle As New Collection
Private mcolGroupBuToggle As New Collection
Private Sub InitializeCollections()
Dim ctl As Control
If mcolGroupTxToggle.Count = 0 Then
For Each ctl In Me.Controls
If ctl.Tag = "txtoggle" Then
mcolGroupTxToggle.Add ctl, ctl.Name
End If
Next ctl
Set ctl = Nothing
End If
If mcolGroupBuToggle.Count = 0 Then
For Each ctl In Me.Controls
If ctl.Tag = "butoggle" Then
mcolGroupBuToggle.Add ctl, ctl.Name
End If
Next ctl
Set ctl = Nothing
End If
End Sub
And on Form_Current event, I call that :
Private Sub OnLoadToggles(mcol As Collection, pcol As Collection)
Dim ctl As Control
Dim btn As Control
Dim strBtn As String
For Each ctl In mcol
'Every button has the same name than the textbox + "b"
strBtn = ctl.Name & "b"
For Each btn In pcol
If btn.Name = strBtn Then
If IsNull(ctl) Then
ctl.Visible = False
btn.Value = False
Else
ctl.Visible = True
btn.Value = True
End If
End If
Next btn
Next ctl
Set ctl = Nothing
End Sub
Everything works well so far, but I'm not sure that's the best way to do it and I figured I would need to repeat some lines in step 2.
Making the distinction between text boxes and buttons in the procedure seems weird, I feel like it should be done prior so that I don't have to do it in every procedure. I also feel like it would be better to loop through each pair of controls (text + button) instead of each control in both collections.
Basically, I'm wondering if it would be (1) better and (2) possible to have something as simple as this :
Private Sub OnLoadToggles(Collection of pairs)
for each [pair of txt and btn]
if isnull(txt) Then
txt.visible = false
btn.value = false
else
...
end if
...
My guess is that I would need to make a public sub where I set a collection of pairs of buttons and text fields based on their tags (there are other controls in my form that need to be left alone) and names, but I'm not sure how, I'm a beginner in VBA.
Any suggestions please ?
-- Edit step 2 --
Thanks to Andre's answer the second part was easier than I thought. I've updated my sample database. So on click events I call this :
Private Sub ClickToggles()
Dim ctl As Control
Dim btn As Control
Dim strBtn As String
Dim strCtl As String
strBtn = Me.ActiveControl.Name
strCtl = Left(strBtn, Len(strBtn) - 1)
Set ctl = Me(strCtl)
Set btn = Me(strBtn)
If IsNull(ctl) Then
btn.Value = True
ctl.Visible = True
ctl.Enabled = True
ctl.SetFocus
Else
ctl.Value = ""
btn.Value = False
ctl.Visible = False
End If
End Sub
It's not perfect but it works. Probably not a good idea to clear the data at this point because misclicks may happen. It would be better to loop through the textboxes right before saving the form and clear values of invisible and/or disabled controls from the collection. I might to that later.
I had to add the .enabled property next to the .visible one because on lostfocus events I was getting an error saying the control was still active so it couldn't make it not visible.
Right now I'm more concerned about the amount of click and lost focus events. I'd rather have some public functions and event handlers dealing with it but it's getting too complicated for me. I'll get back to it when I know more about... everything.
Suggestions are still welcome anyway =) !
Since your control pairs are "paired" by their name anyway, you don't need any fancy constructs, or even the second collection. Just address the matching control directly by its name.
Instead of using If/Else for setting boolean properties, it is usually easier to assign a variable to the property.
Private Sub OnLoadToggles(mcol As Collection)
Dim ctl As Control
Dim btn As Control
Dim strBtn As String
Dim bShowIt As Boolean
For Each ctl In mcol
'Every button has the same name than the textbox + "b"
strBtn = ctl.Name & "b"
' If you have the control name, you can get the control directly:
Set btn = Me(strBtn)
' Using a variable you don't need If/Else
bShowIt = Not IsNull(ctl.Value)
ctl.Visible = bShowIt
btn.Value = bShowIt
Next ctl
' Note: this is not necessary for local variables - they go automatically out of scope
Set ctl = Nothing
End Sub

Highlight sets of matches within textbox

Upon first inspection, I surmised this would be a straightforward task (maybe it still is!), yet I am having difficulty. Suppose I have a form with 1000 textboxes, where each textbox contains randomly distributed, yet in many cases matching strings. For example, the below may be found in any one if the 1000 textboxes:
AAAA-XXXX
AAAA-XXXX
BBBB-XXXX
BBBB-XXXX
CCCC-XXXX
CCCC-XXXX
...
How might I loop through the textboxes, identify all the matching examples and highlight the textbox.backcolor where the matches occur? The backcolor should be the same for the exact matches, yet different for each unique set of matches. There could be as many as 100 different sets!
Just whipped together a working test project, this is the kind of approach I'd take. It avoids comparing every textbox to every other textbox. I've commented it all up for you :)
Private Sub SetBoxColors()
'The keys are textbox texts, the values are the number of times it occurs
Dim UniqueTextsAndUsage As New Dictionary(Of String, Integer)
Dim FirstInstanceTextBoxes As New List(Of TextBox)
'The keys are textbox texts, the values are the colour for the box
Dim UniqueColors As New Dictionary(Of String, System.Drawing.Color)
'Iterate over all the text boxes
' Substitute Me for your Form instance if necessary
For Each TBox As Control In Me.Controls
'Skip things that aren't textboxes
If Not TypeOf TBox Is TextBox Then
Continue For
End If
'If we have seen this textbox text before
If UniqueTextsAndUsage.ContainsKey(TBox.Text) Then
'Increase the usage
UniqueTextsAndUsage(TBox.Text) += 1
If UniqueTextsAndUsage(TBox.Text) = 2 Then
'This is the second usage, generate a colour for this set of boxes
UniqueColors.Add(TBox.Text, GenerateColor(UniqueColors.Count + 1))
End If
'Colour this textbox
' (it won't get the first instance of each unique string)
TBox.BackColor = UniqueColors(TBox.Text)
Else
'We have NOT seen this textbox text before
'Add the first occurence of the text
UniqueTextsAndUsage.Add(TBox.Text, 1)
'Mark this textbox as one we may have to colour later
FirstInstanceTextBoxes.Add(TBox)
End If
Next
'Colour all the first instances
For Each TBox As TextBox In FirstInstanceTextBoxes
'Check there are sufficient uses of this text
If UniqueTextsAndUsage(TBox.Text) > 1 Then
TBox.BackColor = UniqueColors(TBox.Text)
End If
Next
End Sub
Private Function GenerateColor(Id As Integer) As System.Drawing.Color
'Needs more thought - often too dark
Dim KnownColourByIdNumber As System.Drawing.KnownColor = Id
Return System.Drawing.Color.FromKnownColor(KnownColourByIdNumber)
End Function
The GenerateColor function needs more thought so that it generates some nicer colours but I leave that to you.
You may also need a reset that sets every box to the DefaultControl color or whatever it's called, and run that at the top of SetBoxColors.
You can do this
Dim strText As String
For Each TextBox1 As System.Windows.Forms.TextBox In Me.Controls
strText = TextBox1.Text
For Each TextBox2 As System.Windows.Forms.TextBox In Me.Controls
If strText = TextBox2.Text AndAlso TextBox2.Name <> TextBox1.Name Then
TextBox2.BackColor = Color.Red ' or whatever you want to use
TextBox1.BackColor = Color.Red
End If
Next
Next

Manipulating ListBoxes as Objects

I am working on a VBA userform that includes ListBoxes.
So far, when I had to manipulate one or more, I always proceeded like this in my subs, with dlg as the dialogbox name, and it did not pose any problem, given that I never wanted to do anything complicated:
Dim List1 As Object
...
List1 = dlg.GetControl("CBXname")
...
List1.addItem("String",index)
...
Now I would like to do the following in this Sub
...
If (List1.Exists(Cell1.String) = False) Then
List1.addItem(Cell1.String,k)
End If
...
List1.Clear
...
But I can do neither since List1 is an Object. However, if I decide to declare List1 as a Listbox instead, I do not know how to get the proper control on the ListBox from the dialogbox (the current getcontrol gives me an error).
One of the issues with your code is that listbox objects do not have an "exists" property. To check if a value already exists in your listbox items, you will need to loop through the items.
dim i as integer
for i = 0 to List1.listcount - 1
if List1.column(0, i) = myvalue then
'myvalue exists in List1, skip
else
List1.additem myvalue
end if
next i
Where myvalue is whatever value you are trying to add to the listbox. But that brings us to the second issue in your code which is where you add "Cell1.String". If you are trying to add a value from a worksheet range you will need to refer to that range's value, as worksheet ranges do not have a "string" property as you use it here. Ie. Cell1 = Range("A1").value
As for getting control of the listbox, you can simply refer to the objects name as an object of the form. For example, dlg.List1, if the object's name is List1.
Here is a general purpose routine you can call for any list box. The calling code assumes a list box called ListBox1, a text box called TextBox1, and a Command Button called CommandButton. When you click on the button, it searches the listbox for the text from textbox1.
Private Function ExistsInListbox(ByRef aListBox As msforms.ListBox, ByVal Item As String) As Boolean
Dim booFound As Boolean
booFound = False
Dim t As Integer
ExistsInListbox = False
For t = 0 To aListBox.ListCount - 1 'correction, aListBox not ListBox1
If Item = aListBox.List(t) Then
'if we find a match, short-circuit the loop
booFound = True
Exit For
End If
Next
ExistsInListbox = booFound
End Function
private sub CommandButton_click()
Dim answer As String
Dim val As Boolean
val = ExistsInListbox(Me.ListBox1, TextBox1.Text)
If val Then
answer = "found"
Else
answer = "Not Found"
End If
MsgBox "found-" & answer
End Sub

VB Avoiding text box that is occupied

I am making an application where there is a form with a column of text boxes that are filled from two different forms with a button on each. when the button of these forms are clicked they input the results into the text boxes in the column.
the problem I am having is that say i click (form2.button 1) three times it will occupy text boxes 1,2 and 3. Now say I want to use (form1.button 1) to input data into text box 4 it will occupy text box 1.
I have each button set up for multiple clicks so I would like to understand how i can have is so say(form2.button 1) is clicked then (form1.button1) will go to 2nd click for example. there are 10 text boxes so I will need it so that they react to how many times each has been clicked.
Assuming the TextBoxes were called TextBox1 thru TextBox10, you could use a sub like this:
Public Sub AddValue(ByVal value As String)
For i As Integer = 1 To 10
Dim tbName As String = "TextBox" & i
Dim matches() As Control = Me.Controls.Find(tbName, True)
If matches.Length > 0 AndAlso TypeOf matches(0) Is TextBox Then
Dim tb As TextBox = DirectCast(matches(0), TextBox)
If tb.Text.Trim.Length = 0 Then
tb.Text = value
Exit Sub
End If
End If
Next
MessageBox.Show("All TextBoxes are already taken!")
End Sub
Why not just store a counter variable in the form that contains the textboxes, say NextTextBoxNumber that increments each time a textbox is filled in and then you know exactly which textbox to go to next based upon the value of this variable?
Also, to continue with #Oded's comment, you can easily accomplish what he meant using something along the lines of If Textbox1.Text <> "" Then ...
Does that make sense??