65.9K
CodeProject 正在变化。 阅读更多。
Home

Storm - .NET 的世界最佳 IDE 框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (76投票s)

2009年11月16日

LGPL3

11分钟阅读

viewsIcon

306450

downloadIcon

6582

使用 Storm,您可以轻松地创建快速、灵活且可扩展的 IDE 应用程序——几乎不需要编写任何代码!

Moonlite,我用 Storm 创建的 IDE(仍在开发中)

引言

我目前正在开发一个名为 Moonlite 的 IDE 应用程序。当我开始编码时,我寻找现成的、用于创建 IDE 的优秀代码。我什么都没找到。所以,当我快要完成它的时候(我还没有完成,因为这个框架占用了我所有的时间),我觉得让每个人在制作这样的应用程序时都要经历同样的过程很不公平。(我花了 8 个月的时间——每天大约 7-10 小时——来创建它。不是因为主要编码需要这么长时间,而是因为我必须弄清楚如何做到这一点以及什么才是最有效的解决方案。)所以我开始了 Storm,现在它完成了,我要向你们展示!:)

注意事项

请注意,我这是出于我自己的意愿和空闲时间完成的。如果您能尊重我以及我的工作,并在评论中提出如何改进、报告错误等建议,我将非常高兴。感谢您的时间 :)

Using the Code

使用代码是一个简单的任务;只需将您想要的控件引用到工具箱中,然后拖放即可,这样就完成了。但是,对于那些想要更深入教程的人,请转到程序包中的“doc”文件夹并打开“index.htm”。

工作原理

在本章中,我将主要介绍停靠、插件和 TextEditor,因为它们是最先进的。我将不介绍 Win32 和 TabControl。

代码补全

CodeCompletion 依赖于 TextEditor,它并没有一些人想象的那么高级。它只是一个包含可以绘制图标的通用 ListBox 的控件。ListBox 的项目由 CodeCompletion 本身管理。CodeCompletion 处理 TextEditorKeyUp 事件,并在其中根据用户在 TextEditor 中键入的内容显示 ListBox 的成员。

更新CodeCompletion 现在已包含在 TextEditor 库中!

每次检测到按键时,它都会通过调用 GetLastWord() 方法来更新一个包含当前键入字符串的字符串,该方法返回用户当前所在的单词。字符串如何分割成单词由 TextEditor 中的“分隔符”定义。每次调用 GetLastWord() 时,CodeCompletion 会调用本机 Win32 函数 'LockWindowUpdate' 以及父 TextEditor 的句柄,以防止在操作系统渲染 TextEditor/CodeCompletion 时出现闪烁。

实际上,当 CodeCompletion 自动完成子 GListBox 中选定的项目时,它也会这样做。每次 CodeCompletion 检测到它不识别为“有效”字符的按键(任何非字母/数字字符,除非是 _),它都会调用 SelectItem() 方法以及一个特定的 CompleteType

那么,什么是 CompleteType?您会看到,CompleteType 定义了 SelectItem() 在自动完成 GListBox 中选定的项目时如何操作。有两种模式——NormalParenthesis。当使用 Normal 时,SelectItem() 方法会删除整个当前键入的单词;然而,Parenthesis 会删除整个当前键入的单词但第一个字母除外。这可能看起来很奇怪,但在例如用户键入了起始括号时是必需的。您有时也可能发现自动完成的单词不正确——这时您应该使用 Parenthesis 而不是 Normal 作为 CompleteType。(您可以在将成员项添加到 CodeCompletion 时定义自定义 CompleteType。)

由于用户自己定义了成员项的工具提示,因此显示项目描述非常容易。当 GListBox 中选择了一个新项目时,一个方法会更新当前显示的 ToolTip 以匹配所选项目的描述/声明字段。由于普通的 TreeNode/ListBoxItem 无法拥有多个 Tag,因此我创建了 GListBoxItem,它还包含一个 ImageIndex 用于父 GListBoxImageListGListBoxItem 包含许多由用户在初始化时或通过属性设置的值。

每次显示控件本身或其工具提示时,都会更新它们的位置。工具提示的公式是:对于 Y,公式为 Y = CaretPosition.Y + FontHeight * CaretIndex + Math.Ceiling(FontHeight + 2)X 的设置很简单,为 CaretPositon.X + 100 + CodeCompletion.Width + 2CodeCompletionY 公式与工具提示相同;但是,X 不同;X = CaretPosition.X + 100

停靠

首先,我将从一个类图开始,以帮助我

正如您所见,有很多类。一个 DockPane 可以包含 DockPanels,而 DockPanels 是停靠在 DockPane 内的面板。一个 DockPanel 包含一个 Form、一个 DockCaption 和一个 DockTab。当设置了 DockPanelForm 属性时,DockPanel 会更新 Form 以匹配其作为停靠窗体所需的设置。

DockCaption 是一个自定义绘制的面板。它包含两个图标——OptionsGlyphCloseGlyph——它们都继承了 Glyph 类,该类包含一般图标的渲染逻辑。OptionsGlyphCloseGlyph 包含具有透明背景的图像。很多人使用非常复杂的解决方案来处理这个问题;然而,我发现了一个非常、非常简单而简短的解决方案

/// <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 碍事的地方——SendMessageReleaseCapture 被用来完成这项工作。

当一个 DockPanel 添加到一个 DockPane 中,并且该面板的侧边已经有一个 DockPanel 停靠时,用户想要将新的 DockPanel 停靠在那里,DockPane 会使用已停靠的 DockPanelDockTab 将新的 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
    • 首次发布
© . All rights reserved.