利用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”。