微软套装的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的最终结果。
如果你有更好的解决方案,请指教。