Hey, I have recently switched from Tkinter to wxPython, because after reading several comments on that topic I came to the conclusion that this might be a good idea.
In order to keep the GUI responsive during backtracking, I used threading like in this example:
Long running tasks
I also succeeded to implement a working "Stop Button" to abort the backtracking thread.
If someone is interested, here is the full code:
# GUI File
import wx
import time
import SudokuSolverV2_Solver as SOLVER
from threading import *
START_ID = wx.NewId()
STOP_ID = wx.NewId()
EVT_RESULT_ID = wx.NewId()
class ResultEvent(wx.PyEvent):
def __init__(self, data):
wx.PyEvent.__init__(self)
self.SetEventType(EVT_RESULT_ID)
self.data = data
class WorkerThread(Thread):
def __init__(self, notify_window, *args):
Thread.__init__(self)
self._notify_window = notify_window
self.matrix = args[0]
self.aborted = False
self.start()
def run(self):
if self.aborted:
wx.PostEvent(self._notify_window, ResultEvent(None))
return
SOLVER.solve(self.matrix, 0)
wx.PostEvent(self._notify_window, ResultEvent(SOLVER.SOLUTION))
def abort(self):
self.aborted = True
SOLVER.DISABLED = True
self.run()
class SudokuSolver(wx.Frame):
def __init__(self, parent):
super(SudokuSolver, self).__init__(parent, title='SudokuSolver' ,size=(355,435),
style=wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX)
self.Entries = {}
self.seconds = 0
self.minutes = 0
self.timer_mode = 'stopped'
self.Connect(-1, -1, EVT_RESULT_ID, self.OnResult)
self.worker = None
self.InitUI()
self.Centre()
self.Show()
def InitUI(self):
menuBar = wx.MenuBar()
FileBtn = wx.Menu()
InitItem = FileBtn.Append(wx.NewId(), 'Init')
NewItem = FileBtn.Append(wx.NewId(), 'New')
SaveItem = FileBtn.Append(wx.NewId(), 'Save')
ExitItem = FileBtn.Append(wx.ID_EXIT, 'Exit')
self.Bind(wx.EVT_MENU, self.InitSudoku, InitItem)
self.Bind(wx.EVT_MENU, self.BlankGrid, NewItem)
self.Bind(wx.EVT_MENU, self.SaveSudoku, SaveItem)
self.Bind(wx.EVT_MENU, self.Quit, ExitItem)
menuBar.Append(FileBtn, '&File')
OptionsBtn = wx.Menu()
TimerStartItem = OptionsBtn.Append(wx.NewId(), 'Start Timer')
TimerStopItem = OptionsBtn.Append(wx.NewId(), 'Stop Timer')
self.Bind(wx.EVT_MENU, self.StartTimer, TimerStartItem)
self.Bind(wx.EVT_MENU, self.StopTimer, TimerStopItem)
menuBar.Append(OptionsBtn, '&Options')
HelpBtn = wx.Menu()
menuBar.Append(HelpBtn, '&Help')
self.SetMenuBar(menuBar)
self.panel = wx.Panel(self)
self.font = wx.Font(wx.FontInfo(15).Bold())
self.InitGrid()
self.message_label = wx.StaticText(self.panel, label='', pos=(10,358))
self.timer_label = wx.StaticText(self.panel, label='00:00', pos=(280,358))
#self.PrintBtn = wx.Button(self.panel, pos=(0,0), size=(50,28), label='Print')
#self.PrintBtn.Bind(wx.EVT_BUTTON, self.PrintSudokus)
self.StartBtn = wx.Button(self.panel, START_ID, label='Solve', size=(50,28), pos=(88,0))
self.StartBtn.Bind(wx.EVT_BUTTON, self.OnStart, id=START_ID)
self.CheckBtn = wx.Button(self.panel, pos=(146,0), size=(50,28), label='Check')
self.CheckBtn.Bind(wx.EVT_BUTTON, self.CheckSudoku)
#self.TimeBtn = wx.Button(self.panel, pos=(202,0), size=(50,28), label='Start')
#self.TimeBtn.Bind(wx.EVT_BUTTON, self.StartTimer)
self.StopBtn = wx.Button(self.panel, STOP_ID, label='Stop', size=(50,28), pos=(202,0))
self.StopBtn.Bind(wx.EVT_BUTTON, self.OnStop, id=STOP_ID)
def Quit(self, event):
self.Close()
def InitGrid(self):
x, y = 10, 30
for i in range(1,10):
for j in range(1,10):
self.Entries[i,j] = wx.TextCtrl(self.panel, style=wx.TE_CENTRE,
pos=(x, y), size=(30, 30))
self.Entries[i,j].SetFont(self.font)
if j%3==0: x += 40
else: x += 35
if j==9: x = 10
if i%3==0: y += 40
else: y += 35
def BlankGrid(self, event, del_matrix=True):
self.message_label.SetLabel('')
self.ResetTimer()
if del_matrix: SOLVER.clear()
for i in range(1,10):
for j in range(1,10):
L = self.Entries[i,j].GetLineLength(0)
self.Entries[i,j].Remove(0,L)
def InitSudoku(self, event):
self.BlankGrid(None)
SOLVER.SUDOKU = SOLVER.sudoku_from_txt()
for i in range(1,10):
for j in range(1,10):
self.Entries[i,j].write(SOLVER.SUDOKU.matrix[i-1][j-1])
SOLVER.SUDOKU.draw()
def SaveSudoku(self, event):
if SOLVER.SOLUTION.valid() and SOLVER.SOLUTION.complete():
SOLVER.sudoku_to_txt()
self.message_label.SetLabel('Files saved...')
else:
self.message_label.SetLabel('Sudoku must be valid and complete!')
def CheckSudoku(self, event):
self.GetEntries(SOLVER.SOLUTION)
if not SOLVER.SOLUTION.complete():
self.message_label.SetLabel('Sudoku is not complete!')
elif not SOLVER.SOLUTION.valid():
self.message_label.SetLabel('Solution is not correct!')
else:
self.message_label.SetLabel('Solution correct :)')
def GetEntries(self, Sudoku):
for i in range(1,10):
for j in range(1,10):
val = self.Entries[i,j].GetLineText(0)
if val=='': Sudoku.matrix[i-1][j-1] = ' '
elif len(val)!=1: Sudoku.matrix[i-1][j-1] = ' '
elif not val.isdigit(): Sudoku.matrix[i-1][j-1] = ' '
elif int(val) < 1 or int(val) > 9: Sudoku.matrix[i-1][j-1] = ' '
else : Sudoku.matrix[i-1][j-1] = val
def ShowSolution(self, data):
self.BlankGrid(None, False)
for i in range(1,10):
for j in range(1,10):
self.Entries[i,j].write(data.matrix[i-1][j-1])
def StartTimer(self, event):
self.timer_mode = 'started'
def timer():
if self.timer_mode == 'stopped': return
if self.seconds == 60:
self.seconds = 0
self.minutes += 1
if self.seconds > 9 and self.minutes > 9:
self.timer_label.SetLabel('{}:{}'.format(self.minutes, self.seconds))
elif self.seconds > 9 and self.minutes <= 9:
self.timer_label.SetLabel('0{}:{}'.format(self.minutes, self.seconds))
elif self.seconds <= 9 and self.minutes <= 9:
self.timer_label.SetLabel('0{}:0{}'.format(self.minutes, self.seconds))
elif self.seconds <= 9 and self.minutes > 9:
self.timer_label.SetLabel('{}:0{}'.format(self.minutes, self.seconds))
self.seconds += 1
wx.CallLater(1000, timer)
timer()
def StopTimer(self, event):
self.timer_mode = 'stopped'
def ResetTimer(self):
self.minutes = 0
self.seconds = 0
self.timer_mode = 'stopped'
self.timer_label.SetLabel('0{}:0{}'.format(self.minutes, self.seconds))
def OnStart(self, event):
clues, valid = SOLVER.SUDOKU.clues(), SOLVER.SUDOKU.valid()
self.message_label.SetLabel('clues: {}, valid: {}'.format(clues, valid))
if not clues >= 17 or not valid:
self.message_label.SetLabel('This sudoku does not have a solution')
else:
matrix = SOLVER.SUDOKU.copy(SOLVER.SUDOKU.matrix)
if not self.worker:
self.message_label.SetLabel('Backtracking started...')
self.worker = WorkerThread(self, matrix)
else:
self.message_label.SetLabel('WARNING: Still backtracking...')
def OnStop(self, event):
if self.worker:
SOLVER.DISABLED = True
self.message_label.SetLabel('backtracking aborted')
self.worker.abort()
else:
self.message_label.SetLabel('currently not backtracking')
def OnResult(self, event):
if event.data is None:
self.ShowSolution(SOLVER.SUDOKU)
self.message_label.SetLabel('Computation aborted')
else:
self.ShowSolution(event.data)
self.message_label.SetLabel('Solution found! {} backtracks needed'.format(SOLVER.BACKTRACKS))
self.worker = None
SOLVER.BACKTRACKS = 0
SOLVER.DISABLED = 0
"""
def PrintSudokus(self, event):
print('Worker: ', self.worker)
print('Sudoku:')
SOLVER.SUDOKU.draw()
print('Solution:')
SOLVER.SOLUTION.draw()
"""
if __name__ == '__main__':
app = wx.App()
SudokuSolver(None)
app.MainLoop()
# Solver File
import SudokuSolverV2_GUI as SudokuSolver
import SudokuSolverV2_SudokuClass as SUDOKU_
import os
from random import randint
def sudoku_from_txt():
sudoku_dir = os.path.join(os.getcwd(), 'sudokus')
if not os.path.exists(sudoku_dir):
Sudoku = SUDOKU_.Sudoku(EXAMPLE)
return Sudoku
Sudoku = SUDOKU_.Sudoku()
Files = []
for filename in os.listdir(sudoku_dir):
if filename.startswith('sudoku') and filename.endswith('.txt'):
Files.append(filename)
if len(Files) < 1:
print('ERROR: No Sudokus saved, yet! Try this one')
return Sudoku(example)
else:
R = randint(0, len(Files)-1)
sudokuFile = open(os.path.join(sudoku_dir,Files[R]))
row, col = 0, 0
for value in sudokuFile.read():
if value.isdigit() or value==' ':
if value=='0': value = ' ' # zeros can also be used as blanks
Sudoku.matrix[row][col] = value
col += 1
if col==9:
if row==8: break
row += 1
col = 0
sudokuFile.close()
if not Sudoku.valid():
print('{} does not contain a valid sudoku'.format(Files[R]))
return SUDOKU.Sudoku()
else:
return Sudoku
def sudoku_to_txt():
sudoku_dir = os.path.join(os.getcwd(), 'sudokus')
if not os.path.exists(sudoku_dir): os.makedirs(sudoku_dir)
filecount = 1 # variable for how many sudoku files are already existing
while os.path.isfile(os.path.join(sudoku_dir,("sudoku%s.txt" % filecount))):
filecount += 1
# ----- create textfiles for sudoku and the solution ----- #
sudokuFile = open(os.path.join(sudoku_dir,('sudoku%s.txt' % filecount)), 'w')
solutionFile = open(os.path.join(sudoku_dir,('solution%s.txt' % filecount)), 'w')
for row in range(9):
for col in range(9):
sudokuFile.write(SUDOKU.matrix[row][col])
solutionFile.write(SOLUTION.matrix[row][col])
sudokuFile.write('\n')
solutionFile.write('\n')
sudokuFile.close()
solutionFile.close()
def consistent(matrix, row, col, value):
for i in range(9):
if matrix[row][i]==value: return False
if matrix[i][col]==value: return False
rowStart = row - row%3
colStart = col - col%3
for m in range(3):
for k in range(3):
if matrix[rowStart+k][colStart+m]==value: return False
return True
def solve(matrix, num):
global BACKTRACKS, DISABLED
if DISABLED: return True
BACKTRACKS += 1
if num==81:
global SOLUTION
SOLUTION = SUDOKU_.Sudoku(matrix)
return True
else:
row = int(num / 9)
col = num % 9
if matrix[row][col]!=' ':
solve(matrix, num+1)
else:
for value in range(1,10):
if consistent(matrix, row, col, str(value)):
matrix[row][col] = str(value)
if solve(matrix, num+1): return True
matrix[row][col]=' '
return False
def clear():
SUDOKU.matrix = SUDOKU.emptySudoku()
SOLUTION.matrix = SOLUTION.emptySudoku()
DISABLED = False
BACKTRACKS = 0
SUDOKU = SUDOKU_.Sudoku()
SOLUTION = SUDOKU_.Sudoku()
EXAMPLE = [[' ',' ',' ','2','1',' ',' ',' ',' '],
[' ',' ','7','3',' ',' ',' ',' ',' '],
[' ','5','8',' ',' ',' ',' ',' ',' '],
['4','3',' ',' ',' ',' ',' ',' ',' '],
['2',' ',' ',' ',' ',' ',' ',' ','8'],
[' ',' ',' ',' ',' ',' ',' ','7','6'],
[' ',' ',' ',' ',' ',' ','2','5',' '],
[' ',' ',' ',' ',' ','7','3',' ',' '],
[' ',' ',' ',' ','9','8',' ',' ',' ']]
if __name__ == '__main__':
Sudoku = sudoku_from_txt()
Sudoku.draw()
# SudokuClass File
class Sudoku():
def __init__(self, *args):
if len(args) > 0:
self.matrix = self.copy(args[0])
else:
self.matrix = self.emptySudoku()
def draw(self):
for row in range(9):
print("|-----------------------------------|")
for col in range(9):
print("| " + self.matrix[row][col] + " ",end='')
print("|")
print("|-----------------------------------|")
def get(self, row, col):
return self.matrix[row][col]
def copy(self, matrix):
matrix_copy = self.emptySudoku()
for row in range(9):
for col in range(9):
matrix_copy[row][col] = matrix[row][col]
return matrix_copy
def emptySudoku(self):
return [[" " for col in range(9)] for row in range(9)]
def complete(self):
for row in range(9):
for col in range(9):
if self.matrix[row][col] == ' ':
return False
return True
def valid(self):
for row in range(9):
for col in range(9):
val = self.matrix[row][col]
if val != ' ':
self.matrix[row][col] = ' '
if not self.consistent(row, col, val):
self.matrix[row][col] = val
return False
self.matrix[row][col] = val
return True
def clues(self):
clues = 0
for row in range(9):
for col in range(9):
if self.matrix[row][col] != ' ':
clues += 1
return clues
def consistent(self, row, col, value):
for i in range(9):
if self.matrix[row][i]==value: return False
if self.matrix[i][col]==value: return False
rowStart = row - row%3
colStart = col - col%3
for m in range(3):
for k in range(3):
if self.matrix[rowStart+k][colStart+m]==value: return False
return True
if __name__ == '__main__':
matrix = [[' ',' ',' ','2','1',' ',' ',' ',' '],
[' ',' ','7','3',' ',' ',' ',' ',' '],
[' ','5','8',' ',' ',' ',' ',' ',' '],
['4','3',' ',' ',' ',' ',' ',' ',' '],
['2',' ',' ',' ',' ',' ',' ',' ','8'],
[' ',' ',' ',' ',' ',' ',' ','7','6'],
[' ',' ',' ',' ',' ',' ','2','5',' '],
[' ',' ',' ',' ',' ','7','3',' ',' '],
[' ',' ',' ',' ','9','8',' ',' ',' ']]
S = Sudoku(matrix)
S.draw()
print('(1,4) = {}, valid = {}, clues = {}, complete = {}'.format(S.get(0,3),
S.valid(),
S.clues(),
S.complete()))
The "project" is still far from being finished. I am considering to add more options and some kind of README file.
I am very thankful for any improvement suggestions on the code or the "project" in general. I am still new to programming and I want to learn how to get better.