JTextPane 中的对齐和内联组件(或图标)

Aligning and Inlining components (or icons) in JTextPane

我正在 Java 开发一个应用程序,除其他外,它应该在文本字段中显示 Magic: The Gathering 卡片的详细信息(因为我使用的是 Swing,这目前是 JTextPane).

这些详细信息包含文本和小图标,其中一些图标与文本内联(因此文本围绕它们流动),还有一些图标,根据我的设计,与一些左对齐的文本右对齐同一条线。

我从另一个应用程序中截取了一张图像,该应用程序使用与我正在处理的应用程序非常相似的设计(尽管 Java 中没有):

基本上应该是这样的。

现在,出于对一切的热爱,我无法让它在 JTextPane 中工作。

我开始尝试使用 CSS 执行此操作,但发现 JEditorPane 和 subclasses 不支持“浮动”属性,所以我尝试了而是使用 Pane 的 StyledDocument

一开始我没有让第一部分工作(顶部的图标和右对齐文本总是忽略它们的对齐方式,直接放在行中左对齐文本之后),直到我发现this question.

建议使用以下代码行:

pane.setEditorKit(new HTMLEditorKit());

这确实解决了我的第一个问题。

但现在我卡在了第二部分,让第二部分中的那些图标与文本内联。这是我目前得到的:

我发现,出于某种原因,当您使用带有上述代码行的编辑器工具包将 JTextPane 模式切换为 html 模式时,插入组件会变得非常疯狂。

上面的图标(其实我合并成一个图)和下面文字里的(没合并的)都在JLabels里面,不过没关系如果我将它们添加为图像或添加到 JLabels 中。图片或标签绝对不会比你在那里看到的大,我完全不知道额外的空白是从哪里来的。

我找到了 this question,答案表明这是某种错误或只是 JEditorPane 的 html 模式的奇怪行为。

如果我再次删除上面的代码行,我会遇到原来的问题:

根据图标在文本中的确切位置,我得到了各种不同的奇怪结果。我在下面整理了更多示例图片:

那么,我怎么可能解决这个问题? JTextPane 对我来说工作正常,除了这部分,但我可能会使用其他一些解决方案,只要最终结果看起来仍然一样。请记住,我可能想在其中添加一些其他组件(如 Button),因此如果可能的话,我想坚持使用 Swing 原生的东西。

用户将无法编辑 TextPane 的内容,但我想稍后添加一个选项以单击一次复制所有内容(所以我宁愿留在文本区域)。

下面,我整理了一个(不是那么简单的)工作示例供您试用:

(编辑:现在更新了底部的代码!旧代码仍然存在于以下 link。)

http://pastebin.com/RwAdPCzb

我使用的图标如下。您需要重命名它们并更改代码中的路径。




一些注意事项:


更新 1:

在 Sharcoux 的回答之后,我编辑了我的代码,在实际的 JTextPane 上方有 2 个 JLabel,它们包含本应具有不同对齐方式(左对齐和右对齐部分)的两条线。 JTextPane 现在不再使用 HTMLEditorKit,我使用 insertIcon() 将图标插入到文本中。

这样,图标就(几乎)正确插入了!
图片在这里:

不过还有两点小地方我还是不太满意:

第一个:
我需要将所有内容都放入 JScrollPane 中,因为在我的实际应用程序中,TextPane 中的文本 much 更长。由于我现在拥有三个组件而不仅仅是 TextPane,因此我需要将所有内容都放入 JPanel 中,然后将其放入 ScrollPane 中。
但是,如果您这样做,JTextPane 将不知道它的宽度不应再超过 JScrollPane 的宽度。它停止换行文本,只是增长到与整个文本一样大。
我为此开了一个新问题,因为我觉得这是 Swing 的一个基本问题,值得单独提问。如果你想帮忙,这里是 link:

第二个:
这可能是不可能的,但我想我还是会问。当您以这种方式添加图标时,图标与文本具有相同的基线。他们有什么办法可以将它们稍微降低一点吗? 2-3 像素,也许?他们会以这种方式更好地与文本对齐。下面两张图。
这是现在的样子:

这就是我希望它看起来的样子:

也许我可以子class 并覆盖 JTextPane 的某些部分,以将其上呈现的所有图标向下移动一个设定的像素数量,或类似的东西?

供参考,这也是我更新后的新代码。我把上面那个旧的换成了pastebin.comlink,如果你还想看


更新 2:

我的第一个问题已经解决了!我也更新了下面的代码以反映这一点。

我的第二个问题仍然存在!

这是新代码:

import java.awt.EventQueue;
import java.awt.Graphics2D;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.JTextPane;
import javax.swing.JViewport;
 
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import javax.swing.ScrollPaneConstants;
 
public class MinimalExample extends JFrame {
 
    private JPanel contentPane;
    private JScrollPane scrollPane;
    private JTextPane textPane;
 
    // Setup some data for an example card:
    String name = "Absorb Vis";
    String set = "CON";
    String manaCost = "{6}{B}";
    String printedType = "Sorcery";
    String artist = "Brandon Kitkouski";
 
    String rulesText = "Target player loses 4 life and you gain 4 life.\n"
            + "Basic landcycling {1}{B} ({1}{B}, Discard this card: "
            + "Search your library for a basic land card, reveal it, and put "
            + "it into your hand. Then shuffle your library.)";
 
    HashMap<String, BufferedImage> manaSymbolImages;
    private ScrollablePanel textPanel;
    //private JPanel textPanel;
    private JPanel headlinesPanel;
    private JPanel firstHeadlinePanel;
    private JPanel secondHeadlinePanel;
    private JLabel titleLabel;
    private JLabel manaCostLabel;
    private JLabel typeLabel;
    private JLabel setLabel;
 
    public static void main(String[] args) {
 
        EventQueue.invokeLater(new Runnable() {
 
            public void run() {
 
                try {
                    MinimalExample frame = new MinimalExample();
                    frame.setVisible(true);
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
    public MinimalExample() {
 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 230, 400);
        contentPane = new JPanel();
        contentPane.setBackground(Color.WHITE);
        contentPane.setBorder(null);
        setContentPane(contentPane);
        /* HTMLEditorKit eKit = new HTMLEditorKit();
         * textPane.setEditorKit(eKit); HTMLDocument htmlDoc = (HTMLDocument)
         * textPane.getDocument(); htmlDoc.setPreservesUnknownTags(false); */
        contentPane.setLayout(new GridLayout(0, 1, 0, 0));
 
        textPanel = new ScrollablePanel();
        //textPanel = new JPanel();
        textPanel.setBackground(Color.WHITE);
        textPanel.setLayout(new BorderLayout(0, 0));
 
        headlinesPanel = new JPanel();
        headlinesPanel.setBorder(new EmptyBorder(2, 5, 3, 5));
        headlinesPanel.setBackground(Color.WHITE);
        textPanel.add(headlinesPanel, BorderLayout.NORTH);
        headlinesPanel.setLayout(new GridLayout(0, 1, 0, 0));
 
        firstHeadlinePanel = new JPanel();
        firstHeadlinePanel.setBorder(new EmptyBorder(0, 0, 3, 0));
        firstHeadlinePanel.setOpaque(false);
        headlinesPanel.add(firstHeadlinePanel);
        firstHeadlinePanel.setLayout(new BorderLayout(0, 0));
 
        titleLabel = new JLabel("");
        titleLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
        firstHeadlinePanel.add(titleLabel, BorderLayout.WEST);
 
        manaCostLabel = new JLabel("");
        firstHeadlinePanel.add(manaCostLabel, BorderLayout.EAST);
 
        secondHeadlinePanel = new JPanel();
        secondHeadlinePanel.setBorder(null);
        secondHeadlinePanel.setOpaque(false);
        headlinesPanel.add(secondHeadlinePanel);
        secondHeadlinePanel.setLayout(new BorderLayout(0, 0));
 
        typeLabel = new JLabel("");
        typeLabel.setFont(new Font("Tahoma", Font.PLAIN, 12));
        secondHeadlinePanel.add(typeLabel, BorderLayout.WEST);
 
        setLabel = new JLabel("");
        setLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
        secondHeadlinePanel.add(setLabel, BorderLayout.EAST);
 
        scrollPane = new JScrollPane();
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setBackground(Color.WHITE);
        contentPane.add(scrollPane);
 
        textPane = new JTextPane();
        textPane.setBorder(new EmptyBorder(0, 3, 0, 3));
        textPane.setAlignmentY(0.3f);
        textPane.setEditable(false);
 
        textPanel.add(textPane, BorderLayout.CENTER);
 
        scrollPane.setViewportView(textPanel);
 
        loadManaCostIcons();
        setPaneText();
    }
 
    // This part inserts the text into the document of the text pane.
    public void setPaneText() {
 
        titleLabel.setText(name);
        manaCostLabel.setIcon(combineSymbols(manaCost));
        typeLabel.setText(printedType);
        setLabel.setText(set);
 
        StyledDocument textPaneDoc = textPane.getStyledDocument();
 
        SimpleAttributeSet defaultAtts = new SimpleAttributeSet();
        StyleConstants.setFontFamily(defaultAtts, "SansSerif");
        StyleConstants.setFontSize(defaultAtts, 12);
 
        SimpleAttributeSet rulesAtts = new SimpleAttributeSet(defaultAtts);
 
        SimpleAttributeSet artistAtts = new SimpleAttributeSet(defaultAtts);
        StyleConstants.setFontSize(artistAtts, 10);
 
        addTextWithSymbols(rulesText, rulesAtts);
 
        try {
 
            textPaneDoc.insertString(textPaneDoc.getLength(), artist, artistAtts);
        }
        catch (BadLocationException e) {
 
            e.printStackTrace();
        }
 
        textPane.revalidate();
        textPane.repaint();
    }
 
    /* This adds the rest of the text to the pane. The codes for the symbols get
     * replaced by the actual symbols and the text gets inserted piece by piece. */
    public void addTextWithSymbols(String text, SimpleAttributeSet style) {
 
        StyledDocument textPaneDoc = textPane.getStyledDocument();
        Pattern symbolPattern = Pattern.compile("\{(.*?)\}");
 
        try {
 
            Matcher symbolMatcher = symbolPattern.matcher(text);
            int previousMatch = 0;
 
            while (symbolMatcher.find()) {
 
                int start = symbolMatcher.start();
                int end = symbolMatcher.end();
                String subStringText = text.substring(previousMatch, start);
                String currentMatch = text.substring(start, end);
 
                if (subStringText.isEmpty() == false) {
 
                    textPaneDoc.insertString(textPaneDoc.getLength(), subStringText, style);
                }
 
                ImageIcon currentIcon = new ImageIcon(manaSymbolImages.get(currentMatch));
 
                SimpleAttributeSet iconAtts = new SimpleAttributeSet();
                JLabel iconLabel = new JLabel(currentIcon);
                StyleConstants.setComponent(iconAtts, iconLabel);
 
                textPane.insertIcon(currentIcon);
                previousMatch = end;
            }
 
            String subStringText = text.substring(previousMatch);
 
            if (subStringText.isEmpty() == false) {
               
                textPaneDoc.insertString(textPaneDoc.getLength(), subStringText + "\n", style);
            }
        }
        catch (Exception e) {
 
            e.printStackTrace();
        }
    }
 
    /* Everything below is more or less irrelevant. However, you might need to
     * adjust the image image file paths. */
 
    public void loadManaCostIcons() {
 
        manaSymbolImages = new HashMap<String, BufferedImage>();
        try {
 
            // Most likely, those paths won't work for you!
            File bFile = new File("resource/B.png");
            File c1File = new File("resource/1.png");
            File c6File = new File("resource/6.png");
 
            manaSymbolImages.put("{B}", ImageIO.read(bFile));
            manaSymbolImages.put("{1}", ImageIO.read(c1File));
            manaSymbolImages.put("{6}", ImageIO.read(c6File));
        }
        catch (IOException e) {
 
            e.printStackTrace();
        }
    }
 
    public ImageIcon combineSymbols(String symbols) {
 
        String[] manaSymbols = symbols.split("(?<=})");
        int combinedWidth = 0;
        int maxHeight = 0;
 
        for (int i = 0; i < manaSymbols.length; i++) {
 
            BufferedImage currentSymbolImage = manaSymbolImages.get(manaSymbols[i]);
            combinedWidth += currentSymbolImage.getWidth();
 
            if (maxHeight < currentSymbolImage.getWidth()) {
                maxHeight = currentSymbolImage.getWidth();
            }
        }
 
        BufferedImage combinedManaCostImage = new BufferedImage(combinedWidth, maxHeight,
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = combinedManaCostImage.createGraphics();
 
        int currentPosition = 0;
 
        for (int i = 0; i < manaSymbols.length; i++) {
 
            BufferedImage tempCurrentImage = manaSymbolImages.get(manaSymbols[i].trim());
            graphics.drawImage(tempCurrentImage, null, currentPosition, 0);
            currentPosition += tempCurrentImage.getWidth();
        }
 
        graphics.dispose();
        return (new ImageIcon(combinedManaCostImage));
    }
 
    /* Original source of this is here:
     * 
     * And one update to it is here:
     *  */
    private static class ScrollablePanel extends JPanel implements Scrollable {
 
        @Override
        public Dimension getPreferredScrollableViewportSize() {
 
            return super.getPreferredSize();
        }
 
        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
                int direction) {
 
            return 16;
        }
 
        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation,
                int direction) {
 
            return 16;
        }
 
        @Override
        public boolean getScrollableTracksViewportWidth() {
 
            return true;
        }
 
        @Override
        public boolean getScrollableTracksViewportHeight() {
 
            boolean track = true;
            Container parent = getParent();
            if (parent instanceof JViewport) {
 
                JViewport viewport = (JViewport) parent;
                if (viewport.getHeight() < getPreferredSize().height) {
                    track = false;
                }
 
            }
 
            return track;
        }
    }
}

您可以尝试定义自己的 TabStops 来对齐图标。

如果您知道图标的大小和 JTextPane 的宽度,只需将您的内容添加为 "Person Name -tab- icon" 并为段落设置自定义 TabSet。 TabSet 只有一个 TabStop。 TabStop 位置 = jTextPaneWidth - iconWidth

我认为问题在于您插入图像的方式,而且很可能是通过 combineSymbol 方法。

这是在 JTextPane 中插入内容的方法:

public class Test {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame mainFrame = new JFrame("test");
                mainFrame.setSize(300, 100);
                mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Container pane = mainFrame.getContentPane();
                pane.setLayout(new BorderLayout());

                JTP jtp = new JTP();
                pane.add(jtp);

                mainFrame.setVisible(true);
            }
        });
    }

    static class JTP extends JTextPane {

        JTP() {
            HTMLEditorKit eKit = new HTMLEditorKit();
            setEditorKit(eKit);
            HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
            htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML

            //inserting plain text (just change null for an attributeSet for styled text)
            try {
                htmlDoc.insertString(0, "test", null);
            } catch (BadLocationException ex) {
                Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
            }

            //inserting images
            insertIcon(new ImageIcon("image.png"));
            insertIcon(new ImageIcon("image.png"));

            //inserting components (With component, you should specify the yAlignment by yourself)
            JLabel label = new JLabel(new ImageIcon("image.png"));
            label.setAlignmentY(JLabel.TOP);
            insertComponent(label);
        }

    }

}

但是为了使事情变得更简单,我强烈建议您在 JTextPane 之外使用标题行。文本编辑器并不是真正为在同一行上具有不同对齐方式的文本而设计的。这是我的建议:

public class Test {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame mainFrame = new JFrame("test");
                mainFrame.setSize(300, 100);
                mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Container pane = mainFrame.getContentPane();
                pane.setLayout(new BorderLayout());
                pane.setBackground(Color.WHITE);

                pane.add(new JTP());
                pane.add(new Title(), BorderLayout.NORTH);

                mainFrame.setVisible(true);
            }
        });
    }

    static class JTP extends JTextPane {

        JTP() {
            setEditable(false);
            setOpaque(false);

            HTMLEditorKit eKit = new HTMLEditorKit();
            setEditorKit(eKit);
            HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
            htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML

            //inserting plain text (just change null for an attributeSet for styled text)
            try {
                htmlDoc.insertString(0, "capacity : ", null);
            } catch (BadLocationException ex) {
                Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
            }

            //inserting images
            insertIcon(new ImageIcon("image.png"));
            insertIcon(new ImageIcon("image.png"));

            //inserting components (With component, you should specify the yAlignment by yourself)
            JLabel label = new JLabel(new ImageIcon("image.png"));
            label.setAlignmentY(JLabel.TOP);
            insertComponent(label);
        }

    }

    static class Title extends JPanel {

        Title() {
            setLayout(new BorderLayout());
            setOpaque(false);
            add(new JLabel("<html><b>Card title</b></html>"), BorderLayout.CENTER);
            add(new JLabel(new ImageIcon("image.png")), BorderLayout.EAST);
        }

    }

}