× Aktuálně z oboru

Programátoři po celém světě dnes slaví Den programátorů [ clanek/2018091300-programatori-po-celem-svete-dnes-slavi-den-programatoru/ ]
Celá zprávička [ clanek/2018091300-programatori-po-celem-svete-dnes-slavi-den-programatoru/ ]

Tkinter - Řešitel sudoku

[ http://programujte.com/profil/1835-jakub-vojacek/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/118-zdenek-lehocky/ ]Google [ ?rel=author ]       26. 2. 2008       31 545×

Ukážeme si dva algoritmy na řešení sudoku a nakonec naprogramujeme grafické prostředí.

Tak jak jste si přečetli již v perexu, pokusíme se naprogramovat vlastního řešitele sudoku. Nepůjde tedy jen o Tkinter, protože musíme vymyslet nějaký algoritmus, který mřížku vyřeší. V první části článku tedy tento algoritmus naprogramujeme a v druhé části uděláme grafické prostředí.

Algoritmus na řešení sudoku

Na Internetu najdete spoutu způsobů na řešení sudoku. Některé z nich doplňují mřížku zkoušením všech kombinací, některé se snaží postupovat více jako člověk. Dle mého názoru je nejlepší kombinace obou těchto algoritmů. Nejprve se tedy pokusíme vyřešit sudoku podobnými metodami, jakými postupujete, pokud sudoku řešíte a když nenajdeme řešení, zkusíme otestovat všechny kombinace.


V sudoku jsou v podstatě tři pravidla, kterých se musíme držet při doplňování mřížky. Ve vertikální řadě smí být zastoupeno každé číslo pouze jednou. V horizontální řadě smí být zastoupeno každé číslo pouze jednou. V každém z devíti čtverců (plocha 3×3) smí být zastoupeno každé číslo pouze jednou. Prázdná mřížka bude tedy vypadat nějak takto:

Ta malá čísla na každém políčku znázorňují, jaká čísla lze na toto políčko vypsat. Jestliže doplníme několik čísel, bude mřížka vypadat takto:

Na obrázku jasně vidíte, že v posledním políčku na první řádce lze doplnit pouze jedno číslo.

Nyní, když víme, co se snažíme naprogramovat, můžeme přistoupit k samotné realizaci. Každé políčko bude reprezentovat samostatná třída Policko:

class Policko:
    def __init__(self, hodnota):
        self.hodnota=hodnota
        if hodnota == 0:
            self.moznosti=range(1,10)
        else:
            self.moznosti=[]
    def __str__(self):
        return str(self.hodnota)

Pokud tedy nebudeme vědět, jaké číslo se na políčku nachází, bude hodnota self.moznosti nabývat hodnoty [1, 2, 3, 4, 5, 6, 7, 8, 9]. V opačném případě bude samozřejmě hodnota [].

Programu budeme sudoku zadávat pomocí vnořených seznamů. Zadání si můžete sami vytvořit, nebo použít například toto:

zadani=[[9, 0, 1, 0, 5, 0, 0, 4, 0],
       [2, 4, 0, 0, 6, 0, 0, 0, 3],
       [0, 0, 6, 9, 0, 0, 0, 5, 0],
       [0, 0, 0, 6, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 9, 2, 0, 0, 0],
       [0, 0, 0, 0, 0, 3, 0, 0, 0],
       [0, 9, 0, 0, 0, 7, 5, 0, 0],
       [3, 0, 0, 0, 1, 0, 0, 7, 9],
       [0, 8, 0, 0, 2, 0, 4, 0, 6]
           ]

Musíme vytvořit třídu ResLogicky a v ní funkci, která by dokázala jednotlivá čísla nahradit třídou Policko.

class ResLogicky:
    def __init__(self, zadani):
        self.zadani=zadani
        self.zadani=self.uprav_zadani(self.zadani)
        print self.zadani
    def uprav_zadani(self, zadani):
        for radek in range(len(zadani)):
            for cislo in range(len(zadani[radek])):
                zadani[radek][cislo]=Policko(zadani[radek][cislo])
        return zadani

V předchozím kódu se prováděl příkaz print self.zadani. Vypsalo vám to 81 instancí třídy Policko. Tento výpis je velmi nepřehledný a hlavně z něj nic nepoznáme, takže doporučuji přidat třídě Policko speciální metodu __repr__ se stejným obsahem jako má metoda __str__, tedy:

def __repr__(self):
        return str(self.hodnota)

Nyní by to chtělo napsat si funkci, která projde jednotlivě všechny řádky a dle čísel upraví Policko.moznosti. Zároveň musíme přidat metodu odstran do třídy Policko. Tato metoda bude přijímat jako parametr seznam (řádek). Tímto seznam projde a nenulové prvky vymaže z self.moznosti.

#metoda odstran z třídy Policko:
    def odstran(self, radek):
        for prvek in radek:
            hodnota=prvek.hodnota
            if hodnota in self.moznosti:
                self.moznosti.remove(hodnota)
#metoda horizontalne z třídy ResLogicky:
    def horizontalne(self):
        for radek in self.zadani:
            for cislo in radek:
                cislo.odstran(radek)

Obě metody jsou velmi jednoduché. Zbývá naprogramovat opravování možností v jednotlivých sloupcích a pak ve čtvercích.

    def vertikalne(self):
        for x in range(len(self.zadani)):
            s=[]
            for y in range(len(self.zadani[x])):
                s.append(self.zadani[y][x])
            for y in range(len(self.zadani[x])):
                self.zadani[y][x].odstran(s)

Metoda vertikalne už nemusí být pro každého tak jasná jako horizontalne. V metodě horizontalne jsme měli výhodu, že v self.zadani reprezentuje jeden vnořený seznam jeden řádek. V metodě vertikalne potřebujeme pracovat s jednotlivými sloupci, tedy xtým prvkem v každém řádku. Nejtěžší ale bude metoda pro opravování možností ve čtvercích.

    def ctverce(self):
        for X in range(3):
            for Y in range(3):
                s=[]
                for x in range(X*3,X*3+3):
                    for y in range(Y*3,Y*3+3):
                        s.append(self.zadani[x][y])
                for prvek in s:     
                    prvek.odstran(s)

Jak známo, v klasickém sudoku tvoří čtverce plocha 3×3 políčka a právě toho v metodě ctverce využíváme. Postupně procházíme celou mřížku odshora dolů zleva doprava. K tomu právě slouží proměnné X a Y, které postupně nabývají hodnot [0,1,2]. Vysvětlíme si to lépe na příkladu. V prvním cyklu, kdy X=0, Y=0, se snažíme analyzovat čtverec v levém horním rohu. Tento čtverec tvoří tři první buňky v prvních třech řádcích, proto tedy range(X*3,X*3+3) a range(Y*3,Y*3+3).

Nyní, když self.zadani proženeme všemi třemi metodami, možná získáme nějakou buňku, do které pasuje pouze jedno číslo (délka Policko.moznosti je rovna jedné). Tuto úlohu svěříme metodě dopln. Ta projde celou mřížku a pokud se někde bude délka Policko.moznosti rovnat jedné, změní Policko.hodnota právě na tu jedinou hodnotu v Policko.moznosti.

    def dopln(self):
        for x in range(len(self.zadani)):
            for y in range(len(self.zadani[x])):
                bunka=self.zadani[x][y]
                if len(bunka.moznosti) == 1:
                    bunka.hodnota=bunka.moznosti[0]
                    bunka.moznosti=[]	

Pokud tedy vyzkoušíme přidat následující kód do metody __init__:

self.horizontalne()
self.vertikalne()
self.ctverce()
self.dopln()
print self.zadani

Rozhodně nečekejte, že mřížka už bude vyplněná. Konkrétně bylo doplněno pouze jedno číslo. Je to tím, že ještě musíme přidat nějaký kód do metody dopln a celý cyklus několikrát opakovat. Občas totiž může nastat situace, že do nějaké buňky, která má v Policko.moznosti více jak jednu možnost, můžete i přesto doplnit právě jenom jedno číslo. Například, ve čtverci je políčko, které má Policko.moznosti=[7,8,9], ale my na toto políčko doplníme 7, protože je to jediné políčko ve čtverci, které má v Policko.moznosti číslo 7.

#přidejte následující kód do metody dopln
cisla=range(1,10)
for X in range(3):
    for Y in range(3):
        s=[]
        for x in range(X*3,X*3+3):
            for y in range(Y*3,Y*3+3):
                s.append(self.zadani[x][y])
        moznosti=[]
        hodnoty=[]
        for prvek in s:moznosti=moznosti+prvek.moznosti
        for prvek in s:hodnoty.append(prvek.hodnota)
        for cislo in cisla:
            if moznosti.count(cislo) == 1 and cislo not in hodnoty:
                for bunka in s:
                    if cislo in bunka.moznosti:
                        bunka.hodnota=cislo
                        bunka.moznosti=[]

Stejně se ale nevyplnila celá mřížka. Abychom vyplnili celou mřížku, musíme doplňování zopakovat vícekrát.Jelikož nevíme, kolikrát musíme zopakovat celý cyklus, musíme použít cyklus while.

while 1:
    self.horizontalne()
    self.vertikalne()
    self.ctverce()
    self.dopln()

No jo, ale nyní nám program nikdy neskončí, protože se zasekne v nekonečné smyčce. Z cyklu můžeme vystoupit tehdy, když se self.zadani po metodě self.dopln nezměnilo. Nastal ale další problém. V self.zadani reprezentujeme políčka pomocí třídy Policko, bylo by to dobré změnit na číslo (int).

    def ciselne_zadani(self, z):
        zadani=[]
        for radek in z:
            rada=[]
            for cislo in radek:
                rada.append(int(cislo.hodnota))
            zadani.append(rada)
        return zadani

Nyní konečně můžeme dokončit cyklus:

while 1:
    kopie=self.ciselne_zadani(self.zadani)
    self.horizontalne()
    self.vertikalne()
    self.ctverce()
    self.dopln()
    if self.ciselne_zadani(self.zadani) == kopie:
        break

Nyní by váš program měl vyřešit převážnou většinu sudoku. Bohužel ale existují i takové zadání, které není tak lehké vyřešit. Proto naprogramujeme třídu ResNumericky. V této třídě se pokusíme doplnit mřížku pomocí náhodně vybraných čísel. I když, ta čísla nebudou až zas tak náhodná. Kdybychom totiž zkoušeli do poloprázdné mřížky vkládat čísla stylem random.randint(1,9) trvalo by moc dlouho, než bychom našli nějaké řešení. Chce to tedy vymyslet nějaký způsob jak postupně otestovat všechny kombinace.

Řešení je lehké. Na začátku máme nevyřešené sudoku. Najdeme první prázdné místo a vložíme tam jedničku. Otestujeme, zda tam jednička může být (zda není nějaká další jednička ve stejném sloupci/řádku/čtverci). Pokud tam jedničku nemůžeme vložit, nahradíme ji dvojkou a znovu testujeme, zda tam může být. Opět, pokud tam nemůže být zvětšíme o jedna (tedy na trojku). Pokud tam ovšem ono číslo pasuje, můžeme se posunout na další prázdné políčko a provádět samý postup, tj. vložit jedničku a testovat. Pokud tam pasuje, posunout dál, pokud ne, tak zvýšit. Pokud je na políčku devítka, tak tu již pochopitelně zvýšit nemůžeme. Vymažeme proto ono políčko (aby bylo opět prázdné) a vrátíme se na předchozí políčko, které nebylo na začátku vyplněno. Zvětšíme hodnotu tohoto políčka a posunujeme se opět dopředu. Tento algoritmus se slovy poněkud špatně vysvětluje, snad to lépe pochopíte ze samotného kódu.

class ResNumericky:
    def __init__(self, zadani):
        self.zadani=zadani
        self.kontrola=copy.deepcopy(self.zadani)
        self.nuly=[]
        cislo=0
        for prvek in self.zadani:
            for x in prvek:
                if x == 0:
                    self.nuly.append(cislo)
                cislo=cislo+1

V tomto způsobu řešení již nebudeme pravovat s třídou Policko. Byla by to zbytečná ztráta času. Bohatě stačí, když hodnotu políčka bude reprezentovat číslo. Seznam self.kontrola vytváříme proto, že později budeme potřebovat, na kterém místě byla původně nula (které políčko nebylo vyplněné). Kdybychom pracovali pouze s self.zadani a nevytvářeli kopii, nikdy bychom nemohli zjistit, na jakém místě nula byla. Možná vám přijde způsob, jakým je kopie vytvářená, trochu zvláštní. Jde o to, že Python pracuje se seznamy trochu podivným způsobem. Totiž, pokud vytváříte kopii seznamu, tak se nevytváří objekty z prvního seznamu znovu. V kopii se na ně pouze odkáže.

>>> s=[[1,2,3]]
>>> kopie=s
>>> kopie
[[1, 2, 3]]
>>> kopie[0][0]=0
>>> kopie
[[0, 2, 3]]
>>> s
[[0, 2, 3]]
>>> 

Toto ošetříme právě modulem copy:

>>> import copy
>>> s=[[1,2,3]]
>>> kopie=copy.deepcopy(s)
>>> kopie[0][0]=0
>>> kopie
[[0, 2, 3]]
>>> s
[[1, 2, 3]]
>>> 

Seznam self.nuly vytváříme, protože se v algoritmu musíme vracet na předchozí prázdné políčko. Index těchto políček jsou uloženy právě v tomto seznamu.

Celý výpočet bude obstarávat metoda self.pocitej:

    def pocitej(self):
        pos=0
        while pos >= 0:
            x=pos/9
            y=pos%9
            if pos == 81:
                return self.zadani
            if self.kontrola[x][y] == 0:
                h=self.zadani[x][y]+1
                if hodnota > 9:
                    self.zadani[x][y] = 0
                    pos=self.find_vzad(pos)
                else:
                    if self.pasuje(self.zadani,x,y,h):
                        pos=pos+1
                    self.zadani[x][y]=hodnota
            else:
                pos=pos+1
        return False
    def find_vzad(self,pos):
        pos=self.nuly.index(pos)-1
        if pos < 0:return -1
        pos=self.nuly[pos]
        return pos
    def pasuje(self,pole, x,y,co):
        #horizontální kontrola:
        if co in pole[x]:
            return False
        #vertikální kontrola:
        s=[]
        for prvek in pole:
            s.append(prvek[y])
        if co in s:
            return False
        #čtvercová kontrola
        cx,cy=x/3,y/3
        s=[]
        for x in range(cx*3,cx*3+3):
            for y in range(cy*3,cy*3+3):
                s.append(pole[x][y])       
        if co in s :
            return False
        return True

Jak vidíte, celý algoritmus je velmi jednoduchý. Nyní, když už jsme konečně dokončili algoritmus na řešení sudoku, můžeme přistoupit k tomu, co je skutečnou náplní tohoto kurzu.

Grafické prostředí

Nejprve si musíme rozmyslet, jak bude naše aplikace vypadat. Nahoře bude nástrojová lišta tvořená udělátkem Frame. V této liště budou tlačítka Nový hlavolam, Otevřít a Uložit. Pod touto lištou bude na levé straně krajní menu (taky Frame) s tlačítky jako Vyřešit a časomírou. Napravo bude mřižka tvořená udělátkem Canvas.

V nákresu jste si mohli přečíst, že použijeme vám zatím neznámou metodu Canvas.create_image(x,y,window=udelatko). Vlastně to zobrazí jakékoliv udělátko na danou pozici, jak ukazuje následující příklad:

from Tkinter import*
def klik():
    print "Ahoj!"
platno=Canvas()
udelatko=Button(platno, text="Klik!", command=klik)
platno.pack()
platno.create_window(150, 150, window=udelatko)
mainloop()

Nejprve si připravíme okno.

class Gui:
    def __init__(self, okno):
        self.okno=okno
        self.menubar=Frame(self.okno,height=20)
        self.menubar.pack(fill=X)
        ram=Frame(self.okno)
        ram.pack()
        self.platno=Canvas(ram,bg="#687fda", width=295, height=310)
        self.platno.pack(side=RIGHT)
        self.postranni_menu=Frame(ram,bg="#687fda", width=100)
        self.postranni_menu.pack(side=LEFT,fill=Y)
if __name__ == "__main__":
    okno=Tk()
    Gui(okno)
    mainloop()

Políčka tvoří pole 9×9. Použijeme tedy 2 cykly, abychom je vykreslili na plátno:

for x in range(9):
    for y in range(9):
        pole=Label(self.platno, bg='white',text="", width=4, height=2)
        self.platno.create_window(x*32+20,y*34+20,window=pole)

Vždy bude vybrané pouze jedno políčko. Vybrané políčko se od ostatních bude odlišovat barevně. Výběr se bude měnit myší a směrovými klávesami. Číslice se budou klávesnicí zapisovat do právě vybraného políčka. Barva pro vybrané políčko bude #808080. Políčka budeme ukládat do seznamu self.tlacitka. Poté, co políčka vykreslíme, musíme prvnímu políčku dát focus (nastavit zvýrazněnou barvu).

self.tlacitka[0]["bg"]="#808080"
self.vybrane_tlacitko=self.tlacitka[0]

Jak jsem říkal, výběr se bude přepínat levým tlačítkem myši. Proto musíme každému políčku přidat údalost <1>.

pole.bind("<1>",self.klik)

Přičemž v metodě self.klik změníme barvu políčka, na které jsme kliknuli, na barvu zvýrazněného políčka. Také musíme aktuálně stisknuté tlačítko přiřadit proměnné self.vybrane_tlacitko, abychom mohli později zjistit, do kterého tlačítka vložit číslo. Jak zjistit objekt, který událost vyvolal, jste se učili už v geonových lekcích, ale pro jistotu:

def klik(self, udalost):
    udalost.widget["bg"]="#808080"
    self.vybrane_tlacitko=udalost.widget

Pokud kód vyzkoušíte, tak uvidíte, že se barva políček, na které jsme kliknuli, sice mění, ale políčka, která byla zvýrazněná předtím, si svoji barvu stále zachovávají. Toto ošetříme metodou self.prebarvi_tlacitka:

def prebarvi_tlacitka(self):
    for tl in self.tlacitka:
        tl["bg"]="white"

Pokud tuto metodu zavoláte v metodě klik, bude vždy zvýrazněno pouze jedno políčko.

Další věc, kterou musíme udělat, je naprogramovat vkládání čísel do vybraného tlačítka. Proto celému oknu (self.okno) přiřadíme událost <Key>.

self.okno.bind("<Key>",self.akce)

V metodě self.akce zjistíme, jaká klávesa byla stisknuta. Pokud se jednalo o číslo z rozmezí 1-9, tak toto číslo vložíme do aktuálně vybraného políčka. Pokud se jednalo o 0, tak vymažeme obsah aktuálně vybraného tlačítka.

Jak ale zjistit, jaká klávesa byla stisknuta? Je to velmi jednoduché. Metoda self.akce přijímá jako parametr udalost.char. A právě v této proměnné je uložen znak klávesy, která byla stisknuta. Následující příklad to ukazuje:

from Tkinter import*
okno=Tk()
def akce(udalost):
    print udalost.char
okno.bind("<Key>",akce)
mainloop()

Nyní už by neměl být problém napsat metodu self.akce:

def akce(self, udalost):
    pismeno = udalost.char
    if pismeno in ["1","2","3","4","5","6","7","8","9"]:
        self.vybrane_tlacitko["text"]=pismeno
    elif pismeno == "0":
        self.vybrane_tlacitko["text"]=""

Tímto způsobem zápisu se nám podařilo eliminovat jednu možnou chybu. Uživatel už totiž nemůže zadat nic jiného než číslo v rozmezí 1-9.

Nyní sice už můžete zapisovat čísla do mřížky, ale program zatím nemá tlačítko, které by sudoku vyřešilo. Toto tlačítko přidáme do postranního menu.

Button(self.postranni_menu, text=u"Vyřeš sudoku", command=self.vyres_sudoku).pack()

Jelikož jsme před chvíli dopsali algoritmus na řešení sudoku, neměl by být zas takový problém zakomponovat to do programu. Můžete si vybrat, jestli budete používat první, druhý nebo oba způsoby řešení. V tomto článku si ukážeme postup s oběma algoritmy najednou. Nejprve tedy proženeme zadání prvním algoritmem a posléze použijeme i druhý algoritmus. Poté výsledek zapíšeme do políček. Vzniká nám tady trochu problém. V grafickém prostředí máme všechna políčka v jednom seznamu, ale naše algoritmy potřebují zadání pomocí vnořených seznamů.

def vyres_sudoku(self):
      cislo=0
      zadani=[]
      for x in range(9):
          rada=[]
          for y in range(9):
              hodnota=self.tlacitka[x*9+y]["text"]
              if hodnota == "":hodnota=0
              hodnota=int(hodnota)
              rada.append(hodnota)
          zadani.append(rada)
      print zadani

V grafickém prostředí reprezentuje prázdné políčko prázdný řetězec, ale algoritmy řešící sudoku potřebují tyto prázdné řetězce změnit na 0.

zadani=self.priprav_zadani()
sudoku=ResLogicky(zadani)
vysledek=sudoku.vysledek()
sudoku=ResNumericky(vysledek)
vysledek = sudoku.vysledek()
print vysledek

Možná se divíte, kde se vzaly metody ResLogicky.vysledek a ResNumericky.vysledek. Jak víte, metoda __init__ musí vracet None, a proto si musíme vytvořit metodu vysledek, která vrátí self.zadani:

def vysledek(self):
    return self.zadani

Máme v proměnné vysledek uložen výsledek. Tento výsledek je opět reprezentován pomocí vnořených seznamů. Výsledek do políček zapíšeme pomocí metody zapis_vysledek:

def zapis_vysledek(self, vysledek):
      cislo=0
      for x in range(9):
          for y in range(9):
              hodnota=vysledek[x][y]
              self.tlacitka[cislo]["text"]=hodnota
              cislo=cislo+1

Tím, že jsme dovolili uživatelům zadávat pouze čísla, jsme jednu chybu eliminovali, ale další chyba nastane, pokud uživatel zadá například následující kombinaci:

My se pokusíme uživateli zabránit v zadání předchozí kombinace. Použijeme metodu ze třídy ResNumericky.pasuje. Můžete buď zkopírovat metodu z třídy ResNumericky do třídy Gui, nebo použít dědičnost a dědit třídu ResNumericky.

Kontrolu, zda uživatel může číslo do aktuálně vybraného políčka, budeme provádět v metodě akce. Jelikož metoda pasuje přijímá parametry pole, x,y, co, potřebujeme vytvořit zadání pomocí vnořených seznamů. Na to použijeme metodu, kterou jsme si již naprogramovali, tedy self.priprav_zadani. Dále potřebujeme zjistit xové a yové souřadnice aktuálně vybraného políčka. To zjistíme pouze tehdy, když zjistíme index aktuálně vybraného políčka v seznamu self.tlacitka.

def akce(self, udalost):#pole,x,y,co
      pismeno = udalost.char
      if pismeno in ["1","2","3","4","5","6","7","8","9"]:
          zadani=self.priprav_zadani()
          index=self.tlacitka.index(self.vybrane_tlacitko)
          x=index/9
          y=index%9
          if self.pasuje(zadani, x,y,int(pismeno)):
              self.vybrane_tlacitko["text"]=pismeno
          else:
              tkMessageBox.showerror(u"Sudoku",u"Toto číslo sem nelze vložit.")
      elif pismeno == "0":
          self.vybrane_tlacitko["text"]=""

Nyní se dá říci, že máme skoro funkční řešitel. Já jsem ale velmi náročný, a proto náš program ještě vylepšíme.

Plocha sudoku se dělí na 9 čtverců o 3×3 políčkách. V našem programu toto není nijak odlišené, a proto to musíme napravit.

Musíme opravit metodu self.prebarvi_tlacitka. Když jsme programovali první algoritmus na řešení sudoku, použili jsme pro přístup ke čtvercům cyklus:

for X in range(3):
    for Y in range(3):
        for x in range(X*3,X*3+3):
            for y in range(Y*3,Y*3+3):

Podobný způsob použijeme v metodě self.prebarvi_tlacitka. Můžete si všimnout, že souřadnice políček jsou opět určeny pomocí dvou souřadnic x, y. My tyto souřadnice musíme převést do srozumitelného tvaru pro seznam self.tlacitka. Abychom tyto dvě souřadnice převedli do jedné, musíme použít vzorec x*9+y. Budou se střídat dvě barvy, white a grey.

def prebarvi_tlacitka(self):
    cislo=0
    barva="white"
    for X in range(3):
        for Y in range(3):
            barva={"grey":"white","white":"grey"}[barva]
            for x in range(X*3,X*3+3):
                for y in range(Y*3,Y*3+3):
                    pole=self.tlacitka[x*9+y]
                    pole["bg"]=barva

Další vymožeností, kterou bychom měli uživatelům nabídnout, je možnost posunovat výběr pomocí směrových kláves. Tím nám ale vyvstal další problém. Jak poznat, jaká směrová klávesa byla stisknuta? My máme již přesměrované všechny klávesové události do metody self.akce. Jenomže, pokud jste zkoušeli kód print udalost.char v metodě self.akce a stiskli jste směrovou klávesu, možná jste si všimli, že udalost.char je rovna "". Řešení je v použití keysym namísto char. Můžete si to vyzkoušet na následujícím příkladu:

from Tkinter import*
okno=Tk()
def akce(udalost):
    print udalost.keysym
okno.bind("<Key>",akce)
mainloop()

Nás budou zajímat 4 události: Left, Right, Up, Down.

Musíme si uvědomit jednu důležitou věc, a to jakým směrem se jednotlivá políčka vykreslují. Je to trochu matoucí, ale vykreslují se od shora dolů zleva do prava. Vybrané tlačítko zastupuje nějaký index v seznamu self.tlacitka. Pokud například zmáčkneme šipku dolů, musíme přičíst k indexu jedničku.

  • Up: Od indexu odečteme 1
  • Down: K indexu přičteme 1
  • Left: Od indexu odečteme 9
  • Right: K indexu přičteme 9

Celá podmínka v metodě self.akce tedy bude vypadat takto:

if udalost.keysym == "Down":
    pos=self.tlacitka.index(self.vybrane_tlacitko)+1
    self.vyber_tlacitko(pos)
elif udalost.keysym == "Up":
    pos=self.tlacitka.index(self.vybrane_tlacitko)-1
    self.vyber_tlacitko(pos)
elif udalost.keysym == "Right":
    pos=self.tlacitka.index(self.vybrane_tlacitko)+9
    self.vyber_tlacitko(pos)
elif udalost.keysym == "Left":
    pos=self.tlacitka.index(self.vybrane_tlacitko)-9
    self.vyber_tlacitko(pos)

Metoda self.vyber_tlacitko nastaví hodnotu proměnné self.vybrane_tlaciko na self.tlacitka[pos] a tomuto tlačítku také nastaví barvu vybraného tlačítka. Ještě byste měli ošetřit jednu chybu. Hodnota proměnné pos totiž může v krajních případech dosáhnout hodnot, které nebudou v rozsahu range(0,81), a proto vyvolají IndexError.

def vyber_tlacitko(self, pos):
    if pos not in range(0,81):
        return
    self.prebarvi_tlacitka()
    self.vybrane_tlacitko=self.tlacitka[pos]
    self.vybrane_tlacitko["bg"]="#808080"

Menubar

Je na čase vytvořit menubar. Do menubaru umístíme tři tlačítka: Nový soubor, Otevřít a Uložit. Tlačítka pochopitelně nebude reprezentovat text, ale obrázky: novy.png [ http://programujte.com/galerie/200802241921_novy.png ], otevrit.png [ http://programujte.com/galerie/200802241920_otevrit.png ], ulozit.png [ http://programujte.com/galerie/200802241919_ulozit.png ].

Obrázky jsou typu png, a proto musíme použit knihovnu PIL. Podobně jako v předchozích lekcích si vytvoříme metodu vrat_obr.

def vrat_obr(self, f):
    obr=ImageTk.PhotoImage(Image.open(f))
    self.obrazky.append(obr)
    return obr

A takto vložíme tlačítko do menubaru:

Button(self.menubar, image=self.vrat_obr("novy.png"),relief="flat", overrelief="raised").pack(side=LEFT)

Tlačítka budou samozřejmě tři.

První tlačítko je Nový soubor. Po kliknutí na toto tlačítko by se měla mřížka vyprázdnit. Jenomže uživatel na toto tlačítko mohl kliknout omylem, a proto se ho musíme zeptat, zda chce tuto akci opravdu provést. K tomu použijeme modul tkMessageDialog:

>>> import tkMessageBox
>>> tkMessageBox.askyesno('Sudoku',u'Opravdu chcete začít nový hlavolam? Všechny předchozí akce budou ztraceny')

V dialogu se nachází dvě tlačítka Ano, Ne. To, na co kliknete, ovlivní návratou hodnotu. Tedy, pokud kliknete na Ano, je návratová hodnota rovna True, v opačném případě False.

Pro zprovoznění dvou dalších tlačítek (Otevřít a Uložit) budeme potřebovat modul tkFileDialog. Data budeme ukládat pomocí modulu pickle [ http://programujte.com/index.php?akce=clanek&cl=2007052102 ]. Tímto modulem ale nemůžeme ukládat instance Tkinteru, ale pouze datové typy, takže nepřichází v úvahu použít pickle.dump(self.tlacitka, soubor). Musíme tedy přeměnit instance Labelself.tlacitka na čísla, ale to není problém:

def ulozit(self):
    soubor=tkFileDialog.asksaveasfilename(parent=self.okno,title= "Uložit jako...")
    if not soubor:return
    zadani=[]
    for tl in self.tlacitka:
        hodnota=tl["text"]
        if hodnota == "":hodnota=0
        zadani.append(int(hodnota))
    f=file(soubor, "w")
    pickle.dump(zadani, f)
    f.close()
def otevrit(self):
    soubor=tkFileDialog.askopenfilename(parent=self.okno,title= "Otevřít")
    if not soubor:return
    f=file(soubor,"r")
    zadani=pickle.load(f)
    f.close()
    for x in range(len(zadani)):
        hodnota=zadani[x]
        if hodnota == 0:hodnota=""
        self.tlacitka[x]["text"]=str(hodnota)

Není to nic těžkého. Ještě bychom měli ošetřit jednu chybu. Uživatel totiž může při otevírání vybrat soubor, který nebyl vytvořen naším programem. V takovém případě nastane vyjímka KeyError. Ale to snad zvládnete ošetřit sami.

Ukazování možností

Na prvních dvou obrázcích tohoto článku jste si mohli všimnou jedné zajímavé vychytávky. Jedná se o ukazování čísel, které se dají do políčka zapsat. My se něco podobného pokusíme implementovat do našeho programu s jedním rozdílem. Ony první dva obrázky jsou z programu udělaného v grafické knihovně WxPython, která je daleko mocnější než Tkinter, a nebylo by lehké udělat něco podobného. Proto budeme zobrazovat možnosti pouze aktuálně vybraného tlačítka. Tyto moznosti budeme zapisovat do udělátka Entry v postranním menu.

obal2=LabelFrame(self.postranni_menu, text=u"Možnosti",bg="#687fda")
obal2.pack(fill=X)
self.moznosti=Entry(obal2)
self.moznosti.pack(fill=X)

Toto je jeden z důvodů, proč jsme vlastně programovali první algoritmus na řešení sudoku. On by totiž stačil pouze druhý algoritmus a dosáhli bychom při řešení stejných výsledků. Ale první algoritmus už umí hledat možnosti pro jednotlivá tlačítka. Vzpomeňte si na seznam Policko.moznosti. V tomto seznamu jsou pro každé políčkou uloženy možnosti. Musíme proto nějak zprovnoznit metody uprav_zadani, vertikalne, horizontalne, ctverce z třídy ResLogicky v třídě Gui. Já navrhuji použít dědičnost, ale samozřejmě můžete uvedené metody zkopírovat.

Ale tři z těchto čtyř metod musíme upravit. Je řeč o metodách vertikalne, horizontalne, ctverce. Tyto metody nepřijímají žádný parametr a pracují s proměnnou self.zadani. Abychom mohli tyto metody používat ve třídě Gui, musíme jim přidat parametr pole, se kterým budou pracovat namísto self.zadani.

    def ukazovat_moznosti(self):
        zadani=self.priprav_zadani()
        sudoku=self.uprav_zadani(zadani)
        self.vertikalne(sudoku)
        self.horizontalne(sudoku)
        self.ctverce(sudoku)
        pos=self.tlacitka.index(self.vybrane_tlacitko)
        x=pos/9
        y=pos%9
        pole=sudoku[x][y]
        if pole.hodnota == 0:
            self.moznosti.delete(0,END)
            self.moznosti.insert(END, pole.moznosti)
        else:
            self.moznosti.delete(0,END)
            self.moznosti.insert(END,"[]") 

Tuto metodu musíte volat pokaždé, když se změní výběr tlačítka.

Časomíra

Zbývá nám udělat časomíru, tedy něco jako stopky. Budeme k tomu potřebovat následující tři obrázky: spustit.png [ http://programujte.com/galerie/200802241922_spustit.png ], vynulovat.png [ http://programujte.com/galerie/200802241922_vynulovat.png ], zastavit.png [ http://programujte.com/galerie/200802241923_zastavit.png ].

Časomíru umístíme do postranního menu. Bude ji tvořit display (udělátko Entry) a pod ním tři tlačítka vedle sebe.

self.casomira=Entry(self.postranni_menu, bg="black", fg="green", font="Courier 10")
self.casomira.pack()
self.casomira.insert(END,"0:0:0")
obal=Frame(self.postranni_menu,bg="#687fda")
obal.pack(fill=X)
Button(obal,command=self.spustit_stopky, image=self.vrat_obr("spustit.png")).pack(side=LEFT)
Button(obal,command=self.zastavit_stopky, image=self.vrat_obr("zastavit.png")).pack(side=LEFT)
Button(obal,command=self.vynulovat_stopky, image=self.vrat_obr("vynulovat.png")).pack(side=LEFT)

Na začátku budou dvě proměnné:

self.cas=0
self.pustene_stopky=False

Metody spustit_stopky, zastavit_stopky, vynulovat_stopky budou hodnoty těchto proměnných patřičně měnit.

Je jasné, že hodnotu proměnné self.cas musíme každou sekundu zvýšit. Provedeme to pomocí funkce self.okno.after(1000, self.pricti_cas).

def pricti_cas(self):
    if self.pustene_stopky:
        self.cas=self.cas+1
    hodin=self.cas/3600
    minuty=(self.cas%3600)/60
    sekundy=(self.cas%3600)%60
    vypis="%s:%s:%s"%(hodin, minuty, sekundy)
    self.casomira.delete(0,END)
    self.casomira.insert(END, vypis)
    self.okno.after(1000,self.pricti_cas)

A náš program je hotový!

Zdrojový kód

Jelikož byl dnešní článek trochu obsáhlejší a složitější než předešlé články, příkládám i zdrojový kód [ http://programujte.com/storage/200802241932_ResitelSudoku.rar ] celého programu.


Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2008022400-tkinter-resitel-sudoku/ ].