2
\$\begingroup\$

I made this calculator during the weekends as a way to practice using IntelliJ's GUI Designer. It's used extensively in my new employer's Swing projects, but I hadn't heard of it before. As a consequence, this code has a .form file that you need to compile with IntelliJ's compiler. That is, just press the run button and let the IDE do the job, don't use Maven for building unless your Maven config includes a properly configured javac2 plugin

StretchIcon is borrowed from here (I only removed the unnecessary constructors and added a setter)

You can press F1 instead of clicking C. Or press Enter instead of clicking =

Also, you can type anything MathParser can process. For example, you can use your keyboard to type sin(123) and hit Enter to see the result

But generally, no frills. Looks neat, though, imo

package processor;

public interface CalcProcessor {
    double calculateResult(String expression);
}
package processor;

import lombok.SneakyThrows;
import org.mariuszgromada.math.mxparser.Expression;
import org.mariuszgromada.math.mxparser.License;

public class DefaultCalcProcessor implements CalcProcessor {
    static {
        License.iConfirmNonCommercialUse("John Doe");
    }

    @Override
    @SneakyThrows
    public double calculateResult(String expression) {
        String preparedExpression = prepareExpression(expression);
        Expression exp = new Expression(preparedExpression);
        return exp.calculate();
    }

    private String prepareExpression(String expression) {
        return expression.replace("x", "*");
    }
}
public class App {
    public static void main(String[] args) {
        new Calc().launch();
    }
}
/**
 * @(#)StretchIcon.java 1.0 03/27/12
 */

import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Insets;
import java.awt.image.ImageObserver;

import javax.swing.ImageIcon;

/**
 * An <CODE>Icon</CODE> that scales its image to fill the component area,
 * excluding any border or insets, optionally maintaining the image's aspect
 * ratio by padding and centering the scaled image horizontally or vertically.
 * <P>
 * The class is a drop-in replacement for <CODE>ImageIcon</CODE>, except that
 * the no-argument constructor is not supported.
 * <P>
 * As the size of the Icon is determined by the size of the component in which
 * it is displayed, <CODE>StretchIcon</CODE> must only be used in conjunction
 * with a component and layout that does not depend on the size of the
 * component's Icon.
 *
 * @version 1.0 03/27/12
 * @author Darryl
 */
public class StretchIcon extends ImageIcon {

    /**
     * Determines whether the aspect ratio of the image is maintained.
     * Set to <code>false</code> to allow th image to distort to fill the component.
     */
    protected boolean proportionate = true;

    /**
     * Creates a <CODE>StretchIcon</CODE> from the image.
     *
     * @param image the image
     *
     * @see ImageIcon#ImageIcon(java.awt.Image)
     */
    public StretchIcon(Image image) {
        super(image);
    }

    public void setProportionate(boolean proportionate) {
        this.proportionate = proportionate;
    }

    /**
     * Paints the icon.  The image is reduced or magnified to fit the component to which
     * it is painted.
     * <P>
     * If the proportion has not been specified, or has been specified as <code>true</code>,
     * the aspect ratio of the image will be preserved by padding and centering the image
     * horizontally or vertically.  Otherwise, the image may be distorted to fill the
     * component it is painted to.
     * <P>
     * If this icon has no image observer,this method uses the <code>c</code> component
     * as the observer.
     *
     * @param c the component to which the Icon is painted.  This is used as the
     *          observer if this icon has no image observer
     * @param g the graphics context
     * @param x not used.
     * @param y not used.
     *
     * @see ImageIcon#paintIcon(java.awt.Component, java.awt.Graphics, int, int)
     */
    @Override
    public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
        Image image = getImage();
        if (image == null) {
            return;
        }
        Insets insets = ((Container) c).getInsets();
        x = insets.left;
        y = insets.top;

        int w = c.getWidth() - x - insets.right;
        int h = c.getHeight() - y - insets.bottom;

        if (proportionate) {
            int iw = image.getWidth(c);
            int ih = image.getHeight(c);

            if (iw * h < ih * w) {
                iw = (h * iw) / ih;
                x += (w - iw) / 2;
                w = iw;
            }
            else {
                ih = (w * ih) / iw;
                y += (h - ih) / 2;
                h = ih;
            }
        }

        ImageObserver io = getImageObserver();
        g.drawImage(image, x, y, w, h, io == null ? c : io);
    }

    /**
     * Overridden to return 0.  The size of this Icon is determined by
     * the size of the component.
     *
     * @return 0
     */
    @Override
    public int getIconWidth() {
        return 0;
    }

    /**
     * Overridden to return 0.  The size of this Icon is determined by
     * the size of the component.
     *
     * @return 0
     */
    @Override
    public int getIconHeight() {
        return 0;
    }
}
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Arrays;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;

import com.google.common.math.DoubleMath;
import lombok.SneakyThrows;
import processor.CalcProcessor;
import processor.DefaultCalcProcessor;


public class Calc {
    public static String INVALID_INPUT = "invalid input";
    private JTextField display;
    private JButton cancel;
    private JButton one;
    private JPanel buttonPanel;
    private JButton four;
    private JButton seven;
    private JButton two;
    private JButton sign;
    private JButton three;
    private JButton plus;
    private JButton five;
    private JButton six;
    private JButton eight;
    private JButton parentheses;
    private JButton percent;
    private JButton nine;
    private JButton divide;
    private JButton multiply;
    private JButton minus;
    private JButton zero;
    private JButton decimalDelimiter;
    private JButton equal;
    private JPanel mainPanel;
    private JPanel displayPanel;
    private JButton delete;
    private final CalcProcessor processor;
    private final JFrame frame = new JFrame("Calculator");

    public Calc() {
        this(new DefaultCalcProcessor());
    }

    public Calc(CalcProcessor processor) {
        this.processor = processor;
        delete.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
        delete.setIcon(new StretchIcon(((ImageIcon) delete.getIcon()).getImage()));
        delete.setRolloverIcon(new StretchIcon(((ImageIcon) delete.getRolloverIcon()).getImage()));
        delete.setPressedIcon(new StretchIcon(((ImageIcon) delete.getPressedIcon()).getImage()));
        setDefaultButton();
        attachListeners();
    }

    private void setDefaultButton() {
        frame.getRootPane().setDefaultButton(equal);
    }

    private void attachListeners() {
        attachDigitListeners();
        attachOperationListeners();
        attachPercentListener();
        attachDecimalDelimiterListener();
        attachCancelListener();
        attachEqualListener();
        attachParenthesesListener();
        attachDeleteListener();
    }

    private void attachDigitListeners() {
        attachButtonTextAppendingListener(zero, one, two, three, four, five, six, seven, eight, nine);
    }

    private void attachOperationListeners() {
        attachButtonTextAppendingListener(plus, minus, multiply, divide);
    }

    private void attachPercentListener() {
        attachButtonTextAppendingListener(percent);
    }

    private void attachDecimalDelimiterListener() {
        attachButtonTextAppendingListener(decimalDelimiter);
    }

    private void attachButtonTextAppendingListener(JButton... buttons) {
        Arrays.stream(buttons).forEach(b -> b.addActionListener(e -> display.setText(display.getText() + b.getText())));
    }

    private void attachCancelListener() {
        Action action = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                display.setText("");
            }
        };
        cancel.addActionListener(action);
        cancel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0), cancel.getText());
        cancel.getActionMap().put(cancel.getText(), action);
    }

    private void attachParenthesesListener() {
        parentheses.addActionListener(new ActionListener() {
            private int numOfOpenedParentheses = 0;

            @Override
            public void actionPerformed(ActionEvent e) {
                String currentText = display.getText();
                String parenthesis = getAppropriateParenthesis(currentText);
                display.setText(currentText + parenthesis);
            }

            private String getAppropriateParenthesis(String currentText) {
                if (currentText.isEmpty()) {
                    return registerOpeningParenhesis();
                }
                char lastCharacter = currentText.charAt(currentText.length() - 1);

                if (Character.isDigit(lastCharacter)) {
                    if (numOfOpenedParentheses > 0) {
                        return registerClosingParenhesis();
                    }
                    else {
                        return registerOpeningParenhesis();
                    }
                }
                else {
                    return registerOpeningParenhesis();
                }
            }

            private String registerOpeningParenhesis() {
                numOfOpenedParentheses++;
                return "(";
            }

            private String registerClosingParenhesis() {
                numOfOpenedParentheses--;
                return ")";
            }
        });
    }

    private void attachDeleteListener() {
        delete.addActionListener(e -> {
            String currentText = display.getText();
            if (!currentText.isEmpty()) {
                display.setText(currentText.substring(0, currentText.length() - 1));
            }
        });
    }

    private void attachEqualListener() {
        equal.addActionListener(e -> {
            String currentText = display.getText();
            if (!currentText.isEmpty()) {
                double decimalResult = processor.calculateResult(currentText);
                String displayedResult = postProcessResult(decimalResult);
                display.setText(displayedResult);
            }
        });
    }

    private String postProcessResult(double decimalResult) {
        return Double.isNaN(decimalResult) ?
                INVALID_INPUT :
                DoubleMath.isMathematicalInteger(decimalResult) ?
                        String.valueOf((long) decimalResult) :
                        String.valueOf(decimalResult);
    }

    @SneakyThrows
    public void launch() {
        UIManager.setLookAndFeel(NimbusLookAndFeel.class.getName());

        Image icon = new ImageIcon("src/main/java/org/example/demos/ui/guidesigner/calc/src/main/resources/calc-icon.jpg").getImage();
        frame.setIconImage(icon);
        frame.add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="Calc">
  <grid id="27dc6" binding="mainPanel" layout-manager="BorderLayout" hgap="5" vgap="5">
    <constraints>
      <xy x="20" y="20" width="554" height="659"/>
    </constraints>
    <properties>
      <enabled value="false"/>
      <opaque value="false"/>
      <preferredSize width="225" height="270"/>
    </properties>
    <border type="empty">
      <font/>
      <size top="10" left="10" bottom="10" right="10"/>
    </border>
    <children>
      <grid id="45f43" binding="buttonPanel" layout-manager="GridLayoutManager" row-count="5" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="5" vgap="5">
        <margin top="0" left="0" bottom="0" right="0"/>
        <constraints border-constraint="Center"/>
        <properties>
          <preferredSize width="-1" height="-1"/>
        </properties>
        <border type="none"/>
        <children>
          <component id="7bab3" class="javax.swing.JButton" binding="cancel">
            <constraints>
              <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <contentAreaFilled value="true"/>
              <requestFocusEnabled value="false"/>
              <text value="C"/>
            </properties>
          </component>
          <component id="9587" class="javax.swing.JButton" binding="one">
            <constraints>
              <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="1"/>
            </properties>
          </component>
          <component id="124ce" class="javax.swing.JButton" binding="four">
            <constraints>
              <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="4"/>
            </properties>
          </component>
          <component id="ded1b" class="javax.swing.JButton" binding="seven">
            <constraints>
              <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="7"/>
            </properties>
          </component>
          <component id="850d1" class="javax.swing.JButton" binding="two">
            <constraints>
              <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="2"/>
            </properties>
          </component>
          <component id="ba673" class="javax.swing.JButton" binding="sign">
            <constraints>
              <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <borderPainted value="false"/>
              <contentAreaFilled value="false"/>
              <enabled value="false"/>
              <label value=""/>
              <requestFocusEnabled value="false"/>
              <text value=""/>
            </properties>
          </component>
          <component id="c7d5c" class="javax.swing.JButton" binding="three">
            <constraints>
              <grid row="3" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="3"/>
            </properties>
          </component>
          <component id="9f62b" class="javax.swing.JButton" binding="plus">
            <constraints>
              <grid row="3" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="+"/>
            </properties>
          </component>
          <component id="a7cc9" class="javax.swing.JButton" binding="five">
            <constraints>
              <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="5"/>
            </properties>
          </component>
          <component id="1d77f" class="javax.swing.JButton" binding="six">
            <constraints>
              <grid row="2" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="6"/>
            </properties>
          </component>
          <component id="51b28" class="javax.swing.JButton" binding="eight">
            <constraints>
              <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="8"/>
            </properties>
          </component>
          <component id="579c0" class="javax.swing.JButton" binding="parentheses">
            <constraints>
              <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="()"/>
            </properties>
          </component>
          <component id="6c68c" class="javax.swing.JButton" binding="nine">
            <constraints>
              <grid row="1" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="9"/>
            </properties>
          </component>
          <component id="e3a1" class="javax.swing.JButton" binding="divide">
            <constraints>
              <grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="/"/>
            </properties>
          </component>
          <component id="786a0" class="javax.swing.JButton" binding="multiply">
            <constraints>
              <grid row="1" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="x"/>
            </properties>
          </component>
          <component id="b6f18" class="javax.swing.JButton" binding="minus">
            <constraints>
              <grid row="2" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="-"/>
            </properties>
          </component>
          <component id="e9a58" class="javax.swing.JButton" binding="zero">
            <constraints>
              <grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <requestFocusEnabled value="false"/>
              <text value="0"/>
            </properties>
          </component>
          <component id="60af6" class="javax.swing.JButton" binding="decimalDelimiter">
            <constraints>
              <grid row="4" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <enabled value="true"/>
              <requestFocusEnabled value="false"/>
              <text value="."/>
            </properties>
          </component>
          <component id="8ef05" class="javax.swing.JButton" binding="equal">
            <constraints>
              <grid row="4" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <background color="-11814534"/>
              <borderPainted value="true"/>
              <contentAreaFilled value="true"/>
              <requestFocusEnabled value="false"/>
              <text value="="/>
            </properties>
          </component>
          <component id="1f6ef" class="javax.swing.JButton" binding="percent">
            <constraints>
              <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
            </constraints>
            <properties>
              <enabled value="true"/>
              <requestFocusEnabled value="false"/>
              <text value="%"/>
            </properties>
          </component>
        </children>
      </grid>
      <grid id="6af9" binding="displayPanel" layout-manager="GridBagLayout">
        <constraints border-constraint="North"/>
        <properties/>
        <border type="none"/>
        <children>
          <component id="93a0b" class="javax.swing.JTextField" binding="display">
            <constraints>
              <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
              <gridbag weightx="100.0" weighty="1.0"/>
            </constraints>
            <properties>
              <editable value="true"/>
              <font name="DialogInput"/>
              <horizontalAlignment value="4"/>
            </properties>
          </component>
          <component id="b3cba" class="javax.swing.JButton" binding="delete">
            <constraints>
              <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
              <gridbag weightx="1.0" weighty="1.0"/>
            </constraints>
            <properties>
              <actionCommand value="delete"/>
              <borderPainted value="true"/>
              <contentAreaFilled value="false"/>
              <icon value="delete-icon.png"/>
              <maximumSize width="354" height="326"/>
              <minimumSize width="30" height="25"/>
              <opaque value="false"/>
              <preferredSize width="30" height="25"/>
              <pressedIcon value="delete-icon-pressed.png"/>
              <requestFocusEnabled value="false"/>
              <rolloverIcon value="delete-icon-hovered.png"/>
              <text value=""/>
            </properties>
          </component>
        </children>
      </grid>
      <component id="b49d7" class="javax.swing.JLabel">
        <constraints border-constraint="South"/>
        <properties>
          <font style="2"/>
          <horizontalAlignment value="11"/>
          <text value="Powered by MathParser"/>
        </properties>
      </component>
    </children>
  </grid>
</form>

calc-icon.jpg

enter image description here

delete-icon.png

enter image description here

delete-icon-hovered.png

enter image description here

delete-icon-pressed.png

enter image description here

Result:

enter image description here

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This does show that you have learned how to build a simple and crude UI using the UI builder. But since we don't know how your company does their UI, we don't know if any of this is very relevant at all. For this to be worthwhile towards actual production use, it would seem to me that your next goal should be to refactor the "calculator button" into a reusable component. \$\endgroup\$ Commented May 13 at 6:29

1 Answer 1

2
\$\begingroup\$

There isn't all that much to the program (which is a compliment of sorts), but I'll try and find some things to put some salt on. Code is readable, the identifiers are fine (which is a plus for GUI code) and indentation and stuff is spot on.

Alright, here comes the salt - which is very much the type of salt that is put on snails.


I'm not entirely getting how the parentheses is supposed to work especially when regarding multiplication. Things like 2 / (2 + 2(2 - 2)) cannot be entered it seems. There is a reason why () isn't common on calculators.


Arrays.stream(buttons).forEach(b -> b.addActionListener(e -> display.setText(display.getText() + b.getText())));

This directly ties the label text to the math formula that is put within the text field. The problem with that is that you have to reprogram this part if you e.g. replace the text with a nice rendered image instead.


private void attachPercentListener() {
    attachButtonTextAppendingListener(percent);
}

I can see why e.g. the decimal delimiter is put in a separate method, but I'm not sure why this is a separate method from attachOperationListeners(); it seems to do the same.


Some remarks about attachCancelListener:

  • this method also registers a KeyStroke which in my opinion isn't implied within name of the method;
  • the action is registered after the InputMap is being defined, which may leave the reader wondering what is being registered.

getAppropriateParenthesis()

Actually returns the parentheses, but it also calls registerOpeningParenhesis() or registerClosingParenhesis.

Now a getter isn't assumed to have side effects, so the fact that a getter changes the field numOfOpenedParentheses seems unexpected. Furthermore, I would not expect a register call to return a string either.

It is also a bit weird why the parentheses returned aren't represented by a char.


Image icon = new ImageIcon("src/main/java/org/example/demos/ui/guidesigner/calc/src/main/resources/calc-icon.jpg").getImage();

This probably works fine during debugging, but using a path to a source folder is obviously not the way to go.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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