Java Swing 中的垂直堆叠布局?

Vertical stacked layout in Java Swing?

我想在 scrollPane 中排列一些标签,使它们一个接一个地堆叠,并且每个标签的高度都必须刚好(不能超过!)以适合文本包含,并且与相应的容器一样宽.
标签的文本使用 html 格式来实现自动换行效果。

情况如下:

如果我水平调整框架的大小,视口的总高度应该增加或减少,因为文本分别有更少或更多的水平 space.
我发现自己在这里遇到了困难,因为据我所知,布局管理器倾向于将组件放入可用 space 中,而不是相反。
我使用 gridBagLayout 来保持图像和右侧描述之间的 width 比率。
在我看来,这是一个类似于 wrap layout 解决的问题,但从垂直角度来看,基本流布局没有考虑到这一点,
或者一个允许填充水平 space 并重新计算首选高度的 boxLayout。

屏幕如下: 如果您需要一些代码,请告诉我

那么,这就是我想要实现的目标,现在您建议采用哪种方法?

编辑 1:

这是一个小例子,抱歉,如果它花了一段时间,但正如建议的那样,我是从头开始做的(我使用了 eclipse 的 window 构建器)

package view;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;

public class Gui extends JFrame{

    private JPanel contentPane;
    private JScrollPane scrollPane;
    private JPanel panel;
    private JLabel lbThumbnail;
    private JPanel descriptions;
    private JPanel header;
    private JLabel lb1;
    private JLabel lb2;
    private JLabel lb3;
    private JLabel lb4;

    /**
     * Launch the application.
     */
    public static void main(String[] args){
        EventQueue.invokeLater(new Runnable(){
            @Override
            public void run(){
                try{
                    Gui frame=new Gui();
                    frame.setVisible(true);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Create the frame.
     */
    public Gui(){
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100,100,564,365);
        contentPane=new JPanel();
        contentPane.setBorder(new EmptyBorder(5,5,5,5));
        contentPane.setLayout(new BorderLayout(0,0));
        setContentPane(contentPane);

        scrollPane = new JScrollPane();
        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        contentPane.add(scrollPane, BorderLayout.CENTER);

        panel = new JPanel();
        panel.setBackground(Color.RED);
        panel.setBorder(null);
        scrollPane.setViewportView(panel);
        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));

        header = new JPanel();
        header.setBackground(Color.GREEN);
        header.setBorder(null);
        panel.add(header);
        GridBagLayout gbl_header = new GridBagLayout();
        gbl_header.columnWidths = new int[] {120, 46};
        gbl_header.rowHeights = new int[] {14};
        gbl_header.columnWeights = new double[]{0.0, 0.0};
        gbl_header.rowWeights = new double[]{0.0};
        header.setLayout(gbl_header);

        lbThumbnail = new JLabel("thumbnail");
        lbThumbnail.setBorder(new LineBorder(Color.GRAY, 3));
        lbThumbnail.setHorizontalAlignment(SwingConstants.CENTER);
        lbThumbnail.setBackground(Color.RED);
        GridBagConstraints gbc_lbThumbnail = new GridBagConstraints();
        gbc_lbThumbnail.fill = GridBagConstraints.BOTH;
        gbc_lbThumbnail.weightx = 0.3;
        gbc_lbThumbnail.weighty = 1.0;
        gbc_lbThumbnail.gridx = 0;
        gbc_lbThumbnail.gridy = 0;
        header.add(lbThumbnail, gbc_lbThumbnail);

        descriptions = new JPanel();
        descriptions.setBackground(Color.ORANGE);
        descriptions.setBorder(null);
        GridBagConstraints gbc_descriptions = new GridBagConstraints();
        gbc_descriptions.weightx = 0.7;
        gbc_descriptions.fill = GridBagConstraints.BOTH;
        gbc_descriptions.gridx = 1;
        gbc_descriptions.gridy = 0;
        header.add(descriptions, gbc_descriptions);
        descriptions.setLayout(new BoxLayout(descriptions, BoxLayout.PAGE_AXIS));

        lb1 = new JLabel("<html>y y y y y y y y y y y y y y y y y y</html>");
        lb1.setBorder(new LineBorder(Color.GRAY, 3));
        lb1.setHorizontalAlignment(SwingConstants.CENTER);
        lb1.setAlignmentY(Component.TOP_ALIGNMENT);
        lb1.setAlignmentX(Component.CENTER_ALIGNMENT);
        descriptions.add(lb1);

        lb2 = new JLabel("<html>this label should fill the container width, and then eventually increase it's height y y y y y y y y y y y y y y y y y y</html>");
        lb2.setBorder(new LineBorder(Color.GRAY, 3));
        lb2.setHorizontalAlignment(SwingConstants.CENTER);
        lb2.setAlignmentY(Component.TOP_ALIGNMENT);
        lb2.setAlignmentX(Component.CENTER_ALIGNMENT);
        descriptions.add(lb2);

        lb3 = new JLabel("<html>the same y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y</html>");
        lb3.setBorder(new LineBorder(Color.GRAY, 3));
        lb3.setHorizontalAlignment(SwingConstants.CENTER);
        lb3.setAlignmentY(Component.TOP_ALIGNMENT);
        lb3.setAlignmentX(Component.CENTER_ALIGNMENT);
        descriptions.add(lb3);

        lb4 = new JLabel("<html>the upper panel should be as short as it can be, now it's too long<br/>y<br/>y<br/>y<br/></html>");
        lb4.setAlignmentX(0.5f);
        lb4.setBorder(new LineBorder(Color.GRAY, 3));
        lb4.setHorizontalAlignment(SwingConstants.CENTER);
        panel.add(lb4);
    }

}

输出:

希望我没有误解你的问题,所以基本上你希望换行文本标签使用可用宽度并相应地调整它的高度,对吧?我不确定内置布局管理器是否可以解决您的问题,但我认为自定义布局管理器将是最简单的方法。

首先我们将框架作为测试驱动器,它有一个滚动窗格,其中包含应用了自定义布局的面板。该自定义布局需要 "know" 组件,因此我们只需将它们传递给构造函数:

public class MainFrame extends JFrame {
    private final JTextPane txt1 = createTextPane();
    private final JTextPane txt2 = createTextPane();
    private final JTextPane txt3 = createTextPane();

    public MainFrame() {
        JPanel panel = new JPanel();

        panel.setLayout(new CustomLayout(txt1, txt2, txt3));

        panel.add(txt1);
        panel.add(txt2);
        panel.add(txt3);

        add(new JScrollPane(panel));

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setBounds(10, 10, 100, 100);
    }

并且我使用文本窗格来实现自动换行功能:

private static JTextPane createTextPane() {
    JTextPane result = new JTextPane();

    result.setContentType("text/html");
    result.setEditable(false);
    result.setText("<html>Hello World Hello World Hello World Hello World Hello World Hello World</html>");

    return result;
}

然后是CustomLayout的代码。最重要的方法是如何布局组件和 return 首选父级大小。思路是:

  • 将组件的宽度设置为可用宽度(您设置的高度无关紧要,所以我只传递零),并获得首选高度将是包装内容的高度。
  • 请注意,我使用父级的父级来获取可用宽度,因为父级尚未布局,因此其宽度不正确,而父级的父级已经布局。你可以说这是一个危险的假设,所以它真的取决于你实际情况的组件层次和结构。

    public class CustomLayout implements LayoutManager {
        private final JTextPane txt1;
        private final JTextPane txt2;
        private final JTextPane txt3;
    
    public CustomLayout(JTextPane aTxt1, JTextPane aTxt2, JTextPane aTxt3) {
        txt1 = aTxt1;
        txt2 = aTxt2;
        txt3 = aTxt3;
    }
    
    @Override
    public void addLayoutComponent(String name, Component comp) {
    }
    
    @Override
    public void removeLayoutComponent(Component comp) {
    }
    
    @Override
    public Dimension preferredLayoutSize(Container parent) {
        int width = parent.getParent().getWidth();
    
        int height = 0;
    
        height += calculateComponentPreferredHeightForWidth(txt1, width);
        height += calculateComponentPreferredHeightForWidth(txt2, width);
        height += calculateComponentPreferredHeightForWidth(txt3, width);
    
        return new Dimension(width, height);
    }
    
    private static int calculateComponentPreferredHeightForWidth(JComponent component, int width) {
        component.setSize(width, 0);
        return component.getPreferredSize().height;
    }
    
    @Override
    public Dimension minimumLayoutSize(Container parent) {
        return preferredLayoutSize(parent);
    }
    
    @Override
    public void layoutContainer(Container parent) {
        int width = parent.getWidth();
    
        int layoutX = 0;
        int layoutY = 0;
    
        txt1.setBounds(layoutX, layoutY, width, calculateComponentPreferredHeightForWidth(txt1, width));
        layoutY = txt1.getY() + txt1.getHeight();
    
        txt2.setBounds(layoutX, layoutY, width, calculateComponentPreferredHeightForWidth(txt2, width));
        layoutY = txt2.getY() + txt2.getHeight();
    
        txt3.setBounds(layoutX, layoutY, width, calculateComponentPreferredHeightForWidth(txt3, width));
    }
    

    }

您的原始图片具有误导性,因为看起来您的文字正在换行以适合视口的宽度,这令人困惑,因为通常文本在滚动窗格中显示时不会换行,因为它显示在其首选位置尺寸。

但是,您的 MCVE 显示文本确实没有换行。所以问题不在于您的布局管理器,而在于添加到滚动窗格的面板的 Scrollable 接口的实现。默认行为是显示添加到首选的组件 width/height 并在需要时显示滚动条。

在您的情况下,您希望强制 "width" 适合视口的大小,从而使文本根据需要垂直换行,然后根据需要显示垂直滚动条。这将导致标签文本换行并动态重新计算高度(因为 HTML 由 JLabel 处理)。

因此您需要为您的面板实现 Scrollable 接口并将 getScrollableTracksViewportWidth() 方法重写为 return “true”,这将强制面板的宽度适合滚动窗格视口的宽度,因此永远不会出现水平滚动条。

或者更简单的方法是使用 Scrollable Panel,它具有允许您控制 `Scrollable 界面属性的方法。

使用上面的 class 你可以改变你的代码如下:

//panel = new JPanel();
ScrollablePanel panel = new ScrollablePanel();
panel.setScrollableWidth( ScrollablePanel.ScrollableSizeHint.FIT );