Vyrobíme si udělátko, které není součástí Tkinteru ani Pmw.
V dnešní lekci si vyrobíme udělátko TreeWidget. Jedná se o takový rozevírací seznam. Jeho konstrukce je celkem složitá, takže tento díl kurzu je určen spíše pokročilejším pythonýrům. A takto bude náš výtvor vypadat:
Základ třídy
Budeme potřebovat dva obrázky: minus.gif a plus.gif. Nějak takto by měla vypadat kostra třídy.
# -*- coding: cp1250 -*-
from Tkinter import*
class Tree(Frame):
def __init__(self, parent=None):
self.i_plus=PhotoImage(file="plus.gif")
self.i_minus=PhotoImage(file="minus.gif")
self.okno=self
Frame.__init__(self,parent)
ram=Frame(self)
ram.pack(fill=BOTH, expand=1)
scy=Scrollbar(ram)
scy.pack(fill=Y,side=RIGHT)
scx=Scrollbar(ram,orient="horizontal")
scx.pack(fill=X,side=BOTTOM)
self.platno=Canvas(ram,bg='white',yscrollcommand=scy.set,xscrollcommand=scx.set)
self.platno.pack(fill=BOTH, expand=1,side=LEFT)
scy["command"]=self.platno.yview
scx["command"]=self.platno.xview
#
#
methods = Pack.__dict__.keys()
methods = methods + Grid.__dict__.keys()
methods = methods + Place.__dict__.keys()
for m in methods:
if m[0] != '_' and m != 'config' and m != 'configure':
setattr(self, m, getattr(self, m))
if __name__ == "__main__":
okno=Tk()
t=Tree(okno)
t.pack()
mainloop()
Kód, který je již součástí třídy, je samotné GUI programu a poslední část se stará o to, abychom mohli použít grid,pack a place.
Systém ukládání hodnot
Než se pustíme do samotné realizace projektu, měli bychom vymyslet nějaký způsob, jakým budeme vkládat hodnoty do udělátka. Každá hodnota bude mít vlastní id
. Tyto idéčka budeme ukládat do slovníku. Druhou stranu slovníku bude tvořit seznam, do kterého budeme ukládat následující hodnoty: id
, nadřazené id
, text
a status
. První tři hodnoty jsou snad jasné a hodnota status
bude reprezentovat, jestli je daná větev rozbalená (open
) nebo zavřená (close
). Nyní bychom již mohli udělat část metody insert
:
def insert(self, kam,co):
self.id=self.id+1#ve funkci __init__ musíme vytvořit proměnnou id=0
self.slovnik[self.id]=[self.id,kam,co,"close"]#Standardně budou všechny větve zavřené
return self.id
Jak vidíte, metoda insert
vrací self.id
. Doufám, že již všichni pochopili, proč to tak je, ale raději to vysvětlím. Budeme tak vytvářet různé větve, takže pokud bychom chtěli vytvořit něco takového:
Použijeme metodu insert
následujícím způsobem:
t=Tree(okno)
t.pack()
vetev=t.insert(0,"čísla")#Pokud něco vkládáme na základní úroveň, použijeme vždy 0
t.insert(vetev,"jedna")#vlož "jedna" do čísel
t.insert(vetev,"dva")
t.insert(0,"písmena")
Po každém přidání nové hodnoty se nebude celé plátno překreslovat, to by trvalo moc dlouho. Proto budeme volat metodu update
:
def update(self):
self.platno.delete(ALL)#Vyčištění plochy
self.y=10
self.vykresli(0,10)#Vysvětlíme si o odstavec později
Nyní bychom měli napsat nějakou vykreslovací funkci. Budeme jí říkat například vykresli
. Ta bude přijímat parametr id
a odsazeni
. Pak nějak vybereme z self.slovnik
všechny hodnoty na základní úrovni a předáme je jiné funkci, která je zobrazí. Funkce, která bude hledat položky patřící do daného ID, by mohla vypadat takto:
def najdi_polozky(self,id):
s=[ ]
for prvek in self.slovnik.values():#procházej všechny hodnoty ve slovníku
if prvek[1] == id:#pokud se id rovná nadřazené skupině, přidej ho do seznamu
s.append(prvek)
return s
Tím pádem bude vypadat funkce vykresli
takto:
def vykresli(self, id,odsazeni=10):
s=self.najdi_polozky(id)
for prvek in s:
self.maluj(prvek,odsazeni)
Použili jsme funkci maluj
, která ještě není definovaná, nicméně je celkem jasné, co by měla dělat. My zatím pouze vypíšeme text s patřičným odsazením.
def maluj(self, prvek, odsazeni):
self.platno.create_text(odsazeni,self.y,text=unicode(prvek[2],"cp1250"),anchor="w")#Používáme unicode, aby se správně zobrazila diakritika
self.y=self.y+20#Posuneme se o 20 pixelů níže.
Vložíme-li tedy nějaké hodnoty na pozici 0
a zavoláme funkci update()
, mělo by se objevit něco takového:
Pro jistotu uvedu kód, kterým jsme dosáhli předchozího obrázku:
t=Tree(okno)
t.pack()
t.insert(0,"písmena")
t.insert(0,"čísla")
t.update()
Nyní bychom se měli pustit do vykreslovaní vložených hodnot (na jiné než základní úrovni). Proto budeme muset trochu upravit funkci vykresli
. Tato funkce by měla rekurzivně volat sama sebe, pokud některý z prvků má status open
. Zároveň by se mělo zvětšit odsazení
o 10
.
def vykresli(self, id,odsazeni):
s=self.najdi_polozky(id)
for prvek in s:
self.maluj(prvek,odsazeni,zn=zn)
if prvek[3] == "open":#Pokud má prvek status open
self.vykresli(prvek[0],odsazeni+10)#Zavolej vykresli s id=prvek[0]
Nyní, kdybychom vyzkoušeli znovu pustit program, neuvidíme žádnou změnu, protože všechny statusy
jsou standardně nastavené na close
. Proto musíme vytvořit rozevírací tlačítka. Rozevírací tlačítko budeme vykreslovat ve funkci maluj
. Ale jak poznat kdy ho vykreslit? Já jsem použil způsob zavolání funkce najdi_polozky
a pokud by délka vráceného seznamu byla větší než nula, je jasné, že musíme vykreslit tlačítko. Vzniká ale další problém. Jak poznat, jestli máme vykreslit obrázek s plusem nebo s mínusem? Řešení je velmi jednoduché. Při vkládání záznamu do udělátka definujeme, že je close
. Takže vše vyřešíme jednou podmínkou.
#Musíte definovat "self.znacky" ve funkci __init__
#Následující kód patří do funkce maluj
if len(self.najdi_polozky(prvek[0])) != 0:
image=self.i_minus
if prvek[3] == "close":
image=self.i_plus
i=self.platno.create_image(odsazeni,self.y,image=image)
self.znacky[i]=prvek
Snad si ještě vzpomínáte na dřívější lekce o Tkinteru, kde jsme si vysvětlovali metodu tag_bind
.
self.platno.tag_bind("otevrit_zavrit","<1>",self.otevrit_zavrit)#Tento kód přidejte do funkce __init__
Jak vidíte, musíme definovat metodu otevrit_zavrit
. V dřívějších lekcích jsme si vysvětlovali, jak získat objekt plátna, na který jsme klikli. Ale teď potřebujeme zjistit ID
objektu. Proto jsme vytvářeli slovník self.znacky
. Pomocí něho zvládneme získat ID objektu:
def otevrit_zavrit(self, akce):
akce= self.platno.find_withtag(CURRENT)[0]#získej objekt
opak={"open":"close","close":"open"}[self.znacky[akce][3]]#Získej opak: close->open; open->close
self.znacky[akce][3]=opak#Ulož změněnou hodnotu
self.update()
Nyní bychom měli opět vyzkoušet, jak bude náš program vypadat, když ho spustíme:
t=Tree(okno)
t.pack()
vetev=t.insert(0,"čísla")
t.insert(vetev,"jedna")
t.insert(vetev,"dva")
t.insert(0,"písmena")
t.update()
Po vyzkoušení tohoto kódu na nás vykoukne toto:
Jak vidíte, obrázky nepěkně vstupují do textu. Proto musíme každý text, vedle kterého vykreslujeme obrázek, posunout o 12 pixelů doprava. Toto určitě zvládnete sami. Poslední věc, co musíme udělat, je vykreslovaní čar. Naštěstí se nejedná o nic složitého, proto s tím budeme hotovi celkem rychle.
Pro každou hodnotu, kterou do udělátka vložíme, budeme muset vykreslit dvě čáry. Následujícím obrázkem se pokusím nastínit jak vykreslit čáry.
Nyní můžeme již vykreslit obě čáry.
#Tento kód přidejte do funkce maluj
#Vložte ho před vykreslování rozevíracích tlačítek
self.platno.create_line(odsazeni-10, self.y-17,odsazeni-10, self.y+3)#vertikální čára
self.platno.create_line(odsazeni-10, self.y+2,odsazeni-2, self.y+2)#horizontální čára
Teď můžeme opět vyzkoušet program:
Ejhle! Chybí nám tam jedna čára. Chyba je ve funkci vykresli
. Předtím, než vykreslíme podsložky (tedy vložené hodnoty se statusem open
), musíme uložit do proměnné pozice
hodnotu self.y
. Po vykreslení podsložek vykreslíme čáru se souřadnicemi odsazeni-10, pozice-20, odsazeni-10, self.y-20
. Funkce vykresli
tedy bude vypadat takto:
def vykresli(self, id,odsazeni):
s=self.najdi_polozky(id)
for prvek in s:
pozice=self.y
self.maluj(prvek,odsazeni)
if prvek[3] == "open":
self.vykresli(prvek[0],odsazeni+10)
cara=self.platno.create_line(odsazeni-10,pozice-20,odsazeni-10,self.y-20)
self.platno.lower(cara)
Použili jsme možná pro někoho neznámou metodu self.platno.lower(objekt)
. Takto funkce posouvá objekt na spodek zásobníku. Nyní je hlavní část programu za námi a zbývá doladit některé maličkosti.
Funkční posuvníky
Možná jste si všimli, že pokud vložíte do udělátka více hodnot, posuvníky se neaktivují. My musíme ještě manuálně určit, která oblast bude skrolovatelná. Budeme potřebovat metodu bbox:
#Tento kod přidejte nakonec funkce update
self.platno["scrollregion"]=self.platno.bbox(ALL)
Výběr položky
Máme sice funkční udělátko, ale nemůžeme vybrat žádnou hodnotu. Já jsem se rozhodl pro asi nejjednodušší řešení, nicméně není úplně vhodné pro velké množství hodnot, protože je časově náročné.
Já jsem přidal do seznamu, který vytváříme v metodě insert
, položku "unselect"
. Potom musíme definovat tag (například vybrat
) a tento tag přiřadit textu, který vytváříme v metodě maluj
. V metodě self.vybrat
musíme podobně jako v metodě otevrit_zavrit
zjistit ID prvku a následně nahradit unselect
na select
(popř. obráceně):
def vybrat(self, akce):
prvek=self.znacky[self.platno.find_withtag(CURRENT)[0]]
prvek[4]={"select":"unselect","unselect":"select"}[prvek[4]]
self.update()#Opět překreslíme celou plochu
Pokud bychom tuto funkci dále neupravovali, mohli bychom vybrat neomezený počet položek, proto na začátku musíme zavolat metodu unselect_all
, která by mohla vypadat takto:
def unselect_all(self):
for prvek in self.slovnik.values():
prvek[4]="unselect"
Pokud bychom program teď spustili, zjistíme, že vybraná položka není zvýrazněná. Proto musíme ve funkci maluj
přidat podmínku, že pokud prvek[4] == "select"
zabarvíme plochu za textem do modra a změníme barvu písma na bílou. O zabarvení plochy za textem se obstará metoda self.platno.create_rectangle(x1,y1,x2,y2,fill='blue')
. Velikost obdélníku zjistíme nám už známou metodu bbox(objekt)
:
barva="black"
if prvek[4] == "select":
barva="white"
text=self.platno.create_text(odsazeni+pricist,self.y,text=unicode(prvek[2],"cp1250"),anchor="w",tags=("vybrat"),fill=barva)
if prvek[4] == "select":
x1,y1,x2,y2=self.platno.bbox(text)
vyber=self.platno.create_rectangle(x1,y1,x2,y2,fill='blue')
self.platno.lower(vyber)
Získání výběru
Musíme samozřejmě definovat metodu self.get
, která bude vracet právě vybraný text. Tato metoda je velmi jednoduchá. Budeme pouze procházet self.slovnik.values()
a bude položka vybrána (select
), vrátíme její hodnotu:
def get(self):
for prvek in self.slovnik.values():
if prvek[4] == "select":
return prvek[2]
return ""
Zdrojový kód
Protože dnešní lekce byla trochu složitější, radši si ukážeme, jak vypadá zdrojový kód:
# -*- coding: cp1250 -*-
#TreeWidget
from Tkinter import*
import tkFont
class Tree(Frame):
def __init__(self, parent=None):
self.id=0
self.i_plus=PhotoImage(file="plus.gif")
self.i_minus=PhotoImage(file="minus.gif")
self.parent=self
Frame.__init__(self,parent)
ram=Frame(self)
ram.pack(fill=BOTH, expand=1)
scy=Scrollbar(ram)
scy.pack(fill=Y,side=RIGHT)
scx=Scrollbar(ram,orient="horizontal")
scx.pack(fill=X,side=BOTTOM)
self.platno=Canvas(ram,bg='white',yscrollcommand=scy.set,xscrollcommand=scx.set)
self.platno.pack(fill=BOTH, expand=1,side=LEFT)
self.platno.tag_bind("otevrit_zavrit","<1>",self.otevrit_zavrit)
self.platno.tag_bind("vybrat","<1>",self.vybrat)
scy["command"]=self.platno.yview
scx["command"]=self.platno.xview
self.seznam=[]
self.slovnik={}
self.znacky={}
self.texty={}
self.update()
methods = Pack.__dict__.keys()
methods = methods + Grid.__dict__.keys()
methods = methods + Place.__dict__.keys()
for m in methods:
if m[0] != '_' and m != 'config' and m != 'configure':
setattr(self, m, getattr(self, m))
def vybrat(self, akce):
prvek=self.znacky[self.platno.find_withtag(CURRENT)[0]]
self.unselect_all()
prvek[4]={"select":"unselect","unselect":"select"}[prvek[4]]
self.update()
def otevrit_zavrit(self, akce):
akce= self.platno.find_withtag(CURRENT)[0]
opak={"open":"close","close":"open"}[self.znacky[akce][3]]
self.znacky[akce][3]=opak
self.update()
def maluj(self, prvek,odsazeni):
pricist=0
self.platno.create_line(odsazeni-10, self.y-17,odsazeni-10, self.y+3)#vertical line
self.platno.create_line(odsazeni-10, self.y+2,odsazeni-2, self.y+2)
if len(self.najdi_polozky(prvek[0])) != 0:
image=self.i_minus
if prvek[3] == "close":image=self.i_plus
i=self.platno.create_image(odsazeni,self.y,image=image,tags=("otevrit_zavrit"))
self.znacky[i]=prvek
pricist=12
barva="black"
if prvek[4] == "select":
barva="white"
text=self.platno.create_text(odsazeni+pricist,self.y,text=unicode(prvek[2],"cp1250"),anchor="w",tags=("vybrat"),fill=barva)
if prvek[4] == "select":
x1,y1,x2,y2=self.platno.bbox(text)
vyber=self.platno.create_rectangle(x1,y1,x2,y2,fill='blue')
self.platno.lower(vyber)
self.znacky[text]=prvek
self.parent.image=[self.i_plus,self.i_minus]
self.y=self.y+20
def unselect_all(self):
for prvek in self.slovnik.values():
prvek[4]="unselect"
def update(self):
self.platno.delete(ALL)
self.x=10
self.y=10
self.vykresli(0,10)
self.platno["scrollregion"]=self.platno.bbox(ALL)
def vykresli(self, id,odsazeni):
s=self.najdi_polozky(id)
for prvek in s:
pozice=self.y
self.maluj(prvek,odsazeni)
if prvek[3] == "open":
self.vykresli(prvek[0],odsazeni+10)
cara=self.platno.create_line(odsazeni-10,pozice-20,odsazeni-10,self.y-20)
self.platno.lower(cara)
def najdi_polozky(self,id):
s=[]
for prvek in self.slovnik.values():
if prvek[1] == id:
s.append(prvek)
return s
def insert(self,kam,co,status="close"):
self.id=self.id+1
s=[self.id,kam,co,status,"unselect"]
self.slovnik[self.id]=s
return self.id
def get(self):
for prvek in self.slovnik.values():
if prvek[4] == "select":
return prvek[2]
return ""
if __name__ == "__main__":
okno=Tk()
t=Tree(okno)
t.pack()
vetev=t.insert(0,"čísla")
t.insert(vetev,"jedna")
t.insert(vetev,"dva")
t.insert(0,"písmena")
t.update()
mainloop()