JavaSwingComponents: JComponent scrolling


JComponent scrolling

Any JComponent has built-in support for being only partly visible (even if it does not implement Scrollable). The usual container for such purposes is JViewport, which displays only a certain part of the component (and also adds some caching). JScrollPane only adds support for headers and corner components and, important for user actions, the scroll bars to change the currently displayed part.

JComponent.getVisibleRect() returns the visible area. If it is not contained (directly or indirectly) in a JViewport or similar, it will return the whole component area.

JComponent.scrollRectToVisible() causes any scrolling containers up the container hierarchy to make the given part of the component visible. It is forwarded up until a component (like JViewport) is responsible for performing that action.

This document handles more sophisticated scrolling/visibility situations than those that are directly possible with these two methods. The methods presented here will end up calling them, but do conversions before to get more specific behaviour.

As of now, the code only works for components directly contained in a viewport-like container, not for any subcomponents. For such components,getVisibleRect() cannot be (mis-)used to obtain the size of the viewport anymore, so things get much more complicated. One has to walk up the hierarchy and search for the real viewport to get this size, which the following calculations are based on. Even if the component is the viewport's view, getVisibleRect() may return a smaller size than the viewport size - if the component is smaller than the viewport. But then scrolling is never done anyway.

Also, JTextField overrides scrollRectToVisible in a weird way, so it doesn't work as usual anymore.

Accessing component (or part) locations or sizes (or getVisibleRect()) when the component (or any of its ancestors) is invalid (isValid()) should be avoided. scrollRectToVisible() will validate the component before scrolling, so calling this method isn't the problem; but the fact that the Rectangle argument may not be sensible anymore because the layout or size may have changed.

The components that are typically contained in a JScrollPane (JTable, JList, JTable, text components) get invalid (nearly?) always when the model changes because that usually have to change their size. JList/BasicListUI seem to calculate the proper cell bounds even when changes have occured, while JTable generally has to recalculate the column widths (sizeColumnsToFit) and doesn't do that immediately. (#4172618)

Use SwingUtilities.invokeLater() to invoke the bounds-accessing code (which will then also do the scrolling) whenever you think such a change may occur. Maybe even test isValid() before doing any calculations, unless you know more about the component.

The methods sketched here are contained in runnable form in Scrolling.java.

Scrolling to areas

Common operations are scrolling to top/bottom/left/right, not to explicit areas as in the next parts.

public static final int
    NONE = 0,
    TOP = 1,
    VCENTER = 2,
    BOTTOM = 4,
    LEFT = 8,
    HCENTER = 16,
    RIGHT = 32;

public static void scroll(JComponent c, int part)
{
    scroll(c, part & (LEFT|HCENTER|RIGHT), part & (TOP|VCENTER|BOTTOM));
}
    
public static void scroll(JComponent c, int horizontal, int vertical)
{
    Rectangle visible = c.getVisibleRect();
    Rectangle bounds = c.getBounds();
      
    switch (vertical)
    {
        case TOP:     visible.y = 0; break;
        case VCENTER: visible.y = (bounds.height - visible.height) / 2; break;
        case BOTTOM:  visible.y = bounds.height - visible.height; break;
    }

    switch (horizontal)
    {
        case LEFT:    visible.x = 0; break;
        case HCENTER: visible.x = (bounds.width - visible.width) / 2; break;
        case RIGHT:   visible.x = bounds.width - visible.width; break;
    }

    c.scrollRectToVisible(visible);
}

Scrolling with overflow policy

It is unclear how a region is scrolled when it is too large to be displayed all at once. JViewport doesn't scroll at all if only the region is visible, and scrolls the unwanted part away otherwise, but this policy could be different in another viewport container (for example always scrolling the upper-left corner into view). Sometimes it may be desired to be sure that no scrolling happens in the former case, or suggest which part should really be made visible.

Some methods here take a bias argument, which is used in such a situation and one of the following constants (which values they actually have of course does not matter):

public static final int
     VIEWPORT = 0,       // take the policy of the viewport
     UNCHANGED = 1,      // don't scroll if it fills the visible area, otherwise take the policy of the viewport
     FIRST = 2,          // scroll the first part of the region into view 
     CENTER = 3,         // center the region
     LAST = 4;           // scroll the last part of the region into view

Here is scrollRectToVisible() with biases:

public static void scroll(JComponent c, Rectangle r, int horizontalBias, int verticalBias)
{
    Rectangle visible = c.getVisibleRect(),
        dest = new Rectangle(r);

    if (dest.width > visible.width)
    {
        if (horizontalBias == VIEWPORT)
        {
            // leave as is
        }
        else if (horizontalBias == UNCHANGED)
        {
            if (dest.x <= visible.x && dest.x + dest.width >= visible.x + visible.width)
            {
                dest.width = visible.width;
            }
        }
        else
        {
            if (horizontalBias == CENTER)
                dest.x += (dest.width - visible.width) / 2;
            else if (horizontalBias == LAST)
                dest.x += dest.width - visible.width;

            dest.width = visible.width;
        }
    }

    if (dest.height > visible.height)
    {
        // same code as above in the other direction
    }

    if (!visible.contains(dest))
        c.scrollRectToVisible(dest);
}

Scrolling in one direction

scrollRectToVisible() always takes a Rectangle argument. Sometimes it may be desired to only scroll vertically or horizontally, not in both directions, for example to make a certain row in a JTable visible. The code for horizontal and vertical scrolling is completely analogous, here only one variant is shown.

The following method is given a range (from: inclusive, to: exclusive) to make visible.

public static void scrollHorizontally(JComponent c, int from, int to)
{
    Rectangle visible = c.getVisibleRect();

    if (visible.x <= from && visible.x + visible.width >= to)
        return;

    visible.x = from;
    visible.width = to - from;

    c.scrollRectToVisible(visible);
}

As many accessor methods (like JList.getCellBounds()) still return Rectangles, a Rectangle argument can be given, from which only the desired direction will be honored.

public static void scrollHorizontally(JComponent c, Rectangle r)
{
    scrollHorizontally(c, r.x, r.x + r.width);
}

Additionally, you may want to specify a bias for possibly large areas.

public static void scrollHorizontally(JComponent c, int from, int to, int bias)
{
    Rectangle visible = c.getVisibleRect(),
        dest = new Rectangle(visible);

    dest.x = from;
    dest.width = to - from;

    // same adjusting code as above

    if (!visible.contains(dest))
        c.scrollRectToVisible(dest);
}

Centering

Just scrolling sections into view may not be the best thing to do because the area in question may just barely appear at the top/bottom and thus not be considered important by the user. A better choice can be to scroll it into the center of the currently visible section (if this is possible, it can of course not be done for the first/last areas/rows/columns).

Not scrolling into the insets area is here for consistency. If the insets were always ignored, centering would scroll the insets of "view" components (JTable, JList, JTree) into view, while normal scrolling would not because the area accessors of these components never include the insets.

public static void center(JComponent c, Rectangle r, boolean withInsets)
{
    Rectangle visible = c.getVisibleRect();

    visible.x = r.x - (visible.width - r.width) / 2;
    visible.y = r.y - (visible.height - r.height) / 2;

    Rectangle bounds = c.getBounds();
    Insets i = withInsets ? new Insets(0, 0, 0, 0) : c.getInsets();
    bounds.x = i.left;
    bounds.y = i.top;
    bounds.width -= i.left + i.right;
    bounds.height -= i.top + i.bottom;

    if (visible.x < bounds.x)
        visible.x = bounds.x;

    if (visible.x + visible.width > bounds.x + bounds.width)
        visible.x = bounds.x + bounds.width - visible.width;

    if (visible.y < bounds.y)
        visible.y = bounds.y;

    if (visible.y + visible.height > bounds.y + bounds.height)
        visible.y = bounds.y + bounds.height - visible.height;

    c.scrollRectToVisible(visible);
}

Testing for visibility

public static boolean isVisible(JComponent c, Rectangle r)
{
    return c.getVisibleRect().contains(r);
}

public static boolean isHorizontallyVisible(JComponent c, int from, int to)
{
    Rectangle visible = c.getVisibleRect();

    return visible.x <= from
        && visible.x + visible.width >= to;        
}

(C) 2001-2009 Christian Kaufhold (swing@chka.de)