Obsah
2. Vytvoření kostry aplikace s TUI postavené nad knihovnou prompt_toolkit
3. Přepnutí do režimu zobrazení využívajícího celou plochu terminálu
4. Základní prvky TUI: kontejnery a prvky pro vstup/výstup textových údajů
5. Naprogramované reakce na klávesové zkratky
6. Vertikální rozmístění prvků TUI v ploše terminálu
7. Použití prvku typu Window ve funkci oddělovače
8. Horizontální rozmístění prvků TUI v ploše terminálu
9. Současné použití správců rozložení s vertikálním i horizontálním rozmístěním prvků TUI
10. Další ovládací prvky, z nichž se skládají aplikace s textovým uživatelským rozhraním
12. Nastavení stylu zobrazení prvku typu Frame a Box
14. Prvek typu TextArea aneb textový editor snadno a rychle
15. Ovládací prvek typu Button
16. Přepínání fokusu (zaměření, výběru) mezi jednotlivými tlačítky
17. Zákaz přenosu fokusu na zvolený ovládací prvek; přepínání pomocí kláves Tab a Shift+Tab
18. Nastavení stylu zobrazení ovládacích prvků TUI
19. Repositář s demonstračními příklady
1. Tvorba textového uživatelského rozhraní s knihovnou prompt_toolkit: aplikace s celoobrazovkovým rozhraním
Na předchozí trojici článků [1] [2] [3] dnes navážeme, protože si řekneme, jaké základní ovládací prvky (widgety) je možné využít při tvorbě textového uživatelského rozhraní založeného na knihovně prompt_toolkit. Taktéž se seznámíme s takzvanými správci rozvržení (layout managers), které ovládací prvky vhodným způsobem uspořádají na plochu terminálu.

Obrázek 1: Dialog pro výběr odpovědi typu Ano/Ne, který je představován funkcí yes_no_dialog, tentokrát plně počeštěný díky podpoře Unicode.

Obrázek 2: Dialog typu button_dialog se třemi specifikovanými tlačítky.
2. Vytvoření kostry aplikace s TUI postavené nad knihovnou prompt_toolkit
Aplikace s plnohodnotným TUI jsou při použití knihovny prompt_toolkit představovány instancí třídy prompt_toolkit.application.Application. Typicky se konstruktoru této třídy předává takzvaný layout s prvky TUI, nastavené klávesové zkratky atd. Po zavolání metody run() se spustí smyčka událostí (event loop), která se bude mj. starat o reakce ovládacích prvků na akce prováděné uživatelem. V prvním demonstračním příkladu, který si dnes ukážeme, se pouze vytvoří objekt typu Application představující ústřední část aplikace s textovým uživatelským rozhraním. Následně je zavolána metoda run(), která aplikaci spustí. Ovšem vzhledem k tomu, že v aplikaci nejsou definovány žádné prvky textového uživatelského rozhraní, pouze se vypíše zpráva „No layout specified. Press ENTER to quit.“ a aplikace bude čekat na své ukončení stiskem klávesy Enter:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application # vytvoření aplikace s textovým uživatelským rozhraním application = Application() # spuštění aplikace application.run()

Obrázek 3: Vzhledem k tomu, že v aplikaci nejsou specifikovány žádné prvky textového uživatelského rozhraní, vypíše se po jejím spuštění pouze tato zpráva a aplikace bude po stisku klávesy Enter ukončena.
Ve skutečnosti budou všechny následující demonstrační příklady postaveny na poněkud odlišném skriptu, který se od prvního demonstračního příkladu odlišuje pouze tím, že se v něm explicitně používá funkce main. Úplný zdrojový kód tohoto příkladu vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 &#k03nbsp; from prompt_toolkit import Application def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application() # spuštění aplikace application.run() if __name__ == '__main__': main()

Obrázek 4: I po spuštění tohoto skriptu se vypíše stejná zpráva, jako ve skriptu prvním.
Jen pro zajímavost si připomeňme, jak vypadá kostra aplikace při použití knihovny appJar, s níž jsme se již na stránkách Roota seznámili:
#!/usr/bin/env python from appJar import gui app = gui() app.go()
3. Přepnutí do režimu zobrazení využívajícího celou plochu terminálu
Konstruktoru třídy Application je možné předat několik nepovinných (pojmenovaných) parametrů, především pak:
Parametr | Význam |
---|---|
layout | objekt nesoucí informace o všech ovládacích prvcích |
key_bindings | nastavené klávesové zkratky |
clipboard | objekt představující textovou schránku |
full_screen | režim vyplnění celé plochy terminálu |
erase_when_done | povolení vymazání obrazovky před ukončením aplikace |
Mnoho aplikací s textovým uživatelským rozhraním (například Midnight Commander a všechny textové editory) používají pro své zobrazení celou dostupnou plochu terminálu. Toto chování se povolí použitím pojmenovaného parametru full_screen:
application = Application(full_screen=True)
Opět se podívejme na demonstrační příklad (režim plné obrazovky bude použit i ve všech dalších příkladech):
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()

Obrázek 5: Výsledek spuštění předchozího skriptu – aplikace je skutečně spuštěna přes celou plochu terminálu.
4. Základní prvky TUI: kontejnery a prvky pro vstup/výstup textových údajů
Mezi základní třídy, z nichž se v knihovně prompt_toolkit skládá textové uživatelské rozhraní, patří především třída nazvaná UIControl a taktéž třída Container. Jedná se o bázové třídy reprezentující libovolný prvek TUI, resp. zajišťující zabalení a umístění tohoto prvku na ploše terminálu. Od třídy Container jsou odvozeny další třídy, především pak následující čtveřice:
Třída | Stručný popis |
---|---|
HSplit | zajišťuje horizontální rozložení prvků na ploše |
VSplit | zajišťuje vertikální rozložení prvků na ploše |
FloatContainer | používá se například pro menu a další „plovoucí“ prvky |
Window | kontejner, který obaluje jeden konkrétní ovládací prvek |
Od třídy UIControl jsou odvozeny především třídy:
Třída | Stručný popis |
---|---|
FormattedTextControl | zajišťuje vykreslení naformátovaného textu |
BufferControl | zobrazí vstupní buffer |
Prozatím si v demonstračních příkladech vystačíme s prvkem FormattedTextControl. Postup při jeho zobrazení na ploše terminálu je následující:
Nejprve vytvoříme naformátovanou zprávu (viz předchozí článek) a následně tuto zprávu předáme konstruktoru třídy FormattedTextControl:
# naformátovaná zpráva message = HTML("<ansired>Hello</ansired> <ansiblue>world!</ansiblue>") # ovládací prvek s naformátovaným textem text = FormattedTextControl(text=message)
Dále je nutné vytvořený ovládací prvek vložit do vhodného kontejneru. Nejjednodušší bude, alespoň prozatím, použití kontejneru Window:
# okno obsahující jediný ovládací prvek window = Window(content=text)
Následně vytvoříme správce rozvržení a (opět v konstruktoru) specifikujeme, který kontejner se má zobrazit:
# správce rozvržení layout = Layout(window)
5. Naprogramované reakce na klávesové zkratky
Předchozí řádky vlastně popisovaly pouze vzhled aplikace; zbývá doplnit informace o jejím chování. Prozatím se spokojíme s tím, že aplikaci bude možné ukončit klávesou Escape. Vzhledem k tomu, že smyčka událostí je řízena přímo instancí třídy Application, musíme tomuto objektu nějakým způsobem předat informaci o tom, jak má na klávesu Escape reagovat. To zajišťuje další část kódu, v níž vytvoříme instanci třídy KeyBindings, pomocnou callback funkci on_escape_press, která aplikaci ukončí a s využitím anotace navážeme tuto callback funkci právě na stisk klávesy Escape:
# napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit()
Instanci třídy KeyBindings pak předáme konstruktoru Application:
# vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True)

Obrázek 6: Takto vypadá první aplikace se skutečným textovým uživatelským rozhraním po svém spuštění.
Úplný zdrojový kód tohoto příkladu je již poměrně komplikovaný, ovšem vytvořili jsme v něm užitečný základ TUI (což bude patrné z příkladů dalších):
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.layout import Layout from prompt_toolkit.key_binding import KeyBindings # naformátovaná zpráva message = HTML("<ansired>Hello</ansired> <ansiblue>world!</ansiblue>") # ovládací prvek s naformátovaným textem text = FormattedTextControl(text=message) # okno obsahující jediný ovládací prvek window = Window(content=text) # správce rozvržení layout = Layout(window) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
6. Vertikální rozmístění prvků TUI v ploše terminálu
Jednotlivé prvky textového uživatelského rozhraní je možné na ploše terminálu rozmístit různými způsoby s využitím takzvaných správců rozvržení (layout managers), které se ovšem v knihovně prompt_toolkit nazývají kontejnery. Mezi kontejnery patří především tyto tři třídy:
- HSplit
- VSplit
- FlowContainer
- Window
- ConditionalContainer
Nejprve si ukažme příklad použití kontejneru VSplit. Ten slouží pro rozmístění prvků vertikálně – všechny prvky, které do tohoto kontejneru vložíme, budou umístěny vedle sebe v pořadí zleva doprava:
+-----------+------------+------------+------------+ | | | | | | prvek #1 | prvek #2 | prvek #3 | prvek #4 | | | | | | +-----------+------------+------------+------------+
Prvky se do kontejneru předávají již při jeho konstrukci, a to konkrétně v seznamu (povšimněte si hranatých závorek). Počet takto vložených prvků TUI není teoreticky nijak omezen; prakticky jsme samozřejmě omezeni šířkou terminálu, tj. počtem znaků na řádku. Pokud se prvky na šířku terminálu nevejdou, vypíše se zpráva „Window too small“:
# správce rozvržení vsplit = VSplit([ window1, window2, window3])
Jakmile máme objekt typu VSplit vytvořen, můžeme ho předat konstruktoru třídy Layout. Tento objekt si mj. pamatuje i prvek, který má nastaven fokus:
layout = Layout(vsplit)
Následně se layout předá do konstruktoru třídy Application:
application = Application(layout=layout, full_screen=True)

Obrázek 7: Tři prvky textového uživatelského rozhraní rozmístěné vertikálně (vedle sebe).
Úplný zdrojový kód příkladu využívajícího vertikální rozmístění prvků TUI vypadá takto:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, VSplit from prompt_toolkit.key_binding import KeyBindings # naformátované zprávy message1 = HTML("<ansired>Hello</ansired>") message2 = HTML("<ansiblue>world!</ansiblue>") message3 = HTML("<ansiyellow>(Esc to quit)</ansiyellow>") # ovládací prvky s naformátovaným textem text1 = FormattedTextControl(text=message1) text2 = FormattedTextControl(text=message2) text3 = FormattedTextControl(text=message3) # okna obsahující jediný ovládací prvek window1 = Window(content=text1) window2 = Window(content=text2) window3 = Window(content=text3) # správce rozvržení vsplit = VSplit([ window1, window2, window3]) layout = Layout(vsplit) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
7. Použití prvku typu Window ve funkci oddělovače
Kontejneru Window je možné v jeho konstruktoru předat poměrně velké množství volitelných parametrů ovlivňujících jeho chování. Lze například nastavit rozměry okna s využitím parametrů width a height, povolit scrollovací lišty, nastavit styl zobrazení nebo zvolit, jakým znakem má být vyplněna plocha okna. A právě vhodnou kombinací parametrů width a char můžeme dosáhnout toho, že se okno bude chovat jako (viditelný) vertikální oddělovač dalších prvků TUI (samozřejmě lze nastavit i styl atd., to si ukážeme dále):
# správce rozvržení vsplit = VSplit([ window1, Window(width=1, char='|'), window2, Window(width=1, char='|'), window3]) layout = Layout(vsplit)

Obrázek 8: Tři prvky textového uživatelského rozhraní, mezi nimiž jsou použita úzká okna plnící úlohu vizuálního oddělovače.
Úplný demonstrační příklad, v němž se používají dvě úzká okna ve funkci oddělovače, vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, VSplit from prompt_toolkit.key_binding import KeyBindings # naformátované zprávy message1 = HTML("<ansired>Hello</ansired>") message2 = HTML("<ansiblue>world!</ansiblue>") message3 = HTML("<ansiyellow>(Esc to quit)</ansiyellow>") # ovládací prvky s naformátovaným textem text1 = FormattedTextControl(text=message1) text2 = FormattedTextControl(text=message2) text3 = FormattedTextControl(text=message3) # okna obsahující jediný ovládací prvek window1 = Window(content=text1) window2 = Window(content=text2) window3 = Window(content=text3) # správce rozvržení vsplit = VSplit([ window1, Window(width=1, char='|'), window2, Window(width=1, char='|'), window3]) layout = Layout(vsplit) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
8. Horizontální rozmístění prvků TUI v ploše terminálu
Podobně jako jsme si v předchozích dvou kapitolách ukázali vertikální rozmístění prvků textového uživatelského rozhraní s využitím správce rozmístění pojmenovaného VSplit, je samozřejmě možné prvky rozmístit i horizontálně, a to pomocí správce nazvaného příhodně HSplit. Jeho základní použití je prakticky totožné – konstruktoru se předá seznam prvků TUI, které mají být zobrazeny v horizontálních pruzích:
# správce rozvržení hsplit = HSplit([ window1, window2, window3]) layout = Layout(hsplit)

Obrázek 9: Aplikace, v níž jsou tři prvky TUI rozmístěny horizontálně (bez oddělovače).
Následuje úplný zdrojový kód tohoto příkladu:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, HSplit from prompt_toolkit.key_binding import KeyBindings # naformátované zprávy message1 = HTML("<ansired>Hello</ansired>") message2 = HTML("<ansiblue>world!</ansiblue>") message3 = HTML("<ansiyellow>(Esc to quit)</ansiyellow>") # ovládací prvky s naformátovaným textem text1 = FormattedTextControl(text=message1) text2 = FormattedTextControl(text=message2) text3 = FormattedTextControl(text=message3) # okna obsahující jediný ovládací prvek window1 = Window(content=text1) window2 = Window(content=text2) window3 = Window(content=text3) # správce rozvržení hsplit = HSplit([ window1, window2, window3]) layout = Layout(hsplit) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
Okna o výšce jeden znak (resp. přesněji řečeno jeden textový řádek) mohou sloužit jako vizuální oddělovače mezi prvky textového uživatelského rozhraní. Celý systém, zde konkrétně se třemi oddělenými prvky, může vypadat následovně:
# správce rozvržení hsplit = HSplit([ window1, Window(height=1, char='-'), window2, Window(height=1, char='-'), window3]) layout = Layout(hsplit)

Obrázek 10: Aplikace, v níž jsou tři prvky TUI rozmístěny horizontálně a navíc jsou mezi tyto prvky vložena okna o výšce jeden znak, které plní funkci oddělovače.
Opět si ukažme úplný zdrojový kód tohoto příkladu:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, HSplit from prompt_toolkit.key_binding import KeyBindings # naformátované zprávy message1 = HTML("<ansired>Hello</ansired>") message2 = HTML("<ansiblue>world!</ansiblue>") message3 = HTML("<ansiyellow>(Esc to quit)</ansiyellow>") # ovládací prvky s naformátovaným textem text1 = FormattedTextControl(text=message1) text2 = FormattedTextControl(text=message2) text3 = FormattedTextControl(text=message3) # okna obsahující jediný ovládací prvek window1 = Window(content=text1) window2 = Window(content=text2) window3 = Window(content=text3) # správce rozvržení hsplit = HSplit([ window1, Window(height=1, char='-'), window2, Window(height=1, char='-'), window3]) layout = Layout(hsplit) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
9. Současné použití správců rozložení s vertikálním i horizontálním rozmístěním prvků TUI
Vzhledem k tomu, že do jednoho kontejneru můžeme vložit jak přímo jednotlivé prvky TUI, tak i další kontejnery, je možné libovolným způsobem kombinovat správce pro horizontální a vertikální rozložení. To je ukázáno v dalším úryvku kódu, v němž je plocha terminálu rozdělena horizontálně na tři pruhy (horní plocha, jednořádkové okno – oddělovač, dolní plocha), přičemž horní plocha je dále rozdělena na tři podplochy (ovládací prvek, jednosloupcové okno – oddělovač, druhý ovládací prvek):
# správce rozvržení vsplit = HSplit([ VSplit([ window1, Window(width=1, char='|'), window2]), Window(height=1, char='-'), window3]) layout = Layout(vsplit)

Obrázek 11: Aplikace používající kombinaci horizontální a vertikálního rozložení prvků TUI.
Opět si ukažme úplný zdrojový kód tohoto příkladu:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings # naformátované zprávy message1 = HTML("<ansired>Hello</ansired>") message2 = HTML("<ansiblue>world!</ansiblue>") message3 = HTML("<ansiyellow>(Esc to quit)</ansiyellow>") # ovládací prvky s naformátovaným textem text1 = FormattedTextControl(text=message1) text2 = FormattedTextControl(text=message2) text3 = FormattedTextControl(text=message3) # okna obsahující jediný ovládací prvek window1 = Window(content=text1) window2 = Window(content=text2) window3 = Window(content=text3) # správce rozvržení vsplit = HSplit([ VSplit([ window1, Window(width=1, char='|'), window2]), Window(height=1, char='-'), window3]) layout = Layout(vsplit) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
10. Další ovládací prvky, z nichž se skládají aplikace s textovým uživatelským rozhraním
Kromě základních ovládacích prvků FormattedTextControl a BufferControl, které jsou odvozeny přímo od bázové třídy UIControl, je možné použít i plnohodnotné widgety: tlačítka, textová návěští, menu atd. Tyto widgety většinou interně používají kombinaci tříd Window a FormattedTextControl, popř. Window a BufferControl, což vlastně jen dokazuje, že tvorba TUI je v mnoha ohledech jednodušší, než tomu je u plnohodnotného GUI:
Ovládací prvek | Stručný popis |
---|---|
Label | textové návěští |
Button | tlačítko (může získat fokus a reagovat na stlačení) |
Checkbox | zaškrtávací tlačítko |
RadioList | skupina přepínacích tlačítek |
TextArea | vstupní textové pole (textový editor) |
Frame | rámeček okolo libovolného prvku nebo skupiny prvků |
Shadow | stín pod libovolným prvkem nebo skupinou prvků |
Box | okraj okolo libovolného prvku nebo skupiny prvků |
HorizontalLine | oddělovací čára |
VerticalLine | oddělovací čára |
ProgressBar | zobrazení probíhající činnosti |
11. Prvky Frame a Box
První prvky z předchozího seznamu, s nimiž se seznámíme, jsou pasivní, tj. nereagují na žádné akce prováděné uživatelem a slouží pouze pro lepší pozicování prvků aktivních. Jedná se o prvky Frame a Box. Jedním z důvodů, proč si tyto prvky popisujeme ve stejné kapitole je fakt, že se skutečně používají společně, protože Frame slouží k vytvoření viditelného rámečku okolo jiného prvku/kontejneru, zatímco Box k vytvoření neviditelného okraje. Základní použití je následující:
# struktura obrazovky root = Box(Frame(widget))

Obrázek 12: Použití prvků Box a Frame okolo zprávy zobrazené prvkem Label (návěští).
Podívejme se na způsob vložení návěští do prvků Frame a Box. Jak je patrné, mohou oba tyto prvky sloužit jako kontejnery:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.widgets import * # naformátovaná zpráva message = HTML("<ansired>Hello</ansired> <ansiblue>world!</ansiblue>") # widget widget = Label(message) # struktura obrazovky root = Box(Frame(widget)) # správce rozvržení layout = Layout(container=root) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
12. Nastavení stylu zobrazení prvku typu Frame a Box
U prvků typu Box a Frame je možné měnit styl zobrazení. Okraj lze nastavit na libovolnou hodnotu počítanou ve znacích, popř. textových řádcích, styl rámečku je ovlivněn parametrem style, což je ostatně stejné i pro všechny další prvky TUI:
# struktura obrazovky root = Box(Frame(widget, title="About:", style="bg:#ansiblue #ansiwhite"), padding=2)
Výsledek můžeme vidět na dalším screenshotu:

Obrázek 13: Nastavení stylu zobrazení prvků typu Frame a Box.
Opět si ukažme příklad, v němž se styl rámečku nastavuje:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.widgets import * # naformátovaná zpráva message = HTML("<ansibrightred>Hello</ansibrightred> " + "<ansibrightyellow>world!</ansibrightyellow>") # widget widget = Label(message) # struktura obrazovky root = Box(Frame(widget, title="About:", style="bg:#ansiblue #ansiwhite"), padding=2) # správce rozvržení layout = Layout(container=root) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
13. Použití prvku typu Label
Textové návěští, jehož obsah se samozřejmě dá programově modifikovat, je reprezentováno třídou Label. Podobně jako v prakticky všech dalších widgetech je možné i zde při specifikaci zobrazené zprávy použít formátování s využitím minule popsaných tříd ANSI či HTML:
message1 = HTML("<ansibrightred>Hello</ansibrightred>") widget1 = Label(message1) ... ... ...

Obrázek 14: Tři prvky typu Label na ploše aplikace, každý zobrazený v ostylovaném rámečku.
Celý zdrojový kód příkladu, v němž jsou použita tři návěští, vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.widgets import * # naformátované zprávy message1 = HTML("<ansibrightred>Hello</ansibrightred>") message2 = HTML("<ansibrightyellow>world!</ansibrightyellow>") message3 = HTML("<ansibrightyellow>(Esc to quit)</ansibrightyellow>") # widgety widget1 = Label(message1) widget2 = Label(message2) widget3 = Label(message3) # styl rámce style = "bg:#ansiblue #ansiwhite" # správce rozvržení vsplit = HSplit([ VSplit([ Frame(widget1, style=style), Frame(widget2, style=style)]), Frame(widget3, style=style)]) layout = Layout(vsplit) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
14. Prvek typu TextArea aneb textový editor snadno a rychle
Zatímco byly výše popsané prvky Box, Frame a Label zcela pasivní, je tomu v případě prvku TextArea úplně jinak, protože tato část textového uživatelského rozhraní ve skutečnosti představuje implementaci relativně dobrého textového editoru, v němž je možné využít základní klávesové zkratky, s nimiž jsme se již seznámili při popisu funkce prompt. Vložení editoru do TUI je stejně snadné, jako u jakéhokoli jiného prvku:
vsplit = HSplit([ VSplit([ Frame(widget1, style=style), Frame(widget2, style=style)]), Frame(TextArea(), title="Editor"), Frame(widget3, style=style)]) layout = Layout(vsplit)

Obrázek 15: Ovládací prvek typu TextArea zobrazený společně se třemi návěštími. Všechny prvky jsou umístěny v rámečku.
S tímto prvkem se podrobněji seznámíme příště, takže již jen krátce demonstrační příklad, který ho využívá:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application, HTML from prompt_toolkit.layout.containers import Window from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.widgets import * # naformátované zprávy message1 = HTML("<ansibrightred>Hello</ansibrightred>") message2 = HTML("<ansibrightyellow>world!</ansibrightyellow>") message3 = HTML("<ansibrightyellow>(Esc to quit)</ansibrightyellow>") # widgety widget1 = Label(message1) widget2 = Label(message2) widget3 = Label(message3) # styl rámce style = "bg:#ansiblue #ansiwhite" # správce rozvržení vsplit = HSplit([ VSplit([ Frame(widget1, style=style), Frame(widget2, style=style)]), Frame(TextArea(), title="Editor"), Frame(widget3, style=style)]) layout = Layout(vsplit) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
15. Ovládací prvek typu Button
Dalším aktivním ovládacím prvkem je tlačítko představované instancí třídy Button. Tlačítko může reagovat na svůj stisk, který je obsloužen v callback funkci specifikované nepovinným atributem handler:
button1 = Button('Button 1', handler=button1_clicked)

Obrázek 16: Textové uživatelské rozhraní se čtveřicí tlačítek a pasivní textovou plochou.
V následujícím demonstračním příkladu jsou deklarována čtyři tlačítka. Po stisku prvních třech tlačítek se pouze do textového pole vypíše informace o události která nastala. Poslední tlačítko slouží k ukončení aplikace. Prozatím ovšem nebude možné získat fokus těchto tlačítek, takže je aplikaci nutné ukončit „postaru“ klávesou Escape:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.application.current import get_app from prompt_toolkit.widgets import * def button1_clicked(): text_area.text += 'Button 1 clicked\n' def button2_clicked(): text_area.text += 'Button 2 clicked\n' def button3_clicked(): text_area.text += 'Button 3 clicked\n' def exit_clicked(): get_app().exit() button1 = Button('Button 1', handler=button1_clicked) button2 = Button('Button 2', handler=button2_clicked) button3 = Button('Button 3', handler=button3_clicked) button4 = Button('Exit', handler=exit_clicked) buttons = HSplit([button1, button2, button3, button4]) text_area = TextArea() # správce rozvržení root = VSplit([Box(Frame(buttons, style="bg:#ansiblue #ansiwhite"), padding=2), Box(Frame(text_area, title="Events"), padding=2)]) layout = Layout(root) # napojení na klávesové zkratky key_bindings = KeyBindings() @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
16. Přepínání fokusu (zaměření, výběru) mezi jednotlivými tlačítky
Ve skutečnosti je možné přepínání fokusu (neboli zaměření či výběru) ovládacích prvků naprogramovat velmi snadno, a to následujícím způsobem:
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous # napojení na klávesové zkratky key_bindings = KeyBindings() key_bindings.add('up')(focus_previous) key_bindings.add('down')(focus_next)

Obrázek 17: Přepínání mezi tlačítky.
Předchozí řádky zajistí, že se při stisku šipky nahoru zavolá funkce focus_previous() a při stisku šipky dolů pak funkce focus_next(). Tyto dvě funkce budou postupně přepínat mezi ovládacími prvky, a to v pořadí, v jakém jsou definovány (toto pořadí je však možné změnit). Příklad, v němž je toto chování naprogramováno, vypadá takto:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.application.current import get_app from prompt_toolkit.widgets import * def button1_clicked(): text_area.text += 'Button 1 clicked\n' def button2_clicked(): text_area.text += 'Button 2 clicked\n' def button3_clicked(): text_area.text += 'Button 3 clicked\n' def exit_clicked(): get_app().exit() button1 = Button('Button 1', handler=button1_clicked) button2 = Button('Button 2', handler=button2_clicked) button3 = Button('Button 3', handler=button3_clicked) button4 = Button('Exit', handler=exit_clicked) buttons = HSplit([button1, button2, button3, button4]) text_area = TextArea() # správce rozvržení root = VSplit([Box(Frame(buttons, style="bg:#ansiblue #ansiwhite"), padding=2), Box(Frame(text_area, title="Events"), padding=2)]) layout = Layout(root) # napojení na klávesové zkratky key_bindings = KeyBindings() key_bindings.add('up')(focus_previous) key_bindings.add('down')(focus_next) @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
17. Zákaz přenosu fokusu na zvolený ovládací prvek; přepínání pomocí kláves Tab a Shift+Tab
Pokud nebudeme chtít, aby se fokus přenesl na některý ovládací prvek, je nutné při jeho konstrukci použít nepovinný parametr focusable a předat mu hodnotu False:
text_area = TextArea(focusable=False)
Další úprava aplikace spočívá v tom, že nastavíme přepínání fokusu na klávesové zkratky Tab a Shift+Tab, což je pro mnoho uživatelů známější (i když podle mého názoru mnohem horší) volba, než použití šipky nahoru a dolů:
key_bindings = KeyBindings() key_bindings.add('s-tab')(focus_previous) key_bindings.add('tab')(focus_next)
Opět se podívejme na celý zdrojový kód příkladu, který toto nové chování implementuje:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.application.current import get_app from prompt_toolkit.widgets import * def button1_clicked(): text_area.text += 'Button 1 clicked\n' def button2_clicked(): text_area.text += 'Button 2 clicked\n' def button3_clicked(): text_area.text += 'Button 3 clicked\n' def exit_clicked(): get_app().exit() button1 = Button('Button 1', handler=button1_clicked) button2 = Button('Button 2', handler=button2_clicked) button3 = Button('Button 3', handler=button3_clicked) button4 = Button('Exit', handler=exit_clicked) buttons = HSplit([button1, button2, button3, button4]) text_area = TextArea(focusable=False) # správce rozvržení root = VSplit([Box(Frame(buttons, style="bg:#ansiblue #ansiwhite"), padding=2), Box(Frame(text_area, title="Events"), padding=2)]) layout = Layout(root) # napojení na klávesové zkratky key_bindings = KeyBindings() key_bindings.add('s-tab')(focus_previous) key_bindings.add('tab')(focus_next) @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True) # spuštění aplikace application.run() if __name__ == '__main__': main()
18. Nastavení stylu zobrazení ovládacích prvků TUI
V závěru dnešního článku si ukážeme způsob změny stylu zobrazení ovládacích prvků textového uživatelského rozhraní. Nejdříve si připomeňme, že každému prvku můžeme přiřadit libovolnou třídu stylu, a to podobně jako v klasickém CCS, ovšem poněkud odlišným zápisem:
# správce rozvržení root = VSplit([Box(Frame(buttons, style="class:button_panel"), padding=2), Box(Frame(text_area, title="Events", style="class:edit_panel"), padding=2)])
Dále již můžeme specifikovat styly zobrazení, tj. především barvu popředí, pozadí i styl fontu (viz předchozí článek):
style = Style([ ('button_panel', 'bg:#ansiblue #ansiwhite'), ('edit_panel', 'bg:#ansigreen #000000'), ('button', '#ansibrightyellow'), ('button focused', 'bg:#ff0000'), ('text-area focused', 'bg:#ff0000'), ])
Takto vytvořený styl se předá konstruktoru třídy Application:
# vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True, style=style)

Obrázek 18: Textové uživatelské rozhraní aplikace s vlastním nastaveným stylem.
Již naposledy si ukážeme výpis zdrojového kódu příkladu, v němž je použit upravený styl:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from prompt_toolkit import Application from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.application.current import get_app from prompt_toolkit.widgets import * from prompt_toolkit.styles import Style def button1_clicked(): text_area.text += 'Button 1 clicked\n' def button2_clicked(): text_area.text += 'Button 2 clicked\n' def button3_clicked(): text_area.text += 'Button 3 clicked\n' def exit_clicked(): get_app().exit() button1 = Button('Button 1', handler=button1_clicked) button2 = Button('Button 2', handler=button2_clicked) button3 = Button('Button 3', handler=button3_clicked) button4 = Button('Exit', handler=exit_clicked) buttons = HSplit([button1, button2, button3, button4]) text_area = TextArea(focusable=False) # správce rozvržení root = VSplit([Box(Frame(buttons, style="class:button_panel"), padding=2), Box(Frame(text_area, title="Events", style="class:edit_panel"), padding=2)]) layout = Layout(root) # napojení na klávesové zkratky key_bindings = KeyBindings() key_bindings.add('up')(focus_previous) key_bindings.add('down')(focus_next) style = Style([ ('button_panel', 'bg:#ansiblue #ansiwhite'), ('edit_panel', 'bg:#ansigreen #000000'), ('button', '#ansibrightyellow'), ('button focused', 'bg:#ff0000'), ('text-area focused', 'bg:#ff0000'), ]) @key_bindings.add('escape') def on_escape_press(event): """Callback funkce volaná při stisku klávesy Esc.""" print("\n\n[escape]\n\n") event.app.exit() def main(): # vytvoření aplikace s textovým uživatelským rozhraním application = Application(layout=layout, key_bindings=key_bindings, full_screen=True, style=style) # spuštění aplikace application.run() if __name__ == '__main__': main()
19. Repositář s demonstračními příklady
Všechny dnes popisované demonstrační příklady byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/presentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
20. Odkazy na Internetu
- UTF-8 encoded sample plain-text file
http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt - 4 Python libraries for building great command-line user interfaces
https://opensource.com/article/17/5/4-practical-python-libraries - prompt_toolkit 2.0.3 na PyPi
https://pypi.org/project/prompt_toolkit/ - python-prompt-toolkit na GitHubu
https://github.com/jonathanslenders/python-prompt-toolkit - The GNU Readline Library
https://tiswww.case.edu/php/chet/readline/rltop.html - GNU Readline (Wikipedia)
https://en.wikipedia.org/wiki/GNU_Readline - readline — GNU readline interface (Python 3.x)
https://docs.python.org/3/library/readline.html - readline — GNU readline interface (Python 2.x)
https://docs.python.org/2/library/readline.html - GNU Readline Library – command line editing
https://tiswww.cwru.edu/php/chet/readline/readline.html - gnureadline 6.3.8 na PyPi
https://pypi.org/project/gnureadline/ - Editline Library (libedit)
http://thrysoee.dk/editline/ - Comparing Python Command-Line Parsing Libraries – Argparse, Docopt, and Click
https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/ - libedit or editline
http://www.cs.utah.edu/~bigler/code/libedit.html - WinEditLine
http://mingweditline.sourceforge.net/ - rlcompleter — Completion function for GNU readline
https://docs.python.org/3/library/rlcompleter.html - rlwrap na GitHubu
https://github.com/hanslub42/rlwrap - rlwrap(1) – Linux man page
https://linux.die.net/man/1/rlwrap - readline(3) – Linux man page
https://linux.die.net/man/3/readline - history(3) – Linux man page
https://linux.die.net/man/3/history - vi(1) – Linux man page
https://linux.die.net/man/1/vi - emacs(1) – Linux man page
https://linux.die.net/man/1/emacs - Pygments – Python syntax highlighter
http://pygments.org/ - Write your own lexer
http://pygments.org/docs/lexerdevelopment/ - Jazyky podporované knihovnou Pygments
http://pygments.org/languages/ - Pygments FAQ
http://pygments.org/faq/ - TUI – Text User Interface
https://en.wikipedia.org/wiki/Text-based_user_interface - PuDB: výkonný debugger pro Python s retro uživatelským rozhraním (nástroj s plnohodnotným TUI)
https://www.root.cz/clanky/pudb-vykonny-debugger-pro-python-s-retro-uzivatelskym-rozhranim/ - Historie vývoje textových editorů: krkolomná cesta k moderním textovým procesorům
https://www.root.cz/clanky/historie-vyvoje-textovych-editoru-krkolomna-cesta-k-modernim-textovym-procesorum/ - Rosetta Code
http://rosettacode.org/wiki/Rosetta_Code - Mandelbrot set: Sinclair ZX81 BASIC
http://rosettacode.org/wiki/Mandelbrot_set#Sinclair_ZX81_BASIC - Nástroj Dialog
http://invisible-island.net/dialog/ - Projekt Zenity
https://wiki.gnome.org/Projects/Zenity - Xterm256 color names for console Vim
http://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim