26

I have a textbox that does autocompletion like so:

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

It works, but only at the beginning of a textbox. I'd like autocomplete to kick in for any word the user is entering, at any position in the textbox.

Chaddeus
  • 13,134
  • 29
  • 104
  • 162

3 Answers3

39
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<String> SelectedValues
        {
            get
            {
                String[] result = Text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                return new List<String>(result);
            }
        }

    }

}

Sample Usage

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

    }
}
Parimal Raj
  • 20,189
  • 9
  • 73
  • 110
  • 2
    This has the added benefit of working nicely with multiline textboxes. (Be sure to set `AcceptsTab` to *true*, however.) Incredibly useful! – ladenedge Mar 11 '13 at 03:57
  • You really deserve +1, and i would give you more if i could. I'm using your AutoCompleteTextBox in my open source project now and it's a great improvement for my user experience. Thank you! – teamalpha5441 Apr 18 '13 at 19:21
  • 9
    The owner of this custom control code is Peter Holpar, posted in 2010: http://pholpar.wordpress.com/2010/02/25/multivalue-autocomplete-winforms-textbox-for-tagging/ The source code can be downloaded at: http://autocompletetexboxcs.codeplex.com/ Next time acknowledge someone's contribution and work if you care about programming. Don't forget to give credit even if it's a free open code, it's not plagiarism, but it's discourteous and rude. – WhySoSerious Jan 21 '14 at 03:24
  • @WhySoSerious - well, when posting i had in my mind that i got this snippet from internet, but i didn't had the source or else i have always acknowledged the source! – Parimal Raj Jul 25 '14 at 12:10
  • Please can you tell me what is the AutoComplete in TestForm function ? – shahid zaman Jul 22 '16 at 05:29
  • It work like simple autocomplete not like sql like'%hello%' – shahid zaman Jul 22 '16 at 05:39
  • Nice control, but it doesn't work with objects like textbox autocomplete does, how can i adapt it to work like that, any pointers? – Smith Oct 19 '17 at 07:20
  • Just a note for others, this doesn't seem to be the general purpose multiline autocomplete textbox I was looking for, as comments led me to believe. Still looking. – codah Feb 27 '18 at 23:59
8

I made a few changes to the solution proposed by @PaRiMaL RaJ because the list box was not being displayed when the text box was inside a UserControl that was not tall enough. Basically, instead of adding the list box to the parent of the text box, I added to the form and I calculate the absolute position in the form.

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<String> SelectedValues
        {
            get
            {
                String[] result = Text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                return new List<String>(result);
            }
        }

    }
Francisco Goldenstein
  • 13,299
  • 7
  • 58
  • 74
0

The other solutions didn't work for me in a multiline environment to my needs, so I've added to @Francisco Goldenstein's answer to enable this. What I needed was to autocomplete any 'word' in the TextBox and in any position/line. After minimal testing, this class seems to work well enough for me in a multiline TextBox. Hope it helps someone.

Main changes are in UpdateListBox() and this_KeyDown(), to deal with the 'current' word, i.e. the one just before the caret position, rather than the entire textbox contents.

Change the definition of separators in UpdateListBox() to suit your needs.

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;
        }
    }
}
codah
  • 466
  • 4
  • 14