Home > Net >  How do you seperate Java logic from Swing GUI into different files/packages?
How do you seperate Java logic from Swing GUI into different files/packages?

Time:05-26

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);
  }
}

CodePudding user response:

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.

CodePudding user response:

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 build 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...

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 managed 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

  • Related