Java 如何让 JTextArea 最多占据 GridBagLayout 宽度的 80%

How to make JTextArea occupy a max of 80% of the width in GridBagLayout in Java

我正在尝试聊天 window,但我似乎无法弄清楚如何使 JTextArea 最多占据 GridBagLayout 中可用宽度的 80% space。换句话说,如果文本看起来占据了 window 的整个宽度,它应该只能占据 80% 的宽度并且只是换行。此外,如果文本小于 80%,比如说 40%,那么 JTextArea 应该只换行 40% 而不是整个 80%。
我正在使用 GridBagLayout 并将 GridBagConstraints weightx 设置为 1.0,并将填充设置为 HORIZONTAL,左侧或右侧的插图为 40,具体取决于用户。但我似乎无法让 JTextArea 包装文本并允许它在 window 上使用 80% 的宽度(如果需要),否则环绕文本。

这是我当前的聊天 window 的样子:
请注意 David 的第一个条目不应占据整个宽度。它应该只包装文本。



这是我要完成的工作:



下面是我写的当前代码:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.util.LinkedList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;


public class ChatWindow {

    private JFrame mMainFrame;

    public ChatWindow(List<ChatEntry> chatContentList) {
        // Create JFrame, set window size and center on screen.
        mMainFrame = new JFrame();
        mMainFrame.setTitle("Chat Window");
        mMainFrame.setSize(360, 600);
        mMainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mMainFrame.setLocationRelativeTo(null);

        // Create parent container JPanel for all other JComponents.
        JPanel mainPanel = new JPanel();
        mainPanel.setLayout(new GridBagLayout());

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1.0;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        mainPanel.add(buildChatUI(chatContentList), gbc);

        // Add empty JPanel as an object to fill the empty space available.
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weighty = 1.0;
        mainPanel.add(new JPanel(), gbc);

        mMainFrame.getContentPane().add(new JScrollPane(new VerticalScrollPane(mainPanel)));
        mMainFrame.setVisible(true);
    }

    public JPanel buildChatUI(List<ChatEntry> chatContentList) {
        JPanel chatPanel = new JPanel();
        chatPanel.setLayout(new GridBagLayout());
        chatPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        GridBagConstraints gbc = new GridBagConstraints();

        for (ChatEntry chatEntry : chatContentList) {
            JLabel nameLabel = new JLabel(chatEntry.name);

            JTextArea contentTextArea = new JTextArea();
            contentTextArea.setText(chatEntry.content);
            contentTextArea.setOpaque(true);
            contentTextArea.setLineWrap(true);
            contentTextArea.setWrapStyleWord(true);
            contentTextArea.setEditable(false);

            // Arrange each chat entry based on the user.
            if (chatEntry.type == 1) {
                contentTextArea.setBackground(Color.YELLOW);
                gbc.anchor = GridBagConstraints.WEST;
            }
            else {
                contentTextArea.setBackground(Color.CYAN);
                gbc.anchor = GridBagConstraints.EAST;
            }

            gbc.insets.set(0, 0, 0, 0);
            gbc.weightx = 1.0;
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gbc.fill = GridBagConstraints.NONE;
            chatPanel.add(nameLabel, gbc);

            if (gbc.anchor == GridBagConstraints.WEST) {
                gbc.fill = GridBagConstraints.HORIZONTAL;
                gbc.insets.set(0, 0, 0, 40);
                chatPanel.add(contentTextArea, gbc);
            }
            else {
                gbc.fill = GridBagConstraints.HORIZONTAL;
                gbc.insets.set(0, 40, 0, 0);
                chatPanel.add(contentTextArea, gbc);
            }
        }

        return chatPanel;
    }


    /**
     * This class is used to make the JTextArea lines wrap every time the window
     * is resized. Without this, the JTextArea lines will not shrink back if the
     * parent window shrinks. This is achieved by returning true on getScrollableTracksViewportWidth();
     */
    private class VerticalScrollPane extends JPanel implements Scrollable {

        private static final long serialVersionUID = 7477168367035025136L;

        public VerticalScrollPane() {
            this(new GridLayout(0, 1));
        }

        public VerticalScrollPane(LayoutManager lm) {
            super(lm);
        }

        public VerticalScrollPane(Component comp) {
            this();
            add(comp);
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect,
                int orientation, int direction) {
            return 10;
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect,
                int orientation, int direction) {
            return 100;
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return true;
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return false;
        }

    }

    /**
     * Class structure for storing a single chat entry in a full conversation.
     */
    public static class ChatEntry {
        public String name;
        public String content;

        // For type 0=sent, 1=received.
        public int type;

        public ChatEntry(String name, String content, int type) {
            this.name = name;
            this.content = content;
            this.type = type;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                // Simulate a conversation between two users and pass it on to the
                // chat window to test the UI.
                List<ChatEntry> chatContentList = new LinkedList<>();
                chatContentList.add(new ChatEntry("David", "Hey Lori, how are you?", 0));
                chatContentList.add(new ChatEntry("Lori", "Hi David, I'm good. What have you been up to?", 1));
                chatContentList.add(new ChatEntry("David", "I've been super busy with work.", 0));
                chatContentList.add(new ChatEntry("David", "Haven't had much free time to even go out to eat.", 0));
                chatContentList.add(new ChatEntry("Lori", "I know what you mean, I've had to work on projects after projects.", 1));
                chatContentList.add(new ChatEntry("David", "Let's make some time and go to lunch tomorrow!", 0));
                chatContentList.add(new ChatEntry("Lori", "That sounds great, let's do 12pm. I know a great food truck by my building.", 1));
                chatContentList.add(new ChatEntry("David", "Perfect, I'll meet you at the entrance of your building.", 0));
                chatContentList.add(new ChatEntry("Lori", "Awesome, see you tomorrow :)", 1));
                new ChatWindow(chatContentList);
            }
        });
    }

}

当 window 调整大小时,重新布局 TextAreas 的方式非常耗时。我为每个 ChatEntry 添加了 ComponentListeners,它将调整条目 TextArea 的大小。这只是概念证明,可以提高性能。

尝试以下方法,缩小windows宽度,然后慢慢加宽,看看效果。它类似于您想象的目标。当 window 打开时,我还没有立即实现该布局。虽然慢慢地扩大 window 效果很好,但如果你移动得太快,那么由于缓存在 JDK 代码中,行可能会显得太宽。

无论如何,我想分享我的实验,也许它可以帮助你找到解决它的方法。我假设您可能必须实施自己的布局管理器才能完全实现既定目标并规避观察到的缓存。

在 TextArea 中计算行数的方法取自另一个 Whosebug 问题:How to count the number of lines in a JTextArea, including those caused by wrapping?

public JPanel buildChatUI(List<ChatEntry> chatContentList) {
    final JPanel chatPanel = new JPanel();
    chatPanel.setLayout(new GridBagLayout());
    chatPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

    GridBagConstraints gbc = new GridBagConstraints();

    for (ChatEntry chatEntry : chatContentList) {
        JLabel nameLabel = new JLabel(chatEntry.name);

        final JTextArea contentTextArea = new JTextArea();
        contentTextArea.setText(chatEntry.content);
        contentTextArea.setOpaque(true);
        contentTextArea.setLineWrap(true);
        contentTextArea.setWrapStyleWord(true);
        contentTextArea.setEditable(false);
        chatPanel.addComponentListener(new ComponentListener() {
            @Override
            public void componentResized(ComponentEvent e) {
                int lc = countLines(contentTextArea);
                GridBagLayout gbl = (GridBagLayout) chatPanel.getLayout();
                GridBagConstraints constraints = gbl.getConstraints(contentTextArea);
                if (lc == 1) {
                    if (constraints.fill == GridBagConstraints.HORIZONTAL) {
                        constraints.fill = GridBagConstraints.NONE;
                        gbl.setConstraints(contentTextArea, constraints);
                    }
                } else {
                    if (constraints.fill == GridBagConstraints.NONE) {
                        constraints.fill = GridBagConstraints.HORIZONTAL;
                        gbl.setConstraints(contentTextArea, constraints);
                    }
                }
            }
            @Override public void componentMoved(ComponentEvent e) { }
            @Override public void componentShown(ComponentEvent e) { }
            @Override public void componentHidden(ComponentEvent e) { }
        });

        // Arrange each chat entry based on the user.
        if (chatEntry.type == 1) {
            contentTextArea.setBackground(Color.YELLOW);
            gbc.anchor = GridBagConstraints.WEST;
        }
        else {
            contentTextArea.setBackground(Color.CYAN);
            gbc.anchor = GridBagConstraints.EAST;
        }

        gbc.insets.set(0, 0, 0, 0);
        gbc.weightx = 1.0;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.NONE;
        chatPanel.add(nameLabel, gbc);
        gbc.fill = GridBagConstraints.HORIZONTAL;
        if (gbc.anchor == GridBagConstraints.WEST) {
            gbc.insets.set(0, 0, 0, 40);
            chatPanel.add(contentTextArea, gbc);
        }
        else {
            gbc.insets.set(0, 40, 0, 0);
            chatPanel.add(contentTextArea, gbc);
        }
    }

    return chatPanel;
}

/**
 * From 
 * @param textArea the text area of interest
 * @return number of lines in text area
 */
private static int countLines(JTextArea textArea) {
    AttributedString text = new AttributedString(textArea.getText());
    FontRenderContext frc = textArea.getFontMetrics(textArea.getFont())
            .getFontRenderContext();
    AttributedCharacterIterator charIt = text.getIterator();
    LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(charIt, frc);
    float formatWidth = (float) textArea.getSize().width;
    lineMeasurer.setPosition(charIt.getBeginIndex());

    int noLines = 0;
    while (lineMeasurer.getPosition() < charIt.getEndIndex()) {
        lineMeasurer.nextLayout(formatWidth);
        noLines++;
    }

    return noLines;
}

感谢 Kriegel,我终于能够实现我想要的。我能够采用您发布的一些代码并以另一种方式实现它。

这是现在所有内容的屏幕截图:

代码如下:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.geom.Area;
import java.awt.geom.RoundRectangle2D;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.LinkedList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;


public class ChatWindow {

    private JFrame mMainFrame;

    public ChatWindow(List<ChatEntry> chatContentList) {
        // Create JFrame, set window size and center on screen.
        mMainFrame = new JFrame();
        mMainFrame.setTitle("Chat Window");
        mMainFrame.setSize(360, 600);
        mMainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mMainFrame.setLocationRelativeTo(null);

        // Create parent container JPanel for all other JComponents.
        JPanel mainPanel = new JPanel();
        mainPanel.setLayout(new GridBagLayout());

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1.0;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        mainPanel.add(buildChatUI(chatContentList), gbc);

        // Add empty JPanel as an object to fill the empty space available.
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weighty = 1.0;
        mainPanel.add(new JPanel(), gbc);

        mMainFrame.getContentPane().add(new JScrollPane(new VerticalScrollPane(mainPanel)));
        mMainFrame.setVisible(true);
    }

    public JPanel buildChatUI(List<ChatEntry> chatContentList) {
        JPanel chatPanel = new JPanel();
        chatPanel.setLayout(new GridBagLayout());
        chatPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        GridBagConstraints gbc = new GridBagConstraints();

        for (ChatEntry chatEntry : chatContentList) {
            JLabel nameLabel = new JLabel(chatEntry.name);

            BubblePane bubble = new BubblePane(chatPanel, chatEntry.content);

            // Arrange each chat entry based on the user.
            if (chatEntry.type == 1) {
                bubble.setBackground(Color.YELLOW);
                gbc.anchor = GridBagConstraints.WEST;
            }
            else {
                bubble.setBackground(Color.CYAN);
                gbc.anchor = GridBagConstraints.EAST;
            }

            gbc.insets.set(0, 0, 0, 0);
            gbc.weightx = 1.0;
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gbc.fill = GridBagConstraints.NONE;
            chatPanel.add(nameLabel, gbc);

            if (gbc.anchor == GridBagConstraints.WEST) {
                gbc.fill = GridBagConstraints.HORIZONTAL;
                gbc.insets.set(0, 0, 10, 40);
                chatPanel.add(bubble, gbc);
            }
            else {
                gbc.fill = GridBagConstraints.HORIZONTAL;
                gbc.insets.set(0, 40, 10, 0);
                chatPanel.add(bubble, gbc);
            }
        }

        return chatPanel;
    }

    private class BubblePane extends JTextArea {

        private static final long serialVersionUID = -6113801969569504295L;

        private int radius = 10;
        private int strokeThickness = 3;
        private int padding = strokeThickness / 2;
        private JPanel mParent;

        public BubblePane(JPanel parent, String text) {
            mParent = parent;

            setOpaque(false);
            setLineWrap(true);
            setWrapStyleWord(true);
            setEditable(false);
            setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
            setText(text);
        }

        @Override
        public void paintComponent(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;
            g2d.setColor(getBackground());
            int x = padding + strokeThickness;
            int width = getWidth() - (strokeThickness * 2);
            int bottomLineY = getHeight() - strokeThickness;
            g2d.fillRect(x, padding, width, bottomLineY);
            g2d.setRenderingHints(new RenderingHints(
                    RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON));
            g2d.setStroke(new BasicStroke(strokeThickness));
            RoundRectangle2D.Double rect = new RoundRectangle2D.Double(x, padding,
                    width, bottomLineY, radius, radius);
            Area area = new Area(rect);
            g2d.draw(area);


            int lc = countLines(this);
            GridBagLayout gbl = (GridBagLayout) mParent.getLayout();
            GridBagConstraints constraints = gbl.getConstraints(this);
            if (lc == 1) {
                if (constraints.fill == GridBagConstraints.HORIZONTAL) {
                    constraints.fill = GridBagConstraints.NONE;
                    gbl.setConstraints(this, constraints);
                    this.setSize(
                            getFontMetrics(getFont()).stringWidth(getText()) + 
                            this.getBorder().getBorderInsets(this).left +
                            this.getBorder().getBorderInsets(this).right, 
                            getHeight() +
                            this.getBorder().getBorderInsets(this).top + 
                            this.getBorder().getBorderInsets(this).bottom);
                }
            } else {
                if (constraints.fill == GridBagConstraints.NONE) {
                    constraints.fill = GridBagConstraints.HORIZONTAL;
                    gbl.setConstraints(this, constraints);

                }
            }

            super.paintComponent(g);
        }

        private int countLines(JTextArea textArea) {
            AttributedString text = new AttributedString(textArea.getText());
            FontRenderContext frc = textArea.getFontMetrics(textArea.getFont())
                    .getFontRenderContext();
            AttributedCharacterIterator charIt = text.getIterator();
            LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(charIt, frc);
            float formatWidth = (float) textArea.getSize().width;
            lineMeasurer.setPosition(charIt.getBeginIndex());

            int noLines = 0;
            while (lineMeasurer.getPosition() < charIt.getEndIndex()) {
                lineMeasurer.nextLayout(formatWidth);
                noLines++;
            }

            return noLines;
        }
    }


    /**
     * This class is used to make the JTextArea lines wrap every time the window
     * is resized. Without this, the JTextArea lines will not shrink back if the
     * parent window shrinks. This is achieved by returning true on getScrollableTracksViewportWidth();
     */
    private class VerticalScrollPane extends JPanel implements Scrollable {

        private static final long serialVersionUID = 7477168367035025136L;

        public VerticalScrollPane() {
            this(new GridLayout(0, 1));
        }

        public VerticalScrollPane(LayoutManager lm) {
            super(lm);
        }

        public VerticalScrollPane(Component comp) {
            this();
            add(comp);
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect,
                int orientation, int direction) {
            return 10;
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect,
                int orientation, int direction) {
            return 100;
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return true;
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return false;
        }
    }

    /**
     * Class structure for storing a single chat entry in a full conversation.
     */
    public static class ChatEntry {
        public String name;
        public String content;

        // For type 0=sent, 1=received.
        public int type;

        public ChatEntry(String name, String content, int type) {
            this.name = name;
            this.content = content;
            this.type = type;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                // Simulate a conversation between two users and pass it on to the
                // chat window to test the UI.
                List<ChatEntry> chatContentList = new LinkedList<>();
                chatContentList.add(new ChatEntry("David", "Hey Lori, how are you?", 0));
                chatContentList.add(new ChatEntry("Lori", "Hi David, I'm good. What have you been up to?", 1));
                chatContentList.add(new ChatEntry("David", "I've been super busy with work.", 0));
                chatContentList.add(new ChatEntry("David", "Haven't had much free time to even go out to eat.", 0));
                chatContentList.add(new ChatEntry("Lori", "I know what you mean, I've had to work on projects after projects.", 1));
                chatContentList.add(new ChatEntry("David", "Let's make some time and go to lunch tomorrow!", 0));
                chatContentList.add(new ChatEntry("Lori", "That sounds great, let's do 12pm. I know a great food truck by my building.", 1));
                chatContentList.add(new ChatEntry("David", "Perfect, I'll meet you at the entrance of your building.", 0));
                chatContentList.add(new ChatEntry("Lori", "Awesome, see you tomorrow :)", 1));
                new ChatWindow(chatContentList);
            }
        });
    }

}