Nico Nico - 3 months ago 7
Java Question

How to synchronize Swing inside a recursive method

I tried to write a sukoku solver which solves sudokus via backtracking. This is working pretty fine and so I tried to write a GUI which is working, but creating borders was hard and is really long, maybe you have a better idea of how to do this. And now I want that you can see the backtracking algorithm working, but I don't have a clue of how to synchronize Swing.

I think SwingWorker is not working here because it would have to call itself. Synchronized blocks didn't work.

My code:

package games.sudoku;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class MyFrame {
private JFrame f = new JFrame("Sudoku Solver");
private JPanel mainPanel = new JPanel();
private JPanel sudokuPanel = new JPanel();
private JPanel buttonPanel = new JPanel();
private JTextField [][] cells = new JTextField[9][9];
private JTextField progressBar = new JTextField();
private JButton solve = new JButton("Solve!");
private JButton reset = new JButton("Reset");
private int[][] intCells = new int[9][9];
private int x = 0;
private int y = 0;
private Font sudokuFont = new Font("SansSerief", Font.BOLD, 20);
private ActionListener al;
private PropertyChangeSupport pcs = new PropertyChangeSupport(intCells);

public MyFrame() {
f.setSize(400, 400);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(init());
f.setResizable(false);
f.setVisible(true);
}

/**
* create and initialized mainPanel
*
* @return completly initialized mainPanel
*/
private JPanel init(){
listeners();
solve.addActionListener(al);
progressBar.setEditable(false);

// Init the panel where the sudoku is displayed
sudokuPanel.setLayout(new GridLayout(9, 9));
for(x = 0; x < cells.length; x++){
for(y = 0; y < cells[x].length; y++){
cells[x][y] = new JTextField();
cells[x][y].setFont(sudokuFont);
cells[x][y].setHorizontalAlignment(JTextField.CENTER);
//TODO find a better way
//Create borders to simulate a sudoku's layout
if(bordered(x, 0)){ // Top total
if(bordered(y, 0)){ //Left
cells[x][y].setBorder(BorderFactory.createMatteBorder(4, 4, 1, 1, Color.BLACK));
}else if(bordered(y, 1, 4, 7)){ //Mid
cells[x][y].setBorder(BorderFactory.createMatteBorder(4, 1, 1, 1, Color.BLACK));
}else if(bordered(y, 2, 5)){ //Right-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(4, 1, 1, 2, Color.BLACK));
}else if(bordered(y, 3, 6)){ //Left-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(4, 2, 1, 1, Color.BLACK));
}else{ //Right
cells[x][y].setBorder(BorderFactory.createMatteBorder(4, 1, 1, 4, Color.BLACK));
}
}else if(bordered(x, 3, 6)){ //top of box
if(bordered(y, 0)){ //Left
cells[x][y].setBorder(BorderFactory.createMatteBorder(2, 4, 1, 1, Color.BLACK));
}else if(bordered(y, 1, 4, 7)){ //Mid
cells[x][y].setBorder(BorderFactory.createMatteBorder(2, 1, 1, 1, Color.BLACK));
}else if(bordered(y, 2, 5)){ //Right-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(2, 1, 1, 2, Color.BLACK));
}else if(bordered(y, 3, 6)){ //Left-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(2, 2, 1, 1, Color.BLACK));
}else{ //Right
cells[x][y].setBorder(BorderFactory.createMatteBorder(2, 1, 1, 4, Color.BLACK));
}
}else if(bordered(x, 1, 4, 7)){ //Mid
if(bordered(y, 0)){ //Left
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 4, 1, 1, Color.BLACK));
}else if(bordered(y, 1, 4, 7)){ //Mid
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.BLACK));
}else if(bordered(y, 2, 5)){ //Right-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 1, 2, Color.BLACK));
}else if(bordered(y, 3, 6)){ //Left-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 2, 1, 1, Color.BLACK));
}else{ //Right
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 1, 4, Color.BLACK));
}
}else if(bordered(x, 2, 5)){ // Bottom of box
if(bordered(y, 0)){ //Left
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 4, 2, 1, Color.BLACK));
}else if(bordered(y, 1, 4, 7)){ //Mid
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 2, 1, Color.BLACK));
}else if(bordered(y, 2, 5)){ //Right-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 2, 2, Color.BLACK));
}else if(bordered(y, 3, 6)){ //Left-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 2, 2, 1, Color.BLACK));
}else{ //Right
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 2, 4, Color.BLACK));
}
}else if(bordered(x, 8)){ // Bottom overall
if(bordered(y, 0)){ //Left
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 4, 4, 1, Color.BLACK));
}else if(bordered(y, 1, 4, 7)){ //Mid
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 4, 1, Color.BLACK));
}else if(bordered(y, 2, 5)){ //Right-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 4, 2, Color.BLACK));
}else if(bordered(y, 3, 6)){ //Left-Half
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 2, 4, 1, Color.BLACK));
}else{ //Right
cells[x][y].setBorder(BorderFactory.createMatteBorder(1, 1, 4, 4, Color.BLACK));
}
}
sudokuPanel.add(cells[x][y]);
}
}

buttonPanel.setLayout(new FlowLayout());
buttonPanel.add(solve);
buttonPanel.add(reset);

mainPanel.setLayout(new BorderLayout());
mainPanel.add(sudokuPanel, BorderLayout.CENTER);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
mainPanel.add(progressBar, BorderLayout.NORTH);
return mainPanel;
}

private void listeners(){
al = new ActionListener() {

@Override
public void actionPerformed(ActionEvent e) {
if(e.getSource().equals(solve)){
for(int i = 0; i < cells.length; i++){
for(int j = 0; j < cells[i].length; j++){
if(isInt(cells[i][j].getText())){
intCells[i][j] = new Integer(cells[i][j].getText());
}else if(cells[i][j].getText().equals("")){
intCells[i][j] = 0;
cells[i][j].setForeground(Color.ORANGE);
}else{
setProgressBarText(true, "Error: Input has to be a digit between 1 and 9!");
}
}
}
pcs.firePropertyChange("input", null, null);
}else{
//TODO al for reset-button
}

}
};
}

/**
* method to stint y == 0 || y == 1 ...
*
* @param var x or y
* @param args specified numbers to check
* @return true or false if var == args
*/
private boolean bordered(int var, int... args){
for(int i = 0; i < args.length; i++){
if(var == args[i]){
return true;
}
}
return false;
}

/**
* check wether a string can be parsed to int
*
* @param s input String which should be checked
* @return false if s cannot be parsed to int or 0 < x < 10
* to check if the int can be part of the sudoku (1-9 only)
*/
private boolean isInt(String s){
try{
int i = Integer.parseInt(s);
if(i < 0 || i > 10){
return false;
}
}catch(Exception e){
return false;
}
return true;
}

/**
* set text of the progrss bar in top of the frame
*
* @param error if error --> text displayed in red, otherwise in black
* @param text text to be displayed
*/
//TODO synchronize
public void setProgressBarText(boolean error, String text){
progressBar.setText(text);
if(error){
progressBar.setForeground(Color.RED);
}else{
progressBar.setForeground(Color.BLACK);
}
}

/**
* Adds a PropertyChangeLsitener to our PropertyChangeSupport
*
* @param listener PropertyChangeListener to be added to PropertyChangeSupport
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}

/**
* display the sudoku cells
*
* @param cells two-dim int-array containing the number to be displayed
* if "0", nothing ("") is displayed
*/
//TODO synchronize
public void displaySudoku(int[][] cells){
for(int i = 0; i < cells.length; i++){
for(int j = 0; j < cells[i].length; j++){
if(cells[i][j] != 0){
this.cells[i][j].setText(Integer.toString(cells[i][j]));
}else{
this.cells[i][j].setText("");
}
}
}
}

/**
* number of tries, will be displayed in progress bar
*
* @param n ammount of tries
*/
public void setAmmountOfTries(int n){
setProgressBarText(false, Integer.toString(n) + " tries");
}


public int[][] getModel(){
return intCells;
}
}


and

package games.sudoku;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

public class SudokuSolver{

//Sudoku model to be solved
private int[][] model;
private int tries = 1;
private final MyFrame f;

public SudokuSolver() {
f = new MyFrame();

f.addPropertyChangeListener(new PropertyChangeListener() {

@Override
public void propertyChange(PropertyChangeEvent evt) {
model = f.getModel();
//TODO check if model is legally
//Try to solve
if(solve(0, 0)){

}else{
//Model is not solvable
f.setProgressBarText(true, "Sudoku cannot be solved!");
}

}
});

}
/**
* recursive method to solve the sudoku via backtracking
*
* @param row
* @param col
* @return true if a possible solution was found, otherwise false
*/
public boolean solve(int row, int col){
//Update GUI
f.displaySudoku(model);
f.setAmmountOfTries(tries);

tries++;

//Change column if all rows of it are filled
if(row == 9){
row = 0;
if(++col == 9){
return true;
}
}

//Skip non-empty cell
if(model[row][col] != 0){
return solve(row+1, col);
}

//Solve
for(int num = 1; num < 10; num++){
if(isValid(row, col, num)){
model[row][col] = num;
if(solve(row+1, col)){
return true;
}
}
}

//Reset
model[row][col] = 0;
return false;

}

/**
* check if a number can be filled in in position [row][col]
*
* @param row
* @param col
* @param num number to be filled in
* @return true if number is legal, otherwise false
*/

private boolean isValid(int row, int col, int num){
//Check row and column
for(int i = 0; i < 9; i++){
if(num == model[row][i] || num == model[i][col]){
return false;
}
}
//Check box
row = (row / 3) * 3 ;
col = (col / 3) * 3 ;
for(int r = 0; r < 3; r++){
for(int c = 0; c < 3; c++){
if(model[row+r][col+c] == num){
return false;
}
}
}
return true;
}
}

Answer

You appear to be making long running recursive calls on the Swing event thread, and this will cause your GUI to freeze. The solution in this situation is to do what you appear to be actively avoid doing: use a SwingWorker to create a background thread and run the recursive code in this background thread. Synchronization will not help, so don't bark up that tree. You state that you can't call SwingWorker in a recursive fashion, but you don't have to; rather you can start your recursive calls from within the SwingWorker, and then output the data it produces to the GUI either through the done method to return a final result or through the publish/process method pair to show interim output while the SwingWorker is running.

For more on this, please check out Lesson: Concurrency in Swing.

As for borders, I would nest 9 JPanels in a 3 x 3 GridLayout using thin lineborders inside the small JPanels and thicker line borders on the outside of the smaller JPanels.

For example (layout and borders only):

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.GridLayout;
import javax.swing.*;

public class SimpleSudokuPanel extends JPanel {
    private static final int PANEL_THICKNESS = 2;
    private static final int TF_THICKNESS = 1;
    private static final int TF_COLS = 2;
    private static final float TF_PTS = 36f;
    private JTextField[][] grid = new JTextField[9][9];

    public SimpleSudokuPanel() {
        JPanel mainPanel = new JPanel(new GridLayout(3, 3));
        JPanel[] innerPanels = new JPanel[9];
        for (int i = 0; i < innerPanels.length; i++) {
            innerPanels[i] = new JPanel(new GridLayout(3, 3));
            innerPanels[i].setBorder(BorderFactory.createLineBorder(Color.black, PANEL_THICKNESS));
            mainPanel.add(innerPanels[i]);
        }

        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
                grid[i][j] = new JTextField(TF_COLS);
                grid[i][j].setFont(grid[i][j].getFont().deriveFont(Font.BOLD, TF_PTS));
                grid[i][j].setBorder(BorderFactory.createLineBorder(Color.black, TF_THICKNESS));
                grid[i][j].setHorizontalAlignment(JTextField.CENTER);
                int panelIndex = 3 * (i / 3) + j / 3;
                innerPanels[panelIndex].add(grid[i][j]);
            }
        }

        setBorder(BorderFactory.createLineBorder(Color.black, PANEL_THICKNESS));
        setLayout(new BorderLayout());
        add(mainPanel, BorderLayout.CENTER);
    }

    private static void createAndShowGui() {
        JFrame frame = new JFrame("SimpleSudokuPanel");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new SimpleSudokuPanel());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}
Comments