WinForms | C # | AutoCompletar no meio de uma checkbox de texto?

Eu tenho uma checkbox de texto que faz autocompletion assim:

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

Funciona, mas apenas no início de uma checkbox de texto. Eu gostaria que o autocomplete entrasse em vigor para qualquer palavra que o usuário estivesse inserindo, em qualquer posição na checkbox 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 Amostra

 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; } } } 

Fiz algumas alterações na solução proposta por @PaRiMaL RaJ porque a checkbox de listview não estava sendo exibida quando a checkbox de texto estava dentro de um UserControl que não era alto o suficiente. Basicamente, em vez de adicionar a checkbox de listview ao pai da checkbox de texto, adicionei ao formulário e calculo a posição absoluta no formulário.

 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); } } } 

As outras soluções não funcionaram para mim em um ambiente multilinha às minhas necessidades, então adicionei a resposta de @Francisco Goldenstein para habilitar isso. O que eu precisava era autocomplete qualquer “palavra” no TextBox e em qualquer posição / linha. Após um teste mínimo, esta class parece funcionar bem o suficiente para mim em um TextBox multilinhas. Espero que ajude alguém.

As principais alterações estão em UpdateListBox() e this_KeyDown() , para lidar com a palavra ‘atual’, ou seja, aquela logo antes da posição do cursor, em vez de todo o conteúdo da checkbox de texto.

Altere a definição de separators em UpdateListBox() para atender às suas necessidades.

 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; } } }