微軟套裝的ComboBox本身就提供了AutoCompete功能,只要設定AutoCompleteMode和AutoCompleteSource屬性即可,而且功能還是很強大的。但是…還是滿足不了我的要求。

1. AutoComplete時下拉框只顯示近似匹配的項,不是顯示全部項並自動定位到匹配項。

2. 當輸入的內容找不到匹配項時,還允許使用者錄入,並且SelectedIndex將為-1, 同時SelectedValue為null。

但是在多數情況下,我們是不允許使用者輸入選項以外的內容,輸入只是進行lookup。而且SelectedIndex變為-1也不是所期盼的。因此,擴充套件ComboBox做了一個自定義控制元件。

1. 當獲取焦點時全部文字預設是選中的,這時使用者錄入的內容就會代替全部內容去lookup。

2. 如果找到匹配項,就定位到匹配項,並且將文字的選中區域從使用者錄入內容向後選中;沒有匹配項則錄入無效。

3. 當使用者退格或Del時,如果沒有找到匹配項則只是將文字的選中區域擴大。

4. 當使用者刪除了內容時,如果沒有找到匹配項則刪除無效。如果使用者要把內容全部刪掉,就會跟據LimitedToList 屬性來決定是否清空文字,當然也就是是否將SelectedIndex設定為-1。

5. 不允許Ctrl+X,最主要的原因是我在OnKeyPress事件中停止事件繼續冒泡,這樣剪下的內容並沒有複製到剪下板。

6. 不允許ESC,原因是當下拉選單開啟時ESC會將SelectedIndex恢復,但是使用者錄入匹配的文字不會恢復。

public class LookupComboBox : System.Windows.Forms.ComboBox
{
    #region Variables
    /// <summary>
    /// A value indicating whether open drop down list when click comboBox.
    /// </summary>
    private bool dropOnEntry = true;
    /// <summary>
    /// A value indicating whether auto size drop down list.
    /// </summary>
    private bool allowAutoSize = true;
    /// <summary>
    /// A value indicating whether this control can be modified.
    /// </summary>
    private bool readOnly;
    /// <summary>
    /// Hold the value of BackColor property, used for restore BackColor property when change ReadOnly to false.
    /// </summary>
    private System.Drawing.Color tBackColor;
    /// <summary>
    /// Hold the value of ForeColor property, used for restore ForeColor property when change ReadOnly to false.
    /// </summary>
    private System.Drawing.Color tForeColor;
    /// <summary>
    /// Hold the value of DropDownStyle property, used for restore DropDownStyle property when change ReadOnly to false.
    /// </summary>
    private ComboBoxStyle dropDownStyle = ComboBoxStyle.DropDown;
    /// <summary>
    /// The back color value for ReadOnly
    /// </summary>
    private System.Drawing.Color readOnlyBackColor = SystemColors.Control;
    /// <summary>
    /// The fore color value for ReadOnly
    /// </summary>
    private System.Drawing.Color readOnlyForeColor = SystemColors.WindowText;
    /// <summary>
    /// The value indicated whether user pressed DEL.
    /// </summary>
    private bool _pressedDel = false;
    /// <summary>
    /// A value indicate whether Selected an item in the list.
    /// </summary>
    private bool _limitedToList = true;
    #endregion
    #region Public Properties
    /// <summary>
    /// Gets a value indicating whether open drop down list when click comboBox.
    /// </summary>
    public bool DropOnEntry
    {
        set
        {
            dropOnEntry = value;
        }
        get
        {
            return dropOnEntry;
        }
    }
    /// <summary>
    /// Gets a value indicating whether auto size drop down list.
    /// </summary>
    public bool AllowAutoSize
    {
        set
        {
            allowAutoSize = value;
        }
        get
        {
            return allowAutoSize;
        }
    }
    /// <summary>
    /// Gets or sets the back color value for ReadOnly
    /// </summary>
    public System.Drawing.Color ReadOnlyBackColor
    {
        get
        {
            return readOnlyBackColor;
        }
        set
        {
            readOnlyBackColor = value;
        }
    }
    /// <summary>
    /// Gets or sets the fore color value for ReadOnly
    /// </summary>
    public System.Drawing.Color ReadOnlyForeColor
    {
        get
        {
            return readOnlyForeColor;
        }
        set
        {
            readOnlyForeColor = value;
        }
    }
    /// <summary>
    /// Gets a value indicating whether this control can be modified.
    /// </summary>
    public bool ReadOnly
    {
        get
        {
            return readOnly;
        }
        set
        {
            if (readOnly != value)
            {
                readOnly = value;
                if (value)
                {
                    base.DropDownStyle = ComboBoxStyle.Simple;
                    base.Height = 21;
                    this.tBackColor = this.BackColor;
                    this.tForeColor = this.ForeColor;
                    base.BackColor = this.ReadOnlyBackColor;
                    base.ForeColor = this.ReadOnlyForeColor;
                }
                else
                {
                    this.ContextMenu = null;
                    base.DropDownStyle = dropDownStyle;
                    base.BackColor = tBackColor;
                    base.ForeColor = tForeColor;
                }
            }
        }
    }
    /// <summary>
    /// Gets a value indicating whether Selected an item in the list.
    /// If LimitedToList is true. SelectedIndex will be 0 when user deleted all text and leave the focus.
    /// If LimitedToList is false. SelectedIndex will be -1 when user delete all text and leave the focus.
    /// Default value is true.
    /// </summary>
    [Description("If LimitedToList is true SelectedIndex can`t be -1 except no items in list. If can`t match any item in list when OnValidaing, SelectedIndex will be 0. Otherwise can be -1. Default value is true")]
    [DefaultValue(true)]
    public bool LimitedToList
    {
        get
        {
            return this._limitedToList;
        }
        set
        {
            this._limitedToList = value;
        }
    }
    #endregion
    #region Event Handlers
    /// <summary>
    ///  Raises the System.Windows.Forms.Control.MouseDown event.
    /// </summary>
    /// <param name="e">A System.Windows.Forms.MouseEventArgs that contains the event data.</param>
    protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)
    {
        if (this.ReadOnly)
        {
            return;
        }
        if ((dropOnEntry) && (this.DroppedDown != true))
            this.DroppedDown = true;
        base.OnMouseDown(e);
    }
    /// <summary>
    /// Raises the System.Windows.Forms.ComboBox.DropDown event.
    /// </summary>
    /// <param name="e">An System.EventArgs that contains the event data.</param>
    protected override void OnDropDown(System.EventArgs e)
    {
        base.OnDropDown(e);
        if (!this.allowAutoSize)
            return;
        int width = this.DropDownWidth;
        Graphics g = this.CreateGraphics();
        Font font = this.Font;
        int vertScrollBarWidth = (this.Items.Count > this.MaxDropDownItems) ? SystemInformation.VerticalScrollBarWidth : 0;
        int newWidth;
        foreach (object o in this.Items)
        {
            string s = o.ToString();
            newWidth = (int)g.MeasureString(s, font).Width + vertScrollBarWidth;
            if (width < newWidth)
            {
                width = newWidth;
            }
        }
        this.DropDownWidth = width;
    }
    /// <summary>
    /// Raises the System.Windows.Forms.Control.KeyPress event.
    /// </summary>
    /// <param name="e">A System.Windows.Forms.KeyPressEventArgs that contains the event data.</param>
    protected override void OnKeyPress(KeyPressEventArgs e)
    {
        base.OnKeyPress(e);
        string findString = string.Empty;
        if (e.KeyChar == (char)Keys.Back && this._pressedDel) // DEL
        {
            // Special treat for decreasing Change event trigger times.
            if (this.SelectionStart == 0 && this.SelectionLength > 0 && this.SelectionLength == this.Text.Length - 1)
            {
                this.SelectAll();
                e.Handled = true;
                return;
            }
            if (this.SelectionStart < this.Text.Length)
            {
                this.SelectionLength += 1;
            }
            // delete the selected text
            findString = this.ReplaceSelectedString(string.Empty);
        }
        else if (e.KeyChar == (char)Keys.Back && !this._pressedDel) //Back
        {
            // Special treat for decreasing Change event trigger times.
            if (this.SelectionStart == 1 && this.SelectionLength > 0 && this.SelectionLength == this.Text.Length - 1)
            {
                this.SelectAll();
                e.Handled = true;
                return;
            }
            if (this.SelectionStart > 0)
            {
                this.SelectionStart -= 1;
                this.SelectionLength += 1;
            }
            // delete the selected text
            findString = this.ReplaceSelectedString(string.Empty);
        }
        else if (e.KeyChar == (char)22) //CTRL+V
        {
            IDataObject data = Clipboard.GetDataObject();
            string copiedText = data.GetData(DataFormats.Text) as String;
            findString = this.ReplaceSelectedString(copiedText);
        }
        else if (char.IsControl(e.KeyChar))
        {
            return;
        }
        else
        {
            // if user input a normal character
            findString = this.ReplaceSelectedString(e.KeyChar.ToString());
        }
        // if user delete all text set indext to -1
        if (string.IsNullOrEmpty(findString))
        {
            if (this.LimitedToList && this.Items != null && this.Items.Count > 0)
            {
                this.SelectedIndex = 0;
                this.SelectAll();
            }
            else
            {
                this.SelectedIndex = -1;
            }
            e.Handled = true;
            return;
        }
        int matchedIndex = this.FindStringExact(findString);
        if (matchedIndex == -1)
        {
            matchedIndex = this.FindString(findString);
        }
        if (matchedIndex == -1)
        {
            e.Handled = true;
            return;
        }
        this.SelectedIndex = matchedIndex;
        this.SelectionStart = findString.Length;
        this.SelectionLength = this.Text.Length - this.SelectionStart;
        e.Handled = true;
    }
    /// <summary>
    /// Raises the System.Windows.Forms.Control.KeyDown event.
    /// </summary>
    /// <param name="e">A System.Windows.Forms.KeyEventArgs that contains the event data.</param>
    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);
        if (e.KeyCode == Keys.Delete)
        {
            this._pressedDel = true;
            this.OnKeyPress(new KeyPressEventArgs((char)Keys.Back));
            this._pressedDel = false;
            e.Handled = true;
        }
    }
    /// <summary>
    /// Override the event that the character was processed by the control
    /// </summary>
    /// <param name="msg">A System.Windows.Forms.Message, passed by reference, that represents the window message to process.</param>
    /// <param name="keyData">One of the System.Windows.Forms.Keys values that represents the key to process.</param>
    /// <returns>True if the character was processed by the control; otherwise, false.</returns>
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        // blocked Esc
        if (keyData == Keys.Escape || keyData == (Keys.Control | Keys.X))
        {
            return true;
        }
        return base.ProcessCmdKey(ref msg, keyData);
    }
    /// <summary>
    /// Raises the System.Windows.Forms.Control.Validating event.
    /// </summary>
    /// <param name="e">A System.ComponentModel.CancelEventArgs that contains the event data.</param>
    protected override void OnValidating(System.ComponentModel.CancelEventArgs e)
    {
        int exactMatchedIndex;
        // For some reason, the index is reset when the drop down closes
        if (this.DroppedDown)
        {
            this.DroppedDown = false;
        }
        exactMatchedIndex = this.FindStringExact(this.Text);
        // if not matching anything and limited to list set selectedIndex to 0
        if (exactMatchedIndex == -1 && this.LimitedToList && this.Items != null && this.Items.Count > 0)
        {
            exactMatchedIndex = 0;
        }
        this.SelectedIndex = exactMatchedIndex;
        base.OnValidating(e);
    }
    #endregion
    #region Methods
    /// <summary>
    /// Replace selected string with specific text
    /// </summary>
    /// <param name="replaceText">The text want to replace with</param>
    /// <returns>Replace result string</returns>
    private string ReplaceSelectedString(string replaceText)
    {
        return String.Format("{0}{1}{2}", this.Text.Substring(0, this.SelectionStart), replaceText, this.Text.Substring(this.SelectionStart + this.SelectionLength));
    }
    #endregion
}

可能你注意到OnValidating方法。與OnLeave方法不同OnValidating方法不僅在失去焦點時會被呼叫,在使用者使用快捷鍵觸發按鈕事件時也會被呼叫。後者對我來說更重要,因為使用者都通過按鈕的快捷鍵來完成儲存操作,而快捷鍵並不會使ComboBox命運焦點,即不會呼叫OnLeave方法。

做這個控制元件還發生了一個波折,最初我是在OnTextChanged方法中完成匹配選中的,並且沒有新增OnValidating方法,在我的本機(Windows 7)測試通過(當然除了使用者按快捷鍵的情況)。但是使用者使用的環境是XP和Windows 2003,Lookup匹配完成後ComboBox的SelectedIndex競然是-1。

當然如果加上了OnValidatin方法也可以解決,但是使用overrid OnTextChanged的解決方案會引發多次TextChanged事件,最終還是改用了OnKeyPress方法。

順便說一下,如果要在LookupComboBox的SelectedIndexChanged事件中想彈出一個確認對話方塊,讓使用者選是否或取消的那種,在彈框前要呼叫ComboBox.DroppedDown = false,這樣ComboBox會接受lookup的最終結果。

如果你有更好的解決方案,請指教。