「控件控」自动完成菜单、代码提示、函数列表、文本纠错等IntelliSense功能

Conmajia 2012-06-04 02:02:13
加精
题目很响亮吧。

我明后天出门郊游,闪人前从cp弄点好文过来,大家一起赏析。

个人认为这篇文章用好了能让你的程序直接跨入「专业」水准。至少「看起来」,但这就够了。

来点背景介绍,本文是乌克兰人Pavel Torgashov发表在codeproject上的文章,获得了今年"Best C# Article of April"的prize winner。

我已经征得Pavel本人同意,把这篇翻译出来,大家一起研究。

© Written by Pavel Torgashov 2012, translated by 野比「Conmajia」 2012

自动完成菜单
[乌克兰]Pavel Torgashov著,野比译
自定义用于RichTextBox、TextBox和其他控件的自动完成菜单。

点击阅读原文
下载源代码 - 192.4KB
下载DEMO - 22.9KB



简介
我们所有人都用过VisualStudio的自动完成菜单,也就是IntelliSense。它非常管用,不是吗?不幸的是,.NET框架并没有包含内置的自动完成菜单组件。本文制作的组件将填补这个空缺。
AutocompleteMenu允许你轻松地在你的窗体上任何 TextBox或是RichTextBox里加入下拉提示框功能。(就像上面图中演示的那样——野比注)

实现
该组件包含了数个类。下面是主要的类极其功能小结:
AutocompleteMenu - 包含基本功能的主要组件。它订阅TextBox的事件,查找合适的变体,显示一个下拉菜单,并将新的文字插入文本框。
下面是AutocompleteMenu的基本属性:
* AllowTabKey - 允许使用TAB键选择菜单项
* AppearInterval - 菜单显示的间隔(毫秒)
* ImageList - 保存菜单项用到的图片
* Items - 菜单项列表(AutocompleteMenu最简单的用法)
* MaximumSize - 弹出菜单最大尺寸
* MinFragmentLength - 菜单显示的最小片段长度。只有当光标处当前片段长度不低于MinFragmentLength才会显示AutocompleteMenu。
* SearchPattern - 搜索光标处片段的正则表达式
AutocompleteMenuHost - 从ToolStripDropDown派生的可视化组件。该控件能让你在不丢失窗体焦点的情况下显示菜单。
AutocompleteListView - 从UserControl继承的可视化组件。使用GDI+绘制下拉菜单的菜单项。该控件和ListView很像,但能够高效地显示大量的元素。
AutocompleteItem - 菜单项。这个类包含了菜单项的所有必要信息。你可以从AutocompleteItem继承出你的元素,并覆盖其虚方法,这样来扩展菜单功能。下面是AutocompleteItem的基本属性:
* Text - 要插入文本框的文本
* MenuText - 显示在弹出菜单上的文本
* ImageIndex - 菜单项的图片索引
* ToolTipTitle - 工具提示标题。如果ToolTipTitle为null,则不会显示工具提示
* ToolTipText - 工具提示文本
* Tag - 你可在此附加任何数据
下面是一些你可以重写的方法:
* GetTextForReplace - 返回要插入的文本。你可以动态修改要插入的文本。例如,你可以插入当前日期。
* Compare - 这个方法定义了菜单项显示与否。默认情况下,只有菜单项以给定的片段开头,才会显示该项。但是你可以重写这个方法的行为。比如,你可以用子字符串来比较,或是进行一些模糊比较。
* OnSelected - 这个方法会在文本插入文本框的时候调用。你可以在这里对文本进行一些额外的操作。比如,你可以把光标移动到某处。
控件库里还提供了几个从AutocompleteMenu派生的有用的类:SnippetAutocompleteItem(可以用于插入代码段),MethodAutocompleteItem(可以在点后面插入方法名称),SubstringAutocompleteItem(用子字符串来比较文本),MulticolumnAutocompleteItem(绘制多列菜单)。
使用源代码
简单用法:
1) 把AutocompleteMenu组件扔到你的窗体上
2) 在AutocompleteMenu.Items里输入菜单项
就像这样

3) 设置你的文本框的AutocompleteMenu属性
就像这样

4) 搞定收工


高级用法:
1) 把AutocompleteMenu组件扔到你的窗体上
2) 创建一个菜单项列表,用SetAutocompleteItems()或是AddItem()方法添加到菜单。比如:
string[] snippets = { "if(^)\n{\n}", "if(^)\n{\n}\nelse\n{\n}", "for(^;;)\n{\n}", "while(^)\n{\n}", "do${\n^}while();", "switch(^)\n{\n\tcase : break;\n}" };  

private void BuildAutocompleteMenu()
{
var items = new List<AutocompleteItem>();

foreach (var item in snippets)
items.Add(new SnippetAutocompleteItem(item) { ImageIndex = 1 });

//设置为自动完成源
autocompleteMenu1.SetAutocompleteItems(items);
}

同样,你也可以添加自己的菜单项,就是从AutocompleteItem继承而来的那种。比如:
internal class EmailSnippet : AutocompleteItem  
{
public EmailSnippet(string email): base(email)
{
ImageIndex = 0;
ToolTipTitle = "Insert email:";
ToolTipText = email;
}

public override CompareResult Compare(string fragmentText)
{
if (fragmentText == Text)
return CompareResult.VisibleAndSelected;
if (fragmentText.Contains("@"))
return CompareResult.Visible;
return CompareResult.Hidden;
}
}

更多详细内容请参考Demo中的AdvancedSample例程。

快捷键:
你可以使用以下的快捷键:
* Ctrl+Space - 强制打开AutocompleteMenu
* 上、下、上翻页、下翻页 - 在菜单中来回移动
* 回车、Tab、鼠标双击 - 插入选中的文本(Tab键只在AllowTabKey为true时才起作用)
* Esc - 关闭菜单
注意,尽管窗体焦点位于文本框,这些按键仍然哼正常工作。
当你点选了菜单项,就会显示相应的工具提示。

自定义ListView
你可以用自定义控件来显示AutocompleteMenu(如ListView、ListBox、DataGridView、TreeView等等)。首先创建自己的控件(从Control类派生),然后实现IAutocompleteListView接口。更多详情请参考CustomListViewSample。

动态上下文菜单
如果你要显示的菜单并非固定内容,而是根据文本而动态改变,那么你会经常用到这个部分。
请注意菜单的SetAutocompleteItems()方法采用了IEnumerable接口作为要显示的菜单项集合参数。
所以,你不必在程序一开始就生成菜单项列表。你只需要在调用菜单项的时候再动态生成就可以了。
下面的代码演示了这个思路:
autocompleteMenu1.SetAutocompleteItems(new DynamicCollection(tb));  
....

internal class DynamicCollection : IEnumerable<AutocompleteItem>
{
public IEnumerator<AutocompleteItem> GetEnumerator()
{
return BuildList().GetEnumerator();
}

private IEnumerable<AutocompleteItem> BuildList()
{
//找到文本中所有单词
var words = new Dictionary<string, string>();
foreach (Match m in Regex.Matches(tb.Text, @"\b\w+\b"))
words[m.Value] = m.Value;

//返回自动完成项
foreach(var word in words.Keys)
yield return new AutocompleteItem(word);
}
}

完整的实现代码请参考DynamicMenuSample。

兼容性
自动完成菜单可以兼容TextBox、RichTextBox、MaskedTextBox、FastColoredTextBox(一个非常强大的支持代码着色的文本框控件。近期将对其进行翻译。——野比注)和其他派生自TextBoxBase的控件。
同样,自动完成菜单也兼容任何支持以下属性和方法的控件:
* string SelectedText{get;set;}
* int SelectionLength{get;set;}
* int SelectionStart{get;set;}
* Point GetPositionFromCharIndex(int charPos)
即使你的控件不支持这些方法(或属性),你也可以为它创建自己的包装器。要这样做,你必须创建自己的包装类,并实现ITextBoxWrapper接口。
下面是ITextBoxWrapper的方法和属性:
public interface ITextBoxWrapper  
{
Control TargetControl { get; }
string Text { get; }
string SelectedText { get; set; }
int SelectionLength { get; set; }
int SelectionStart { get; set; }
Point GetPositionFromCharIndex(int pos);
event EventHandler LostFocus;
event ScrollEventHandler Scroll;
event KeyEventHandler KeyDown;
event MouseEventHandler MouseDown;
}


做好了包装器之后,你就可以简单地把AutocompleteMenu附加到你的控件上去了。就像这样:

public partial class Form1 : Form  
{
public Form1()
{
InitializeComponent();

//把myControl1附加到autocompleteMenu1
autocompleteMenu1.TargetControlWrapper = new MyControlWrapper(myControl1);
}
}

internal class MyControlWrapper : ITextBoxWrapper
{
private MyControl tb;

public MyControlWrapper(MyControl tb)
{
this.tb = tb;
}

//在这里实现ITextBoxWrapper
//(略)
}


示例
Demo中包含了几个示例:
SimplestSample - 展示最简单的使用控件方法
CustomItemSample - 展示了怎样创建从AutocompleteItem派生的类
AdvancedSample - 展示了怎样创建自定义的带关键字、代码段、方法提示、文本纠错等的自动完成菜单
ExtraLargeSample - 演示了在极大量(100万)菜单项情况下组件的性能
ComboboxSample - 展示了怎样创建模拟Combobox,带特别大的下拉列表和子字符串搜索功能
MulticolumnSample - 展示了怎样制作多列自动完成菜单。就像这样:


CustomListViewSample - 展示了怎样在自动完成菜单中制作自定义ListView。就像这样:


DynamicMenuSample - 这个例子展示了怎样创建动态的上下文敏感的自动完成菜单
DataGridViewSample - 展示了怎样把AutocompleteMenu附加到DataGridView。就像这样:


历史
2012年4月13日 - 首发。
2012年4月21日 - 重构了控件。增加了对FastColoredTextBox和其他控件的支持。
2012年5月9日 - 重构了控件。增加了一些例子。

许可
本文及相关源代码和文件,均采用GNU通用公共许可证(LGPLv3)授权。
(本译文也如此——野比注)

关于作者
Pavel Torgashov(乌克兰)
我是Pavel Torgashov。我住在乌克兰基辅市。
我从1998年就开始开发软件了。
我的联系email是:tp_soft[at]mail.ru

(全文完)

© Written by Pavel Torgashov 2012, translated by 野比「Conmajia」 2012
...全文
3763 81 打赏 收藏 转发到动态 举报
写回复
用AI写文章
81 条回复
切换为时间正序
请发表友善的回复…
发表回复
beifang1986 2013-06-17
  • 打赏
  • 举报
回复
确实很NX,顶顶
yy405405 2013-01-22
  • 打赏
  • 举报
回复
异常强大,下载试用中,谢谢啦~
tyzqqq 2012-12-27
  • 打赏
  • 举报
回复
Cosmic_Spy 2012-12-26
  • 打赏
  • 举报
回复
好强的文章!
Joson.e8love 2012-12-26
  • 打赏
  • 举报
回复
mickers 2012-12-24
  • 打赏
  • 举报
回复
年尾了,又见各种大牛出没
huangyoukuo 2012-07-17
  • 打赏
  • 举报
回复
学习了 牛人一个
EIT王子 2012-06-24
  • 打赏
  • 举报
回复
好东西啊。收藏了
yinsuxia 2012-06-21
  • 打赏
  • 举报
回复
虽然看不懂,感觉很NX
Conmajia 2012-06-18
  • 打赏
  • 举报
回复
疑问1:
不冲突。名字、类型、作用域都不同。
疑问2:
Or
lmhcs 2012-06-17
  • 打赏
  • 举报
回复
这个帖子好高了。好贴就是好贴。

我是学vb.net,我把这个控件转为vb/net
在转换TextBoxWrapper.cs遇到问提,搞不清楚,特来请教

using System;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;

namespace AutocompleteMenuNS
{
/// <summary>
/// Wrapper over the control like TextBox.
/// </summary>
public class TextBoxWrapper : ITextBoxWrapper
{
private Control target;
private PropertyInfo selectionStart;
private PropertyInfo selectionLength;
private PropertyInfo selectedText;
private MethodInfo getPositionFromCharIndex;
private event ScrollEventHandler RTBScroll;

private TextBoxWrapper(Control targetControl)
{
this.target = targetControl;
Init();
}

protected virtual void Init()
{
var t = target.GetType();
selectedText = t.GetProperty("SelectedText");
selectionLength = t.GetProperty("SelectionLength");
selectionStart = t.GetProperty("SelectionStart");
getPositionFromCharIndex = t.GetMethod("GetPositionFromCharIndex") ?? t.GetMethod("PositionToPoint");

if (target is RichTextBox)
(target as RichTextBox).VScroll += new EventHandler(TextBoxWrapper_VScroll);
}

void TextBoxWrapper_VScroll(object sender, EventArgs e)
{
if (RTBScroll != null)
RTBScroll(sender, new ScrollEventArgs(ScrollEventType.EndScroll, 0, 1));
}

public static TextBoxWrapper Create(Control targetControl)
{
var result = new TextBoxWrapper(targetControl);

if (result.selectedText == null || result.selectionLength == null || result.selectionStart == null ||
result.getPositionFromCharIndex == null)
return null;

return result;
}

public virtual string Text
{
get { return target.Text; }
set { target.Text = value; }
}

public virtual string SelectedText
{
get { return (string)selectedText.GetValue(target, null); }
set { selectedText.SetValue(target, value, null); }
}

public virtual int SelectionLength
{
get { return (int)selectionLength.GetValue(target, null); }
set { selectionLength.SetValue(target, value, null); }
}

public virtual int SelectionStart
{
get { return (int)selectionStart.GetValue(target, null); }
set { selectionStart.SetValue(target, value, null); }
}

public virtual Point GetPositionFromCharIndex(int pos)
{
return (Point)getPositionFromCharIndex.Invoke(target, new object[] { pos });
}


public virtual Form FindForm()
{
return target.FindForm();
}

public virtual event EventHandler LostFocus
{
add { target.LostFocus += value; }
remove { target.LostFocus -= value; }
}

public virtual event ScrollEventHandler Scroll
{
add
{
if (target is RichTextBox)
RTBScroll += value;
else
if (target is ScrollableControl) (target as ScrollableControl).Scroll += value;

}
remove
{
if (target is RichTextBox)
RTBScroll -= value;
else
if (target is ScrollableControl) (target as ScrollableControl).Scroll -= value;
}
}

public virtual event KeyEventHandler KeyDown
{
add { target.KeyDown += value; }
remove { target.KeyDown -= value; }
}

public virtual event MouseEventHandler MouseDown
{
add { target.MouseDown += value; }
remove { target.MouseDown -= value; }
}

public virtual Control TargetControl
{
get { return target; }
}
}
}


如上面代码 在变量声明时 如 private PropertyInfo selectionStart;
这是声明一个类型为 PropertyInfo 的selectionStart局部变量吧?
既然有变量名称为selectionStart为什么还能声明方法
public virtual int SelectionStart
{
get { return (int)selectionStart.GetValue(target, null); }
set { selectionStart.SetValue(target, value, null); }
}
这两个不冲突吗?方法SelectionStart返回的类型 是int 变量的类型PropertyInfo ???
这是疑问1
疑问2
在本例中
public static TextBoxWrapper Create(Control targetControl)
{
var result = new TextBoxWrapper(targetControl);

if (result.selectedText == null || result.selectionLength == null || result.selectionStart == null ||
result.getPositionFromCharIndex == null)
return null;

return result;
}
这个函数翻译为vb.net 该如何翻译,我在http://www.developerfusion.com/tools/convert/csharp-to-vb/ 换换的就结果是
Public Shared Function Create(targetControl As Control) As TextBoxWrapper
Dim result = New TextBoxWrapper(targetControl)

If result.selectedText Is Nothing OrElse result.selectionLength Is Nothing OrElse result.selectionStart Is Nothing OrElse result.getPositionFromCharIndex Is Nothing Then
Return Nothing
End If

Return result
End Function
这个结果在vs提示错误,我把它就修改为

Public Shared Function Create(ByVal targetControl As Control) As TextBoxWrapper
Dim result = New TextBoxWrapper(targetControl)

'If result.SelectedText Is Nothing OrElse result.SelectionLength = 0 OrElse result.SelectionStart = 0 _
'OrElse result.GetPositionFromCharIndex Is Nothing Then
If result.SelectedText Is Nothing OrElse result.SelectionLength = 0 OrElse result.SelectionStart = 0 Then
Return Nothing
End If
Return result
End Function

最后一个条件就不知道该怎么修改。。。
officeit 2012-06-12
  • 打赏
  • 举报
回复
收藏了,感谢分享
韬哥~~ 2012-06-12
  • 打赏
  • 举报
回复
收下了
Conmajia 2012-06-12
  • 打赏
  • 举报
回复
[Quote=引用 108 楼 的回复:]
每次看 野比的文章都会有收获,

呵呵 必须的顶啊,

不过 我还是没有把你的 山寨苹果浏览器的 tab 弄出来,惭愧惭愧啊,
[/Quote]

加油,那个不是很复杂,步骤看清楚
  • 打赏
  • 举报
回复


每次看 野比的文章都会有收获,

呵呵 必须的顶啊,

不过 我还是没有把你的 山寨苹果浏览器的 tab 弄出来,惭愧惭愧啊,
yrnaaa 2012-06-11
  • 打赏
  • 举报
回复
[Quote=引用 94 楼 的回复:]

引用 92 楼 的回复:

引用 76 楼 的回复:

引用 75 楼 的回复:

to 楼主:
不清楚楼主是从事什么工作的,经历过什么,感觉楼主知识面是相当的广泛,研究的也有深度,能否介绍一下如何快速成为高手?


社会闲散人员一个,没啥基础,不懂理论,脱离规范。
我的玩法不一定适合你,因为我不靠这吃饭,所以可以什么都玩。
我只是玩玩,玩得开心就行,根本不必理会什……
[/Quote]
理解,世外高手都这么说,还有一种就是从事研究所工作,不方便说工作内容,呵呵
stiff_neck 2012-06-11
  • 打赏
  • 举报
回复
好东西,慢慢看
ayun00 2012-06-11
  • 打赏
  • 举报
回复

这篇文章发现晚了啊
河西石头 2012-06-11
  • 打赏
  • 举报
回复
不错的东东,以前没有想到可以这么用
Conmajia 2012-06-11
  • 打赏
  • 举报
回复
[Quote=引用 98 楼 的回复:]

好东西啊,支持lua吗?
[/Quote]

NO
加载更多回复(61)

110,534

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧