Java: T&D Question Regarding Separation of Layout+Events

XML, Perl, Python, and other languages can be discussed here, even if it isn't PHP (We might forgive you).

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Java: T&D Question Regarding Separation of Layout+Events

Post by Chris Corbyn »

I've been messing about with Swing for a little while now and have always thought it was a little bit tricky to keep "controller" logic out of the View since actual visual components dispatch events which are picked up and dealt with by "event listeners" which will commonly just change something on the GUI -- potentially all a bit of a mish-mash.

You could of course make a separate class (event listener) for each Swing component which needs an event listener but this feels like a bit of an overkill if all the listener does is updates a bit of text on the window or something. Using one class isn't feasible neither since the event listeners follow an interface for that type of event so you'd have a completely ambiguous event being dispatched to the listener.

What I've done here is I've tried to create a sort of Front-controller (I called it UIEventController) which applies the events to the view components itself and uses instances of anonymous classes in order to prevent the need for separate files... unless of course the logic got lengthy this feels reasonable I think. Currently all that happens is when a ComboBox is changed, other ComboBoxes change based upon what was selected (e.g. Countries change to reflect the chosen continent).

The only way I've been able to cleanly separate this though is by creating a Registry to store instances of all my window components, and injecting the model (ermm... a bit backward here) from the view into the controller. Does this feel icky? Read the comments cos the code is probably a bit left to be desired.

The User-interface which gets painted:

Code: Select all

/**
 * Provides the layout of the window and holds all the components
 */

package org.w3style.suntracker.ui;

import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import org.w3style.suntracker.model.Locations;
import org.w3style.suntracker.grapher.AzimuthAltitudeGraph;

/**
 * FrontEnd is the GUI seen by the user, using Swing
 */
public class FrontEnd
{
	/**
	 * The locations data (continent, country, region, longitude, latitude etc)
	 */
	public Locations locations = null;
	/**
	 * The graphing engine - A wrapper around JFreeChart
	 */
	public AzimuthAltitudeGraph grapher = null;
	/**
	 * A bit of an attempt to handle events away from the presentation
	 */
	public UIEventController eventController = null;
	
	/**
	 * Constructor
	 */
	public FrontEnd()
	{
		this.locations = new Locations();
		this.grapher = new AzimuthAltitudeGraph();
	}
	/**
	 * Show the GUI, running in the event-dispatching thread for safety
	 */
	public void show()
	{
		System.out.println("Rendering application interface");
		
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				createUIEventController();
				revealGUI();
			}
		});
	}
	/**
	 * Start the event listening/controlling layer
	 */
	public void createUIEventController()
	{
		this.eventController = new UIEventController(this.locations);
	}
	/**
	 * Compile and show the GUI (make it visible)
	 */
	public void revealGUI()
	{
		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (Exception e) {
			System.out.println("Unable to set System Look & Feel");
		}
		
		JFrame frame = new JFrame("SunTracker");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		this.addFeaturesToLayout(frame.getContentPane());
		
		frame.pack();
		frame.setVisible(true);
	}
	/**
	 * Populate the parent window with all the internal Swing components
	 * @param Container The parent window
	 */
	protected void addFeaturesToLayout(Container pane)
	{
		pane.setLayout(new GridBagLayout());
		
		GridBagConstraints c = new GridBagConstraints();
		c.fill = GridBagConstraints.BOTH;
		
		JPanel top = new JPanel();
		this.addControlFeatures(top);
		c.gridx = 0;
		c.gridy = 0;
		c.ipadx = 10;
		c.ipady = 10;
		
		JPanel bottom = new JPanel();
		this.addGraphingArea(bottom);
		
		c.gridy = 0;
		c.anchor = GridBagConstraints.NORTH;
		pane.add(top, c);
		
		c.gridy = 1;
		pane.add(bottom, c);
	}
	/**
	 * Add all the user controls to the top-half of the GUI
	 * TODO: Finish writing the event listeners to update the components as needed; Write a calendar widget
	 * @param JPanel The top panel in the window
	 */
	protected void addControlFeatures(JPanel pane)
	{
		pane.setLayout(new GridBagLayout());
		pane.setBorder(BorderFactory.createTitledBorder("Complete the fields below and hit \"Generate\""));
		
		GridBagConstraints c = new GridBagConstraints();
		c.fill = GridBagConstraints.HORIZONTAL;
		c.insets = new Insets(4, 4, 4, 4);
		c.gridx = 0;
		c.gridy = 0;
		
		/** Top left **/
		//Get the continents from the model
		String[] continents = this.locations.getContinents();
		c.gridwidth = 2;
		//Create a combo-box with the list
		JComboBox continentDropdown = new JComboBox(continents);
		//Allow the event listening layer to apply needed listeners
		this.eventController.addContinentDropdownListener(continentDropdown);
		//Add it to the window
		pane.add(continentDropdown, c);
		//Register it for global access
		WidgetRegistry.getInstance().register(continentDropdown, "continents");
		
		/** Top second-left **/
		//As for continent, except without data for now
		c.gridx = 2;
		String[] countries = { " - Country/State - " };
		JComboBox countriesDropdown = new JComboBox(countries);
		this.eventController.addCountriesDropdownListener(countriesDropdown);
		pane.add(countriesDropdown, c);
		WidgetRegistry.getInstance().register(countriesDropdown, "countries");
		
		/** Top third-left **/
		//See above
		c.gridx = 4;
		String[] cities = { " - Region - " };
		JComboBox citiesDropdown = new JComboBox(cities);
		pane.add(citiesDropdown, c);
		WidgetRegistry.getInstance().register(citiesDropdown, "cities");
		
		/** Top right **/
		//Add a calendar to choose a date (see TODO)
		c.gridwidth = 1;
		c.gridheight = 3;
		c.gridx = 6;
		c.fill = GridBagConstraints.BOTH;
		pane.add(new JButton("Calendar goes here"), c);
		
		/** Second-top left **/
		JTextField latLong = new JTextField();
		JLabel latLongLabel = new JLabel("Lat/Long ");
		latLongLabel.setLabelFor(latLong);
		
		c.fill = GridBagConstraints.HORIZONTAL;
		c.gridheight = 1;
		c.gridy = 1;
		c.gridx = 0;
		pane.add(latLongLabel, c);
		
		c.gridx = 1;
		pane.add(latLong, c);
		WidgetRegistry.getInstance().register(latLong, "latlong");
		
		/** Second-top third-left **/
		//Create a time-in-hours combo-box
		String[] hours = new String[25];
		hours[0] = "HH";
		for (int i = 0; i < 24; i++) { hours[(i+1)] = (i < 10 ? "0" + i : "" + i); }
		JComboBox hoursDropdown = new JComboBox(hours);
		c.gridx = 2;
		pane.add(hoursDropdown, c);
		
		/** Second-top fourth-left **/
		//Create a time-in-minutes combo-box
		String[] mins = new String[((int)(60/5)+1)];
		mins[0] = "MM";
		for (int a = 1, i = 0; i < 60; i+=5, a++) { mins[a] = (i < 10 ? "0" + i : "" + i); }
		JComboBox minsDropdown = new JComboBox(mins);
		c.gridx = 3;
		pane.add(minsDropdown, c);
		
		/** Second-top right **/
		//Checkbox for DST
		JCheckBox dstCheck = new JCheckBox();
		JLabel dstLabel = new JLabel("Daylight Saving");
		dstLabel.setLabelFor(dstCheck);
		
		c.gridx = 4;
		pane.add(dstCheck, c);
		c.gridx = 5;
		pane.add(dstLabel, c);
		
		//Blank line - hackish at best
		c.gridy = 2;
		c.gridx = 0;
		c.gridwidth = 6;
		pane.add(new JLabel("  ", JLabel.CENTER), c);
		
		/** Bottom center **/
		c.gridx = 2;
		c.gridwidth = 2;
		c.gridy = 3;
		pane.add(new JButton("Generate"), c);
	}
	/**
	 * Add the Graph (JFreeChart) to the bottom of the GUI
	 * TODO: Get around to implementing this
	 * @param JPanel The bottom panel in the GUI
	 */
	protected void addGraphingArea(JPanel pane)
	{
		pane.setLayout(new GridBagLayout());
		pane.setBorder(BorderFactory.createTitledBorder("Graph output"));
		GridBagConstraints c = new GridBagConstraints();
		c.fill = GridBagConstraints.BOTH;
		c.gridx = 0;
		c.gridy = 0;
		JLabel graphArea = new JLabel();
		
		BufferedImage chart = this.grapher.getChartAsBufferedImage(700, 350);
		
		graphArea.setIcon(new ImageIcon(chart));
		
		pane.add(graphArea, c);
	}
}
The event-listening controller

Code: Select all

/**
 * The layer which deals with all the User events from the interface
 */

package org.w3style.suntracker.ui;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import org.w3style.suntracker.model.Locations;

/**
 * Attaches event listeners to window components and works with the model to update components as required
 */
public class UIEventController
{
	/**
	 * The locations data from the database (model)
	 */
	protected Locations locations = null;
	
	/**
	 * Constructor
	 * @param Locations The current instance of the model used by the View layer
	 */
	public UIEventController(Locations injected)
	{
		this.locations = injected;
	}
	/**
	 * Add the event listener(s) to the combo-box which has the continents in it
	 * @param JComboBox The combo-box from the View
	 */
	public void addContinentDropdownListener(JComboBox continents)
	{
		continents.addItemListener(new ItemListener() {
			public void itemStateChanged(ItemEvent e)
			{
				if (e.getStateChange() == ItemEvent.SELECTED)
				{
					//Change country dropdown to countries for this continent
					int continentIndex = ((JComboBox)WidgetRegistry.getInstance().get("continents")).getSelectedIndex();
					String[] countriesDataset = locations.getCountries(continentIndex);
					JComboBox countriesDropdown = (JComboBox)WidgetRegistry.getInstance().get("countries");
					countriesDropdown.setModel(new DefaultComboBoxModel(countriesDataset));
					countriesDropdown.updateUI();
					
					//Reset the Locations dropdown
					JComboBox citiesDropdown = (JComboBox)WidgetRegistry.getInstance().get("cities");
					String[] citiesDataset = { " - Region - " };
					citiesDropdown.setModel(new DefaultComboBoxModel(citiesDataset));
					citiesDropdown.updateUI();
				}
			}
		});
	}
	/**
	 * Add the event listener(s) the combo-box containing the list of countries on the View
	 * @param JComboBox The countries/states component
	 */
	public void addCountriesDropdownListener(JComboBox countries)
	{
		countries.addItemListener(new ItemListener() {
			public void itemStateChanged(ItemEvent e)
			{
				if (e.getStateChange() == ItemEvent.SELECTED)
				{
					int countryIndex = ((JComboBox)WidgetRegistry.getInstance().get("countries")).getSelectedIndex();
					String[] citiesDataset = locations.getCities(countryIndex);
					JComboBox citiesDropdown = (JComboBox)WidgetRegistry.getInstance().get("cities");
					citiesDropdown.setModel(new DefaultComboBoxModel(citiesDataset));
					citiesDropdown.updateUI();
				}
			}
		});
	}
}
The other components I've left out are fairly generic anyway, just a database access layer and a singleton registry.
Post Reply