Storm - .NET 的世界最佳 IDE 框架
使用 Storm,您可以轻松地创建快速、灵活且可扩展的 IDE 应用程序——几乎不需要编写任何代码!
引言
我目前正在开发一个名为 Moonlite 的 IDE 应用程序。当我开始编码时,我寻找现成的、用于创建 IDE 的优秀代码。我什么都没找到。所以,当我快要完成它的时候(我还没有完成,因为这个框架占用了我所有的时间),我觉得让每个人在制作这样的应用程序时都要经历同样的过程很不公平。(我花了 8 个月的时间——每天大约 7-10 小时——来创建它。不是因为主要编码需要这么长时间,而是因为我必须弄清楚如何做到这一点以及什么才是最有效的解决方案。)所以我开始了 Storm,现在它完成了,我要向你们展示!:)
注意事项
请注意,我这是出于我自己的意愿和空闲时间完成的。如果您能尊重我以及我的工作,并在评论中提出如何改进、报告错误等建议,我将非常高兴。感谢您的时间 :)
Using the Code
使用代码是一个简单的任务;只需将您想要的控件引用到工具箱中,然后拖放即可,这样就完成了。但是,对于那些想要更深入教程的人,请转到程序包中的“doc”文件夹并打开“index.htm”。
工作原理
在本章中,我将主要介绍停靠、插件和 TextEditor,因为它们是最先进的。我将不介绍 Win32 和 TabControl。
代码补全
CodeCompletion
依赖于 TextEditor,它并没有一些人想象的那么高级。它只是一个包含可以绘制图标的通用 ListBox 的控件。ListBox 的项目由 CodeCompletion
本身管理。CodeCompletion
处理 TextEditor
的 KeyUp
事件,并在其中根据用户在 TextEditor
中键入的内容显示 ListBox 的成员。
更新:CodeCompletion
现在已包含在 TextEditor 库中!
每次检测到按键时,它都会通过调用 GetLastWord()
方法来更新一个包含当前键入字符串的字符串,该方法返回用户当前所在的单词。字符串如何分割成单词由 TextEditor
中的“分隔符”定义。每次调用 GetLastWord()
时,CodeCompletion
会调用本机 Win32 函数 'LockWindowUpdate
' 以及父 TextEditor
的句柄,以防止在操作系统渲染 TextEditor
/CodeCompletion
时出现闪烁。
实际上,当 CodeCompletion
自动完成子 GListBox
中选定的项目时,它也会这样做。每次 CodeCompletion
检测到它不识别为“有效”字符的按键(任何非字母/数字字符,除非是 _),它都会调用 SelectItem()
方法以及一个特定的 CompleteType
。
那么,什么是 CompleteType
?您会看到,CompleteType
定义了 SelectItem()
在自动完成 GListBox
中选定的项目时如何操作。有两种模式——Normal
和 Parenthesis
。当使用 Normal
时,SelectItem()
方法会删除整个当前键入的单词;然而,Parenthesis
会删除整个当前键入的单词但第一个字母除外。这可能看起来很奇怪,但在例如用户键入了起始括号时是必需的。您有时也可能发现自动完成的单词不正确——这时您应该使用 Parenthesis
而不是 Normal
作为 CompleteType
。(您可以在将成员项添加到 CodeCompletion
时定义自定义 CompleteType
。)
由于用户自己定义了成员项的工具提示,因此显示项目描述非常容易。当 GListBox
中选择了一个新项目时,一个方法会更新当前显示的 ToolTip
以匹配所选项目的描述/声明字段。由于普通的 TreeNode
/ListBoxItem
无法拥有多个 Tag
,因此我创建了 GListBoxItem
,它还包含一个 ImageIndex
用于父 GListBox
的 ImageList
。GListBoxItem
包含许多由用户在初始化时或通过属性设置的值。
每次显示控件本身或其工具提示时,都会更新它们的位置。工具提示的公式是:对于 Y
,公式为 Y = CaretPosition.Y + FontHeight * CaretIndex + Math.Ceiling(FontHeight + 2)
。X
的设置很简单,为 CaretPositon.X + 100 + CodeCompletion.Width + 2
。CodeCompletion
的 Y
公式与工具提示相同;但是,X
不同;X = CaretPosition.X + 100
。
停靠
首先,我将从一个类图开始,以帮助我
正如您所见,有很多类。一个 DockPane
可以包含 DockPanel
s,而 DockPanel
s 是停靠在 DockPane
内的面板。一个 DockPanel
包含一个 Form
、一个 DockCaption
和一个 DockTab
。当设置了 DockPanel
的 Form
属性时,DockPanel
会更新 Form
以匹配其作为停靠窗体所需的设置。
DockCaption
是一个自定义绘制的面板。它包含两个图标——OptionsGlyph
和 CloseGlyph
——它们都继承了 Glyph
类,该类包含一般图标的渲染逻辑。OptionsGlyph
和 CloseGlyph
包含具有透明背景的图像。很多人使用非常复杂的解决方案来处理这个问题;然而,我发现了一个非常、非常简单而简短的解决方案
/// <summary>
/// Represents an image with a transparent background.
/// </summary>
[ToolboxItem(false)]
public class TransImage
: Panel
{
#region Properties
/// <summary>
/// Gets or sets the image of the TransImage.
/// </summary>
public Image Image
{
get { return this.BackgroundImage; }
set
{
if (value != null)
{
Bitmap bitmap = new Bitmap(value);
bitmap.MakeTransparent();
this.BackgroundImage = bitmap;
Size = bitmap.Size;
}
}
}
#endregion
/// <summary>
/// Initializes a new instance of TransImage.
/// </summary>
/// <param name="image">Image that should
/// have a transparent background.</param>
public TransImage(Image image)
{
// Set styles to enable transparent background
this.SetStyle(ControlStyles.Selectable, false);
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.Transparent;
this.Image = image;
}
}
就这么简单。一个具有透明背景的基本面板,当然还有一个 Image
属性——Bitmap.MakeTransparent()
会完成其余的工作。Panel
确实是一个可爱的控件。在我们继续讨论这篇文章时,您会发现我的许多控件都基于 Panel
。
好吧,DockCaption
负责 DockPanel
的取消停靠和 DockForm
的移动。是的,DockForm
。当一个 DockPanel
从其 DockPane
容器中取消停靠时,会创建一个 DockForm
,并将 DockPanel
添加到其中。DockForm
是一个自定义绘制的窗体,可以调整大小和移动,看起来很像 Visual Studio 2010 的停靠窗体。
由于 DockForm
的标题栏已被移除,因此 DockCaption
负责移动。这就是 Win32 碍事的地方——SendMessage
和 ReleaseCapture
被用来完成这项工作。
当一个 DockPanel
添加到一个 DockPane
中,并且该面板的侧边已经有一个 DockPanel
停靠时,用户想要将新的 DockPanel
停靠在那里,DockPane
会使用已停靠的 DockPanel
的 DockTab
将新的 DockPanel
添加为 TabPage
。然后用户可以在 DockPanel
之间切换。
DockTab
继承自普通的 TabControl
,并重写了它的绘图方法。这意味着它对用户完全可定制,对我们来说也非常灵活。
插件
插件库是较短的一个;然而,它可能是最复杂的。由于 PluginManager
类必须定位动态链接库,因此我们应该检查它们是否是实际的插件,检查它们是否使用了可选的 plugin
属性,如果使用了,就将找到的信息存储在 IPlugin
中,如果它是 UserControl,就将找到的插件添加到用户提供的窗体中。
所以基本上,这些过程大部分都发生在 LoadPlugins
方法中。然而,LoadPlugins
方法只是一个包装器,它调用 LoadPluginsInDirectory
并将 PluginsPath
设置为用户指定的路径。现在,LoadPluginsInDirectory
方法会遍历特定文件夹中的所有文件,检查它们的文件扩展名是否为“.dll”(这表明该文件是代码库),然后开始整个“检查库是否包含插件以及插件是否有任何属性”的过程。
这是使用位于 System.Reflection
命名空间中的 Assembly
类完成的。
Assembly a = Assembly.LoadFile(file);
然后,声明一个 System.Type
数组,并将其设置为 a.GetTypes()
。这为我们提供了程序集中所有类型(类、枚举、接口等)的数组。然后,我们可以遍历 Type
数组中的每个 Type
,并使用这个小技巧来检查它是否是实际的插件。
(t.IsSubclassOf(typeof(IPlugin)) == true ||
t.GetInterfaces().Contains(typeof(IPlugin)) == true)
是的——很简单——这绝对不会出错。嗯,我们都知道接口不能像普通类一样初始化。所以,取而代之的是,我们使用 System.Activator
类的 CreateInstance
方法。
IPlugin currentPlugin = (IPlugin)Activator.CreateInstance(t);
搞定。我们就像初始化一个普通类一样初始化了一个接口。很方便,对吧?现在,我们只需要设置初始化接口的属性,使其与 PluginManager
的选项和当前环境匹配。插件的创建者可以使用此功能来创建更具交互性的插件。完成这些之后,我们将 IPlugin
添加到 PluginManager
的已加载插件列表中。
然而,由 PluginManager
加载的插件默认情况下并非启用。这时就需要用户采取一些操作。用户必须遍历 PluginManager.LoadedPlugins
列表中的所有 IPlugin
,并对其调用 PluginManager.EnablePlugin(plugin)
方法。
现在,如果您在应用程序中有一个例如管理插件的窗体,就像 Firefox 那样,您可以使用 PluginManager.GetPluginAttribute
方法来获取一个包含插件信息的属性,前提是插件的创建者提供了这些信息。
它的工作方式是创建一个 object
数组,并将其设置为 System.Type
方法 GetCustomAttributes()
。变量“type
”被设置为插件的 Type
属性,该属性在加载插件时设置。
object[] pAttributes = type.GetCustomAttributes(typeof(Plugin), false);
将其添加到插件列表中
attributes.Add(pAttributes[0] as Plugin);
然后,当我们完成循环后,我们将最终返回找到的属性列表。
TextEditor
由于我喜欢我的 TextEditor,我将为您提供一个关于它功能的小预览。而且它的功能可不少;)
正如您可能已经想象到的,这个库拥有极其众多的类/枚举/接口/命名空间。事实上,数量之多,以至于我不会放上类图或解释所有类之间的联系。
TextEditor 本质上只是 TextEditorBase
类的容器;实际上是 TextEditorBase
包含了在 TextEditor 中进行操作的所有逻辑。TextEditor 只管理其四个 TextEditorBase
以及在您将 TextEditor 分割成两个或多个视图时使用的分割器。
然而,TexteditorBase
并不负责绘制;它只包含一个 DefaultPainter
字段,该字段包含渲染所有不同内容的逻辑。每当需要绘制时,TextEditorBase
就会调用 DefaultPainter
中的相应渲染方法。DefaultPainter
还包含一个名为 RenderAll
的方法,正如您可能已经想到的,该方法会渲染所有应该在 TextEditor 中渲染的内容。
由于不同的高亮模式是在 XML 文件中定义的,因此需要一个 XML 文件读取器。LanguageReader
会解析给定的 XML 文件,并告诉解析器如何解析在 TextEditor 中键入的文本中找到的每个标记。用户不会直接使用 LanguageReader
;用户可以使用 TextEditor 的 SetHighlighting
方法(这是一个包装器),或者使用 TextEditorSyntaxLoader.SetSyntax
方法。
不幸的是,我不能完全归功于我。我基于 DotNetFireball 的 CodeEditor;然而,代码非常混乱、效率低下且结构混乱,以至于重写它可能比修复所有这些问题花费的时间更少。代码仍然不太好;然而,它肯定比以前好。
更新:由于我现已完成了所有源代码的审查,并进行了文档记录和更新以符合我的标准,因此我声称这个 TextEditor 是我的原创作品。然而,我的做事方式仍然与原始版本相同,因此我将功劳归于原作者。
我应该提一下,DotNetFireball并没有创建 CodeEditor。他们只是采用了另一个组件 SyntaxBox,并更改了它的名称。仅供您参考。
结论
所以,正如您所见(或者我当然希望您能看到),这是一个庞大的项目,难以管理,我给您一个建议:不要在家尝试。它花费了我如此多的时间,我并不后悔,但说真的,如果已经存在这样的框架,为什么不使用它呢?制作自己的会很逊。
我并不是因为自己的过错而这么说,以便获得更多用户,而是因为我不想让您经历我为实现这些“基本”(并非真的基本,而是现代用户要求应用程序拥有的功能)功能所经历的同样过程。
所以是的,我想这就是全部了。您可以在这里说“享受”,留下最后的说明等等。是的,享受它,好好利用它——让我看到一些用它制作的精彩应用程序,拜托 :)
计划更新
- 为 Storm.Docking 添加自动隐藏功能。
- 为 Storm.Docking 添加设计器支持。
- 从头重写
TabStrip
。
历史
- v1.1.0.2
- [文本编辑器]
- [特性]
- 添加了
AutomaticLanguageDetection
属性 - 为正则表达式解析添加了更多功能
- 为通过“
CurrentLanguage
”属性选择的语法高亮语言添加了设计时支持 - 为 CodeCompletion 添加了更多扩展性
- 将样式从类似 Visual Studio 2008 的样式更改为类似 Visual Studio 2010 的样式
- 在语言定义文件中实现了继承
- 添加了
- [错误修复]
- 修复了解析中的许多小错误
- 修复了一个导致解析器为给定关键字找到错误样式的 bug
- 修复了语言定义读取中的许多错误
- [其他]
- 彻底审查了所有源代码文件中的代码
- 为所有源代码编写了文档
- 将 CodeCompletion 移至 TextEditor 的库中
- [特性]
- [停靠]
- [特性]
- [错误修复]
- 修复了一个导致停靠窗口冻结且无法拖动的重大 bug
- [其他]
- [插件]
- [特性]
- 添加了加载单个插件的功能
- [错误修复]
- [其他]
- [特性]
- [文本编辑器]
- v1.0.0.0
- 首次发布