Scintilla開源庫使用指南


Scintilla是一個免費、跨平台、支持語法高亮的編輯控件。它完整支持源代碼的編輯和調試,包括語法高亮、錯誤指示、代碼完成(code completion)和調用提示(call tips)。能包含標記(marker)的頁邊(margin)可用於標記斷點、折疊和高亮當前行。

Scintilla是一個免費、跨平台、支持語法高亮的編輯控件。它完整支持源代碼的編輯和調試,包括語法高亮、錯誤指示、代碼完成 (code completion)和調用提示(call tips)。能包含標記(marker)的頁邊(margin)可用於標記斷點、折疊和高亮當前行。

可以從這里下載Scintilla庫:http://scintilla.sourceforge.net/ScintillaDownload.html

這里有Scinilla相關的庫下載,比如wxScintilla就是Scintilla的wxWidgets移植版。http://www.scintilla.org/ScintillaRelated.html

另外,Scintilla的作者為了演示這個東東的功能,編寫了一個叫SciTE的演示程序。不過這個程序的功能已經強大到足以作為我 們的常用代碼編輯器,很值得下載下來學習學習。

老規矩,還是從編譯說起

偶只在Windows下編譯過,所以只好說說Windows環境下的編譯方法。對於Linux,沒試過(丟人-_-)

下載、解壓略過不提

首先進入scintilla的win32目錄:

cd scintilla\win32
  • 對於mingw,輸入:
    mingw32-make
  • 對於VC6以上版本,輸入:
    nmake -f scintilla.mak
  • 對於VC6(沒試過,從Readme里看來的),輸入:
    nmake -f scintilla_vc6.mak
  • 對於C++Builder,輸入:
    make -fscintilla.mak

編譯完成后,在bin目錄里會得到Scintilla.dll和SciLexer.dll文件,SciLexer.dll是包含了語法解析器 (Lexer)的Scintilla控件,一般來說我們只要用它就可以了。

需要說明的是,不管是用什么編譯器生成的DLL文件,都可以供給其它編譯器使用(就象系統DLL一樣,任何編譯器都能使用),所以不用為各種編譯器都編譯 一份。

如果覺得生成的SciLexer.dll太大的話,可以考慮去除自帶的部分語法解析器。比如你打算只用它來高亮C++代碼的話,可以:

  1. 進到src目錄里,移除除LexCPP.cxx以外的所有Lex*.cxx文件
  2. 執行LexGen.py重建make文件和KeyWords.cxx文件(需要安裝Python)。
  3. 重新按前面的方法編譯,這樣生成的SciLexer.dll就只帶有C++語法解析器了,體積也大小減小了(我VC編譯的結果是從1.4M減小到 206K)。

啟用Scintilla作為編輯控件

要啟用Scintilla,首先當然是要加載之前編譯的DLL文件啦~~

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

SciLexer.dll加載后會自動以"Scintilla"作為類名注冊一個窗體類,我們只要直接用這個類名建立窗體就可以了:

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

演示(在C++Builder下編寫)

由於Scintilla主要是窗體操作,為了減少不必要的窗體代碼(主要是偷懶外加推廣一下C++Builder,呵呵),這里使用C++Builder 來寫演示程序。對於一些C++Builder的VCL庫特有的東東,后面會有解釋的。

首先新建一個窗體應用程序,

然后在WinMain里載入SciLexer.dll:

WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    LoadLibrary(_T("SciLexer.dll"));
    ...

最后,在TForm1的構造里建立Scintilla窗體:

#define SCINT_ID 1010

__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
    ::CreateWindow(_T("Scintilla"),
        NULL, WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS|WS_VISIBLE,
        0,0,ClientWidth,ClientHeight,
        Handle,
        (HMENU)SCINT_ID, HInstance, NULL);
}

很簡單,是吧?對於Scintilla來說,沒什么好解釋的了。

這里主要給不了解C++Builder的童鞋介紹一下VCL的東東,以便於接下來的講解和代碼閱讀(以及移植到其它編譯器中)。

  • TForm1是一個C++Builder自動生成的窗體類,它繼承自TForm,可以把它看成是WS_OVERLAPPEDWINDOW風格的HWND的 封裝。
  • ClientWidth和ClientHeight是TForm的屬性,看名字就知道它是客戶區(ClientRect)的寬和高
  • Handle也是TForm的屬性,就是該窗體的HWND
  • HInstance不用解釋了吧,這是VCL的一個全局變量。

現在,我們的成果是這樣的:


現在,看上去還比較土,接下來我們開始配置它,為使它成為可與VS媲美的代碼編程器而戰!

配置Scintilla的兩種方法

配置Scintilla控件是通過向該控件發送配置命令實現的,各種配置命令可以在doc目錄下找到(或者是這里http://scintilla.sourceforge.net/ScintillaDoc.html),后面的大部分事情都是在介紹這些配置命令。

有兩種方法來發送配置命令,一種是直接使用SendMessage API。另一種是取得直接控制函數,通過函數來執行配置命令。

在Windows下,第二種方法要比第一種快得多。

直接控制函數的定義為:

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);//文檔注釋(/**開頭)
 

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

一、選定語法解析器

語法解析器用於把一大段代碼分解成一個個的單詞(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接受的是一個字符串參數,這個字符串定義於代碼解析器源代碼(src\lex*.cxx) 最后面LexerModule開頭的那行代碼,那里的第三個參數就是。

二、設置關鍵字

語法解析只負責把代碼拆分開,至於哪些是關鍵字,還得我們來指定。

這種方式帶來了些許的靈活性,比如我們要高亮一種自定義的語言,這種語言的風格與C++類似(如Java、C#、php等),我們也 可以選定SCLEX_CPP作為語法解析器,然后定義自己的關鍵字。(所以不需要把各種解析器都編譯進DLL文件里)

設置關鍵字的命令是SCI_SETKEYWORDS。它的wParam用於指定關鍵字種類,可以是0~8即9種類型,這樣我們可以做 更細致的區分,如把關鍵字for if和int bool區分顯示。lParam指定關鍵字,以空格分隔。    

三、設置文本元素對應的字體風格

即字體、前景色、背景色、斜體粗體等

設置字體風格的命令以SCI_STYLE作為前綴,這組命令比較多,為了不浪費篇幅,偶這里只列舉幾個,其它的可以參考這里 (http://scintilla.sourceforge.net/ScintillaDoc.html#StyleDefinition)。

?
SCI_STYLESETBACK( int styleNumber, int colour) //設置背景色SCI_STYLESETFORE(int styleNumber, int colour) //設置前景色SCI_STYLESETFONT(int styleNumber, char *fontName) //設置字體SCI_STYLESETSIZE(int styleNumber, int sizeInPoints)//設置字號SCI_STYLESETBOLD(int styleNumber, bool bold) //設置粗體
這里的styleNumber是指文本元素,如關鍵字、行號、控制字串等。前面代碼中的SCE_C_XXXX是C++解析器分解出的 語法相關的元素。另外還有STYLE_DEFAULT(默認)、STYLE_LINENUMBER(行號)、STYLE_BRACELIGHT(括號匹 配)、STYLE_BRACEBAD(括號失配)、STYLE_CONTROLCHAR(控制字符)、STYLE_INDENTGUIDE(縮進線)、 STYLE_CALLTIP(調用提示)。
?
SCI_STYLECLEARALL //把所有文本元素設置成與STYLE_DEFAULT相同的風格

Scintilla文檔建議的順序是先向STYLE_DEFAULT設置一些通用風格,然后再用SCI_STYLECLEARALL 把所有元素風格重置成與STYLE_DEFAULT一致,最后單獨設置其它元素。

演示,我們的編輯器支持C++高亮啦!

?
#include <Scintilla.h> 
#include <SciLexer.h> 
... 
void TForm1::setCppStyle() 
     const char * szKeywords1= 
         "asm auto break case catch class const " 
         "const_cast continue default delete do double " 
         "dynamic_cast else enum explicit extern false " 
         "for friend goto if inline mutable " 
         "namespace new operator private protected public " 
         "register reinterpret_cast return signed " 
         "sizeof static static_cast struct switch template " 
         "this throw true try typedef typeid typename " 
         "union unsigned using virtual volatile while"
     const char * szKeywords2= 
         "bool char float int long short void wchar_t"
     // 設置全局風格 
     SendEditor(SCI_STYLESETFONT, STYLE_DEFAULT,(sptr_t) "Courier New" ); 
     SendEditor(SCI_STYLESETSIZE, STYLE_DEFAULT,10); 
     SendEditor(SCI_STYLECLEARALL); 
     //C++語法解析 
     SendEditor(SCI_SETLEXER, SCLEX_CPP); 
     SendEditor(SCI_SETKEYWORDS, 0, (sptr_t)szKeywords1); //設置關鍵字 
     SendEditor(SCI_SETKEYWORDS, 1, (sptr_t)szKeywords2); //設置關鍵字 
     // 下面設置各種語法元素風格 
     SendEditor(SCI_STYLESETFORE, SCE_C_WORD, 0x00FF0000);   //關鍵字 
     SendEditor(SCI_STYLESETFORE, SCE_C_WORD2, 0x00800080);   //關鍵字 
     SendEditor(SCI_STYLESETBOLD, SCE_C_WORD2, TRUE);   //關鍵字 
     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); //文檔注釋(/**開頭) 
    
     SendEditor(SCI_SETCARETLINEVISIBLE, TRUE); 
     SendEditor(SCI_SETCARETLINEBACK, 0xb0ffff); 
    
__fastcall TForm1::TForm1(TComponent* Owner) 
     : TForm(Owner) 
     ... 
     setCppStyle(); 
}

看上去不錯,如果你願意,還可以加上當前行高亮功能:

?
SendEditor(SCI_SETCARETLINEVISIBLE, TRUE);
SendEditor(SCI_SETCARETLINEBACK, 0xb0ffff);
最后,建議把TAB寬度由默認的8改為4(依個人習慣~~) 
?
SendEditor(SCI_SETTABWIDTH, 4);
現在,我們的成果是這樣的:

 

這里繼續學習Scintilla更多的控制命令和實現細節,完善我們的編輯器;

頁邊(Margins)和標記(Markers)

代碼折疊是現代IDE和代碼編輯器的必備功能,如果現在推出一個不支持折疊的編輯器,那是要被BS地~~。為了不被BS,很有必要先“研究”一下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-藍色 
}

顯示效果是:

如果你不喜歡這些圓圈,可以用SCI_MARKERDEFINE命令改變標記的樣式,可選的有:

SC_MARK_CIRCLE, SC_MARK_ROUNDRECT, SC_MARK_ARROW, SC_MARK_SMALLRECT,
SC_MARK_SHORTARROW, SC_MARK_EMPTY, SC_MARK_ARROWDOWN, SC_MARK_MINUS,
SC_MARK_PLUS, SC_MARK_VLINE, SC_MARK_LCORNER, SC_MARK_TCORNER, SC_MARK_BOXPLUS,
SC_MARK_BOXPLUSCONNECTED, SC_MARK_BOXMINUS, SC_MARK_BOXMINUSCONNECTED,
SC_MARK_LCORNERCURVE, SC_MARK_TCORNERCURVE, SC_MARK_CIRCLEPLUS,
SC_MARK_CIRCLEPLUSCONNECTED, SC_MARK_CIRCLEMINUS, SC_MARK_CIRCLEMINUSCONNECTED,
SC_MARK_BACKGROUND, SC_MARK_DOTDOTDOT, SC_MARK_ARROWS, SC_MARK_PIXMAP,
SC_MARK_FULLRECT, SC_MARK_LEFTRECT, SC_MARK_CHARACTER

默認是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");
?
#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); 
        
    
}

這里TForm1::WndProc方法是Scintilla父窗體即我們的TForm1的窗口處理函數。

代碼折疊以后我們要通過點擊頁邊上的+和-標記來打開和折疊代碼,所以需要頁邊接收鼠標點擊事件:

這里TForm1::WndProc方法是Scintilla父窗體即我們的TForm1的窗口處理函數。

代碼折疊以后我們要通過點擊頁邊上的+和-標記來打開和折疊代碼,所以需要頁邊接收鼠標點擊事件:

SendEditor(SCI_SETMARGINSENSITIVEN, MARGIN_FOLD_INDEX, TRUE); //響應鼠標消息

這樣,當有鼠標點擊該頁邊后,Scintilla就會向它的父窗體發送代碼為SCN_MARGINCLICK的WM_NOTIFY消息,其中的LParam為SCNotification*類型。關於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就可以,想必ACDSee也行吧。

xpm比較特殊的地方是它可以作為頭文件直接被C語言調用,吃驚吧?用文本編輯器打開它看看,呵呵,其實它就是一個數組定義。

如下面這個數據(代碼)就是后面馬上就要用到的minus.xpm和plus.xpm圖片文件的內容(從eclipse里挖出來的):

?
/* XPM */
static const char *minus_xpm[] = {
/* width height num_colors chars_per_pixel */
"     9     9       16            1" ,
/* colors */
"` c #8c96ac" ,
". c #c4d0da" ,
"# c #daecf4" ,
"a c #ccdeec" ,
"b c #eceef4" ,
"c c #e0e5eb" ,
"d c #a7b7c7" ,
"e c #e4ecf0" ,
"f c #d0d8e2" ,
"g c #b7c5d4" ,
"h c #fafdfc" ,
"i c #b4becc" ,
"j c #d1e6f1" ,
"k c #e4f2fb" ,
"l c #ecf6fc" ,
"m c #d4dfe7" ,
/* pixels */
"hbc.i.cbh" ,
"bffeheffb" ,
"mfllkllfm" ,
"gjkkkkkji" ,
"da`````jd" ,
"ga#j##jai" ,
"f.k##k#.a" ,
"#..jkj..#" ,
"hemgdgc#h"
};
?
/* XPM */
static const char *plus_xpm[] = {
/* width height num_colors chars_per_pixel */
"     9     9       16            1" ,
/* colors */
"` c #242e44" ,
". c #8ea0b5" ,
"# c #b7d5e4" ,
"a c #dcf2fc" ,
"b c #a2b8c8" ,
"c c #ccd2dc" ,
"d c #b8c6d4" ,
"e c #f4f4f4" ,
"f c #accadc" ,
"g c #798fa4" ,
"h c #a4b0c0" ,
"i c #cde5f0" ,
"j c #bcdeec" ,
"k c #ecf1f6" ,
"l c #acbccc" ,
"m c #fcfefc" ,
/* pixels */
"mech.hcem" ,
"eldikille" ,
"dlaa`akld" ,
".#ii`ii#." ,
"g#`````fg" ,
".fjj`jjf." ,
"lbji`ijbd" ,
"khb#idlhk" ,
"mkd.ghdkm"
};

演示,使用自定義圖形:

?
#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有一拼了吧^_^):

怎樣支持自動縮進

在VS里編寫C++代碼時,輸入回車換行后會保持和上一行的縮進一致,輸入"{'字符后回車還會幫我們多縮進一次,輸入'}'后又能自動退回。我們的編輯器也要實現這個功能。

現在再仔細了解一下Scintilla的通知消息(http://scintilla.sourceforge.net/ScintillaDoc.html#Notifications),除了前面用到的頁邊點擊事件外,還有很多事件非常有用。

實現自動縮進功能我們要關心的事件通知是SCN_CHARADDEDSCN_UPDATEUI

  • 當用戶輸入一個字符時,SCN_CHARADDED事件觸發,SCNotification的ch成員保存了輸入的字符。
  • 當更新文檔界面時,SCN_UPDATEUI事件觸發。輸入字符,改變字體風格,改變選區都會引起界面更新

演示代碼

改寫TForm1::WndProc,處理這兩個事件,我們的編輯器支持自動縮進啦

?
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的代碼完成和函數提示功能是很值得稱道的,它們可以極大地提高我們的編程效率(造成我現在寫代碼時往往只記住前四個字母,如果在對象后面點了小數點后不出現提示就會心慌意亂的說-_-),盡管有時也會失效。

做為IDE這個功能是絕對不能少D。即使你只打算做個編輯器,如果有這個功能那也是一大亮點啊~~(目前很多代碼編輯器都沒這個功能的說)。

關於函數提示的幾個命令以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 = s

再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM