Thursday, June 15, 2006

 

Some notes on SharpDevelop - 2

1.
from http://www.cnblogs.com/michael-zhang/articles/621148.html
http://www.cnblogs.com/michael-zhang/articles/629724.html
http://www.cnblogs.com/michael-zhang/articles/636381.html
http://www.cnblogs.com/michael-zhang/articles/651825.html
http://www.cnblogs.com/michael-zhang/articles/655267.html

SharpDevelop浅析_1_AddInTree

使用ICSharpCode.Core创建插件支持的应用程序

2、使用AddIn好处
方便扩展,可以看到SharpDevelop几乎是通过插接功能模块组装而成;核心可以不必实现自己的定义,方便地通过接口扩充功能;插件dll可以放在任意位置,对插件使用拷贝、粘贴式的部署。
许多应用程序也使用了一些插件机制,但大多数局限于特定的功能,如扩展菜单或新文件格式。SharpDevelop插件体系的目标是为应用程序提供简单易用而又强大的扩展点,allowing AddIns to extend nearly everything.

3、AddIn实现分析:
简单分析后,实现思路是这样的:定义一个接口ICommand,声明void DoCommand()方法,新增插件必须实现此接口;
单击菜单项或工具栏按钮时需要与主窗体交互,这可以通过在ICommand中定义属性MainForm或在void DoCommand(MainForm frm)中增加方法参数来传递主窗体的引用,这些实现起来倒也简单。
接下来的问题是如何通知应用程序新增加了插件呢,答案是使用xml配置文件,怎么组织这个配置文件的结构呢?这个问题其实成了实现插件功能的重点和难点,配置文件中希望说明新增插件的dll位置、类名、插接入主程序的菜单还是工具栏项、插接位置,或许还希望配置文件更容易被扩展?
这里(http: //www.codeproject.com/cs/library/Net_AddinProjFrmwork.asp)有一个结构不太好的配置文件定义形式(可能也是我们简单分析后会想到的定义方式,可以看出结构固定,且不易扩展),大家可以自行分析下:
sample.xml
1
2 1
3 Report Addin
4 1
5
6 Bar Code
7 1
8
9 Bar Code
10 1
11
12 Test Menu
13 AddinFunctionName
14 Some Status bar text
15 Some tool tip text
16 Addin2Settings.ico
17 Ctrl + H
18

19
20 ..
21 ..
22

23

24
25 ..
26 ..
27

28

29
30
31
32

33

现在来看看SharpDevelop的AddIn配置文件结构(参见Demo中的Entry.myAddins.Menus.addin):
Menus.addin
1 2 author = "michael zhang"
3 url = "http://www.cnblogs.com/michael-zhang/"
4 description = "基本插件"
5 addInManagerHidden = "true">
6
7
8

9
10
11

12
13
14
15

16
17

18
19 20 type = "Menu"
21 label = "${res:Demo.Menu.File}">
22 23 label = "Cmd&Black"
24 shortcut = "Control|B"
25 icon = "qq.face1"
26 class = "MainForm.CmdBlack"/>
27
28
29 30 label = "E&xit"
31 shortcut = "Control|X"
32 class = "MainForm.CmdExit"/>
33

34 35 type = "Menu"
36 label = "&Manager">
37
38

39

40
41 42 tooltip = "Black command"
43 icon = "qq.face1"
44 class = "MainForm.CmdBlack"/>
45
46 47 tooltip = "Exit the app"
48 icon = "CloseIcon"
49 class = "MainForm.CmdExit"/>
50

51
节提供了插件的名称、作者、url、插件描述等信息
提供了插件的唯一标识名称及版本,其它插件的配置文件可以引用该名称,如
指向该插件引用的dll位置
后面的形如...的是定义配置文件的核心数据,path节的name属性指明该节下的节点所处(在AddInTree中)的命名层次,节点下的MenuItem, ToolBarItem, FileFilter, Include等统称为Condon,各代表菜单项、工具栏按钮、文件过滤等,这些数据结构可以非常简单地被扩展、解析。
注:
addin配置文件中的 label = "${res:Demo.Menu.File}", icon = "qq.face1" 等属性值是指向资源文件的引用,资源文件见Entry项目的StrImgRes.resx

4、SharpDevelop插件树中的重要概念
Condon: 代表节下的一个(一般化)节点(如:...)统称为 Condon,该类含ID、Name、InsertBefore、InsertAfter、Conditions、Properties(类似于 HashTable的结构)等属性,配置节中的其它属性(除ID,Name,InsertBefore,InsertAfter外,如label, shortcut等)存储在Properties对象中。
Doozer: 代表Condon节点的更具体的实例,如MenuItemDoozer, ToolBarItemDoozer, FileFilterDoozer, IncludeDoozer, FileFilterDoozer等,用以创建具体的object对象,可以扩展编写自定义的Doozer。

5、Demo项目代码分析
至此,我们大概能猜到SharpDevelop中的插件机制是怎样的,下面就结合Demo的分析来体验一下SharpDevelop的插件功能:
MainForm.FrmMain.cs中使用单件模式获取此类,关键代码如下:
FrmMain.cs
1using ICSharpCode.Core;
2
3// 变量声明
4const string _BoundProperty = "FormBounds";
5Label _lblMsg;
6MenuStrip _menuStrip;
7ToolStrip _toolStrip;
8//
9void IniFrm()
10{
11 // 设置窗体位置
12 Rectangle rect = PropertyService.Get(_BoundProperty, new Rectangle(10, 10, this.Width, this.Height));
13 this.StartPosition = FormStartPosition.Manual;
14 this.Bounds = rect;
15 this.FormClosing += delegate
16 {
17 // 设置用户属性信息
18 PropertyService.Set(_BoundProperty, this.Bounds);
19 };
20
21 _lblMsg = new Label();
22 _lblMsg.Dock = DockStyle.Fill;
23 _lblMsg.Font = new Font("Arial", 16, FontStyle.Bold);
24 _lblMsg.Text = "App loaded!";
25 this.Controls.Add(_lblMsg);
26
27 _toolStrip = ToolbarService.CreateToolStrip(this, "/michael/myToolbar");
28 this.Controls.Add(_toolStrip);
29
30 _menuStrip = new MenuStrip();
31 MenuService.AddItemsToMenu(_menuStrip.Items, this, "/michael/myMenus");
32 this.Controls.Add(_menuStrip);
33}
34public void DrawMsg(string msg,Color color)
35{
36 _lblMsg.Text = msg;
37 _lblMsg.ForeColor = color;
38}主窗体的类中声明了Label, MenuStrip, ToolStrip 分别用以显示文字、菜单、工具栏。菜单、工具栏对象的获取通过ICSharpCode.Core内置类仅用简单的两行代码实现。此类中公开的 DrawMsg(...)方法用以向扩展菜单、工具栏按钮等提供公开可调用的功能。创建菜单、工具栏的两个方法中的一个重要参数是路径参数(分别是 "/michael/myToolbar"和"/michael/myMenus"),在前面的配置文件代码中可以找到相关定义节,其中相关节点一个重要属性是class, 该属性指定了(菜单、工具栏按钮的)相关类,其实现代码如下(MainForm项目的Commands.cs):
注:
FrmMain 类的IniFrm()方法中前几行代码用以设置启动窗体的大小和位置,使用到了ICSharpCode.Core.PropertyService类,该类将配置文件保存在"C:\Documents and Settings\michael\Application Data\michael's add-in test\myCfgParas.xml",其中[michael]是计算机名;[michael's add-in test]是应用程序名称,在程序启动时创建CoreStartup实例时指定;[myCfgParas.xml]由属性 CoreStartup.PropertiesName指定(详见后面Main()中的代码)。
Commands.cs
1//using
2namespace MainForm
3{
4 public class CmdBlack : AbstractMenuCommand
5 {
6 public override void Run()
7 {
8 FrmMain frm = (FrmMain)this.Owner;
9
10 StringBuilder sBuilder = new StringBuilder();
11 ArrayList alDatas = AddInTree.BuildItems("/michael/BlackText", null, true);
12 foreach (string str in alDatas)
13 sBuilder.AppendLine("suported types: " + str);
14
15 frm.DrawMsg(sBuilder.ToString(), Color.Black);
16 }
17 }
18
19 public class CmdExit : AbstractMenuCommand
20 {
21 public override void Run()
22 {
23 FrmMain frm = (FrmMain)this.Owner;
24 if (MessageBox.Show("Sure to exit?","Info:",MessageBoxButtons.YesNoCancel,MessageBoxIcon.Question) == DialogResult.Yes)
25 frm.Close();
26 }
27 }
28}
可以看到插件类必须实现ICommand接口或继承AbstractMenuCommand类,ICommand接口定义的Owner属性返回该对象的拥有者Object,在此例子中即FrmMain对象,CmdBlack类中通过((FrmMain)Owner).DrawMsg(...)向主窗体发出功能命令。
至此,只剩下对ICSharpCode.Core进行必要的初始化配置了(参见Demo中的Entry项目Program.cs中的static void Main()函数):
void Main
1LoggingService.Info("Application start");
2Assembly asm = Assembly.GetExecutingAssembly();
3FileUtility.ApplicationRootPath = Path.GetDirectoryName(asm.Location);
4ResourceService.RegisterNeutralStrings(new ResourceManager("Entry.StrImgRes", asm));
5ResourceService.RegisterNeutralImages(new ResourceManager("Entry.StrImgRes", asm));
6
7LoggingService.Info("Starting core services");
8CoreStartup coreStartup = new CoreStartup("michael's add-in test");
9coreStartup.PropertiesName = "myCfgParas";
10coreStartup.StartCoreServices();
11// 在指定文件夹中搜寻插件的配置文件
12coreStartup.AddAddInsFromDirectory(Path.Combine(FileUtility.ApplicationRootPath, "myAddIns"));
13// AddinManager 插件的属性:保存用户禁用的插件信息
14coreStartup.ConfigureExternalAddIns(Path.Combine(PropertyService.ConfigDirectory, "AddIns.xml"));
15// AddinManager 插件的属性:保存用户安装的插件信息
16coreStartup.ConfigureUserAddIns(Path.Combine(PropertyService.ConfigDirectory, "AddInInstallTemp"),
17 Path.Combine(PropertyService.ConfigDirectory, "AddIns"));
18coreStartup.RunInitialization();
19try
20{
21 LoggingService.Info("Running application");
22 Application.Run(MainForm.FrmMain.Instance);
23}catch{
24
注:
Demo 项目引用的AddinManager也是SharpCode.Core中的一个插件实现,查看AddinManager.Addin,可见其定义了菜单项、新界面(点击新菜单时Run()方法中定义弹出的新窗体)、新界面的上下文菜单,上下文菜单中使用配置菜单项何时可用……
Demo项目中扩展菜单的例子参见Demo中的ExtenalMenus工程中的Command.cs和ExternalMenu.addin,该项目实现了两个新的菜单项,并且实现了一个自定义的Doozer。

6、总结:
a, ICSharpCode.Core默认实现的Doozer
Class 根据配置文件的声明由System.Reflection创建出相关对象
FileFilter 创建出路径后缀名的过滤选项提供给OpenFileDialog或是SaveFileDialog使用
Include 向addin tree引进一个(使用item属性)或多个(使用path属性)子项
Icon 用以创建文件类型与图标间的关联
MenuItem 创建菜单项 type可为:Seperator, CheckBox, Item/Command, Menu, Builder
ToolBarItem 创建工具栏按钮荐 type可为:Seperator, CheckBox, Item, ComboBox, DropDownButton

b, 配置文件中引用预定义资源格式
${res:ResourceName} 引用ResourceService系统资源
${property:PropertyName} 引用PropertyService中的属性
${env:VariableName} 引用系统变量
${exe:ProperName} 引用整个程序集的属性

c, Condition
[略(有待进一步分析)]

7、相关资料:
《Dissecting a C# Application Inside SharpDevelop.pdf》
SharpDevelop源代码(\src\ 和 \samples\ICSharpCode.Core.Demo\)
http://www.sharpdevelop.com/OpenSource/SD/Default.aspx
http://www.codeproject.com/csharp/ICSharpCodeCore.asp

SharpDevelop浅析_2_User Interface

创建易扩展且功能模块松散耦合的应用程序

2、相关概念:
主窗体部分: Workbench
磁盘文件查看窗口: Pad
文件查看窗口: ViewContent
说明:
Pad可以使用ICSharpCode.Core的Addin插件机制来扩充Pad,如Visual Studio的资源管理器、类查看器、属性窗口、消息窗口等都属于Pad
ViewContent同样支持插件扩充,根据不同的文件类型使用不同的控件来处理显示,Demo中已实现的文件类型支持:网页文件、图片、普通文本,Visual Studio中的代码窗口、窗体设计器、资源编辑器、对象查看器等都属于ViewContent
Workbench与WorkbenchLayout结合使用来控制主窗口的外观显示,如普通的MDI(PhotoShop?)或Demo使用的WeifenLuo.WinFormsUI.Docking显示方式……

3、Demo代码分析
(注:下面的分析前提是你已经了解如何使用ICSharpCode.Core的Addin插件机制)
通过上面的界面及功能说明,可以确定程序至少应包括接口声明、基本实现两个项目,而接口中又分为:Pad接口、ViewContent接口、Workbench接口,这样的程序设计重点与难点在哪呢?思考后的结果应该是接口,从接口中能看到程序各部分是如何交互的,以及插件的扩展点,接口的设计好坏决定着程序的易维护性与易扩展性,所以下面就重点看接口的定义(附介绍相关对象的创建等):

Pad接口
1// Core|Interface 定义部分:
2public interface IPadContent : IDisposable
3{
4 System.Windows.Forms.Control Control { get; }
5
6 void RedrawContent();
7}
8public class PadDescriptor : IDisposable
9{
10 Codon codon;
11 IPadContent padContent;
12 bool padContentCreated;
13
14 public string Title {
15 get {
16 return codon.Properties["title"];
17 }
18 }
19 public string Icon {
20 get {
21 return codon.Properties["icon"];
22 }
23 }
24 public string Class {
25 get {
26 return codon.Properties["class"];
27 }
28 }
29 public IPadContent PadContent {
30 get {
31 CreatePad();
32 return padContent;
33 }
34 }
35 public void CreatePad()
36 {
37 if (!padContentCreated) {
38 padContentCreated = true;
39 padContent = (IPadContent)codon.AddIn.CreateObject(Class);
40 }
41 }
42 //省略部分方法、属性
43 public PadDescriptor(Codon codon)
44 {
45 this.codon = codon;
46 }
47}
48// Gui项目中的实现
49class PadContentWrapper : DockContent
50{
51 PadDescriptor padDescriptor; //通过此对象来获取相关属性
52 //
53}看到IPadContent接口返回一个WinForm的Control控件,此控件在实现时被相应的窗体获取并适当地显示,Demo中的磁盘查看器Pad返回的Control是一个UserConrol,使用了TreeView和ListView组合。注意IPadContent接口中定义返回一个Control而非Form是很聪明的技巧,可以看到实现端的PadContentWrapper继承自WeifenLuo.WinFormsUI.DockContent,这依赖于实现端的表现方式,而接口可以不受影响。PadDescriptor是个辅助类,用以返回Pad相关的属性信息,包括返回IPadContent的实例对象。
接下来看Pad的xml声明及客户端调用:
Pad声明及使用
1//取自Entry项目的SD_UI.addin
2
3
16 17 category = "Tools"
18 title = "${res:MainWindow.Windows.FileScoutLabel}"
19 icon = "PadIcons.FileBrowser"
20 shortcut = "Control|Alt|F"
21 class = "SDUserInterface.GUI.Pad.FileScout"/>
22

23// 取自Gui项目的DefaultWorkbench.cs
24void InitializeWorkspace()
25{
26 //
27 ArrayList contents = AddInTree.GetTreeNode("/SharpDevelop/Workbench/Pads").BuildChildItems(this);
28 foreach (PadDescriptor content in contents)
29 {
30 if (content != null)
31 {
32 ShowPad(content);
33 }
34 }
35 //
36}
配置文件中的class指定了实现IPadContent的一个类型(全称限定名),使用时通过ICSharpCode.Core的AddInTree对象构建相关PadDescriptor集合……

下面来看ViewContent的定义:

ViewContent接口
1// Core|Interface 定义部分:
2public interface IViewContent : IDisposable
3{
4 Control Control { get; set; }
5
6 IWorkbenchWindow WorkbenchWindow { get; set; }
7
8 string TitleName { get; set; }
9
10 string FileName { get; set; }
11
12 bool IsReadOnly { get; }
13
14 void Load(string fileName);
15
16 event EventHandler TitleNameChanged;
17}
18public class DisplayBindingDescriptor
19{
20 object binding = null;
21 Codon codon;
22
23 public IDisplayBinding Binding {
24 get {
25 if (binding == null) {
26 binding = codon.AddIn.CreateObject(codon.Properties["class"]);
27 }
28 return binding as IDisplayBinding;
29 }
30 }
31
32 public Codon Codon {
33 get {
34 return codon;
35 }
36 }
37
38 public DisplayBindingDescriptor(Codon codon)
39 {
40 this.codon = codon;
41 }
42
43 public bool CanAttachToFile(string fileName)
44 {
45 string fileNameRegex = codon.Properties["fileNamePattern"];
46 if (fileNameRegex == null || fileNameRegex.Length == 0) // no regex specified
47 return true;
48 return Regex.IsMatch(fileName, fileNameRegex, RegexOptions.IgnoreCase);
49 }
50}
51public interface IDisplayBinding
52{
53 bool CanCreateContentForFile(string fileName);
54
55 IViewContent CreateContentForFile(string fileName);
56}
57// Gui项目中的实现:
58public class SdiWorkspaceWindow : DockContent, IWorkbenchWindow
59{
60 IViewContent content;
61 //
62}
可以看到IViewContent同样是返回一个Conrol,供使用端(SdiWorkspaceWindow)根据需要封装组合;DisplayBindingDescriptor同样是个辅助类,除了返回ViewContent的相关属性信息外,提供了bool CanAttachToFile(string fileName)方法,用以判断当前显示插件是否可以显示相关类型的文件,这里的判断是通过配置文件中的fileNamePattern属性作正则判断(注:文件名称符合一定规则的可能会用到此属性,一般不常用),注意到该辅助类返回了一个IDisplayBinding接口,查看该接口的方法可以看到使用它来更进一步地判断当前文件是否是可支持类型(通过文件扩展名或试读取等方式),如果属于该插件支持类型的文件则创建并返回IViewContenet接口。ViewContent插件的声明如下:

ViewContent插件声明
1
2 3 supportedformats = "Web Pages"
4 class = "SDUserInterface.GUI.ViewContent.BrowserDisplayBinding"/>
5 6 insertafter = "Browser"
7 supportedformats = "Text Files,Source Files"
8 class = "SDUserInterface.GUI.ViewContent.TextViewDisplayBinding" />
9 10 insertbefore = "Text"
11 supportedformats = "图片"
12 class = "SDUserInterface.GUI.ViewContent.ImageDisplayBinding" />
13

值得注意的是insertbefore, insertafter 属性,此属性指明获取所有DisplayBindingDescriptor后的先后顺序,如:一个.rtf文件可以由Office-Word和记事本打开,一般要优先选择使用Word打开。
获取ViewContent对象的过程如下:双击磁盘文件Pad中的一个文件项时,调用FileService中的OpenFile(string fileName)方法,相关代码如下:

获取/使用ViewContent
1// 取自Gui项目中的Common/FileService.cs
2public static IWorkbenchWindow OpenFile(string fileName)
3{
4 //
5 IDisplayBinding binding = DisplayBindingService.GetBindingPerFileName(fileName);
6
7 if (binding != null) {
8 binding.CreateContentForFile(fileName);
9 WorkbenchSingleton.Workbench.ShowView(newContent);
10 //
11 } else {
12 throw new ApplicationException("Can't open " + fileName + ", no display codon found.");
13 }
14 return GetOpenFile(fileName);
15}
16// 取自Gui项目中的Common/DisplayBindingService.cs
17static DisplayBindingDescriptor GetCodonPerFileName(string filename)
18{
19 foreach (DisplayBindingDescriptor binding in bindings) {
20 if (binding.CanAttachToFile(filename)) {
21 if (binding.Binding != null && binding.Binding.CanCreateContentForFile(filename)) {
22 return binding;
23 }
24 }
25 }
26 return null;
27}
接下来看主窗体的定义:

Workbench接口
1public interface IWorkbench
2{
3 string Title { get; set; }
4
5 List ViewContentCollection { get; }
6
7 List PadContentCollection { get; }
8
9 IWorkbenchWindow ActiveWorkbenchWindow { get; }
10
11 object ActiveContent { get; }
12
13 IWorkbenchLayout WorkbenchLayout { get; set; }
14
15 void ShowView(IViewContent content);
16
17 void CloseAllViews();
18
19 void CloseView(IViewContent content);
20
21 void ShowPad(PadDescriptor content);
22
23 PadDescriptor GetPad(Type type);
24
25 void RedrawAllComponents();
26}
27public interface IWorkbenchLayout
28{
29 bool FullScreen { get; set; }
30
31 IWorkbenchWindow ActiveWorkbenchwindow { get; }
32
33 object ActiveContent { get; }
34
35 void Attach(IWorkbench workbench);
36
37 void Detach();
38
39 void ShowPad(PadDescriptor content);
40
41 void ShowPad(PadDescriptor content,bool bActivateIt);
42
43 IWorkbenchWindow ShowView(IViewContent content);
44
45 void RedrawAllComponents();
46
47 void LoadConfiguration();
48 void StoreConfiguration();
49}
IWorkbench定义主窗体,IWorkbenchLayout定义窗体布局,可以看到IWorkbench的两个重要属性是Pad和ViewContent的对象集合(维护已打开的窗体记录,避免重复打开等作用),其实现类的ShowPad()/ShowView()方法执行的操作即向对应的集合添加成员,然后调用IWorkbenchLayout的ShowPad()/ShowView()。IWorkbenchLayout的LoadConfiguration()和StoreConfiguration()方法用以在窗体加载或关闭时执行加载或保存子窗口布局的操作。

至此Demo的核心已分析完了,我们可以根据定义的接口扩展程序,如增加项目管理/查看Pad, 增加.swf等文件查看的ViewContent, 或更换主界面显示为普通MDI方式……

4、总结:
此Demo更说明了ICSharpCode.Core的插件支持可以应用到很多的方面:
a, 界面组成部分Pad
b, 界面中不同类型的文件查看器ViewContent
c, 磁盘文件查看Pad的下侧文件列表针对不同文件显示相关图标(详见SD_UI.addin文件的"/Workspace/Icons"层次下的定义)
...
SharpDevelop的这种界面设计方案使得应用程序很容易扩展和进一步开发,而不与已开发的模块冲突;从中得到的启发是应用程序要重点设计接口(契约)以及处理对象间的关系……

说明:Demo代码基本来源于SharpDevelop源码,删改了部分代码以使Demo精简和突出重点。


SharpDevelop浅析_3_Internationalization & TextEditor

国际化、文档编辑器、语法高亮显示

2、SharpDevelop的Internationalization的使用
多语言的实现就是在显示时根据键获取相应语言环境下的键值(Dictionary),因此在编写程序时应该引用键,而非直接书写要显示的字符。一般地,应用程序的显示包括菜单、状态栏、提示字符等,因此Demo中的主菜单在配置文件(参见Basic.addin)中使用label = "${res:Menu.File.Open}",退出应用程序的提示字符使用string s = StringParser.Parse("${res:Info.Exit}");。ICSharpCode.Core.dll支持语言环境的实时修改(修改后语言设置不需重启应用程序),因此要在应用程序订阅语言环境改变的事件,相关代码如下:


语言环境的修改及事件响应
1//a, 设置语言
2//引自SharpPad项目的Dialogs\SelectCulturePanel.cs
3public override bool ReceiveDialogMessage(DialogMessage message)
4{
5 if (message == DialogMessage.OK) {
6 if (SelectedCulture != null) {
7 PropertyService.Set("CoreProperties.UILanguage", SelectedCulture);
8 }
9 }
10 return true;
11}
12//返回当前窗体用户选择的语言设置(从显示国家国旗的ListView控件)
13string SelectedCulture {
14 get {
15 if (listView.SelectedItems.Count > 0) {
16 return listView.SelectedItems[0].SubItems[1].Text;
17 }
18 return null;
19 }
20}
21//b, 刷新当前应用程序
22//引自SharpPad项目的SharpPad.cs
23void IniFrm()
24{
25 //
26 _menuStrip = new MenuStrip();
27 MenuService.AddItemsToMenu(_menuStrip.Items, this, "/michael/myMenus");
28 this.Controls.Add(_menuStrip);
29
30 PropertyService.PropertyChanged += new PropertyChangedEventHandler(PropertyService_PropertyChanged);
31 ResourceService.LanguageChanged += delegate{
32 //更新菜单项的Text显示
33 foreach (ToolStripItem item in _menuStrip.Items)
34 {
35 if (item is IStatusUpdate)
36 {
37 ((IStatusUpdate)item).UpdateText();
38 }
39 }
40 };
41}
42//引自ICSharpCode.Core的src\AddinTree\Addin\DefaultDoozers\MenuItem\Gui\IStatusUpdate.Core
43using System;
44namespace ICSharpCode.Core
45{
46 public interface IStatusUpdate
47 {
48 void UpdateStatus();
49 void UpdateText();
50 }
51}



另外,可以看到Demo中的选项命令窗口也采用了插件模式来构造窗体(实现细节就不多谈了),容器窗体是TreeViewOption,插件窗体如:SelectCulturePanel, SelectStylePanel, DemoNothing,真是“扩展--无处不在”呀。SharpDevelop源码中的这些窗体的成员控件是通过.xfrm配置文件配置,窗体继承自XmlUserControl来根据配置文件生成控件,有一定的灵活性。
新的语言包资源文件放在\data\resources\目录下(如StringResources.cn-gb.resources),注意应用程序默认语言选项以及在未找到相关语言资源时都是引用Entry中的myRes.resx资源文件;语言声明文件是\data\resources\languages\LanguageDefinition.xml,其格式如下:
LanguageDefinition.xml
1
2
3
4
5

SharpPad中动态显示可选语言项的分析类是LanguageService.cs和Language.cs,此处就不多解释了。

3、SharpDevelop的Internationalization的实现分析
SharpDevelop的多语言支持的键值对是通过本地资源文件存储的,当Demo中更改语言环境设置时,引发ICSharpCode.Core.dll中的事件及方法顺序如下:
PropertyService类的属性更新引发PropertyChanged事件 -> ResourceService响应接收到的事件并重新加载保存在内存中的资源键值对,然后引发LanguageChanged事件(由使用端接收并作相关处理,如Demo中的SharpPad.cs中的事件订阅/处理)属性更新的相关代码如下:
属性更改
1//PropertyService类实际是提供了对Properties类的包装,因此直接看Properties类中的代码:
2//引自ICSharpCode.Core\src\Services\PropertService\Properties.cs
3public void Set(string property, T value)
4{
5 T oldValue = default(T);
6 if (!properties.ContainsKey(property)) {
7 properties.Add(property, value);
8 } else {
9 oldValue = Get(property, value);
10 properties[property] = value;
11 }
12 OnPropertyChanged(new PropertyChangedEventArgs(this, property, oldValue, value));
13}
14protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
15{
16 if (PropertyChanged != null) {
17 PropertyChanged(this, e);
18 }
19}
20public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
21public event PropertyChangedEventHandler PropertyChanged;
资源服务类的事件响应代码如下:
ResourceService类的事件响应
1//引自ICSharpCode.Core\src\Services\ResourceService\ResourceService.cs
2//首先在类的初始化中订阅属性更改事件:
3PropertyService.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChange);
4//类级别变量:
5static Hashtable localStrings = null;
6static Hashtable localIcons = null;
7public static event EventHandler LanguageChanged;
8//事件响应:
9static void OnPropertyChange(object sender, PropertyChangedEventArgs e)
10{
11 if (e.Key == uiLanguageProperty && e.NewValue != e.OldValue) {
12 LoadLanguageResources((string)e.NewValue);
13 if (LanguageChanged != null)
14 LanguageChanged(null, e);
15 }
16}
17static void LoadLanguageResources(string language)
18{
19 try
20 {
21 Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(language);
22 }
23 catch (Exception)
24 {
25 try
26 {
27 Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(language.Split('-')[0]);
28 }
29 catch (Exception) { }
30 }
31
32 localStrings = Load(stringResources, language);
33 if (localStrings == null && language.IndexOf('-') > 0)
34 {
35 localStrings = Load(stringResources, language.Split('-')[0]);
36 }
37
38 localIcons = Load(imageResources, language);
39 if (localIcons == null && language.IndexOf('-') > 0)
40 {
41 localIcons = Load(imageResources, language.Split('-')[0]);
42 }
43
44 localStringsResMgrs.Clear();
45 localIconsResMgrs.Clear();
46 currentLanguage = language;
47 foreach (ResourceAssembly ra in resourceAssemblies)
48 {
49 ra.Load();
50 }
51}
52static Hashtable Load(string name, string language)
53{
54 return Load(resourceDirectory + Path.DirectorySeparatorChar + name + "." + language + ".resources");
55}
56static Hashtable Load(string fileName)
57{
58 if (File.Exists(fileName)) {
59 Hashtable resources = new Hashtable();
60 ResourceReader rr = new ResourceReader(fileName);
61 foreach (DictionaryEntry entry in rr) {
62 resources.Add(entry.Key, entry.Value);
63 }
64 rr.Close();
65 return resources;
66 }
67 return null;
68}

4、SharpDevelop的文档管理的基本概念
代码编写工具的核心是代码编辑窗口(如打开一个.cs文件的窗口),如何在应用程序中存储该窗口内的字符内容?有些文件可能有上千行,统计下来可能有过万个字符,如果使用string对象存储字符内容,显然是不能满足性能要求,而且要实现语法高亮显示的话,还需要区分存储TextWord和相应颜色……
先看一下字符管理的基本要求:
ITextBufferStrategy接口
1//引自ICSharpCode.TextEditor\src\Document\TextBufferStrategy\ITextBufferStrategy.cs
2namespace ICSharpCode.TextEditor.Document
3{
4 /**////
5 /// Interface to describe a sequence of characters that can be edited.
6 ///

7 public interface ITextBufferStrategy
8 {
9 /**////
10 /// The current length of the sequence of characters that can be edited.
11 ///

12 int Length {
13 get;
14 }
15
16 /**////
17 /// Inserts a string of characters into the sequence.
18 ///

19 ///
20 /// offset where to insert the string.
21 ///
22 ///
23 /// text to be inserted.
24 ///
25 void Insert(int offset, string text);
26
27 /**////
28 /// Removes some portion of the sequence.
29 ///

30 ///
31 /// offset of the remove.
32 ///
33 ///
34 /// number of characters to remove.
35 ///
36 void Remove(int offset, int length);
37
38 /**////
39 /// Replace some portion of the sequence.
40 ///

41 ///
42 /// offset.
43 ///
44 ///
45 /// number of characters to replace.
46 ///
47 ///
48 /// text to be replaced with.
49 ///
50 void Replace(int offset, int length, string text);
51
52 /**////
53 /// Fetches a string of characters contained in the sequence.
54 ///

55 ///
56 /// Offset into the sequence to fetch
57 ///
58 ///
59 /// number of characters to copy.
60 ///
61 string GetText(int offset, int length);
62
63 /**////
64 /// Returns a specific char of the sequence.
65 ///

66 ///
67 /// Offset of the char to get.
68 ///
69 char GetCharAt(int offset);
70
71 /**////
72 /// This method sets the stored content.
73 ///

74 ///
75 /// The string that represents the character sequence.
76 ///
77 void SetContent(string text);
78 }
79}
SharpDevelop采用的策略是使用带Gap的字符,Gap的长度小于预定规格时,按约定增加一定长度(有些类似于SqlServer的表空间管理?),插入/修改/删除字符时只是修改Gap的长度和位置和移动部分字符,核心函数如下:
GapTextBufferStrategy类
1//引自ICSharpCode.TextEditor\src\Document\TextBufferStrategy\GapTextBufferStrategy.cs
2//类级别变量:
3char[] buffer = new char[0];
4int gapBeginOffset = 0;
5int gapEndOffset = 0;
6/**////
7/// 小于此长度时增长Buffer/Gap
8///

9int minGapLength = 32;
10/**////
11/// 每次需要增长时,增长此长度的Gap
12///

13int maxGapLength = 256;
14//重要函数:
15public void SetContent(string text)
16{
17 if (text == null) {
18 text = String.Empty;
19 }
20 buffer = text.ToCharArray();
21 gapBeginOffset = gapEndOffset = 0;
22}
23public void Insert(int offset, string text)
24{
25 Replace(offset, 0, text);
26}
27public void Remove(int offset, int length)
28{
29 Replace(offset, length, String.Empty);
30}
31public void Replace(int offset, int length, string text)
32{
33 if (text == null) {
34 text = String.Empty;
35 }
36
37 // Math.Max is used so that if we need to resize the array
38 // the new array has enough space for all old chars
39 PlaceGap(offset + length, Math.Max(text.Length - length, 0));
40 text.CopyTo(0, buffer, offset, text.Length);
41 gapBeginOffset += text.Length - length;
42}
43void PlaceGap(int offset, int length)
44{
45 int deltaLength = GapLength - length;
46 // 如果Gap的长度足够大,则只是作移动相关字符的处理
47 if (minGapLength <= deltaLength && deltaLength <= maxGapLength) {
48 int delta = gapBeginOffset - offset;
49 // check if the gap is already in place
50 if (offset == gapBeginOffset) {
51 return;
52 } else if (offset < gapBeginOffset) {
53 int gapLength = gapEndOffset - gapBeginOffset;
54 Array.Copy(buffer, offset, buffer, offset + gapLength, delta);
55 } else { //offset > gapBeginOffset
56 Array.Copy(buffer, gapEndOffset, buffer, gapBeginOffset, -delta);
57 }
58 gapBeginOffset -= delta;
59 gapEndOffset -= delta;
60 return;
61 }
62
63 // 否则,需要新分析buffer的大小,并修改offset等位置数值
64 int oldLength = GapLength;
65 int newLength = maxGapLength + length;
66 int newGapEndOffset = offset + newLength;
67 char[] newBuffer = new char[buffer.Length + newLength - oldLength]; //新分配后将有maxGapLength长度的Gap
68
69 if (oldLength == 0) {
70 Array.Copy(buffer, 0, newBuffer, 0, offset);
71 Array.Copy(buffer, offset, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
72 } else if (offset < gapBeginOffset) {
73 int delta = gapBeginOffset - offset;
74 Array.Copy(buffer, 0, newBuffer, 0, offset);
75 Array.Copy(buffer, offset, newBuffer, newGapEndOffset, delta);
76 Array.Copy(buffer, gapEndOffset, newBuffer, newGapEndOffset + delta, buffer.Length - gapEndOffset);
77 } else {
78 int delta = offset - gapBeginOffset;
79 Array.Copy(buffer, 0, newBuffer, 0, gapBeginOffset);
80 Array.Copy(buffer, gapEndOffset, newBuffer, gapBeginOffset, delta);
81 Array.Copy(buffer, gapEndOffset + delta, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
82 }
83
84 buffer = newBuffer;
85 gapBeginOffset = offset;
86 gapEndOffset = newGapEndOffset;
87}

有了基本的数据容器,核心问题已经解决了,但是在绘制界面时直接使用上面的类,显然不够方便,于是就定义了LineSegment和TextWord类来分别存储行、单词,注意这两个类存储的只是int类型的offset和length信息,而不存储字符或字符串对象。注意TextWord类中有个HighlightColor类型的变量用以存储语法高亮显示信息。
有了上面的这些类,便可以组合起来补充些其它信息对外提供服务了,IDocument接口存在的目的即在于此,它封装了ITextEditorProperties(是否显示空格/Tab/Eol/HRuler等)、UndoStack、ITextBufferStrategy、IHighlightingStrategy、FoldingManager、BookmarkManager等属性对象。

5、SharpDevelop的SyntaxHighlighting配置文件的定义
是时候对语法高亮显示作一些分析了,此处不详述其实现,而重点分析其配置定义,从中亦可以猜出部分实现。
相关配置文件均保存在ICSharpCode.TextEditor项目\Resource\目录下
首先SyntaxMode.xml文件中显示了已定义的文件类型及其声明文件位置,其解析类参见\src\Document\HighlightingStrategy\SyntaxModes\FileSyntaxModeProvider.cs


SyntaxMode.xml
1
2 3 name = "ASP/XHTML"
4 extensions = ".asp;.aspx;.asax;.asmx"/>
5
6 7 name = "BAT"
8 extensions = ".bat"/>
9
10 11 name = "C++.NET"
12 extensions = ".c;.h;.cc;.C;.cpp;.hpp"/>
13
14 15 name = "C#"
16 extensions = ".cs"/>
17
18 19 name = "XML"
20 extensions = ".xml;.xsl;.xslt;.xsd;.manifest;.config;.addin;.xshd; .wxs;.proj;.csproj;.vbproj;.ilproj;.booproj;.build;.xfrm;.targets; .xaml;.xpt;.xft;.map;.wsdl;.disco"/>
21

取CSharp-Mode.xshd(注:xshd是Xml Syntax Highlighting Definition的缩写)为例,查看其定义:


CSharp-Mode.xshd
1
2
3
4
5

6
7
8
9 &<>~!%^*()-+=|\#/{}[]:;"' , .?
10
11 #
12

13
14 /*
15 */
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

42
43
44
45
46

47
48
49
50
51
52
53
54
55
56
57

58

59

60

上面的文件公摘选了部分标签,其中
标签定义了指定名称属性的指定值
标签定义了数值的字体显示样式
定义了分隔单词的字符
定义了包含在此指定Begin/End中的字符的显示样式,注意没有标签的一般设stopateol(stop at end-of-line)为true
指定了其子集声明的单词的显示样式

6、SharpDevelop的TextEditor控件的实现概述
接下来的任务是要显示和支持用户输入了,直接使用TextBox或RichTextBox好像都不太现实,效率上也必定有不少损失,于是SharpDevelop的方式是直接继承自Control, Panel, UserControl 的方式来实现编辑控件(参见ICSharpCode.TextEditor项目\src\Gui\...)。
首先使用TextEditorControlBase(继承自UserControl)封装当前文件路径、Encoding、IDocument对象、ITextEditorProperties对象、快捷键列表(Dictionary类型)的变量,提供LoadFile()、SaveFile()等重要方法。
TextEditorControl类继承自上面的类,并声明了Panel、Splitter、TextAreaControl、PrintDocument控件,其中显示文件的核心控件是TextAreaControl,该控件在此类中被声明了两个变量,默认只有一个primaryTextArea显示,支持切分为两个窗口的显示,当有两个窗口时,Splitter控件才有效;PrinDocument控件用以打印输出内容;该类的另一个重要功能是提供了UnDo()、Redo()方法。
TextAreaControl(继承自Panel)控件,封装了TextArea、VScrollBar、HScrollBar控件,其中TextArea负责文件数据显示,另外两个控件负责文件内容大于可见尺寸时的滚动条服务。
TextArea(继承自Control)封装了TextView, IconBarMargin, GutterMargin, FoldMargin, SelectionManager, Caret 等控件或类对象,其中前四个控件均继承自AbstractMargin,代表区域对象,各自负责字符区域(较大的文件内容绘制区)、图标区域(如Bookmark图标所在列)、Gutter区域(如行号)、折叠控制区域的绘制,SelectionManager用以控制选中项,Caret用以控制光标位置调整和显示。

下面是一些我在读代码的过程中有过的疑问及解答:
a, 加载文件时发生了什么?
答:加载文件时控件根据文件的后缀名选择了相应的高亮显示策略,然后读取文件的内容并生成相应的GapTextBufferStrategy, LineSegment, TextWord 等对象,并且对所有的TextWord对象的HighlightColor类型成员变量完成分析赋值(用以在Paint函数中显示),相关代码如下:


LoadFile()相关代码
1//取自TextEditorControlBase.cs
2public void LoadFile(string fileName, bool autoLoadHighlighting, bool autodetectEncoding)
3{
4 BeginUpdate();
5 document.TextContent = String.Empty;
6 document.UndoStack.ClearAll();
7 document.BookmarkManager.Clear();
8 if (autoLoadHighlighting) {
9 //根据文件扩展名判断并赋值高亮显示的策略
10 document.HighlightingStrategy = HighlightingStrategyFactory.CreateHighlightingStrategyForFile(fileName);
11 }
12
13 if (autodetectEncoding) {
14 Encoding encoding = this.Encoding;
15 //赋值
16 Document.TextContent = Util.FileReader.ReadFileContent(fileName, ref encoding, this.TextEditorProperties.Encoding);
17 this.Encoding = encoding;
18 } else {
19 using (StreamReader reader = new StreamReader(fileName, this.Encoding)) {
20 Document.TextContent = reader.ReadToEnd();
21 }
22 }
23
24 this.FileName = fileName;
25 OptionsChanged();
26 Document.UpdateQueue.Clear();
27 EndUpdate();
28
29 Refresh();
30}
31//引自DefaultDocument.cs
32ITextBufferStrategy textBufferStrategy = null;
33ILineManager lineTrackingStrategy = null;
34public string TextContent {
35 get {
36 return GetText(0, textBufferStrategy.Length);
37 }
38 set {
39 Debug.Assert(textBufferStrategy != null);
40 Debug.Assert(lineTrackingStrategy != null);
41 OnDocumentAboutToBeChanged(new DocumentEventArgs(this, 0, 0, value));
42 //赋值
43 textBufferStrategy.SetContent(value);
44 //赋值 && 分析并完成高亮分析的赋值
45 lineTrackingStrategy.SetContent(value);
46
47 OnDocumentChanged(new DocumentEventArgs(this, 0, 0, value));
48 OnTextContentChanged(EventArgs.Empty);
49 }
50}
51//引自DefaultLineManager.cs
52public void SetContent(string text)
53{
54 lineCollection.Clear();
55 if (text != null) {
56 textLength = text.Length;
57 // 生成LineSegment集合
58 CreateLines(text, 0, 0);
59 // 高亮分析
60 RunHighlighter();
61 }
62}
b, 控件如何响应键盘事件?
答:对于方向键及快捷功能键通过预定义的实现IEditAction接口的类响应(执行功能,不影响字符内容);其它字母/数字键直接输入,同时执行更新Folding, Bookmark, Higlighting等属性信息。相关代码:
键盘事件响应
1//功能键的定义(引自TextEditorControlBase.cs):
2protected Dictionary editactions = new Dictionary();
3protected TextEditorControlBase()
4{
5 GenerateDefaultActions();
6 HighlightingManager.Manager.ReloadSyntaxHighlighting += new EventHandler(ReloadHighlighting);
7}
8void GenerateDefaultActions()
9{
10 editactions[Keys.Left] = new CaretLeft();
11 editactions[Keys.Left | Keys.Shift] = new ShiftCaretLeft();
12 editactions[Keys.Left | Keys.Control] = new WordLeft();
13 editactions[Keys.Left | Keys.Control | Keys.Shift] = new ShiftWordLeft();
14 editactions[Keys.Right] = new CaretRight();
15 editactions[Keys.Right | Keys.Shift] = new ShiftCaretRight();
16 editactions[Keys.Right | Keys.Control] = new WordRight();
17 editactions[Keys.Right | Keys.Control | Keys.Shift] = new ShiftWordRight();
18 editactions[Keys.Up] = new CaretUp();
19 editactions[Keys.Up | Keys.Shift] = new ShiftCaretUp();
20 editactions[Keys.Up | Keys.Control] = new ScrollLineUp();
21 editactions[Keys.Down] = new CaretDown();
22 editactions[Keys.Down | Keys.Shift] = new ShiftCaretDown();
23 editactions[Keys.Down | Keys.Control] = new ScrollLineDown();
24
25 editactions[Keys.Insert] = new ToggleEditMode();
26 editactions[Keys.Insert | Keys.Control] = new Copy();
27 editactions[Keys.Insert | Keys.Shift] = new Paste();
28 editactions[Keys.Delete] = new Delete();
29 editactions[Keys.Delete | Keys.Shift] = new Cut();
30 editactions[Keys.Home] = new Home();
31 editactions[Keys.Home | Keys.Shift] = new ShiftHome();
32 editactions[Keys.Home | Keys.Control] = new MoveToStart();
33 editactions[Keys.Home | Keys.Control | Keys.Shift] = new ShiftMoveToStart();
34 editactions[Keys.End] = new End();
35 editactions[Keys.End | Keys.Shift] = new ShiftEnd();
36 editactions[Keys.End | Keys.Control] = new MoveToEnd();
37 editactions[Keys.End | Keys.Control | Keys.Shift] = new ShiftMoveToEnd();
38 editactions[Keys.PageUp] = new MovePageUp();
39 editactions[Keys.PageUp | Keys.Shift] = new ShiftMovePageUp();
40 editactions[Keys.PageDown] = new MovePageDown();
41 editactions[Keys.PageDown | Keys.Shift] = new ShiftMovePageDown();
42
43 editactions[Keys.Return] = new Return();
44 editactions[Keys.Tab] = new Tab();
45 editactions[Keys.Tab | Keys.Shift] = new ShiftTab();
46 editactions[Keys.Back] = new Backspace();
47 editactions[Keys.Back | Keys.Shift] = new Backspace();
48
49 editactions[Keys.X | Keys.Control] = new Cut();
50 editactions[Keys.C | Keys.Control] = new Copy();
51 editactions[Keys.V | Keys.Control] = new Paste();
52
53 editactions[Keys.A | Keys.Control] = new SelectWholeDocument();
54 editactions[Keys.Escape] = new ClearAllSelections();
55
56 editactions[Keys.Divide | Keys.Control] = new ToggleComment();
57 editactions[Keys.OemQuestion | Keys.Control] = new ToggleComment();
58
59 editactions[Keys.Back | Keys.Alt] = new Actions.Undo();
60 editactions[Keys.Z | Keys.Control] = new Actions.Undo();
61 editactions[Keys.Y | Keys.Control] = new Redo();
62
63 editactions[Keys.Delete | Keys.Control] = new DeleteWord();
64 editactions[Keys.Back | Keys.Control] = new WordBackspace();
65 editactions[Keys.D | Keys.Control] = new DeleteLine();
66 editactions[Keys.D | Keys.Shift | Keys.Control] = new DeleteToLineEnd();
67
68 editactions[Keys.B | Keys.Control] = new GotoMatchingBrace();
69}
70internal IEditAction GetEditAction(Keys keyData)
71{
72 if (!editactions.ContainsKey(keyData)) {
73 return null;
74 }
75 return (IEditAction)editactions[keyData];
76}
77//功能键的响应(取自TextArea.cs):
78protected override bool ProcessDialogKey(Keys keyData)
79{
80 return ExecuteDialogKey(keyData) || base.ProcessDialogKey(keyData);
81}
82public bool ExecuteDialogKey(Keys keyData)
83{
84 // try, if a dialog key processor was set to use this
85 if (DoProcessDialogKey != null && DoProcessDialogKey(keyData)) {
86 return true;
87 }
88
89 if (keyData == Keys.Back || keyData == Keys.Delete || keyData == Keys.Enter) {
90 if (TextEditorProperties.UseCustomLine == true) {
91 if (SelectionManager.HasSomethingSelected) {
92 if (Document.CustomLineManager.IsReadOnly(SelectionManager.SelectionCollection[0], false))
93 return true;
94 } else {
95 int curLineNr = Document.GetLineNumberForOffset(Caret.Offset);
96 if (Document.CustomLineManager.IsReadOnly(curLineNr, false) == true)
97 return true;
98 if ((Caret.Column == 0) && (curLineNr - 1 >= 0) && keyData == Keys.Back &&
99 Document.CustomLineManager.IsReadOnly(curLineNr - 1, false) == true)
100 return true;
101 if (keyData == Keys.Delete) {
102 LineSegment curLine = Document.GetLineSegment(curLineNr);
103 if (curLine.Offset + curLine.Length == Caret.Offset &&
104 Document.CustomLineManager.IsReadOnly(curLineNr + 1, false) == true) {
105 return true;
106 }
107 }
108 }
109 }
110 }
111
112 // if not (or the process was 'silent', use the standard edit actions
113 IEditAction action = motherTextEditorControl.GetEditAction(keyData);
114 AutoClearSelection = true;
115 if (action != null) {
116 motherTextEditorControl.BeginUpdate();
117 try {
118 lock (Document) {
119 // 执行相关的功能操作
120 action.Execute(this);
121 if (SelectionManager.HasSomethingSelected && AutoClearSelection /**//*&& caretchanged*/) {
122 if (Document.TextEditorProperties.DocumentSelectionMode == DocumentSelectionMode.Normal) {
123 SelectionManager.ClearSelection();
124 }
125 }
126 }
127 } finally {
128 motherTextEditorControl.EndUpdate();
129 Caret.UpdateCaretPosition();
130 }
131 return true;
132 }
133 return false;
134}
135
136//输入键的响应(取自TextArea.cs):
137protected override void OnKeyPress(KeyPressEventArgs e)
138{
139 base.OnKeyPress(e);
140 SimulateKeyPress(e.KeyChar);
141 e.Handled = true;
142}
143public void SimulateKeyPress(char ch)
144{
145 if (Document.ReadOnly) {
146 return;
147 }
148
149 if (TextEditorProperties.UseCustomLine == true) {
150 if (SelectionManager.HasSomethingSelected) {
151 if (Document.CustomLineManager.IsReadOnly(SelectionManager.SelectionCollection[0], false))
152 return;
153 } else if (Document.CustomLineManager.IsReadOnly(Caret.Line, false) == true)
154 return;
155 }
156
157 if (ch < ' ') {
158 return;
159 }
160
161 if (!HiddenMouseCursor && TextEditorProperties.HideMouseCursor) {
162 HiddenMouseCursor = true;
163 Cursor.Hide();
164 }
165 CloseToolTip();
166
167 motherTextEditorControl.BeginUpdate();
168 // INSERT char
169 if (!HandleKeyPress(ch)) {
170 switch (Caret.CaretMode) {
171 case CaretMode.InsertMode:
172 InsertChar(ch);
173 break;
174 case CaretMode.OverwriteMode:
175 ReplaceChar(ch);
176 break;
177 default:
178 Debug.Assert(false, "Unknown caret mode " + Caret.CaretMode);
179 break;
180 }
181 }
182
183 int currentLineNr = Caret.Line;
184 int delta = Document.FormattingStrategy.FormatLine(this, currentLineNr, Document.PositionToOffset(Caret.Position), ch);
185
186 motherTextEditorControl.EndUpdate();
187 if (delta != 0) {
188// this.motherTextEditorControl.UpdateLines(currentLineNr, currentLineNr);
189 }
190}
191//输入键通过底层的方法输入字符和更改高亮显示,如下分析(以Insert为例,取自DefaultDocument.cs):
192public void Insert(int offset, string text)
193{
194 if (readOnly) {
195 return;
196 }
197 OnDocumentAboutToBeChanged(new DocumentEventArgs(this, offset, -1, text));
198 DateTime time = DateTime.Now;
199 //增加字符
200 textBufferStrategy.Insert(offset, text);
201
202 time = DateTime.Now;
203 //更新LineSegment, TextWord对象
204 lineTrackingStrategy.Insert(offset, text);
205
206 time = DateTime.Now;
207
208 undoStack.Push(new UndoableInsert(this, offset, text));
209
210 time = DateTime.Now;
211 OnDocumentChanged(new DocumentEventArgs(this, offset, -1, text));
212}
213// 追踪到DefaultLineManager.cs中Insert()方法实际上调用到Replace(offset,length,string.Empty)方法:
214public void Replace(int offset, int length, string text)
215{
216 int lineNumber = GetLineNumberForOffset(offset);
217 int insertLineNumber = lineNumber;
218 if (Remove(lineNumber, offset, length)) {
219 --lineNumber;
220 }
221
222 lineNumber += Insert(insertLineNumber, offset, text);
223
224 int delta = -length;
225 if (text != null) {
226 delta = text.Length + delta;
227 }
228
229 if (delta != 0) {
230 AdaptLineOffsets(lineNumber, delta);
231 }
232 //启用高亮显示分析 ----- 注:此时的 markLines 仅存储变化的相关行,而SetContent时存储的是所有行,因此只是按需分析
233 RunHighlighter();
234}
c, 文件字符的绘制究竟是在何处?
答:在TextView.cs中的public override void Paint(Graphics g, Rectangle rect)函数中,重要函数:PaintLinePart(),辅助绘制函数:DrawDocumentWord(), DrawBracketHighlight(), DrawSpaceMarker(), DrawVerticalRuler() 等

d, 括号匹配在何处被定义和捕捉更新?
答:TextArea.cs中的List bracketshemes = new List();变量存储在查找的匹配项,SearchMatchingBracket()方法中搜索并更新匹配项的显示,相关代码如下:


括号匹配
1//引自TextArea.cs
2List bracketshemes = new List();
3Caret caret;
4public TextArea(TextEditorControl motherTextEditorControl, TextAreaControl motherTextAreaControl)
5{
6 // 省略无关代码
7 bracketshemes.Add(new BracketHighlightingSheme('{', '}'));
8 bracketshemes.Add(new BracketHighlightingSheme('(', ')'));
9 bracketshemes.Add(new BracketHighlightingSheme('[', ']'));
10
11 caret.PositionChanged += new EventHandler(SearchMatchingBracket);
12 // 省略无关代码
13}
14void SearchMatchingBracket(object sender, EventArgs e)
15{
16 if (!TextEditorProperties.ShowMatchingBracket) {
17 textView.Highlight = null;
18 return;
19 }
20 bool changed = false;
21 if (caret.Offset == 0) {
22 if (textView.Highlight != null) {
23 int line = textView.Highlight.OpenBrace.Y;
24 int line2 = textView.Highlight.CloseBrace.Y;
25 textView.Highlight = null;
26 UpdateLine(line);
27 UpdateLine(line2);
28 }
29 return;
30 }
31 foreach (BracketHighlightingSheme bracketsheme in bracketshemes) {
32// if (bracketsheme.IsInside(textareapainter.Document, textareapainter.Document.Caret.Offset)) {
33 Highlight highlight = bracketsheme.GetHighlight(Document, Caret.Offset - 1);
34 if (textView.Highlight != null && textView.Highlight.OpenBrace.Y >=0 && textView.Highlight.OpenBrace.Y < Document.TotalNumberOfLines) {
35 //取消旧匹配项的高亮显示
36 UpdateLine(textView.Highlight.OpenBrace.Y);
37 }
38 if (textView.Highlight != null && textView.Highlight.CloseBrace.Y >=0 && textView.Highlight.CloseBrace.Y < Document.TotalNumberOfLines) {
39 //取消旧匹配项的高亮显示
40 UpdateLine(textView.Highlight.CloseBrace.Y);
41 }
42 textView.Highlight = highlight;
43 if (highlight != null) {
44 changed = true;
45 break;
46 }
47// }
48 }
49 if (changed || textView.Highlight != null) {
50 int line = textView.Highlight.OpenBrace.Y;
51 int line2 = textView.Highlight.CloseBrace.Y;
52 if (!changed) {
53 textView.Highlight = null;
54 }
55 //更新显示新匹配项 OpenBrace
56 UpdateLine(line);
57 //更新显示新匹配项 CloseBrace
58 UpdateLine(line2);
59 }
60}


7、待分析的部分
本篇讨论暂未涉及如下(有价值?)内容的分析:
SyntaxHighlighting实现分析
BookmarkManager, FoldingManager, FormattingManager等
Paint()处理中坐标分析及转换

8、总结
本篇分析对应的《Dissecting a C# Application Inside SharpDevelop》书中的章节:
Chapter 7: Internationalization
Chapter 8: Document Management
Chapter 9: Syntax Highlighting
Chapter 11: Writing the Editor Control

对于该Demo中尚未实现的代码折叠、字符串的查找/替换功能将在下个礼拜读了10,12,13,14章后作进一步分析

SharpDevelop浅析_4_TextEditor_自动完成、代码折叠……

Parser及其应用: Code Completion, Method Insight, Class Scout ...

可见新增功能如下(仅支持.cs文件):
a, 鼠标停留在方法、属性等位置时,会显示出相关的文档描述tooltip
b, 输入时支持自动完成
c, 编辑窗口顶部有类列表和成员(方法、变量等)列表下拉框用以快速浏览、定位
d, 编辑窗口左侧有折叠线用以方法、类等的代码折叠
相应的Demo工程中新增项目如下:
a, SharpEditor: 包含扩展TextEditor的控件, Dom结构, ParserService, 自动完成功能代码等
b, NRefactor : 代码解析功能
c, CSharpBinding: 对应 .cs 文件的具体实现支持

[题外话]:
关于代码解析(Parser)相关的代码,我没看懂,所以在这里只说个大概,更多地谈谈Parser的使用;抛砖引玉,希望有相关经验的网友提供详尽的分析。
前两周工作上的项目实施,每天都搞得比较累,所以这篇文章到现在才写了出来,明天是大年三十了,这个系列的文章也只剩下一篇Windows Form Designer,只能等过了年再放上来喽。
另外,这个系列写完后,暂不打算深究一些没明白的细节,接下来想看下os workflow 或 CommunityServer...



2、Parser实现概述
(1)首先,SharpEditor项目中的Dom下定义了以下重要类:
a, IDecoration及其子类: 代码表现的辅助对象,如IClass, IMethod, IProperty等
b, ResolveResult及其子类: 分析结果对象,如MethodResolveResult, TypeResolveResult等
c, 其它重要类: IExpressionFinder, IParser, IResolve, ICompilationUnit 等

(2)重要服务类:
ParserService: 提供 GetExpressionFinder(), Resolve(), ParseFile()等重要方法,相关重要类: ProjectContentRegistry, DefaultProjectContent, ReflectionProjectContent等
AmbienceService: 提供 IAmbience的实现类用以将分析结果转换为相应的字符描述

(3)Parser分析步骤:
以鼠标悬浮的Tooltip显示为例:DebuggerService根据文件类型返回对应的IExpressionFinder实现类,再根据鼠标位置找到并返回ExpressionResult对象,然后找到适当的IResolver实现类调用Resolve()方法返回结果ResolveResult对象,最后由相应的IAmbience实现类转换成结果字符,并调用e.ShowToolTip(toolTipText);显示。

(4)对于.NET默认类库的分析转换:
默认引进的命名空间的类结构和文档说明一般可以在"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\"目录下找到(如System.dll和System.xml),但是如果每次都要重新分析dll代码结构和xml注释显然是比较花费时间的,于是SharpDevelop采用的方式是将分析过的数据(Dom下的类结构表示数据?,二进制的.dat文件)存储到"C:\Documents and Settings\michael\Local Settings\Temp\SharpDevelop"下,代码结构存储到DomCacheDebug目录下,文档注释存储到DocumentationCacheDebug目录下。
首先在ParserService的CreateDefaultProjectContent()中加载默认命名空间的引用:

CreateDefaultProjectContent()
1static void CreateDefaultProjectContent()
2{
3 LoggingService.Info("Creating default project content");
4 LoggingService.Debug("Stacktrace is:\n" + Environment.StackTrace);
5 defaultProjectContent = new DefaultProjectContent();
6 defaultProjectContent.ReferencedContents.Add(ProjectContentRegistry.Mscorlib);
7 string[] defaultReferences = new string[] {
8 "System",
9 "System.Data",
10 "System.Drawing",
11 "System.Windows.Forms",
12 "System.XML",
13 "Microsoft.VisualBasic",
14 };
15 foreach (string defaultReference in defaultReferences)
16 {
17 //ReferenceProjectItem item = new ReferenceProjectItem(null, defaultReference);
18 IProjectContent pc = ProjectContentRegistry.GetProjectContentForReference(defaultReference);
19 if (pc != null)
20 {
21 defaultProjectContent.ReferencedContents.Add(pc);
22 }
23 }
24}
其中ProjectContentRegistry的GetProjectContentForReference()方法如下:

GetProjectContentForReference()
1public static IProjectContent GetProjectContentForReference(string Include)
2{
3 // 省略部分代码
4 Assembly assembly = GetDefaultAssembly(shortName);
5 ReflectionProjectContent pc;
6 if (assembly != null)
7 {
8 pc = DomPersistence.LoadProjectContentByAssemblyName(assembly.FullName);
9 if (pc == null)
10 {
11 pc = new ReflectionProjectContent(assembly);
12 DomPersistence.SaveProjectContent(pc);
13 }
14 }
15 else
16 {
17 pc = LoadProjectContent(itemFileName, itemInclude);
18 }
19 // 省略部分代码
20 return pc;
21}
22static Assembly GetDefaultAssembly(string shortName)
23{
24 // These assemblies are already loaded by SharpDevelop, so we don't need to load
25 // them in a separate AppDomain.
26 switch (shortName) {
27 case "System": // System != mscorlib !!!
28 return SystemAssembly;
29 case "System.Data":
30 return typeof(System.Data.DataException).Assembly;
31 case "System.Design":
32 return typeof(System.ComponentModel.Design.DesignSurface).Assembly;
33 case "System.DirectoryServices":
34 return typeof(System.DirectoryServices.AuthenticationTypes).Assembly;
35 case "System.Drawing":
36 return typeof(System.Drawing.Color).Assembly;
37 case "System.Web.Services":
38 return typeof(System.Web.Services.WebService).Assembly;
39 case "System.Windows.Forms":
40 return typeof(System.Windows.Forms.Control).Assembly;
41 case "System.Xml":
42 case "System.XML":
43 return typeof(XmlReader).Assembly;
44 case "Microsoft.Build.Engine":
45 return typeof(Microsoft.Build.BuildEngine.BuildSettings).Assembly;
46 case "Microsoft.Build.Framework":
47 return typeof(Microsoft.Build.Framework.LoggerVerbosity).Assembly;
48 default:
49 return null;
50 }
51}
可以看到DomPersistence类的作用即在加载或保存dll的代码结构数据, 如果尚未有分析过的数据,则在ReflectionProjectContnet类的构造函数中加以分析,同时调用XmlDoc类的相关方法加载、保存文档注释数据。

(5)对于文件的转换:
[略]



3、Parser应用: MouseHover Tooltip
注意在SharpEditor项目中有个DebuggerService类,它提供了一个重要方法如下:

BindTextAreaEvent()
1public static void BindTextAreaEvent(TextEditorControl control)
2{
3 TextArea textArea = control.ActiveTextAreaControl.TextArea;
4 //textArea.IconBarMargin.MouseDown += IconBarMouseDown;
5 textArea.ToolTipRequest -= TextArea_ToolTipRequest;
6 textArea.ToolTipRequest += TextArea_ToolTipRequest;
7}
此方法即用以绑定需要鼠标悬浮提示的TextEditor控件,在SharpPad项目的Open菜单类方法中调用此类绑定编辑控件。
注意上面的方法中可以看到ICSharpCode.TextEditor控件是通过其TextArea的ToolTipRequest事件公开给外部,用以分析并由外部提供ToolTip数据,而TextArea_ToolTipRequest(object sender, ToolTipRequestEventArgs e)方法中根据鼠标位置分析出要显示的数据后,最终调用e.ShowToolTip(toolTipText);用以设置提供数据。

TextArea_ToolTipRequest()
1static void TextArea_ToolTipRequest(object sender, ToolTipRequestEventArgs e)
2{
3 try
4 {
5 TextArea textArea = (TextArea)sender;
6 if (e.ToolTipShown) return;
7 if (oldToolTipControl != null && !oldToolTipControl.AllowClose) return;
8 if (!CodeCompletionOptions.TooltipsEnabled) return;
9
10 if (CodeCompletionOptions.TooltipsOnlyWhenDebugging)
11 {
12 if (currentDebugger == null) return;
13 if (!currentDebugger.IsDebugging) return;
14 }
15
16 if (e.InDocument)
17 {
18 Point logicPos = e.LogicalPosition;
19 IDocument doc = textArea.Document;
20 IExpressionFinder expressionFinder = ParserService.GetExpressionFinder(textArea.MotherTextEditorControl.FileName);
21 if (expressionFinder == null)
22 return;
23 LineSegment seg = doc.GetLineSegment(logicPos.Y);
24 if (logicPos.X > seg.Length - 1)
25 return;
26 string textContent = doc.TextContent;
27 ExpressionResult expressionResult = expressionFinder.FindFullExpression(textContent, seg.Offset + logicPos.X);
28 string expression = expressionResult.Expression;
29 if (expression != null && expression.Length > 0)
30 {
31 // Look if it is variable
32 ResolveResult result = ParserService.Resolve(expressionResult, logicPos.Y + 1, logicPos.X + 1, textArea.MotherTextEditorControl.FileName, textContent);
33 bool debuggerCanShowValue;
34 string toolTipText = GetText(result, expression, out debuggerCanShowValue);
35 DebuggerGridControl toolTipControl = null;
36 if (toolTipText != null)
37 {
38 if (Control.ModifierKeys == Keys.Control)
39 {
40 toolTipText = "expr: " + expressionResult.ToString() + "\n" + toolTipText;
41 }
42 else if (debuggerCanShowValue && currentDebugger != null)
43 {
44 toolTipControl = currentDebugger.GetTooltipControl(expressionResult.Expression);
45 toolTipText = null;
46 }
47 }
48 if (toolTipText != null)
49 {
50 e.ShowToolTip(toolTipText);
51 }
52 if (oldToolTipControl != null)
53 {
54 Form frm = oldToolTipControl.FindForm();
55 if (frm != null) frm.Close();
56 }
57 if (toolTipControl != null)
58 {
59 toolTipControl.ShowForm(textArea, logicPos);
60 }
61 oldToolTipControl = toolTipControl;
62 }
63 }
64 }
65 catch (Exception ex)
66 {
67 MessageBox.Show(ex.ToString());
68 }
69}

4、Parser应用: CodeCompletion & MethodInsight
ICSharpCode.TextEditor控件使用ICompletionData类以及ICompletionDataProvider接口(GenerateCompletionData()方法)定义数据自动完成列表的数据及其提供者: SharpEditor项目中的AbstractCodeCompletionDataProvider类中由GetExpression()及AddResolveResults()两个辅助方法生成数据,代码如下:

AbstractCodeCompletionDataProvider 类
1protected ArrayList completionData = null;
2//
3protected ExpressionResult GetExpression(TextArea textArea)
4{
5 IDocument document = textArea.Document;
6 IExpressionFinder expressionFinder = ParserService.GetExpressionFinder(fileName);
7 if (expressionFinder == null) {
8 return new ExpressionResult(TextUtilities.GetExpressionBeforeOffset(textArea, textArea.Caret.Offset));
9 } else {
10 ExpressionResult res = expressionFinder.FindExpression(document.GetText(0, textArea.Caret.Offset), textArea.Caret.Offset - 1);
11 if (overrideContext != null)
12 res.Context = overrideContext;
13 return res;
14 }
15}
16protected void AddResolveResults(ResolveResult results, ExpressionContext context)
17{
18 insertedElements.Clear();
19 insertedPropertiesElements.Clear();
20 insertedEventElements.Clear();
21
22 if (results != null) {
23 AddResolveResults(results.GetCompletionData(ParserService.CurrentProjectContent), context);
24 }
25}
26protected void AddResolveResults(ICollection list, ExpressionContext context)
27{
28 if (list == null) {
29 return;
30 }
31 completionData.Capacity += list.Count;
32 CodeCompletionData suggestedData = null;
33 foreach (object o in list) {
34 if (context != null && !context.ShowEntry(o))
35 continue;
36 CodeCompletionData ccd = CreateItem(o, context);
37 if (object.Equals(o, context.SuggestedItem))
38 suggestedData = ccd;
39 if (ccd != null && !ccd.Text.StartsWith("___"))
40 completionData.Add(ccd);
41 }
42 if (context.SuggestedItem != null) {
43 if (suggestedData == null) {
44 suggestedData = CreateItem(context.SuggestedItem, context);
45 if (suggestedData != null) {
46 completionData.Add(suggestedData);
47 }
48 }
49 if (suggestedData != null) {
50 completionData.Sort();
51 this.DefaultIndex = completionData.IndexOf(suggestedData);
52 }
53 }
54}
CommentCompletionDataProvider类提供注释的自动完成,相关代码如下:

CommentCompletionDataProvider 类
1string[][] commentTags = new string[][] {
2 new string[] {"c", "marks text as code"},
3 new string[] {"code", "marks text as code"},
4 new string[] {"example", "A description of the code example\n(must have a tag inside)"},
5 new string[] {"exception cref=\"\"", "description to an exception thrown"},
6 new string[] {"list type=\"\"", "A list"},
7 new string[] {"listheader", "The header from the list"},
8 new string[] {"item", "A list item"},
9 new string[] {"term", "A term in a list"},
10 new string[] {"description", "A description to a term in a list"},
11 new string[] {"para", "A text paragraph"},
12 new string[] {"param name=\"\"", "A description for a parameter"},
13 new string[] {"paramref name=\"\"", "A reference to a parameter"},
14 new string[] {"permission cref=\"\"", ""},
15 new string[] {"remarks", "Gives description for a member"},
16 new string[] {"include file=\"\" path=\"\"", "Includes comments from other files"},
17 new string[] {"returns", "Gives description for a return value"},
18 new string[] {"see cref=\"\"", "A reference to a member"},
19 new string[] {"seealso cref=\"\"", "A reference to a member in the seealso section"},
20 new string[] {"summary", "A summary of the object"},
21 new string[] {"value", "A description of a property"}
22};
23public override ICompletionData[] GenerateCompletionData(string fileName, TextArea textArea, char charTyped)
24{
25 caretLineNumber = textArea.Caret.Line;
26 caretColumn = textArea.Caret.Column;
27 LineSegment caretLine = textArea.Document.GetLineSegment(caretLineNumber);
28 string lineText = textArea.Document.GetText(caretLine.Offset, caretLine.Length);
29 if (!lineText.Trim().StartsWith("///") && !lineText.Trim().StartsWith("'''")) {
30 return null;
31 }
32
33 ArrayList completionData = new ArrayList();
34 foreach (string[] tag in commentTags) {
35 completionData.Add(new CommentCompletionData(tag[0], tag[1]));
36 }
37 return (ICompletionData[])completionData.ToArray(typeof(ICompletionData));
38}
类似地,ICSharpCode.TextEditor控件提供了IInsightDataProvider接口(GetInsightData()方法),SharpEditor项目中实现类MethodInsightDataProvider和IndexerInsightDataProvider提供相关数据。

除了上面需要提供数据的类外,还要绑定TextEditor的TextArea的KeyEventHandler事件(详见SharpEditor项目的SharpDevelopTextAreaControl.cs),其中辅助类的处理代码如下:

KeyEventHandler 事件响应
1// 默认处理 DefaultCodeCompletionBinding(详见SharpEditor项目的CodeCompletionBinding.cs):
2public virtual bool HandleKeyPress(SharpDevelopTextAreaControl editor, char ch)
3{
4 switch (ch)
5 {
6 case '(':
7 if (enableMethodInsight && CodeCompletionOptions.InsightEnabled)
8 {
9 editor.ShowInsightWindow(new MethodInsightDataProvider());
10 return true;
11 }
12 else
13 {
14 return false;
15 }
16 case '[':
17 if (enableIndexerInsight && CodeCompletionOptions.InsightEnabled)
18 {
19 editor.ShowInsightWindow(new IndexerInsightDataProvider());
20 return true;
21 }
22 else
23 {
24 return false;
25 }
26 case '<':
27 if (enableXmlCommentCompletion)
28 {
29 editor.ShowCompletionWindow(new CommentCompletionDataProvider(), ch);
30 return true;
31 }
32 else
33 {
34 return false;
35 }
36 case '.':
37 if (enableDotCompletion)
38 {
39 editor.ShowCompletionWindow(new CodeCompletionDataProvider(), ch);
40 return true;
41 }
42 else
43 {
44 return false;
45 }
46 case ' ':
47 if (!CodeCompletionOptions.KeywordCompletionEnabled)
48 return false;
49 string word = editor.GetWordBeforeCaret();
50 if (word != null)
51 return HandleKeyword(editor, word);
52 else
53 return false;
54 default:
55 return false;
56 }
57}
58// .cs 文件的处理(详见CSharpBinding项目的CSharpCompletionBinding.cs):
59public override bool HandleKeyPress(SharpDevelopTextAreaControl editor, char ch)
60{
61 Parser.ExpressionFinder ef = new Parser.ExpressionFinder(editor.FileName);
62 int cursor = editor.ActiveTextAreaControl.Caret.Offset;
63 ExpressionContext context = null;
64 if (ch == '(')
65 {
66 if (CodeCompletionOptions.KeywordCompletionEnabled)
67 {
68 switch (editor.GetWordBeforeCaret().Trim())
69 {
70 case "for":
71 case "lock":
72 context = ExpressionContext.Default;
73 break;
74 case "using":
75 context = ExpressionContext.TypeDerivingFrom(ReflectionReturnType.Disposable.GetUnderlyingClass(), false);
76 break;
77 case "catch":
78 context = ExpressionContext.TypeDerivingFrom(ReflectionReturnType.Exception.GetUnderlyingClass(), false);
79 break;
80 case "foreach":
81 case "typeof":
82 case "sizeof":
83 case "default":
84 context = ExpressionContext.Type;
85 break;
86 }
87 }
88 if (context != null)
89 {
90 if (IsInComment(editor)) return false;
91 editor.ShowCompletionWindow(new CtrlSpaceCompletionDataProvider(context), ch);
92 return true;
93 }
94 else if (EnableMethodInsight && CodeCompletionOptions.InsightEnabled)
95 {
96 editor.ShowInsightWindow(new MethodInsightDataProvider());
97 return true;
98 }
99 return false;
100 }
101 else if (ch == '[')
102 {
103 LineSegment line = editor.Document.GetLineSegmentForOffset(cursor);
104 if (TextUtilities.FindPrevWordStart(editor.Document, cursor) <= line.Offset)
105 {
106 // [ is first character on the line
107 // -> Attribute completion
108 editor.ShowCompletionWindow(new AttributesDataProvider(), ch);
109 return true;
110 }
111 }
112 else if (ch == ',' && CodeCompletionOptions.InsightRefreshOnComma && CodeCompletionOptions.InsightEnabled)
113 {
114 // Show MethodInsightWindow or IndexerInsightWindow
115 string documentText = editor.Text;
116 int oldCursor = cursor;
117 string textWithoutComments = ef.FilterComments(documentText, ref cursor);
118 int commentLength = oldCursor - cursor;
119 if (textWithoutComments != null)
120 {
121 Stack parameters = new Stack();
122 char c = '\0';
123 while (cursor > 0)
124 {
125 while (--cursor > 0 &&
126 ((c = textWithoutComments[cursor]) == ',' ||
127 char.IsWhiteSpace(c))) ;
128 if (c == '(')
129 {
130 ShowInsight(editor, new MethodInsightDataProvider(cursor + commentLength, true), parameters, ch);
131 return true;
132 }
133 else if (c == '[')
134 {
135 ShowInsight(editor, new IndexerInsightDataProvider(cursor + commentLength, true), parameters, ch);
136 return true;
137 }
138 string expr = ef.FindExpressionInternal(textWithoutComments, cursor);
139 if (expr == null || expr.Length == 0)
140 break;
141 parameters.Push(ParserService.Resolve(new ExpressionResult(expr),
142 editor.ActiveTextAreaControl.Caret.Line,
143 editor.ActiveTextAreaControl.Caret.Column,
144 editor.FileName,
145 documentText));
146 cursor = ef.LastExpressionStartPosition;
147 }
148 }
149 }
150 else if (ch == '=')
151 {
152 LineSegment curLine = editor.Document.GetLineSegmentForOffset(cursor);
153 string documentText = editor.Text;
154 int position = editor.ActiveTextAreaControl.Caret.Offset - 2;
155
156 if (position > 0 && (documentText[position + 1] == '+'))
157 {
158 ExpressionResult result = ef.FindFullExpression(documentText, position);
159
160 if (result.Expression != null)
161 {
162 ResolveResult resolveResult = ParserService.Resolve(result, editor.ActiveTextAreaControl.Caret.Line, editor.ActiveTextAreaControl.Caret.Column, editor.FileName, documentText);
163 if (resolveResult != null && resolveResult.ResolvedType != null)
164 {
165 IClass underlyingClass = resolveResult.ResolvedType.GetUnderlyingClass();
166 if (underlyingClass != null && underlyingClass.IsTypeInInheritanceTree(ProjectContentRegistry.Mscorlib.GetClass("System.MulticastDelegate")))
167 {
168 EventHandlerCompletitionDataProvider eventHandlerProvider = new EventHandlerCompletitionDataProvider(result.Expression, resolveResult);
169 eventHandlerProvider.InsertSpace = true;
170 editor.ShowCompletionWindow(eventHandlerProvider, ch);
171 }
172 }
173 }
174 }
175 }
176 else if (ch == ';')
177 {
178 LineSegment curLine = editor.Document.GetLineSegmentForOffset(cursor);
179 // don't return true when inference succeeds, otherwise the ';' won't be added to the document.
180 TryDeclarationTypeInference(editor, curLine);
181 }
182
183 return base.HandleKeyPress(editor, ch);
184}

5、Parser应用: QuickClassBrowserPanel
QuickClassBrowserPanel即编辑窗口顶部的类及成员快速浏览窗口,其实现如下:
首先在SharpDevelopTextAreaControl类中重写override void OnFileNameChanged(EventArgs e)方法,即打开新文件时,调用ActivateQuickClassBrowserOnDemand()来根据是否有能够分析此文件的Parser来决定是否显示控件,如果可以显示,则实例化出一个新对象,并添加进来。实例化QuickClassBrowserPanel对象的相关代码如下:

QuickClassBrowserPanel
1private System.Windows.Forms.ComboBox classComboBox;
2private System.Windows.Forms.ComboBox membersComboBox;
3ICompilationUnit currentCompilationUnit;
4//
5public QuickClassBrowserPanel(SharpDevelopTextAreaControl textAreaControl)
6{
7 InitializeComponent();
8 this.membersComboBox.MaxDropDownItems = 20;
9
10 base.Dock = DockStyle.Top;
11 this.textAreaControl = textAreaControl;
12 this.textAreaControl.ActiveTextAreaControl.Caret.PositionChanged += new EventHandler(CaretPositionChanged);
13}
14void CaretPositionChanged(object sender, EventArgs e)
15{
16 // ignore simple movements
17 if (e != EventArgs.Empty) {
18 return;
19 }
20 try {
21
22 ParseInformation parseInfo = ParserService.GetParseInformation(textAreaControl.FileName);
23 if (parseInfo != null) {
24 if (currentCompilationUnit != (ICompilationUnit)parseInfo.MostRecentCompilationUnit) {
25 currentCompilationUnit = (ICompilationUnit)parseInfo.MostRecentCompilationUnit;
26 if (currentCompilationUnit != null) {
27
28 FillClassComboBox(true);
29 FillMembersComboBox();
30 }
31 }
32 UpdateClassComboBox();
33 UpdateMembersComboBox();
34 }
35 } catch (Exception ex) {
36 MessageService.ShowError(ex);
37 }
38}
可以看到类、成员列表的ComboBox数据的填充是在textAreaControl.ActiveTextAreaControl.Caret.PositionChanged事件后执行,因为光标位置的改变可能需要同步更新顶部列表框的显示;注意两个列表的列表项使用自定义类ComboBoxItem, 包括成员的Line, Column等信息;该类中绑定两个列表的SelectedIndexChange事件用以在编辑器中定位相应的类或成员位置:

ComboBoxSelectedIndexChanged()
1void ComboBoxSelectedIndexChanged(object sender, System.EventArgs e)
2{
3 ComboBox comboBox = (ComboBox)sender;
4 if (autoselect) {
5 ComboBoxItem item = (ComboBoxItem)comboBox.Items[comboBox.SelectedIndex];
6 if (item.IsInCurrentPart)
7 {
8 textAreaControl.ActiveTextAreaControl.Caret.Position = new Point(item.Column, item.Line);
9 textAreaControl.ActiveTextAreaControl.TextArea.Focus();
10 }
11 }
12}

6、Parser应用: Folding
ICSharpCode.TextEditor控件提供了IFoldingStrategy接口用以定义代码折叠需实现的方法:

IFoldingStrategy 接口
1/**////
2/// This interface is used for the folding capabilities
3/// of the textarea.
4///

5public interface IFoldingStrategy
6{
7 /**////
8 /// Calculates the fold level of a specific line.
9 ///

10 List GenerateFoldMarkers(IDocument document, string fileName, object parseInformation);
11}
SharpEditor项目中,SharpDevelopTextAreaControl在构造函数中指明了FoldingStrategy:

SharpDevelopTextAreaControl 构造函数
1public SharpDevelopTextAreaControl()
2{
3 Document.FoldingManager.FoldingStrategy = new ParserFoldingStrategy();
4
5 // 由于UpdateClassMemberBookmarks()方法中以Bookmark形式显示方法、属性、变量等
6 // 故此处需要定义Bookmark Factory 以使上面的Bookmark 与用户加入的书签区别开
7 Document.BookmarkManager.Factory = new Bookmarks.SDBookmarkFactory(Document.BookmarkManager);
8}
9protected override void OnFileNameChanged(EventArgs e)
10{
11 base.OnFileNameChanged(e);
12 ActivateQuickClassBrowserOnDemand();
13 ForceFoldingUpdate();
14}
15void ForceFoldingUpdate()
16{
17 ParseInformation parseInfo = ParserService.GetParseInformation(FileName);
18 if (parseInfo == null)
19 {
20 parseInfo = ParserService.ParseFile(FileName, Document.TextContent, false, false);
21 }
22 Document.FoldingManager.UpdateFoldings(FileName, parseInfo);
23
24 // 在编辑器的类、属性、变量等行左侧显示相应图标
25 UpdateClassMemberBookmarks(parseInfo);
26}
同样需要在打开新文件时调用更新代码折叠标签,代码折叠FoldMarker生成的具体实现详见SharpEditor项目的ParserFoldingStrategy.cs


7、总结
上面简要介绍了Parser的部分实现, 期待有相关经验的网友指定我的理解错误或给予补充解释。
以及Parser的部分应用:MouseHover Tooltip, CodeCompletion & MethodInsight, QuickClassBrowserPanel, Folding;SharpDevelop中的其它应用如类视图ClassBrowser, 对象浏览AssemblyPad(SharpDevelop2.0源码中未见到?).

SharpDevelop浅析_5_Windows Forms Designer
自己动手创建应用程序界面设计器

功能概述:
a, 窗体左侧为工具栏,可以单击、双击、拖曳的方式来添加控件
b, 窗体右侧为属性(事件)窗口及控件(浏览)选择Combo
c, 窗体中部包括设计器及代码查看Tab页
d, 窗体顶部实现了编辑、对齐、运行等命令项

2、Demo实现简述
a, 设计器的核心是.NET框架提供的DesignSurface类、ServiceContainer类
b, 设计器的扩展点(自定义部分)是通过向ServiceContainer添加自动定义服务类(IOC模式?)或订阅服务类的事件……
c, Demo中的使用或创建的服务类包含了ISelectionService, IComponentChangeService MenuCommandService, CustomToolboxService, NameCreationService, DesignerEventService, EventBindingService.
d, 补充说明: Demo左侧工具栏项目是通过\data\SharpDevelopControlLibrary.xml配置文件定义;窗口中部的代码查看页中使用了SharpDevelop的TextEditorControl.

3、参考资料
关于自定义窗体设计器的具体设计过程请参照如下资源:

Create And Host Custom Designers With The .NET Framework 2.0
http://msdn.microsoft.com/msdnmag/issues/06/03/DesignerHosting/default.aspx

利用 .NET Framework 2.0 创建并宿主自定义的设计
http://www.microsoft.com/china/MSDN/library/netFramework/netframework/ DesignerHosting.mspx?mfr=true

Hosting Windows Forms Designers
http://www.divil.co.uk/net/articles/designers/hosting.asp

沧海月明 "写Form设计器尝试"系列
http://www.cnblogs.com/panjiwen/category/36995.html

Demo中左侧工具栏的控件使用了 纶巾客 写的一个控件:制作VS风格的Toolbox控件
http://www.cnblogs.com/guanjinke/archive/2007/01/10/617092.html

对该控件的主要修改如下:
ToolBoxItem类添加了Tag及Image属性以存储Tag数据及显示图片
ToolBox增加了SelectedItemChanged, ItemDoubleClicked, ItemDragStart事件

This page is powered by Blogger. Isn't yours?