实现自定义LookupComboBox

微软套装的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的最终结果。

如果你有更好的解决方案,请指教。

你可能感兴趣的:(lookup,自定义,combobox)