Notepad++源码编译及其分析

Notepad++是一个小巧精悍的编辑器,其使用方法我就不多说了,由于notepad++是使用c++封装的windows句柄以及api来实现的,因此对于其源码的研究有助于学习如何封装自己简单的库(当然不是MTL、MFC或者QT那样大型的库)。Notepad++源码:https://github.com/notepad-plus-plus/notepad-plus-plus/releases/tag/v6.7.9.2。

下面是Notepad++源码的目录:

     

其主目录如第一张图所示,包含了两个开源工程,第一个PowerEditor就是notepad++;第二scintilla是一个代码编辑器的开源库,十分强大,许多编辑器都是基于这个库封装起来的,对于scintilla来说可以实现代码折叠、标记、缩进等等很多功能,具体情况可以去scintilla的官网以及一些博客来了解如何使用这个开源库:

http://www.scintilla.org/

http://www.cnblogs.com/superanyi/archive/2011/04/07/2008636.html

将第一个工程PowerEditor打开之后将如上右图所示,这里最重要的是src源码文件以及installer中的配置文件。当我用vs2012打开visual.net打开工程文件notepadPlus.vs2005.vcproj后出现了几个问题,第一个问题,就是一大堆找不到预编译头文件,解决方法是不使用预编译头文件即可。第二个问题出现在Sorters.h头文件中,vs2012虽然实现了c++11的部分特性,但是却没有实现std::unique_ptr,由此引发出很多语法错误,解决办法可以有自己写一个类似于unique_ptr这样的智能指针或者有办法替换vs2012内置编译器也可以。我比较懒,恰好笔记本中安装有vs2013,是支持unique_ptr的,我就直接换环境了。

然后程序跑起来还是有一些问题,在WinMain中作者对一些左值进行了赋值,语法直接报错了,对于这个直接注释掉这一行即可,其次再删除一个预编译源文件和实现一个函数声明之后程序就跑了起来,之后的小问题就不多说了,因为本文主要是讲notePad++的运行机制是啥。我稍微统计了下,整个工程大概是21W行代码,加上注释也比较少,实在花了我好几天才摸清楚大概情况。

从界面开始说起,整个工程中的窗口都是继承自Window这个类的,这个类封装最重要的一个成员就是HWND _hSelf,这个就是用来存放CreateWindow函数返回的窗口句柄,其次就是父窗口句柄HWND _hParent,以及实例HINSTANCE _hInst。还提供了许多窗口都能够用到的方法,都是以虚函数的方法来提供的,比如display函数用来显示窗口,reSizeTo用来调整窗口,redraw用来重绘窗口等等许多函数。有了Window类,后面的就容易理解一些了,其中整个NotePad++的主窗口类Notepad_plus_Window继承自Window,notepad++中所有的对话框都是继承自StaticDialog的,而StaticDialog也是继承自Window这个父类的。下面是Window类的源码:

从直观上来说,因为像菜单栏、工具栏、编辑框等等这些窗口应该属于主窗口,不过作者在主窗口Notepad_plus_Window和这些子窗口中间添加了一层,将所有的子窗口对象都封装在了Notepad_plus这个类中,再由Notepad_plus_Window来封装Notepad_plus对象_notepad_plus_plus_core。这样一来让主窗口的代码和子窗口的一些实现分离了,让Notepad_plus_Window的功能变得很清晰,不过Notepad_plus这个类因为封装可大量的子窗口对象变得十分复杂,另一个问题就是这些子窗口的父窗口需要指定,但是这个父窗口句柄被封装在Notepad_plus_Window中,于是Notepad_plus类中又封装了Notepad_plus_Window对象指针,机智的通过编译又能够拿到父窗口句柄了。下面是Notepad_plus_Window源码:

接下来就从WinMain这个函数(源码在下面)入口来讲解程序是怎么跑起来的,首先NotePad++是能够接受命令行的,从最开始就使用GetCommandLine()来接受命令行,作者为了更好的支持命令行,写了两个类ParamVector和CmdLineParams来分别保存命令行以及根据这些命令决定了程序运行的一些属性(如是否允许插件、可读性等等)。作者将GetCommandLine()返回的LPTSTR类型变量使用算法parseCommandLine()保存到了ParamVector对象中,在利用CmdLineParams方法isInList()来判断是否命令行带有某些程序运行属性,并将其保存在CmdLineParams对象中,这里有些属性马上就用到,有些属性都是很久之后才用到的。

因为NotePad++将许多配置信息都保存在了本地文件中,比如哪国语言、整体风格、用户快捷键等等都是如此,因此在命令行之后就应该处理好这些参数,以让窗口和子窗口显示出来时候都是按照以前设置的配置来的。这里作者创建NppParameters类来控制配置信息,这个类的头文件和源文件也是多的够呛,分别是1700行和6500行。总的来说就是把本地的配置信息读取到内存中来,NppGUI用来保存界面配置,ScintillaViewParams用来保存ScintillaView的配置,LexerStylerArray和StyleArray用来保存颜色以及字体,还有许多vector类来保存各种快捷键。这些配置的载入时从NppParameters的load()函数运行开始的,这些配置文件都应该跟程序在同一个文件夹下,因为代码中默认在程序运行的同一路径之下去查找这些配置文件的,在经过读取config.xml、stylers.xml、userDefineLang.xml、nativeLang.xml、toolbarIcons.xml、shortcuts.xml、contextMenu.xml、session.xml、blacklist.xml这些配置文件读入之后,load()函数就返回了,有读当然有写,写函数也是定义在NppParameters类中。其实只要找到一个配置文件debug一趟就明白来龙去脉了。

之后回到WinMain中判断程序是否允许多实例,如果不允许多实例并且还不是第一个启动的实例的话,就直接用::FindWindow()找到已经存在在内存中的窗口就好,之后显示这个主窗口,如果有参数的话就把参数利用::SendMessage()以WM_COPYDATA的消息传递过去,之后返回。如果是允许多实例(用户可以在首选项设定)或者是第一次启动NotePad++的话直接跳过这段往后执行。

如果是一个新的实例的话,先创建主界面封装类Notepad_plus_Window对象notepad_plus_plus留着后面用。紧接着程序看看当前目录下的updater目录下有没有GUP.exe这个程序,这个程序是用来升级NotePad++的,如果当前的日期比较陈旧并且存在这个程序并且操作系统比XP新再并且是第一个NotePad++实例的话就运行这个NotePad++的程序,如果有一个不符合就过喽。

之后当我第一次看到了MSG msg;这句代码,我很高兴,说明消息循环要开始了,后面要透明了,但是在此之前首先执行了notepad_plus_plus.init(hInstance, NULL, quotFileName.c_str(), &cmdLineParams); notepad_plus_plus我之前说过是主界面对象,这个对象在初始化的时候基本没干什么事情,反而在这里要露肌肉了,因为init()函数太过庞大,容我先传上WinMain函数的代码再来解释这个函数:

现在进入主窗口对象notepad_plus_plus的init()函数进行探究,init()函数的第一件事情就是创建窗口类,这里指定了主窗口的整体风格以及其菜单名称,这里的菜单就是作者事先准备好的菜单资源,这个资源定义在Notepad_plus.rc资源脚本中。之后就是大家熟知的注册窗口类,用CreateWindowEx创建窗口,返回的句柄保存在从Window继承下来的_hSelf成员变量中。因为有CreateWindowEx这个函数,系统会发送WM_CREATE消息到消息队列中,因为这个消息比较特殊,在消息循环未建立好之前也会被回调函数捕捉处理。因此在CreateWindowEx函数之后就应该转到窗口句柄对应的回调函数去,这个函数的实现位于NppBigSwitch.cpp中,下面是这个函数,只有WM_CREATE是现场处理的,其他的消息都被转到了_notepad_plus_plus_core的process函数中去了:

在处理WM_CREATE消息中调用了_notepad_plus_plus_core的init()函数,按照程序执行的步骤来讲解,这里也转到init()函数内部来进行讲解,先理一下顺序:WinMain->notepad_plus_plus.init()->CreateWindwEx()->WM_CREATE->_note_plus_plus_core.init();当然只有WM_CREATE会被首先捕捉处理,其他的消息仍然是在构建好消息循环之后接受处理的。

程序首先判定语言菜单项是不是紧凑型的,如果是紧凑型的话就将现有的语言菜单栏给移走,因为这里需要改变语言栏的形状,因为_notepad_plus_plus_core中已经定义了两个ScitillaEditView(开源项目scintilla的封装)对象_mainEditView和_subEditView,这两个很容易理解,因为Notepad++是能够将编辑框分割成左边一般右边一般的,因此这两个一个是左边的编辑框,一个是右边的编辑框。然而还定义了两个DocTabView对象,_mainDocTab和_subDocTab,这个十分有必要说一下,DocTabView类是继承自TabBarPlus的,而TabBarPlus是集成自TabBar的,而TabBar是继承自Window的,说明DocTabView是一个窗口,如果看到程序后面的DocTabView对象的init()函数就知道,这个DocTabView对象不仅继承自TabBarPlus而且还将ScitillaEditView封装在自己的对象中,从字面意思能够看懂TabBarPlus是一个标签栏,一个标签栏和编辑框组合在一起,明显肯定是想要利用标签栏来控制编辑框的关闭、移动窗口等等工作。事实上,DocTabView的init()函数还接受了IconList对象,其实这个对象_docTabIconList中只是用来管理标签栏的图标的,一共只有三种,也就是未保存状态的图标,保存状态的图标和文件只可读的图标:

上面有个tabBarStatus用来保存标签栏的状态,决定了是缩小还是不缩小,这个功能在实际的软件的设置->首选项->常用->标签栏中可以勾选或者不选缩小。之后程序又为一个不可见的ScitillaEditView对象_invisibleEditView进行了初始化,这个_invisibleEditView是为了用户搜索之后将搜索结果放在这个_invisibleEditView中显示的,平时当让不可见了。再之后就是对三个作者封装的编辑框进行了初始化,因为scintilla本身十分之强大,而作者的ScitillaEditView主要是对这个功能进行了自己的封装,之后都是通过execute()函数来进行调用scintilla的功能的,这些针对于ScitillaEditView的设置暂时放在一边。之后初始化了两个对话框:

一个是语言格式设置,一个就是首选项了。在这之后就是就是调整标签栏的显示状况、标签栏上的关闭按钮、绘制顶层标签栏、决定标签栏是否能够拖动等等一系列事情,再加载了标签栏的风格:

之后又初始化了分割栏SplitterContainer对象_subSplitter,这个从字面上来理解为分割容器的类其实十分之强大和费解,我在读取有关这个类的代码的时候一度十分困扰,跟踪了许多消息才恍然大悟是如此使用。其实除了这个_subSplitter之外,还有一个_pMainSplitter,让我们先搞懂_subSplitter这个对象,这个类SplitterContainer也是继承自Window的,_subSplitter在创建开始的时候吸纳了两个带有标签栏的编辑框,并在对象内维护了一个真正的Splitter,也就是说事实上我们看到的NotePad++的运行界面上的两个编辑框事实上还有一个SplitterContainer在显示,只不过其Background是NULL,也就是说不可见的。至于这样做的好处,可谓十分机制,在后面会有讲述。之后又初始化了状态栏,设定了状态栏的大致情况:

之后判断主界面是否需要被最小化,如果需要最小化的话则先保存。之后又让插件管理器初始化了,因为后面需要加载插件信息。接着后面就比较简单了,就是为主菜单栏的菜单项添加子菜单,有宏菜单、运行菜单、语言、文件、插件、窗口菜单,一个比较特殊的就是“升级”这个菜单项是否应该出现在菜单中,如果不应该就删除嘛。另外语言菜单项还需要将用户排除的那些编程语言去除语言菜单项中,这些代码虽然长,但是比较简单:

 

后面就是程序另一大功能了,添加程序快捷键,作者将这些快捷键分开管理了,其实添加快捷键的代码还是比较麻烦的,主要的还是先将普通菜单栏的一些菜单项的快捷键设置好,其次是插件的快捷键,宏的快捷键,用户自定义的快捷键,右键菜单。分别是都是用vector来保存的:vector<CommandShortcut> vector<MacroShortcut> vector<UserCommand> vector<PluginCmdShortcut>。在这期间将英语设置成了用户之前自定义的语言:

之后就到了映射快捷键的时候的,经过层层调用,最终使用系统函数CreateAcceleratorTable来进行映射:

还有编辑框的快捷键的映射,因为走的不同的通道,所以这里本来就应该分开的:

之后就是工具栏了嘛,工具栏在win32中有比较方便的实现方法,这里作者也是直接用的,首先需要TBBUTTON这个结构体,这个东西可以直接和ImageList挂钩起来,设置好图片之后直接给toolbar发送一条消息就可以:TB_SETIMAGELIST。因为这个是现有的就不多做解释了,网上资料不多但是还能够找到点:

https://msdn.microsoft.com/en-us/library/bb787433(v=vs.85).aspx

http://www.gamedev.net/topic/451684-win32-non-mfc-rebar-and-toolbar-problems/

再下面就是初始化其他的对话框,比如查找代替啊,运行对话框啊等等…:

下面就是最麻烦的一个问题了,用户自定义语言格式是一个对话框,但是这个对话框的特殊之处在于带有了一个dock按钮,也就是浮动功能,但是这个浮动和其他窗口的浮动完全不一样,如果这个对话框在显示的状态下被按下了dock按钮,会干许多的事情:

注意最后一个case情况,这里的udd->doDialog()能够将对话框显示出来,因为没有浮动的情况下,这个对话框是没有父类的,所以能够看到,读者可以自行调试一下。但是如果是浮动的状态会通过消息的形式来模拟dock按钮被按下,那按下之后做了什么事情呢:

上面像显示透明度条和按钮啊都将被隐藏,因为在主窗口中透明肯定不好实现,之后向父窗口也就是整个主窗口发送了一条消息

WM_DOCK_USERDEFINE_DLG,因为主窗口多了一个用户自定义语言对话框,毫无疑问肯定需要重新设计整个界面的大小和排版了,于是如下:

再进入到这个函数:

这里终于重新见到了_pMainSplitter,这个意思就是如果用户自定义语言窗口浮动了,就将_pMainSplitter设定为_subSplitter和用户自定义对话框,而_subSplitter在上面已经讲过了是两个文本框放在一起的!这里的_pMainSplitter完美的反映出目前的状况,这个机制的实现比较复杂,使用了指针指针对象以及部分多态,重点是跨越的点实在太远了,很难联想到一起。在设置了这些窗口之后,用一个WM_SIZE消息调整一下大小,这个调整也很机智:

乍一看,感觉一点关系都没有,只不过就是改变了状态栏的高度神马的。这里引出了一个新的对象_dockingManager,浮动管理器,这个东西感觉就是用来管理用户自定义语言对话框的,但是感觉十分不靠谱,我在这里被坑了很久。其实这里的浮动管理器管理的额浮动根本就不是针对于两个编辑框以及用户自定义语言窗口的!这个是为了像插件窗口这样的可以浮动的窗口准备的,整个_dockingManager管理者四块区域,也就是上下左右,如果一点有哪个窗口dock在了上下左右中的一个就会引起整个主界面客户去的调整,首先调整的是dock窗口本身,其次!是ppMainWindow!也就是说只有dock窗口先调整,之后再轮到原来的两个编辑框和用户对话框的调整:

这最后一句让我醍醐灌顶,注意这里是多态,所以调用的不是父类的resizeto(),而是:

有时候传递的消息反而是我们容易忽略的,但是恰恰最重要,这里的WM_RESIZE_CONTAINER消息之后,所有的主界面都将被调整好。经过这些深入的探究也该回到_notepad_plus_plus_core的init()了,其实后面做的事情就是为编辑框加载文件了,之后正常返回:

返回之后就到了最开始notepad_plus_plus.init()这里,在CreateWindowEx代码之后继续执行,之后嘛该最小化就最小化,添加主题选择器和语言选择器等等,再之后一些细节加载完成就返回到了WinMain了,之后就构建了消息循环,开始处理之前在消息队列中存放的消息,之后程序就跑起来了:

2 7 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部