1
\$\begingroup\$

The below code works as intended and I'm generally happy with it - the sliders each change an RGB value to modify the color of the circle in circlePanel. Likewise the diameter slider changes the size.

Could the code be refactored/restructured to make it better/more readable? The CircleSlider constructor is quite long - I'm wondering could some of it be removed to separate methods/classes.

CircleSlider.java

//import statements
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;

//CircleSlider class to setup GUI and main logic
public class CircleSlider extends JFrame{
    //instantiate GUI components
    private final JPanel circlePanel;
    private final JSlider diameterSlider, redSlider, greenSlider, blueSlider;
    private final JLabel diameterLabel, redLabel, greenLabel, blueLabel;
    private final JTextArea displayArea;
    private final GridBagLayout layout;
    private final GridBagConstraints constraints;

    //circle properties
    private int diameter = 75;

    private Color circleColor;

    //constructor to set up GUI
    public CircleSlider(){
        //call to JFrame constructor to set title
        super("Circle Slider");

        //create new layout
        layout = new GridBagLayout();
        constraints = new GridBagConstraints();
        setLayout(layout);

        //add calculation display area
        displayArea = new JTextArea(6,10);
        displayArea.setText("Adjust the diameter slider to see information on the circle");
        addComponent(displayArea, 14, 0, 20, 8, 1, 1,
                GridBagConstraints.BOTH, GridBagConstraints.CENTER);

        //add JPanel to house the circle
        circlePanel = new JPanel(){
            @Override
            protected void paintComponent(Graphics g){
                super.paintComponent(g);

                //set color of drawn graphics
                g.setColor(circleColor);
                //Draw circle in centre of circlePanel
                g.fillOval((circlePanel.getWidth() - diameter) / 2, (circlePanel.getHeight() - diameter) / 2,
                        diameter, diameter);
            }
        };
        addComponent(circlePanel, 1, 3, 16, 11, 1, 1,
                GridBagConstraints.BOTH, GridBagConstraints.CENTER);

        //add label for diameter slider
        diameterLabel = new JLabel("Diameter");
        addComponent(diameterLabel, 1, 1, 1, 1, 0, 1,
                GridBagConstraints.NONE, GridBagConstraints.WEST);

        //add the slider to control diameter of circle
        diameterSlider = new JSlider(0, 150, 50);
        //add listener as anonymous inner class
        diameterSlider.addChangeListener(
                new ChangeListener() {
                    @Override
                    public void stateChanged(ChangeEvent e) {
                        double radius = diameter / 2.0;
                        setDiameter(diameterSlider.getValue());
                        String message = String.format(
                                "Diameter: %d%nArea: %.2f%nCircumference: %.2f",
                                diameter, getArea(radius), getCircumference(radius));
                        displayArea.setText(message);
                    }
                }
        );
        addComponent(diameterSlider, 2, 1, 1, 1, 0, 1, GridBagConstraints.NONE,
                GridBagConstraints.WEST);

        //create listener for color sliders
        SliderListener sliderListener = new SliderListener();

        //add redSlider and label
        redLabel = new JLabel("RGB: Red");
        addComponent(redLabel, 4, 1, 1, 1, 0, 1, GridBagConstraints.NONE,
                GridBagConstraints.WEST);

        redSlider = new JSlider(0, 255, 0);
        redSlider.addChangeListener(sliderListener);
        addComponent(redSlider, 5, 1, 1, 1, 0, 1, GridBagConstraints.NONE,
                GridBagConstraints.WEST);

        //add greenSlider and label
        greenLabel = new JLabel("RGB: Green");
        addComponent(greenLabel, 7, 1, 1, 1, 0, 1, GridBagConstraints.NONE,
                GridBagConstraints.WEST);

        greenSlider = new JSlider(0, 255, 0);
        greenSlider.addChangeListener(sliderListener);
        addComponent(greenSlider, 8, 1, 1, 1, 0, 1, GridBagConstraints.NONE,
                GridBagConstraints.WEST);

        //add blueSlider and label
        blueLabel = new JLabel("RGB: Blue");
        addComponent(blueLabel, 11, 1, 1, 1, 0, 1, GridBagConstraints.NONE,
                GridBagConstraints.WEST);

        blueSlider = new JSlider(0, 255, 0);
        blueSlider.addChangeListener(sliderListener);
        addComponent(blueSlider, 12, 1, 1, 1, 0, 1, GridBagConstraints.NONE,
                GridBagConstraints.WEST);
    }

    //ChangeListener for color sliders - implemented as private inner class as
    //all color sliders perform same method, setColor()
    class SliderListener implements ChangeListener{
        @Override
        public void stateChanged(ChangeEvent e){
            setColor(redSlider.getValue(), greenSlider.getValue(), blueSlider.getValue());
        }
    }

    //helper method to add a component to JPanel or JFrame
    private void addComponent(Component component, int row, int column, int width, int height,
                              int weightx, int weighty, int fillConstraint, int anchorConstraint) {
        constraints.gridx = column;
        constraints.gridy = row;
        constraints.gridwidth = width;
        constraints.gridheight = height;
        constraints.weightx = weightx;
        constraints.weighty = weighty;
        constraints.fill = fillConstraint;
        constraints.anchor = anchorConstraint;
        layout.setConstraints(component, constraints);
        add(component);
    }

    //set the diameter of the circle
    private void setDiameter(int newDiameter){
        //if new diameter is negative, set to 10
        diameter = newDiameter >= 0 ? newDiameter : 10;
        repaint();
    }

    //set the color of the circle
    private void setColor(int newRed, int newGreen, int newBlue){
        circleColor = new Color(
                //if any given color is negative, set to 10
                newRed >= 0 ? newRed : 10,
                newGreen >= 0 ? newGreen : 10,
                newBlue >= 0 ? newBlue : 10);
        getGraphics().setColor(circleColor);
        repaint();
    }

    //Find the area of a circle given the radius
    private double getArea(double radius){
        return Math.PI * Math.pow((radius), 2);
    }

    //find circumference of circle given the radius
    private double getCircumference(double radius){
        return 2 * Math.PI * radius;
    }
}

CircleSliderTest.java

import javax.swing.*;

public class CircleSliderTest {
    public static void main(String[] args){
        CircleSlider app = new CircleSlider();

        app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        app.setSize(600,400);
        app.setVisible(true);
    }
}
\$\endgroup\$
0
\$\begingroup\$

1) Is your CircleSlider really a window? Your code says yes, but as a user, I would find that quite strange. Your CircleSlider should use JFrame as an instance. You can see that clearly in your main method. You are using three methods from it. What about the rest? JFrame has probably 30 methods or more. Can I call all of them without destroying your CircleSlider class? Do I have to call the methods you provided or can I leave them untouched? Should I be able to call them?
- Probably not. Define your own methods.

2) I guess you are writing this because you feel like it would waste too many lines otherwise, right?

private final JSlider diameterSlider, redSlider, greenSlider, blueSlider;

The problem is not the waste of lines, but the number of instance variables you need for your class. That's your state and your state consists of 14 variables.
Personally, I would try to stay below 5. It makes reasoning much easier, it makes it more testable, it keeps your class smaller and you could probably initialize the full state through the constructor - to name a few advantages.

3)

//set the diameter of the circle
private void setDiameter(int newDiameter){
    //if new diameter is negative, set to 10
    diameter = newDiameter >= 0 ? newDiameter : 10;
    repaint();
}

If you already write something like "This method does X with Y" then the method should be a member of Y and Y should be a class. In your case, make a circle class and put the methods with these comments inside it.

4) Yes, your constructor is really long. It seems like your application has different parts, but all of them are inside your CircleSlider class. You should create new classes for these parts, for example, a class containing the sliders for your circle (CircleAdjustment could be a name for it).

5)

addComponent(greenLabel, 7, 1, 1, 1, 0, 1, GridBagConstraints.NONE, GridBagConstraints.WEST);

It's good that you are trying to help yourself with additional methods, but this one is quite difficult to read. I could easily swap some numbers and without looking at your method I wouldn't even dare to guess what these numbers mean. You could use additional classes to get around this:

    // instead of:
    constraints.gridx = column;
    constraints.gridy = row;
    constraints.gridwidth = width;
    constraints.gridheight = height;
    // something like this:
    new Grid(column, row, width, height)

6) All these numbers you are using... ask yourself, do they depend on each other? I am sure some of them do because you are a grid-based layout and if you enlarge one field, it would influence another. This should be visible in your code. The easiest approach (not best) would be to make constants at the beginning of the class which depends on each other:

private static final int RED_SLIDER_Y = 1;
private static final int GREEN_SLIDER_Y = RED_SLIDER_Y +1;
// ...

EDIT:

Here is an example of how an Adjustment class and some others around it could look like. I didn't implement the layout stuff completely, but the rest should be ok. If you've further questions, I am here.

/**
 * Offers a convenient way to initialize a slider. Note that it is final like
 * the other classes. If you don't design your class to be inherited from, make
 * it final. Careless inheritance is dangerous and leads to bad code and bugs.
 */
public final class SliderValues {
    private final int min;
    private final int max;
    private final int value;

    /**
     * Secondary constructor. It's purpose is to offer another way to construct
     * the object. It's important that every secondary constructor has
     * only a single statement consisting of "this(...)" which calls the
     * primary constructor. Only the primary constructor sets the fields.
     */
    public SliderValues(int min, int max) {
        this(min, max, 0);
    }

    /**
     * Primary constructor. Ideally this is how a primary constructor should
     * look like: Just a simple setup without methods or calculations.
     */
    public SliderValues(int min, int max, int value) {
        this.min = min;
        this.max = max;
        this.value = value;
    }

    /**
     * Yes, you could also have just three getters, but I prefer this option,
     * because the purpose of this class is to initialize a slider and that's
     * what this method does.
     * This class is not a data container.
     */
    public JSlider initialized(JSlider slider) {
        slider.setMinimum(min);
        slider.setMaximum(max);
        slider.setValue(value);
        return slider;
    }
}

public final class LabeledSlider {
    private final JPanel panel;
    private final JSlider slider;
    private final JLabel label;

    /**
     * A secondary constructor again for the sake of convenience.
     */
    public LabeledSlider(String text, int min, int max, ChangeListener listener) {
        this(
            text,
            new SliderValues(min, max),
            listener
        );
    }

    /**
     * Why didn't I just use a constructor with five values instead of the
     * SliderValues class? First, three integers in a row are risky for the
     * client. He could mistaken the order. Additionally, I don't want more than
     * 4 parameters per method/constructor.
     * @param listener I replaced the ChangeListener for the Consumer. The advantage
     *  is that the caller doesn't need to somehow get the value of the slider
     *  but instead he just gets it from the slider. Additionally, the slider
     *  can hold its encapsulation and doesn't need to provide a getValue()
     *  method.
     */
    public LabeledSlider(String text, SliderValues values, Consumer<Integer> listener) {
        label = new JLabel(text);
        slider = values.initialized(new JSlider());
        slider.addChangeListener(
            e -> listener.accept(slider.getValue())
        );
        panel = new JPanel(new BorderLayout());
        panel.add(label, BorderLayout.NORTH);
        panel.add(slider, BorderLayout.SOUTH);
    }

    /**
     * Note that I am not inheriting from any class, because there is exactly
     * one thing I want this class to do: Being attached on a container.
     * Inheriting from Container would give me this possibility - and 30 other
     * methods I don't want. This is a neat alternative using only one method.
     */
    public void addOn(Container container) {
        container.add(panel);
    }
}

public final class Adjustment {
    private final JPanel panel;

    public Adjustment(Circle circle) {
        panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        new LabeledSlider(
            "Diameter",
            new SliderValues(0, 150, 50),
            /*
            I don't know if you've used the other syntax on purpose, but if not,
            this is a way to shorten your ChangeListener creation. It's called
            "lambda" and is usable on Interfaces with only one method
            (ChangeListener is such an Interface).

            Note that this isn't the best application for a lambda. Normally
            you use it if you have short code around 4 lines or less, because
            it tends to hurt readability if it's longer.
             */
            e -> {
                double radius = diameter / 2.0;
                setDiameter(diameterSlider.getValue());
                String message = String.format(
                    "Diameter: %d%nArea: %.2f%nCircumference: %.2f",
                    diameter, getArea(radius), getCircumference(radius));
                displayArea.setText(message);
            }
        ).addOn(panel);
        new LabeledSlider(
            "RGB: Red",
            0,
            255,
            value -> circle.red(value)
        ).addOn(panel);
        new LabeledSlider(
            "RGB: Green",
            0,
            255,
            circle::green // alternative syntax for the lambda called "method reference".
            // If you don't understand it, just use the normal one.

            // It works only if your lambda provides the same amount and types
            // of values as the the method you're referencing takes.
            // In this case, green takes one argument (integer) and you get
            // one integer from the lambda.
        ).addOn(panel);
        // ...
    }

    public void addOn(Container container) {
        container.add(panel);
    }
}
\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer. The application now just creates an instance of JFrame instead of extending it. I've also created a Circle class and moved any of the logic relating to the appearance of the circle to that class. I'm confused about how to move any of the other parts to separate classes. For instance, for the CircleAdjustment class containing the sliders, how would I add these to the JFrame? Would the CircleAdjustment class implement the changeListener for the JSliders? \$\endgroup\$ – colinhowlin Nov 14 '18 at 15:00
  • \$\begingroup\$ edit I added some code to show how the Adjustment class could look like. Note that I am using different panels and layouts because the classes represent their own area on the window and thus they should've their own panel and their own layout. \$\endgroup\$ – Synth Nov 14 '18 at 23:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.