Scintilla是一个免费、跨平台、支持语法高亮的编辑控件。它完整支持源代码的编辑和调试,包括语法高亮、错误指示、代码完成(code completion)和调用提示(call tips)。能包含标记(marker)的页边(margin)可用于标记断点、折叠和高亮当前行。
Scintilla源代码下载:http://scintilla.sourceforge.net

编译scintilla
首先进入scintilla的win32目录:

cd scintilla\win32

mingw编译器:

mingw32-make

VC6以上版本:

nmake -f scintilla.mak

编译完成后,在bin目录里会得到Scintilla.dll和SciLexer.dll文件,SciLexer.dll是包含了语法解析器 (Lexer)的Scintilla控件,一般来说只要SciLexer.dll就可以了。
如果觉得生成的SciLexer.dll太大的话,可以去除自带的部分语法解析器。比如只用它来高亮C++代码,可以进到src目录里,移除除LexCPP.cxx以外的所有Lex*.cxx文件;执行LexGen.py重建make文件和KeyWords.cxx文件(需要安装Python)。;重新按前面的方法编译,这样生成的SciLexer.dll就只带有C++语法解析器了,体积也减小了许多。

启用Scintilla作为编辑控件
要启用Scintilla,首先要加载编译的DLL文件

::LoadLibrary(_T("SciLexer.dll"));

SciLexer.dll加载后会自动以”Scintilla”作为类名注册一个窗体类,我们只要直接用这个类名建立窗体就可以了:

::CreateWindow(_T("Scintilla"),...);

配置Scintilla的方法
直接控制函数的定义为:

typedef sptr_t (*SciFnDirect)(sptr_t ptr, unsigned int iMessage, uptr_t wParam, sptr_t lParam);

后三个参数和SendMessage的后三个参数一样。
SciFnDirect的第一个参数用于指定具体的Scintilla窗体,它类似于窗体的HWND又不完全相同,它是用一个配置命令取得的。
取得直接控制函数和句柄的方法是:

SciFnDirect fnDirect = (SciFnDirect)SendMessage(hwndEditor,SCI_GETDIRECTFUNCTION,0,0);
sptr_t ptrDirect = (sptr_t)SendMessage(hwndEditor,SCI_GETDIRECTPOINTER,0,0);

取得这两样东西以后,就可以直接执行配置命令了,如:

m_fnDirect(fnDirect, SCI_CLEARALL, 0, 0);

演示代码:编写成员函数SendEditor,用于配置之前建立的Scintilla控件。

#include <Scintilla.h> 
#include <SciLexer.h> 
class TForm1 : public TForm 
{ 
__published:    // IDE-managed Components 
private:    // User declarations 
SciFnDirect m_fnDirect; 
sptr_t m_ptrDirect; 
public:        // User declarations 
__fastcall TForm1(TComponent* Owner); 
sptr_t SendEditor(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0) 
{ 
    return m_fnDirect(m_ptrDirect, iMessage, wParam, lParam); 
} 
}; 
#define SCINT_ID 1010 
__fastcall TForm1::TForm1(TComponent* Owner) 
: TForm(Owner) 
{ 
HWND hwndEditor = ::CreateWindow(_T("Scintilla"), 
    NULL, WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS|WS_VISIBLE, 
    0,0,ClientWidth,ClientHeight, 
    Handle, 
    (HMENU)SCINT_ID, HInstance, NULL); 
m_fnDirect = (SciFnDirect)SendMessage(hwndEditor,SCI_GETDIRECTFUNCTION,0,0); 
m_ptrDirect = (sptr_t)SendMessage(hwndEditor,SCI_GETDIRECTPOINTER,0,0); 
}

让Scintilla支持语法高亮
有了前面的SendEditor控制函数,我们就可以配置语法高亮了,下面这段代码可以Scintilla控件显示C++语法高亮代码:


const char* g_szKeywords=
"asm auto bool break case catch char class const "
"const_cast continue default delete do double "
"dynamic_cast else enum explicit extern false finally "
"float for friend goto if inline int long mutable "
"namespace new operator private protected public "
"register reinterpret_cast register return short signed "
"sizeof static static_cast struct switch template "
"this throw true try typedef typeid typename "
"union unsigned using virtual void volatile "
"wchar_t while";
...

SendEditor(SCI_SETLEXER, SCLEX_CPP); //C++语法解析
SendEditor(SCI_SETKEYWORDS, 0, (sptr_t)g_szKeywords);//设置关键字
// 下面设置各种语法元素前景色
SendEditor(SCI_STYLESETFORE, SCE_C_WORD, 0x00FF0000);
//关键字
SendEditor(SCI_STYLESETFORE, SCE_C_STRING, 0x001515A3);//字符串
SendEditor(SCI_STYLESETFORE, SCE_C_CHARACTER, 0x001515A3);//字符
SendEditor(SCI_STYLESETFORE, SCE_C_PREPROCESSOR, 0x00808080);//预编译开关
SendEditor(SCI_STYLESETFORE, SCE_C_COMMENT, 0x00008000);//块注释
SendEditor(SCI_STYLESETFORE, SCE_C_COMMENTLINE, 0x00008000);//行注释
SendEditor(SCI_STYLESETFORE, SCE_C_COMMENTDOC, 0x00008000);//文档注释(/**开头)

要支持语法高亮,要做三件事:

1.选定语法解析器
语法解析器用于把一大段代码分解成一个个的单词(token),另外还用于代码折叠的控制

选定语法解析器的命令是SCI_SETLEXER,如:

SendEditor(SCI_SETLEXER, SCLEX_CPP);

除了SCLEX_CPP以外,还有SCLEX_HTML、SCLEX_PERL、SCLEX_SQL、SCLEX_VB等一大堆,定 义在SciLexer.h里。现代的IDE应该可以定位SCLEX_CPP定义,它周围的SCLEX_XXX就是其它的语法解析器。
另外,也可以用SCI_SETLEXERLANGUAGE命令,如:

SendEditor(SCI_SETLEXERLANGUAGE, 0, (sptr_t)"cpp");

SCI_SETLEXERLANGUAGE接受的是一个字符串参数,这个字符串定义于代码解析器源代码(srclex*.cxx) 最后面LexerModule开头的那行代码,那里的第三个参数就是。


2.设置关键字
语法解析只负责把代码拆分开,至于哪些是关键字,还得我们来指定。
这种方式带来了些许的灵活性,比如我们要高亮一种自定义的语言,这种语言的风格与C++类似(如Java、C#、php等),我们也可以选择SCLEX_CPP作为语法解析器,然后定义自己的关键字。(所以不需要把各种解析器都编译进DLL文件里)
设置关键字的命令是SCI_SETKEYWORDS。它的wParam用于指定关键字种类,可以是0~8即9种类型,这样我们可以做 更细致的区分,如把关键字for if和int bool区分显示。lParam指定关键字,以空格分隔。

3.设置文本元素对应的字体风格
即字体、前景色、背景色、斜体粗体等
设置字体风格的命令以SCI_STYLE作为前缀,这组命令比较多,可以参考这里 (http://scintilla.sourceforge.net/ScintillaDoc.html#StyleDefinition)。

SCI_STYLECLEARALL //把所有文本元素设置成与STYLE_DEFAULT相同的风格

这里的styleNumber是指文本元素,如关键字、行号、控制字串等。前面代码中的SCE_C_XXXX是C++解析器分解出的 语法相关的元素。另外还有STYLE_DEFAULT(默认)、STYLE_LINENUMBER(行号)、STYLE_BRACELIGHT(括号匹 配)、STYLE_BRACEBAD(括号失配)、STYLE_CONTROLCHAR(控制字符)、STYLE_INDENTGUIDE(缩进线)、 STYLE_CALLTIP(调用提示)。
Scintilla文档建议的顺序是先向STYLE_DEFAULT设置一些通用风格,然后再用SCI_STYLECLEARALL 把所有元素风格重置成与STYLE_DEFAULT一致,最后单独设置其它元素。

页边(Margins)和标记(Markers)
代码折叠是现代IDE和代码编辑器的必备功能,如果现在推出一个不支持折叠的编辑器,那是要被MS地~~。为了不被MS,很有必要先“研究”一下Scintilla的页边(Margins)和标记(Markers)功能。
页 边(Margins):页边是位于文本显示区左边的一竖条区域,它可以用于显示行号、书签、断点标记等东东。Scintilla最多可以有5个页边(从左 到右的编号为0~4),每个页边可以使用SCI_SETMARGINTYPEN命令确定是用于显示行号还是符号。我们可以用 SCI_SETMARGINWIDTHN命令控制一个页边的宽度,如果设置为0,则表示不显示该页边。默认是只显示宽度为16的1号页边。
标 记(Markers):标记,不用说也知道是用来标记文本位置(确切地说,是文本行)的。我们可以使用32种标记(编号0~31),我们可以自由决定这 32种标记的意义,如标记0用来表示断点、标记1~10表示书签、标记20表示语法错误行等等。不过,如果编辑器要支持代码折叠功能,我们得把标记 25~31留出来,把这7个标记作为代码折叠专用标记。

告诉页边显示哪些标记
当页边不是设定为显示行号时(由SCI_SETMARGINTYPEN命令设置),那么它就会显示标记。Scintilla有32种标记,一般来说不会让一个页边来显示所有的标记,而是只显示部分标记。
在一个页边里可以显示哪几种标记由SCI_SETMARGINMASKN命令设置,它的参数是一个32位掩码(mask)值,掩码值的第n位为1时表示该页边可显示n号标记。
所有页边相关的命令以SCI_SETMARGIN或SCI_GETMARGIN作为前缀,如:
SCI_SETMARGINTYPEN(int margin, int type) 设置页边显示行号还是符号,type可以是SC_MARGIN_SYMBOL或SC_MARGIN_NUMBER
SCI_SETMARGINWIDTHN(int margin, int pixelWidth) 设置页边宽度
SCI_SETMARGINMASKN(int margin, int mask) 设置页边掩码
SCI_SETMARGINSENSITIVEN(int margin, bool sensitive) 设置页边是否接受鼠标点击事件
所有标记相关的命令以SCI_MARKER作为前缀,如:
SCI_MARKERADD(int line, int markerNumber) 在指定行加入一个markerNumber号标记
SCI_MARKERDEFINE(int markerNumber, int markerSymbols) 定义markerNumber号标记的样式
SCI_MARKERDELETE(int line, int markerNumber) 在指定行上的删除markerNumber号标记
SCI_MARKERDELETEALL(int markerNumber) 删除文本中所有markerNumber号标记
SCI_MARKERSETFORE(int markerNumber, int colour) 为markerNumber号标记指定前景色
SCI_MARKERSETBACK(int markerNumber, int colour) 为markerNumber号标记指定背景色

// 标记和页边演示 
void TForm1::example() 
{ 
// 先写10行文本上去 
for(int i=0; i<10; i++) 
    SendEditor(SCI_APPENDTEXT, 12, (sptr_t)"hello world "); 
// 0号页边,宽度为9,显示0号标记(0..0001B) 
SendEditor(SCI_SETMARGINTYPEN,0,SC_MARGIN_SYMBOL); 
SendEditor(SCI_SETMARGINWIDTHN,0, 9); 
SendEditor(SCI_SETMARGINMASKN,0, 0x01); 
// 1号页边,宽度为9,显示1,2号标记(0..0110B) 
SendEditor(SCI_SETMARGINTYPEN,1, SC_MARGIN_SYMBOL); 
SendEditor(SCI_SETMARGINWIDTHN,1, 9); 
SendEditor(SCI_SETMARGINMASKN,1, 0x06); 
// 2号页边,宽度为20,显示行号 
SendEditor(SCI_SETMARGINTYPEN,2, SC_MARGIN_NUMBER); 
SendEditor(SCI_SETMARGINWIDTHN,2, 20); 
for(int i=0; i<10; i++) 
{ 
    // 前10行分别加入0~2号标记 
    SendEditor(SCI_MARKERADD, i, i%3); 
  } 
// 设置标记的前景色 
SendEditor(SCI_MARKERSETFORE,0,0x0000ff);//0-红色 
SendEditor(SCI_MARKERSETFORE,1,0x00ff00);//1-绿色 
SendEditor(SCI_MARKERSETFORE,2,0xff0000);//2-蓝色 
}

默认是SC_MARK_CIRCLE,小圆圈。你可以试试其它的。(注意SC_MARK_CHARACTER比较特殊,它和一个ASCII码加起来决定标记显示为一个对应的ASCII字符)

有了这些基础,我们可以动手为Scintilla加入代码折叠功能了…

为Scintilla加入代码折叠功能
当编辑器有代码折叠功能时,25号到31号这7个标记是作为代码折叠专用标记的。在scintilla.h中,我们可以找到它们的定义:

#define SC_MARKNUM_FOLDEREND 25  //折叠状态(多级中间)
#define SC_MARKNUM_FOLDEROPENMID 26  //展开状态(多级中间)
#define SC_MARKNUM_FOLDERMIDTAIL 27  //被折叠代码块尾部(多级中间)
#define SC_MARKNUM_FOLDERTAIL 28  //被折叠代码块尾部
#define SC_MARKNUM_FOLDERSUB 29   //被折叠的代码块
#define SC_MARKNUM_FOLDER 30     //折叠状态
#define SC_MARKNUM_FOLDEROPEN 31 //展开状态
//显示这些标记的掩码是0xFE000000,同样头文件里已经定义好了
#define SC_MASK_FOLDERS 0xFE000000

要加入代码折叠功能,还有一个最最关键的事情,就是要得到语法解析器(Lexer)的支持,上面的这些标记都是由语法解析器自动添加删除的。一般来说,只要用下面这条命令就可以了让语法解析器支持代码折叠了:

SendEditor(SCI_SETPROPERTY,(sptr_t)"fold",(sptr_t)"1");

这里TForm1::WndProc方法是Scintilla父窗体即我们的TForm1的窗口处理函数。
代码折叠以后我们要通过点击页边上的+和-标记来打开和折叠代码,所以需要页边接收鼠标点击事件:

SendEditor(SCI_SETMARGINSENSITIVEN, MARGIN_FOLD_INDEX, TRUE); //响应鼠标消息

这样,当有鼠标点击该页边后,Scintilla就会向它的父窗体发送代码为SCN_MARGINCLICK的WM_NOTIFY消息,其中的LParam为SCNotification*类型。SCNotification的position成员指出了点击位置对应的行号,最后我们用SCI_TOGGLEFOLD命令折叠或展开代码。

使用自定义图形
Scintilla自带的标记样式和VS比起来还有差距,反正偶是怎么调都觉得有点土。Scintilla允许我们自己定义标记的样式,方法是:
1、用SCI_MARKERDEFINE命令设置标记的样式为SC_MARK_PIXMAP
2、用SCI_MARKERDEFINEPIXMAP命令设置标记使用的图形,这里的图形要求是xpm格式。

怎样得到xpm格式图形
xpm在linux系统下用得比较多,它和BMP、jpg一样也是一种图片格式,有不少工具可以把图片转换成xpm格式的,比如XnView。xpm比较特殊的地方是它可以作为头文件直接被C语言调用,用文本编辑器打开它,其实它就是一个数组定义。
如下面这个数据(代码)就是后面马上就要用到的minus.xpm和plus.xpm图片文件的内容(从eclipse里挖出来的):

#define MARGIN_FOLD_INDEX 2 
void TForm1::setFold() 
{ 
SendEditor(SCI_SETPROPERTY,(sptr_t)"fold",(sptr_t)"1"); 
SendEditor(SCI_SETMARGINTYPEN, MARGIN_FOLD_INDEX, SC_MARGIN_SYMBOL);//页边类型 
SendEditor(SCI_SETMARGINMASKN, MARGIN_FOLD_INDEX, SC_MASK_FOLDERS); //页边掩码 
SendEditor(SCI_SETMARGINWIDTHN, MARGIN_FOLD_INDEX, 11); //页边宽度 
SendEditor(SCI_SETMARGINSENSITIVEN, MARGIN_FOLD_INDEX, TRUE); //响应鼠标消息 
// 折叠标签样式 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDER, SC_MARK_CIRCLEPLUS);  
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPEN, SC_MARK_CIRCLEMINUS);  
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEREND,  SC_MARK_CIRCLEPLUSCONNECTED); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPENMID, SC_MARK_CIRCLEMINUSCONNECTED); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERMIDTAIL, SC_MARK_TCORNERCURVE); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERSUB, SC_MARK_VLINE);  
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERTAIL, SC_MARK_LCORNERCURVE); 
// 折叠标签颜色 
SendEditor(SCI_MARKERSETBACK, SC_MARKNUM_FOLDERSUB, 0xa0a0a0); 
SendEditor(SCI_MARKERSETBACK, SC_MARKNUM_FOLDERMIDTAIL, 0xa0a0a0); 
SendEditor(SCI_MARKERSETBACK, SC_MARKNUM_FOLDERTAIL, 0xa0a0a0); 
SendEditor(SCI_SETFOLDFLAGS, 16|4, 0); //如果折叠就在折叠行的上下各画一条横线 
} 
__fastcall TForm1::TForm1(TComponent* Owner) 
: TForm(Owner) 
{ 
... 
setFold(); 
} 
void __fastcall TForm1::WndProc(Messages::TMessage &Message) 
{ 
TForm::WndProc(Message); 
if(Message.Msg == WM_NOTIFY){ 
    SCNotification* notify = (SCNotification*)Message.LParam; 
    if(notify->nmhdr.code == SCN_MARGINCLICK && 
        notify->nmhdr.idFrom == SCINT_ID){ 
        // 确定是页边点击事件 
        const int line_number = SendEditor(SCI_LINEFROMPOSITION,notify->position); 
        SendEditor(SCI_TOGGLEFOLD, line_number); 
    } 
} 
}

演示,使用自定义图形:

#include "minus.xpm" 
#include "plus.xpm" 
void TForm1::setFold() 
{ 
... 
// 折叠标签样式 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDER, SC_MARK_PIXMAP); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPEN, SC_MARK_PIXMAP); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEREND,  SC_MARK_PIXMAP); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPENMID, SC_MARK_PIXMAP); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERMIDTAIL, SC_MARK_TCORNERCURVE); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERSUB, SC_MARK_VLINE); 
SendEditor(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERTAIL, SC_MARK_LCORNERCURVE); 
// 
SendEditor(SCI_MARKERDEFINEPIXMAP, SC_MARKNUM_FOLDER, (sptr_t)plus_xpm); 
SendEditor(SCI_MARKERDEFINEPIXMAP, SC_MARKNUM_FOLDEROPEN, (sptr_t)minus_xpm); 
SendEditor(SCI_MARKERDEFINEPIXMAP, SC_MARKNUM_FOLDEREND, (sptr_t)plus_xpm); 
SendEditor(SCI_MARKERDEFINEPIXMAP, SC_MARKNUM_FOLDEROPENMID, (sptr_t)minus_xpm); 
... 
}

怎样支持自动缩进
在VS里编写C++代码时,输入回车换行后会保持和上一行的缩进一致,输入”{‘字符后回车还会帮我们多缩进一次,输入’}’后又能自动退回。我们的编辑器也要实现这个功能。
现在再仔细了解一下Scintilla的通知消息(http://scintilla.sourceforge.net/ScintillaDoc.html#Notifications),除了前面用到的页边点击事件外,还有很多事件非常有用。
实现自动缩进功能我们要关心的事件通知是SCN_CHARADDED和SCN_UPDATEUI。
当用户输入一个字符时,SCN_CHARADDED事件触发,SCNotification的ch成员保存了输入的字符。
当更新文档界面时,SCN_UPDATEUI事件触发。输入字符,改变字体风格,改变选区都会引起界面更新
演示代码:

void __fastcall TForm1::WndProc(Messages::TMessage &Message) 
{ 
TForm::WndProc(Message); 
if(Message.Msg == WM_NOTIFY) 
{ 
    ... 
    // 处理自动缩进 
    static int LastProcessedChar = 0; 
    //在CharAdded事件中记录最后输入的字符 
    if(notify->nmhdr.code == SCN_CHARADDED) 
    { 
        LastProcessedChar = notify->ch; 
    } 
    // 在UpdateUI事件中处理缩进 
    if(notify->nmhdr.code == SCN_UPDATEUI && LastProcessedChar!=0) 
    { 
        int pos = SendEditor(SCI_GETCURRENTPOS); //取得当前位置 
        int line = SendEditor(SCI_LINEFROMPOSITION,pos); //取得当前行 
        //如果最后输入的字符是右括号的话就自动让当前行缩进和它匹配的左括号所在行一致 
        if( strchr("})>]",LastProcessedChar) && 
            isspace(SendEditor(SCI_GETCHARAT,pos-2)) && //要求右括号左边是空白字符 
            LastProcessedChar!=0) 
        { 
            //找前一个单词起始位置,这里用它来确定右括号左边是否全是空白字符 
            int startpos = SendEditor(SCI_WORDSTARTPOSITION,pos-1,false); 
            int linepos = SendEditor(SCI_POSITIONFROMLINE,line); //当前行起始位置 
            if(startpos == linepos) //这样相当于判断右括号左边是否全是空白字符 
            { 
                int othpos = SendEditor(SCI_BRACEMATCH,pos-1); //得到对应的左括号所在的位置 
                int othline = SendEditor(SCI_LINEFROMPOSITION,othpos);  //左括号所在行 
                int nIndent = SendEditor(SCI_GETLINEINDENTATION,othline);//左括号所在行的缩进值 
                // 替换右括号前面的空白字符,使之与左括号缩进一致 
                char space[1024]; 
                memset(space,' ',1024); 
                SendEditor(SCI_SETTARGETSTART, startpos); 
                SendEditor(SCI_SETTARGETEND, pos-1); 
                SendEditor(SCI_REPLACETARGET,nIndent,(sptr_t)space); 
            } 
        } 
        // 如果输入的是回车,则保持与上一行缩进一致 
        // 如果上一行最后有效字符为左括号,就多缩进四个空格 
        if(LastProcessedChar == ' ') 
        { 
            if(line > 0) 
            { 
                // 得到上一行缩进设置 
                int nIndent = SendEditor(SCI_GETLINEINDENTATION,line-1); 
                // 查找上一行最后一个有效字符(非空白字符) 
                int nPrevLinePos = SendEditor(SCI_POSITIONFROMLINE,line-1); 
                int c = ' '; 
                for(int p = pos-2; 
                    p>=nPrevLinePos && isspace(c); 
                    p--, c=SendEditor(SCI_GETCHARAT,p)); 
                // 如果是左括号,就多缩进四格 
                if(c && strchr("{([<",c)) nIndent+=4; 
                // 缩进... 
                char space[1024]; 
                memset(space,' ',1024); 
                space[nIndent] = 0; 
                SendEditor(SCI_REPLACESEL, 0, (sptr_t)space); 
            } 
        } 
        LastProcessedChar = 0; 
    } 
} 
}

下面是代码中用到的Scintilla命令的简单介绍
SCN_CHARADDED事件记录最后输入的字符,在SCN_UPDATEUI事件中处理缩进。
当输入回车时(LastProcessedChar == ‘ ‘),我们只需要保证新行和前一行的缩进相同就可以了。
SCI_GETLINEINDENTATION命令可以取得指定行的缩进数(即行首的空格数目)。
SCI_REPLACESEL命令用指定字符串替换选择区域
SCI_GETCURRENTPOS命令取得当前位置
SCI_GETCHARAT命令取得指定位置的字符
SCI_LINEFROMPOSITION命令取得指定位置所在的行号
SCI_POSITIONFROMLINE命令取得指定行号的起始位置
SCI_WORDSTARTPOSITION命令取得指定位置所在单词的起始位置,如xxx|xx,(|代表指定位置),那么它会返回|xxxxx的位置。同样还有SCI_WORDENDPOSITION命令。
SCI_BRACEMATCH取得括号的另一半位置,如指定位置的字符是’}’时,它返回匹配的'{‘所在的位置。
SCI_SETTARGETSTART和SCI_SETTARGETEND设置TARGET的起始和始止位置,SCI_REPLACETARGET命令用指定字符串替换TARGET指定范围内的字符。

VS的代码完成和函数提示功能是很值得称道的,它们可以极大地提高我们的编程效率(造成我现在写代码时往往只记住前四个字母,如果在对象后面点了小数点后不出现提示就会心慌意乱的说-_-),尽管有时也会失效。

关于函数提示的几个命令以SCI_CALLTIP作为前缀,这里只介绍我们即将使用的几个命令(更多命令见:http://scintilla.sourceforge.net/ScintillaDoc.html#CallTips
SCI_CALLTIPSHOW(int posStart, const char *definition) 显示提示。posStart表示显示位置,definition是显示的内容
SCI_CALLTIPCANCEL 取消提示
SCI_CALLTIPACTIVE 如果当前编辑器中有提示信息,返回1,否则返回0
SCI_CALLTIPSETHLT(int highlightStart, int highlightEnd) 设置提示中的高亮位置,在VS里我们输入函数实参时函数提示会高亮当前输入的参数名。
在我们程序中加入提示的最佳时机是SCN_CHARADDED(见上一节)事件。当用户输入左圆括号'(‘时,取得括号左边的函数名,然后显示出该函数的完整定义。
下面的代码实现了CreateWindow和MoveWindow两个API的函数提示:

//我们要高亮的两个函数 
const size_t FUNCSIZE=2; 
char* g_szFuncList[FUNCSIZE]={ //函数名 
"CreateWindow(", 
"MoveWindow(" 
}; 
char* g_szFuncDesc[FUNCSIZE]={ //函数信息 
"HWND CreateWindow(" 
"LPCTSTR lpClassName," 
" LPCTSTR lpWindowName," 
" DWORD dwStyle, " 
" int x," 
" int y," 
" int nWidth," 
" int nHeight, " 
" HWND hWndParent," 
" HMENU hMenu," 
" HANDLE hInstance," 
" PVOID lpParam" 
")", 
"BOOL MoveWindow(" 
"HWND hWnd," 
" int X," 
" int Y," 
" int nWidth," 
" int nHeight," 
" BOOL bRepaint" 
")" 
}; 
void __fastcall TForm1::WndProc(Messages::TMessage &Message) 
{ 
TForm::WndProc(Message); 
if(Message.Msg == WM_NOTIFY) 
{ 
    SCNotification* notify = (SCNotification*)Message.LParam; 
    ... 
    if(notify->nmhdr.code == SCN_CHARADDED) 
    { 
        ... 
        // 函数提示功能 
        static const char* pCallTipNextWord = NULL;//下一个高亮位置 
        static const char* pCallTipCurDesc = NULL;//当前提示的函数信息 
        if(notify->ch == '(') //如果输入了左括号,显示函数提示 
        { 
            char word[1000]; //保存当前光标下的单词(函数名) 
            TextRange tr;    //用于SCI_GETTEXTRANGE命令 
            int pos = SendEditor(SCI_GETCURRENTPOS); //取得当前位置(括号的位置) 
            int startpos = SendEditor(SCI_WORDSTARTPOSITION,pos-1);//当前单词起始位置 
            int endpos = SendEditor(SCI_WORDENDPOSITION,pos-1);//当前单词终止位置 
            tr.chrg.cpMin = startpos;  //设定单词区间,取出单词 
            tr.chrg.cpMax = endpos; 
            tr.lpstrText = word; 
            SendEditor(SCI_GETTEXTRANGE,0, sptr_t(&tr)); 
            for(size_t i=0; i<FUNCSIZE; i++) //找找有没有我们认识的函数? 
            { 
                if(memcmp(g_szFuncList[i],word,sizeof(g_szFuncList[i])) == 0) 
                {     //找到啦,那么显示提示吧 
                    pCallTipCurDesc = g_szFuncDesc[i]; //当前提示的函数信息 
SendEditor(SCI_CALLTIPSHOW,pos,sptr_t(pCallTipCurDesc));//显示这个提示 
                    const char *pStart = strchr(pCallTipCurDesc,'(')+1; //高亮第一个参数 
                    const char *pEnd = strchr(pStart,',');//参数列表以逗号分隔 
                    if(pEnd == NULL) pEnd = strchr(pStart,')');//若是最后一个参数,后面是右括号 
                    SendEditor(SCI_CALLTIPSETHLT, 
                        pStart-pCallTipCurDesc, pEnd-pCallTipCurDesc); 
                    pCallTipNextWord = pEnd+1;//指向下一参数位置 
                    break; 
                } 
            } 
        } 
        else if(notify->ch == ')') //如果输入右括号,就关闭函数提示 
        { 
            SendEditor(SCI_CALLTIPCANCEL); 
            pCallTipCurDesc = NULL; 
            pCallTipNextWord = NULL;                 
        } 
        else if(notify->ch == ',' && SendEditor(SCI_CALLTIPACTIVE) && pCallTipCurDesc) 
        { 
            //输入的是逗号,高亮下一个参数 
            const char *pStart = pCallTipNextWord; 
            const char *pEnd = strchr(pStart,','); 
            if(pEnd == NULL) pEnd = strchr(pStart,')'); 
            if(pEnd == NULL) //没有下一个参数啦,关闭提示 
                SendEditor(SCI_CALLTIPCANCEL); 
            else 
            { 
                SendEditor(SCI_CALLTIPSETHLT,pStart-pCallTipCurDesc, pEnd-pCallTipCurDesc); 
                pCallTipNextWord = pEnd+1; 
            } 
        } 
    }//if(notify->nmhdr.code == SCN_CHARADDED) 
    ... 
}//if(Message.Msg == WM_NOTIFY) 
 }

当然,这个提示功能相当山寨。比如函数名和括号之间有空格提示就不出来了,函数嵌套调用时只会提示最后一个函数的参数。

代码完成和函数提示的用法类似,前缀是SCI_AUTOC,具体命令见:http://scintilla.sourceforge.net/ScintillaDoc.html#Autocompletion

void __fastcall TForm1::WndProc(Messages::TMessage &Message) 
{ 
TForm::WndProc(Message); 
if(Message.Msg == WM_NOTIFY) 
{ 
    ... 
    if(notify->nmhdr.code == SCN_CHARADDED) 
    { 
        ... 
        if(notify->ch == '.') 
        { 
            char word[1000]; //保存当前光标下的单词 
            TextRange tr;    //用于SCI_GETTEXTRANGE命令 
            int pos = SendEditor(SCI_GETCURRENTPOS); //取得当前位置 
            int startpos = SendEditor(SCI_WORDSTARTPOSITION,pos-1);//当前单词起始位置 
            int endpos = SendEditor(SCI_WORDENDPOSITION,pos-1);//当前单词终止位置 
            tr.chrg.cpMin = startpos;  //设定单词区间,取出单词 
            tr.chrg.cpMax = endpos; 
            tr.lpstrText = word; 
            SendEditor(SCI_GETTEXTRANGE,0, sptr_t(&tr)); 
            if(strcmp(word,"file.") == 0) //输入file.后提示file对象的几个方法 
            { 
                SendEditor(SCI_AUTOCSHOW,0, 
                    sptr_t( 
                        "close " 
                        "eof " 
                        "good " 
                        "open " 
                        "rdbuf " 
                        "size" 
                    )); 
            } 
        } 
        ...

SCI_AUTOCSHOW命令的第一个参数表示已经输入了多少个字符。这对于代码自动完成是很有帮助的,比如我们可以用它帮助用户输入长串的单词,如:

if(strcmp(word,"Create") == 0) 
{ 
SendEditor(SCI_AUTOCSHOW,6,//已经输入了6位字符 
    sptr_t( 
        "CreateBitmap " 
        "CreateDC " 
        "CreateHandle " 
        "CreateWindow " 
        "CreateWindowEx" 
    )); 
 }

本文由云之梦歌编辑转载,侵删。

Categories: 文章随笔