Related
I'm a Windows user, and I have developed a small software to calculate few parameter based on some inputs. The GUI in Windows is displayed as expected, but in Mac OS it's different. Please let me know how to correct it. The code for the same is.
package home;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import java.awt.Component;
import java.awt.ComponentOrientation;
/**
* Home Page for the application.
* #author Harshit Rathore
*
*/
public class Home_Main {
private JFrame f;
private JXTabbedPane tabbedpane;
private ImageIcon icon_view, icon_analysis, icon_algo, icon_info, icon_sche_gen;
Schedule_Generation schedule_gen;
View_JPanel2D view2d;
View_JPanel3D view3d;
MapJPanel mapPanel;
Home_Main() {
f = new JFrame();
f.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
f.setTitle("Anti-Accidental Algorithm");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
tabbedpane = new JXTabbedPane(JTabbedPane.LEFT);
AbstractTabRenderer renderer = (AbstractTabRenderer)tabbedpane.getTabRenderer();
renderer.setPrototypeText("This text is a prototype->");
renderer.setHorizontalTextAlignment(SwingConstants.LEADING);
icon_view = new ImageIcon(this.getClass().getResource("/view_icon.png"));
icon_analysis = new ImageIcon(this.getClass().getResource("/analysis_icon.png"));
icon_algo = new ImageIcon(this.getClass().getResource("/algo_icon.png"));
icon_info = new ImageIcon(this.getClass().getResource("/info_icon.png"));
icon_sche_gen = new ImageIcon(this.getClass().getResource("/schedule_icon.png"));
mapPanel = new home.MapJPanel();
view2d = new home.View_JPanel2D();
view3d = new home.View_JPanel3D();
schedule_gen = new home.Schedule_Generation(view2d, view3d, mapPanel);
tabbedpane.addTab("Algorithm", icon_algo, new home.Algo_JPanel());
tabbedpane.addTab("<html>Schedule<br>Generation</html>", icon_sche_gen, schedule_gen);
tabbedpane.addTab("<html>View Data 2D<br>(Azimuth, Elevation)</html>", icon_view, view2d);
tabbedpane.addTab("<html>View Data 3D<br>(Azimuth, Elevation, Tilt)</html>", icon_view, view3d);
tabbedpane.addTab("MapPanel", icon_info, mapPanel);
tabbedpane.addTab("<html>Windload<br>Torque</html>", icon_analysis, new home.WindloadTorque());
f.getContentPane().add(tabbedpane);
f.setMinimumSize(new Dimension(1100, 800));
f.setSize(1600, 800);
f.setVisible(true);
f.setEnabled(true);
}
/**
* The main function of the software.
* #param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
new Home_Main();
}
// custom tabbedpane
class JXTabbedPane extends JTabbedPane {
/**
*
*/
private static final long serialVersionUID = 1L;
private ITabRenderer tabRenderer = new DefaultTabRenderer();
public JXTabbedPane() {
super();
}
public JXTabbedPane(int tabPlacement) {
super(tabPlacement);
}
public JXTabbedPane(int tabPlacement, int tabLayoutPolicy) {
super(tabPlacement, tabLayoutPolicy);
}
public ITabRenderer getTabRenderer() {
return tabRenderer;
}
public void setTabRenderer(ITabRenderer tabRenderer) {
this.tabRenderer = tabRenderer;
}
#Override
public void addTab(String title, Component component) {
this.addTab(title, null, component, null);
}
#Override
public void addTab(String title, Icon icon, Component component) {
this.addTab(title, icon, component, null);
}
#Override
public void addTab(String title, Icon icon, Component component, String tip) {
super.addTab(title, icon, component, tip);
int tabIndex = getTabCount() - 1;
Component tab = tabRenderer.getTabRendererComponent(this, title, icon, tabIndex);
super.setTabComponentAt(tabIndex, tab);
}
}
interface ITabRenderer {
public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex);
}
abstract class AbstractTabRenderer implements ITabRenderer {
private String prototypeText = "";
private Icon prototypeIcon = new ImageIcon(this.getClass().getResource("/view_icon.png"));
private int horizontalTextAlignment = SwingConstants.CENTER;
private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
public AbstractTabRenderer() {
super();
}
public void setPrototypeText(String text) {
String oldText = this.prototypeText;
this.prototypeText = text;
firePropertyChange("prototypeText", oldText, text);
}
public String getPrototypeText() {
return prototypeText;
}
public Icon getPrototypeIcon() {
return prototypeIcon;
}
public void setPrototypeIcon(Icon icon) {
Icon oldIcon = this.prototypeIcon;
this.prototypeIcon = icon;
firePropertyChange("prototypeIcon", oldIcon, icon);
}
public int getHorizontalTextAlignment() {
return horizontalTextAlignment;
}
public void setHorizontalTextAlignment(int horizontalTextAlignment) {
this.horizontalTextAlignment = horizontalTextAlignment;
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return propertyChangeSupport.getPropertyChangeListeners();
}
public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
return propertyChangeSupport.getPropertyChangeListeners(propertyName);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
}
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
PropertyChangeListener[] listeners = getPropertyChangeListeners();
for (int i = listeners.length - 1; i >= 0; i--) {
listeners[i].propertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue));
}
}
}
class DefaultTabRenderer extends AbstractTabRenderer implements PropertyChangeListener {
private Component prototypeComponent;
public DefaultTabRenderer() {
super();
prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(),
getHorizontalTextAlignment());
addPropertyChangeListener(this);
}
private Component generateRendererComponent(String text, Icon icon, int horizontalTabTextAlignmen) {
JPanel rendererComponent = new JPanel(new GridBagLayout());
rendererComponent.setOpaque(false);
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 4, 2, 4);
c.fill = GridBagConstraints.HORIZONTAL;
rendererComponent.add(new JLabel(icon), c);
c.gridx = 1;
c.weightx = 1;
rendererComponent.add(new JLabel(text, horizontalTabTextAlignmen), c);
return rendererComponent;
}
#Override
public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex) {
Component rendererComponent = generateRendererComponent(text, icon, getHorizontalTextAlignment());
int prototypeWidth = prototypeComponent.getPreferredSize().width;
int prototypeHeight = prototypeComponent.getPreferredSize().height;
rendererComponent.setPreferredSize(new Dimension(prototypeWidth, prototypeHeight));
return rendererComponent;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
if ("prototypeText".equals(propertyName) || "prototypeIcon".equals(propertyName)) {
this.prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(),
getHorizontalTextAlignment());
}
}
}
}
The outputs are-
First one is from Windows (desired one) second one is from Mac OS(need modification).
Thank You
The reason for the buttons looking different is because of Java Swing's LAF (Look & Feel).
The Look and Feel is like the master “theme” for your Swing Application where it defines specific rules on how certain components should look, etc…
Java Swing on Windows looks different from on Unix & OSX is because there is a different provider for the LAF (known as LAF "metal")
You thus can pack a LAF library into your program, and you can find many here.
To call a theme to be used, you must place this before any GUI/Swing events are called (AKA repaints, component inits, etc.):
try {
UIManager.setLookAndFeel(myThemeLAF.class.getName());
} catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
Alternatively, you could override the individual paintComponent(Graphics g) methods.
This can be achieved either through anonymous classes or just having a class extend that object and override it from there:
#1 Anonymous Inner Classes
JPanel myPanel = new JPanel() {
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//... draw stuffs
}
};
#2 Extending existing objects
public class MyPanel extends JPanel {
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// paint stuffs
}
}
This is just a simple audio player that I was messing around with to dip my feet into making a GUI. How do you efficiently seperate Java logic and the GUI elements into different files and packages? I tried creating methods in the main class to perform the ActionListeners but the "track" object can't be globally used. This is my first time messing with Swing and GUI's in general.
import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.*;
import java.io.*;
public class Main extends JFrame{
public static void main(String[] args) throws LineUnavailableException, IOException, UnsupportedAudioFileException {
// Audio
File filePath = new File("src/Tracks/Track.wav");
AudioInputStream stream = AudioSystem.getAudioInputStream(filePath);
Clip track = AudioSystem.getClip();
track.open(stream);
// Buttons
JButton playButton = new JButton("Play");
playButton.addActionListener(e -> {track.start();});
playButton.setHorizontalAlignment(SwingConstants.LEFT);
playButton.setFont(new Font("JetBrains Mono", Font.BOLD, 25));
playButton.setBounds(0,0,100,50);
JButton pauseButton = new JButton("Pause");
pauseButton.addActionListener(e -> {track.stop();});
pauseButton.setHorizontalAlignment(SwingConstants.CENTER);
pauseButton.setFont(new Font("JetBrains Mono", Font.BOLD, 25));
pauseButton.setBounds(150,0,100,50);
JButton replayButton = new JButton("Replay");
replayButton.addActionListener(e -> {track.setMicrosecondPosition(0);});
replayButton.setHorizontalAlignment(SwingConstants.RIGHT);
replayButton.setFont(new Font("JetBrains Mono", Font.BOLD, 25));
replayButton.setBounds(300,0,100,50);
// Frame
JFrame frame = new JFrame();
frame.setTitle("MusicPlayer");
frame.setSize(500, 300);
frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
frame.setVisible(true);
frame.getContentPane().setBackground(new Color(123, 100, 250));
frame.setLayout(new FlowLayout());
frame.add(playButton);
frame.add(pauseButton);
frame.add(replayButton);
}
}
I'd recommend using decomposition, packages, and dependency injection.
Swing encourages developers to put reams of code into large main methods. That's a mistake, in my opinion.
Decompose that main method into classes. Make them independently testable and only assemble the UI from constitutive parts. Only create the JFrame class in the main method. Give the JFrame an instance of JPanel in its constructor.
There ought to be a /ui package with a class that extends JPanel. Give it all the buttons, text boxes, and GUI assets that it needs in its constructors.
Decomposition is your friend. Start breaking the problem up into classes.
Create a factory that gives you track instances. You can reuse it every where it's needed if you do.
Introduction
Your code has problems with implementing Swing as well as being one main method.
Oracle has a helpful tutorial, Creating a GUI With Swing. Skip the Learning Swing with the NetBeans IDE section.
Let's take this step by step.
Step 1
The first thing I did was rename your main GUI class to something more meaningful, remove the player code, and focus on the GUI.
I added a call to the SwingUtilities invokeLater method. This method ensures that the Swing components are created and executed on the Event Dispatch Thread.
Here's the result of that step.
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
public class SimpleMusicPlayer implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new SimpleMusicPlayer());
}
#Override
public void run() {
// Buttons
JButton playButton = new JButton("Play");
// playButton.addActionListener(e -> {
// track.start();
// });
playButton.setHorizontalAlignment(SwingConstants.LEFT);
playButton.setFont(new Font("JetBrains Mono", Font.BOLD, 25));
playButton.setBounds(0, 0, 100, 50);
JButton pauseButton = new JButton("Pause");
// pauseButton.addActionListener(e -> {
// track.stop();
// });
pauseButton.setHorizontalAlignment(SwingConstants.CENTER);
pauseButton.setFont(new Font("JetBrains Mono", Font.BOLD, 25));
pauseButton.setBounds(150, 0, 100, 50);
JButton replayButton = new JButton("Replay");
// replayButton.addActionListener(e -> {
// track.setMicrosecondPosition(0);
// });
replayButton.setHorizontalAlignment(SwingConstants.RIGHT);
replayButton.setFont(new Font("JetBrains Mono", Font.BOLD, 25));
replayButton.setBounds(300, 0, 100, 50);
// Frame
JFrame frame = new JFrame();
frame.setTitle("MusicPlayer");
frame.setSize(500, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
frame.getContentPane().setBackground(new Color(123, 100, 250));
frame.setLayout(new FlowLayout());
frame.add(playButton);
frame.add(pauseButton);
frame.add(replayButton);
}
}
This step took the code out of the static realm.
Step 2
There are a couple of changes in the next step.
I created a JPanel to hold the JButtons. The JPanel uses a FlowLayout to arrange the buttons in a row. I added some spacing around and between the buttons.
I rearranged the JFrame method calls. These methods must be called in a specific order. The setVisible method must be called last.
Here's the result of that step.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class SimpleMusicPlayer implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new SimpleMusicPlayer());
}
#Override
public void run() {
JFrame frame = new JFrame("Music Player");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(createMainPanel(), BorderLayout.CENTER);
frame.setSize(500, 300);
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JPanel createMainPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 30, 5));
panel.setBackground(new Color(123, 100, 250));
panel.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
Font font = new Font("JetBrains Mono", Font.BOLD, 25);
JButton playButton = new JButton("Play");
// playButton.addActionListener(e -> {
// track.start();
// });
playButton.setFont(font);
panel.add(playButton);
JButton pauseButton = new JButton("Pause");
// pauseButton.addActionListener(e -> {
// track.stop();
// });
pauseButton.setFont(font);
panel.add(pauseButton);
JButton replayButton = new JButton("Replay");
// replayButton.addActionListener(e -> {
// track.setMicrosecondPosition(0);
// });
replayButton.setFont(font);
panel.add(replayButton);
return panel;
}
}
Since this is a simple GUI, this concludes the GUI cleanup.
Step 3
Now, we add the audio back in as a separate class. This class becomes the model of our Swing application.
I didn't test this, since I don't have a WAV file handy.
Here's the result of that step.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.io.File;
import java.io.IOException;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class SimpleMusicPlayer implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new SimpleMusicPlayer());
}
private final MusicPlayer player;
public SimpleMusicPlayer() {
this.player = new MusicPlayer("src/Tracks/Track.wav");
}
#Override
public void run() {
JFrame frame = new JFrame("Music Player");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(createMainPanel(), BorderLayout.CENTER);
frame.setSize(500, 300);
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JPanel createMainPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 30, 5));
panel.setBackground(new Color(123, 100, 250));
panel.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
Font font = new Font("JetBrains Mono", Font.BOLD, 25);
JButton playButton = new JButton("Play");
playButton.addActionListener(e -> {
player.start();
});
playButton.setFont(font);
panel.add(playButton);
JButton pauseButton = new JButton("Pause");
pauseButton.addActionListener(e -> {
player.stop();
});
pauseButton.setFont(font);
panel.add(pauseButton);
JButton replayButton = new JButton("Replay");
replayButton.addActionListener(e -> {
player.replay();
});
replayButton.setFont(font);
panel.add(replayButton);
return panel;
}
public class MusicPlayer {
private final Clip track;
public MusicPlayer(String filename) {
this.track = openClip(filename);
}
private Clip openClip(String filename) {
try {
File filePath = new File(filename);
AudioInputStream stream = AudioSystem.getAudioInputStream(filePath);
Clip track = AudioSystem.getClip();
track.open(stream);
return track;
} catch (UnsupportedAudioFileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (LineUnavailableException e) {
e.printStackTrace();
}
return null;
}
public void start() {
if (track != null) {
track.start();
}
}
public void stop() {
if (track != null) {
track.stop();
}
}
public void replay() {
if (track != null) {
track.setMicrosecondPosition(0);
}
}
}
}
There are lots of way you might achieve this, but lets break down you problems...
UI Customisations...
Swing is generally highly customisable, for example, you could make your own custom look and feel (either in parts or as a whole). I think building a whole new look and feel is probably little bit of overkill for your problem, but you can supply a look and feel for individual components, for example...
How to customize a JProgressBar?
Java JButton set text background color
Java: Making a pretty JProgressBar
Now, if all you want to do is change a few properties of the components, then it might be better to simply modify the look and feel default properties instead, for example UIManager.put("Button.font", new Font(...));.
You need to be careful with this approach, as not all UI delegates will use the same keys (looking at you Nimbus) and also consider that this will set the default properties used for ALL new components which you create.
Alternatively, if you just wanted to modify a given set of components, you might consider using a "factory" style workflow, for example...
public class Style {
public static JButton makeButton() {
JButton btn = new JButton();
// Apply any configuration you need
// by default
return btn;
}
}
This would then then give you a centralised location for the creation of these components, as well as a centralised management workflow should you want to tweak it.
Or, you could use a builder pattern, which can be used to configure the component based on how you want to use it...
public class ButtonBuilder {
private String title;
private ActionListener actionListener;
public ButtonBuilder withTitle(String title) {
this.title = title;
return this;
}
public ButtonBuilder withActionListener(ActionListener actionListener) {
this.actionListener = actionListener;
return this;
}
// Add any additional properties you might like to configure
public JButton build() {
JButton btn = new JButton();
if (title != null) {
btn.setText(title);
}
if (actionListener != null) {
btn.addActionListener(actionListener);
}
// Configure the button as you see fit
return btn;
}
}
I would also consider looking at How to Use Actions which is a great way creating a self contained workflows, but which can easily be applied to different areas of the UI.
Code design...
The following concepts apply to just about any type of coding you might do. You want to decouple the code, reducing cohesion between classes - stop and think, if I changed something here, how hard would it be to modify the code? Then work towards a solution which is easily maintained and managed.
When you're presented with a problem try breaking the problem down into as many small units of work as possible (as state - decomposition)
These support the Single Responsibility Principle
For example, you have a "media player". Is it responsible for loading the media? Is it responsible for managing the live cycle of the media? Does it care where the media comes from our how it's actually played?
In general, the answer is, no. It only cares about allowing the user to select an action and applying the action to the current "track"
Let's break it down. Starting with a concept of a "track"...
public interface TrackModel {
enum State {
STOPPED, PLAYING, PAUSED;
}
public Clip getClip();
public State getState();
public void open() throws LineUnavailableException, IOException;
public void close();
public boolean isOpen();
public void play() throws IOException;
public void stop();
public void pause();
public void addChangeListener(ChangeListener changeListener);
public void removeChangeListener(ChangeListener changeListener);
}
Now, a "track" could be manage any thing, a single, an album, a playlist, we don't care, we just care about what the track can do.
It has functionality to control the life cycle (open/close), the state (play/pause/stop) and provides an Observer Pattern so we can observer state changes to it. It doesn't describe how those actions actually work, only that anyone with a reference to an implementation can perform these operations on it.
Personally, I always like to make use of a abstract implementation to define some of the more "common" or "boiler plate" operations, for example, the handling of state changes is going to pretty common, so, we'll put those into out base implementation...
public abstract class AbstractTrackModel implements TrackModel {
private State state;
private List<ChangeListener> changeListeners;
public AbstractTrackModel() {
state = State.STOPPED;
changeListeners = new ArrayList<>(8);
}
protected void setState(State state) {
if (this.state == state) {
return;
}
this.state = state;
fireStateDidChange();
}
#Override
public State getState() {
return state;
}
#Override
public void addChangeListener(ChangeListener changeListener) {
changeListeners.add(changeListener);
}
#Override
public void removeChangeListener(ChangeListener changeListener) {
changeListeners.remove(changeListener);
}
protected void fireStateDidChange() {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
listener.stateChanged(evt);
}
}
}
Next, we need a concrete implementation, so the following is a simple implementation that makes use of the javax.sound.sampled.AudioSystem and javax.sound.sampled.CLip APIs
public class AudioSystemClipTrackModel extends AbstractTrackModel {
private Clip clip;
private AudioInputStream stream;
public AudioSystemClipTrackModel(String named) throws UnsupportedAudioFileException, IOException, LineUnavailableException {
this.stream = AudioSystem.getAudioInputStream(getClass().getResource(named));
this.clip = AudioSystem.getClip();
}
protected AudioInputStream getStream() {
return stream;
}
#Override
public Clip getClip() {
return clip;
}
#Override
public boolean isOpen() {
return getClip().isOpen();
}
#Override
public void open() throws LineUnavailableException, IOException {
if (isOpen()) {
return;
}
getClip().open(getStream());
}
#Override
public void close() {
if (!isOpen()) {
return;
}
getClip().close();
}
#Override
public void play() throws IOException {
if (!isOpen()) {
throw new IOException("Track is not open");
}
clip.start();
setState(State.PLAYING);
}
#Override
public void stop() {
getClip().stop();
getClip().setFramePosition(0);
setState(State.STOPPED);
}
#Override
public void pause() {
getClip().stop();
setState(State.PAUSED);
}
}
Wow, we haven't even got to the UI yet! But the nice thing about this, is these classes will all work nicely without them.
Next, we look at the UI...
public class PlayerPane extends JPanel {
private TrackModel model;
private JLabel stateLabel;
private ChangeListener changeListener;
private PlayerControlsPane controlsPane;
public PlayerPane(TrackModel model) {
changeListener = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
configureState();
}
};
stateLabel = new JLabel("...");
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = gbc.REMAINDER;
controlsPane = new PlayerControlsPane(model);
add(stateLabel, gbc);
add(controlsPane, gbc);
setModel(model);
}
public void setModel(TrackModel model) {
if (this.model != null) {
this.model.removeChangeListener(changeListener);
}
this.model = model;
this.model.addChangeListener(changeListener);
configureState();
controlsPane.setModel(model);
}
public TrackModel getModel() {
return model;
}
protected void configureState() {
switch (getModel().getState()) {
case STOPPED:
stateLabel.setText("Stopped");
break;
case PLAYING:
stateLabel.setText("Playing");
break;
case PAUSED:
stateLabel.setText("Paused");
break;
}
}
}
public class PlayerControlsPane extends JPanel {
private TrackModel model;
private JButton playButton;
private JButton stopButton;
private JButton pauseButton;
private ChangeListener changeListener;
public PlayerControlsPane(TrackModel model) {
changeListener = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
configureState();
}
};
setLayout(new GridLayout(1, -1));
playButton = Style.makeButton();
playButton.setText("Play");
playButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
try {
model.play();
} catch (IOException ex) {
JOptionPane.showMessageDialog(PlayerControlsPane.this, "Track is not playable");
}
}
});
stopButton = new ButtonBuilder()
.withTitle("Stop")
.withActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
model.stop();
}
})
.build();
pauseButton = Style.makeButton(new PauseAction(model));
add(playButton);
add(stopButton);
add(pauseButton);
setModel(model);
}
public void setModel(TrackModel model) {
if (this.model != null) {
this.model.removeChangeListener(changeListener);
}
this.model = model;
this.model.addChangeListener(changeListener);
configureState();
}
public TrackModel getModel() {
return model;
}
protected void configureState() {
switch (getModel().getState()) {
case STOPPED:
playButton.setEnabled(true);
stopButton.setEnabled(false);
pauseButton.setEnabled(false);
break;
case PLAYING:
playButton.setEnabled(false);
stopButton.setEnabled(true);
pauseButton.setEnabled(true);
break;
case PAUSED:
playButton.setEnabled(true);
stopButton.setEnabled(false);
pauseButton.setEnabled(false);
break;
}
}
}
public class PauseAction extends AbstractAction {
private TrackModel model;
public PauseAction(TrackModel model) {
this.model = model;
putValue(NAME, "Pause");
// Bunch of other possible keys
}
#Override
public void actionPerformed(ActionEvent e) {
model.pause();
}
}
The UI is, for demonstration purposes, broken into three elements.
You have the primary player component, which contains a PlayerControlsPane and makes use of the PauseAction (again, for demonstration).
The important thing here is, the player and control components are making us of the same model. They each attach a "observer" to the model and update their states independently based on the changes to the model.
This plays into the concept of Dependency injection, for example and example.
It also allows the player and controls to have different layout managers, without a lot of juggling.
Now, do you need to do this with ever UI you make, no, but you should aim to do as many as possible, perfect practice makes perfect 😉
Parting comments...
Extending from a top level container, like JFrame is generally discouraged, you're not adding any new functionality to the class and you're locking yourself into a single use case. What happens if you want to put the player into a different container? Well, in your case, you can't, you're stuck.
This also supports Composition Over Inheritance, for reference...
Composition over inheritance
Prefer composition over inheritance?
... and I'm sure you can find lots more discussion on the subject.
And, this File filePath = new File("src/Tracks/Track.wav"); is going to be problematic. Never reference src in your code, it won't exist once the program is built and exported.
When the resource is embedded like this, you will need to use Class#getResource or Class#getResourceAsStream, depending on your API requirements - this is demonstrated in the code examples
You should probably also look at the Model–view–controller concept.
Runnable example...
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import static javax.swing.Action.NAME;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
JFrame frame = new JFrame();
TrackModel track = new AudioSystemClipTrackModel("/sound/your embedded audio file");
track.open();
frame.add(new PlayerPane(track));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (UnsupportedAudioFileException | IOException | LineUnavailableException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
}
public interface TrackModel {
enum State {
STOPPED, PLAYING, PAUSED;
}
public Clip getClip();
public State getState();
public void open() throws LineUnavailableException, IOException;
public void close();
public boolean isOpen();
public void play() throws IOException;
public void stop();
public void pause();
public void addChangeListener(ChangeListener changeListener);
public void removeChangeListener(ChangeListener changeListener);
}
public abstract class AbstractTrackModel implements TrackModel {
private State state;
private List<ChangeListener> changeListeners;
public AbstractTrackModel() {
state = State.STOPPED;
changeListeners = new ArrayList<>(8);
}
protected void setState(State state) {
if (this.state == state) {
return;
}
this.state = state;
fireStateDidChange();
}
#Override
public State getState() {
return state;
}
#Override
public void addChangeListener(ChangeListener changeListener) {
changeListeners.add(changeListener);
}
#Override
public void removeChangeListener(ChangeListener changeListener) {
changeListeners.remove(changeListener);
}
protected void fireStateDidChange() {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
listener.stateChanged(evt);
}
}
}
public class AudioSystemClipTrackModel extends AbstractTrackModel {
private Clip clip;
private AudioInputStream stream;
public AudioSystemClipTrackModel(String named) throws UnsupportedAudioFileException, IOException, LineUnavailableException {
this.stream = AudioSystem.getAudioInputStream(getClass().getResource(named));
this.clip = AudioSystem.getClip();
}
protected AudioInputStream getStream() {
return stream;
}
#Override
public Clip getClip() {
return clip;
}
#Override
public boolean isOpen() {
return getClip().isOpen();
}
#Override
public void open() throws LineUnavailableException, IOException {
if (isOpen()) {
return;
}
getClip().open(getStream());
}
#Override
public void close() {
if (!isOpen()) {
return;
}
getClip().close();
}
#Override
public void play() throws IOException {
if (!isOpen()) {
throw new IOException("Track is not open");
}
clip.start();
setState(State.PLAYING);
}
#Override
public void stop() {
getClip().stop();
getClip().setFramePosition(0);
setState(State.STOPPED);
}
#Override
public void pause() {
getClip().stop();
setState(State.PAUSED);
}
}
public class Style {
public static JButton makeButton() {
JButton btn = new JButton();
// Apply any configuration you need
// by default
return btn;
}
public static JButton makeButton(Action action) {
JButton btn = makeButton();
btn.setAction(action);
return btn;
}
}
public class ButtonBuilder {
private String title;
private ActionListener actionListener;
public ButtonBuilder withTitle(String title) {
this.title = title;
return this;
}
public ButtonBuilder withActionListener(ActionListener actionListener) {
this.actionListener = actionListener;
return this;
}
// And any additional properties you might like to configure
public JButton build() {
JButton btn = new JButton();
if (title != null) {
btn.setText(title);
}
if (actionListener != null) {
btn.addActionListener(actionListener);
}
// Configure the button as you see fit
return btn;
}
}
public class PlayerPane extends JPanel {
private TrackModel model;
private JLabel stateLabel;
private ChangeListener changeListener;
private PlayerControlsPane controlsPane;
public PlayerPane(TrackModel model) {
changeListener = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
configureState();
}
};
stateLabel = new JLabel("...");
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = gbc.REMAINDER;
controlsPane = new PlayerControlsPane(model);
add(stateLabel, gbc);
add(controlsPane, gbc);
setModel(model);
}
public void setModel(TrackModel model) {
if (this.model != null) {
this.model.removeChangeListener(changeListener);
}
this.model = model;
this.model.addChangeListener(changeListener);
configureState();
controlsPane.setModel(model);
}
public TrackModel getModel() {
return model;
}
protected void configureState() {
switch (getModel().getState()) {
case STOPPED:
stateLabel.setText("Stopped");
break;
case PLAYING:
stateLabel.setText("Playing");
break;
case PAUSED:
stateLabel.setText("Paused");
break;
}
}
}
public class PlayerControlsPane extends JPanel {
private TrackModel model;
private JButton playButton;
private JButton stopButton;
private JButton pauseButton;
private ChangeListener changeListener;
public PlayerControlsPane(TrackModel model) {
changeListener = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
configureState();
}
};
setLayout(new GridLayout(1, -1));
playButton = Style.makeButton();
playButton.setText("Play");
playButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
try {
model.play();
} catch (IOException ex) {
JOptionPane.showMessageDialog(PlayerControlsPane.this, "Track is not playable");
}
}
});
stopButton = new ButtonBuilder()
.withTitle("Stop")
.withActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
model.stop();
}
})
.build();
pauseButton = Style.makeButton(new PauseAction(model));
add(playButton);
add(stopButton);
add(pauseButton);
setModel(model);
}
public void setModel(TrackModel model) {
if (this.model != null) {
this.model.removeChangeListener(changeListener);
}
this.model = model;
this.model.addChangeListener(changeListener);
configureState();
}
public TrackModel getModel() {
return model;
}
protected void configureState() {
switch (getModel().getState()) {
case STOPPED:
playButton.setEnabled(true);
stopButton.setEnabled(false);
pauseButton.setEnabled(false);
break;
case PLAYING:
playButton.setEnabled(false);
stopButton.setEnabled(true);
pauseButton.setEnabled(true);
break;
case PAUSED:
playButton.setEnabled(true);
stopButton.setEnabled(false);
pauseButton.setEnabled(false);
break;
}
}
}
public class PauseAction extends AbstractAction {
private TrackModel model;
public PauseAction(TrackModel model) {
this.model = model;
putValue(NAME, "Pause");
// Bunch of other possible keys
}
#Override
public void actionPerformed(ActionEvent e) {
model.pause();
}
}
}
In my java application, my objective is to display or output an image using MVC architecture. My java application is comprised of an imagecontroller(main), imageview, and image model. I am currently able to select an image, the compiler acknowledges where the image has been selected from in the c: drive however it does not output or display the image. Here is a copy of my code below:
package imagecontroller;
import javax.swing.SwingUtilities;
import java.io.File;
public class ImageController {
private final ImageModel model;
private final ImageView view;
public ImageController () {
this.view = new ImageView(this);
this.model = new ImageModel(this.view);
public static void launch () {
new ImageController();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(ImageController::launch);
}
}
package imagecontroller;
import java.awt.Dimension;
import java.io.File;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.filechooser.FileNameExtensionFilter;
public class ImageView extends JFrame{
private final ImageController controller;
public ImageView(ImageController controller) {
this.controller = controller;
JFileChooser Chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter("JPG & PNG Images", "jpg", "png");
Chooser.addChoosableFileFilter(filter);
Chooser.setCurrentDirectory(new File(System.getProperty("user.home")));
Chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
int result
Chooser.showOpenDialog(null);
if (result == JFileChooser.APPROVE_OPTION) {
Chooser.setAcceptAllFileFilterUsed(true);
File selectedFile = Chooser.getSelectedFile();
System.out.println("Selected file: " + selectedFile.getAbsolutePath());
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setTitle("ImageAnnotator");
frame.setVisible(true);
frame.setSize(new Dimension (500,500));
}
}
}
package imagecontroller;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
public class ImageModel extends JComponent {
private final ImageView view;
private BufferedImage image;
public ImageModel(ImageView view) {
this.view = view;
}
public void CustomComponent (File png) {
BufferedImage image = null;
setPreferredSize(new Dimension(400, 400));
try {
this.image = ImageIO.read(new File("640px-Pleiades_large.png"));
} catch (IOException x) {
JOptionPane.showMessageDialog(null, "Not an ImageFile, Please Select an Image");
}
}
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
g = g.create();
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
int margin = 20;
int w = (getWidth() - (2 * margin + 2)) / 2;
int h = this.image.getHeight() * w / this.image.getWidth();
g.drawImage(image, h, h, WIDTH, HEIGHT, view);
}
}
The reason that the MVC pattern is called the MVC pattern is that the name suggests the order. In other words, create the model, then the view, then the controllers.
Here's an image display GUI I put together.
So, let's start with the model. For an image viewer GUI, the model is pretty simple.
public class ImageDisplayModel {
private BufferedImage image;
private File file;
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
}
The ImageDisplayModel class is a plain Java class that holds a BufferedImage and a File path. By saving the File path, the next time the user selects an image, the directory will be set to be the same as the last image.
The ImageDisplayModel class is an ordinary getter / setter class.
The next step is to create the view.
public class ImageDisplay implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new ImageDisplay());
}
private ImageDisplayModel model;
private ImagePanel imagePanel;
private JFrame frame;
public ImageDisplay() {
this.model = new ImageDisplayModel();
}
#Override
public void run() {
frame = new JFrame("Image Display");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setJMenuBar(createMenuBar());
imagePanel = new ImagePanel(model);
frame.add(imagePanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JMenuBar createMenuBar() {
JMenuBar menubar = new JMenuBar();
JMenu filemenu = new JMenu("File");
JMenuItem openitem = new JMenuItem("Open...");
openitem.addActionListener(new OpenFileListener(this, model));
filemenu.add(openitem);
menubar.add(filemenu);
return menubar;
}
public void updateImagePanel(int width, int height) {
imagePanel.setPreferredSize(width, height);
imagePanel.repaint();
frame.pack();
}
public JFrame getFrame() {
return frame;
}
}
We start the application with a call to the SwingUtilities invokeLater method. This method ensures that the Swing components are created and executed on the Event Dispatch Thread.
The view constructor instantiates the application model.
The JFrame code is in the run method.
The JMenuBar method allows us to select multiple images, one after the other.
The getFrame method allows the eventual controller class to access the JFrame instance. The updateImagePanel method allows the eventual controller class to update the image panel.
Next, we create the DrawingPanel class.
public class ImagePanel extends JPanel {
private static final long serialVersionUID = 1L;
private ImageDisplayModel model;
public ImagePanel(ImageDisplayModel model) {
this.model = model;
this.setPreferredSize(649, 480);
}
public void setPreferredSize(int width, int height) {
this.setPreferredSize(new Dimension(width, height));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
BufferedImage image = model.getImage();
if (image != null) {
g.drawImage(image, 0, 0, this);
}
}
}
Simple and straightforward. We draw the BufferedImage if one exists. We also adjust the size of the JPanel so that the image fits.
The only information that the DrawingPanel class needs is the model class. The drawing panel will draw the image. Period.
Finally, we create the controller class.
public class OpenFileListener implements ActionListener {
private ImageDisplay frame;
private ImageDisplayModel model;
public OpenFileListener(ImageDisplay frame, ImageDisplayModel model) {
this.frame = frame;
this.model = model;
}
#Override
public void actionPerformed(ActionEvent event) {
JFileChooser chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"JPG & PNG Images", "jpg", "png");
chooser.addChoosableFileFilter(filter);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
File file = model.getFile();
if (file != null) {
chooser.setCurrentDirectory(file);
}
int result = chooser.showOpenDialog(frame.getFrame());
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
model.setFile(selectedFile);
BufferedImage image;
try {
image = ImageIO.read(selectedFile);
model.setImage(image);
frame.updateImagePanel(image.getWidth(),
image.getHeight());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
The controller class updates the model information and instructs the view to repaint itself. The controller isn't concerned with the view internals. All the controller needs to know is that the view can update.
Finally, here's the complete runnable code. I made the classes inner classes so I could post this code as one block.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileNameExtensionFilter;
public class ImageDisplay implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new ImageDisplay());
}
private ImageDisplayModel model;
private ImagePanel imagePanel;
private JFrame frame;
public ImageDisplay() {
this.model = new ImageDisplayModel();
}
#Override
public void run() {
frame = new JFrame("Image Display");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setJMenuBar(createMenuBar());
imagePanel = new ImagePanel(model);
frame.add(imagePanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JMenuBar createMenuBar() {
JMenuBar menubar = new JMenuBar();
JMenu filemenu = new JMenu("File");
JMenuItem openitem = new JMenuItem("Open...");
openitem.addActionListener(new OpenFileListener(this, model));
filemenu.add(openitem);
menubar.add(filemenu);
return menubar;
}
public void updateImagePanel(int width, int height) {
imagePanel.setPreferredSize(width, height);
imagePanel.repaint();
frame.pack();
}
public JFrame getFrame() {
return frame;
}
public class ImagePanel extends JPanel {
private static final long serialVersionUID = 1L;
private ImageDisplayModel model;
public ImagePanel(ImageDisplayModel model) {
this.model = model;
this.setPreferredSize(649, 480);
}
public void setPreferredSize(int width, int height) {
this.setPreferredSize(new Dimension(width, height));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
BufferedImage image = model.getImage();
if (image != null) {
g.drawImage(image, 0, 0, this);
}
}
}
public class OpenFileListener implements ActionListener {
private ImageDisplay frame;
private ImageDisplayModel model;
public OpenFileListener(ImageDisplay frame, ImageDisplayModel model) {
this.frame = frame;
this.model = model;
}
#Override
public void actionPerformed(ActionEvent event) {
JFileChooser chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"JPG & PNG Images", "jpg", "png");
chooser.addChoosableFileFilter(filter);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
File file = model.getFile();
if (file != null) {
chooser.setCurrentDirectory(file);
}
int result = chooser.showOpenDialog(frame.getFrame());
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
model.setFile(selectedFile);
BufferedImage image;
try {
image = ImageIO.read(selectedFile);
model.setImage(image);
frame.updateImagePanel(image.getWidth(),
image.getHeight());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class ImageDisplayModel {
private BufferedImage image;
private File file;
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
}
}
I really found this interesting and thought to explore it a little more. Taking pointers from The MVC pattern and Swing (which I suggest you read especially the accepted answer) lets try have a go.
Explanation:
Brief Summary of the code to follow (based off of the above link)
The LoginView. The view is simply your UI components, with getter methods for any components needed by the LoginController as well as implementing a PropertyChangeListener which is used by the view to receive PropertyChangeEvents which could be fired by our LoginController or LoginModel.
The LoginController The controller has access to both the view and the model. The controller sets up the necessary events on the views components and reacts to them by validating input from the view and then asks the model to do its job and potentially that will change its state. The controller takes your views actions and interprets them. If you click on a button, it's the controller's job to figure out what that means and how the model should be manipulated based on that action.
The controller may also ask the view to change it does this by subscribing the view (which implements PropertyChangeListener) to its SwingPropertyChangeSupport. When the controller receives an action from the view, in this case the Login button pressed it validates inputs from the view and fires the necessary validation errors (if any). It then calls the model to do the actual validation.
The LoginModel The controller calls methods on the model which intern notifies the view when its state has changed. When something changes in the model, based either on some action you took (like clicking a button) the model notifies the view that its state has changed through the same mechanism as the controller.
The view can also ask the model for state. The view gets the state it displays directly from the model. For instance, if we pass in a model instance to our view which already has authenticated set to true, you will immediately receive the message to say you are logged in (this has not been demonstrated here, but would entail passing the model into the views constructor and after initializing the initView() doing something like onAuthenticatedPropertyChange(model.isAuthenticated())).
Some extra information is both controller and model implement the observer pattern (as in the view observes changes to the controller and models in order to react to them)
The controller also does all model execution on a background thread via a SwingWorker, and all property change listeners are fired back on the EDT
Here is the code which demonstrates the above:
TestApp.java:
import javax.swing.SwingUtilities;
public class TestApp {
public TestApp() {
createAndShowGUI();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(TestApp::new);
}
private void createAndShowGUI() {
LoginModel model = new LoginModel();
LoginView view = new LoginView();
LoginController controller = new LoginController(view, model);
}
}
LoginView.java:
import java.awt.Color;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.border.EmptyBorder;
public class LoginView implements PropertyChangeListener {
private JFrame frame;
private JPanel loginPanel;
private JTextField usernameTextField;
private JTextField passwordTextField;
private JLabel errorLabel;
private JButton loginButton;
public LoginView() {
initView();
}
private void initView() {
frame = new JFrame("TestApp");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
loginPanel = createLoginPanel();
frame.add(loginPanel);
frame.pack();
frame.setVisible(true);
}
private JPanel createLoginPanel() {
JPanel panel = new JPanel();
panel.setBorder(new EmptyBorder(20, 20, 20, 20));
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
JLabel usernameLabel = new JLabel("Username:");
usernameLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
usernameTextField = new JTextField();
usernameTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
usernameTextField.setColumns(20);
JLabel passwordLabel = new JLabel("Password:");
passwordLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
passwordTextField = new JTextField();
passwordTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
passwordTextField.setColumns(20);
loginButton = new JButton("Login");
loginButton.setAlignmentX(Component.CENTER_ALIGNMENT);
errorLabel = new JLabel();
errorLabel.setForeground(Color.RED);
errorLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
errorLabel.setVisible(false);
panel.add(usernameLabel);
panel.add(usernameTextField);
panel.add(passwordLabel);
panel.add(passwordTextField);
panel.add(errorLabel);
panel.add(loginButton);
return panel;
}
public JButton getLoginButton() {
return loginButton;
}
public JTextField getUsernameTextField() {
return usernameTextField;
}
public JTextField getPasswordTextField() {
return passwordTextField;
}
public JFrame getFrame() {
return frame;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
Object newValue = evt.getNewValue();
switch (propertyName) {
case "authenticated":
// authentication has finished lets do something
onAuthenticatedPropertyChange((Boolean) newValue);
break;
case "authenticating":
// lets disable the UI as we authenticate
errorLabel.setVisible(false);
disableLoginPanelComponents(true);
break;
case "usernameInvalid":
showError((String) newValue);
break;
case "passwordInvalid":
showError((String) newValue);
break;
}
}
private void onAuthenticatedPropertyChange(Boolean authenticated) {
if (authenticated == true) {
errorLabel.setVisible(false);
JOptionPane.showMessageDialog(frame, "You are in!");
} else {
showError("Invalid username or password!");
}
// re-enable components after authentication regadless of fail or pass
disableLoginPanelComponents(false);
}
private void showError(String error) {
errorLabel.setText(error);
errorLabel.setVisible(true);
}
private void disableLoginPanelComponents(boolean disable) {
for (int i = 0; i < loginPanel.getComponentCount(); i++) {
Component component = (Component) loginPanel.getComponent(i);
if (component instanceof JTextField || component instanceof JButton) {
((JComponent) loginPanel.getComponent(i)).setEnabled(!disable);
}
}
}
}
LoginController:
import java.awt.event.ActionEvent;
import javax.swing.SwingWorker;
import javax.swing.event.SwingPropertyChangeSupport;
public class LoginController {
private final SwingPropertyChangeSupport propertyChangeSupport;
private final LoginView view;
private final LoginModel model;
public LoginController(LoginView view, LoginModel model) {
this.view = view;
this.model = model;
propertyChangeSupport = new SwingPropertyChangeSupport(this, true);
initController();
}
private void initController() {
// make the view a listener of both the model and controller as both can send events which the view must react too
model.addPropertyChangeListener(view);
propertyChangeSupport.addPropertyChangeListener(view);
view.getLoginButton().addActionListener((ActionEvent e) -> {
// lets do some basic validation of username and password fields
String username = view.getUsernameTextField().getText();
String password = view.getPasswordTextField().getText();
// validate user input before sending it to the model (this is the contorllers job - besdoes business rules i..e username doesnt exist etc)
if (username.isEmpty()) {
propertyChangeSupport.firePropertyChange("usernameInvalid", null, "Username cannot be empty!");
return;
}
if (password.isEmpty()) {
propertyChangeSupport.firePropertyChange("passwordInvalid", null, "Password cannot be empty!");
return;
}
// call the model to login on a background thread as the model may query the db or hit an API etc
new SwingWorker<Void, Void>() {
#Override
protected Void doInBackground() throws Exception {
model.login(username, password);
return null;
}
}.execute();
});
}
}
LoginModel.java:
import java.beans.PropertyChangeListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.SwingPropertyChangeSupport;
public class LoginModel {
private final SwingPropertyChangeSupport propertyChangeSupport;
private boolean authenticated;
public LoginModel() {
propertyChangeSupport = new SwingPropertyChangeSupport(this, true); // we pass in true to noifty observers on the Event Dispatch Thread
}
public void login(String username, String password) {
propertyChangeSupport.firePropertyChange("authenticating", null, true);
// lets simulate query a database or something for 3 seconds
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
Logger.getLogger(LoginModel.class.getName()).log(Level.SEVERE, null, ex);
}
authenticated = username.equals("admin") && password.equals("password");
propertyChangeSupport.firePropertyChange("authenticated", null, authenticated);
}
public void addPropertyChangeListener(PropertyChangeListener prop) {
propertyChangeSupport.addPropertyChangeListener(prop);
}
public boolean isAuthenticated() {
return authenticated;
}
}
The above is generic and doesn't directly answer YOUR question but gives you an example of an MVC pattern in Swing.
Update:
Here is an example specific to your issue
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.filechooser.FileNameExtensionFilter;
public class TestApp {
public TestApp() {
createAndShowGUI();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(TestApp::new);
}
private void createAndShowGUI() {
DisplayImageModel model = new DisplayImageModel();
DisplayImageView view = new DisplayImageView();
DisplayImageController controller = new DisplayImageController(view, model);
}
class DisplayImageView implements PropertyChangeListener {
private JFrame frame;
private ImagePanel imagePanel;
private JLabel imageLabel;
private JButton pickImageButton;
public DisplayImageView() {
initView();
}
private void initView() {
frame = new JFrame("TestApp");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = createImagePanel();
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
private JPanel createImagePanel() {
JPanel panel = new JPanel();
panel.setBorder(new EmptyBorder(20, 20, 20, 20));
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
pickImageButton = new JButton("Pick an image");
pickImageButton.setAlignmentX(Component.CENTER_ALIGNMENT);
imagePanel = new ImagePanel();
imagePanel.getPanel().setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(imagePanel.getPanel());
panel.add(pickImageButton);
return panel;
}
public JButton getPickAnImageButton() {
return pickImageButton;
}
public JLabel getImageLabel() {
return imageLabel;
}
public JFrame getFrame() {
return frame;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
Object newValue = evt.getNewValue();
switch (propertyName) {
case "image":
onImagePropertyChange((BufferedImage) newValue);
break;
}
}
private void onImagePropertyChange(BufferedImage image) {
imagePanel.setImage(image);
frame.pack();
}
}
class DisplayImageModel {
private final SwingPropertyChangeSupport propertyChangeSupport;
private BufferedImage image;
public DisplayImageModel() {
propertyChangeSupport = new SwingPropertyChangeSupport(this, true); // we pass in true to noifty observers on the Event Dispatch Thread
}
public void setImage(BufferedImage image) {
this.image = image;
propertyChangeSupport.firePropertyChange("image", null, image);
}
public void addPropertyChangeListener(PropertyChangeListener prop) {
propertyChangeSupport.addPropertyChangeListener(prop);
}
public BufferedImage getImage() {
return image;
}
}
class DisplayImageController {
private final SwingPropertyChangeSupport propertyChangeSupport;
private final DisplayImageView view;
private final DisplayImageModel model;
public DisplayImageController(DisplayImageView view, DisplayImageModel model) {
this.view = view;
this.model = model;
propertyChangeSupport = new SwingPropertyChangeSupport(this, true);
initController();
}
private void initController() {
// make the view a listener of both the model and controller as both can send events which the view must react too
model.addPropertyChangeListener(view);
propertyChangeSupport.addPropertyChangeListener(view);
view.getPickAnImageButton().addActionListener((ActionEvent e) -> {
JFileChooser chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"JPG & PNG Images", "jpg", "png");
chooser.addChoosableFileFilter(filter);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
int result = chooser.showOpenDialog(view.getFrame());
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
BufferedImage image;
try {
image = ImageIO.read(selectedFile);
model.setImage(image);
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
}
}
class ImagePanel {
private final JPanel panel;
private BufferedImage image;
public ImagePanel() {
this.panel = new JPanel() {
#Override
public Dimension getPreferredSize() {
if (image != null) {
return new Dimension(image.getWidth(), image.getHeight());
} else {
return new Dimension(200, 200);
}
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (image != null) {
g.drawImage(image, 0, 0, this);
}
}
};
panel.setBorder(new LineBorder(Color.GRAY, 1));
}
public void setImage(BufferedImage image) {
this.image = image;
panel.revalidate();
panel.repaint();
}
public JPanel getPanel() {
return panel;
}
}
}
I've got a JFrame with two separate JPanels. One of the JPanels is filled with JButtons while the other has a couple of text fields. I added mouse listeners to the buttons via the JFrame and I want to make it so that when an event is fired from the first JPanel, the text fields in the second JPanel are changed. The two panels have their own classes. How would I go about doing this?
Use MVC, Model-View-Control, separation of concerns.
Have the Control, which holds your listeners, change the state of the model.
The Views -- your GUI's, have listeners added to them by the control, so that user input is transmitted to the control and thereby to the model.
The View can also either directly add listeners to the model so that they can change their display if the model changes, or often this is done indirectly through the control.
Don't add MouseListeners to JButtons. Use ActionListeners as that's what they're for. For example, if you disable a JButton, any ActionListeners attached to it won't work -- which is correct behavior. The same is not true for MouseListeners.
For more specific help, consider creating and posting a minimal example program.
Edit
For example:
import java.awt.GridLayout;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.SwingPropertyChangeSupport;
public class MvcMain {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
MvcView view = new MvcView();
MvcModel model = new MvcModel();
MvcControl control = new MvcControl(view, model);
view.createAndDisplay();
}
});
}
}
class MvcView {
private MvcModel model;
private ButtonPanel buttonPanel = new ButtonPanel();
private TextFieldPanel textFieldPanel = new TextFieldPanel();
private JPanel mainPanel = new JPanel();
public MvcModel getModel() {
return model;
}
public ButtonPanel getButtonPanel() {
return buttonPanel;
}
public TextFieldPanel getTextFieldPanel() {
return textFieldPanel;
}
public MvcView() {
mainPanel.setLayout(new GridLayout(0, 1));
mainPanel.add(textFieldPanel);
mainPanel.add(buttonPanel);
}
public void setModel(MvcModel model) {
this.model = model;
model.addPropertyChangeListener(new ModelListener());
}
public void createAndDisplay() {
JFrame frame = new JFrame("MVC Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(mainPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private class ModelListener implements PropertyChangeListener {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (ButtonTitle.class.getCanonicalName().equals(evt.getPropertyName())) {
ButtonTitle newValue = model.getButtonTitle();
textFieldPanel.textFieldSetText(newValue.getTitle());
}
}
}
}
enum ButtonTitle {
START("Start"), STOP("Stop"), PAUSE("Pause");
private String title;
private ButtonTitle(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
#SuppressWarnings("serial")
class ButtonPanel extends JPanel {
public ButtonPanel() {
setBorder(BorderFactory.createTitledBorder("Button Panel"));
setLayout(new GridLayout(1, 0, 5, 0));
for (ButtonTitle btnTitle : ButtonTitle.values()) {
add(new JButton(new ButtonAction(btnTitle)));
}
}
private class ButtonAction extends AbstractAction {
private ButtonTitle btnTitle;
public ButtonAction(ButtonTitle btnTitle) {
super(btnTitle.getTitle());
this.btnTitle = btnTitle;
}
public void actionPerformed(java.awt.event.ActionEvent e) {
Object oldValue = null;
ButtonTitle newValue = btnTitle;
ButtonPanel.this.firePropertyChange(
ButtonTitle.class.getCanonicalName(), oldValue, newValue);
};
}
}
#SuppressWarnings("serial")
class TextFieldPanel extends JPanel {
private JTextField textField = new JTextField(15);
public TextFieldPanel() {
setBorder(BorderFactory.createTitledBorder("TextField Panel"));
add(textField);
}
public void textFieldSetText(String text) {
textField.setText(text);
}
}
class MvcControl {
private MvcView view;
private MvcModel model;
public MvcControl(MvcView view, MvcModel model) {
this.view = view;
this.model = model;
view.setModel(model);
view.getButtonPanel().addPropertyChangeListener(new ButtonPanelListener());
}
private class ButtonPanelListener implements PropertyChangeListener {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (ButtonTitle.class.getCanonicalName().equals(evt.getPropertyName())) {
ButtonTitle newValue = (ButtonTitle) evt.getNewValue();
model.setButtonTitle(newValue);
}
}
}
}
class MvcModel {
private ButtonTitle buttonTitle;
private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(
this);
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(listener);
}
public ButtonTitle getButtonTitle() {
return buttonTitle;
}
public void setButtonTitle(ButtonTitle buttonTitle) {
ButtonTitle oldValue = this.buttonTitle;
ButtonTitle newValue = buttonTitle;
this.buttonTitle = buttonTitle;
pcSupport.firePropertyChange(ButtonTitle.class.getCanonicalName(),
oldValue, newValue);
}
}
The example is lacking in use of interfaces which would allow for a further separation of concerns resulting in looser coupling (a good thing).
Greetings,
I have been studying Obserser/Observable implementation to Model-View-Controller.
I may also misused the pattern but here's what I've done so far.
In my code. When Submit was press, it triggers the makeChange() of the Model.
But never triggers the update() of Testing.
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Observable;
import java.util.Observer;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
public class Testing implements Observer {
public Testing() {
model.addObserver(this);
loadListener1();
}
private void loadListener1() {
view1.submitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
model = new Model(view1.data1Field.getText(), view1.data2Field.getText());
model.makeChange();
model.notifyObservers();
}
});
}
public void update(Observable o, Object arg) { System.out.println("Notify - Observer"); }
public static void main(String[] a) { Testing testing = new Testing(); }
private View view1 = new View("1");
private Model model = new Model();
}
class View extends JFrame {
public View(String frame) {
super("Frame: " + frame);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300, 300);
setVisible(true);
setLayout(new GridLayout(3, 2));
add(data1Label); add(data1Field);
add(data2Label); add(data2Field);
add(submitButton); add(cancelButton);
}
private final JLabel data1Label = new JLabel("Data1");
private final JLabel data2Label = new JLabel("Data2");
public final JTextField data1Field = new JTextField();
public final JTextField data2Field = new JTextField();
public final JButton submitButton = new JButton("Submit");
public final JButton cancelButton = new JButton("Cancel");
}
class Model extends Observable {
public Model() { }
public Model(String data1, String data2) {
setData1(data1);
setData2(data2);
}
public String getData1() { return data1; }
public final void setData1(String data1) { this.data1 = data1; }
public String getData2() { return data2; }
public final void setData2(String data2) { this.data2 = data2; }
public void makeChange() {
setChanged();
notifyObservers();
System.out.println("Notify - Observable");
}
private String data1;
private String data2;
}
Can you guide me thru?
Your reply is highly appreciated.
Thanks,
Cyril H.
In constructor of clas Testing your register itself to instance of Model that is created during field initialisation.
In actionPerformed method of submit button you creatd a new instance of Model and later call on this new instance the method notifyObservers which has no registered observers!
Try this code:
private void loadListener1() {
view1.submitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
model.setData1(view1.data1Field.getText()),
model.setData2(view1.data2Field.getText());
model.makeChange();
}
});
}