Выравнивание и встраивание компонентов (или значков) в JTextPane

Я работаю над приложением на Java, которое, помимо прочего, должно отображать детали карты Magic: The Gathering в текстовом поле (так как я использую Swing, это в настоящее время JTextPane).

Эти детали содержат текст и небольшие значки, причем некоторые из этих значков выделены текстом (так что текст обтекает их), а некоторые значки, по моему дизайну, выровнены по правому краю с некоторым выравниванием по левому краю в одной строке.

Я взял изображение из другого приложения, которое использует дизайн, очень похожий на тот, над которым я работаю (хотя не на Java):

введите описание изображения здесь

Вот как это должно выглядеть в принципе.

Теперь, ради любви ко всему, я не могу заставить это работать в JTextPane,

Я начал с попытки сделать это с помощью CSS, но обнаружил, что JEditorPane и подклассы не поддерживают атрибут "float", поэтому я попробовал это с использованием панели StyledDocument вместо.

Сначала не сработала первая часть (значок и текст с выравниванием по правому краю всегда игнорировали их выравнивание и помещались непосредственно после текста с выравниванием по левому краю в строке), пока я не нашел этот вопрос.

Было предложено использовать следующую строку кода:

pane.setEditorKit(new HTMLEditorKit());

что-то действительно исправило мою первую проблему.

Но теперь я застрял во второй части, получая эти значки во второй части в соответствии с текстом. Вот что я сейчас получил:

введите описание изображения здесь

Я обнаружил, что по какой-то причине, когда вы переключаете JTextPane в режиме HTML с помощью редактора с строкой кода выше, вставка компонентов просто сходит с ума.

И значки в верхней части (которые я фактически слил в одно изображение), и значки в тексте ниже (не объединены) находятся внутри JLabels, но это не имеет значения, если я добавлю их в виде изображений или внутри JLabels, Изображения или метки определенно не больше, чем вы видите там, я понятия не имею, откуда взялись лишние пробелы.

Я нашел этот вопрос, и ответ предполагает, что это какая-то ошибка или просто странное поведение в режиме html JEditorPane,

Если я снова удалю приведенную выше строку кода, я получу исходную проблему:

введите описание изображения здесь

В зависимости от того, где именно значки находятся в тексте, я получаю всевозможные странные результаты. Я собрал еще несколько примеров фотографий ниже:

введите описание изображения здесь

Итак, как я мог это исправить? JTextPane работает нормально для меня, за исключением этой части, но я мог бы использовать какое-то другое решение, пока конечный результат все еще выглядит так же. Помните, что я, возможно, захочу добавить туда некоторые другие компоненты (например, кнопку), поэтому я хотел бы придерживаться чего-то родного для Swing, если это вообще возможно.

Пользователь не сможет редактировать содержимое TextPane, но я бы хотел добавить опцию позже, чтобы скопировать все содержимое одним щелчком мыши (так что я бы предпочел остаться с текстовой областью).

Ниже я собрал (не совсем минимальный) рабочий пример, с которым вы можете поиграть:

(РЕДАКТИРОВАТЬ: обновленный код в нижней части! Старый код все еще там по следующей ссылке.)

http://pastebin.com/RwAdPCzb

Значки, которые я использую, приведены ниже. Вам нужно будет переименовать их и изменить путь в коде.

введите описание изображения здесь
введите описание изображения здесь
введите описание изображения здесь

Некоторые вещи, на которые стоит обратить внимание:

  • В начале я стилизовал текст с помощью класса `Style`, как описано в учебном пособии" Как использовать панели редактора и текстовые панели "на веб-сайте Oracle (TextSamplerDemo.java, для вашей справки). При этом я даже не смог выполнить правую часть вверху. Странно, но когда я использовал класс SimpleAttributeSet для стилизации, даже с теми же настройками, это работает.
  • Я пробовал разные варианты выравнивания как текста, так и надписей, которые содержат значки. Независимо от того, какие варианты я использовал, не было никакой видимой разницы вообще.

ОБНОВЛЕНИЕ 1:

После ответа Sharcoux я отредактировал свой код так, чтобы над JTextPane было 2 JLabels, которые содержат две строки, которые должны были иметь разные выравнивания (часть, выровненная по левому и правому краям). JTextPane больше не использует HTMLEditorKit, и я использую insertIcon() для вставки значков в текст.

Таким образом, значки вставляются (почти) правильно!
Изображение здесь:
введите описание изображения здесь

Тем не менее, есть две маленькие вещи, которыми я все еще не удовлетворен:

Первый:
Мне нужно поместить все в JScrollPane, потому что текст в TextPane намного больше в моем реальном приложении. Поскольку теперь у меня есть три компонента вместо просто TextPane, мне нужно было поместить все в JPanel, а это в ScrollPane.
Однако, если вы сделаете это так, JTextPane не будет знать, что его значение больше не должно превышать значение JScrollPane. Он останавливает обтекание текста и становится таким же большим, как весь текст.
Я открыл для этого новый вопрос, так как чувствую, что это основная проблема Swing и заслуживает отдельного вопроса. Если вы хотите помочь, вот ссылка:
JTextComponent внутри JPanel внутри JScrollPane

Во- вторых:
Вероятно, это невозможно, но я все равно спрошу. Значки имеют ту же базовую линию, что и текст, когда вы добавляете их таким образом. Есть ли способ переместить их чуть ниже? 2-3 пикселя, может быть? Таким образом, они будут лучше соответствовать тексту. Две картинки ниже.
Вот как это выглядит сейчас:
введите описание изображения здесь
И вот как я бы хотел, чтобы это выглядело так:
введите описание изображения здесь

Может быть, я могу создать подкласс и переопределить какую-то часть JTextPane, чтобы переместить все иконки, отображаемые на ней, на заданное количество пикселей или что-то в этом роде?

Для справки, здесь также мой новый, обновленный код. Я заменил старый выше на ссылку pastebin.com, если вы все еще хотите посмотреть на него.


ОБНОВЛЕНИЕ 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:
     * https://stackru.com/questions/15783014/jtextarea-on-jpanel-inside-jscrollpane-does-not-resize-properly
     * 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;
        }
    }
}

2 ответа

Решение

Я думаю, что проблема заключается в том, как вы вставляете свои изображения, и, скорее всего, из вашего метода combSymbol.

Вот способ вставить материал в 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);
        }

    }

}

Вы можете попытаться определить свои собственные TabStops для выравнивания значков.

Если вы знаете размер иконки и ширину JTextPane, просто добавьте свой контент как "Имя человека -tab- icon" и установите пользовательский TabSet для абзаца. TabSet имеет только один TabStop. Позиция TabStop = jTextPaneWidth - iconWidth

Другие вопросы по тегам