× Aktuálně z oboru

Vychází Game Ready ovladače pro Far Cry 5 [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]
Celá zprávička [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]

Tkinter - TreeWidget

[ http://programujte.com/profil/1835-jakub-vojacek/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/2327-tomas-barton/ ]Google [ ?rel=author ]       3. 12. 2007       14 112×

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 [ http://programujte.com/galerie/200711241253_minus.gif ] a plus.gif [ http://programujte.com/galerie/200711241253_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í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 [ http://tkinter.programujte.com/canvas.htm#Tkinter.Canvas.bbox-method ]:

#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 malujpř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()

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