JScrollBar 内自动换行的布局

Posted

技术标签:

【中文标题】JScrollBar 内自动换行的布局【英文标题】:Layout with auto-wrap within JScrollBar 【发布时间】:2020-01-26 14:00:51 【问题描述】:

我正在尝试构建一个面板,其组件水平布局,如果位置不足则自动换行和垂直滚动条。

类似这样的:

+-----------------+
|[1][2][3][4][5]  |
|                 | 
+-----------------+

缩小宽度:

+-----------+
|[1][2][3]  |
|[4][5]     |
+-----------+

再次缩小宽度,出现滚动条:

+---------+
|[1][2]  ^|
|[3][4]  v|
+---------+

我离解决方案不远了:

public class TestFlow extends JFrame 

    public TestFlow() 

        getContentPane().setLayout(new BorderLayout());

        JPanel panel = new JPanel(new FlowLayout());
        JScrollPane scroll = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new MyComponent("A"));
        panel.add(new MyComponent("B"));
        panel.add(new MyComponent("C"));
        panel.add(new MyComponent("D"));
        panel.add(new MyComponent("E"));
        panel.add(new MyComponent("F"));
        panel.add(new MyComponent("G"));
        panel.add(new MyComponent("H"));
        panel.add(new MyComponent("I"));
        panel.add(new MyComponent("J"));
        panel.add(new MyComponent("K"));
        panel.add(new MyComponent("L"));
        panel.add(new MyComponent("M"));
        panel.add(new MyComponent("N"));
        panel.add(new MyComponent("O"));

        scroll.addComponentListener(new ComponentAdapter() 

            @Override
            public void componentResized(ComponentEvent e) 
                Dimension max=((JScrollPane)e.getComponent()).getViewport().getExtentSize();
//                panel.setMaximumSize(new Dimension(max.width,Integer.MAX_VALUE));
                panel.setPreferredSize(new Dimension(max.width,Integer.MAX_VALUE));
//                panel.setPreferredSize(max);
                panel.revalidate();
//                panel.repaint();
//                System.out.println(panel.getSize().width+"--"+max.width);
            

        );

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(500, 200);
        setLocationRelativeTo(null);
    

    public static void main(String[] args) 
        SwingUtilities.invokeLater(() -> new TestFlow().setVisible(true));
    

    private static class MyComponent extends JLabel 

        public MyComponent(String text) 
            super(String.join("", Collections.nCopies((int)(Math.round(Math.random()*4)+4), text)));
            setOpaque(true);
            setBackground(Color.YELLOW);
        

    


,但仍有奇怪的行为:

用解决方案panel.setPreferredSize(new Dimension(max.width,Integer.MAX_VALUE));

调整窗口大小后,面板变空。我必须手动移动滚动条才能显示内容 滚动条始终可见,但不应该

用解决方案panel.setPreferredSize(max);

调整窗口大小后,面板不会重新布局。我必须再次手动移动窗口才能重新布置内容。 滚动条永远不可见,但它应该可见。

该代码中有什么建议吗?

[编辑] 我将原始代码复杂化,并应用了迄今为止提供的建议。

出于设计目的,我想在面板顶部使用 MigLayout。一开始,一切都布置得很好。放大窗口时,它也可以工作。但不是减少窗口。 addComponentListener不会带来任何附加值。

public class TestMigFlow extends JFrame 

    public TestMigFlow() 

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel(new MigLayout("debug, fill, flowy", "[fill]"));
        JScrollPane scroll = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new JLabel("A title as spearator "), "growy 0");

        JPanel sub = new JPanel(new WrapLayout());
//        panel.add(sub, "growx 0"); // Works well on shrink but not on grow
        panel.add(sub); // Works well on grow but not on shrink

        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));

        addComponentListener(new ComponentAdapter() 

            @Override
            public void componentResized(ComponentEvent e) 
                Dimension max = new Dimension(scroll.getWidth(), Short.MAX_VALUE);
                panel.setMaximumSize(max);
                panel.repaint();
            

        );

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    

    public static void main(String[] args) 
        SwingUtilities.invokeLater(() -> new TestMigFlow().setVisible(true));
    

    private static class MyComponent extends JLabel 

        public MyComponent(String text) 
            super(String.join("", Collections.nCopies((int) (Math.round(Math.random() * 4) + 4), text)));
            setOpaque(true);
            setBackground(Color.YELLOW);
        

    

【问题讨论】:

How do I make this FlowLayout wrap within its JSplitPane?的可能重复 链接问题中的WrapLayout 真的可以做到。但附带说明:在 any 大小的计算中使用 Integer.MAX_VALUE 会严重、严重地搞砸事情。任何像 MAX_VALUE + 1 这样的计算都会导致结果为 negative 值。最接近您应该使用的“无限大”组件的是Short.MAX_VALUE(但在 > 20 年的 Swing 编程中,我只在非常特定的上下文中使用过这个一次 - 通常,更精确地知道组件的大小...) 谢谢。 WrapLayout 是该示例的解决方案。集成为由 Mi​​gLayout 布局的 Panel 中的组件,它的行为不太好。我将更新示例。 我已将示例更新为更复杂的示例。 【参考方案1】:

下面的代码对我有用。

我已将代码拆分为一个组件,该组件执行所有布局和测试代码以验证它是否工作(在static main)方法中。

我不是在 JScrollPane 上挂钩以调整事件大小,而是在整个 TestFlow (TF) 组件上执行此操作 - 我相信这是等效的,但它似乎更安全。每当调整 TF 大小时,我都会更新面板的首选大小,就像您所做的那样 - 但有两个步骤您没有采取:

内面板的宽度可以是外面板指定的任何宽度,但如果需要,还应为垂直滚动条留出足够的空间。 内部面板的高度应该是它所显示的最低组件的高度。在我的例子中,我采取了假设最低行中的所有组件高度相同的捷径,但这并不一定总是如此。

深入了解 Swing 如何处理布局和绘画,以及当谁总是让我想到黑魔法时,调用了几个看起来相似的方法中的哪一个。

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

public class TestFlow extends JPanel 

    JPanel panel;
    JScrollPane scroll;

    public TestFlow() 
        panel = new JPanel(new FlowLayout());
        scroll = new JScrollPane(panel, 
            ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, 
            ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        setLayout(new BorderLayout());
        add(scroll, BorderLayout.CENTER);
        addComponentListener(new ComponentAdapter()
            @Override
            public void componentResized(ComponentEvent e)
                Rectangle lastChildBounds = panel
                    .getComponent(panel.getComponentCount() - 1).getBounds();
                Dimension preferred = new Dimension(
                    scroll.getWidth(), 
                    lastChildBounds.y + lastChildBounds.height);
                if (scroll.getVerticalScrollBar().isVisible()) 
                    preferred.width -= scroll.getVerticalScrollBar().getWidth();
                
                // System.err.println("setting inner panel size to " + preferred);
                panel.setPreferredSize(preferred);
                panel.repaint();
            
        );
    

    public JPanel getInnerPanel()  return panel; 

    public static void main(String[] args) 
        SwingUtilities.invokeLater(() -> 
            // get a TestFlow instance and fill it with random labels
            TestFlow tf = new TestFlow();
            Random r = new Random();
            for (String s : "A B C D E F G H I J K L M N O".split(" ")) 
                String labelText = String.join("", Collections.nCopies(4+r.nextInt(3)*4, s));
                JLabel label = new JLabel(labelText);
                label.setOpaque(true);
                label.setBackground(Color.YELLOW);
                tf.getInnerPanel().add(label);
            

            // display it in a window
            JFrame jf = new JFrame("test");
            jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);            
            jf.add(tf, BorderLayout.CENTER);
            jf.setSize(500, 200);
            jf.setLocationRelativeTo(null);
            jf.setVisible(true);
        );
    

【讨论】:

谢谢。但是,对于原始情况, WrapLayout 建议工作正常且更容易。我在更复杂的情况下应用了您的建议,但没有带来附加值。【参考方案2】:

可能我误解了你的问题,但是当我添加revalidate 调用时,当面板缩小时也一切正常。

Dimension max = new Dimension(scroll.getWidth(), Short.MAX_VALUE);
panel.setMaximumSize(max);
// trigger layout recalculation
panel.revalidate();
panel.repaint();

这里有一点复杂的改进(为滚动条和布局提供更好的尺寸考虑),如果需要的话;)

public class TestMigFlow extends JFrame 
    private Integer layoutDecr;

    public TestMigFlow() 

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel(new MigLayout("debug, fill, flowy", "[fill]"));
        JScrollPane scroll =
                new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new JLabel("A title as spearator "), "growy 0");

        JPanel sub = new JPanel(new WrapLayout());
        // panel.add(sub, "growx 0"); // Works well on shrink but not on grow
        panel.add(sub); // Works well on grow but not on shrink

        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));

        addComponentListener(new ComponentAdapter() 

            @Override
            public void componentResized(ComponentEvent e) 
                if (layoutDecr == null && panel.getWidth() > 0) 
                    layoutDecr = panel.getWidth() - sub.getWidth();
                
                JScrollBar vBar = scroll.getVerticalScrollBar();
                int decr = vBar.isVisible() ? vBar.getPreferredSize().width : 0;
                decr += layoutDecr == null ? 0 : layoutDecr;
                Dimension max = new Dimension(scroll.getWidth() - decr, Short.MAX_VALUE);
                sub.setMaximumSize(max);
                sub.revalidate();
                sub.repaint();
            

        );

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    

    public static void main(String[] args) 
        SwingUtilities.invokeLater(() -> new TestMigFlow().setVisible(true));
    

    private static class MyComponent extends JLabel 

        public MyComponent(String text) 
            super(String.join("", Collections.nCopies((int) (Math.round(Math.random() * 4) + 4), text)));
            setOpaque(true);
            setBackground(Color.YELLOW);
        

    

【讨论】:

内容在启动和扩大窗口时布局良好,但在缩小窗口宽度时没有。要正确缩小窗口,请先缩小窗口,然后再将其小幅放大。那么你做对了。所以这没有按预期工作。 Miglayout 选项要做些什么? 摆脱 MigLayout 怎么样?我的目标是拥有一堆元素:一个永远不会垂直增长或收缩的标签分隔符,然后是一个包含不同图像(示例中的“AAA”、“BBB”)的面板,这些图像可以垂直增长和收缩,具体取决于显示所有元素所需的位置,具体取决于可用宽度。这种组合标签分隔符/图像容器可以重复多次。 Miglayout 似乎是一个不错的方法... 对话框级别的addComponentListener 是错误的选择。因为那个事件的那一刻,面板没有收到它的全宽,所有的计算都是正确的。诀窍还在于使用 WrapLayout 的preferredLayoutSize 在重新验证之前计算所有内容。查看我的解决方案。【参考方案3】:

解决办法是

    等到面板容器的大小调整完毕。因此在面板级别而不是框架级别拥有addComponentListener 使用 WrapLayout 依靠 WrapLayout 的preferredLayoutSize 计算出足够的面板尺寸 在 MigLayout 中使用“fillx”选项

解决方案的缺点是在调整窗口大小时有点闪烁。

public class TestMigFlow2 extends JFrame 

    public TestMigFlow2() 

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel(new MigLayout("fillx, flowy", "[fill]"));
        JScrollPane scroll
                = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new MySeparator("sep1"), "growy 0, shrinky 100");

        JPanel sub = new JPanel(new WrapLayout());
        panel.add(sub, "shrinky 100"); // Works well on grow but not on shrink

        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));

        panel.add(new MySeparator("sep2"), "growy 0, shrinky 100");

        panel.addComponentListener(new ComponentAdapter() 
            @Override
            public void componentResized(ComponentEvent e) 

                WrapLayout wl = (WrapLayout) sub.getLayout();
                Dimension prefdim = wl.preferredLayoutSize(sub);
                sub.setPreferredSize(prefdim);
                panel.revalidate();
                panel.repaint();
            

        );

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    

同样也适用于 BoxLayout 而不是 MigLayout 并添加了 2 个功能:

    添加 VerticulGlue 作为最后一个元素

    为面板设置一个最大高度和首选项高度,以防止 BoxLayout 共享面板和垂直胶水之间的额外空间。

    公共 TestBoxLayout()

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel();
        BoxLayout layout = new BoxLayout(panel, BoxLayout.PAGE_AXIS);
        panel.setLayout(layout);
        JScrollPane scroll
                = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    
        getContentPane().add(scroll, BorderLayout.CENTER);
    
        JLabel separator;
        separator = new MySeparator("Sep1");
        panel.add(separator);
    
        JPanel sub = new MyPanel(new WrapLayout());
        sub.setAlignmentX(0f);
        panel.add(sub);
    
        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));
    
        separator = new MySeparator("Sep2");
        panel.add(separator);
    
        // -- Un filler --
        panel.add(Box.createVerticalGlue());
    
    
        panel.addComponentListener(new ComponentAdapter() 
            @Override
            public void componentResized(ComponentEvent e) 
    
                WrapLayout wl=(WrapLayout) sub.getLayout();
                Dimension prefdim=wl.preferredLayoutSize(sub);
                sub.setPreferredSize(prefdim);
                // Force the max height = pref height to prevent the BoxLayout dispatching the remaining height between the panel and the glue.
                Dimension maxdim=new Dimension(Short.MAX_VALUE,prefdim.height);
                sub.setMaximumSize(maxdim);
                panel.revalidate();
                panel.repaint();
            
    
        );
    
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    
    

【讨论】:

以上是关于JScrollBar 内自动换行的布局的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义View(LineBreakLayout-自动换行的标签容器)

CSS自动换行强制不换行强制断行超出显示省略号

Flutter 流式布局自动换行(WrapFlow)

C# winform label控件 行高 自动换行等问题

android 中多个TextView放在一个LinearLayout中,请问如何将TextView中的文字自动换行?或者修改布局实现

CSS自动换行强制不换行强制断行超出显示省略号