Firstly, I would use some kind of model to manage the values ββon the "virtual" board, this will separate the logic from the user interface and allow either to change without affecting the other.
I would provide an appropriate model event to allow the user interface to be updated when the model changes, as well as provide facilities for each field to update the model as needed.
Then I would reduce the problem to the smallest conceptual component that will be a subordinate board and create a user interface to present it in the most abstract way. This allows you to reuse it and helps with debugging, as if one board had a problem, you can fix it in one place for everyone.
public class Sudoku { public static void main(String[] args) { new Sudoku(); } public Sudoku() { EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException ex) { } catch (InstantiationException ex) { } catch (IllegalAccessException ex) { } catch (UnsupportedLookAndFeelException ex) { } JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLayout(new BorderLayout()); frame.add(new SudokuBoard()); frame.add(new MenuPane(), BorderLayout.AFTER_LINE_ENDS); frame.pack(); frame.setVisible(true); } }); } public class MenuPane extends JPanel { public MenuPane() { setBorder(new EmptyBorder(4, 4, 4, 4)); setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; add(new JButton("Solve"), gbc); gbc.gridy++; add(new JButton("New"), gbc); gbc.gridy++; add(new JButton("Hint"), gbc); gbc.gridy++; add(new JButton("Reset"), gbc); } } public class SudokuBoard extends JPanel { public static final int ROWS = 3; public static final int COLUMNS = 3; private SubBoard[] subBoards; public SudokuBoard() { setBorder(new EmptyBorder(4, 4, 4, 4)); subBoards = new SubBoard[ROWS * COLUMNS]; setLayout(new GridLayout(ROWS, COLUMNS, 2, 2)); for (int row = 0; row < ROWS; row++) { for (int col = 0; col < COLUMNS; col++) { int index = (row * ROWS) + col; SubBoard board = new SubBoard(); board.setBorder(new CompoundBorder(new LineBorder(Color.GRAY, 3), new EmptyBorder(4, 4, 4, 4))); subBoards[index] = board; add(board); } } } } public class SubBoard extends JPanel { public static final int ROWS = 9; public static final int COLUMNS = 9; private JTextField[] fields; public SubBoard() { setLayout(new GridLayout(ROWS, COLUMNS, 2, 2)); fields = new JTextField[ROWS * COLUMNS]; for (int row = 0; row < ROWS; row++) { for (int col = 0; col < COLUMNS; col++) { int index = (row * COLUMNS) + col; JTextField field = new JTextField(4); fields[index] = field;
UPDATED
To limit text fields to enter only numeric values, you can watch the JTextField character limit input and accept only numeric values for some ideas

UPDATED (with 2D arrays)
Here, an implementation that uses 2D arrays also subgroups the sub-boards, so each 3x3 grid of fields has its own board ...

public class Sudoku { public static void main(String[] args) { new Sudoku(); } public Sudoku() { EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException ex) { } catch (InstantiationException ex) { } catch (IllegalAccessException ex) { } catch (UnsupportedLookAndFeelException ex) { } JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLayout(new BorderLayout()); frame.add(new SudokuBoard()); frame.add(new MenuPane(), BorderLayout.AFTER_LINE_ENDS); frame.pack(); frame.setVisible(true); } }); } public class MenuPane extends JPanel { public MenuPane() { setBorder(new EmptyBorder(4, 4, 4, 4)); setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; add(new JButton("Solve"), gbc); gbc.gridy++; add(new JButton("New"), gbc); gbc.gridy++; add(new JButton("Hint"), gbc); gbc.gridy++; add(new JButton("Reset"), gbc); } } public class SudokuBoard extends JPanel { public static final int ROWS = 3; public static final int COLUMNS = 3; private SubBoard[][] subBoards; public SudokuBoard() { setBorder(new EmptyBorder(4, 4, 4, 4)); subBoards = new SubBoard[ROWS][COLUMNS]; setLayout(new GridLayout(ROWS, COLUMNS, 2, 2)); for (int row = 0; row < ROWS; row++) { for (int col = 0; col < COLUMNS; col++) { int index = (row * ROWS) + col; SubBoard board = new SubBoard(); board.setBorder(new CompoundBorder(new LineBorder(Color.GRAY, 3), new EmptyBorder(4, 4, 4, 4))); subBoards[row][col] = board; add(board); } } } } public class SubBoard extends JPanel { public SubBoard() { setLayout(new GridLayout(3, 3, 2, 2)); for (int index = 0; index < 3*3; index++) { add(new ChildBoard(3, 3)); } } } public class ChildBoard extends JPanel { private JTextField[][] fields; public ChildBoard(int rows, int cols) { setBorder(new LineBorder(Color.LIGHT_GRAY)); setLayout(new GridLayout(rows, cols, 2, 2)); fields = new JTextField[rows][cols]; for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { JTextField field = new JTextField(4); fields[row][col] = field; add(field); } } } } }
or, if you want to try to save all the fields in one top-level link, you can do something like ...
public class SubBoard extends JPanel { private JTextField[][] fields; public SubBoard() { setLayout(new GridLayout(3, 3, 2, 2)); fields = new JTextField[9][9]; for (int row = 0; row < 9; row++) { for (int col = 0; col < 9; col++) { fields[row][col] = new JTextField(4); } } for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { int startRow = row * 3; int startCol = col * 3; add(new ChildBoard(3, 3, fields, startRow, startCol)); } } } } public class ChildBoard extends JPanel { public ChildBoard(int rows, int cols, JTextField[][] fields, int startRow, int startCol) { setBorder(new LineBorder(Color.LIGHT_GRAY)); setLayout(new GridLayout(rows, cols, 2, 2)); for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { JTextField field = fields[startRow + row][startCol + col]; fields[row][col] = field; add(field); } } } }
UPDATED with one class
Ok, so instead of subclassing, just use a few methods to create each separate section of the board from which you can make repeated calls ...

Remember to reduce and reuse.
public class Sudoku { public static void main(String[] args) { new Sudoku(); } public Sudoku() { EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException ex) { } catch (InstantiationException ex) { } catch (IllegalAccessException ex) { } catch (UnsupportedLookAndFeelException ex) { } JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLayout(new BorderLayout()); frame.add(new SudokuBoard()); frame.add(new MenuPane(), BorderLayout.AFTER_LINE_ENDS); frame.pack(); frame.setVisible(true); } }); } public class MenuPane extends JPanel { public MenuPane() { setBorder(new EmptyBorder(4, 4, 4, 4)); setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; add(new JButton("Solve"), gbc); gbc.gridy++; add(new JButton("New"), gbc); gbc.gridy++; add(new JButton("Hint"), gbc); gbc.gridy++; add(new JButton("Reset"), gbc); } } public class SudokuBoard extends JPanel { public static final int GRID_ROWS = 3; public static final int GRID_COLUMNS = 3; public static final int BOARD_ROWS = 9; public static final int BOARD_COLUMNS = 9; private JTextField fields[][]; public SudokuBoard() { setBorder(new EmptyBorder(4, 4, 4, 4)); fields = new JTextField[GRID_ROWS * BOARD_ROWS][GRID_COLUMNS * BOARD_COLUMNS]; setLayout(new GridLayout(GRID_ROWS, GRID_COLUMNS, 2, 2)); for (int row = 0; row < GRID_ROWS; row++) { for (int col = 0; col < GRID_COLUMNS; col++) { int startRow = row * GRID_ROWS; int startCol = col * GRID_COLUMNS; add(createBoard(fields, startRow, startCol)); } } } protected JPanel createBoard(JTextField fiels[][], int startRow, int startCol) { JPanel panel = new JPanel(new GridLayout(3, 3, 2, 2)); panel.setBorder(new CompoundBorder(new LineBorder(Color.DARK_GRAY, 2), new EmptyBorder(2, 2, 2, 2))); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { int rowIndex = (startRow + row) * 3; int colIndex = (startCol + col) * 3; panel.add(createSubBoard(fields, rowIndex, colIndex)); } } return panel; } protected JPanel createSubBoard(JTextField[][] fields, int startRow, int startCol) { JPanel panel = new JPanel(new GridLayout(3, 3, 2, 2)); panel.setBorder(new CompoundBorder(new LineBorder(Color.GRAY, 2), new EmptyBorder(2, 2, 2, 2))); populateFields(fields, startRow, startCol); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { panel.add(fields[row + startRow][col + startCol]); } } return panel; } protected void populateFields(JTextField[][] fields, int startRow, int startCol) { for (int row = startRow; row < startRow + 3; row++) { for (int col = startCol; col < startCol + 3; col++) { fields[row][col] = new JTextField(4); } } } } }