Chapter 8. Split Panes

In this chapter:

8.1 JSplitPane

class javax.swing.JSplitPane

Split panes allow the user to dynamically change the size of two or more components displayed side-by-side (within a window or another panel). Special dividers can be dragged with the mouse to increase space for one component and decrease the display space for another. Note that the total display area does not change. This gives applications a more modern and sophisticated view. A familiar example is the combination of a tree and a table separated by a horizontal divider (e.g. file explorer-like applications). The Swing framework for split panes consists only of JSplitPane.

JSplitPane can hold two components separated by a horizontal or vertical divider. The components on either side of a JSplitPane can be added either in one of the constructors, or with the proper setXXComponent() methods (where XX is substituted by Left, Right, Top, or Bottom). We can also set the orientation at run-time using its setOrientation() method.

The divider between the side components represents the only visible part of JSplitPane. It's size can be managed with the setDividerSize() method, and its position by the two overloaded setDividerLocation() methods (which take an absolute location in pixels or proportional location as a double). The divider location methods have no affect until JSplitPane is displayed. JSplitPane also maintains a oneTouchExpandable property which, when true, places two small aro! ws inside the divider that will move the divider to its extremities when clicked.

 

UI Guideline : Resizable Panelled Display

Split pane becomes really useful when your design has panelled the display for ease of use but you (as designer) have no control over the actual window size. The Netscape e-mail reader is a good example of this. A split pane is introduced to let the user vary the size of the message header panel against the size of the message text panel.

An interesting feature of the JSplitPane component is that you can specify whether or not to repaint side components during the divider's motion using the setContinuousLayout() method. If you can repaint components fast enough, resizing will have a more natural view with this setting. Otherwise, this flag should be set to false, in which case side components will be repainted only when the divider's new location is chosen. In this latter case, a divider line will be shown as the divider location is dragged to illustrate the new position.

JSplitPane will not size any of its constituent components smaller than their minimum sizes. If the minimum size of each component is larger than the size of the splitpane the divider will be effectively disabled (unmovable). Also note that we can call its resetToPreferredSize() method to resize its children to their preferred sizes, if possible.

 

UI Guideline : Use Split Pane in conjunction with Scroll Pane

Its important to use a Scroll Pane on the panels which are being split with the Split Pane. The scroll bar will then invoke automatically as required when data is obscured as the split pane is dragged back and forth. The introduction of the scroll pane gives the viewer a clear indication that there is some hidden data. They can then choose to scroll with the scroll bar or uncover the data using the split pane.

 

8.2 Basic split pane example

This basic introductory demo shows JSplitPane at work. We can manipulate four simple custom panels placed in three JSplitPanes:

Figure 8.1 Split Pane example displaying simple custom panels.

<<file figure8-1.gif>>

The Code: SplitSample.java

see \Chapter8\1

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class SplitSample extends JFrame
{
	protected JSplitPane m_sp;

	public SplitSample()
	{
		super("Simple SplitSample Example");
		setSize(400, 400);
		getContentPane().setLayout(new BorderLayout());
		
		Component c11 = new SimplePanel();
		Component c12 = new SimplePanel();
		JSplitPane spLeft = new JSplitPane(
			JSplitPane.VERTICAL_SPLIT, c11, c12);
		spLeft.setDividerSize(8);
		spLeft.setContinuousLayout(true);
		
		Component c21 = new SimplePanel();
		Component c22 = new SimplePanel();
		JSplitPane spRight = new JSplitPane(
			JSplitPane.VERTICAL_SPLIT, c21, c22);
		spRight.setDividerSize(8);
		spRight.setContinuousLayout(true);

		m_sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, 
			spLeft, spRight);
		m_sp.setContinuousLayout(false);
		m_sp.setOneTouchExpandable(true);

		getContentPane().add(m_sp, BorderLayout.CENTER);

		WindowListener wndCloser = new WindowAdapter()
		{
			public void windowClosing(WindowEvent e) 
			{
				System.exit(0);
			}
		};
		addWindowListener(wndCloser);
		
		setVisible(true);
	}

	public static void main(String argv[]) 
	{
		new SplitSample();
	}
}

class SimplePanel extends JPanel
{

	public Dimension getPreferredSize()
	{
		return new Dimension(200, 200);
	}

	public Dimension getMinimumSize()
	{
		return new Dimension(40, 40);
	}

	public void paintComponent(Graphics g)
	{
		g.setColor(Color.black);
		Dimension sz = getSize();
		g.drawLine(0, 0, sz.width, sz.height);
		g.drawLine(sz.width, 0, 0, sz.height);
	}
}

Understanding the Code

Class SplitSample

Four instances of SimplePanel (see below) are used to fill a 2x2 structure. Two left components (c11 and c12) are placed in the vertically split spLeft panel. The two right components (c21 and c22) are placed in the vertically split spRight panel. The spLeft and spRight panels are! placed in the horizontally split m_sp panel.

Several properties are assigned to demonstrate JSplitPane’s behavior. The continuousLayout property is set to true for spLeft and spRight, and false for m_sp panel. So as the divider moves inside the left and right panels, child components are repainted continuously, producing immediate results. However, as the vertical divider is moved it is denoted by a black line until a new position is chosen (i.e. the mouse is released). Only then are its child components validated and repainted. The first kind of behavior is recommended for si! mple components that can be rendered quickly, while the second is recomended for components whose repainting can take a significant amount of time.

Also note that the oneTouchExpandable property is set to true for the vertical JSplitPane. This places small arrow widgets on the divider. By pressing these arrows with the mouse we can instantly move the divider to the leftmost or rightmost position. When in the left or right-most positions, pressing these arrows will then move the divider to its most recent location, maintained by the lastDividerLocation property.

Class SimplePanel

SimplePanel represents a simple Swing component to be used in this example. Method paintComponent() draws two diagonal lines across this component. Note that the overridden getMinimumSize() method defines the minimum space required for this component. JSplitPane will prohibit the user from moving the divider if the resulting child size will become less than its minimum size.

 

Note: The arrow widgets associated with the oneTouchExpandable property will move the divider to the extreme location without regard to minimum sizes of child components.

Running the Code

Note how child components can be resized with dividers. Also note the difference between resizing with continuous layout (side panes) and without it (center pane). Play with the "one touch expandable" widgets for quick expansion and collapse.

8.3 Gas model simulation using a split pane

In this section we'll use JSplitPane for an interesting scientific experiment: a simulation of the gas model. Left and right components represent containers holding a two-dimensional "ideal gas." The JSplitPane component provides a moveable divider between them. By moving the divider we observe how gas reacts when its volume is changed. Online educational software is ever-increasing as the internet flourishes, and here we show how Swing can be used to demonstrate one of the most basic laws of thermodynamics!

 

Note: As you may remember from a physics or chemistry course, an ideal gas is a physical model in which gas atoms or molecules move in random directions bouncing elastically from a container's bounds. Mutual collisions are negligible. The speed of the atoms depends on gas temperature. Several laws can be demonstrated with this model. One states that under the condition of constant temperature, multiplication of pressure P and volume V is constant: PV = const.

To model the motion of atoms we'll use threads. So this example also gives a good example of using several threads in Swing.

Figure 8.2 Gas model simulation showing moving atoms.

<<file figure8-2.gif>>

The Code: Split.java

see \Chapter8\2

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

public class Split extends JFrame implements Runnable
{
	protected GasPanel m_left;
	protected GasPanel m_right;

	public Split()
	{
		super("Gas Pressure [Split Pane]");
		setSize(600, 300);
		getContentPane().setLayout(new BorderLayout());
		
		ImageIcon ball1 = new ImageIcon("ball1.gif");
		m_left = new GasPanel(30, ball1.getImage());
		ImageIcon ball2 = new ImageIcon("ball2.gif");
		m_right = new GasPanel(30, ball2.getImage());
		JSplitPane sp = new JSplitPane(
			JSplitPane.HORIZONTAL_SPLIT, m_left, m_right);
		sp.setDividerSize(10);
		sp.setContinuousLayout(true);
		getContentPane().add(sp, BorderLayout.CENTER);

		WindowListener wndCloser = new WindowAdapter()
		{
			public void windowClosing(WindowEvent e) 
			{
				System.exit(0);
			}
		};
		addWindowListener(wndCloser);
		
		setVisible(true);

		new Thread(m_left).start();
		new Thread(m_right).start();
		new Thread(this).start();
	}

	public void run()
	{
		while (true)
		{
			int p1  = (int)m_left.m_px2;
			int pv1 = p1*m_left.getWidth();
			int p2 = (int)m_right.m_px1;
			int pv2 = p2*m_right.getWidth();
			System.out.println("Left: p="+p1+"\tpv="+pv1+
				"\tRight: p="+p2+"\tpv="+pv2);
			m_left.clearCounters();
			m_right.clearCounters();
			
			try 
			{ 
				Thread.sleep(20000); 
			} 
			catch(InterruptedException e) {}
		}
	}

	public static void main(String argv[]) 
	{
		new Split();
	}
}

class GasPanel extends  JPanel implements Runnable
{
	protected Atom[] m_atoms;
	protected Image  m_img;
	protected Rectangle m_rc;

	public double m_px1 = 0;
	public double m_px2 = 0;
	public double m_py1 = 0;
	public double m_py2 = 0;

	public GasPanel(int nAtoms, Image img)
	{
		setBackground(Color.white);
		enableEvents(ComponentEvent.COMPONENT_RESIZED);

		m_img = img;
		m_atoms = new Atom[nAtoms];
		m_rc = new Rectangle(getPreferredSize());
		for (int k = 0; k < nAtoms; k++)
		{
		   m_atoms[k] = new Atom(this);
		}
	}

	public Dimension getPreferredSize()
	{
		return new Dimension(300, 300);
	}

	public void run()
	{
		while (true)
		{
			for (int k = 0; k < m_atoms.length; k++)
				m_atoms[k].move(m_rc);
			repaint();
			
			try 
			{ 
			   Thread.sleep(100); 
			} 
			catch(InterruptedException e) {}
		}
	}

	public void paintComponent(Graphics g)
	{
		g.setColor(getBackground());
		g.fillRect(m_rc.x, m_rc.y, m_rc.width, m_rc.height);

		for (int k=0; k < m_atoms.length; k++)
			g.drawImage(m_img, m_atoms[k].getX(), 
				m_atoms[k].getY(), this);
	}

	protected void processComponentEvent(ComponentEvent e)
	{
		if (e.getID() == ComponentEvent.COMPONENT_RESIZED)
		{
			m_rc.setSize(getSize());
			for (int k=0; k < m_atoms.length; k++)
				m_atoms[k].ensureInRect(m_rc);
		}
	}

	public void clearCounters()
	{
		m_px1 = 0;
		m_px2 = 0;
		m_py1 = 0;
		m_py2 = 0;
	}
}

class Atom
{
	protected double m_x;
	protected double m_y;
	protected double m_vx;
	protected double m_vy;

	protected GasPanel m_parent;

	public Atom(GasPanel parent)
	{
		m_parent = parent;
		m_x = parent.m_rc.x + parent.m_rc.width*Math.random();
		m_y = parent.m_rc.y + parent.m_rc.height*Math.random();
		double angle = 2*Math.PI*Math.random();
		m_vx = 10*Math.cos(angle);
		m_vy = 10*Math.sin(angle);
	}

	public void move(Rectangle rc)
	{
		double x = m_x + m_vx;
		double y = m_y + m_vy;
		int x1 = rc.x;
		int x2 = rc.x + rc.width;
		int y1 = rc.y;
		int y2 = rc.y + rc.height;
		for (int bounce = 0; bounce < 2; bounce++)
		{
			if (x < x1)
			{
				x += 2*(x1-x);
				m_vx = - m_vx;
				m_parent.m_px1 += 2*Math.abs(m_vx);
			}
			if (x > x2)
			{
				x -= 2*(x-x2);
				m_vx = - m_vx;
				m_parent.m_px2 += 2*Math.abs(m_vx);
			}
			if (y < y1)
			{
				y += 2*(y1-y);
				m_vy = - m_vy;
				m_parent.m_py1 += 2*Math.abs(m_vy);
			}
			if (y > y2)
			{
				y -= 2*(y-y2);
				m_vy = - m_vy;
				m_parent.m_py2 += 2*Math.abs(m_vy);
			}
		}
		m_x = x;
		m_y = y;
	}

	public void ensureInRect(Rectangle rc)
	{
		if (m_x < rc.x)
			m_x = rc.x;
		if (m_x > rc.x + rc.width)
			m_x = rc.x + rc.width;
		if (m_y < rc.y)
			m_y = rc.y;
		if (m_y > rc.y + rc.height)
			m_y = rc.y + rc.height;
	}

	public int getX() { return (int)m_x; }

	public int getY() { return (int)m_y; }
}

Understanding the Code

Class Split

The constructor of the Split frame creates two instances of the GasPanel class (which models a gas container, see below) and places them in a JSplitPane. All other code requires little discussion, but we must comment on one thing. Both the Split and GasPanel classes implement the Runnable interface, so threads are created and started to run all three instances.

 

Reminder: The Runnable interface should be implemented by classes which do not intend to use any Thread functionality other than the run() method. In such a case we don't have to sub-class the Thread class. Instead we can simply implement Runnable and define the run() method. In this method we can use the static Thread.sleep() method to yield control to other threads for a specified amount of time.

The run() method of our Split class periodically interrogates the pressure on the divider from the left and right containers, as well as each container’s width, which is proportional to the container's volume. Then it prints out the results of our measurements, clears the counters of the containers (see below) and sleeps for another 20 seconds.

 

Note: Because of the random nature of the gas model all observations are statistical. Each time you run this simulation you're likely to observe a slightly different results. The more atoms that constitute the gas, the more accurate the results achieved will be. A real gas has about 1022 atoms/cm3, and fluctuations in its parameters are very small. In our model only a few dozen "atoms" are participating, so fluctuations are considerable, and we have to wait a bit before we can obtain meaningful measurements (through averaging several results).

Class GasPanel

Class GasPanel models a two-dimensional gas container. It implements the Runnable interface to model the random motion of its contained atoms. Seven instance variables have the following meaning:

Atom[] m_atoms: array of Atom instances hosted by this container.

Image m_img: image used to draw atoms.

Rectangle m_rc: container's rectangular bounds.

double m_px1: counter to measure pressure on the left wall.

double m_px2: counter to measure pressure on the right wall.

double m_py1: counter to measure pressure on the top wall.

double m_py2: counter to measure pressure on the bottom wall.

The GasPanel constructor takes a number of atoms to be created as a parameter as well as a reference to the image to represent them. Method enableEvents() is called to enable the processing of resize events on this component. Finally, an array of atoms is created (see the Atom class below).

The getPreferredSize() method returns the preferred size of this component (used by our split pane to determine the initial position of its divider).

The run() method activates the gas container. For each child atom it invokes the Atom move() method (see below) which changes an Atom’s coordinates. The component is then repainted and the calling thread sleeps for 100 ms to provide smooth continuous motion.

The paintComponent() method is overridden to draw each child atom. It clears the component's area and, for each atom, draws the specified image (typically a small ball) at the current atom location.

The processComponentEvent() method is overridden to process resizing events. It updates the m_rc rectangle (used to limit atom motion) and calls the ensureInRect() method for all child atoms to force them to stay inside this component’s bounds. A check for the COMPONENT_RESIZED ID is done to skip the processing of COMPONENT_MOVED events which are also delivered to this component even though we've explicitly asked for only COMPONENT_RESIZED events (see enableEven! ts() call in constructor).

The clearCounters() method clears the counters used to measure the pressure on each of the walls.

Class Atom

An Atom represents a single object moving within a specific rectangular region and bouncing elastically from its walls. Instance variables:

double m_x: current x-coordinate.

double m_y: current y-coordinate.

double m_vx: current x-component of velocity.

double m_vy: current y-component of velocity.

GasPanel m_parent: reference to parent container.

The Atom constructor takes a reference to a parent GasPanel as parameter. It initializes its coordinates randomly within the parent's boudning rectangle using Math.random() as a random number generator. An atom's velocity vector is assigned a fixed absolute magnitude (10) and a random orientation in the x-y plane.

 

Note: In a more realistic model, velocity would be a normally distributed random value. However, this is not very significant for our purposes.

The move() method moves an atom to a new position and is called during each time the parent GasPanel’s run() loop is executed. When new coordinates x, y are calculated, this method checks for possible bounces from this container's walls:

  public void move(Rectangle rc) {
    double x = m_x + m_vx;
    double y = m_y + m_vy;
    int x1 = rc.x;
    int x2 = rc.x + rc.width;
    int y1 = rc.y;
    int y2 = rc.y + rc.height;
    for (int bounce = 0; bounce<2; bounce++) {
      // pseudo-code
      if (x < x1) { // bounce off of left wall... }
      if (x > x2) { // bounce off of right wall... }
      if (y < y1) { // bounce off of top wall... }
      if (y > y2) { // bounce off of bottom wall... }
    }
    m_x = x;
    m_y = y;
  }

If a new point lies behind one of four walls, a bounce occurs, which changes the coordinate and velocity vector. This contributes to the pressure on the wall the bounce occurred on (as an absolute change in the velocity's component), which is accumulated in the parent GasPanel. Note that bouncing is checked twice to take into account the rare case that two subsequent bounces occur in a single step. That can occur near the container's corners, when, after the first bounce, the moving particle is repositioned beyond the nearest perpendicular wall.

The final methods of our Atom class are fairly straightforward. The ensureInRect() method is called to ensure that an Atom’s coordinates lie within the given rectangle, and the getX() and getY() methods return the current coordinates as integers.

Running the Code

Note how the gas reacts to the change in the parent container’s volume by adjusting the position of the split pane divider. Also try adjusting the size of the application frame.

The following are some P and PV measurements we obtained when experimenting with this example:

Left: p=749 pv=224700 Right: p=996 pv=276888

Left: p=701 pv=210300 Right: p=1006 pv=279668

Left: p=714 pv=214200 Right: p=1028 pv=285784

Left: p=770 pv=231000 Right: p=1018 pv=283004

Left: p=805 pv=241500 Right: p=1079 pv=299962

Left: p=1586 pv=190320 Right: p=680 pv=311440

Left: p=1757 pv=210840 Right: p=594 pv=272052

Left: p=1819 pv=218280 Right: p=590 pv=270220

Left: p=1863 pv=223560 Right: p=573 pv=262434

Left: p=1792 pv=215040 Right: p=621 pv=284418

We can see tell at a certain time the divider had been moved from right to left by the increase in pressure on the left side, and a decrease in pressure on the right side. However, the PV value (in arbitrary units) remains practically unchanged.