SWT Section reserves too much vertical space when

2019-06-06 07:31发布

问题:

I use a GridLayout with one column inside a Section. There are three "rows" in the grid, having a white background. I would like the Section to grab horizontal space but only as much vertical space as needed:

Each row uses a custom row layout and has two children. Furthermore, that row layout has two states:

a) show on a single line (if total width is large enough)

b) show second child (red) on an extra line (if total width of window is too small for both children)

For both states I would expect the total height of the Section content (blue) to equal the sum of the heights of the rows (neglecting spacing).

However, the total height of the Section is state a) is the height that I expect for case b). The section somehow seems to reserve vertical space so that it does not need to change its height when the width is adapted.

If the GridLayout is not placed inside a section, the height is fine. If I use a standard RowLayout instead of my custom row layout, the total height is also fine. The excess vertical space increases with the number of rows.

=> I guess that there is something wrong with my custom row layout?.

=> Or is there an option that I have to set for the Section to not reserve space?

Should my custom layout "somehow communicate" a second type of height/hint for the parent layout? Since the height of the white region is fine and all rows are located at the top, the method computeSize seems to be fine.

I don't want the Section to reserve space. I want it to adapt its height lazily / as required.

Related question:

SWT RowLayout with last element grabbing excess horizontal space?

My custom layout:

package org.treez.core.adaptable.composite;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Layout;

/**
 * A custom row layout for exactly two children. If there is not enough space for the second child it is wrapped to a
 * second line. The second child grabs excess horizontal space (which would not be possible with a normal RowLayout).
 */
public final class ExtendingRowLayout extends Layout {

    //#region ATTRIBUTES

    private int minWidthForFirstChild;

    private int minWidthForSecondChild;

    private int spacing = 5;

    //#end region

    //#region CONSTRUCTORS

    public ExtendingRowLayout() {
        this(80, 200);
    }

    public ExtendingRowLayout(int minWidthForFirstChild, int minWidthForSecondChild) {
        this.minWidthForFirstChild = minWidthForFirstChild;
        this.minWidthForSecondChild = minWidthForSecondChild;
    }

    //#end region

    //#region METHODS

    @Override
    protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
        Point extent = layoutHorizontal(composite, false, wHint, flushCache);
        return extent;
    }

    @Override
    protected void layout(Composite composite, boolean flushCache) {
        Rectangle clientArea = composite.getClientArea();
        layoutHorizontal(composite, true, clientArea.width, flushCache);
    }

    @Override
    protected boolean flushCache(Control control) {
        return true;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName();
    }

    private Point layoutHorizontal(
            Composite composite,
            boolean doPositionChildren,
            int clientWidth,
            boolean flushCache) {

        Control[] children = composite.getChildren();

        if (children.length != 2) {
            String message = "There must be exactly two children and not " + children.length + ".";
            throw new IllegalStateException(message);
        }

        int clientX = 0;
        int clientY = 0;
        if (doPositionChildren) {
            Rectangle rect = composite.getClientArea();
            clientX = rect.x;
            clientY = rect.y;
        }

        Control firstChild = children[0];
        Control secondChild = children[1];

        Point firstSize = firstChild.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
        Point secondSize = secondChild.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);

        int firstChildWidth = Math.max(firstSize.x, minWidthForFirstChild);

        int correctedSpacing = spacing;
        if (firstChildWidth == 0) {
            correctedSpacing = 0;
        }

        int minForSecondChildWidth = Math.max(secondSize.x, minWidthForSecondChild);
        int minForTotalWidthOfSingleLine = firstChildWidth + correctedSpacing + minForSecondChildWidth;

        int maxHeight = Math.max(firstSize.y, secondSize.y);

        int firstY = maxHeight / 2 - firstSize.y / 2;

        if (doPositionChildren) {
            firstChild.setBounds(clientX, clientY + firstY, firstChildWidth, firstSize.y);
        }

        boolean showOnSingleLine = minForTotalWidthOfSingleLine + spacing <= clientWidth;

        if (showOnSingleLine) {
            int x = clientX + firstChildWidth + correctedSpacing;
            int y = clientY + maxHeight / 2 - secondSize.y / 2;
            int width = clientWidth - firstChildWidth - correctedSpacing;
            int height = secondSize.y;

            if (doPositionChildren) {
                secondChild.setBounds(x, y, width, height);
            }

            int totalWidth = Math.max(minForTotalWidthOfSingleLine, clientWidth);
            return new Point(totalWidth, maxHeight);
        } else {
            int x = clientX;
            int y = (int) (clientY + firstSize.y + 1.5 * spacing);
            int width = Math.max(clientWidth, minWidthForSecondChild);
            int height = secondSize.y;

            if (doPositionChildren) {
                secondChild.setBounds(x, y, width, height);
            }

            int totalHeight = (int) (firstSize.y + 1.5 * spacing + secondSize.y);

            return new Point(width, totalHeight);
        }
    }

    //#end region

}

Example usage:

    package org.treez.core.adaptable.composite;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.Section;

public class ExtendingRowLayoutDemo {

    public static void main(String[] args) {

        Shell shell = createShell();

        shell.setSize(500, 300);

        Section section = createSection(shell);

        Composite parentComposite = createParentComposite(section);

        createRow(parentComposite, "first");

        createRow(parentComposite, "second");

        createRow(parentComposite, "third");

        showUntilClosed(shell);

    }

    private static Shell createShell() {
        Display display = new Display();
        Shell shell = new Shell(display);

        GridLayout shellGridLayout = new GridLayout(1, false);
        shell.setLayout(shellGridLayout);
        return shell;
    }

    private static Section createSection(Shell shell) {
        Section section = new Section(
                shell,
                ExpandableComposite.TWISTIE | ExpandableComposite.EXPANDED | ExpandableComposite.TITLE_BAR);

        GridData gridData = new GridData(SWT.FILL, SWT.NONE, true, false);
        section.setLayoutData(gridData);
        return section;
    }

    private static Composite createParentComposite(Section section) {

        Composite parentComposite = new Composite(section, SWT.NONE);
        section.setClient(parentComposite);

        parentComposite.setBackground(new Color(null, 0, 0, 255));

        GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
        parentComposite.setLayoutData(gridData);

        GridLayout gridLayout = new GridLayout(1, false);
        parentComposite.setLayout(gridLayout);

        return parentComposite;
    }

    private static Composite createRow(Composite parent, String text) {

        Composite row = new Composite(parent, SWT.NONE);
        row.setBackground(new Color(null, 255, 255, 255));

        GridData rowGridData = new GridData(SWT.FILL, SWT.FILL, true, false);
        row.setLayoutData(rowGridData);

        Label label = new Label(row, SWT.NONE);
        label.setText(text);

        Button checkBox = new Button(row, SWT.CHECK);
        checkBox.setBackground(new Color(null, 255, 0, 0));

        ExtendingRowLayout rowLayout = new ExtendingRowLayout();
        row.setLayout(rowLayout);

        //RowLayout standardRowLayout = new RowLayout();
        //row.setLayout(standardRowLayout);

        return row;

    }

    private static void showUntilClosed(Shell shell) {

        shell.open();
        Display display = Display.getCurrent();
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        display.dispose();
    }

}

回答1:

For what you're trying to do, I think it would be safest (and easiest) to avoid extending Layout, and instead use an existing Layout implementation. The first thought that comes to mind when seeing the two images you've attached is that you're switching between having one and two columns in a layout when some arbitrary minimum size is reached.

With that in mind, you could instead use a ControlListener (actually ControlAdapter because we only care about resizing) and update the number of columns when the desired size threshold is reached. For example:

public class RowControlListener extends ControlAdapter {

    // This can be computed from other values instead of being constant
    private static final int MIN_WIDTH = 200;

    private final Composite row;
    private final Section section;

    public RowControlListener(final Composite row, final Section section) {
        this.row = row;
        this.section = section;
    }

    @Override
    public void controlResized(final ControlEvent e) {
        final int width = row.getClientArea().width;
        final GridLayout rowLayout = (GridLayout) row.getLayout();
        final int numColumns = rowLayout.numColumns;
        final int updatedNumColumns = width < MIN_WIDTH ? 1 : 2;
        // Only do this if there's a change
        if (numColumns != updatedNumColumns) {
            rowLayout.numColumns = updatedNumColumns;
            section.layout(true, true);
        }
    }
}

Now that we have a listener, the only other changes are to add the listener, and then some minor cosmetic updates:

public static void main(final String[] args) {
    // ... other setup ...
    final Composite row1 = createRow(parentComposite, "first");
    final Composite row2 = createRow(parentComposite, "second");
    final Composite row3 = createRow(parentComposite, "third");

    row1.addControlListener(new RowControlListener(row1, section));
    row2.addControlListener(new RowControlListener(row2, section));
    row3.addControlListener(new RowControlListener(row3, section));
    // ... other setup ...
}

Minor changes of setting GridData on the Label and Button. We then use the widthHint on the GridData for the Label so that everything lines up. In an actual setup, that should be computed and passed in (similar to what you're doing currently by passing in 80) so that it's guaranteed to be greater than the length of the text in all cases.

public static Composite createRow(final Composite parent, final String text) {
    // ... other setup ...
    final Label label = new Label(row, SWT.NONE);
    final GridData labelGridData = new GridData(SWT.FILL, SWT.FILL, false,
            false);
    labelGridData.widthHint = 80;
    label.setLayoutData(labelGridData);
    label.setText(text);

    final Button checkBox = new Button(row, SWT.CHECK);
    checkBox.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
    checkBox.setBackground(new Color(null, 255, 0, 0));

    final GridLayout rowLayout = new GridLayout(2, false);
    rowLayout.marginHeight = 0;
    rowLayout.marginWidth = 0;
    row.setLayout(rowLayout);

    return row;
}

It's worth mentioning that I've initially specified the GridLayout to have 2 columns because I knew that the Shell would be large enough to accommodate. In reality, if you don't know the Shell size in advance, what you can do is arbitrarily pick 1 or 2, but call section.layout() after the listeners are added, so that things are rearranged appropriately.

The result: