利用C#制作一個仿IE地址欄的文本框
利用IE上網時,只要在地址欄中輸入幾個字母,與這幾個字母模糊匹配的地址就會自動顯示出來供用戶選擇(如下圖),用戶通過按鍵盤上的上、下箭頭在已有選項中遍歷,找到自己需要的選項后,按回車鍵進行選擇,也可以直接用鼠標進行操作,非常方便,我們在程序中也可以利用這一功能,實現自動提示,方便用戶輸入,下面就以一個實際例子介紹我在工作中是如何實現的。

從上圖中可以看出,最佳的辦法似乎就是繼承ComboBox寫一個控件,在TextChanged事件中,根據內容變化決定是否應拉下提示框,以及該提示什么內容,在實際工作中我們發現,ComboBox非常難以控制,如修改了SelectedIndex屬性后,會自動產生TextChanged事件,造成死循環等等,雖然可以添加其他一些變量來標記該文本改變是用戶引起的還是由程序引起的以進行區別對待處理,但效果始終不理想,經過反復試驗,最后選擇了文本框+列表框的方式,并做成控件庫供其他程序調用,效果很理想,其中,文本框接收用戶輸入,列表框提供選項讓用戶選擇。
新建一個普通的windows應用程序用來測試,不妨取名為Test吧,然后單擊菜單“文件”→“添加項目”→“新建項目(N)”,從彈出來的對話框選擇“windows控件庫”,將該項目和Test項目放在同一個文件夾中,不妨取名為“TextBoxExt”,該項目是本文的重點。
在“解決方案資源管理器” 中,右鍵單擊“TextBoxExt”項目,從彈出的菜單中選擇“屬性”,會彈出屬性配置對話框,在左上角的“配置(C)”中選擇“所有配置”,設置輸出路徑為“..\output”,注意,該輸入有點特殊,兩個小數點+反斜杠+output,意思是當前文件夾上一層的ouput文件夾(從VC過來的朋友可能比較熟悉這種方式),如下圖:

編譯一下,在“我的電腦”或“資源管理器”中我們可以就可以看到與TextBoxExt文件夾同一級自動創建了一個output文件夾,輸出的TextboxExt.dll乖乖地躺在這里:

再來配置一下依賴性,點菜單“項目”→“設置依賴性(D)”,設置項目Test取決于“TextBoxExt”,至此,開發環境配置完畢。
在“解決方案資源管理器” 中雙擊“UserControl1.cs”文件,再切換到代碼窗口中。為便于引用,我們將命名空間“namespace TextBoxExt”改為“namespace Tools”,由于本控件繼承于文本框,因此,將代碼
public class UserControl1 : System.Windows.Forms.UserControl
{
public UserControl1()
{
InitializeComponent();
}
修改為:
public class TextBoxExt : System.Windows.Forms.TextBox
{
private System.ComponentModel.Container components = null;
public TextBoxExt()
{
}
也就是說將命名空間改為Tools,將類名改為TextBoxExt,讓該類繼承于System.Windows.Forms.TextBox,并修改相應的構造函數。編譯成功后,在“解決方案資源管理器中”雙擊項目“Test”的“Form1.cs”來切換到窗體設計,在“工具箱”窗口中切換到“我的用戶控件”選項中,在空白處單擊右鍵,從彈出的菜單中選擇“添加/移出項”,接下來會彈出一個對話框,點“瀏覽”按鈕,找到剛才生成的控件庫,如下圖:

最后點“確定”按鈕,在“工具箱”的“我的控件庫”中就多了一個“TextBoxExt”控件,同操作普通文本框控件一樣,在窗體上添加幾個該自定義控件。可以看出,該文本框和平時使用的文本框目前完全一樣。
切換到UserControl1.cs代碼設計窗口中,添加一個列表框變量,用于顯示提示:
private System.Windows.Forms.ListBox m_lstShowChoice=null;
添加一段代碼,用于響應列表框鼠標的MouseUp事件,也就是用戶通過單擊列表框進行選擇:
private void lstBox_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
ListBox box=(ListBox)sender;
if((box.SelectedIndex>-1) && !this.ReadOnly)
{
this.Text=box.SelectedItem.ToString();
//選擇后文本框失去了焦點,這里移回來
this.Focus();
}
}
添加鼠標在列表框中移動的事件,當用戶在列表框中移動鼠標時,根據鼠標位置,自動設置列表框當前項。
private void lstBox_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
ListBox box=(ListBox)sender;
Point pt = new Point(e.X,e.Y);
int n=box.IndexFromPoint(pt);
if(n>=0)
box.SelectedIndex=n;
}
為了美觀,可以設置列表框的前景、背景、邊框風格等,按常規,我們可以設置一個public變量來供程序調用,但在程序設計時無法通過屬性窗口直接修改,不方便,通過“性質”來引用可以解決,下面的代碼是設置列表框的背景,比較好理解:
#region 設置提示框的背景
private Color m_lstForColor=System.Drawing.SystemColors.InfoText;
/// <summary>
/// 設置/獲取提示的背景色
/// </summary>
public Color PromptBackColor
{
get
{
return m_lstBackColor;
}
set
{
m_lstBackColor=value;
//lstPrompt的創建見下面的代碼
ListBox box=this.lstPrompt;
if(box!=null)
box.BackColor=m_lstBackColor;
}
}
#endregion
設置前景、邊框的方法完全一樣,不再贅述,經過這樣處理,我們在程序設計時就可以直接在“屬性”窗口中指定了,如下圖:
接下來,也是通過“性質”來返回當前列表框,如果沒有則創建:
private System.Windows.Forms.ListBox lstPrompt
{
get
{
//如果沒有列表用于顯示提示的列表框,則創建一個
if((m_lstShowChoice==null) && this.Parent!=null)
{
m_lstShowChoice=new ListBox();
m_lstShowChoice.Visible=false;
m_lstShowChoice.Left=this.Left;
m_lstShowChoice.Top=this.Bottom;
m_lstShowChoice.Width=this.Width;
m_lstShowChoice.TabStop=false;
m_lstShowChoice.Sorted=true;
m_lstShowChoice.ForeColor=this.m_lstForColor; //前景
m_lstShowChoice.BackColor=this.m_lstBackColor; //背景(參見m_lstForColor的創建
m_lstShowChoice.BorderStyle=this.m_lstBordrStyle; //邊框,背景(參見m_lstForColor的創建
//如果提示框過低,則顯示到上面
if(m_lstShowChoice.Bottom>this.Parent.Height)
m_lstShowChoice.Top=this.Top-m_lstShowChoice.Height+8;
m_lstShowChoice.MouseUp += new System.Windows.Forms.MouseEventHandler(this.lstBox_MouseUp);
m_lstShowChoice.MouseMove+= new System.Windows.Forms.MouseEventHandler(this.lstBox_MouseMove);
this.Parent.Controls.Add(m_lstShowChoice);
this.Parent.ResumeLayout(false);
m_lstShowChoice.BringToFront();
}
return m_lstShowChoice;
}
}
創建一個ArrayList用于存放全部可供選擇的項目:
private ArrayList m_ForChoice=new ArrayList();
public ArrayList ChoiceArray
{
get
{
return m_ForChoice;
}
set
{
m_ForChoice=(ArrayList)(value.Clone());
ListBox box=this.lstPrompt;
if(box!=null )
{
box.Items.Clear();
box.Items.AddRange(m_ForChoice.ToArray());
}
}
}
用戶在輸入時,經常喜歡加空格,如兩個字的姓名“張三”,用戶喜歡輸成“張 三”,雖然可以和三個字的姓名對齊,比較美觀,但對程序中姓名檢索等非常不便,因為你還要判斷中間是否有空格,有多少空格等,為此,我們設置一個“性質”來決定是否允許用戶輸入空格。
private bool m_AllowSpace=false;
public bool AllowSpace
{
get
{
return m_AllowSpace;
}
set
{
m_AllowSpace=value;
}
}
在輸入過程中,有些內容是只能選擇不能輸入的,就象我們將ComboBox的DropDownStyle設置為DropDownList一樣,但為了方便用戶,我們允許用戶輸入,光標離開本文本框時,進行檢查,判斷用戶輸入的內容是否在可選項內,如果不在,則清空用戶輸入,因此,添加下面變量:
private bool m_bChoiceOnly=false;
public bool ChoicOnly
{
get
{
return this.m_bChoiceOnly;
}
set
{
this.m_bChoiceOnly=value;
}
}
按常理,我們應該在響應文本框的TextChange事件中根據文本變化來決定是否顯示列表框,以及列表框中該出現哪些可選項,但同ComboBox一樣,文本改變事件可能有多種事件連帶引發(如程序使用了TextBox1.Text=”abc”之類),難以控制,我們這里響應KeyUp事件,在通過KeyUp和KeyDown的配合來完成,就避開了上述問題。本文開始處我們說過,可以通過鍵盤上的“↑”和“↓”在可選項中遍歷,而文本框默認上下箭頭也用來移動當前鍵盤光標,因此,我們在KeyDown中記下當前光標位置,在KeyUp中恢復,故聲明一個變量m_nOldPos來記錄按鍵前鍵盤光標位置,在實際工作中我們發現,部分輸入法存在BUG,用戶鍵入一個鍵后,產生了一個KeyDown后產生兩個KeyUp事件,為此,通過一個變量bKeyDown來記錄鍵是否按下,供KeyUp正確判斷,文本改變發生在KeyDown和KeyUp之間,因此,在KeyDown中必須記錄當前文本,在KeyUp中判斷文本是否改變,根據上述分析,我們添加文本框的KeyDown事件響應代碼:
private int m_nOldPos=0;
private bool bKeyDown=false;
private string m_strOldText="";
private void TextBoxExt_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
m_nOldPos=this.SelectionStart;
bKeyDown=true;
m_strOldText=this.Text;
}
創建一個函數,用來根據文本框當前內容,從可供選擇的m_ForChoice數組中篩選出模糊匹配的選項,添加到列表框中:
private void FillPrompt(string p_strText)
{
ListBox box=this.lstPrompt;
if(box!=null)
{
box.Items.Clear();
if(p_strText.Length==0)//沒有內容,顯示全部
box.Items.AddRange(this.m_ForChoice.ToArray());
else
{
foreach(string s in this.m_ForChoice)
{
if(s.ToLower().IndexOf(p_strText.ToLower())>=0)
box.Items.Add(s);
}
}
}
}
添加文本框的KeyUp事件響應代碼:
private void TextBoxExt_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e)
{
if(!bKeyDown)//忽略掉多余的KeyUp事件
return;
bKeyDown=false;
ListBox box=this.lstPrompt;
switch(e.KeyCode)
{
//通過上下箭頭在待選框中移動
case System.Windows.Forms.Keys.Up:
case System.Windows.Forms.Keys.Down:
if((box!=null) && !this.Multiline)//多行文本通過上下箭頭在兩行之間移動
{
if((e.KeyCode==System.Windows.Forms.Keys.Up) && (box.SelectedIndex>-1))//↑
box.SelectedIndex--;
else if((e.KeyCode==System.Windows.Forms.Keys.Down) && (box.SelectedIndex<box.Items.Count-1))//↑
box.SelectedIndex++;
//上下箭頭不能移動當前光標,因此,還原原來位置
this.SelectionStart=m_nOldPos;
//顯示提示框
if(!box.Visible)
{
if(box.Width!=this.Width)
box.Width=this.Width;
box.Visible=true;
}
}
break;
case System.Windows.Forms.Keys.Escape://ESC隱藏提示
if((box!=null) && box.Visible)
box.Hide();
break;
case System.Windows.Forms.Keys.Return://回車選擇一個或跳到下一控件
if((box==null) || this.Multiline)
break;
//沒有顯示提示框時,移動到下一控件
if( !box.Visible)
{
SendKeys.Send("{TAB}");
}
else //有提示,關閉提示
{
if(box.SelectedIndex>-1)//有選擇,使用當前選擇的內容
this.Text=box.SelectedItem.ToString();
this.SelectionStart=this.Text.Length;
this.SelectAll();
box.Hide();
}
break;
default://判斷文本是否改變
string strText=this.Text;
//不允許產生空格,去掉文本中的空格
if(!m_AllowSpace)
strText=this.Text.Replace(" ","");
int nStart=this.SelectionStart;
if(strText!=m_strOldText)//文本有改變
{
//設置當前文本和鍵盤光標位置
this.Text=strText;
if(nStart>this.Text.Length)
nStart=this.Text.Length;
this.SelectionStart=nStart;
//修改可供選擇的內容,并顯示供選擇的列表框
if(box!=null)
{
this.FillPrompt(strText);
if(!box.Visible)
{
if(box.Width!=this.Width)
box.Width=this.Width;
box.Visible=true;
}
}
}
break;
}
}
當文本框失去鍵盤光標后,必須隱藏提示,對于只選型文本框,還要判斷用戶輸入是否在可選項中:
private void TextBoxExt_Leave(object sender, System.EventArgs e)
{
//對于只選字段,必須輸入同待選相匹配的值
if(this.m_bChoiceOnly)
{
int nIndex=this.ChoiceArray.IndexOf(this.Text);
if(nIndex<0)
this.Text="";
}
//失去焦點后,必須隱藏提示
ListBox box=this.lstPrompt;
if(box!=null)
box.Visible=false;
}
在IE地址欄中,右邊有一個“↓”,我們可以點該箭頭來打開提示框,本例中沒有增加該功能不能不說是一個缺陷,這里,我們用雙擊文本框的辦法來代替(也可以通過在右邊加一個按鈕模擬下拉箭頭,制作方法見筆者另一篇文章《在C#中制作組合控件》):
private void TextBoxExt_DoubleClick(object sender, System.EventArgs e)
{
if(this.ReadOnly)
return;
ListBox box=this.lstPrompt;
if((box!=null) && (!box.Visible))
{
if(box.Width!=this.Width)
box.Width=this.Width;
box.Visible=true;
}
}
切換到Test項目Form1的代碼窗口中,在前面添加一個引用:using Tools;并添加Form1的Load事件響應代碼:
private void Form1_Load(object sender, System.EventArgs e)
{
//創建一個數組,用于填充本控件的可選項
ArrayList arr=new ArrayList();
arr.Add("aaabbbcccddd");
arr.Add("bbbcccdddeee");
arr.Add("cccdddeeefff");
arr.Add("dddeeefffggg");
//遍歷所有控件,篩選出增強型文本框控件進行可選項賦值
foreach(Control ctl in this.Controls)
{
TextBoxExt txtbox=ctl as TextBoxExt;
if(txtbox==null)
continue;
txtbox.ChoiceArray=arr;
}
}
運行一下,當在文本框中輸入一些字母后,文本框會自動匹配,去掉不符合要求的文本,同時,通過按上下箭頭、回車鍵、ESC鍵、雙擊等可以檢驗效果,運行結果如下圖所示:

至此,類似IE地址欄的控件就開發完成了,在工作中可以擴充該控件,如在控件中增加一個“代表字段”的性質,程序設計時在屬性窗口中指定其所代表的字段(如“姓名”),在上面Form1的Load事件的“foreach(Control ctl in this.Controls)”中,根據該字段自動從數據庫中搜索所有姓名,并填充到文本框的可選項中供用戶選擇,這一樣一來,就沒有必要在代碼中人為指定某文本框同什么字段綁定,大大減化了工作,連文本框的name屬性都可不指定了,且通用性強,以上只是一個思路和骨干代碼,實際使用的控件要強大的多,你可以根據自己工作情況不斷完善和補充,本例所有代碼請到我的ftp://202.107.251.26的“茍安廷”文件夾中下載,文件名為“文本框擴展.rar”。