Swing Tip: JSplitPane with zero-size divider
Modern GUIs are becoming more and more minimalistic. Most controls (e.g. text fields or buttons) nowadays use 1 pixel thin borders. Everybody is removing borders from scroll and split panes. Even the split pane divider is often reduced to 1 pixel (e.g. on Mac OS X since years or in current Mozilla Thunderbird).
But how to create a 1 pixel thin divider with JSplitPane?
First idea was of course to invoke splitPane.setDividerSize(1)
.
This seems to work, but has the disadvantage that it is very hard for the user
to hit that single pixel line to move the divider. What we need is a
transparent divider that has an easy-to-hit
width (e.g. 9 pixels) and is placed between and over
the left and right split pane components.
Thanks to Swing's flexible design, it is relative easy to implement this.
First we set the divider size to 1 and let the split pane layout manager do
its work. The trick is now to override JSplitPane.layout()
and
modify the bounds of the divider (e.g. increase width and move left):
public class JSplitPaneWithZeroSizeDivider extends JSplitPane { private int dividerDragSize = 9; private int dividerDragOffset = 4; public JSplitPaneWithZeroSizeDivider() { setDividerSize( 1 ); setContinuousLayout( true ); } @Override public void layout() { super.layout(); // increase divider width or height BasicSplitPaneDivider divider = ((BasicSplitPaneUI)getUI()).getDivider(); Rectangle bounds = divider.getBounds(); if( orientation == HORIZONTAL_SPLIT ) { bounds.x -= dividerDragOffset; bounds.width = dividerDragSize; } else { bounds.y -= dividerDragOffset; bounds.height = dividerDragSize; } divider.setBounds( bounds ); }
Then we need our own UI delegate that creates our divider.
@Override public void updateUI() { setUI( new SplitPaneWithZeroSizeDividerUI() ); revalidate(); } private class SplitPaneWithZeroSizeDividerUI extends BasicSplitPaneUI { @Override public BasicSplitPaneDivider createDefaultDivider() { return new ZeroSizeDivider( this ); } }
And finally our divider, which draws the divider line and updates the drag locations.
private class ZeroSizeDivider extends BasicSplitPaneDivider { public ZeroSizeDivider( BasicSplitPaneUI ui ) { super( ui ); super.setBorder( null ); setBackground( UIManager.getColor( "controlShadow" ) ); } @Override public void setBorder( Border border ) { // ignore } @Override public void paint( Graphics g ) { g.setColor( getBackground() ); if( orientation == HORIZONTAL_SPLIT ) g.drawLine( dividerDragOffset, 0, dividerDragOffset, getHeight() - 1 ); else g.drawLine( 0, dividerDragOffset, getWidth() - 1, dividerDragOffset ); } @Override protected void dragDividerTo( int location ) { super.dragDividerTo( location + dividerDragOffset ); } @Override protected void finishDraggingTo( int location ) { super.finishDraggingTo( location + dividerDragOffset ); } } }
That's it.
Tested with Oracle/Sun Java 5, 6, and 7. Licensed under BSD-2-Clause with clause 2 removed.
I've been testing this splitter in my application that used to have the tiny hit-target this was ment to replace.
There's some fishy stuff going on when the preferred widths of the two components in the splitter are greater than the width of the splitpane. I haven't been able to come up with a good solution aside from changing the preferred sizes.
There's also a problem with the initial display of this control where the divider jumps dividerDragOffset pixels. I've solved it by this override to SplitPaneWithZeroSizeDividerUI:
Unfortunately this doesn't fix the previous problem.
I've also changed the constants:
-Charlie
I also changed the override of layout to an override of doLayout as layout is deprecated.
-Charlie
@Charlie: thanks for your comments. Was not yet able to reproduce your problems.
But your suggestion to override getDividerLocation() has a major side effect when having a horizontal split pane that contains a vertical split pane as right component: while dragging the horizontal split divider the vertical split divider magically moves down.
You can see this effect in the demo program (that is included in the ZIP) when changing:
to
Regarding overriding deprecated method layout(): You're right, it is deprecated, but layout() does the real work and I think that it is safer to override layout() because I don't know whether it is invoked directly somewhere. However I should add a @Deprecated annotation to the overridden method to keep the deprecation.