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 x
ové a y
ové 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, otevrit.png, 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. 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 Label
z self.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, vynulovat.png, 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 celého programu.