Centering the contents of a component in a JScrollPane

When creating a drawing program it’s nice to be able to center the contents of your view if the content area of the JScrollPane is larger than the contents of your document. By default your contents are slammed to the upper-left; however, by using the Scrollable interface you can prevent the viewport containing your content view from shrinking smaller than the content area of the scroll pane, thus allowing you to do things like center your content.

Here is some code which I used to do this. I’m posting the code here so I can remember this in the future.

First, the class itself:

public class ContentView extends JComponent implements Scrollable
{
    private int width = 500;    // document width, height. (replace with whatever code you use to find these values.
    private int height = 400;
    private Rectangle offset;   // caching code; see discussion below.

I’m declaring a width and height inside my class for demo purposes., but you can obtain the visible width and height anyway you wish.

Now the centering code:

    /********************************************************************************/
    /*                                                                              */
    /*  Viewport Centering                                                          */
    /*                                                                              */
    /********************************************************************************/
    
    /**
     * Internal routine which computes the smallest rectangle size
     * encompassing the document area and the scroll viewport.
     */
    private Dimension getViewportSize()
    {
        Dimension size = getParent().getSize(); // get my viewport size

        int xsize = size.width;
        if (xsize < width) xsize = width;
        int ysize = size.height;
        if (ysize < height) ysize = height;
        
        return new Dimension(xsize,ysize);
    }
    
    /**
     * getMinimumSize returns the smallest acceptable size of this view. This
     * is then used resize
     */
    public Dimension getMinimumSize()
    {
        return new Dimension(300,200);
    }
    
    /**
     * The getPreferredSize routine returns my preferred size; this returns the
     * viewport size
     */
    public Dimension getPreferredSize()
    {
        return getViewportSize();
    }

    /**
     * The Scrollable.getPreferredScrollableViewportSize routine returns the suggested
     * size of the viewport. This will return the viewport size I want, which is
     * the larger of the view area size and the existing viewport size
     */
    public Dimension getPreferredScrollableViewportSize()
    {
        return getViewportSize();
    }


    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction)
    {
        int x;
        if (orientation == SwingConstants.VERTICAL) {
            x = visibleRect.width - 12;
        } else {
            x = visibleRect.height - 12;
        }
        if (x < 12) x = 12;
        return x;
    }

    public boolean getScrollableTracksViewportHeight()
    {
        return false;
    }

    public boolean getScrollableTracksViewportWidth()
    {
        return false;
    }

    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction)
    {
        return 12;
    }

The core of the code above is the getViewportSize() routine, which determines a size which contains the size of my content and the viewport size. This is then returned for the getPreferredSize and getPreferredScrollableViewportSize() routines–both are used by the JScrollPane to resize my view within the scroll view area.

The routine getMinimumSize() is used to return the minimum acceptable size. To make a long story short, in my custom LayoutManager, I call the JScrollPane’s getMinimumSize() routine to get the minimum area allowed for resizing my window; this makes sure the contents area of my scroll bar doesn’t shrink below the minimum size returned.

The other interface methods for the Scrollable are filled in with acceptable defaults for a drafting program: page scrolling scrolls almost the entire width or height of the visible area, and clicking the arrows steps 12 pixels.

Now to hang all this together you need to then draw the contents in the right place; this means you need a rectangle representing the content area of your document.

    /********************************************************************************/
    /*                                                                              */
    /*  Drawing Support                                                             */
    /*                                                                              */
    /********************************************************************************/

    /**
     * invalidate is called when the size or boundaries of this thing has been
     * invalidated. This resets my offset rectangle because I will need to recalculate
     * it.
     */
    public void invalidate()
    {
        super.invalidate();
        offset = null;
    }
    
    /**
     * getOffset calculates (if needed) and returns a rectangle which defines the
     * location of my drawing in my view. If the visible area of my view is larger
     * than my drawing, this will return a rectangle offset appropriately
     * @return
     */
    public Rectangle getOffset()
    {
        if (offset == null) {
            Dimension parentSize = getParent().getSize();
            int xoff = (parentSize.width - width) / 2;
            if (xoff < 0) xoff = 0;
            int yoff = (parentSize.height - height) / 2;
            if (yoff < 0) yoff = 0;
            
            offset = new Rectangle(xoff,yoff,width,height);
        }
        return offset;
    }
    
    protected void paintComponent(Graphics g)
    {
        Rectangle r = getOffset();
        Dimension size = getSize();
        
        // erase to background
        g.setColor(Color.lightGray);
        g.fillRect(0, 0, size.width, size.height);
        
        // erase my document area
        g.setColor(Color.WHITE);
        g.fillRect(r.x, r.y, r.width, r.height);
    }

The offset field above is used to cache the current offset and location of my contents in my drawing area; I cache this value so I’m not constantly recomputing this on a mouse down/move/up cycle. The calculation itself is handled in getOffset(). The invalidate() method is called by the OS whenever the size of my view is changed; this allows me the chance to dump the previously cached value to force a redraw.

And the paintComponent() routine will repaint the contents; here I draw the area as a white area within the content area.

As a footnote, if you write code to recalculate the location of the scroll bar and use that code to calculate the minimum acceptable size of your view, your LayoutManager should use getMinimumSize rather than getPreferredSize when determining the size for scrolling. (I’m lazy; my LayoutManagers use the same value for preferredLayoutSize and minimumLayoutSize.) Otherwise, you will find the window constantly resizing itself larger and larger as the custom getPreferredSize() above interacts poorly with the resize code.

Look; this is a hack. I make no promises this works correctly; standard disclaimers apply.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s