javaswingjlabeljcheckboxjradiobutton

Change JLabel based on what user choose


I am task to do a simple order system. I want the JLabel (Amount: $0.00) show the corresponding amount on what the user picks for his burger and condiments. For example, if the user click on beef, the label will change into "Amount: $4.00", and when he choose a condiment, it will add $0.50 to the total based on how many condiments he picks and vice versa. Also, when the user unchecks a condiment (JCheckBox), it will deduct $0.50 dollars to the total.

enter image description here

My code for beef JRadioButton:

private void beef_radioBtnActionPerformed(java.awt.event.ActionEvent evt) {
    total_amount.setText("Amount: $4.00");
    ketchup_Checkbox.setEnabled(true);
    mustard_Checkbox.setEnabled(true);
    pickles_Checkbox.setEnabled(true);
}  

Code for ketchup JCheckBox:

private void ketchup_CheckboxActionPerformed(java.awt.event.ActionEvent evt) {                                                 

    float condiments_amount = (float) 0.50;
    float beef_amount = (float) 4.00;
    float total;
    if (beef_radioBtn.isSelected()){
        total = beef_amount + condiments_amount;
        total_amount.setText("Amount: $" + decimal.format(total));
        if (!ketchup_Checkbox.isSelected()){
            total_amount.setText("Amount: $" + decimal.format(4.50 - condiments_amount)); 
        }
        else if (mustard_Checkbox.isSelected()){
            total_amount.setText("Amount: $" + decimal.format(4.50 + condiments_amount));
        } 
        else if (pickles_Checkbox.isSelected()){
            total_amount.setText("Amount: $" + decimal.format(4.50 + condiments_amount));
        }
    }
 }

Solution

  • Okay, buckle up, this is going to be a bit of ride.

    One of the most powerful concepts you have available to you is the concept of "model". A model is just something that "models" something and is a way to seperate different areas of your program. So a model, models the data (think of it like a container) and the view will then use those models to format the data to the user (separation of concerns). A model may also contain business logic or perform calculations depending on its requirements.

    The allows you to centralise concepts so you don't end up repeating yourself or forgetting to do things. It's also a way to change how parts of the program work, also known as "delegation"

    Starting point, some interfaces

    Well, that's a lot of "blah", so let's get started. I prefer to use interfaces to describe things, it provides a lot of freedom, as you can put different interfaces together to suit different requirements.

    The Menu

    Okay, simple concept, this will be a list of items which are available to sell

    public interface Menu {
        public List<MainMenuItem> getMenuItems();
    }
    

    Menu items

    A description of a menu item, pretty basic

    public interface MenuItem {
        public String getDescription();
        public double getPrice();
    }
    

    "Main" menu items

    These are all the "top level", "stand alone" menu items and in our case, can have condiments :D

    public interface MainMenuItem extends MenuItem {
        public List<Condiment> getCondiments();
    }
    

    Condiments

    Condiments are a "special" MenuItem, as they are associated with a MainMenuItem

    public interface Condiment extends MenuItem {
    }
    

    Burgers

    This is just a demonstration of some of the things you could do, Burger isn't anything special, but as you will see, we can use this concept to do different things

    public interface Burger extends MainMenuItem {
    }
    

    Order

    And finally, the "order", what have we ordered and what condiments do we want with it

    public interface Order {
        public MainMenuItem getItem();
        public void setItem(MainMenuItem item);
        public List<Condiment> getCondiments();
        public void addCondiment(Condiment condiment);
        public void removeCondiment(Condiment condiment);
        public double getTally();
    }
    

    This is a good demonstration of the power of the model. The Order has a getTally method, which is used to calculate what is owed. Different implementations of the model might apply different calculations, like tax or discounts

    Implementations

    Okay, since you're probably aware, we can't create an instance of a interface, we need some "default" implementations to work with...

    public class DefaultOrder implements Order {
    
        private MainMenuItem item;
        private List<Condiment> condiments = new ArrayList<>();
    
        @Override
        public MainMenuItem getItem() {
            return item;
        }
    
        @Override
        public List<Condiment> getCondiments() {
            return Collections.unmodifiableList(condiments);
        }
    
        @Override
        public double getTally() {
            double tally = 0;
            if (item != null) {
                tally += item.getPrice();
            }
            for (Condiment condiment : condiments) {
                tally += condiment.getPrice();
            }
            return tally;
        }
    
        @Override
        public void setItem(MainMenuItem item) {
            this.item = item;
            // Oh look, we've established a "rule" that this model
            // applies, by itself, sweet
            condiments.clear();
        }
    
        @Override
        public void addCondiment(Condiment condiment) {
            // Bit pointless if the menu item is not set
            if (item == null) {
                return;
            }
            // Probably should check for duplicates
            condiments.add(condiment);
        }
    
        @Override
        public void removeCondiment(Condiment condiment) {
            // Bit pointless if the menu item is not set
            if (item == null) {
                return;
            }
            condiments.remove(condiment);
        }
    
    }
    
    public class DefaultMenu implements Menu {
    
        private List<MainMenuItem> menuItems = new ArrayList<>();
    
        public void add(MainMenuItem menuItem) {
            menuItems.add(menuItem);
        }
    
        @Override
        public List<MainMenuItem> getMenuItems() {
            return Collections.unmodifiableList(menuItems);
        }
    
    }
    
    public abstract class AbstractMenuItem implements MenuItem {
    
        private String description;
        private double price;
    
        public AbstractMenuItem(String description, double price) {
            this.description = description;
            this.price = price;
        }
    
        @Override
        public String getDescription() {
            return description;
        }
    
        @Override
        public double getPrice() {
            return price;
        }
    
    }
    
    public class DefaultCondiment extends AbstractMenuItem implements Condiment {
    
        public DefaultCondiment(String description, double price) {
            super(description, price);
        }
    
    }
    
    public class DefaultBurger extends AbstractMenuItem implements Burger {
    
        private List<Condiment> condiments;
    
        public DefaultBurger(String description, double price, List<Condiment> condiments) {
            super(description, price);
            // Protect ourselves from external modifications
            this.condiments = new ArrayList<>(condiments);
        }
    
        @Override
        public List<Condiment> getCondiments() {
            return Collections.unmodifiableList(condiments);
        }
    
    }
    

    Okay, try not to get too caught up in this, but have a look at the use of abstract here. AbstractMenuItem encapsulates a lot of the "common" functionality that all MenuItem implementations are going to need, so we don't need to repeat ourselves, sweet.

    Some of these implementations are already making decisions or applying rules. For example, the DefaultOrder will clear the condiments when ever the MainMenuItem is changed. It could also make sure that the condiment which is been applied is actually available fo this item.

    Also note, the tally method is not a stored property, but is re-calculated every time you call it. This is design decision, it wouldn't be hard to make it a stored property instead, so each time you change the MenuMenuItem, add and/or remove condiments, the property was updated, but I'm feeling lazy. But you can see how these things can be changed, and it will effect ALL users of these models, sweet :D

    Okay, but how does this actually answer the question? Well, quite a bit actually.

    So, the idea is, you start with a blank Order. The user selects a "main item" (ie a burger), you set this to the Order and then you update the UI in response. The UI asks the Order to calculate the tally and presents that to the user.

    More over, the same concept works for condiments as well. Each time a condiment is added or removed by the user, the Order is updated and you update the UI.

    Okay, but maybe, it's a little easier to understand with an example...

    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.text.NumberFormat;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import javax.swing.ButtonGroup;
    import javax.swing.JButton;
    import javax.swing.JCheckBox;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JRadioButton;
    import javax.swing.border.EmptyBorder;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
    
                    List<Condiment> condiments = new ArrayList<>(3);
                    condiments.add(new DefaultCondiment("Ketchup", 0.5));
                    condiments.add(new DefaultCondiment("Mustard", 0.5));
                    condiments.add(new DefaultCondiment("Pickles", 0.5));
    
                    DefaultMenu menu = new DefaultMenu();
                    menu.add(new DefaultBurger("Beef", 4.0, condiments));
                    menu.add(new DefaultBurger("Chicken", 3.5, condiments));
                    menu.add(new DefaultBurger("Veggie", 4.0, condiments));
    
                    MenuPane menuPane = new MenuPane();
                    menuPane.setMenu(menu);
    
                    frame.add(menuPane);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class MenuPane extends JPanel {
    
            private Menu menu;
            private Order order;
            private List<Condiment> selectedCondiments = new ArrayList<>();
    
            private JPanel burgerPanel;
            private JPanel condimentPanel;
            private JPanel totalPanel;
    
            private JLabel totalLabel;
            private JButton clearButton;
            private JButton payButton;
    
            private NumberFormat currencyFormatter;
    
            public MenuPane() {
                setLayout(new GridBagLayout());
    
                order = new DefaultOrder();
    
                burgerPanel = new JPanel();
                burgerPanel.setBorder(new EmptyBorder(8, 8, 8, 8));
                condimentPanel = new JPanel();
                condimentPanel.setBorder(new EmptyBorder(8, 8, 8, 8));
                totalPanel = makeTotalPanel();
                totalPanel.setBorder(new EmptyBorder(8, 8, 8, 8));
    
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.gridx = 0;
                gbc.gridy = 0;
                gbc.fill = GridBagConstraints.BOTH;
                gbc.weightx = 1;
                gbc.weighty = 0.5;
    
                add(burgerPanel, gbc);
    
                gbc.gridy++;
                add(condimentPanel, gbc);
    
                gbc.gridy++;
                gbc.weighty = 0;
                gbc.fill = GridBagConstraints.HORIZONTAL;
                add(totalPanel, gbc);
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(400, 200);
            }
    
            protected NumberFormat getCurrentFormatter() {
                if (currencyFormatter != null) {
                    return currencyFormatter;
                }
                currencyFormatter = NumberFormat.getCurrencyInstance();
                currencyFormatter.setMinimumFractionDigits(2);
                return currencyFormatter;
            }
    
            protected JPanel makeTotalPanel() {
                JPanel totalPanel = new JPanel(new GridBagLayout());
                totalLabel = new JLabel();
                clearButton = new JButton("CLR");
                payButton = new JButton("PAY");
    
                clearButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        order = new DefaultOrder();
                        buildCondiments();
                        orderDidChange();
                    }
                });
    
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.weightx = 1;
                gbc.gridx = 0;
                gbc.gridy = 0;
                totalPanel.add(totalLabel, gbc);
    
                gbc.weightx = 0;
                gbc.gridx++;
                totalPanel.add(clearButton, gbc);
                gbc.gridx++;
                totalPanel.add(payButton, gbc);
                return totalPanel;
            }
    
            protected void buildBurgerMenu() {
                burgerPanel.removeAll();
                burgerPanel.setLayout(new GridBagLayout());
                if (menu == null) {
                    return;
                }
    
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.gridwidth = GridBagConstraints.REMAINDER;
                gbc.anchor = GridBagConstraints.NORTHWEST;
                gbc.weightx = 1;
    
                ButtonGroup bg = new ButtonGroup();
    
                // Stick with me, this is a little more advanced, but provides
                // a really nice concept and ease of use
                // We could also make use of the Action API, but that might
                // pushing you just a little to far ;)
                ActionListener actionListener = new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (!(e.getSource() instanceof JComponent)) {
                            return;
                        }
                        JComponent comp = (JComponent) e.getSource();
                        Object obj = comp.getClientProperty("MenuItem");
                        // I'm putting this here to demonstrate part of the concept
                        // of polymorphism - techncially, we don't have to care
                        // of it's a burger or some other type of menu item,
                        // only that this is going to represent the "main" item
                        if (!(obj instanceof MainMenuItem)) {
                            return;
                        }
                        MainMenuItem item = (MainMenuItem) obj;
                        order.setItem(item);
    
                        buildCondiments();
                        orderDidChange();
                    }
                };
    
                System.out.println(menu.getMenuItems().size());
                for (MenuItem item : menu.getMenuItems()) {
                    // Only interested in burgers
                    // Could have the Menu do this, but that's a design
                    // decision
                    if (!(item instanceof Burger)) {
                        continue;
                    }
                    Burger burger = (Burger) item;
                    JRadioButton btn = new JRadioButton(burger.getDescription() + " (" + getCurrentFormatter().format(burger.getPrice()) + ")");
                    // Ok, this is just a little cheeky, but we're associating the
                    // butger with the button for simplicity
                    btn.putClientProperty("MenuItem", burger);
                    bg.add(btn);
                    // Add all the buttons share the same listener, because of polymorphism :D
                    btn.addActionListener(actionListener);
    
                    burgerPanel.add(btn, gbc);
                }
            }
    
            protected void buildCondiments() {
                condimentPanel.removeAll();
                condimentPanel.setLayout(new GridBagLayout());
                if (menu == null || order.getItem() == null) {
                    return;
                }
    
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.gridwidth = GridBagConstraints.REMAINDER;
                gbc.anchor = GridBagConstraints.NORTHWEST;
                gbc.weightx = 1;
    
                // Stick with me, this is a little more advanced, but provides
                // a really nice concept and ease of use
                // We could also make use of the Action API, but that might
                // pushing you just a little to far ;)
                ActionListener actionListener = new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (!(e.getSource() instanceof JCheckBox)) {
                            return;
                        }
                        JCheckBox checkBox = (JCheckBox) e.getSource();
                        Object obj = checkBox.getClientProperty("Condiment");
                        if (!(obj instanceof Condiment)) {
                            return;
                        }
                        Condiment condiment = (Condiment) obj;
                        if (checkBox.isSelected()) {
                            order.addCondiment(condiment);
                        } else {
                            order.removeCondiment(condiment);
                        }
                        orderDidChange();
                    }
                };
    
                for (Condiment condiment : order.getItem().getCondiments()) {
                    JCheckBox btn = new JCheckBox(condiment.getDescription() + " (" + getCurrentFormatter().format(condiment.getPrice()) + ")");
                    // Ok, this is just a little cheeky, but we're associating the
                    // butger with the button for simplicity
                    btn.putClientProperty("Condiment", condiment);
                    // Add all the buttons share the same listener, because of polymorphism :D
                    btn.addActionListener(actionListener);
                    condimentPanel.add(btn, gbc);
                }
            }
    
            public Menu getMenu() {
                return menu;
            }
    
            public void setMenu(Menu menu) {
                this.menu = menu;
                order = new DefaultOrder();
                buildBurgerMenu();
                orderDidChange();
            }
    
            protected void orderDidChange() {
                if (order == null) {
                    totalLabel.setText("Amount: " + getCurrentFormatter().format(0));
                    return;
                }
    
                // And now, some magic, how easy is it to get the expected
                // tally amount!!
                totalLabel.setText("Amount: " + getCurrentFormatter().format(order.getTally()));
            }
    
        }
    
        public interface Menu {
            public List<MainMenuItem> getMenuItems();
        }
    
        public interface MenuItem {
            public String getDescription();
            public double getPrice();
        }
    
        public interface Condiment extends MenuItem {
        }
    
        public interface MainMenuItem extends MenuItem {
            public List<Condiment> getCondiments();
        }
    
        public interface Burger extends MainMenuItem {
        }
    
        public interface Order {
            public MainMenuItem getItem();
            public void setItem(MainMenuItem item);
            public List<Condiment> getCondiments();
            public void addCondiment(Condiment condiment);
            public void removeCondiment(Condiment condiment);
            public double getTally();
        }
    
        public class DefaultOrder implements Order {
    
            private MainMenuItem item;
            private List<Condiment> condiments = new ArrayList<>();
    
            @Override
            public MainMenuItem getItem() {
                return item;
            }
    
            @Override
            public List<Condiment> getCondiments() {
                return Collections.unmodifiableList(condiments);
            }
    
            @Override
            public double getTally() {
                double tally = 0;
                if (item != null) {
                    tally += item.getPrice();
                }
                for (Condiment condiment : condiments) {
                    tally += condiment.getPrice();
                }
                return tally;
            }
    
            @Override
            public void setItem(MainMenuItem item) {
                this.item = item;
                // Oh look, we've established a "rule" that this model
                // applies, by itself, sweet
                condiments.clear();
            }
    
            @Override
            public void addCondiment(Condiment condiment) {
                // Bit pointless if the menu item is not set
                if (item == null) {
                    return;
                }
                // Probably should check for duplicates
                condiments.add(condiment);
            }
    
            @Override
            public void removeCondiment(Condiment condiment) {
                // Bit pointless if the menu item is not set
                if (item == null) {
                    return;
                }
                condiments.remove(condiment);
            }
    
        }
    
        public class DefaultMenu implements Menu {
    
            private List<MainMenuItem> menuItems = new ArrayList<>();
    
            public void add(MainMenuItem menuItem) {
                menuItems.add(menuItem);
            }
    
            @Override
            public List<MainMenuItem> getMenuItems() {
                return Collections.unmodifiableList(menuItems);
            }
    
        }
    
        public abstract class AbstractMenuItem implements MenuItem {
    
            private String description;
            private double price;
    
            public AbstractMenuItem(String description, double price) {
                this.description = description;
                this.price = price;
            }
    
            @Override
            public String getDescription() {
                return description;
            }
    
            @Override
            public double getPrice() {
                return price;
            }
    
        }
    
        public class DefaultCondiment extends AbstractMenuItem implements Condiment {
    
            public DefaultCondiment(String description, double price) {
                super(description, price);
            }
    
        }
    
        public class DefaultBurger extends AbstractMenuItem implements Burger {
    
            private List<Condiment> condiments;
    
            public DefaultBurger(String description, double price, List<Condiment> condiments) {
                super(description, price);
                // Protect ourselves from external modifications
                this.condiments = new ArrayList<>(condiments);
            }
    
            @Override
            public List<Condiment> getCondiments() {
                return Collections.unmodifiableList(condiments);
            }
    
        }
    }
    

    Okay, that's a lot to take in. Lets take a closer look at the buildBurgerMenu method. This gets called when ever the main menu is changed.

    Pay close attention to the actionListener used in this method, there's only one and it's shared by all the buttons

    protected void buildBurgerMenu() {
        burgerPanel.removeAll();
        burgerPanel.setLayout(new GridBagLayout());
        if (menu == null) {
            return;
        }
    
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.anchor = GridBagConstraints.NORTHWEST;
        gbc.weightx = 1;
    
        ButtonGroup bg = new ButtonGroup();
    
        // Stick with me, this is a little more advanced, but provides
        // a really nice concept and ease of use
        // We could also make use of the Action API, but that might
        // pushing you just a little to far ;)
        ActionListener actionListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!(e.getSource() instanceof JComponent)) {
                    return;
                }
                JComponent comp = (JComponent) e.getSource();
                Object obj = comp.getClientProperty("MenuItem");
                // I'm putting this here to demonstrate part of the concept
                // of polymorphism - techncially, we don't have to care
                // of it's a burger or some other type of menu item,
                // only that this is going to represent the "main" item
                if (!(obj instanceof MainMenuItem)) {
                    return;
                }
                MainMenuItem item = (MainMenuItem) obj;
                order.setItem(item);
    
                buildCondiments();
                orderDidChange();
            }
        };
    
        System.out.println(menu.getMenuItems().size());
        for (MenuItem item : menu.getMenuItems()) {
            // Only interested in burgers
            // Could have the Menu do this, but that's a design
            // decision
            if (!(item instanceof Burger)) {
                continue;
            }
            Burger burger = (Burger) item;
            JRadioButton btn = new JRadioButton(burger.getDescription() + " (" + getCurrentFormatter().format(burger.getPrice()) + ")");
            // Ok, this is just a little cheeky, but we're associating the
            // butger with the button for simplicity
            btn.putClientProperty("MenuItem", burger);
            bg.add(btn);
            // Add all the buttons share the same listener, because of polymorphism :D
            btn.addActionListener(actionListener);
    
            burgerPanel.add(btn, gbc);
        }
    }
    

    When ever the actionListener is triggered (via a user interaction for example), it makes a bunch of decisions, which, if all goes well, ends in the Order been updated, the buildCondiments and orderDidChange methods been called, which updates the available condiments and updates the UI's tally.

    This is all done in a "abstract" way. The actionListener doesn't care about what type of MainMenuItem the user selected, that doesn't change its workflow, it only needs to apply the item to the Order. In the same vain, the Order doesn't care, as it just needs the price information in order to calculate the tally.

    So you can add new menu items and/or change the prices and everything just keeps on working (🤞).

    Let's look at the orderDidChange method...

    protected void orderDidChange() {
        if (order == null) {
            totalLabel.setText("Amount: " + getCurrentFormatter().format(0));
            return;
        }
    
        // And now, some magic, how easy is it to get the expected
        // tally amount!!
        totalLabel.setText("Amount: " + getCurrentFormatter().format(order.getTally()));
    }
    

    Not super complicated is it! All the work is been done by the, MODEL!

    What I left out

    For brevity, I left out one other concept, often used with models, the concept of the "observer pattern".

    The observer pattern allows an interested party to be notified when some other object changes. It's pretty common concept and you've already used, ActionListener is an example of an observer pattern. It allows you to "observer" "actions events" when they are triggered by a given object.

    Sure, but how is that helpful?

    Well, imagine now if, instead of having to manually call orderDidChange every time you wanted to update the UI (or even forgetting to and spending a few hours debugging why), the MenuPane could, instead, registered itself as an observer directly to the Order and be notified when the order changed!! Super sweet!

    This further helps you de-couple your code and makes it super easy to update the UI in a verity of ways independently of the model or other code requirements.

    Models 💪