在JTextPane中如何换行长单词(Java 7)

17

在Java 6及以下的所有版本中,将JTextPane放入JScrollPane的默认行为是:尽可能在单词边界处换行。如果不行,则无论如何都要换行。

JDK 7的默认行为似乎是:尽可能在单词边界处换行。如果不行,则只是扩展JTextPane的宽度(永远不会换行长单词)。

很容易复现这个问题,以下是一个SSCCE示例:


public class WrappingTest extends JFrame
{

    public static void main ( String[] args )
    {
        new WrappingTest(); 
    }

    public WrappingTest ()
    {
        setSize(200,200);
        getContentPane().setLayout(new BorderLayout());
        JTextPane jtp = new JTextPane();
        JScrollPane jsp = new JScrollPane(jtp);
        jsp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        getContentPane().add(jsp,BorderLayout.CENTER);
        setVisible(true);
    }

}
只需在JDK 6中运行它,在JDK 7中编写一些小单词和一个长单词,您就会看到差异。我的问题很简单... JDK 7中的新默认行为完全混乱了我的程序(Oracle在更改此类默认值时应更加小心...它们似乎不重要,但当您使用JTextPane显示通常包含非常长的字母字符串的数据时,它们并不那么不重要-事实上,我将提交错误报告,但在此期间/如果他们没有解决它,我想有一个解决方法)。有没有办法返回以前的行为?请注意,我已经检查了相关问题的答案How is word-wrapping implemented in JTextPane, and how do I make it wrap a string without spaces?,但它没有回答这个问题-它提供了一种使JTextPane在完全不考虑空格的情况下换行的方法,但对我来说,期望的行为是在可能的情况下在空格处拆分行,否则在其他地方(与以前的Java版本相同)。

使用 invokeLater() 有帮助吗? - Catalina Island
我有完全相同的问题。相关链接:https://forums.oracle.com/forums/thread.jspa?threadID=2374090(没有答案...)那里的帖子作者已经创建了一个错误报告,但被关闭为“不是错误”,并没有任何解释... - PhiLho
5个回答

14

对我来说这个修复方法有效(在1.7.0_09版本下测试过)

import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;

public class WrapTestApp extends JFrame {

    public static void main ( String[] args ) {
        new WrapTestApp();
    }

    public WrapTestApp () {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(200,200);
        getContentPane().setLayout(new BorderLayout());
        JTextPane jtp = new JTextPane();
        jtp.setEditorKit(new WrapEditorKit());
        JScrollPane jsp = new JScrollPane(jtp);
        jsp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        getContentPane().add(jsp, BorderLayout.CENTER);
        jtp.setText("ExampleOfTheWrapLongWordWithoutSpaces");
        setVisible(true);
    }

    class WrapEditorKit extends StyledEditorKit {
        ViewFactory defaultFactory=new WrapColumnFactory();
        public ViewFactory getViewFactory() {
            return defaultFactory;
        }

    }

    class WrapColumnFactory implements ViewFactory {
        public View create(Element elem) {
            String kind = elem.getName();
            if (kind != null) {
                if (kind.equals(AbstractDocument.ContentElementName)) {
                    return new WrapLabelView(elem);
                } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                    return new ParagraphView(elem);
                } else if (kind.equals(AbstractDocument.SectionElementName)) {
                    return new BoxView(elem, View.Y_AXIS);
                } else if (kind.equals(StyleConstants.ComponentElementName)) {
                    return new ComponentView(elem);
                } else if (kind.equals(StyleConstants.IconElementName)) {
                    return new IconView(elem);
                }
            }

            // default to text display
            return new LabelView(elem);
        }
    }

    class WrapLabelView extends LabelView {
        public WrapLabelView(Element elem) {
            super(elem);
        }

        public float getMinimumSpan(int axis) {
            switch (axis) {
                case View.X_AXIS:
                    return 0;
                case View.Y_AXIS:
                    return super.getMinimumSpan(axis);
                default:
                    throw new IllegalArgumentException("Invalid axis: " + axis);
            }
        }

    }
}

1
事实上,我们所需要的只是让LabelView的getMinimumSpan()函数在X_AXIS方向返回0。ViewFactory是一种替换默认LabelView的方式。 - StanislavL
啊啊,我明白并且懂了,谢谢。对于 jtp.setComponentOrientation(RTL); 同样适用。 - mKorbel
我们应该如何解决在 jtp.setEditorKit(new WrapEditorKit()) 这一行中的转换问题?我在那一行中得到了 javax.swing.text.DefaultStyledDocument 无法转换为 javax.swing.text.html.HTMLDocument 的错误。 - Luis Gonçalves
你可以使用另一个编辑器工具包。猜测是 HTMLEditorKit。对于该工具包执行相同的操作。 - StanislavL

3

从 @dk89 的好的捕捉,但不幸的是给出的解决方案不起作用:JDK 7 显然仍然没有提供一种方法在 JTextComponent 上设置自定义 BreakIterator;甚至连在 GlyphView 上生成 BreakIterator 的方式也是私有的。如果我们逐个字符地插入字符串,它仍然不起作用:我想 Attribute Set 相同的连续文本运行被合并在一起。

我花了两天时间尝试做一个自定义的 EditorKit,根据其他建议,但它不能很好地工作(至少在 JDK 1.7.0_4 中)作为文本。

我尝试了在How to word wrap text stored in JTextPanes which are cells in a JList和变体中找到的解决方案http://www.experts-exchange.com/Programming/Languages/Java/Q_20393892.html

但我发现当 JTextPane 小于句子中最长的单词时,breakView 不再被调用。因此,当只有一个(长)单词时,它根本不起作用。这是我们的情况,因为我们在相当小的空间中显示用户提供的类似标识符的字符串,通常没有空格。

最后,我找到了一个简单的解决方案,从 bug 报告中得出:确实,逐个字符地插入字符串,但交替样式!因此,我们有和字符一样多的段落,字符串在字符边界处换行。直到下一次“故障修复”?

代码片段:

private JTextPane tp;
private SimpleAttributeSet sas = new SimpleAttributeSet();

tp= new JTextPane();
sas.addAttribute( "A", "C" ); // Arbitrary attribute names and value, not used actually

    // Set the global attributes (italics, etc.)
    tp.setParagraphAttributes(styleParagraphAttributes, true);

    Document doc = tp.getDocument();
    try
    {
        doc.remove(0, doc.getLength()); // Clear
        for (int i = 0; i < textToDisplay.length(); i++)
        {
            doc.insertString(doc.getLength(), textToDisplay.substring(i, i+1),
                    // Change attribute every other char
                    i % 2 == 0 ? null : sas);
        }
    }
    catch (BadLocationException ble)
    {
        log.warn("Cannot happen...", ble);
    }

如该错误所述,他们应该提供一种简单的方法(例如某些属性或可注入的内容)以恢复旧行为。


1

谢谢,非常相似...但不幸的是它似乎不是同一个错误(它与属性集有关,并且早已修复) :/ - Al-Khwarizmi
我认为这是相关的,尽管1.6中的行为没有改变。看看这个评论:“应该注意到,请求的默认拆分行为(当BreakIterator找不到有效断点时,在任何JDK版本中都不应恢复将GlyphView在任意位置断开)是非常错误的,也不应在任何JDK版本中恢复,无论是未来还是过去。” 看起来我们现在需要做更多的工作... - PhiLho

1

重写getMinimumSpan并将X_AXIS的返回值设为0,可以通过空格来分割文本。如果你想要基于字符的分割,请尝试以下方法:


    internal class WhitespaceBasedWrapLabelView(elem: Element?) : LabelView(elem) {
        override fun getMinimumSpan(axis: Int): Float {
            return when (axis) {
                X_AXIS -> 0f
                Y_AXIS -> super.getMinimumSpan(axis)
                else -> throw IllegalArgumentException("Invalid axis: $axis")
            }
        }
    }
    
    internal class CharacterBasedWrapLabelView(elem: Element?) : LabelView(elem) {
        override fun getMinimumSpan(axis: Int): Float {
            return when (axis) {
                X_AXIS -> 0f
                Y_AXIS -> super.getMinimumSpan(axis)
                else -> throw IllegalArgumentException("Invalid axis: $axis")
            }
        }
    
        override fun breakView(axis: Int, p0: Int, pos: Float, len: Float): View {
            if (axis == X_AXIS) {
                checkPainter()
                val p1 = glyphPainter.getBoundedPosition(this, p0, pos, len)
                // bounded region.
                if (p0 == startOffset && p1 == endOffset) {
                    return this
                }
                val v = createFragment(p0, p1) as GlyphView
                // v.x = pos
                getTabbedSpan(pos, tabExpander)
                return v
            }
            return super.breakView(axis, p0, pos, len)
        }
    
        override fun getBreakWeight(axis: Int, pos: Float, len: Float): Int {
            if (axis == X_AXIS) {
                checkPainter()
                val p0 = startOffset
                val p1 = glyphPainter.getBoundedPosition(this, p0, pos, len)
                return if (p1 == p0) BadBreakWeight else GoodBreakWeight
            }
            return super.getBreakWeight(axis, pos, len)
        }
    }


1

嗨,我遇到了同样的问题,但找到了一个解决方法:

只需创建一个JTextPane的扩展类,例如:

        MyWrapJTextPane extends JTextPane

覆盖以下方法 - 它有效 ;-)

        public boolean getScrollableTracksViewportWidth() {
            return true;
        }

抱歉:不行!这只解决了包含空格的长行问题 - 现在它们被正确地换行了 - 但是长单词仍然没有被换行 :-( - Thomwiesel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接