javaswingjoptionpane

JOptionPane behaving weird with KeyListener


I am currently working on a Sudoku solver with a GUI. Neither is finished, but the GUI is partly operational. However I have encountered something weird. The code follows below:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class SudokuWindow extends JFrame {
    public SudokuWindow() {
        SwingUtilities.invokeLater(()->createSudokoWindow());
    }
    
    private void createSudokoWindow() {
        JFrame frame = new JFrame("Sudoku!");
        int height = 600;
        int width = 1000;
        frame.setPreferredSize(new Dimension(width,height));
        JPanel mainPanel = new JPanel();
        mainPanel.setLayout(new FlowLayout());
        SudokuBoard sB= new SudokuBoard(width/2,width/2,9,9);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JButton button1 = new JButton("Import solvabel setup");
        JButton button2 = new JButton("Find solution");
        JTextField t1=new JTextField("0");  
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
        panel.add(button1);
        panel.add(button2);
        panel.add(t1);
        mainPanel.add(sB,BorderLayout.WEST);
        mainPanel.add(panel, BorderLayout.EAST);
        frame.add(mainPanel);
        frame.pack();
        frame.setVisible(true);
    }
    
    public static void main(String[] args) {
        new SudokuWindow();
    }
}

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFormattedTextField;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

public class SudokuBoard extends JPanel {
      int width, height, rows, cols;
      JFormattedTextField[][] txtM = new JFormattedTextField[9][9];
      
      SudokuBoard(int w, int h, int r, int c) {
            setSize(width = w, height = h);
            this.setPreferredSize(new Dimension(w,h));
            for(int i = 0; i < r; i++) {
                for(int j = 0; j < c; j++) {
                    txtM[i][j] = new JFormattedTextField();
                }
            }
            rows = r;
            cols = c;
      }
      
      public void paint(Graphics g) {
            width = getSize().width;
            height = getSize().height;
            
            int rowHt = height / rows;
            int colWt = width / cols;
            JFormattedTextField t;
            
            for (int i = 0; i < rows; i++) {
                for(int j = 0; j < cols; j++) {
                    t = txtM[i][j];
                    t.setText("0");
                    t.setSize(rowHt,colWt);
                    t.setMargin(new Insets(0,18,0,0));
                    t.setFont(new Font("TimesRoman",Font.PLAIN,26));
                    t.setLocation(i*colWt, j*rowHt);
                    t.addKeyListener(keyListener);
                    if((((j+1) % 9 == 1 || (j+1) % 9 == 2 || (j+1) % 9 == 3) && ((i+1) % 9 == 1 || (i+1) % 9 == 2 || (i+1) % 9 == 3))){
                        t.setBackground(Color.MAGENTA);
                    } else if(((j+1) % 9 == 7 || (j+1) % 9 == 8 || (j+1) % 9 == 0) && ((i+1) % 9 == 1 || (i+1) % 9 == 2 || (i+1) % 9 == 3)){
                        t.setBackground(Color.MAGENTA);
                    } else if(((j+1) % 9 == 7 || (j+1) % 9 == 8 || (j+1) % 9 == 0) && ((i+1) % 9 == 7 || (i+1) % 9 == 8 || (i+1) % 9 == 0)){
                        t.setBackground(Color.MAGENTA);
                    } else if(((j+1) % 9 == 1 || (j+1) % 9 == 2 || (j+1) % 9 == 3) && ((i+1) % 9 == 7 || (i+1) % 9 == 8 || (i+1) % 9 == 0)){
                        t.setBackground(Color.MAGENTA);
                    } else if(((j+1) % 9 == 4 || (j+1) % 9 == 5 || (j+1) % 9 == 6) && ((i+1) % 9 == 4 || (i+1) % 9 == 5 || (i+1) % 9 == 6)){
                        t.setBackground(Color.MAGENTA);
                    }
                    add(t);
                }
            }
        }
        
        KeyListener keyListener = new KeyListener() {
            @Override
            public void keyPressed(KeyEvent e) {}

            @Override
            public void keyTyped(KeyEvent e) {
                if(Character.isLetter(e.getKeyChar()) || ((Character) e.getKeyChar()).equals('0')){
                    JOptionPane.showMessageDialog(null, "Invalid input!");
                }
                
            }
            
            @Override
            public void keyReleased(KeyEvent e) {}
        };
}

The code works as intended so far except for one thing. When the KeyListener finds a Letter or a 0, the JOptionPane pops up three times. This is kind of annoying, and I don't understand why it does that.

Also I am trying to reach the JFormattedTextFields so I can reset them after a Invalid input but can't reach them through the KeyListener. Is there some nice way to do this?

If anyone could help me figure this out I would greatly appreciate it!

Thank you!


Solution

  • TL;DR
    Your problem is that you are adding another KeyListener to each JFormattedTextField every time method paint is called and each listener is executed. So if...

    the JOptionPane pops up three times

    then it means that method paint was called three times which caused three listeners to be added to each JFormattedTextField.


    Other general notes regarding your code:

    Class SudokuWindow:

    1. Swing application classes, such as your SudokuWindow, don't need to extend JFrame. In any case you create a new JFrame in method createSudokoWindow() (of class SudokuWindow) so really no need for class SudokuWindow to extend JFrame.
    2. mainPanel.add(sB,BorderLayout.WEST); Because you explicitly set the layout of mainPanel to FlowLayout, the BorderLayout constraints are ignored. If you want to use BorderLayout constraints then you need to set the layout manager for mainPanel to BorderLayout.
    3. frame.setPreferredSize( Better you should not use mainPanel at all and instead use the content pane of the JFrame – whose default layout manager is BorderLayout – and set the preferred size of the SudokuBoard and add it to BorderLayout.CENTER.

    Class SudokuBoard:

    1. Set its layout manager to GridLayout and set its preferred size. Then you don't have to worry about explicitly setting sizes for each JFormattedTextField.
    2. If you are going to override a painting method, then it almost always should be paintComponent and not paint. However you don't need to override any painting method since you should create the JFormattedTextFields in the constructor and set the background colors also in the constructor rather than in method paint.
    3. As mentioned by @camickr in his comment, you don't need a KeyListener if you are using JFormattedTextField.

    Consider the following (which is my alternative solution):
    (Notes and explanations after the code.)

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Font;
    import java.awt.GridLayout;
    import java.text.ParseException;
    
    import javax.swing.BorderFactory;
    import javax.swing.Box;
    import javax.swing.BoxLayout;
    import javax.swing.JButton;
    import javax.swing.JFormattedTextField;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JTextField;
    import javax.swing.LookAndFeel;
    import javax.swing.UIDefaults;
    import javax.swing.UIManager;
    import javax.swing.text.MaskFormatter;
    
    public class SudokuPuzzle {
        private static final int  COLS = 9;
        private static final int  ROWS = 9;
        private static final Color[][]  BACKGROUNDS;
    
        static {
            LookAndFeel laf = UIManager.getLookAndFeel();
            UIDefaults dflts = laf.getDefaults();
            Color dflt = (Color) dflts.get("TextField.background");
            Color mgnt = Color.magenta;
            BACKGROUNDS = new Color[][] {{mgnt, mgnt, mgnt, dflt, dflt, dflt, mgnt, mgnt, mgnt},
                                         {mgnt, mgnt, mgnt, dflt, dflt, dflt, mgnt, mgnt, mgnt},
                                         {mgnt, mgnt, mgnt, dflt, dflt, dflt, mgnt, mgnt, mgnt},
                                         {dflt, dflt, dflt, mgnt, mgnt, mgnt, dflt, dflt, dflt},
                                         {dflt, dflt, dflt, mgnt, mgnt, mgnt, dflt, dflt, dflt},
                                         {dflt, dflt, dflt, mgnt, mgnt, mgnt, dflt, dflt, dflt},
                                         {mgnt, mgnt, mgnt, dflt, dflt, dflt, mgnt, mgnt, mgnt},
                                         {mgnt, mgnt, mgnt, dflt, dflt, dflt, mgnt, mgnt, mgnt},
                                         {mgnt, mgnt, mgnt, dflt, dflt, dflt, mgnt, mgnt, mgnt}};
        }
    
        private void buildAndDisplayGui() throws ParseException {
            JFrame frame = new JFrame("Sudoku!");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(createBoard(), BorderLayout.CENTER);
            frame.add(createButtonsPanel(), BorderLayout.LINE_END);
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
        }
    
        private JPanel createBoard() throws ParseException {
            JPanel board = new JPanel(new GridLayout(0, 9));
            board.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
            for (int row = 0; row < ROWS; row++) {
                for (int col = 0; col < COLS; col++) {
                    board.add(createSquare(row, col));
                }
            }
            return board;
        }
    
        private JPanel createButtonsPanel() {
            JPanel buttonsPanel = new JPanel();
            buttonsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20));
            BoxLayout layout = new BoxLayout(buttonsPanel, BoxLayout.PAGE_AXIS);
            buttonsPanel.setLayout(layout);
            buttonsPanel.add(Box.createVerticalGlue());
            JButton button1 = new JButton("Import solvable setup");
            buttonsPanel.add(button1);
            buttonsPanel.add(Box.createRigidArea(new Dimension(0, 5)));
            JButton button2 = new JButton("Find solution");
            button2.setMaximumSize(button1.getMaximumSize());
            buttonsPanel.add(button2);
            buttonsPanel.add(Box.createRigidArea(new Dimension(0, 5)));
            JTextField t1 = new JTextField("0");
            t1.setMaximumSize(button1.getMaximumSize());
            buttonsPanel.add(t1);
            buttonsPanel.add(Box.createVerticalGlue());
            return buttonsPanel;
        }
    
        private JFormattedTextField createSquare(int row, int col) throws ParseException {
            MaskFormatter formatter = new MaskFormatter("#");
            formatter.setValidCharacters("123456789");
            JFormattedTextField ftf = new JFormattedTextField(formatter);
            ftf.setColumns(2);
            ftf.setHorizontalAlignment(JTextField.CENTER);
            ftf.setFont(new Font("TimesRoman",Font.PLAIN,26));
            ftf.setBackground(BACKGROUNDS[row][col]);
            return ftf;
        }
    
        public static void main(String[] args) {
            SudokuPuzzle puzzle = new SudokuPuzzle();
            EventQueue.invokeLater(() -> {
                try {
                    puzzle.buildAndDisplayGui();
                }
                catch (ParseException xParse) {
                    xParse.printStackTrace();
                }
            });
        }
    }
    
    1. I don't see a need for a SudokuBoard class that extends JPanel since there is no need to override any of JPanel class methods.
    2. Since the valid characters are 1 - 9, I can't set the text of each JFormattedTextField to 0 (zero), hence the JFormattedTextFields are initially all empty.
    3. Default behavior of JFormatedTextField, when entering an invalid value, is to beep. If you want to display a JOptionPane instead then I think you will need to write your own formatter and use that instead of MaskFormatter. I felt it was too much work for me so I just left the default behavior.

    Here is how it looks:

    screen capture