Why does an SWT Composite sometimes require a call to resize() to layout correctly?
Asked Answered
E

4

21

Sometimes we encounter an SWT composite that absolutely refuses to lay itself out correctly. Often we encounter this when we have called dispose on a composite, and then replaced it with another; although it does not seem to be strictly limited to this case.

When we run into this problem, about 50 % of the time, we can call pack() and layout() on the offending composite, and all will be well. About 50 % of the time, though, we have to do this:

Point p = c.getSize();
c.setSize(p.x+1, p.y+1);
c.setSize(p);

We've had this happen with just about every combination of layout managers and such.

I wish I had a nice, simple, reproducible case, but I don't. I'm hoping that someone will recognize this problem and say: "Well, duh, you're missing xyz...."

Enloe answered 25/2, 2009 at 15:19 Comment(0)
X
31

Looks to me like the layout's cache is outdated and needs to be refreshed.

Layouts in SWT support caches, and will usually cache preferred sizes of the Controls, or whatever they like to cache:

public abstract class Layout {
    protected abstract Point computeSize (Composite composite, int wHint, int hHint, boolean flushCache);
    protected boolean flushCache (Control control) {...}
    protected abstract void layout (Composite composite, boolean flushCache);
}

I'm relatively new to SWT programming (former Swing programmer), but encountered similar situations in which the layout wasn't properly updated. I was usually able to resolve them using the other layout methods that will also cause the layout to flush its cache:

layout(boolean changed)

layout(boolean changed, boolean allChildren)
Xenophobe answered 25/2, 2009 at 15:48 Comment(3)
I have had the same problem as the author of the question. layout(true, true) has helped me.Volley
Thanks! layout(boolean changed, boolean allChildren) solved my issue.Gaea
Thanks this answer put me on the right track, however to add to it there is also a requestLayout() API that can be used similar to the parent.changed(new Control[] { theChangedChild }); API before the layout() invocation. bugs.eclipse.org/bugs/show_bug.cgi?id=468854Helve
X
21

In the meantime I learned a little more about SWT's shortcomings when changing or resizing parts of the control hierarchy at runtime. ScrolledComposites and ExpandBars need also to be updated explicitly when the should adapt their minimal or preferred content sizes.

I wrote a little helper method that revalidates the layout of a control hierarchy for a control that has changed:

public static void revalidateLayout (Control control) {

    Control c = control;
    do {
        if (c instanceof ExpandBar) {
            ExpandBar expandBar = (ExpandBar) c;
            for (ExpandItem expandItem : expandBar.getItems()) {
                expandItem
                    .setHeight(expandItem.getControl().computeSize(expandBar.getSize().x, SWT.DEFAULT, true).y);
            }
        }
        c = c.getParent();

    } while (c != null && c.getParent() != null && !(c instanceof ScrolledComposite));

    if (c instanceof ScrolledComposite) {
        ScrolledComposite scrolledComposite = (ScrolledComposite) c;
        if (scrolledComposite.getExpandHorizontal() || scrolledComposite.getExpandVertical()) {
            scrolledComposite
                .setMinSize(scrolledComposite.getContent().computeSize(SWT.DEFAULT, SWT.DEFAULT, true));
        } else {
            scrolledComposite.getContent().pack(true);
        }
    }
    if (c instanceof Composite) {
        Composite composite = (Composite) c;
        composite.layout(true, true);
    }
}
Xenophobe answered 26/3, 2009 at 16:45 Comment(1)
Yeah - I've had to do that before - it's a pain. It wouldn't hurt to phrase this as a separate question and answer, for searchability's sake.Enloe
S
16

A composite's layout is responsible for laying out the children of that composite. So if the composite's size does not change, but the relative positions and sizes of the children need to be updated, you call layout() on the composite. If, however, the size or position of the composite itself needs to be updated, you will have to call layout() on its parent composite (and so on, until you reach the shell).

A rule of thumb: If you have added or removed a control, or otherwise done something that requires a relayout, walk up the widget hierarchy until you find a composite with scrollbars and call layout() on it. The reason for stopping at the composite with scrollbars is that its size will not change in response to the change - its scrollbars will "absorb" that.

Note that if the change requiring a layout is not a new child, or a removed child, you should call Composite.changed(new Control[] {changedControl}) before calling layout.

Singlecross answered 25/2, 2009 at 15:59 Comment(0)
M
4

I have just become aware of Composite.changed(Control[] children). There is an extensive article which I read a couple years ago:

http://www.eclipse.org/articles/article.php?file=Article-Understanding-Layouts/index.html

This article also mentions to call Composite.layout(boolean changed, boolean all) to update the layout: "Calling layout() is the same as calling layout(true) which tells the ColumnLayout to flush its caches before setting the bounds of the children." That is all correct and what I have been doing ever since. But it is not what one wants, since it basically defeats the benefit of the layout cache when you want to update the layout because one or a few controls have changed requirements.

Imagine you have a bunch of StyledText widgets in a GridLayout and you need to change the size of one of them. Calling computeSize() on StyledText is very expensive. Instead of this:

Wrong:

parent.layout(true);

... which calls computeSize() on all children, even though their requirements have not changed. You should do this:

Right:

parent.changed(new Control[] { theChangedChild });

Then either

rootComposite.layout(false, true);

or

parent.layout(false);

Not very intuitive. The parameter to layout() is just badly named. Instead of calling it "changed" it should have been called "ignoreCache" or something. The intuitive thing is to pass "true" when something changed. Instead you need to pass "false", but invalidate the cache for just the changed Control with changed() before you do...

Note that calling changed() will also recursively invalidate the cache for just the parent Control in its own parent, which totally makes sense. So when you call layout(), you should call it on the root composite (most often the Shell) with all=true, unless you know that the size of the parent of the changed control will or can not change in response.

Method answered 12/10, 2015 at 9:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.