GridBagLayout gridwidth 不能按预期工作

GridBagLayout gridwidth doesn't work as expected

我正在使用 java swing LayoutManager GridBagLayout,运行 遇到了这个问题。我想要这样的布局

ACC
BB

但是得到这样的布局

ACC
B

尽管 B 的网格宽度为 2,而 A 的网格宽度为 1,但 A 和 B 占用的列数相同。我认为 A、B 和 C 之间不会有一个非常小的列,因为 C 开始于第 1 列。如果 C 的网格宽度为 1 而不是 2,则不会出现此问题。我对输出感到困惑。

为什么会这样/我该如何解决?

JFrame test = new JFrame();
test.setSize(800,800);
test.setLayout(new GridBagLayout());
test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

GridBagConstraints c;

c = new GridBagConstraints();
c.gridx=0;
c.gridy=0;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("A"), c);

c = new GridBagConstraints();
c.gridx=0;
c.gridy=2;
c.gridwidth=2;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("B"), c);

c = new GridBagConstraints();
c.gridx=1;
c.gridy=0;
c.gridwidth=2;
c.gridheight=2;
c.fill = GridBagConstraints.BOTH;
c.weightx = 2;
c.weighty = 2;
test.add(new JButton("C"), c);

test.setVisible(true);

我知道你的感受...似乎 GridBagLayout 在列未被 1x1 组件填充时将列的宽度减小为 0。我不知道以下是否是最有效的解决方案,但它有效(通过将两个虚拟 JPanel 添加到 "inflate" 第 1 列和第 2 列):

JFrame test = new JFrame();
test.setSize(800,800);
test.setLayout(new GridBagLayout());
test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

GridBagConstraints c;

c = new GridBagConstraints();
c.gridx=0;
c.gridy=0;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("A"), c);

c = new GridBagConstraints();
c.gridx=0;
c.gridy=1;
c.gridwidth=2;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("B"), c);

c = new GridBagConstraints();
c.gridx=1;
c.gridy=0;
c.gridwidth=2;
c.gridheight=1;
c.fill = GridBagConstraints.BOTH;
c.weightx = 2;
c.weighty = 2;
test.add(new JButton("C"), c);

c = new GridBagConstraints();
c.gridx=1;
c.gridy=2;
c.gridwidth=1;
c.gridheight=0;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.weighty = 0;
test.add(new JPanel(), c);

c = new GridBagConstraints();
c.gridx=2;
c.gridy=3;
c.gridwidth=1;
c.gridheight=0;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.weighty = 0;
test.add(new JPanel(), c);

test.setVisible(true);

TL;DR

在 small-sized single-cell JPanelJSeparator 的第一行添加一个虚拟行作为填充 克服这个buggy-feature。如果您事先知道您的权重(例如,如果您只希望所有列具有相同的大小),另一种选择是将 GridBagLayout.columnWeights 设置为合理的值,但是虽然此解决方法解决了权重问题,但它仍然可能导致到错误的列 widths.

这个答案描述了列宽的问题,但完全相同的算法适用于行高,所以如果您在使用 multi-row 组件时遇到问题,以下所有内容仍然有效(只需更改 xy,宽度到高度)。

文档

这种行为背后的原因是一个真正奇怪的 属性 一个非常灵活和有用的布局管理器。我什至不知道这是一个错误还是一个功能,因为列(以及因此,组件)的宽度由权重决定,所有关于权重的文档都是

The grid bag layout manager calculates the weight of a column to be the maximum weightx of all the components in a column. If the resulting layout is smaller horizontally than the area it needs to fill, the extra space is distributed to each column in proportion to its weight. A column that has a weight of zero receives no extra space.

这根本没有帮助,因为它没有说明一个组件占用多个单元格的情况。教程有点“帮助”:

Generally weights are specified with 0.0 and 1.0 as the extremes: the numbers in between are used as necessary. Larger numbers indicate that the component's row or column should get more space. For each column, the weight is related to the highest weightx specified for a component within that column, with each multicolumn component's weight being split somehow between the columns the component is in. Similarly, each row's weight is related to the highest weighty specified for a component within that row. Extra space tends to go toward the rightmost column and bottom row.

(强调我的。)

真的吗? 以某种方式拆分?现在,那个真的很有帮助。

实际算法

这种拆分背后的实际算法确实不直观。它多次遍历所有组件。在第一遍中,它仅适用于尺寸最小的组件(就占用的单元格而言)。然后它采用下一个尺寸并再次迭代,直到所有尺寸都消失。在您的例子中,组件 A 的大小为 1,组件 B 和 C 的大小为 2,因此迭代顺序为 A、C、B。如果 A 的大小为 3,则顺序为 C、B、A .

在每次迭代期间,算法使用以下过程计算组件占用的列的权重。

  1. 取组件权重并减去它占据的所有列的权重,因为它们是此时计算的(包括以前的较小尺寸的迭代!)。得到的数字是当前列的总权重与组件的权重之间的差值。

  2. 如果结果数为零或更小,什么也不做。

  3. 否则,在列之间分配此差异,与其当前权重成正比!

  4. 如果重量差有任何剩余,将这些“剩余”添加到组件所占的最右边的单元格中。

感兴趣的实际代码:

px = constraints.tempX + constraints.tempWidth; // tempX is gridx, tempWidth is gridwidth
weight_diff = constraints.weightx; // the weight of the component
for (k = constraints.tempX; k < px; k++) // iteration over the cells
    weight_diff -= r.weightX[k]; // r.weightX holds the current column weights
if (weight_diff > 0.0) {
    weight = 0.0;
    for (k = constraints.tempX; k < px; k++)
        weight += r.weightX[k]; // weight is the total weight of the cells
    for (k = constraints.tempX; weight > 0.0 && k < px; k++) {
        double wt = r.weightX[k];
        double dx = (wt * weight_diff) / weight; // a part of the weight
        r.weightX[k] += dx;
        weight_diff -= dx;
        weight -= wt;
    }
    /* Assign the remainder to the rightmost cell */
    r.weightX[px-1] += weight_diff;
}

(部分评论是我的。)

这个算法听起来或多或少是合理的,但它有一些缺点。

如果其中一列的当前权重为零,则它根本不会增加权重。如果 所有 列的权重为零,那么循环甚至不会开始,所有权重都将被视为“剩余”并进入最右边的单元格!

这就是你争论的原因

I don't think there can be a vanishingly small column between A,B and C because C starts in column 1.

并没有真正起作用,因为它是 C 结束 (而不是开始!)的专栏。

例子

这对你来说意味着什么?我认为 gridy=2 是一个错字,BB 应该有 gridy=1 (但这并没有真正改变任何东西,因为你所做的只是在 [ 之间添加一个不可见的行 1 =24=] 和 2).

  1. 在第一遍中,只访问了A。它有 weightx1,并且全部转到 0 列。这里不足为奇。但是 12 列的权重仍然为零。

  2. 在第二遍中,首先访问CC。它有 weightx2。它占据 12 列,但由于它们的权重均为零,因此所有权重都进入现在权重为 2.

    的列 2
  3. 然后访问BB。它有 weightx1。它分为 01 列。但是 1 列的权重仍然为零,因此所有这些都进入了 0 列。但是它已经有了权重1,所以它的权重和组件的权重之差为零,什么也没有发生。

得到的权重为1-0-2,布局为

A||CC
B|| // B actually goes into the second column, but it has zero width

但是如果 BBweightx2 呢?然后直到第 3 步,一切都将相同,但是组件的权重 (2) 和列的当前权重 0 (1) 之间的差异将是 1,这样1就会加到0列的权重上,它的权重就变成了2。列 1 的权重仍为零,生成的布局将是

AA||CC
BB|| // B actually goes into the second column, but it has zero width

所有三个组件都具有相同的宽度,但我仍然不像我们期望的那样。

解决方法

另一个答案中提到的一个明显的解决方案是在第一行中添加一些虚拟 single-cell 组件。一旦算法完成第一行,它将导致列在所有传递期间具有 non-zero 权重。 JSeparatorJPanel 适用于此目的。通过调整 insets 和组件的高度,很容易获得漂亮的外观。

另一种可能的解决方案

如果您事先知道您的列权重,您可以简单地覆盖整个算法并通过

简单地提供正确的权重
GridBagLayout layout = new GridBagLayout();
layout.columnWeights = new double[] { 1.0, 1.0, 1.0 };
test.setLayout(layout);

如果您需要列具有不同的权重,请相应地调整权重。然而,这种方法的问题是应用了一种有点类似的算法来计算列 widths(与权重相反)。特别是,如果在某个时刻发现某个组件需要的宽度超过该组件占据的列的总宽度,则宽度 差异 分布在 之间所有 列。

这意味着,例如,如果需要 150 个像素的组件想要占据当前宽度为 0 和 50 的两列,则差值 (100) 会在两列之间平均分配,从而得到 50 /100 拆分而不是人们期望的 75/75。解决方案是要么切换到假的第一行解决方案,要么以与 columnWeights 相同的方式直接设置列宽(通过设置 GridBagLayout.columnWidths)。当然,这需要比宽度更复杂的计算。