Hlavní navigace

Tvorba GUI v Pythonu: widgety pro zobrazení tabulek a stromů v knihovně appJar

Pavel Tišnovský

V páté části článku o knihovně appJar určené pro snadnou tvorbu aplikací s GUI v Pythonu si představíme některé pokročilejší ovládací prvky. Zaměříme se především na tvorbu tabulek (s omezenou možností jejich editace) a stromů.

Obsah

1. Pokročilejší ovládací prvky nabízené knihovnou appJar

2. Ovládací prvek grid – jednoduchá tabulka

3. První příklad – použití prvku grid pro zobrazení klávesnice telefonu

4. Vytvoření tabulky s použitím n-tic a generátorů

5. První řádek tabulky s nadpisy sloupců

6. Zobrazení tabulky s malou násobilkou

7. Získání informací o vybraných políčkách tabulky

8. Zpracování slovníku s informacemi o vybraných políčkách tabulky

9. Tlačítka zobrazená u každého řádku tabulky a reakce na jejich stisk

10. Zobrazení vstupních políček určených pro rozšíření tabulky

11. Kombinace předchozích dvou příkladů – rozšíření tabulky a výpočty nad daty v řádku

12. Ovládací prvek tree – strom zobrazující strukturu XML

13. Zobrazení jednoduchého stromu

14. Konfigurace barev použitých při vykreslení stromu

15. Strom s editovatelnými koncovými uzly

16. Získání obsahu vybraného uzlu

17. Repositář s demonstračními příklady

18. Odkazy na Internetu

1. Pokročilejší ovládací prvky nabízené knihovnou appJar

Všechny základní ovládací prvky (widgety) grafického uživatelského rozhraní, které jsou nabízeny knihovnou appJar, jsme si popsali v předchozích čtyřech částech tohoto seriálu. Již popsané widgety mohou být dostačující pro tvorbu jednodušších aplikací i pro výuku základních konceptů používaných v GUI (ostatně právě pro tento účel knihovna appJar vznikla), ovšem mnoho složitějších aplikací vyžaduje použití dalších ovládacích prvků. Mezi tyto vyžadované prvky patří zejména tabulka (zde nazývaná grid) a taktéž prvek pro zobrazení stromové struktury (tree). Tyto ovládací prvky nám knihovna appJar taktéž nabízí, ovšem s tím, že v současné verzi knihovny appJar jsou možnosti těchto prvků omezeny, což bude ostatně patrné i při spuštění demonstračních příkladů. Kromě toho knihovna appJar nabízí i několik dalších spíše jednoúčelových prvků, o nichž se samozřejmě taktéž ve stručnosti zmíníme.

2. Ovládací prvek grid – jednoduchá tabulka

Prvním ovládacím prvkem, s nímž se dnes seznámíme, je tabulka, která je v knihovně appJar nazývána grid. Při konstrukci gridu se konstruktoru představovanému metodou addGrid předává dvourozměrná datová struktura, která je v Pythonu reprezentována tím nejjednodušším možným způsobem, tj. seznamem obsahujícím jednotlivé řádky tabulky, přičemž každý řádek je taktéž tvořen seznamem:

tabulka = [
   ["prvky, "na", "prvním", "řádku"],
   ["prvky, "na", "druhém", "řádku"],
   ["prvky, "na", "třetím", "řádku"]
]

Pokud všechny podseznamy obsahují shodný počet prvků, vytvoří se pravidelná tabulka; v opačném případě budou některá políčka výsledné tabulky nevyplněna. Ve skutečnosti je však při konstrukci gridu možné namísto seznamů použít i n-tice (tuple) nebo generátory (generator), což si samozřejmě taktéž ukážeme v demonstračních příkladech.

3. První příklad – použití prvku grid pro zobrazení klávesnice telefonu

Ukažme si nyní, jakým způsobem je možné zobrazit jednoduchou tabulku s využitím ovládacího prvku grid. Nejprve vytvoříme zdrojová data, tj. seznam seznamů obsahujících jednotlivé prvky, které mají být v tabulce zobrazeny, a předáme ji konstruktoru ovládacího prvku grid. Prvek musí být pojmenován, což využijeme v dalších příkladech:

app.addGrid("grid",
            [[' 1 ', ' 2 ', ' 3 '],
             [' 4 ', ' 5 ', ' 6 '],
             [' 7 ', ' 8 ', ' 9 '],
             [' * ', ' 0 ', ' # ']])

Obrázek 1: Prozatím jen velmi primitivní tabulka s klávesnicí ze starých (dobrých) tlačítkových telefonů. Povšimněte si, že první řádek tabulky tvoří nadpisy sloupců.

Do okna navíc přidáme menu s příkazem Quit, což je téma, kterému jsme se již věnovali minule:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addGrid("grid",
            [[' 1 ', ' 2 ', ' 3 '],
             [' 4 ', ' 5 ', ' 6 '],
             [' 7 ', ' 8 ', ' 9 '],
             [' * ', ' 0 ', ' # ']])
 
app.go()

4. Vytvoření tabulky s použitím n-tic a generátorů

Ve druhé kapitole jsme si řekli, že při tvorbě tabulky je možné zdrojová data, která mají být zobrazena, vytvořit jako seznam seznamů, popř. že je možné použít n-tice (tuple) nebo generátory. Nejprve se podívejme na použití n-tic. Je to stejně snadné jako použití seznamů:

app.addGrid("grid",
            ((' 1 ', ' 2 ', ' 3 '),
             (' 4 ', ' 5 ', ' 6 '),
             (' 7 ', ' 8 ', ' 9 '),
             (' * ', ' 0 ', ' # ')))

Poznámka: n-tice jsou neměnitelné, což v některých případech může zjednodušit pochopení zdrojového kódu.

Obrázek 2: Prakticky stejná aplikace, která však nyní používá n-tice a nikoli seznamy pro zdrojová data tabulky.

Upravený zdrojový kód příkladu:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addGrid("grid",
            ((' 1 ', ' 2 ', ' 3 '),
             (' 4 ', ' 5 ', ' 6 '),
             (' 7 ', ' 8 ', ' 9 '),
             (' * ', ' 0 ', ' # ')))
 
app.go()

Použití generátorů si vysvětlíme na známé konstrukci range. Zdrojová data tabulky lze v případě potřeby (rozsáhlé tabulky) vytvořit i následujícím způsobem:

app.addGrid("grid",
            (range(1, 4),
             range(4, 7),
             range(7, 10),
             (' * ', ' 0 ', ' # ')))

Upravený zdrojový kód příkladu:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addGrid("grid",
            (range(1, 4),
             range(4, 7),
             range(7, 10),
             (' * ', ' 0 ', ' # ')))
 
app.go()

5. První řádek tabulky s nadpisy sloupců

Z předchozího screenshotu je patrné, že první řádek tabulky je zvýrazněn, protože slouží pro zobrazení nadpisů. V současné verzi knihovny appJar ovšem není možné nadpisy eliminovat, což znamená, že pokud nechceme, aby se první řádek tlačítek telefonu zobrazil v prvním řádku, musíme explicitně ve zdrojové tabulce vytvořit prázdný řádek, což není příliš elegantní řešení:

app.addGrid("grid",
            [['', '', ''],
             [' 1 ', ' 2 ', ' 3 '],
             [' 4 ', ' 5 ', ' 6 '],
             [' 7 ', ' 8 ', ' 9 '],
             [' * ', ' 0 ', ' # ']])

Obrázek 3: Nyní je řádek s nadpisy prázdný, ale stále viditelný.

Zdrojový text demonstračního příkladu se změní jen nepatrně:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addGrid("grid",
            [['', '', ''],
             [' 1 ', ' 2 ', ' 3 '],
             [' 4 ', ' 5 ', ' 6 '],
             [' 7 ', ' 8 ', ' 9 '],
             [' * ', ' 0 ', ' # ']])
 
app.go()

6. Zobrazení tabulky s malou násobilkou

V dalším příkladu si zobrazíme klasickou tabulku s malou násobilkou (resp. libovolně velkou tabulku s násobilkou, nemusíme se totiž omezit na činitele od jedné do deseti). Pro deklaraci zdrojových dat je možné použít hned několik metod pro vytvoření seznamu seznamů s hodnotami malé násobilky. Nejkratší je zápis využívající generátorovou notaci seznamu neboli list comprehension. Náš úkol je však nepatrně složitější, protože výsledkem nemá být jeden seznam, ale seznam seznamů, takže budeme postupovat tak, že nejdříve vytvoříme podseznamy, které představují řádky tabulky a z nich vytvoříme výsledný seznam řádků. Pro každý následující řádek se zvýší hodnota počitadla i. Funkci, která vrátí výslednou tabulku, je možné zapsat následovně:

def createTable(n):
    return [[i*j for i in range(1, n+1)] for j in range(1, n+1)]

Volání výše deklarované funkce createTable samozřejmě můžeme umístit přímo do konstruktoru tabulky:

app.addGrid("grid", createTable(10))

Obrázek 4: Malá násobilka.

Opět si ukažme, jak může vypadat úplný zdrojový kód tohoto příkladu:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def createTable(n):
    return [[i*j for i in range(1, n+1)] for j in range(1, n+1)]
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addGrid("grid", createTable(10))
 
app.go()

7. Získání informací o vybraných políčkách tabulky

Tabulka zobrazená ve widgetu grid není určena pouze pro zobrazení předem připravených hodnot, ale je ji možné použít i pro další operace, i když se samozřejmě v žádném případě nejedná o plnohodnotný tabulkový procesor. Při testování předchozích příkladů jste si pravděpodobně všimli, že jednotlivá políčka tabulek je možné vybírat pomocí myši. Pro získání informací o vybraných políčkách se používá metoda getGridSelectedCells, která vrací slovník, jehož klíči jsou adresy políček a hodnotami True pro vybrané políčko a False pro políčko nevybrané. Zkusme si nyní příklad upravit tak, aby dokázal zobrazit vybraná políčka. Nejdříve do okna přidáme nové tlačítko:

app.addButton("Show selected cells", onButtonPress)

Na stisk tohoto tlačítka je navázána callback funkce nazvaná onButtonPress, která přečte informace o tabulce pomocí výše zmíněné metody getGridSelectedCells a získanou informaci bez dalšího zpracování vytiskne na standardní výstup:

def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    print(cells)

Obrázek 5: Výběr buněk v tabulce.

Zdrojový kód takto upraveného příkladu vypadá následovně:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    print(cells)
 
 
def createTable(n):
    return [[i*j for i in range(1, n+1)] for j in range(1, n+1)]
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addGrid("grid", createTable(10))
 
app.addButton("Show selected cells", onButtonPress)
 
app.go()

8. Zpracování slovníku s informacemi o vybraných políčkách tabulky

Podívejme se nyní na příklad slovníku, který byl získán metodou getGridSelectedCells:

{
    '4-6': True, '4-7': False, '0-8': False, '7-8': False, '5-5': False,
    '8-7': False, '3-4': False, '7-7': False, '6-7': False, '7-4': False,
    '1-2': False, '2-5': True, '5-6': False, '0-3': False, '6-4': False,
    '6-9': False, '2-9': False, '5-1': False, '8-3': False, '0-7': False,
    '7-9': False, '3-2': False, '5-4': False, '5-3': False, '3-3': False,
    '1-7': False, '2-2': False, '4-4': False, '8-0': False, '8-9': True,
    '6-2': False, '7-3': False, '6-3': False, '8-2': False, '6-0': False,
    '5-9': False, '4-9': False, '4-3': False, '7-5': False, '0-9': False,
    '5-0': False, '8-5': False, '8-8': False, '0-5': False, '7-6': False,
    '1-5': False, '2-8': False, '1-9': False, '5-7': False, '1-3': False,
    '4-0': False, '2-3': False, '3-8': False, '5-2': False, '3-0': False,
    '7-2': False, '1-8': False, '3-6': False, '3-5': False, '0-0': False,
    '8-4': False, '1-1': False, '5-8': False, '0-4': False, '1-4': False,
    '2-6': False, '1-6': False, '6-5': False, '3-9': False, '4-2': False,
    '8-1': False, '8-6': False, '6-8': False, '3-1': False, '2-4': False,
    '6-6': False, '0-2': False, '6-1': False, '4-5': False, '1-0': False,
    '4-8': False, '0-6': False, '0-1': True, '4-1': False, '7-1': False,
    '2-0': False, '3-7': False, '7-0': False, '2-1': False, '2-7': False
}

Vidíme, že klíče ve slovníku představují adresu políčka zapsanou v řetězci, v němž je řádek a sloupec oddělen pomlčkou. Hodnoty pak skutečně nabývají hodnot True pro vybrané políčko a False pro políčko, které nebylo vybráno. Toho lze využít pro snadné získání adres pouze těch buněk, které jsou vybrány. Opět využijeme generátorovou notaci seznamu; tentokrát je ovšem iterační smyčka doplněna o podmínku na hodnotu záznamu ve slovníku:

cells = app.getGridSelectedCells("grid")
selectedCells = [c for c, v in cells.items() if v]

Příklad použití – výpis vybraných buněk tabulky:

def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    selectedCells = [c for c, v in cells.items() if v]
    selectedCells.sort()
    print(selectedCells)
    if selectedCells:
        message = " ".join(selectedCells)
        app.infoBox("Selected cells", message)
    else:
        app.warningBox("Warning", "Please select at least one cell")

V případě potřeby lze adresu políčka (řádek, sloupec) z řetězce získat například jednoduchým zpracováním řetězce s adresou (pro jednoduchost neprovádím v kódu žádné kontroly na korektnost vstupu):

def parseCellAddress(cell):
    a = cell.split("-")
    return (int(a[0]), int(a[1]))
 
 
def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    selectedCells = [c for c, v in cells.items() if v]
    selectedCells.sort()
    print(selectedCells)
    if selectedCells:
        message = " ".join(selectedCells)
        app.infoBox("Selected cells", message)
    else:
        app.warningBox("Warning", "Please select at least one cell")
    addresses = [parseCellAddress(c) for c in selectedCells]
    print(addresses)

Obrázek 6: Zobrazení vybraných buněk v tabulce.

Opět se podívejme na celý zdrojový kód příkladu:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    selectedCells = [c for c, v in cells.items() if v]
    selectedCells.sort()
    print(selectedCells)
    if selectedCells:
        message = " ".join(selectedCells)
        app.infoBox("Selected cells", message)
    else:
        app.warningBox("Warning", "Please select at least one cell")
 
 
def createTable(n):
    return [[i*j for i in range(1, n+1)] for j in range(1, n+1)]
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addGrid("grid", createTable(10))
 
app.addButton("Show selected cells", onButtonPress)
 
app.go()

9. Tlačítka zobrazená u každého řádku tabulky a reakce na jejich stisk

U každého řádku tabulky je možné zobrazit tlačítko, které po svém stisku zavolá callback funkci, jíž se předá obsah daného řádku tabulky. Ukažme si použití těchto tlačítek na jednoduché tabulce, ve které je zobrazen počet odpracovaných hodin pro jednotlivé zaměstnance:

table = [["Name", "Work hours"],
         ["Petr", 160],
         ["Pavel", 90],
         ["Honza", 120]]

Zobrazení tlačítek a registrace callback funkce se provede takto:

app.addGrid("grid", table, action=onGridButton)

V callback funkci pak z předaného parametru získáme obsah obou buněk na vybraném řádku tabulky (jména a počtu odpracovaných hodin):

name = values[0]
hours = int(values[1])

Z těchto údajů snadno vypočteme odpracované procento z normohodin a zobrazíme ho uživateli v dialogu:

norm = 160
utilization = 100.0 * hours / norm
message = "{h} of {n} hours: {u:.0f} %".format(h=hours, n=norm, u=utilization)
app.infoBox("Work hours for " + name, message)

Obrázek 7: Výpočet procent z pracovní doby.

Opět si ukažme, jak vypadá úplný demonstrační příklad:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    selectedCells = [c for c, v in cells.items() if v]
    selectedCells.sort()
    print(selectedCells)
    if selectedCells:
        message = " ".join(selectedCells)
        app.infoBox("Selected cells", message)
    else:
        app.warningBox("Warning", "Please select at least one cell")
 
 
def onGridButton(values):
    name = values[0]
    hours = int(values[1])
    norm = 160
    utilization = 100.0 * hours / norm
    message = "{h} of {n} hours: {u:.0f} %".format(h=hours, n=norm, u=utilization)
    app.infoBox("Work hours for " + name, message)
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
table = [["Name", "Work hours"],
         ["Petr", 160],
         ["Pavel", 90],
         ["Honza", 120]]
 
app.addGrid("grid", table, action=onGridButton)
 
app.addButton("Show selected cells", onButtonPress)
 
app.go()

10. Zobrazení vstupních políček určených pro rozšíření tabulky

Na poslední řádek tabulky je možné umístit vstupní políčka, která umožní rozšířit tabulku o další řádky. Postupovat budeme následovně. Nejprve povolíme zobrazení vstupních políček (a navíc i potvrzovacího tlačítka):

app.addGrid("grid", table, action=onAddRow, addRow=True)

Obrázek 8: Vyplnění dat pro nový řádek tabulky.

Dále připravíme callback funkci onAddRow tak, aby se zkontrolovala data zadaná uživatelem a pokud jsou data v pořádku, aby se do tabulky vložil nový řádek s těmito údaji. Povšimněte si, že se této callback funkci předává řetězec „newRow“, protože se jedná o stejnou callback funkci, jakou jsme použili v předchozím příkladu (což je, pravda, poněkud podivné). Uživatelem zadaná data se získají metodou getGridEntries:

def onAddRow(data):
    if data == "newRow":
        values = app.getGridEntries("grid")
        print(values)
        if values[0] and values[1]:
            app.addGridRow("grid", values)

Obrázek 9: Nová data byla skutečně do tabulky přidána.

Použití této callback funkce v příkladu může vypadat následovně:

#!/usr/bin/env python
 
from appJar import gui
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    selectedCells = [c for c, v in cells.items() if v]
    selectedCells.sort()
    print(selectedCells)
    if selectedCells:
        message = " ".join(selectedCells)
        app.infoBox("Selected cells", message)
    else:
        app.warningBox("Warning", "Please select at least one cell")
 
 
def onAddRow(data):
    if data == "newRow":
        values = app.getGridEntries("grid")
        print(values)
        if values[0] and values[1]:
            app.addGridRow("grid", values)
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
table = [["Name", "Work hours"],
         ["Petr", 160],
         ["Pavel", 90],
         ["Honza", 120]]
 
app.addGrid("grid", table, action=onAddRow, addRow=True)
 
app.addButton("Show selected cells", onButtonPress)
 
app.go()

11. Kombinace předchozích dvou příkladů – rozšíření tabulky a výpočty nad daty v řádku

Předchozí dva demonstrační příklady používaly shodnou callback funkci, ovšem v prvním příkladu byla tato funkce zavolána ve chvíli, kdy se měly zobrazit informace o odpracovaných hodinách zvoleného zaměstnance, zatímco v příkladu druhém sloužila tatáž callback funkce pro přidání nového řádku do tabulky. Pokud budeme chtít využít obě operace – výpočet i přidání nového řádku, lze postupovat například takto:

def onAddRow(data):
    if data == "newRow":
        addNewRow(data)
    else:
        showWorkHours(data)

Obrázek 10: Vyplnění dat pro nový řádek tabulky.

Tato podmínka bude fungovat vždy, a to i ve chvíli, kdyby tabulka měla jen jeden sloupec a uživatel zadal do vstupního pole taktéž řetězec „newRow“. V takovém případě by se callback funkci předal seznam s jediným prvkem. Další zpracování již známe:

def showWorkHours(values):
    name = values[0]
    hours = int(values[1])
    norm = 160
    utilization = 100.0 * hours / norm
    message = "{h} of {n} hours: {u:.0f} %".format(h=hours, n=norm, u=utilization)
    app.infoBox("Work hours for " + name, message)
 
 
def addNewRow(values):
    values = app.getGridEntries("grid")
    print(values)
    if values[0] and values[1] and re.search('\d', values[1]):
        app.addGridRow("grid", values)

Obrázek 11: Výpočet procent z pracovní doby pro nový řádek tabulky.

Úplný kód tohoto příkladu vypadá následovně:

#!/usr/bin/env python
 
from appJar import gui
import re
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def onButtonPress(buttonName):
    cells = app.getGridSelectedCells("grid")
    selectedCells = [c for c, v in cells.items() if v]
    selectedCells.sort()
    print(selectedCells)
    if selectedCells:
        message = " ".join(selectedCells)
        app.infoBox("Selected cells", message)
    else:
        app.warningBox("Warning", "Please select at least one cell")
 
 
def showWorkHours(values):
    name = values[0]
    hours = int(values[1])
    norm = 160
    utilization = 100.0 * hours / norm
    message = "{h} of {n} hours: {u:.0f} %".format(h=hours, n=norm, u=utilization)
    app.infoBox("Work hours for " + name, message)
 
 
def addNewRow(values):
    values = app.getGridEntries("grid")
    print(values)
    if values[0] and values[1] and re.search('\d', values[1]):
        app.addGridRow("grid", values)
 
 
def onAddRow(data):
    if data == "newRow":
        addNewRow(data)
    else:
        showWorkHours(data)
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
table = [["Name", "Work hours"],
         ["Petr", 160],
         ["Pavel", 90],
         ["Honza", 120]]
 
app.addGrid("grid", table, action=onAddRow, addRow=True)
 
app.addButton("Show selected cells", onButtonPress)
 
app.go()

12. Ovládací prvek tree – strom zobrazující strukturu XML

Další ovládací prvek, s nímž se v dnešním článku seznámíme, se jmenuje tree, takže už jeho název napovídá, že se jedná o widget určený pro zobrazení stromové struktury. Teoreticky by se mělo jednat o nejsložitější ovládací prvek vůbec (viz například poměrně těžko ovladatelný widget JTree z knihovny Swing pro Javu). Ve skutečnosti je však práce s widgetem tree velmi jednoduchá, protože zdrojem dat je prakticky jakýkoli řetězec obsahující validní XML. Takový řetězec může být zapsán přímo v programu, nebo ho je možné načíst z externího souboru, získat z webové služby apod. Navíc ovládací prvek tree umožňuje editaci listů stromu, tj. koncových uzlů. Pokud jsou například zdrojová data reprezentována tímto XML:

<html>
    <head>
        <title>Titulek</title>
    </head>
    <body>
        <div>První odstavec</div>
        <div>Druhý odstavec</div>
        <div>Třetí odstavec</div>
    </body>
</html>

vytvoří se strom, jehož kořen se bude jmenovat html, bude obsahovat dva poduzly head a body atd. Listy stromu pak budou obsahovat titulek a texty zapsané do odstavců:

Obrázek 12: Strom vytvořený z předchozího XML.

13. Zobrazení jednoduchého stromu

Zkusme si nyní podobný strom zobrazit v naší aplikaci. Nejdříve nadeklarujeme řetězec s XML:

treeContent = """
<html>
    <head>
        <title>Titulek</title>
    </head>
    <body>
        <div>První odstavec</div>
        <div>Druhý odstavec</div>
        <div>Třetí odstavec</div>
    </body>
</html>
"""

Obrázek 13: Zabalený strom.

Následně z těchto dat vytvoříme strom a zakážeme editaci jeho listů:

app.addTree("tree", treeContent)
app.setTreeEditable("tree", False)

A to je vše…

Obrázek 14: Strom po rozbalení některých uzlů.

Další příklad po svém spuštění zobrazí strom a taktéž menu s příkazem pro ukončení aplikace:

#!/usr/bin/env python
 
from appJar import gui
import re
 
 
from appJar import gui
 
app = gui()
 
treeContent = """
<html>
    <head>
        <title>Titulek</title>
    </head>
    <body>
        <div>První odstavec</div>
        <div>Druhý odstavec</div>
        <div>Třetí odstavec</div>
    </body>
</html>
"""
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addTree("tree", treeContent)
app.setTreeEditable("tree", False)
 
app.go()

14. Konfigurace barev použitých při vykreslení stromu

Knihovna appJar nabízí několik metod určených pro konfiguraci barev použitých při vykreslení stromu. Nastavit je možné barvu textu uzlů, pozadí celého stromu, barvu vybraného uzlu a taktéž barvu pozadí vybraného uzlu. Všechny čtyři zmíněné barvy lze – v tomto pořadí – specifikovat v metodě setTreeColours:

app.setTreeColours("tree", "black", "#aaffaa", "red", "yellow")

Obrázek 15: Strom se změněnou barvou pozadí.

Alternativně je možné barvy nastavit postupně jednou z těchto metod:

Metoda Význam
setTreeFg barva nevybraných uzlů
setTreeBg pozadí stromu
setTreeHighlightFg barva textu vybraného uzlu
setTreeHighlightBg pozadí vybraného uzlu

Obrázek 16: Změnila se i barva textu a pozadí vybraného uzlu.

Upravme si nyní náš demonstrační příklad takovým způsobem, aby pozadí stromu bylo světle zelené a vybrané uzly měly červený text a žluté pozadí:

#!/usr/bin/env python
 
from appJar import gui
import re
 
 
from appJar import gui
 
app = gui()
 
treeContent = """
<html>
    <head>
        <title>Titulek</title>
    </head>
    <body>
        <div>První odstavec</div>
        <div>Druhý odstavec</div>
        <div>Třetí odstavec</div>
    </body>
</html>
"""
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addTree("tree", treeContent)
app.setTreeEditable("tree", False)
app.setTreeColours("tree", "black", "#aaffaa", "red", "yellow")
 
app.go()

15. Strom s editovatelnými koncovými uzly

Pokud je zapotřebí zařídit, aby mohl uživatel editovat koncové uzly stromu (listy), postačuje zavolat následující metodu:

app.setTreeEditable("tree", True)

Poznámka: ve skutečnosti je strom editovatelný už ve výchozím nastavení, uzly je možné editovat po dvojkliku.

Obrázek 17: Editace uzlu stromu.

Úprava programu je zcela triviální (viz zvýrazněný příkaz):

#!/usr/bin/env python
 
from appJar import gui
import re
 
 
from appJar import gui
 
app = gui()
 
treeContent = """
<html>
    <head>
        <title>Titulek</title>
    </head>
    <body>
        <h1>Kapitola</h1>
        <div>První odstavec</div>
        <div>Druhý odstavec</div>
        <div>Třetí odstavec</div>
    </body>
</html>
"""
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addTree("tree", treeContent)
app.setTreeEditable("tree", True)
 
app.go()

Obrázek 18: Strom po editaci.

16. Získání obsahu vybraného uzlu

V dnešním posledním demonstračním příkladu si ukážeme, jakým způsobem se získá obsah (text) vybraného uzlu. K tomu poslouží metoda nazvaná getTreeSelected, které se pouze předá jednoznačný identifikátor widgetu:

item = app.getTreeSelected("tree")

Tuto metodu použijeme v callback funkci zavolané po stisku tlačítka přidaného do okna aplikace. V callback funkci jen zkontrolujeme, jestli je vůbec nějaký uzel stromu vybrán:

def onButtonPress(buttonName):
    item = app.getTreeSelected("tree")
    if item is None:
        app.warningBox("Warning", "No item (node) selected")
    else:
        app.infoBox("Selected item", item)

Registrace callback funkce se provádí nám již známým konstruktorem tlačítka:

app.addButton("Show selected item", onButtonPress)

Obrázek 19: Zobrazení obsahu vybraného uzlu stromu.

Opět následuje výpis zdrojového kódu celého příkladu:

#!/usr/bin/env python
 
from appJar import gui
import re
 
 
from appJar import gui
 
app = gui()
 
treeContent = """
<html>
    <head>
        <title>Titulek</title>
    </head>
    <body>
        <h1>Kapitola</h1>
        <div>První odstavec</div>
        <div>Druhý odstavec</div>
        <div>Třetí odstavec</div>
    </body>
</html>
"""
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def onButtonPress(buttonName):
    item = app.getTreeSelected("tree")
    if item is None:
        app.warningBox("Warning", "No item (node) selected")
    else:
        app.infoBox("Selected item", item)
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addTree("tree", treeContent)
app.setTreeEditable("tree", True)
 
app.addButton("Show selected item", onButtonPress)
 
app.go()

17. Repositář s demonstračními příklady

Zdrojové kódy všech čtrnácti dnes popsaných demonstračních příkladů byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/pre­sentations. Pokud nechcete klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

Příklad Adresa
55_simple_grid.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/55_simple_grid­.py
56_simple_grid_from_tuple.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/56_simple_grid_from_tu­ple.py
57_simple_grid_from_range.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/57_simple_grid_from_ran­ge.py
58_grid_1st_row.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/58_grid_1st_row­.py
59_multiply_table.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/59_multiply_ta­ble.py
60_selected_cells.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/60_selected_ce­lls.py
61_better_selected_cells.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/61_better_se­lected_cells.py
62_grid_buttons.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/62_grid_but­tons.py
63_add_row.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/63_add_row­.py
64_add_row_compute_hours.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/64_add_row_com­pute_hours.py
65_simple_tree.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/65_simple_tre­e.py
66_tree_colors.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/66_tree_co­lors.py
67_editable_tree.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/67_editable_tre­e.py
68_get_node.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/68_get_node­.py

Poznámka: pro úspěšné spuštění těchto příkladů musíte mít v aktuálním adresáři rozbalenou knihovnu appJar!. Podrobnosti o instalaci jsme si řekli v úvodním článku.

18. Odkazy na Internetu

  1. Hra Breakout napísaná v Tkinteri
    https://www.root.cz/clanky/hra-breakout-napisana-v-tkinteri/
  2. Hra Snake naprogramovaná v Pythone s pomocou Tkinter
    https://www.root.cz/clanky/hra-snake-naprogramovana-v-pythone-s-pomocou-tkinter/
  3. The Java™ Tutorials: How to Use Trees
    https://docs.oracle.com/ja­vase/tutorial/uiswing/com­ponents/tree.html
  4. Tree (data structure)
    https://en.wikipedia.org/wi­ki/Tree_%28data_structure%29
  5. TkDND
    http://freecode.com/projects/tkdnd
  6. Python Tkinter Fonts
    https://www.tutorialspoin­t.com/python/tk_fonts.htm
  7. The Tkinter Canvas Widget
    http://effbot.org/tkinter­book/canvas.htm
  8. Ovládací prvek (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Ovl%C3%A1dac%C3%AD_prvek_­%28po%C4%8D%C3%ADta%C4%8D%29
  9. Rezervovaná klíčová slova v Pythonu
    https://docs.python.org/3/re­ference/lexical_analysis.html#ke­ywords
  10. TkDocs: Styles and Themes
    http://www.tkdocs.com/tuto­rial/styles.html
  11. Drawing in Tkinter
    http://zetcode.com/gui/tkin­ter/drawing/
  12. Changing ttk widget text color (StackOverflow)
    https://stackoverflow.com/qu­estions/16240477/changing-ttk-widget-text-color
  13. The Hitchhiker's Guide to Pyhton: GUI Applications
    http://docs.python-guide.org/en/latest/scenarios/gui/
  14. 7 Top Python GUI Frameworks for 2017
    http://insights.dice.com/2014/11/26/5-top-python-guis-for-2015/
  15. GUI Programming in Python
    https://wiki.python.org/mo­in/GuiProgramming
  16. Cameron Laird's personal notes on Python GUIs
    http://phaseit.net/claird/com­p.lang.python/python_GUI.html
  17. Python GUI development
    http://pythoncentral.io/introduction-python-gui-development/
  18. Graphic User Interface FAQ
    https://docs.python.org/2/faq/gu­i.html#graphic-user-interface-faq
  19. TkInter
    https://wiki.python.org/moin/TkInter
  20. Tkinter 8.5 reference: a GUI for Python
    http://infohost.nmt.edu/tcc/hel­p/pubs/tkinter/web/index.html
  21. TkInter (Wikipedia)
    https://en.wikipedia.org/wiki/Tkinter
  22. appJar
    http://appjar.info/
  23. appJar (Wikipedia)
    https://en.wikipedia.org/wiki/AppJar
  24. appJar na Pythonhosted
    http://pythonhosted.org/appJar/
  25. appJar widgets
    http://appjar.info/pythonWidgets/
  26. Stránky projektu PyGTK
    http://www.pygtk.org/
  27. PyGTK (Wikipedia)
    https://cs.wikipedia.org/wiki/PyGTK
  28. Stránky projektu PyGObject
    https://wiki.gnome.org/Pro­jects/PyGObject
  29. Stránky projektu Kivy
    https://kivy.org/#home
  30. Stránky projektu PyQt
    https://riverbankcomputin­g.com/software/pyqt/intro
  31. PyQt (Wikipedia)
    https://cs.wikipedia.org/wiki/PyGTK
  32. Stránky projektu PySide
    https://wiki.qt.io/PySide
  33. PySide (Wikipedia)
    https://en.wikipedia.org/wiki/PySide
  34. Stránky projektu Kivy
    https://kivy.org/#home
  35. Kivy (framework, Wikipedia)
    https://en.wikipedia.org/wi­ki/Kivy_(framework)
  36. QML Applications
    http://doc.qt.io/qt-5/qmlapplications.html
  37. KDE
    https://www.kde.org/
  38. Qt
    https://www.qt.io/
  39. GNOME
    https://en.wikipedia.org/wiki/GNOME
  40. Category:Software that uses PyGTK
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_PyGTK
  41. Category:Software that uses PyGObject
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_PyGObject
  42. Category:Software that uses wxWidgets
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_wxWidgets
  43. GIO
    https://developer.gnome.or­g/gio/stable/
  44. GStreamer
    https://gstreamer.freedesktop.org/
  45. GStreamer (Wikipedia)
    https://en.wikipedia.org/wi­ki/GStreamer
  46. Wax Gui Toolkit
    https://wiki.python.org/moin/Wax
  47. Python Imaging Library (PIL)
    http://infohost.nmt.edu/tcc/hel­p/pubs/pil/
  48. Why Pyjamas Isn’t a Good Framework for Web Apps (blogpost z roku 2012)
    http://blog.pyjeon.com/2012/07/29/why-pyjamas-isnt-a-good-framework-for-web-apps/
Našli jste v článku chybu?