WinForms | C # | ¿Autocompletar en medio de un cuadro de texto?

Tengo un cuadro de texto que hace autocompletar así:

txtName.AutoCompleteMode = AutoCompleteMode.Suggest; txtName.AutoCompleteSource = AutoCompleteSource.CustomSource; txtName.AutoCompleteCustomSource = namesCollection; 

Funciona, pero solo al comienzo de un cuadro de texto. Me gustaría que la función autocompletar se active con cualquier palabra que ingrese el usuario, en cualquier posición en el cuadro de texto.

 using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; namespace TubeUploader { public class AutoCompleteTextBox : TextBox { private ListBox _listBox; private bool _isAdded; private String[] _values; private String _formerValue = String.Empty; public AutoCompleteTextBox() { InitializeComponent(); ResetListBox(); } private void InitializeComponent() { _listBox = new ListBox(); KeyDown += this_KeyDown; KeyUp += this_KeyUp; } private void ShowListBox() { if (!_isAdded) { Parent.Controls.Add(_listBox); _listBox.Left = Left; _listBox.Top = Top + Height; _isAdded = true; } _listBox.Visible = true; _listBox.BringToFront(); } private void ResetListBox() { _listBox.Visible = false; } private void this_KeyUp(object sender, KeyEventArgs e) { UpdateListBox(); } private void this_KeyDown(object sender, KeyEventArgs e) { switch (e.KeyCode) { case Keys.Tab: { if (_listBox.Visible) { InsertWord((String)_listBox.SelectedItem); ResetListBox(); _formerValue = Text; } break; } case Keys.Down: { if ((_listBox.Visible) && (_listBox.SelectedIndex < _listBox.Items.Count - 1)) _listBox.SelectedIndex++; break; } case Keys.Up: { if ((_listBox.Visible) && (_listBox.SelectedIndex > 0)) _listBox.SelectedIndex--; break; } } } protected override bool IsInputKey(Keys keyData) { switch (keyData) { case Keys.Tab: return true; default: return base.IsInputKey(keyData); } } private void UpdateListBox() { if (Text == _formerValue) return; _formerValue = Text; String word = GetWord(); if (_values != null && word.Length > 0) { String[] matches = Array.FindAll(_values, x => (x.StartsWith(word, StringComparison.OrdinalIgnoreCase) && !SelectedValues.Contains(x))); if (matches.Length > 0) { ShowListBox(); _listBox.Items.Clear(); Array.ForEach(matches, x => _listBox.Items.Add(x)); _listBox.SelectedIndex = 0; _listBox.Height = 0; _listBox.Width = 0; Focus(); using (Graphics graphics = _listBox.CreateGraphics()) { for (int i = 0; i < _listBox.Items.Count; i++) { _listBox.Height += _listBox.GetItemHeight(i); // it item width is larger than the current one // set it to the new max item width // GetItemRectangle does not work for me // we add a little extra space by using '_' int itemWidth = (int)graphics.MeasureString(((String)_listBox.Items[i]) + "_", _listBox.Font).Width; _listBox.Width = (_listBox.Width < itemWidth) ? itemWidth : _listBox.Width; } } } else { ResetListBox(); } } else { ResetListBox(); } } private String GetWord() { String text = Text; int pos = SelectionStart; int posStart = text.LastIndexOf(' ', (pos < 1) ? 0 : pos - 1); posStart = (posStart == -1) ? 0 : posStart + 1; int posEnd = text.IndexOf(' ', pos); posEnd = (posEnd == -1) ? text.Length : posEnd; int length = ((posEnd - posStart) < 0) ? 0 : posEnd - posStart; return text.Substring(posStart, length); } private void InsertWord(String newTag) { String text = Text; int pos = SelectionStart; int posStart = text.LastIndexOf(' ', (pos < 1) ? 0 : pos - 1); posStart = (posStart == -1) ? 0 : posStart + 1; int posEnd = text.IndexOf(' ', pos); String firstPart = text.Substring(0, posStart) + newTag; String updatedText = firstPart + ((posEnd == -1) ? "" : text.Substring(posEnd, text.Length - posEnd)); Text = updatedText; SelectionStart = firstPart.Length; } public String[] Values { get { return _values; } set { _values = value; } } public List SelectedValues { get { String[] result = Text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); return new List(result); } } } } 

Uso de la muestra

 using System; using System.Windows.Forms; namespace AutoComplete { public partial class TestForm : Form { private readonly String[] _values = { "one", "two", "three", "tree", "four", "fivee" }; public TestForm() { InitializeComponent(); // AutoComplete is our special textbox control on the form AutoComplete.Values = _values; } } } 

Hice algunos cambios a la solución propuesta por @PaRiMaL RaJ porque el cuadro de lista no se mostraba cuando el cuadro de texto estaba dentro de un UserControl que no era lo suficientemente alto. Básicamente, en lugar de agregar el cuadro de lista al elemento primario del cuadro de texto, agregué al formulario y calculé la posición absoluta en el formulario.

 public class AutoCompleteTextBox : TextBox { private ListBox _listBox; private bool _isAdded; private String[] _values; private String _formerValue = String.Empty; public AutoCompleteTextBox() { InitializeComponent(); ResetListBox(); } private void InitializeComponent() { _listBox = new ListBox(); this.KeyDown += this_KeyDown; this.KeyUp += this_KeyUp; } private void ShowListBox() { if (!_isAdded) { Form parentForm = this.FindForm(); // new line added parentForm.Controls.Add(_listBox); // adds it to the form Point positionOnForm = parentForm.PointToClient(this.Parent.PointToScreen(this.Location)); // absolute position in the form _listBox.Left = positionOnForm.X; _listBox.Top = positionOnForm.Y + Height; _isAdded = true; } _listBox.Visible = true; _listBox.BringToFront(); } private void ResetListBox() { _listBox.Visible = false; } private void this_KeyUp(object sender, KeyEventArgs e) { UpdateListBox(); } private void this_KeyDown(object sender, KeyEventArgs e) { switch (e.KeyCode) { case Keys.Enter: case Keys.Tab: { if (_listBox.Visible) { Text = _listBox.SelectedItem.ToString(); ResetListBox(); _formerValue = Text; this.Select(this.Text.Length, 0); e.Handled = true; } break; } case Keys.Down: { if ((_listBox.Visible) && (_listBox.SelectedIndex < _listBox.Items.Count - 1)) _listBox.SelectedIndex++; e.Handled = true; break; } case Keys.Up: { if ((_listBox.Visible) && (_listBox.SelectedIndex > 0)) _listBox.SelectedIndex--; e.Handled = true; break; } } } protected override bool IsInputKey(Keys keyData) { switch (keyData) { case Keys.Tab: if (_listBox.Visible) return true; else return false; default: return base.IsInputKey(keyData); } } private void UpdateListBox() { if (Text == _formerValue) return; _formerValue = this.Text; string word = this.Text; if (_values != null && word.Length > 0) { string[] matches = Array.FindAll(_values, x => (x.ToLower().Contains(word.ToLower()))); if (matches.Length > 0) { ShowListBox(); _listBox.BeginUpdate(); _listBox.Items.Clear(); Array.ForEach(matches, x => _listBox.Items.Add(x)); _listBox.SelectedIndex = 0; _listBox.Height = 0; _listBox.Width = 0; Focus(); using (Graphics graphics = _listBox.CreateGraphics()) { for (int i = 0; i < _listBox.Items.Count; i++) { if (i < 20) _listBox.Height += _listBox.GetItemHeight(i); // it item width is larger than the current one // set it to the new max item width // GetItemRectangle does not work for me // we add a little extra space by using '_' int itemWidth = (int)graphics.MeasureString(((string)_listBox.Items[i]) + "_", _listBox.Font).Width; _listBox.Width = (_listBox.Width < itemWidth) ? itemWidth : this.Width; ; } } _listBox.EndUpdate(); } else { ResetListBox(); } } else { ResetListBox(); } } public String[] Values { get { return _values; } set { _values = value; } } public List SelectedValues { get { String[] result = Text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); return new List(result); } } } 

Las otras soluciones no me funcionaron en un entorno de líneas múltiples según mis necesidades, así que agregué la respuesta de @Francisco Goldenstein para habilitar esto. Lo que necesitaba era autocompletar cualquier ‘palabra’ en el TextBox y en cualquier posición / línea. Después de pruebas mínimas, esta clase parece funcionar bastante bien para mí en un TextBox de varias líneas. Espero que ayude a alguien.

Los principales cambios están en UpdateListBox() y this_KeyDown() , para tratar con la palabra ‘actual’, es decir, la que está justo antes de la posición de intercalación, en lugar de todo el contenido de la caja de texto.

Cambie la definición de separators en UpdateListBox() para satisfacer sus necesidades.

 using System; using System.Drawing; using System.Windows.Forms; class MultiLineAutoCompleteTextBox : TextBox { private ListBox _listBox; private bool _isAdded; private String[] _values; private String _formerValue = String.Empty; private int _prevBreak; private int _nextBreak; private int _wordLen; public MultiLineAutoCompleteTextBox() { InitializeComponent(); ResetListBox(); } private void InitializeComponent() { _listBox = new ListBox(); KeyDown += this_KeyDown; KeyUp += this_KeyUp; } private void ShowListBox() { if (!_isAdded) { Form parentForm = FindForm(); if (parentForm == null) return; parentForm.Controls.Add(_listBox); Point positionOnForm = parentForm.PointToClient(Parent.PointToScreen(Location)); _listBox.Left = positionOnForm.X; _listBox.Top = positionOnForm.Y + Height; _isAdded = true; } _listBox.Visible = true; _listBox.BringToFront(); } private void ResetListBox() { _listBox.Visible = false; } private void this_KeyUp(object sender, KeyEventArgs e) { UpdateListBox(); } private void this_KeyDown(object sender, KeyEventArgs e) { switch (e.KeyCode) { case Keys.Enter: case Keys.Tab: case Keys.Space: { if (_listBox.Visible) { Text = Text.Remove(_prevBreak == 0 ? 0 : _prevBreak + 1, _prevBreak == 0 ? _wordLen + 1 : _wordLen); Text = Text.Insert(_prevBreak == 0 ? 0 : _prevBreak + 1, _listBox.SelectedItem.ToString()); ResetListBox(); _formerValue = Text; Select(Text.Length, 0); e.Handled = true; } break; } case Keys.Down: { if ((_listBox.Visible) && (_listBox.SelectedIndex < _listBox.Items.Count - 1)) _listBox.SelectedIndex++; e.Handled = true; break; } case Keys.Up: { if ((_listBox.Visible) && (_listBox.SelectedIndex > 0)) _listBox.SelectedIndex--; e.Handled = true; break; } } } protected override bool IsInputKey(Keys keyData) { switch (keyData) { case Keys.Tab: if (_listBox.Visible) return true; else return false; default: return base.IsInputKey(keyData); } } private void UpdateListBox() { if (Text == _formerValue) return; if (Text.Length == 0) { _listBox.Visible = false; return; } _formerValue = Text; var separators = new[] { '|', '[', ']', '\r', '\n', ' ', '\t' }; _prevBreak = Text.LastIndexOfAny(separators, CaretIndex > 0 ? CaretIndex - 1 : 0); if (_prevBreak < 1) _prevBreak = 0; _nextBreak = Text.IndexOfAny(separators, _prevBreak + 1); if (_nextBreak == -1) _nextBreak = CaretIndex; _wordLen = _nextBreak - _prevBreak - 1; if (_wordLen < 1) return; string word = Text.Substring(_prevBreak + 1, _wordLen); if (_values != null && word.Length > 0) { string[] matches = Array.FindAll(_values, x => (x.ToLower().Contains(word.ToLower()))); if (matches.Length > 0) { ShowListBox(); _listBox.BeginUpdate(); _listBox.Items.Clear(); Array.ForEach(matches, x => _listBox.Items.Add(x)); _listBox.SelectedIndex = 0; _listBox.Height = 0; _listBox.Width = 0; Focus(); using (Graphics graphics = _listBox.CreateGraphics()) { for (int i = 0; i < _listBox.Items.Count; i++) { if (i < 20) _listBox.Height += _listBox.GetItemHeight(i); // it item width is larger than the current one // set it to the new max item width // GetItemRectangle does not work for me // we add a little extra space by using '_' int itemWidth = (int)graphics.MeasureString(((string)_listBox.Items[i]) + "_", _listBox.Font).Width; _listBox.Width = (_listBox.Width < itemWidth) ? itemWidth : Width; ; } } _listBox.EndUpdate(); } else { ResetListBox(); } } else { ResetListBox(); } } public int CaretIndex => SelectionStart; public String[] Values { get { return _values; } set { _values = value; } } }