# 02.6: features # [1] disable initial cells # [2] integrate solver # [3] reuse hints # [4] make hintcell selection playable import logging if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) log = logging.getLogger() import tkinter as tk from tkinter.font import Font from collections import namedtuple RC = namedtuple('RC', 'rows columns') BBox = namedtuple('BBox', 'x y width height') class HintWindow(tk.Toplevel): def __init__(self, bbox, bg='yellow', alpha=0.3, state='normal'): assert state in ('normal', 'withdrawn') super().__init__(bg=bg) if state == 'withdrawn': self.withdraw() self.overrideredirect(True) self.geometry("%dx%d+%d+%d" % (bbox.width, bbox.height, bbox.x, bbox.y)) self.wm_attributes('-alpha', alpha) self._last_xy = bbox.x, bbox.y def move(self, dx, dy): x0, y0 = self._last_xy x1, y1 = x0 + dx, y0 + dy self.geometry("+%d+%d" % (x1, y1)) self._last_xy = x1, y1 def delete(self): self.destroy() def show(self): state = self.state() if state == 'withdrawn': self.deiconify() def hide(self): state = self.state() if state == 'normal': self.withdraw() class Cell: _iid = None _square_cfg = dict(fill='white', width=1, outline='white', activeoutline='red', activewidth=3, tag='cell') _is_initial = False _hintcell = None @property def iid(self): return self._square @property def size(self): return self.canvas.cellsize @property def is_initial(self): return self._is_initial def __init__(self, canvas, rc, coords, text_color='black', initial=True): self.canvas = canvas self.rc = rc self.coords = coords self.center = (coords[0] + coords[2]) // 2, (coords[1] + coords[3]) // 2 self._square = canvas.create_rectangle( coords, self._square_cfg) self._label = canvas.create_text(self.center, fill=text_color, text='', font=canvas.font, tag='cell') self.value = 0 canvas._items[self._label] = canvas._items[self._square] = self def set_value(self, value, initial=False): itemconfig = self.canvas.itemconfigure s = str(value) if value else '' itemconfig(self._label, text=s) self.value = value if initial: self.disable() self._is_initial = True ## if self._hintcell is not None: ## self._hintcell.master.hide() def disable(self): itemconfig = self.canvas.itemconfigure itemconfig(self._label, state='disabled') itemconfig(self._square, state='disabled') def create_circle(self): '''hint as canvas item''' canvas = self.canvas x, y = self.center D = (self.size // 2) - 2 assert D > 0 iid = canvas.create_oval( x - D, y - D, x + D, y + D, width=3, dash=(3, 1), outline='green') log.debug('circle: iid=%d' % iid) return iid def create_cross(self): '''hint as toplevel''' bbox = self.canvas.root_bbox(self.iid, 1, 1) w = HintWindow(bbox, bg='white', alpha=0.7) canvas = tk.Canvas(w, width=bbox.width, height=bbox.height, bd=0, highlightthickness=False, background='white') canvas.pack(fill='both') D = self.size - 2 x = y = 2 assert D > 0 canvas.create_line( x, y, x + D, y + D, width=3, fill='red') canvas.create_line( x, y + D, x + D, y, width=3, fill='red') return w def create_hintcell(self, values): '''hint as reusable toplevel''' if self._hintcell is None: bbox = self.canvas.root_bbox(self.iid, 1, 1) w = HintWindow(bbox, bg='white', alpha=0.7, state='withdrawn') #log.debug(bbox) hints = self._hintcell = HintsGrid(w, self) hints.pack(fill='both', padx=2, pady=2) self._hintcell.load(values) return self._hintcell def __str__(self): return 'cell(%d, %d)=%s' % (self.rc[0], self.rc[1], self.value) class Grid(tk.Canvas): _cfg = None cellsize = None font = None RC = None SPACING = None font_config = None def __init__(self, parent=None): self._cfg['width'] = self.RC[0] * (self.cellsize + self.SPACING) self._cfg['height'] = self.RC[1] * (self.cellsize + self.SPACING) super().__init__(parent, self._cfg) self.font = Font(**self.font_config) self._items = {} # maps iids to cells. self._cells = {} # maps (r, c) to cell for r in range(self.RC.rows): y0 = r * (self.cellsize + self.SPACING) y1 = y0 + self.cellsize for c in range(self.RC.columns): x0 = c * (self.cellsize + self.SPACING) x1 = x0 + self.cellsize rc = (r+1, c+1) self._cells[rc] = self.create_cell(rc, (x0, y0, x1, y1)) assert len(self._cells) == self.RC.rows * self.RC.columns def cell(self, r, c): if (r, c) in self._cells: return self._cells[(r, c)] log.warning('rc=(%d, %d) out of bound' % (r, c)) def root_bbox(self, iids, dx=0, dy=0): if not isinstance(iids, list): iids = (iids,) x0, y0, x1, y1 = self.bbox(*iids) w = x1 - x0 h = y1 - y0 x = self.winfo_rootx() + x0 y = self.winfo_rooty() + y0 return BBox(x+dx, y+dy, w-dx, h-dy) class SudokuGrid(Grid): _cfg = dict(bd=0, highlightthickness=False, background='grey') cellsize = 42 RC = RC(9, 9) SPACING = 2 font_config = dict(family='Helvetica', size=14) _toplevel = None _toplevel_lastxy = None _model = None def __init__(self, parent): self.create_cell = lambda rc, coords: Cell(self, rc, coords) super().__init__(parent) # create thick boxes assert self.RC == (9, 9) for c in range(3, self.RC.columns, 3): u = c * (self.cellsize + self.SPACING) - 1 self.create_line( (u, 0), (u, self._cfg['height']), fill='black', width=2) self.create_line( (0, u), (self._cfg['width'], u), fill='black', width=2) self._hints = [] self.bind('', self.on_keyPress) def load(self, initial_values): # set initial values for r, values in enumerate(initial_values, 1): [ self.cell(r, c).set_value(v, initial=True) for c, v in enumerate(values, 1) if v ] def update(self): super().update() toplevel = self.toplevel self._last_xy = toplevel.winfo_rootx(), toplevel.winfo_rooty() toplevel.bind('', self.on_moveHints) # maybe + def on_keyPress(self, event): value = event.keysym if event.keysym in '123456789' else '' iid = self.find_withtag('current')[0] if iid in self._items: self._items[iid].set_value(value) @property def toplevel(self): if self._toplevel is None: self._toplevel = self.winfo_toplevel() return self._toplevel def on_moveHints(self, event): toplevel = self.toplevel x0, y0 = self._last_xy x1, y1 = toplevel.winfo_rootx(), toplevel.winfo_rooty() dx, dy = x1-x0, y1-y0 [ h.move(dx, dy) for h in self._hints if isinstance(h, HintWindow) ] self._last_xy = x1, y1 def clear_hints(self): log.debug('clear_hints') [ h.delete() for h in self._hints if isinstance(h, HintWindow) ] # use tag instead of loop? [ self.delete(iid) for iid in self._hints if isinstance(iid, (int, str)) ] self._hints = [] # clear list def _highlight(self, cells): bb = self.root_bbox([ c.iid for c in cells ]) self._hints.append(HintWindow(bb)) def highlight_cell(self, r, c): self._highlight([ self.cell(r, c)]) def highlight_row(self, r): self._highlight([ self.cell(r, c) for c in range(1, self.RC.columns+1) ]) def highlight_column(self, c): self._highlight([ self.cell(r, c) for r in range(1, self.RC.rows+1) ]) def highlight_rectangle(self, rc1, rc2): r1, c1 = rc1 r2, c2 = rc2 self._highlight([ self.cell(r, c) for r in range(r1, r2+1) for c in range(c1, c2+1)]) def circle_cell(self, r, c): self._hints.append(self.cell(r, c).create_circle()) def cross_cell(self, r, c): self._hints.append(self.cell(r, c).create_cross()) def hint_cell(self, r, c, values): self._hints.append(self.cell(r, c).create_hintcell(values)) class HintsGrid(Grid): _cfg = dict(bd=0, highlightthickness=False, background='white') cellsize = 10 RC = RC(3, 3) SPACING = 4 font_config = dict(family='Helvetica', size=8) def __init__(self, parent, cell, values=None): self.create_cell = lambda rc, coords: Cell(self, rc, coords, text_color='blue') self._cell = cell super().__init__(parent) self.tag_bind('cell', '<1>', self.on_select) # deprecated interface if values is not None: self.load(values) def load(self,values): z = 1 for r in range(self.RC.rows): for c in range(self.RC.columns): cell = self.cell(r+1, c+1) if z in values: cell.set_value(z) else: cell.set_value(0) cell.disable() z += 1 self.master.show() def on_select(self, event): log.debug('on_select') iids = self.find_withtag('current') log.debug('on_select: iids=(%s)' % ', '.join(str(_) for _ in iids)) iid = iids[0] if iid in self._items: cell = self._items[iid] self._cell.set_value(cell.value) self.master.hide() self._cell.canvas.clear_hints() def delete(self): self.master.hide() if __name__ == '__main__': G = [ (0, 2, 0, 5, 0, 1, 0, 9, 0), (8, 0, 0, 2, 0, 3, 0, 0, 6), (0, 3, 0, 0, 6, 0, 0, 7, 0), (0, 0, 1, 0, 0, 0, 6, 0, 0), (5, 4, 0, 0, 0, 0, 0, 1, 9), (0, 0, 2, 0, 0, 0, 7, 0, 0), (0, 9, 0, 0, 3, 0, 0, 8, 0), (2, 0, 0, 8, 0, 4, 0, 0, 7), (0, 1, 0, 9, 0, 7, 0, 6, 0), ] app = tk.Tk() grid = SudokuGrid(app) grid.load(initial_values=G) grid.pack(fill='both') grid.update() # update grid to get real position from xxx_bbox grid.focus_set() grid.highlight_cell(2,8) grid.highlight_row(8) grid.highlight_column(5) grid.highlight_rectangle((4,1), (6,3)) grid.circle_cell(5, 2) grid.cross_cell(8,1) grid.hint_cell( 1, 1, (3, 4, 6, 7)) app.mainloop()