12.12.5 使用JTextPane

使用EditPlusEclipse等工具时会发现,当在这些工具中输入代码时,如果输入的单词是程序关键字、类名等,则这些关键字将会自动变色。使用JTextPane组件,就可以开发出这种带有语法高亮的编辑器
JTextPane使用StyledDocument作为它的model对象,而StyleDocument允许对文档的不同段落分别设置不同的颜色、字体属性

Document使用Element来表示文档中的组成部分,Element可以表示章(chapter)、段落(paragraph)等.
在普通文档中,Element也可以表示一行。

AttributeSet接口

为了设置StyledDocument中文字的字体、颜色,Swing提供了AttributeSet接口来表示文档字体、颜色等属性

SwingStyledDocument提供了DefaultstyledDocument实现类,该实现类就是JTextPanemodel实现类;为AttributeSet接口提供了MutableAttributeSet子接口,并为该接口提供了SimpleAttributeSet实现类,程序通过这些接口和实现类就可以很好地控制JTextPane中文字的字体和颜色。

StyledDocument

StyledDocument提供了如下一个方法来设置文档中局部文字的字体、颜色

方法 描述
void setParagraphAttributes(int offset, int length, AttributeSet s, boolean replace) 设置文档中从offset开始,长度为length处的文字使用s属性(控制字体、颜色等),最后一个参数控制新属性是替换原有属性,还是将新属性累加到原有属性上。

StyleConstants工具类

AttributeSet的常用实现类是MutableAttributeSet,为了给MutableAttributeSet对象设置字体、颜色等属性,Swing提供了StyleConstants工具类,该工具类里大致包含了如下常用的静态方法来设置MutableAttributeSet里的字体、颜色等。

方法 描述
static void setAlignment(MutableAttributeSet a, int align) 设置文本对齐方式。
static void setBackground(MutableAttributeSet a, Color fg) 设置背景色
static void setBold(MutableAttributeSet a, boolean b) 设置是否使用粗体字
static void setFirstLineIndent(MutableAttributeSet a, float i) 设置首行缩进的大小。
static void setFontFamily(MutableAttributeSet a, String fam) 设置字体。
static void setFontSize(MutableAttributeSet a, int s) 设置字体大小。
static void setForeground(MutableAttributeSet a, Color fg) 设置字体前景色。
static void setItalic(MutableAttributeSet a, boolean b) 设置是否采用斜体字。
static void setLeftIndent(MutableAttributeSet a, float i) 设置左边缩进大小。
static void setLineSpacing(MutableAttributeSet a, float i) 设置行间距。
static void setRightIndent(MutableAttributeSet a, float i) 设置右边缩进大小。
static void setStrikeThrough(MutableAttributeSet a, boolean b) 设置是否为文字添加删除线。
static void setSubscript(MutableAttributeSet a, boolean b) 设置将指定文字设置成下标。
static void setSuperscript(MutableAttributeSet a, boolean b) 设置将指定文字设置成上标。
static void setUnderline(MutableAttributeSet a, boolean b) 设置是否为文字添加下画线。

上面这些方法用于控制文档中文字的外观样式,如果读者对这些外观样式不是太熟悉,则可以参考Word里设置“字体”属性的设置效果。
图12.60显示了Document及其相关实现类,以及相关辅助类的类关系图。

程序 文本编辑器设置 字体 颜色

下面程序简单地定义了三个SimpleAttributeSet对象,并为这三个对象设置了对应的文字、颜色、字体等属性,并使用三个SimpleAttributeSet对象设置文档中三段文字的外观。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;

public class JTextPaneTest {
JFrame mainWin = new JFrame("测试JTextPane");
JTextPane txt = new JTextPane();
StyledDocument doc = txt.getStyledDocument();
// 定义三个SimpleAttributeSet对象
SimpleAttributeSet android = new SimpleAttributeSet();
SimpleAttributeSet java = new SimpleAttributeSet();
SimpleAttributeSet javaee = new SimpleAttributeSet();

public void init() {
// 为android属性集设置颜色、字体大小、字体和下划线
StyleConstants.setForeground(android, Color.RED);
StyleConstants.setFontSize(android, 24);
StyleConstants.setFontFamily(android, "Dialog");
StyleConstants.setUnderline(android, true);
// 为java属性集设置颜色、字体大小、字体和粗体字
StyleConstants.setForeground(java, Color.BLUE);
StyleConstants.setFontSize(java, 30);
StyleConstants.setFontFamily(java, "Arial Black");
StyleConstants.setBold(java, true);
// 为javaee属性集设置颜色、字体大小、斜体字
StyleConstants.setForeground(javaee, Color.GREEN);
StyleConstants.setFontSize(javaee, 32);
StyleConstants.setItalic(javaee, true);
// 设置不允许编辑
txt.setEditable(false);
txt.setText("疯狂Android讲义\n" + "疯狂Java讲义\n" + "轻量级Java EE企业应用实战\n");
// 分别为文档中三段文字设置不同的外观样式
doc.setCharacterAttributes(0, 12, android, true);
doc.setCharacterAttributes(12, 12, java, true);
doc.setCharacterAttributes(24, 30, javaee, true);
mainWin.add(new JScrollPane(txt), BorderLayout.CENTER);
// 获取屏幕尺寸
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int inset = 100;
// 设置主窗口的大小
mainWin.setBounds(inset, inset, screenSize.width - inset * 2, screenSize.height - inset * 2);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.setVisible(true);
}

public static void main(String[] args) {
new JTextPaneTest().init();
}
}

上面程序其实很简单,程序中的第一段粗体字代码为三个SimpleAttributeSet对象设置了字体、字体大小、颜色等外观样式,第二段粗体字代码使用前面的三个SimpleAttributeSet对象来控制文档中三段文字的外观样式。运行上面程序,将看到如图12.61所示的界面。

图12.61

从图12.61中可以看出,窗口中文字具有丰富的外观,而且还可以选中这些文字,表明它们依然是文字,而不是直接绘制上去的图形。
如果希望开发出类似于EditplusEclipse等的代码编辑窗口,程序可以扩展JTextPane的子类,为该对象添加按键监听器和文档监听器。当文档内容被修改时,或者用户在该文档内进行击键动作时,程序负责分析该文档的内容,对特殊关键字设置字体颜色。

为了保证具有较好的性能,程序并不总是分析文档中的所有内容,而是只分析文档中被改变的部分,这个要求看似简单,只为文档添加文档监听器即可:当文档内容改变时分析被改变部分,并设置其中关键字的颜色。但问题是:Documentlistener监听器里的三个方法不能改变文档本身,所以程序还是必须通过监听按键事件来启动语法分析,DocumentListener监听器中仅仅记录文档改变部分的位置和长度。

除此之外,程序还提供了一个SyntaxFormatter类根据语法文件来设置文档中的文字颜色。

程序 带语法高亮的文本编辑器 单词着色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

public class MyTextPane extends JTextPane {
private static final long serialVersionUID = 7177281290506716762L;
protected StyledDocument doc;
protected SyntaxFormatter formatter = new SyntaxFormatter("my.stx");
// 定义该文档的普通文本的外观属性
private SimpleAttributeSet normalAttr = formatter.getNormalAttributeSet();
private SimpleAttributeSet quotAttr = new SimpleAttributeSet();
// 保存文档改变的开始位置
private int docChangeStart = 0;
// 保存文档改变的长度
private int docChangeLength = 0;

public MyTextPane() {
StyleConstants.setForeground(quotAttr, new Color(255, 0, 255));
StyleConstants.setFontSize(quotAttr, 16);
this.doc = super.getStyledDocument();
// 设置该文档的页边距
this.setMargin(new Insets(3, 40, 0, 0));
// 添加按键监听器,当按键松开时进行语法分析
this.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent ke) {
syntaxParse();
}
});
// 添加文档监听器
doc.addDocumentListener(new DocumentListener() {
// 当Document的属性或属性集发生了改变时触发该方法
public void changedUpdate(DocumentEvent e) {
}

// 当向Document中插入文本时触发该方法
public void insertUpdate(DocumentEvent e) {
docChangeStart = e.getOffset();
docChangeLength = e.getLength();
}

// 当从Document中删除文本时触发该方法
public void removeUpdate(DocumentEvent e) {
}
});
}

public void syntaxParse() {
try {
// 获取文档的根元素,即文档内的全部内容
Element root = doc.getDefaultRootElement();
// 获取文档中光标插入符的位置
int cursorPos = this.getCaretPosition();
int line = root.getElementIndex(cursorPos);
// 获取光标所在位置的行
Element para = root.getElement(line);
// 定义光标所在行的行头在文档中位置
int start = para.getStartOffset();
// 让start等于start与docChangeStart中较小值。
start = start > docChangeStart ? docChangeStart : start;
// 定义被修改部分的长度
int length = para.getEndOffset() - start;
length = length < docChangeLength ? docChangeLength + 1 : length;
// 取出所有可能被修改的字符串
String s = doc.getText(start, length);
// 以空格、点号等作为分隔符
String[] tokens = s.split("\\s+|\\.|\\(|\\)|\\{|\\}|\\[|\\]");
// 定义当前分析单词的在s字符串中的开始位置
int curStart = 0;
// 定义单词是否处于引号以内
boolean isQuot = false;
for (String token : tokens) {
// 找出当前分析单词在s字符串中的位置
int tokenPos = s.indexOf(token, curStart);
if (isQuot && (token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
isQuot = false;
} else if (isQuot && !(token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
} else if ((token.startsWith("\"") || token.startsWith("\'"))
&& (token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
} else if ((token.startsWith("\"") || token.startsWith("\'"))
&& !(token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
isQuot = true;
} else {
// 使用格式器对当前单词设置颜色
formatter.setHighLight(doc, token, start + tokenPos, token.length());
}
// 开始分析下一个单词
curStart = tokenPos + token.length();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}

// 重画该组件,设置行号
public void paint(Graphics g) {
super.paint(g);
Element root = doc.getDefaultRootElement();
// 获得行号
int line = root.getElementIndex(doc.getLength());
// 设置颜色
g.setColor(new Color(230, 230, 230));
// 绘制显示行数的矩形框
g.fillRect(0, 0, this.getMargin().left - 10, getSize().height);
// 设置行号的颜色
g.setColor(new Color(40, 40, 40));
// 每行绘制一个行号
for (int count = 0, j = 1; count <= line; count++, j++) {
g.drawString(String.valueOf(j), 3, (int) ((count + 1) * 1.535 * StyleConstants.getFontSize(normalAttr)));
}
}

public static void main(String[] args) {
JFrame frame = new JFrame("文本编辑器");
// 使用MyTextPane
frame.getContentPane().add(new JScrollPane(new MyTextPane()));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final int inset = 50;
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
frame.setBounds(inset, inset, screenSize.width - inset * 2, screenSize.height - inset * 2);
frame.setVisible(true);
}
}

// 定义语法格式器
class SyntaxFormatter {
// 以一个Map保存关键字和颜色的对应关系
private Map<SimpleAttributeSet, ArrayList<String>> attMap = new HashMap<>();
// 定义文档的正常文本的外观属性
SimpleAttributeSet normalAttr = new SimpleAttributeSet();

public SyntaxFormatter(String syntaxFile) {
// 设置正常文本的颜色、大小
StyleConstants.setForeground(normalAttr, Color.BLACK);
StyleConstants.setFontSize(normalAttr, 16);
// 创建一个Scanner对象,负责根据语法文件加载颜色信息
Scanner scaner = null;
try {
scaner = new Scanner(new File(syntaxFile));
} catch (FileNotFoundException e) {
throw new RuntimeException("丢失语法文件:" + e.getMessage());
}
int color = -1;
ArrayList<String> keywords = new ArrayList<>();
// 不断读取语法文件的内容行
while (scaner.hasNextLine()) {
String line = scaner.nextLine();
// 如果当前行以#开头
if (line.startsWith("#")) {
if (keywords.size() > 0 && color > -1) {
// 取出当前行的颜色值,并封装成SimpleAttributeSet对象
SimpleAttributeSet att = new SimpleAttributeSet();
StyleConstants.setForeground(att, new Color(color));
StyleConstants.setFontSize(att, 16);
// 将当前颜色和关键字List对应起来
attMap.put(att, keywords);
}
// 重新创建新的关键字List,为下一个语法格式准备
keywords = new ArrayList<>();
color = Integer.parseInt(line.substring(1), 16);
} else {
// 对于普通行,每行内容添加到关键字List里
if (line.trim().length() > 0) {
keywords.add(line.trim());
}
}
}
// 把所有关键字和颜色对应起来
if (keywords.size() > 0 && color > -1) {
SimpleAttributeSet att = new SimpleAttributeSet();
StyleConstants.setForeground(att, new Color(color));
StyleConstants.setFontSize(att, 16);
attMap.put(att, keywords);
}
}

// 返回该格式器里正常文本的外观属性
public SimpleAttributeSet getNormalAttributeSet() {
return normalAttr;
}

// 设置语法高亮
public void setHighLight(StyledDocument doc, String token, int start, int length) {
// 保存需要对当前单词对应的外观属性
SimpleAttributeSet currentAttributeSet = null;
outer: for (SimpleAttributeSet att : attMap.keySet()) {
// 取出当前颜色对应的所有关键字
ArrayList<String> keywords = attMap.get(att);
// 遍历所有关键字
for (String keyword : keywords) {
// 如果该关键字与当前单词相同
if (keyword.equals(token)) {
// 跳出循环,并设置当前单词对应的外观属性
currentAttributeSet = att;
break outer;
}
}
}
// 如果当前单词对应的外观属性不为空
if (currentAttributeSet != null) {
// 设置当前单词的颜色
doc.setCharacterAttributes(start, length, currentAttributeSet, false);
}
// 否则使用普通外观来设置该单词
else {
doc.setCharacterAttributes(start, length, normalAttr, false);
}
}
}

配置文件

展开/折叠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#FF0000
System
Class
String
Integer
Object


#0000FF
abstract
assert
boolean
break
byte
case
catch
char
class
const
continue
default
do
double
else
enum
extends
false
final
finally
float
for
goto
if
import
implements
int
interface
instanceof
long
native
new
null
package
private
protected
public
return
short
static
strictfp
super
switch
synchronized
this
throw
throws
transient
try
true
void
volatile
while

上面程序中的粗体字代码负责分析当前单词与哪种颜色关键字匹配,并为这段文字设置字体颜色。其实这段程序为文档中的单词设置颜色并不难,难点在于找出每个单词与哪种关键字匹配,并要标识出该单词在文档中的位置,然后才可以为该单词设置颜色。
运行上面程序,会看到如图12.62所示的带语法高亮的文本编辑器

图12.62

上面程序已经完成了对不同类型的单词进行着色,所以会看到如图12.62示的运行界面。如果进行改进,则可以为上面的编辑器增加括号配对、代码折叠等功能,这些都可以通过JTextPane组件来完成对于此文本编辑器,只要传入不同的语法文件,程序就可以为不同的源代码显示语法高亮