寫自己的文字編輯器(一): 高亮關鍵字
一. 高亮的內容:
需要高亮的內容有:
1. 關鍵字, 如 public, int, true 等.
2. 運算子, 如 +, -, *, /等
3. 數字
4. 高亮字串, 如 "example of string"
5. 高亮單行注釋
6. 高亮多行注釋
二. 實現高亮的核心方法:
StyledDocument.setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace)
三. 文字編輯器選擇.
Java中提供的多行文字編輯器有: JTextComponent, JTextArea, JTextPane, JEditorPane等, 都可以使用. 但是因為文法著色中文本要使用多種風格的樣式, 所以這些文字編輯器的document要使用StyledDocument.
JTextArea使用的是PlainDocument, 此document不能進行多種格式的著色.
JTextPane, JEditorPane使用的是StyledDocument, 預設就可以使用.
為了實現文法著色, 可以繼承自DefaultStyledDocument, 設定其為這些文字編輯器的documet, 或者也可以直接使用JTextPane, JEditorPane來做. 為了方便, 這裡就直接使用JTextPane了.
四. 何時進行著色.
當文字編輯器中有字元被插入或者刪除時, 文本的內容就發生了變化, 這時檢查, 進行著色.
為了監視到文本的內容發生了變化, 要給document添加一個DocumentListener監聽器, 在他的removeUpdate和insertUpdate中進行著色處理.
而changedUpdate方法在文本的屬性例如前景色彩, 背景色, 字型等風格改變時才會被調用.
@Override<br /> public void changedUpdate(DocumentEvent e) {</p><p> }</p><p> @Override<br /> public void insertUpdate(DocumentEvent e) {<br /> try {<br /> colouring((StyledDocument) e.getDocument(), e.getOffset(), e.getLength());<br /> } catch (BadLocationException e1) {<br /> e1.printStackTrace();<br /> }<br /> }</p><p> @Override<br /> public void removeUpdate(DocumentEvent e) {<br /> try {<br /> // 因為刪除後游標緊接著影響的單詞兩邊, 所以長度就不需要了<br /> colouring((StyledDocument) e.getDocument(), e.getOffset(), 0);<br /> } catch (BadLocationException e1) {<br /> e1.printStackTrace();<br /> }<br /> }<br />
五. 著色範圍:
pos: 指變化前游標的位置.
len: 指變化的字元數.
例如有關鍵字public, int
單詞"publicint", 在"public"和"int"中插入一個空格後變成"public int", 一個單詞變成了兩個, 這時對"public" 和 "int"進行著色.
著色範圍是public中p的位置和int中t的位置加1, 即是pos前面單詞開始的下標和pos+len開始單詞結束的下標. 所以上例中要著色的範圍是"public int".
提供了方法indexOfWordStart來取得pos前單詞開始的下標, 方法indexOfWordEnd來取得pos後單詞結束的下標.
public int indexOfWordStart(Document doc, int pos) throws BadLocationException {<br /> // 從pos開始向前找到第一個非單詞字元.<br /> for (; pos > 0 && isWordCharacter(doc, pos - 1); --pos);<br /> return pos;<br /> }</p><p> public int indexOfWordEnd(Document doc, int pos) throws BadLocationException {<br /> // 從pos開始向前找到第一個非單詞字元.<br /> for (; isWordCharacter(doc, pos); ++pos);<br /> return pos;<br /> }</p><p>
一個字元是單詞的有效字元: 是字母, 數字, 底線.
public boolean isWordCharacter(Document doc, int pos) throws BadLocationException {<br /> char ch = getCharAt(doc, pos); // 取得在文檔中pos位置處的字元<br /> if (Character.isLetter(ch) || Character.isDigit(ch) || ch == '_') { return true; }<br /> return false;<br /> }</p><p>
所以著色的範圍是[start, end] :
int start = indexOfWordStart(doc, pos);<br />int end = indexOfWordEnd(doc, pos + len);<br />
六. 關鍵字著色.
從著色範圍的開始下標起進行判斷, 如果是以字母開或者底線開頭, 則說明是單詞, 那麼先取得這個單詞, 如果這個單詞是關鍵字, 就進行關鍵字著色, 如果不是, 就進行普通的著色. 著色完這個單詞後, 繼續後面的著色處理. 已經著色過的字元, 就不再進行著色了.
public void colouring(StyledDocument doc, int pos, int len) throws BadLocationException {<br /> // 取得插入或者刪除後影響到的單詞.<br /> // 例如"public"在b後插入一個空格, 就變成了:"pub lic", 這時就有兩個單詞要處理:"pub"和"lic"<br /> // 這時要取得的範圍是pub中p前面的位置和lic中c後面的位置<br /> int start = indexOfWordStart(doc, pos);<br /> int end = indexOfWordEnd(doc, pos + len);</p><p> char ch;<br /> while (start < end) {<br /> ch = getCharAt(doc, start);<br /> if (Character.isLetter(ch) || ch == '_') {<br /> // 如果是以字母或者底線開頭, 說明是單詞<br /> // pos為處理後的最後一個下標<br /> start = colouringWord(doc, start);<br /> } else {<br /> //SwingUtilities.invokeLater(new ColouringTask(doc, pos, wordEnd - pos, normalStyle));<br /> ++start;<br /> }<br /> }<br /> }</p><p> public int colouringWord(StyledDocument doc, int pos) throws BadLocationException {<br /> int wordEnd = indexOfWordEnd(doc, pos);<br /> String word = doc.getText(pos, wordEnd - pos); // 要進行著色的單詞</p><p> if (keywords.contains(word)) {<br /> // 如果是關鍵字, 就進行關鍵字的著色, 否則使用普通的著色.<br /> // 這裡有一點要注意, 在insertUpdate和removeUpdate的方法調用的過程中, 不能修改doc的屬性.<br /> // 但我們又要達到能夠修改doc的屬性, 所以把此任務放到這個方法的外面去執行.<br /> // 實現這一目的, 可以使用新線程, 但放到swing的事件隊列裡去處理更輕便一點.<br /> SwingUtilities.invokeLater(new ColouringTask(doc, pos, wordEnd - pos, keywordStyle));<br /> } else {<br /> SwingUtilities.invokeLater(new ColouringTask(doc, pos, wordEnd - pos, normalStyle));<br /> }</p><p> return wordEnd;<br /> }</p><p>因為在insertUpdate和removeUpdate方法中不能修改document的屬性, 所以著色的任務放到這兩個方法外面, 所以使用了SwingUtilities.invokeLater來實現.<br /> private class ColouringTask implements Runnable {<br /> private StyledDocument doc;<br /> private Style style;<br /> private int pos;<br /> private int len;</p><p> public ColouringTask(StyledDocument doc, int pos, int len, Style style) {<br /> this.doc = doc;<br /> this.pos = pos;<br /> this.len = len;<br /> this.style = style;<br /> }</p><p> public void run() {<br /> try {<br /> // 這裡就是對字元進行著色<br /> doc.setCharacterAttributes(pos, len, style, true);<br /> } catch (Exception e) {}<br /> }<br /> }</p><p>
七: 源碼
關鍵字著色的完成代碼如下, 可以直接編譯運行. 對於數字, 運算子, 字串等的著色處理在以後的教程中會繼續進行詳解.
import java.awt.Color;<br />import java.util.HashSet;<br />import java.util.Set;</p><p>import javax.swing.JFrame;<br />import javax.swing.JTextPane;<br />import javax.swing.SwingUtilities;<br />import javax.swing.event.DocumentEvent;<br />import javax.swing.event.DocumentListener;<br />import javax.swing.text.BadLocationException;<br />import javax.swing.text.Document;<br />import javax.swing.text.Style;<br />import javax.swing.text.StyleConstants;<br />import javax.swing.text.StyledDocument;</p><p>public class HighlightKeywordsDemo {<br /> public static void main(String[] args) {<br /> JFrame frame = new JFrame();</p><p> JTextPane editor = new JTextPane();<br /> editor.getDocument().addDocumentListener(new SyntaxHighlighter(editor));<br /> frame.getContentPane().add(editor);</p><p> frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);<br /> frame.setSize(500, 500);<br /> frame.setVisible(true);<br /> }<br />}</p><p>/**<br /> * 當文本輸入區的有字元插入或者刪除時, 進行高亮.<br /> *<br /> * 要進行文法高亮, 文本輸入組件的document要是styled document才行. 所以不要用JTextArea. 可以使用JTextPane.<br /> *<br /> * @author Biao<br /> *<br /> */<br />class SyntaxHighlighter implements DocumentListener {<br /> private Set<String> keywords;<br /> private Style keywordStyle;<br /> private Style normalStyle;</p><p> public SyntaxHighlighter(JTextPane editor) {<br /> // 準備著色使用的樣式<br /> keywordStyle = ((StyledDocument) editor.getDocument()).addStyle("Keyword_Style", null);<br /> normalStyle = ((StyledDocument) editor.getDocument()).addStyle("Keyword_Style", null);<br /> StyleConstants.setForeground(keywordStyle, Color.RED);<br /> StyleConstants.setForeground(normalStyle, Color.BLACK);</p><p> // 準備關鍵字<br /> keywords = new HashSet<String>();<br /> keywords.add("public");<br /> keywords.add("protected");<br /> keywords.add("private");<br /> keywords.add("_int9");<br /> keywords.add("float");<br /> keywords.add("double");<br /> }</p><p> public void colouring(StyledDocument doc, int pos, int len) throws BadLocationException {<br /> // 取得插入或者刪除後影響到的單詞.<br /> // 例如"public"在b後插入一個空格, 就變成了:"pub lic", 這時就有兩個單詞要處理:"pub"和"lic"<br /> // 這時要取得的範圍是pub中p前面的位置和lic中c後面的位置<br /> int start = indexOfWordStart(doc, pos);<br /> int end = indexOfWordEnd(doc, pos + len);</p><p> char ch;<br /> while (start < end) {<br /> ch = getCharAt(doc, start);<br /> if (Character.isLetter(ch) || ch == '_') {<br /> // 如果是以字母或者底線開頭, 說明是單詞<br /> // pos為處理後的最後一個下標<br /> start = colouringWord(doc, start);<br /> } else {<br /> SwingUtilities.invokeLater(new ColouringTask(doc, start, 1, normalStyle));<br /> ++start;<br /> }<br /> }<br /> }</p><p> /**<br /> * 對單詞進行著色, 並返回單詞結束的下標.<br /> *<br /> * @param doc<br /> * @param pos<br /> * @return<br /> * @throws BadLocationException<br /> */<br /> public int colouringWord(StyledDocument doc, int pos) throws BadLocationException {<br /> int wordEnd = indexOfWordEnd(doc, pos);<br /> String word = doc.getText(pos, wordEnd - pos);</p><p> if (keywords.contains(word)) {<br /> // 如果是關鍵字, 就進行關鍵字的著色, 否則使用普通的著色.<br /> // 這裡有一點要注意, 在insertUpdate和removeUpdate的方法調用的過程中, 不能修改doc的屬性.<br /> // 但我們又要達到能夠修改doc的屬性, 所以把此任務放到這個方法的外面去執行.<br /> // 實現這一目的, 可以使用新線程, 但放到swing的事件隊列裡去處理更輕便一點.<br /> SwingUtilities.invokeLater(new ColouringTask(doc, pos, wordEnd - pos, keywordStyle));<br /> } else {<br /> SwingUtilities.invokeLater(new ColouringTask(doc, pos, wordEnd - pos, normalStyle));<br /> }</p><p> return wordEnd;<br /> }</p><p> /**<br /> * 取得在文檔中下標在pos處的字元.<br /> *<br /> * 如果pos為doc.getLength(), 返回的是一個文檔的結束符, 不會拋出異常. 如果pos<0, 則會拋出異常.<br /> * 所以pos的有效值是[0, doc.getLength()]<br /> *<br /> * @param doc<br /> * @param pos<br /> * @return<br /> * @throws BadLocationException<br /> */<br /> public char getCharAt(Document doc, int pos) throws BadLocationException {<br /> return doc.getText(pos, 1).charAt(0);<br /> }</p><p> /**<br /> * 取得下標為pos時, 它所在的單詞開始的下標. ±wor^d± (^表示pos, ±表示開始或結束的下標)<br /> *<br /> * @param doc<br /> * @param pos<br /> * @return<br /> * @throws BadLocationException<br /> */<br /> public int indexOfWordStart(Document doc, int pos) throws BadLocationException {<br /> // 從pos開始向前找到第一個非單詞字元.<br /> for (; pos > 0 && isWordCharacter(doc, pos - 1); --pos);</p><p> return pos;<br /> }</p><p> /**<br /> * 取得下標為pos時, 它所在的單詞結束的下標. ±wor^d± (^表示pos, ±表示開始或結束的下標)<br /> *<br /> * @param doc<br /> * @param pos<br /> * @return<br /> * @throws BadLocationException<br /> */<br /> public int indexOfWordEnd(Document doc, int pos) throws BadLocationException {<br /> // 從pos開始向前找到第一個非單詞字元.<br /> for (; isWordCharacter(doc, pos); ++pos);</p><p> return pos;<br /> }</p><p> /**<br /> * 如果一個字元是字母, 數字, 底線, 則返回true.<br /> *<br /> * @param doc<br /> * @param pos<br /> * @return<br /> * @throws BadLocationException<br /> */<br /> public boolean isWordCharacter(Document doc, int pos) throws BadLocationException {<br /> char ch = getCharAt(doc, pos);<br /> if (Character.isLetter(ch) || Character.isDigit(ch) || ch == '_') { return true; }<br /> return false;<br /> }</p><p> @Override<br /> public void changedUpdate(DocumentEvent e) {</p><p> }</p><p> @Override<br /> public void insertUpdate(DocumentEvent e) {<br /> try {<br /> colouring((StyledDocument) e.getDocument(), e.getOffset(), e.getLength());<br /> } catch (BadLocationException e1) {<br /> e1.printStackTrace();<br /> }<br /> }</p><p> @Override<br /> public void removeUpdate(DocumentEvent e) {<br /> try {<br /> // 因為刪除後游標緊接著影響的單詞兩邊, 所以長度就不需要了<br /> colouring((StyledDocument) e.getDocument(), e.getOffset(), 0);<br /> } catch (BadLocationException e1) {<br /> e1.printStackTrace();<br /> }<br /> }</p><p> /**<br /> * 完成著色任務<br /> *<br /> * @author Biao<br /> *<br /> */<br /> private class ColouringTask implements Runnable {<br /> private StyledDocument doc;<br /> private Style style;<br /> private int pos;<br /> private int len;</p><p> public ColouringTask(StyledDocument doc, int pos, int len, Style style) {<br /> this.doc = doc;<br /> this.pos = pos;<br /> this.len = len;<br /> this.style = style;<br /> }</p><p> public void run() {<br /> try {<br /> // 這裡就是對字元進行著色<br /> doc.setCharacterAttributes(pos, len, style, true);<br /> } catch (Exception e) {}<br /> }<br /> }<br />}<br />