本教程适用于JDK 8。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
有关Java SE 9及后续版本中更新的语言功能的摘要,请参阅Java语言更改。
有关所有JDK版本的新功能、增强功能和已删除或不建议使用的选项的信息,请参阅JDK发布说明。
本节讨论如何使用java.awt和java.awt.font包中的类处理双向文本。这些类允许您在任何由Unicode标准支持的语言或脚本中绘制样式化文本:Unicode标准是处理各种现代、古典和历史语言的全球字符编码系统。在绘制文本时,必须考虑到文本的阅读方向,以便字符串中的所有单词都能正确显示。这些类保持文本的方向,并正确绘制文本,无论字符串是从左到右、从右到左还是双向运行。双向文本在正确定位插入符、准确定位选择和正确显示多行方面存在有趣的问题。同时,双向和从右到左的文本在响应右箭头和左箭头键按下时在正确方向上移动插入符也存在类似的问题。
以下主题将被涵盖:
如果您计划使用Swing组件,请参阅使用JTextComponent类处理双向文本和使用文本组件获取更多信息。
Java SE将文本以逻辑顺序存储在内存中,这是字符和单词的阅读和写入顺序。逻辑顺序不一定与视觉顺序相同,视觉顺序是对应字形显示的顺序。
即使混合使用语言,书写系统的视觉顺序在双向文本中也必须保持。下图显示了嵌入在英语句子中的阿拉伯语短语,以说明这一点。
注意:在这个和后面的例子中,阿拉伯语和希伯来语的文本以大写字母表示,空格以下划线表示。每个示例都包含两个部分:内存中存储的字符的表示(逻辑顺序的字符)后跟显示这些字符的表示(视觉顺序的字符)。字符框下面的数字表示插入偏移量。
尽管它们是英语句子的一部分,但阿拉伯语单词以阿拉伯语字母顺序从右到左显示。由于斜体的阿拉伯语单词在逻辑上位于普通文本的阿拉伯语之后,所以在视觉上位于普通文本的左边。
当显示一行混合了从左到右和从右到左文本的文本时,基本方向非常重要。基本方向是主要书写系统的脚本顺序。例如,如果文本主要是英语并嵌入了一些阿拉伯语,则基本方向是从左到右。如果文本主要是阿拉伯语并嵌入了一些英语或数字,则基本方向是从右到左。
基本方向确定了以相同方向的文本段的顺序显示。在前一个示例中,基本方向是从左到右。此示例中有三个方向运行:句子开头的英文文本从左到右运行,阿拉伯文本从右到左运行,句号从左到右运行。
图形通常嵌入在文本流中。这些内联图形在影响文本流和换行方面的行为类似于字形。因此,这些内联图形需要使用相同的双向布局算法定位,以便它们在字符流中出现在正确的位置。
Java SE使用Unicode双向算法,这是一种用于在一行内对字形进行排序,从而确定双向文本方向性的算法。在大多数情况下,您不需要包含任何额外的信息,以便该算法获取正确的显示顺序。
为了允许用户编辑双向文本,您必须能够执行以下操作:
在可编辑文本中,使用插入符号来图形化表示当前插入点,即新字符将插入文本的位置。通常,插入符号显示为两个字形之间闪烁的竖线。新字符被插入并在插入符号的位置显示。
计算插入符位置可能会很复杂,特别是对于双向文本。在定向边界上的插入偏移量有两个可能的插入符位置,因为与字符偏移对应的两个字形不会相邻显示。这在下图中有所说明。在这个图中,插入符以方括号的形式显示,以表示插入符对应的字形。
字符偏移量8对应于下划线之后、A之前的位置。如果用户输入一个阿拉伯字符,其字形将显示在A的右侧(之前);如果用户输入一个英文字符,其字形将显示在下划线的右侧(之后)。
为了处理这种情况,一些系统显示双插入符,一个强插入符和一个弱插入符。强插入符表示当插入字符的方向与文本的基本方向相同时,插入字符将显示的位置。弱插入符则表示当插入字符的方向与基本方向相反时,插入字符将显示的位置。TextLayout自动支持双插入符。
在处理双向文本时,不能简单地将字符偏移之前的字形宽度相加以计算插入符位置。如果这样做,插入符会出现在错误的位置,如下图所示:
为了使插入符正确定位,需要将偏移量左侧的字形宽度相加,并考虑当前上下文。如果不考虑上下文,字形度量可能与显示不匹配。(上下文可能会影响使用的字形。)
所有文本编辑器都允许用户使用箭头键移动插入符。用户期望插入符按照按下的箭头键的方向移动。在从左到右的文本中,移动插入偏移是简单的:右箭头键增加插入偏移一个单位,左箭头键减小插入偏移一个单位。在双向文本或带有连字的文本中,这种行为会导致插入符在方向边界跳跃并在不同方向运行中以相反方向移动。
为了在双向文本中平滑移动插入符,需要考虑文本运行的方向。不能简单地在按下右箭头键时增加插入偏移,在按下左箭头键时减小插入偏移。如果当前插入偏移在从右到左字符的运行内部,则右箭头键应减小插入偏移,左箭头键应增加插入偏移。
在跨越方向边界时,移动插入符号更加复杂。下图说明了用户使用箭头键导航时,当跨越方向边界时会发生什么。在显示文本中向右移动三个位置对应于移动到字符偏移量为7、19,然后是18。
某些字形之间永远不应该有插入符号;相反,插入符号应该像这些字形表示一个字符一样移动。例如,如果字形由两个单独的字符表示,那么字母o和变音标记之间永远不应该有插入符号。
TextLayout类提供了方法(getNextRightHit和getNextLeftHit),使您可以轻松地通过双向文本平滑地移动插入符号。
通常,需要将设备空间中的位置转换为文本偏移量。例如,当用户在可选择文本上点击鼠标时,鼠标的位置会被转换为文本偏移量,并用作选择范围的一个端点。从逻辑上讲,这与定位插入符号的过程相反。
在处理双向文本时,显示中的单个位置可能对应源文本中的两个不同偏移量,如下图所示:
由于单个视觉位置可能对应两个不同的偏移量,因此进行双向文本的命中测试不仅仅是测量字形宽度,直到找到正确位置的字形,然后将该位置映射回字符偏移量。检测到命中位置所在的方向有助于区分两个选择。
您可以使用TextLayout.hitTestChar进行命中测试。命中信息封装在一个TextHitInfo对象中,并包含有关命中位置所在方向的信息。
一段选定的字符范围通过高亮区域在图形上表示,高亮区域是一个区域,在其中字形以反色或不同的背景颜色显示。
高亮区域,就像插入符一样,在双向文本中比单向文本更复杂。在双向文本中,一个连续的字符范围在显示时可能不具有连续的高亮区域。相反,显示为连续的字形范围的高亮区域可能不对应于单个连续的字符范围。
这导致了在双向文本中突出显示选择的两种策略:
逻辑高亮:逻辑高亮中,所选字符在文本模型中始终是连续的,而高亮区域可以是不连续的。下面是逻辑高亮的示例:
视觉高亮:视觉高亮中,可能有多个所选字符范围,但高亮区域始终是连续的。下面是视觉高亮的示例:
逻辑高亮更容易实现,因为所选字符在文本中始终是连续的。
示例演示了逻辑高亮:SelectionSample.java
根据您使用的Java API,您可以根据需要对文本布局进行少量或大量的控制:
如果您只想显示一段文本或需要一个可编辑的文本控件,可以使用JTextComponent,它将为您执行文本布局。 JTextComponent被设计用于处理大多数国际应用程序的需求,并支持双向文本。有关JTextComponent的更多信息,请参阅使用JTextComponent类处理双向文本和使用文本组件。
如果您想显示一个简单的文本字符串,可以调用Graphics2D.drawString方法让Java 2D为您布局字符串。您还可以使用Graphics2D.drawString来渲染样式化字符串和包含双向文本的字符串。有关通过Graphics2D渲染文本的更多信息,请参阅使用文本API。
如果您想要实现自己的文本编辑功能,可以使用TextLayout来管理文本布局、高亮和点击检测。TextLayout提供的功能可以处理大多数常见情况,包括具有混合字体、混合语言和双向文本的文本字符串。有关使用TextLayout的更多信息,请参阅管理文本布局。
如果您想要完全控制文本的形状和位置,可以使用GlyphVector类构建自己的实例,然后通过Graphics2D类进行渲染。
通常情况下,您不需要自己执行文本布局操作。对于大多数应用程序来说,JTextComponent是显示静态和可编辑文本的最佳解决方案。然而,JTextComponent不支持在双向文本中显示双插符或不连续选择。如果您的应用程序需要这些功能,或者您更喜欢实现自己的文本编辑程序,可以使用Java 2D文本布局API。
TextLayout类支持包含多种样式和来自不同书写系统(包括阿拉伯语和希伯来语)的字符的文本。(阿拉伯语和希伯来语特别难以显示,因为您必须对文本进行重塑和重新排序以获得可接受的表示。)
即使您只处理英文文本,使用TextLayout,您也可以轻松实现高质量的排版效果,不需要额外的努力。
TextLayout设计得无论是用于显示简单的单向文本还是显示阿拉伯语或希伯来语文本时,都不会有明显的性能影响。当使用TextLayout显示阿拉伯语或希伯来语文本时,会有一些额外的处理开销。但是,它通常是每个字符微秒级的处理时间,并且受到正常绘图代码的执行的支配。
TextLayout类为您管理字形的定位和排序。您可以使用TextLayout执行以下操作:
TextLayout可以自动布局文本,包括双向文本,并正确地进行形状和排序。为了正确地形状和排序表示一行文本的字形,TextLayout必须了解文本的完整上下文:
TextLayout。TextLayout。您必须使用LineBreakMeasurer提供足够的上下文。有关更多信息,请参阅绘制多行文本。文本的基本方向通常由文本上的属性(样式)设置。如果该属性缺失,TextLayout将遵循Unicode双向算法,并从段落中的初始字符推导基本方向。
TextLayout维护插入符号信息,例如插入符号的Shape、位置和角度。您可以使用这些信息轻松地在单向和双向文本中显示插入符号。当为双向文本绘制插入符号时,使用TextLayout可以确保插入符号的正确定位。
TextLayout提供默认的插入符号Shape,并且自动支持双插入符号。对于斜体和倾斜字形,TextLayout会生成倾斜的插入符号,如下图所示。这些插入符号的位置也用作高亮和击中测试之间字形的边界,从而产生一致的用户体验。
给定一个插入偏移量,getCaretShapes 方法返回一个包含两个元素的 Shape 对象的数组:元素0包含强插入符,元素1包含弱插入符(如果存在)。要显示双插入符,只需绘制两个插入符 Shape 对象;插入符将自动在正确的位置呈现。
如果要使用自定义的插入符,可以从 TextLayout 中检索插入符的位置和角度,并自行绘制。
示例 演示了双插入符。HitTestSample.java
点击 o 旁边的 o 指向希伯来文本将记录结束用户在 o 之后点击,该字符是英文文本的一部分。这将使弱插入符(黑色)位于 o 旁边,并将强插入符(红色)位于 H 前面:
点击 o 右侧的空格将记录结束用户点击的空格,该空格是希伯来文本的一部分。这将使强插入符(红色)位于 o 旁边,并将弱插入符(黑色)位于 H 前面:
您还可以使用 TextLayout 类来确定用户按下左箭头或右箭头键时的插入偏移量。给定一个表示当前插入偏移量的 TextHitInfo 对象,getNextRightHit 方法返回一个表示按下右箭头键时正确插入偏移量的 TextHitInfo 对象。而 getNextLeftHit 方法提供了左箭头键的相同信息。
以下是从示例中提取的代码片段,演示了如何在用户按下左箭头或右箭头键时确定插入位置的偏移:ArrowKeySample.java
public class ArrowKeySample extends JPanel implements KeyListener {
// ...
private static void createAndShowGUI() {
// 创建和设置窗口。
ArrowKey demo = new ArrowKey();
frame = new JFrame("Arrow Key Sample");
frame.addKeyListener(demo);
// ...
}
private void handleArrowKey(boolean rightArrow) {
TextHitInfo newPosition;
if (rightArrow) {
newPosition = textLayout.getNextRightHit(insertionIndex);
} else {
newPosition = textLayout.getNextLeftHit(insertionIndex);
}
// 如果当前位置的右(左)边没有插入位置,则getNextRightHit() / getNextLeftHit()将返回null。
if (newPosition != null) {
// 更新插入位置。
insertionIndex = newPosition.getInsertionIndex();
// 重新绘制组件,以显示新的插入符号。
frame.repaint();
}
}
// ...
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_RIGHT) {
handleArrowKey(keyCode == KeyEvent.VK_RIGHT);
}
}
}
TextLayout类提供了一种简单的命中测试文本的机制。 hitTextChar方法接受鼠标的x和y坐标作为参数,并返回一个TextHitInfo对象。 TextHitInfo包含指定位置的插入偏移量和命中位置所在的一侧。 插入偏移量是最接近命中位置的偏移量:如果命中位置在行的末尾之后,则返回行末尾处的偏移量。
以下是从中提取的代码片段,用于检索鼠标单击的偏移量:HitTestSample.java
private class HitTestMouseListener extends MouseAdapter {
public void mouseClicked(MouseEvent e) {
Point2D origin = computeLayoutOrigin();
// 计算鼠标单击位置相对于textLayout原点的位置。
float clickX = (float) (e.getX() - origin.getX());
float clickY = (float) (e.getY() - origin.getY());
// 获取鼠标单击的字符位置。
TextHitInfo currentHit = textLayout.hitTestChar(clickX, clickY);
insertionIndex = currentHit.getInsertionIndex();
// 重新绘制组件,以显示新的插入符号。
repaint();
}
}
您可以从Shape中获取表示高亮区域的对象,该对象是由TextLayout生成的。在计算高亮区域的尺寸时,TextLayout会自动考虑上下文。 TextLayout支持逻辑和视觉两种高亮。
以下摘自的代码演示了一种显示高亮文本的方法:SelectionSample.java
public void paint(Graphics g) {
// ...
boolean haveCaret = anchorEnd == activeEnd;
if (!haveCaret) {
// 获取选择范围的高亮区域。
Shape highlight =
textLayout.getLogicalHighlightShape(anchorEnd, activeEnd);
// 用高亮颜色填充高亮区域。
graphics2D.setColor(HIGHLIGHT_COLOR);
graphics2D.fill(highlight);
}
// ...
}
// ...
private class SelectionMouseMotionListener extends MouseMotionAdapter {
public void mouseDragged(MouseEvent e) {
Point2D origin = computeLayoutOrigin();
// 计算鼠标位置相对于
// textLayout的原点。
float clickX = (float) (e.getX() - origin.getX());
float clickY = (float) (e.getY() - origin.getY());
// 获取鼠标位置的字符位置。
TextHitInfo position = textLayout.hitTestChar(clickX, clickY);
int newActiveEnd = position.getInsertionIndex();
// 如果newActiveEnd与activeEnd不同,更新activeEnd,
// 并重绘面板以显示新的选择。
if (activeEnd != newActiveEnd) {
activeEnd = newActiveEnd;
frame.repaint();
}
}
}
private class SelectionMouseListener extends MouseAdapter {
public void mousePressed(MouseEvent e) {
Point2D origin = computeLayoutOrigin();
// 计算鼠标位置相对于
// TextLayout的原点。
float clickX = (float) (e.getX() - origin.getX());
float clickY = (float) (e.getY() - origin.getY());
// 将选择的起始和活动位置设置为鼠标位置的字符位置。
TextHitInfo position = textLayout.hitTestChar(clickX, clickY);
anchorEnd = position.getInsertionIndex();
activeEnd = anchorEnd;
// 重绘面板以显示新的选择。
frame.repaint();
}
}
方法SelectionMouseListener.mousePressed指定了变量anchorEnd,它是鼠标点击的文本位置。方法SelectionMouseMotionListener.mouseDragged指定了变量activeEnd,它是鼠标拖动到的文本位置。paint方法检索一个表示选定文本的Shape对象(即anchorEnd和activeEnd之间的文本)。然后paint方法用高亮颜色填充Shape对象。