Could anyone please tell me what i should do programmatically to be able to select an item in a listbox using the keyboard when there are multiple items starting with the same character/s.
For eg,
One
Two
Three
Once
Orange
If i want to get the focus on "Once" by typing o,n,c what should i do? Instead of jumping from one item to the other as opposed to the default behaviour.
Add a KeyPress event handler to the ListBox and track the keys that are pressed. Then compare the complete value that already has been typed to the values from the items in the ListBox. If there's a match, select the item.
Edit:
Here's a solution that I created that works. I also measure the time between keypresses. This way, if the time between to keypresses is more than 1.5 seconds, the search-string is emptied and re-filled with the last search-character. After that it's like I said: find a match and if there's a match, select that item. The two private fields are class-level fields to keep track of the time of the last keypress and the string is to store the search-string.
private DateTime _lastKeyPress;
private string _searchString;
private void ListBox1KeyPress(object sender, KeyPressEventArgs e)
{
var newDate = DateTime.Now;
var diff = newDate - _lastKeyPress;
if (diff.TotalSeconds >= 1.5)
_searchString = string.Empty;
_searchString += e.KeyChar;
for (var i = 0; i < listBox1.Items.Count; i++)
{
var item = listBox1.Items[i].ToString();
if (item.ToLower().StartsWith(_searchString))
{
listBox1.SelectedItem = item;
break;
}
}
_lastKeyPress = newDate;
e.Handled = true; //REALLY IMPORTANT TO HAVE THIS
}
And here's an example using LinQ to get a match for the searchitem:
private void ListBox1KeyPress(object sender, KeyPressEventArgs e)
{
var newDate = DateTime.Now;
var diff = newDate - _lastKeyPress;
if (diff.TotalSeconds >= 1.5)
_searchString = string.Empty;
_searchString += e.KeyChar;
var found = listBox1.Items.Cast<object>().Select(t => t.ToString()).Where(item => item.ToLower().StartsWith(_searchString)).FirstOrDefault();
if(!String.IsNullOrEmpty(found))
listBox1.SelectedItem = found;
_lastKeyPress = newDate;
e.Handled = true;
}
Hope this helps! ;)
Edit2:
I don't know if you noticed the comment with the imortance of the e.Handled. By default if you press a key in the ListBox, the control will select the first-found item with that key-character. But it has not the functionality that my code has. So, if you remote the e.Handled line, the code will work but the control will also process the keypress and you don't want that: items will not be correctly selected!
You want to use the LBS_SORT list box style. This style cannot be applied after the control has been created and so to add this style you need to override the CreateParams property. Create a class (lets call it SortedListBox) which derives from ListBox and override this property like so
public class MyListBox : ListBox
{
protected override CreateParams CreateParams
{
get
{
var returnValue = base.CreateParams;
returnValue.Style |= 0x2; // Add LBS_SORT
returnValue.Style ^= 128; // Remove LBS_USETABSTOPS (optional)
return returnValue;
}
}
}
This list box should now both sort the items in your list & support incremental searching (you can't turn the sorting off I'm afraid, if you need to control the sort order then you need to do the incremental searching yourself, as Abbas has suggested)
Update: If you additionally remove the LBS_USETABSTOPS style you even get an edit carret showing what character the incremental search is currently matching on
I just modified Abbas code for my case. This is my desktop program and using in one of the form which has ListBox control. This is working great for me. Thanks to Abbas
private DateTime _lastKeyPress;
private string _searchString;
private void lstEmployer_KeyPress(object sender, KeyPressEventArgs e)
{
var newDate = DateTime.Now;
var diff = newDate - _lastKeyPress;
if (diff.TotalSeconds >= 2)
_searchString = string.Empty;
_searchString += e.KeyChar;
lstEmployer.SelectedIndex = lstEmployer.FindString(_searchString);
_lastKeyPress = newDate;
e.Handled = true; //REALLY IMPORTANT TO HAVE THIS
}
Related
Hello I have a program that has a series of comboboxes that users can pick different values from. One of the cb has a 1, 2, and 3 int value in it. The values correspond to first, second and third shift, respectively. I would like to disable values 2 and 3 if the system time is from 6 am. Till 2 pm. And then do the same thing for the other shifts. Disable or grey out the number 1 and 3 if the system time is from 2 pm till 10 pm. And disable CB values 1 and 2 if the system time is anywhere from 10 pm. to 6 am. I am currently showing the system time inside a label on the UI using a timer.
I don't know where to begin. I've looked in Google and all the answers there, pertain to WPF or Javascript not Winforms. Is this function even possible in WinForms c#?
Thank you.
Note: This answer borrowed heavily from this article, written by Apurba Ranjan.
One "hacky" thing you can do is create a custom class for your combo box items that contains an Enabled property:
class ComboBoxItem
{
public string Text { get; set; }
public bool Enabled { get; set; }
public ComboBoxItem(string text, bool enabled)
{
Text = text;
Enabled = enabled;
}
}
Then, when we add items of this class to the combo box, we set the Enabled property based on the current time (note that we also set the DisplayMember and hook up an event handler for the SelectedIndexChanged event):
private void Form1_Load(object sender, EventArgs e)
{
cboShifts.DisplayMember = "Text";
cboShifts.SelectedIndexChanged += CboShifts_SelectedIndexChanged;
var currentTime = DateTime.Now;
var firstEnabled = currentTime >= DateTime.Parse("6:00 AM") &&
currentTime < DateTime.Parse("2:00 PM");
var secondEnabled = currentTime >= DateTime.Parse("2:00 PM") &&
currentTime < DateTime.Parse("10:00 PM");
var thirdEnabled = !firstEnabled && !secondEnabled;
cboShifts.Items.Add(new ComboBoxItem("1", firstEnabled));
cboShifts.Items.Add(new ComboBoxItem("2", secondEnabled));
cboShifts.Items.Add(new ComboBoxItem("3", thirdEnabled));
}
Then, in the SelectedIndexChanged event, we can get the selected item and if it's null (meaning selected index is -1) then we return, otherwise if its Enabled property is false, we set the selected index to -1 (basically not allowing them to select it):
private void CboShifts_SelectedIndexChanged(object sender, EventArgs e)
{
var item = (sender as ComboBox)?.SelectedItem as ComboBoxItem;
if (item == null) return;
// Don't let the user select a disabled item
if (!item.Enabled)
{
cboShifts.SelectedIndex = -1;
MessageBox.Show($"Item '{item.Text}' is not enabled");
return;
}
// Optionally do something here for a valid selection
}
The AutoCompleteSource and AutoCompleteMode properties of the TextBox allow me to use automatic completion in textboxes.
I have bound directly a datatable as AutoCompleteSource of the textbox and it works well.
In some situations that the input words is not available in the sources, the auto completion has no result and so, i need to do something else in those situations.
How should i check whether the automatic completion result is empty?
Here is one approach you can take. The following code will get suggestions in the TextChanged event of the textbox when more than 3 characters have been entered. We go get the suggestions and then check if any suggestions were returned. If yes, we set the AutoCompleteCustomSource. Otherwise, we will do something--whatever we want to do.
private void textBox1_TextChanged(object sender, EventArgs e)
{
TextBox t = sender as TextBox;
if (t != null)
{
// Here I am making the assumption we will get suggestions after
// 3 characters are entered
if (t.Text.Length >= 3)
{
// This will get the suggestions from some place like db,
// table etc.
string[] arr = GetSuggestions(t.Text);
if (arr.Length == 0) {// do whatever you want to}
else
{
var collection = new AutoCompleteStringCollection();
collection.AddRange(arr);
this.textBox1.AutoCompleteCustomSource = collection;
}
}
}
}
I'm making an application that consists of comboBoxes. If the user selected "Chauffeur" in the comboBox the total price goes up by 10% once my btnAddDriver is clicked. However when I select "Chauffeur" the total price does not increase by 10% when I click Add Driver in fact when using brake points it doesn't seem to realise I have selected "Chauffeur" and skips the calculation within the if statement.
My Code is as fallows
int policy = 500;
double Chauffeur = 0.10;
private void cmbOccupation_Loaded(object sender, RoutedEventArgs e)
{
// ... A List.
List<string> occupation = new List<string>();
occupation.Add("Chauffeur ");
occupation.Add("Accountant");
// ... Get the ComboBox reference.
var comboBox = sender as ComboBox;
// ... Assign the ItemsSource to the List.
comboBox.ItemsSource = occupation;
// ... Make the first item selected.
comboBox.SelectedIndex = 0;
}
private void btnAddDriver_Click(object sender, RoutedEventArgs e)
{
txtPolicy.Text = policy.ToString();
if (cmbOccupation.SelectedItem.ToString() == "Chauffeur")
{
txtPolicy.Text = (policy * Chauffeur).ToString();
}
}
"Chauffeur" and "Chauffeur " are two different strings in C#.
That'll be $150, please pay the girl at the desk on your way out.
Change occupation.Add("Chauffeur ");
To occupation.Add("Chauffeur");
I am working on a simple application (phonebook) in C# and in my project I have got a listview filled with contacts. I have been trying to implement the possibility to automatically (instantly) search through a listview using a textbox. I have managed to make it work, but not in the desired way. You will realise the actual problem if I give you an example. Let's say that I have got a contact named Bill Gates and when I try searching for it - it gets found and that part is OK. But, the problem is when I try to search for another contact. In that case, I have to clear the textbox before I type another name, but it is possible to remove only letter by letter. When I start removing the whole name, after removing a first letter it acts like I have just entered the name - it selects the item (and focuses as well) - actually there is no time to remove the whole name before it finds a contact again. I have to remove a first letter, then switch back to the textbox, remove another letter etc. Is there any solution for searching to be automatic - as it is now, but on the other hand for removing (clearing the textbox) without selecting contacts to be possible.
Take a look at the code:
private void txt_Search_TextChanged(object sender, System.EventArgs e)
{
if (txt_Search.Text != "")
{
foreach (ListViewItem item in listView1.Items)
{
if (item.Text.ToLower().Contains(txt_Search.Text.ToLower()))
{
item.Selected = true;
}
else
{
listView1.Items.Remove(item);
}
}
if (listView1.SelectedItems.Count == 1)
{
listView1.Focus();
}
}
else
{
LoadContacts();
RefreshAll();
}
}
There are some things wrong in your code, firstly when modifying a collection in a loop through it, we should not use foreach although in some case it seems to work but not really, it will surely be strange in future and confuse you. We should use a for loop instead and loop in the reverse order. The second wrong thing is you set the Selected to true which may cause your textBox lose focus to the listView. The solution is we have to use some other way to indicate that the item is selected, such as by using BackColor instead:
private void txt_Search_TextChanged(object sender, System.EventArgs e)
{
if (txt_Search.Text != "") {
for(int i = listView1.Items.Count - 1; i >= 0; i--) {
var item = listView1.Items[i];
if (item.Text.ToLower().Contains(txt_Search.Text.ToLower())) {
item.BackColor = SystemColors.Highlight;
item.ForeColor = SystemColors.HighlightText;
}
else {
listView1.Items.Remove(item);
}
}
if (listView1.SelectedItems.Count == 1) {
listView1.Focus();
}
}
else
LoadContacts();
RefreshAll();
}
}
Also after user focusing the ListView, all the BackColor and ForeColor should be reset, we can handle the Enter event of ListView:
//Enter event handler for listView1
private void listView1_Enter(object sender, EventArgs e){
foreach(ListViewItem item in listView1.Items){
item.BackColor = SystemColors.Window;
item.ForeColor = SystemColors.WindowText;
}
}
EDIT
you better not use Text_Changed, rather try Key_Down method as follows
private void txt_Search_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter) //apply your search only when pressing ENTER key
{
// you do your search as it was before
// i personally don't have suggestions here
if (!txt_Search.AutoCompleteCustomSource.Contains(txt_Search.Text)) txt_Search.AutoCompleteCustomSource.Add(txt_Search.Text);
//the line above will save all your searched contacts and display it in a beautiful format
}
else if (txt_Search.Text == "")
{
LoadContacts();
RefreshAll();
}
}
Of course don't forget to set the properties of txt_Search
AutoCompleteMode = SuggestAppend and AutoCompleteSource = CustomSource
This kind of feels like a hack, but you could track the length of text that has been typed into the textbox, and only perform your searching and focus logic if the length of text is greater than the previous time the event was called. That way if someone deletes a letter, the searching and focusing won't occur. Something like:
// declare lastSearchLength as a int outside of your TextChanged delegate
if (!String.IsNullOrEmpty(txt_Search.Text) && txt_Search.Text.Length > lastSearchLength)
{
foreach (ListViewItem item in listView1.Items)
{
if (item.Text.ToLower().Contains(txt_Search.Text.ToLower()))
{
item.Selected = true;
}
else
{
listView1.Items.Remove(item);
}
}
if (listView1.SelectedItems.Count == 1)
{
listView1.Focus();
}
lastSearchLength = txt_Search.Text.Length;
}
else
{
LoadContacts();
RefreshAll();
}
}
You are doing a postback with every key press. When the page reloads, it will not retain focus where you would expect. I recommend implementing this in JavaScript on the client, or using a search button instead of TextChanged event.
Here is a short program that reproduces the problem I just encountered. This was compiled under MS Windows 7 with .NET 4.0, just in case that makes a difference.
using System;
using System.Drawing;
using System.Windows.Forms;
// Compile with "csc /target:exe /out:comboboxbug.exe /r:System.dll /r:System.Drawing.dll /r:System.Windows.Forms.dll comboboxbug.cs"
// in a Visual Studio command prompt.
static class Program
{
[STAThread]
static void Main()
{
//Create a label.
Label oLabel = new Label();
oLabel.Location = new Point (10, 10);
oLabel.Size = new Size (100, 15);
oLabel.Text = "Combo box bug:";
// Create a combo-box.
ComboBox oComboBox = new ComboBox();
oComboBox.Location = new Point (10, 50);
oComboBox.Size = new Size (150, 21);
oComboBox.Items.AddRange (new object[]
{ "A", "A B", "A C", "A B C", "A C B", "A B C D", "A C B D" });
oComboBox.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
oComboBox.AutoCompleteSource = AutoCompleteSource.ListItems;
oComboBox.SelectionChangeCommitted
+= new EventHandler (comboBox_SelectionChangeCommitted);
// Create a form.
Form oForm = new Form();
oForm.Size = new Size (200, 150);
oForm.Controls.Add (oLabel);
oForm.Controls.Add (oComboBox);
// Run this form.
Application.Run (oForm);
}
static void comboBox_SelectionChangeCommitted (object sender,
EventArgs e)
{
MessageBox.Show ("SelectionChangeCommitted");
}
}
Click in the text portion of the combo-box and type "A". You will get a list of autocomplete suggestions. Click one of the selections with your mouse. The SelectionChangeCommitted event doesn't happen!
Select a menu-item without using autocomplete. You'll get a message-box showing that the SelectionChangeCommitted event happened!
Given that the selection was changed by the user in both cases, shouldn't SelectionChangeCommitted be called in both cases?
Using the SelectedIndexChanged event is not an option, because for the application behind this canned example, I only want it to happen when the user makes a selection, not when it's set programmatically.
EDIT 2020-Oct-28: I found another case where SelectionChangeCommitted doesn't get called! Auto-complete doesn't even need to be set for the problem to happen! Click to open the combo-box, press a key that matches the beginning of one of the combo-box items, and then press Tab to leave. The combo-box item gets selected, but SelectionChangeCommitted is not called! My revised answer is below.
Using the SelectedIndexChanged event is not an option, because for the application behind this canned example, I only want it to happen when the user makes a selection, not when it's set programmatically.
You could also accomplish this by writing a wrapper method for changing the selection that temporarily disables your event.
Unfortunately I do not know off hand a solution to the issue that SelectionChangeCommitted not being started for the more general case (such as where you don't control the ComboBox or how it is accessed).
EDIT:
I made a streamer of all the events that ComboBox calls, and it doesn't appear that any other event will do what you are looking for. The only solution I can think of would involve hooking into the events that the AutoComplete triggers. The difficulty is knowing what those events are, since they don't appear to trigger off the ComboBox from what my minor testing shows.
FYI, here was the best solution I ever came up with. Obviously, this is a Leave event-handler on a ComboBox subclass. The SelectionChangeCommitted event doesn't happen on the mouse-click, but at least it happens during the normal flow of GUI interaction.
private void this_Leave (object sender, EventArgs e)
{
// If this is an autocomplete combo-box, select the
// item that was found by autocomplete.
// This seems like something that ComboBox should be
// doing automatically...I wonder why it doesn't?
if (this.AutoCompleteMode != AutoCompleteMode.None)
{
// Determine which combo-box item matches the text.
// Since IndexOf() is case-sensitive, do our own
// search.
int iIndex = -1;
string strText = this.Text;
ComboBox.ObjectCollection lstItems = this.Items;
int iCount = lstItems.Count;
for (int i = 0; i < iCount; ++i)
{
string strItem = lstItems[i].ToString();
if (string.Compare (strText, strItem, true) == 0)
{
iIndex = i;
break;
}
}
// If there's a match, and this isn't already the
// selected item, make it the selected item.
//
// Force a selection-change-committed event, since
// the autocomplete was driven by the user.
if (iIndex >= 0
&& this.SelectedIndex != iIndex)
{
this.SelectedIndex = iIndex;
OnSelectionChangeCommitted (EventArgs.Empty);
}
}
}
If someone got this problem, I suggest a solution that works fine to me...
Think with me, to accept the suggest of Combo-box, generally the user needs to key down with an Enter key.
You can write into KeyDown event of Combo-box property, something like this:
private void cboProperty_SelectionChangeCommitted(object sender, EventArgs e)
{
//Call here the event of SelectionChangeCommitted
cboProperty_SelectionChangeCommitted(sender,null);
}
It will raise the SelectionChangeCommitted on the right time.
This workaround worked fine for me and hope for you too. When use Autocomplete by typing data in your combo box to get an item through keyboard or mouse selection you need _KeyDown event. From inside invoke _SelectionChangeCommitted method which contains code for other operations. See code below:
private void YourComboBox_KeyDown(object sender, KeyEventArgs e)
{
//Works also when user select and click on autocomplete list.
if (e.KeyCode == Keys.Enter && YourComboBox.SelectedItem != null)
YourComboBox_SelectionChangeCommitted(sender, e);
}
For the non-auto-complete case mentioned above (i.e. my 2020-Oct-28 edit), this Leave event-handler on a ComboBox subclass incorporates the new case as well as the old one, as long as your SelectionChangeCommitted event-handler is idempotent. Compared to my previous answer, it removes the test for auto-complete, and always calls OnSelectionChangeCommitted().
private void this_Leave (object sender, EventArgs e)
{
// Determine which combo-box item matches the text.
// Since IndexOf() is case-sensitive, do our own
// search.
int iIndex = -1;
string strText = this.Text;
ComboBox.ObjectCollection lstItems = this.Items;
int iCount = lstItems.Count;
for (int i = 0; i < iCount; ++i)
{
string strItem = lstItems[i].ToString();
if (string.Compare (strText, strItem, true) == 0)
{
iIndex = i;
break;
}
}
// If there's a match, and this isn't already the
// selected item, make it the selected item.
if (iIndex >= 0
&& this.SelectedIndex != iIndex)
this.SelectedIndex = iIndex;
// Force a selection-change-committed event, since
// the autocomplete was driven by the user.
OnSelectionChangeCommitted (EventArgs.Empty);
}