微軟套裝的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的最終結果。
如果你有更好的解決方案,請指教。