在百度和Google上搜索了一下,竟然没发现多少有用的资料。不过我觉得也没必要做到像IDE的智能提示那样的完美,因此按自己的想法做估计也不会太复杂。
首先建立一个TipsListBox类,其作用是显示提示的信息,此类继承了ListBox,以便我们可以自己控制。将DrawMode属性设为OwnerDrawFixed。添加一个类型为string的属性Prefix。此属性的作用以后会提到。最后我们重写DrawItem事件。代码如下:
private void TipsListBox_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0)
return;
//是否选中了该项
bool selected = (e.State & DrawItemState.Selected) == DrawItemState.Selected ? true : false;
e.DrawBackground();
System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
if (selected)
{
Image img = Image.FromStream(asm.GetManifestResourceStream("MM.App.Resources.focus.gif"));
Brush b = new TextureBrush(img);
//参数中,e.Bounds 表示当前选项在整个listbox中的区域
e.Graphics.FillRectangle(b, e.Bounds);
}
else
{
//在背景上画空白
e.Graphics.FillRectangle(Brushes.White, e.Bounds);
Image img = Image.FromStream(asm.GetManifestResourceStream("MM.App.Resources.line.bmp"));
Brush b = new TextureBrush(img);
//底下线的图片,参数中,23和是根据图片来的,因为需要在最下面显示线条
e.Graphics.FillRectangle(b, e.Bounds.X, e.Bounds.Y + 23, e.Bounds.Width, 1);
}
//最后把要显示的文字画在背景图片上
e.Graphics.DrawString(this.Items[e.Index].ToString(), this.Font, Brushes.Black, e.Bounds.X + 15, e.Bounds.Y + 6, StringFormat.GenericDefault);
//再画一下边框
ControlPaint.DrawBorder(e.Graphics,this.ClientRectangle,
Color.Beige, 2, ButtonBorderStyle.Solid,
Color.Beige, 2, ButtonBorderStyle.Solid,
Color.Beige, 2, ButtonBorderStyle.Solid,
Color.Beige, 2, ButtonBorderStyle.Solid);
}
发送短信的界面是这样的:
在发送内容的输入框里输入要发送的短信,系统应该能提取用户最后输入的字串,然后将此字串放到预定义的常用短语库里匹配,将匹配到的短语列表显示在一个ListBox中。我这里暂时采取的规则比较简单,只提取以空格切分的最后一串的字符,然后匹配常用短语库中以这字串开头的短语。以后再根据客户需要进行扩展修改。
首先重写短信内容的文本框(RichTextBox)的事件:
private void txtMessageContent_TextChanged(object sender, EventArgs e)
{
//提示框的name
const string controlKey = "lstTips";
RichTextBox tb = ((RichTextBox)sender);
//以空格切分
string[] array = tb.Text.Split(" ".ToCharArray());
if (array != null && array.Length > 0)
{
TipsListBox lstTips = null;
if(tb.Controls.ContainsKey(controlKey))
{
lstTips = (TipsListBox)tb.Controls[controlKey];
}
else
{
lstTips = new TipsListBox();
lstTips.Name = "lstTips";
//我们要重写这两个事件
lstTips.KeyDown += new KeyEventHandler(lstTips_KeyDown);
lstTips.Click += new EventHandler(lstTips_Click);
}
//这个前缀就是放到常用短语库中去匹配的
string prefix = array[array.Length - 1];
if (string.IsNullOrEmpty(prefix))
{
lstTips.Hide();
return;
}
//从常用短语库中查找
List<GeneralPhraseInfo> list = GeneralPhrasePool.Search(prefix);
if (list == null)
return;
//将此前缀保存起来
lstTips.Prefix = prefix;
lstTips.Items.Clear();
foreach (GeneralPhraseInfo p in list)
{
lstTips.Items.Add(p.Phrase);
}
lstTips.Show();
lstTips.Width = 200;
lstTips.TabIndex = 100;
//让提示框跟随光标
lstTips.Location = tb.GetPositionFromCharIndex(tb.SelectionStart);
lstTips.Left += 10;
lstTips.SelectedIndex = 0;
if (!tb.Controls.ContainsKey(controlKey))
tb.Controls.Add(lstTips);
}
}
用户在短信输入文本框里按中了键盘的下方向键的话,就将焦点移到ListBox提示框里。
private void txtMessageContent_KeyDown(object sender, KeyEventArgs e)
{
RichTextBox tb = ((RichTextBox)sender);
const string controlKey = "lstTips";
if (e.KeyCode == Keys.Down && tb.Controls.ContainsKey(controlKey))
{
TipsListBox lstTips = (TipsListBox)tb.Controls[controlKey];
if(lstTips.Visible)
{
lstTips.Focus();
}
}
}
然后重写ListBox的两个事件,比较简单,直接上代码:
void lstTips_Click(object sender, EventArgs e)
{
TipsListBox lstTips = (TipsListBox)sender;
if(lstTips.SelectedIndex > -1)
{
string tips = lstTips.SelectedItem.ToString();
txtMessageContent.AppendText(tips = tips.Substring(lstTips.Prefix.Length, tips.Length - lstTips.Prefix.Length));
lstTips.Hide();
txtMessageContent.Focus();
}
}
void lstTips_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
//如果敲的是回车,就选定短语
TipsListBox lstTips = (TipsListBox)sender;
if (lstTips.SelectedIndex > -1)
{
string tips = lstTips.SelectedItem.ToString();
txtMessageContent.AppendText(tips = tips.Substring(lstTips.Prefix.Length, tips.Length - lstTips.Prefix.Length));
lstTips.Hide();
txtMessageContent.Focus();
}
return;
}
//只允许在ListBox上操作上键和下键,其它键都使焦点返回到短信输入框
if (e.KeyCode != Keys.Down && e.KeyCode != Keys.Up)
txtMessageContent.Focus();
}
到了这里,大家该差不多明白其中的流程了。不过可能对这一句有点疑惑:List<GeneralPhraseInfo> list = GeneralPhrasePool.Search(prefix);
为了提高性能,我预先将常用短语提取出来排好序,然后放到内存中。排序非常简单,用一条sql就可以搞定:Select * from dbo.GeneralPhrase order by Phrase
GeneralPhrase表里只有两个字段,一个是自增型主键,另一个就是Phrase类型为varchar。
既然已经排好序了,那当然用二分查找法。
public static List<GeneralPhraseInfo> Search(string prefix)
{
if (list == null || list.Count == 0)
return null;
int start = 0;
int end = list.Count - 1;
int middle = (start + end) / 2;
int first = 0;
int last = 0;
while (start <= end)
{
if (list[middle].Phrase.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
//好,找到了一个
first = middle;
if(middle > start)
{
--middle;
//只要这个之前的也符合
while (list[middle].Phrase.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
first = middle;
if (middle > -1)
--middle;
else
break;
}
}
//重置索引
middle = (start + end) / 2;
last = middle;
if(middle < end)
{
++middle;
//只要这个之后的也符合
while (list[middle].Phrase.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
last = middle;
if (middle < list.Count)
++middle;
else
break;
}
}
return list.GetRange(first, last - first + 1);
}
else if (list[middle].Phrase.ToLower().CompareTo(prefix) < 0 )
{
start = middle + 1;
}
else
{
end = middle - 1;
}
middle = (start + end) / 2;
}
//找不到
return null;
}