Java7中JEditorPane的自动换行

4
首先,我希望新开一个话题不会成为问题。说实话,我不知道如何在已有回答的基础上提出问题,所以就创建了这个话题。
我对Java还比较陌生,我的问题如下。我正在编写一个简单的聊天程序,使用JEditorPaneHTMLEditorKit来显示不同颜色的文本,显示表情符号和超链接。
我的问题是,在进行了一些研究后,我发现问题可能是由于Java7引起的,无法正确地使换行符正常工作。我希望文本自动换行,并且在超出组件宽度的字符串中间进行换行。 自动换行功能正常,但如果有人输入了一个相当长的字符串,则JEditorPane会被扩展,您需要调整框架大小才能将所有内容显示在屏幕上,我不想让这种情况发生。
我尝试了一些修复此问题的方法,但它们只允许字母换行,因此自动换行将不再起作用。此外,我希望用户可以通过按Enter键来换行。因此,我向文本添加了\n,而使用这些修复方法将不再影响结果,并且所有内容都将在一行中显示。
我感觉自己花了很多时间在网上寻找解决方案,但迄今为止还没有任何解决方法适用于我的情况,尤其是因为似乎一直都是相同的解决方案。我希望你们可以帮帮我。
总结一下:
现有情况:
- 在长字符串之间以空格分隔时,自动换行功能正常。 - 如果您使用Windows并且输入包含通过按Enter键创建的换行符,则该文本也会自动换行。 - 如果您输入一个没有空格的非常长的字符串,则面板会扩展,需要调整框架大小。 - HTML格式允许我显示不同的颜色、超链接和表情符号。
需要实现的内容:
- 在可能的情况下,仍然保持当前的自动换行行为,但仅在未使用空格分隔的长字符串中进行字母换行,以防止面板扩展。 - 在输入区域手动添加的换行符,或者如果我将预先格式化的文本复制到输入面板。 - 与我已经拥有的HTML格式相同。
我尝试过但没有效果的方法:
- jtextpane doesn't wrap text - JTextPane is not wrapping text 这里有一些代码可供尝试。在左下方有一个输入区域,可以输入一些文本。您也可以通过按Enter键添加换行符。单击按钮后,您将在上面的区域中看到文本。
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.border.TitledBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;

@SuppressWarnings("serial")
public class LineWrapTest extends JFrame implements ActionListener, KeyListener {

private JButton btnSend;
private JTextArea textAreaIn;
private JEditorPane textAreaOut;
private HTMLEditorKit kit;
private HTMLDocument doc;

public LineWrapTest() {

    this.setSize(600, 500);
    this.setDefaultCloseOperation(EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    this.setTitle("Linewrap Test");
}

/**
 * Not important for problem
 */
public void paintScreen() {

    this.setLayout(new BorderLayout());

    this.add(this.getPanelOut(), BorderLayout.CENTER);
    this.add(this.getPanelIn(), BorderLayout.SOUTH);

    this.textAreaIn.requestFocusInWindow();
    this.setVisible(true);
}

/**
 * Not important for problem
 * 
 * @return panelOut
 */
private JPanel getPanelOut() {

    JPanel panelOut = new JPanel();
    panelOut.setLayout(new BorderLayout());

    this.textAreaOut = new JEditorPane();
    this.textAreaOut.setEditable(false);
    this.textAreaOut.setContentType("text/html");

    this.kit = new HTMLEditorKit();
    this.doc = new HTMLDocument();

    StyleSheet styleSheet = this.kit.getStyleSheet();
    this.kit.setStyleSheet(styleSheet);

    this.textAreaOut.setEditorKit(this.kit);
    this.textAreaOut.setDocument(this.doc);

    TitledBorder border = BorderFactory.createTitledBorder("Output");
    border.setTitleJustification(TitledBorder.CENTER);

    panelOut.setBorder(border);
    panelOut.add(this.textAreaOut);

    return panelOut;
}

/**
 * Not important for problem
 * 
 * @return panelIn
 */
private JPanel getPanelIn() {

    JPanel panelIn = new JPanel();
    panelIn.setLayout(new BorderLayout());

    this.textAreaIn = new JTextArea();
    this.textAreaIn.setLineWrap(true);
    this.textAreaIn.setWrapStyleWord(true);

    TitledBorder border = BorderFactory.createTitledBorder("Input");
    border.setTitleJustification(TitledBorder.CENTER);

    panelIn.setBorder(border);
    panelIn.add(this.getBtnSend(), BorderLayout.EAST);
    panelIn.add(this.textAreaIn, BorderLayout.CENTER);

    return panelIn;
}

/**
 * Not important for problem
 * 
 * @return btnSend
 */
private JButton getBtnSend() {

    this.btnSend = new JButton("Send");
    this.btnSend.addActionListener(this);

    return this.btnSend;
}

private void append(String text) {

    try {
        this.kit.insertHTML(this.doc, this.doc.getLength(), text, 0, 0, null);
    } catch (BadLocationException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private String getHTMLText() {

    String txtIn = this.textAreaIn.getText().trim().replaceAll(SEPARATOR, "<br/>");

    StringBuffer htmlBuilder = new StringBuffer();

    htmlBuilder.append("<HTML>");
    htmlBuilder.append(txtIn);
    htmlBuilder.append("</HTML>");

    return htmlBuilder.toString();
}

@Override
public void actionPerformed(ActionEvent e) {

    if (e.getSource() == this.btnSend) {
        this.append(this.getHTMLText());
        this.textAreaIn.setText("");
        this.textAreaIn.requestFocusInWindow();
    }
}

public static void main(String[] args) {

    LineWrapTest test = new LineWrapTest();
    test.paintScreen();
}

@Override
public void keyPressed(KeyEvent e) {

    if (e.getKeyCode() == KeyEvent.VK_ENTER)
        if (!this.textAreaIn.getText().trim().isEmpty())
            this.textAreaIn.setText(this.textAreaIn.getText() + SEPARATOR);
}

@Override
public void keyReleased(KeyEvent e) {
}

@Override
public void keyTyped(KeyEvent e) {
}
}

更新: 基于 http://java-sl.com/tip_java7_text_wrapping_bug_fix.html 的部分内容。

我成功地离我的目标更近了一步。我试图将 HTMLEditorKit 的修复和 StlyedEditorKit 的修复结合起来。但说实话,我并不知道我到底做了什么 :( 不幸的是,这个替代 HTMLEditorKit 的修复会导致手动换行不再起作用。 也许你可以以此为基础进行更好的实现。

要在我的示例中使用它,只需在项目中创建一个新类,使用 CustomEditorKit,并将 HTMLEditorKit 替换为该 CustomEditorKit。 您会发现单词换行和字母换行现在都可以工作了,但是如果您按 ENTER 进行自己的换行,则此更改将不再出现在输出面板中,并且所有内容都将显示在一行中。 另一个奇怪的问题是,如果调整窗口大小,有时线条会相互覆盖。

import javax.swing.SizeRequirements;
import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.InlineView;
import javax.swing.text.html.ParagraphView;

@SuppressWarnings("serial")
public class CustomEditorKit extends HTMLEditorKit {

@Override
public ViewFactory getViewFactory() {

    return new HTMLFactory() {
        @Override
        public View create(Element e) {
            View v = super.create(e);
            if (v instanceof InlineView) {
                return new InlineView(e) {
                    @Override
                    public int getBreakWeight(int axis, float pos, float len) {
                        return GoodBreakWeight;
                    }

                    @Override
                    public View breakView(int axis, int p0, float pos, float len) {
                        if (axis == View.X_AXIS) {
                            this.checkPainter();
                            this.removeUpdate(null, null, null);
                        }
                        return super.breakView(axis, p0, pos, len);
                    }
                };
            }
            else if (v instanceof ParagraphView) {
                return new ParagraphView(e) {
                    @Override
                    protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) {
                        if (r == null) {
                            r = new SizeRequirements();
                        }
                        float pref = this.layoutPool.getPreferredSpan(axis);
                        float min = this.layoutPool.getMinimumSpan(axis);
                        // Don't include insets, Box.getXXXSpan will include them. 
                        r.minimum = (int) min;
                        r.preferred = Math.max(r.minimum, (int) pref);
                        r.maximum = Integer.MAX_VALUE;
                        r.alignment = 0.5f;
                        return r;
                    }

                };
            }
            return v;
        }
    };
    }
}

好的,以下是上面代码对我的影响。JTextArea 完美换行,长字符串和单词都会换到下一行。JEditorPane 只有在遇到长字符串时才会换行,否则只会在单词处换行。要想实现自定义换行,必须用比组件更长的句子来进行单词换行。目前按回车键不会引起自定义换行,而是使用通用行分隔符。您需要的是: JEditorPane 对长字符串进行换行,不需要添加空格,就像 JTextArea 一样,这样它就不会被扩展。此外,通过按回车键在 JEpane 中进行自定义换行。 - Patrick Sebastien
2个回答

6

好的!所以,我最终解决了你遇到的所有问题。这需要一些研究和大量的试错,但是现在它已经完成了:

这是我所做的:

  • 将JEditorPane放入JScrollPane中,以便您可以随着消息变得更长而上下滚动
  • 添加自定义换行。自定义换行将在单词和长单词的所需位置处换行。你是对的,这是Java当前版本的一个错误。http://bugs.sun.com/view_bug.do?bug_id=7125737
  • 添加了用户通过按Enter键换行的功能。这会干扰自定义换行,因此您可能不喜欢我如何实现这一点。在代码示例中,我建议使用其他选项。
  • 保留了您的HTMLDocument功能。我曾经想过不这样做,但我找到了解决方法,使其能够被保留。
  • 应用程序仍然使用JEditorPane,但是如果您愿意,可以将其切换为JTextPane。我尝试过两种方法,它们都是可行的。

所以这里是代码。它有点长,你可能希望根据自己的喜好进行更改。我在做出更改的地方进行了注释并尝试解释它们。

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.SizeRequirements;
import javax.swing.border.TitledBorder;
import javax.swing.text.*;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.InlineView;
import javax.swing.text.html.StyleSheet;

@SuppressWarnings("serial")
public class LineWrapTest extends JFrame implements ActionListener, KeyListener {

    //This is the separator.
    private String SEPARATOR = System.getProperty("line.separator");
    private JButton btnSend;
    private JTextArea textAreaIn;
    private JEditorPane textAreaOut;
    private JScrollPane outputScrollPane;
    private HTMLEditorKit kit;
    private HTMLDocument doc;


    public LineWrapTest() {

        this.setSize(600, 500);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setLocationRelativeTo(null);
        this.setTitle("Linewrap Test");
    }

    /**
     * Not important for problem
     */
    public void paintScreen() {

        this.setLayout(new BorderLayout());

        this.add(this.getPanelOut(), BorderLayout.CENTER);
        this.add(this.getPanelIn(), BorderLayout.SOUTH);

        this.textAreaIn.requestFocusInWindow();
        this.setVisible(true);
    }


    /**
     * Not important for problem
     * 
     * @return panelOut
     */
    private JPanel getPanelOut() {

        JPanel panelOut = new JPanel();
        panelOut.setLayout(new BorderLayout());

        this.textAreaOut = new JEditorPane();
        this.textAreaOut.setEditable(false);
        this.textAreaOut.setContentType("text/html");

        //I added this scroll pane.
        this.outputScrollPane = new JScrollPane(this.textAreaOut);

        /*
         * This is a whole whack of code.  It's a combination of two sources.
         * It achieves the wrapping you desire: by word and longgg strings
         * It is a custom addition to HTMLEditorKit
         */
        this.kit = new HTMLEditorKit(){
           @Override 
           public ViewFactory getViewFactory(){ 

               return new HTMLFactory(){ 
                   public View create(Element e){ 
                      View v = super.create(e); 
                      if(v instanceof InlineView){ 
                          return new InlineView(e){ 
                              public int getBreakWeight(int axis, float pos, float len) { 
                                  //return GoodBreakWeight;
                                  if (axis == View.X_AXIS) {
                                      checkPainter();
                                      int p0 = getStartOffset();
                                      int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, len);
                                      if (p1 == p0) {
                                          // can't even fit a single character
                                          return View.BadBreakWeight;
                                      }
                                      try {
                                          //if the view contains line break char return forced break
                                          if (getDocument().getText(p0, p1 - p0).indexOf(SEPARATOR) >= 0) {
                                              return View.ForcedBreakWeight;
                                          }
                                      }
                                      catch (BadLocationException ex) {
                                          //should never happen
                                      }  

                                  }
                                  return super.getBreakWeight(axis, pos, len);
                              } 
                              public View breakView(int axis, int p0, float pos, float len) { 
                                  if (axis == View.X_AXIS) {
                                      checkPainter();
                                      int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, len);
                                      try {
                                          //if the view contains line break char break the view
                                          int index = getDocument().getText(p0, p1 - p0).indexOf(SEPARATOR);
                                          if (index >= 0) {
                                              GlyphView v = (GlyphView) createFragment(p0, p0 + index + 1);
                                              return v;
                                          }
                                      }
                                      catch (BadLocationException ex) {
                                          //should never happen
                                      }

                                  }
                                  return super.breakView(axis, p0, pos, len);
                            } 
                          }; 
                      } 
                      else if (v instanceof ParagraphView) { 
                          return new ParagraphView(e) { 
                              protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { 
                                  if (r == null) { 
                                        r = new SizeRequirements(); 
                                  } 
                                  float pref = layoutPool.getPreferredSpan(axis); 
                                  float min = layoutPool.getMinimumSpan(axis); 
                                  // Don't include insets, Box.getXXXSpan will include them. 
                                    r.minimum = (int)min; 
                                    r.preferred = Math.max(r.minimum, (int) pref); 
                                    r.maximum = Integer.MAX_VALUE; 
                                    r.alignment = 0.5f; 
                                  return r; 
                                } 

                            }; 
                        } 
                      return v; 
                    } 
                }; 
            } 
        }; 

        this.doc = new HTMLDocument();

        StyleSheet styleSheet = this.kit.getStyleSheet();
        this.kit.setStyleSheet(styleSheet);

        this.textAreaOut.setEditorKit(this.kit);
        this.textAreaOut.setDocument(this.doc);

        TitledBorder border = BorderFactory.createTitledBorder("Output");
        border.setTitleJustification(TitledBorder.CENTER);

        panelOut.setBorder(border);

        //I changed this to add the scrollpane, which now contains
        //the JEditorPane
        panelOut.add(this.outputScrollPane);

        return panelOut;
    }

    /**
     * Not important for problem
     * 
     * @return panelIn
     */
    private JPanel getPanelIn() {

        JPanel panelIn = new JPanel();
        panelIn.setLayout(new BorderLayout());

        this.textAreaIn = new JTextArea();
        this.textAreaIn.setLineWrap(true);
        this.textAreaIn.setWrapStyleWord(true);

        //This disables enter from going to a new line.  Your key listener does that.
        this.textAreaIn.getInputMap().put(KeyStroke.getKeyStroke("ENTER"), "none");
        //For the key listener to work, it needs to be added to the component
        this.textAreaIn.addKeyListener(this);

        TitledBorder border = BorderFactory.createTitledBorder("Input");
        border.setTitleJustification(TitledBorder.CENTER);

        panelIn.setBorder(border);
        panelIn.add(this.getBtnSend(), BorderLayout.EAST);
        panelIn.add(this.textAreaIn, BorderLayout.CENTER);

        return panelIn;
    }

    /**
     * Not important for problem
     * 
     * @return btnSend
     */
    private JButton getBtnSend() {

        this.btnSend = new JButton("Send");
        this.btnSend.addActionListener(this);

        return this.btnSend;
    }


    private void append(String text) {

        try {
            this.kit.insertHTML(this.doc, this.doc.getLength(), text, 0, 0, null);
        } catch (BadLocationException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String getHTMLText() {
        //I tried to find a work around for this but I couldn't.  It could be done
        //by manipulating the HTMLDocument but it's beyond me.  Notice I changed
        //<br/> to <p/>.  For some reason, <br/> no longer went to the next line
        //when I added the custom wrap.  <p/> seems to work though.
        String txtIn = this.textAreaIn.getText().trim().replaceAll(SEPARATOR, "<p/>");

        //My IDE recommends you use StringBuilder instead, that's up to you.
        //I am not sure what the difference would be.
        StringBuffer htmlBuilder = new StringBuffer();

        htmlBuilder.append("<HTML>");
        htmlBuilder.append(txtIn);
        htmlBuilder.append("</HTML>");

        return htmlBuilder.toString();
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        if (e.getSource() == this.btnSend) {
            this.append(this.getHTMLText());
            this.textAreaIn.setText("");
            this.textAreaIn.requestFocusInWindow();
        }
    }

    public static void main(String[] args) {
        LineWrapTest test = new LineWrapTest();
        test.paintScreen();
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_ENTER){
            if (!this.textAreaIn.getText().trim().isEmpty()) {
                //I made this work by defining the SEPARATOR.
                //You could use append(Separator) instead if you want.
                this.textAreaIn.setText(this.textAreaIn.getText() + SEPARATOR);
            }
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

}

以下是我用来解决这个问题的(大部分)链接:

使用HTMLDocument启用JTextPane中的自动换行

自定义折行是这两个的组合:

http://java-sl.com/tip_html_letter_wrap.html

http://java-sl.com/wrap.html

删除 JTextArea 的键绑定:

http://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html

如果您有任何问题,请在下方评论。我会回答它们。我真诚地希望这能解决您的问题。

嗨,艾伦,感谢你详细的回答。特别是分隔符的事情对我来说是新的。但是你的修复并没有帮助我。我不是有单词换行或滚动窗格的问题,我需要单词和字母换行。你写道你在我的代码示例中遇到了麻烦,出了什么问题?它对我来说是可以工作的 :/ - BenGe89
@BenGe89 或许我误解了你的问题。你可以编辑一下,让它更清晰明确,说明你需要什么以及遇到了什么问题?当我运行你的代码时,只出现了一个带有底部右侧按钮的空白窗口。 - Patrick Sebastien
我已经编辑了原帖,希望现在更清楚了。我还在示例中添加了一些边框和简短的描述。由于滚动条与问题无关,我已将其删除,对此我感到抱歉。简而言之,我想要已有的内容,但是如果可能的话,使用Java6行为包装单词,否则使用HTMLEditorKit包装字母。该示例仅显示了问题。我尚未添加line.separator,因为替换没有起作用。通过按Enter键,我希望程序能够创建自己的换行。 - BenGe89
@BenGe89 好的,谢谢你的编辑。我会看一下,看看是否有任何解决方案,并相应地编辑我的答案。如果我无法弄清楚,可能就不得不删除我的答案了。 - Patrick Sebastien
非常感谢!真的很感激你的帮助!与此同时,我又尝试了一些东西。虽然不是很成功,但其中一个小成功是将HTMLEditorKit和StyledEditorKit的修复组合在一起。我已经将其添加到我的示例中,也许你可以从中获取一些信息,但我不能:/ 我肯定需要更深入地了解Java... 顺便问一下,你知道比我现在做的创建自己的换行方式更好的方法吗?不太喜欢“replace(String,String)”的方式,特别是因为它似乎不能使用line.separator。 - BenGe89
显示剩余4条评论

1
我找到了一个更好的解决方案: <br>标签可以被HTMLEditorKit正确处理,但是Patrick Sebastien在他的帖子中提到它不能。这是因为它的ViewFactory将所有InlineView对象视为可包装的,但BRView也是一种InlineView。请参见下面的我的解决方案:
class WrapColumnFactory extends HTMLEditorKit.HTMLFactory {

        @Override
        public View create(Element elem) {
            View v = super.create(elem);

            if (v instanceof LabelView) {

                // the javax.swing.text.html.BRView (representing <br> tag) is a LabelView but must not be handled
                // by a WrapLabelView. As BRView is private, check the html tag from elem attribute
                Object o = elem.getAttributes().getAttribute(StyleConstants.NameAttribute);
                if ((o instanceof HTML.Tag) && o == HTML.Tag.BR) {
                    return v;
                }

                return new WrapLabelView(elem);
            }

            return v;
        }
    }

    class WrapLabelView extends LabelView {

        public WrapLabelView(Element elem) {
            super(elem);
        }

        @Override
        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);
            }
        }

    }

这是我找到的最佳(最简单且最全面)解决方案。我已将此解决方案组件化为[类似问题的答案](https://dev59.com/4VzUa4cB1Zd3GeqP2Vgz#31930559)。 - Parker

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