I've created a few GUI applications in Java before, and every time I'm really just messing around until the thing does what I want it to do, using snippits from several GUI examples I find on the web. Recently I read some more about good practices when writing a GUI in Java using Swing, but still some things are unclear to me. Let me first describe what I want to do in my next project so that we can use that as an example:
I'd like to create an executable application that gives a user the ability to load images, perform some actions on these images and save the project. There should be a main image viewer at the core with a simple navigator below it to switch between the loaded images. There should be panels with buttons that perform operations on an image (for example a button 'pick background color from image') and these operations should optionally be accessible from a toolbar or a menu.
I know that for example the GUI should be started from the Event Dispatch Thread and that I can use a SwingWorker for timeconsuming operations. Also I learned that by using Actions I can separate functionality from state and create one Action for both a panel button, a toolbar button and a menu item.
What I don't understand is how all these things communicate with each other, and where I put what. For example: do I maintain the state of my program (so which image is currently displayed, what settings are set) in a separate model and put a reference to this model in my main window class? And what about the controllers? Do I keep a reference to the models in the controllers are well? And when I did some calculation on an image, do I update the image in the gui from the controller, or from the gui itself using a simple repaint?
I guess the main problem I have is that I don't really understand how to let the different parts of my program communicate. A GUI consists of lots of parts, and then there are all these Actions and Listeners, models, controllers, and all of these need to interact somehow. I keep adding references to almost everything in all of these objects, but it makes everything extremely messy.
Another example I found on the web:
http://www.devdaily.com/java/java-action-abstractaction-actionlistener
I understand how this works, what I don't understand is how to actually change the "Would have done the 'Cut' action." into the actual cut action. Let's say it involves cutting a part out of my image in the viewer, would I have passed the image to the action? And if this process would take long, would I have created a new SwingWorker inside the action? And then how would I let the SwingWorker update the GUI while calculating? Would I pass a reference of the GUI to the SwingWorker such that it can update it from time to time?
Does anyone have a good example on how to do this or maybe some tips on how to correctly learn this, because I'm at a bit of a loss. There's just so much information and so many different ways to do things, and I really want to learn how to create scalable applications with clean code. Is there maybe a good open-source project with not too much code that very nicely demonstrates a GUI like what I described so that I can learn from that?
I've built a few Swing and SWT GUI's. What I've found is that a GUI requires its own model - view (MV) structure. The application controller interacts with the GUI model, rather than the GUI components.
Basically, I build a Java bean for every Swing JPanel in my GUI. The GUI components interact with the Java bean, and the application controller interacts with the Java bean(s).
Here's a Spirograph GUI that I created.
Here's the Java bean that manages the Spirograph parameters.
import java.awt.Color;
public class ControlModel {
protected boolean isAnimated;
protected int jpanelWidth;
protected int outerCircle;
protected int innerCircle;
protected int penLocation;
protected int penSize;
protected Color penColor;
protected Color backgroundColor;
public ControlModel() {
init();
this.penColor = Color.BLUE;
this.backgroundColor = Color.WHITE;
}
public void init() {
this.jpanelWidth = 512;
this.outerCircle = 1000;
this.innerCircle = 520;
this.penLocation = 400;
this.penSize = 2;
this.isAnimated = true;
}
public int getOuterCircle() {
return outerCircle;
}
public void setOuterCircle(int outerCircle) {
this.outerCircle = outerCircle;
}
public int getInnerCircle() {
return innerCircle;
}
public void setInnerCircle(int innerCircle) {
this.innerCircle = innerCircle;
}
public int getPenLocation() {
return penLocation;
}
public void setPenLocation(int penLocation) {
this.penLocation = penLocation;
}
public int getPenSize() {
return penSize;
}
public void setPenSize(int penSize) {
this.penSize = penSize;
}
public boolean isAnimated() {
return isAnimated;
}
public void setAnimated(boolean isAnimated) {
this.isAnimated = isAnimated;
}
public Color getPenColor() {
return penColor;
}
public void setPenColor(Color penColor) {
this.penColor = penColor;
}
public Color getBackgroundColor() {
return backgroundColor;
}
public void setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
public int getJpanelWidth() {
return jpanelWidth;
}
public int getJpanelHeight() {
return jpanelWidth;
}
}
Edited to add the Control Panel class.
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import com.ggl.spirograph.model.ControlModel;
public class ControlPanel {
protected static final Insets entryInsets = new Insets(0, 10, 4, 10);
protected static final Insets titleInsets = new Insets(0, 10, 20, 10);
protected ControlModel model;
protected DrawingPanel drawingPanel;
protected JButton drawButton;
protected JButton stopButton;
protected JButton resetButton;
protected JButton foregroundColorButton;
protected JButton backgroundColorButton;
protected JLabel message;
protected JPanel panel;
protected JTextField outerCircleField;
protected JTextField innerCircleField;
protected JTextField penLocationField;
protected JTextField penSizeField;
protected JTextField penFadeField;
protected JToggleButton animationToggleButton;
protected JToggleButton fastToggleButton;
protected static final int messageLength = 100;
protected String blankMessage;
public ControlPanel(ControlModel model) {
this.model = model;
this.blankMessage = createBlankMessage();
createPartControl();
setFieldValues();
setColorValues();
}
public void setDrawingPanel(DrawingPanel drawingPanel) {
this.drawingPanel = drawingPanel;
}
protected String createBlankMessage() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < messageLength / 4; i++) {
sb.append(" ");
}
return sb.toString();
}
protected void createPartControl() {
panel = new JPanel();
panel.setLayout(new GridBagLayout());
int gridy = 0;
JLabel title = new JLabel("Spirograph Parameters");
title.setHorizontalAlignment(SwingConstants.CENTER);
addComponent(panel, title, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
resetButton = new JButton("Reset Default Parameters");
resetButton.setHorizontalAlignment(SwingConstants.CENTER);
resetButton.addActionListener(new ResetButtonListener());
addComponent(panel, resetButton, 0, gridy++, 4, 1, entryInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
JLabel outerCircleLabel = new JLabel("Outer circle radius:");
outerCircleLabel.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, outerCircleLabel, 0, gridy, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
outerCircleField = new JTextField(5);
outerCircleField.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, outerCircleField, 2, gridy++, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
JLabel innerCircleLabel = new JLabel("Inner circle radius:");
innerCircleLabel.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, innerCircleLabel, 0, gridy, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
innerCircleField = new JTextField(5);
innerCircleField.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, innerCircleField, 2, gridy++, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
JLabel penLocationLabel = new JLabel("Pen location radius:");
penLocationLabel.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, penLocationLabel, 0, gridy, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
penLocationField = new JTextField(5);
penLocationField.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, penLocationField, 2, gridy++, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
JLabel penSizeLabel = new JLabel("Pen size:");
penSizeLabel.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, penSizeLabel, 0, gridy, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
penSizeField = new JTextField(5);
penSizeField.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, penSizeField, 2, gridy++, 2, 1, entryInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
message = new JLabel(blankMessage);
message.setForeground(Color.RED);
message.setHorizontalAlignment(SwingConstants.LEFT);
addComponent(panel, message, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL);
title = new JLabel("Drawing Speed");
title.setHorizontalAlignment(SwingConstants.CENTER);
addComponent(panel, title, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 4, 0));
animationToggleButton = new JToggleButton("Animated");
animationToggleButton.setSelected(model.isAnimated());
animationToggleButton.setHorizontalAlignment(SwingConstants.CENTER);
animationToggleButton.addActionListener(new DrawingSpeedListener(
animationToggleButton));
buttonPanel.add(animationToggleButton);
fastToggleButton = new JToggleButton("Fast");
fastToggleButton.setSelected(!model.isAnimated());
fastToggleButton.setHorizontalAlignment(SwingConstants.CENTER);
fastToggleButton.addActionListener(new DrawingSpeedListener(
fastToggleButton));
buttonPanel.add(fastToggleButton);
addComponent(panel, buttonPanel, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
title = new JLabel("Drawing Colors");
title.setHorizontalAlignment(SwingConstants.CENTER);
addComponent(panel, title, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
buttonPanel = new JPanel(new GridLayout(1, 2, 4, 0));
foregroundColorButton = new JButton("Pen");
foregroundColorButton.setHorizontalAlignment(SwingConstants.CENTER);
foregroundColorButton.addActionListener(new ColorSelectListener(
foregroundColorButton));
buttonPanel.add(foregroundColorButton);
backgroundColorButton = new JButton("Paper");
backgroundColorButton.setHorizontalAlignment(SwingConstants.CENTER);
backgroundColorButton.addActionListener(new ColorSelectListener(
backgroundColorButton));
buttonPanel.add(backgroundColorButton);
addComponent(panel, buttonPanel, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
title = new JLabel("Drawing Controls");
title.setHorizontalAlignment(SwingConstants.CENTER);
addComponent(panel, title, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
buttonPanel = new JPanel(new GridLayout(1, 2, 4, 0));
drawButton = new JButton("Draw");
drawButton.setHorizontalAlignment(SwingConstants.CENTER);
drawButton.addActionListener(new DrawButtonListener());
buttonPanel.add(drawButton);
stopButton = new JButton("Stop");
stopButton.setHorizontalAlignment(SwingConstants.CENTER);
stopButton.addActionListener(new StopButtonListener());
buttonPanel.add(stopButton);
addComponent(panel, buttonPanel, 0, gridy++, 4, 1, titleInsets,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
}
protected void addComponent(Container container, Component component,
int gridx, int gridy, int gridwidth, int gridheight,
Insets insets, int anchor, int fill) {
GridBagConstraints gbc = new GridBagConstraints(gridx, gridy,
gridwidth, gridheight, 1.0, 1.0, anchor, fill, insets, 0, 0);
container.add(component, gbc);
}
protected void setFieldValues() {
outerCircleField.setText(Integer.toString(model.getOuterCircle()));
innerCircleField.setText(Integer.toString(model.getInnerCircle()));
penLocationField.setText(Integer.toString(model.getPenLocation()));
penSizeField.setText(Integer.toString(model.getPenSize()));
}
protected void setColorValues() {
foregroundColorButton.setForeground(model.getBackgroundColor());
foregroundColorButton.setBackground(model.getPenColor());
backgroundColorButton.setForeground(model.getPenColor());
backgroundColorButton.setBackground(model.getBackgroundColor());
}
public JPanel getPanel() {
return panel;
}
public class ResetButtonListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
model.init();
setFieldValues();
}
}
public class StopButtonListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent arg0) {
drawingPanel.stop();
}
}
public class DrawButtonListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
message.setText(blankMessage);
int ocTest = isNumeric(outerCircleField.getText());
int icTest = isNumeric(innerCircleField.getText());
int plTest = isNumeric(penLocationField.getText());
int psTest = isNumeric(penSizeField.getText());
boolean isInvalid = false;
if (psTest < 0) {
message.setText("Pen size is not a valid integer");
isInvalid = true;
}
if (plTest < 0) {
message.setText("Pen location radius is not a valid integer");
isInvalid = true;
}
if (icTest < 0) {
message.setText("Inner circle radius is not a valid integer");
isInvalid = true;
}
if (ocTest < 0) {
message.setText("Outer circle radius is not a valid integer");
isInvalid = true;
}
if (isInvalid) {
return;
}
if (ocTest > 1000) {
message.setText("The outer circle cannot be larger than 1000");
isInvalid = true;
}
if (ocTest <= icTest) {
message.setText("The inner circle must be smaller than the outer circle");
isInvalid = true;
}
if (icTest <= plTest) {
message.setText("The pen location must be smaller than the inner circle");
isInvalid = true;
}
if (isInvalid) {
return;
}
model.setOuterCircle(ocTest);
model.setInnerCircle(icTest);
model.setPenLocation(plTest);
model.setPenSize(psTest);
drawingPanel.draw(model.isAnimated());
}
protected int isNumeric(String field) {
try {
return Integer.parseInt(field);
} catch (NumberFormatException e) {
return Integer.MIN_VALUE;
}
}
}
public class DrawingSpeedListener implements ActionListener {
JToggleButton selectedButton;
public DrawingSpeedListener(JToggleButton selectedButton) {
this.selectedButton = selectedButton;
}
@Override
public void actionPerformed(ActionEvent arg0) {
if (selectedButton.equals(animationToggleButton)) {
model.setAnimated(true);
} else {
model.setAnimated(false);
}
animationToggleButton.setSelected(model.isAnimated());
fastToggleButton.setSelected(!model.isAnimated());
}
}
public class ColorSelectListener implements ActionListener {
JButton selectedButton;
public ColorSelectListener(JButton selectedButton) {
this.selectedButton = selectedButton;
}
@Override
public void actionPerformed(ActionEvent arg0) {
if (selectedButton.equals(foregroundColorButton)) {
Color initialColor = model.getPenColor();
Color selectedColor = JColorChooser.showDialog(drawingPanel,
"Select pen color", initialColor);
if (selectedColor != null) {
model.setPenColor(selectedColor);
}
} else if (selectedButton.equals(backgroundColorButton)) {
Color initialColor = model.getBackgroundColor();
Color selectedColor = JColorChooser.showDialog(drawingPanel,
"Select paper color", initialColor);
if (selectedColor != null) {
model.setBackgroundColor(selectedColor);
}
}
setColorValues();
}
}
}