Obsah
1. Interaktivní tvorba grafického uživatelského rozhraní s využitím nástroje Pygubu (2)
2. Krátké připomenutí: příklady popsané minule
7. Základní typy widgetů v knihovně Tk/Tkinter
10. Realizace aplikace s „odtrhávacím“ menu
11. Klávesové zkratky navázané na jednotlivé položky menu
12. Realizace aplikace se specifikovanými klávesovými zkratkami
13. Registrace handleru události po stisku klávesové zkratky
14. Změna tématu zobrazení za běhu aplikace
15. Návrh aplikace s podobným chováním v nástroji Pygubu
16. Implementace handlerů zavolaných po stisku tlačítek na GUI
17. Standardní dialogová okna s otázkou (Ok/Cancel, Yes/No, Ok/Retry)
18. Příklad zobrazující všechna čtyři standardní dialogová okna
19. Repositář s demonstračními příklady
1. Interaktivní tvorba grafického uživatelského rozhraní s využitím nástroje Pygubu (2)
V úvodním článku o projektu Pygubu jsme si řekli základní informace o možnostech poskytovaných tímto užitečným nástrojem. Ukázali jsme si taktéž trojici jednoduchých demonstračních příkladů a na závěr jsme se zmínili o některých alternativních projektech, především o RAD (Rapid Application Development) nástrojích Lazarus, Gambas a Qt Creator. Dnes si ukážeme některé další možnosti, které jsou nabízeny knihovnou Tkinter a podporovány v Pygubu. Většina dále popisovaných vlastností bude ukázána i na demonstračních příkladech, podobně jako tomu bylo i minule.
Obrázek 1: Dnes pro porovnání použijeme nejnovější verzi Pygubu designeru: 0.10.9, jejíž rozhraní je odlišné od předchozích verzí. Navíc byly přidány i některé užitečné vlastnosti, například podpora pro specifikaci klávesových zkratek u položek menu.
Obrázek 2: Grafické uživatelské rozhraní Pygubu designeru verze 0.10.9.
2. Krátké připomenutí: příklady popsané minule
Připomeňme si, že v předchozím článku jsme si ukázali trojici (velmi jednoduchých až triviálních) demonstračních příkladů. Odkazy na tyto příklady naleznete v devatenácté kapitole.
Obrázek 3: Pro porovnání – takto vypadá Pygubu designer verze 0.9.
V prvním demonstračním příkladu byla do okna aplikace (což je prvek grafického uživatelského rozhraní nazvaný frame) vložena trojice ovládacích prvků GUI (takzvaných widgetů, o nichž se podrobněji zmíníme v dalším textu). Přitom byly jednotlivé ovládací prvky umístěny v rámci pravidelné mřížky (grid), která je sice neviditelná, ovšem umožňuje „pozicovat“ jednotlivé prvky v GUI nezávisle na rozlišení obrazovky a současně i nezávisle na zvolené velikosti okna. Toto řešení představuje poměrně zásadní odklon od staršího tradičního pohledu na GUI, v němž byly prvky umisťovány absolutně, tedy zadáním souřadnic vztažených k nějakému významnému bodu okna (typicky k levému hornímu rohu pracovní plochy okna – titulkový pruh ležel nad touto pracovní plochou), a například změna měřítka celého GUI v rámci nastavení operačního systému nebyla ve všech aplikacích korektně reflektována.
Obrázek 4: Rámec (okno aplikace) s dvojicí dalších prvků jsou základem prvního příkladu.
Ve druhém demonstračním příkladu jsme do hlavního (a současně i jediného) okna aplikace vložili menu. Na panelu menu byly zobrazeny dvě položky (File a Edit), po jejichž výběru se rozbalila skutečná menu s dalšími položkami, přičemž každá položka byla reprezentována jak textovým popisem, tak i ikonou uloženou v souboru typu PNG (ovšem podporovány jsou i další formáty, a to díky využití knihovny PIL/Pilow; samotná knihovna Tkinter/Tk ze známějších formátů podporuje pouze formát GIF). U textového popisku je možné zvolit index znaku, který bude podtržený a který lze využít pro rychlý výběr dané položky (to mj. znamená, že by daný znak měl být v rámci všech ostatních vybraných znaků v menu unikátní). Mezi jednotlivými položkami menu je možné vložit takzvaný separátor, což je pouze vizuální oddělovač, který nedokáže reagovat na operace prováděné uživatelem.
Obrázek 5: Návrh vzhledu hlavního menu aplikace.
Ve třetím a současně i posledním demonstračním příkladu bylo provedeno propojení mezi ovládacími prvky grafického uživatelského rozhraní na jedné straně s pythonovským kódem na straně druhé. Co to ovšem znamená v praxi? Pygubu nepracuje stejným způsobem jako plnohodnotné RAD, nedokáže tedy propojit návrh GUI s programovým kódem. Ovšem umožňuje specifikovat takzvanou callback funkci (ve skutečnosti však spíše callback metodu), která se má zavolat ve chvíli, kdy je nějaký prvek grafického uživatelského rozhraní vybrán. V Pygubu je nutné zapsat jméno příslušné metody, přičemž vazba mezi tímto jménem a konkrétní metodou implementovanou v Pythonu je provedena až v čase běhu aplikace (tedy v runtime). To je poměrně významný posun oproti některým tradičním RAD, v nichž se tato vazba vytváří již v čase návrhu aplikace (můžeme tedy říci, že v compile time). Zde je nutné poznamenat, že oba přístupy mají své přednosti, ale i zápory (kontrola aplikace při jejím návrhu versus mnohem větší flexibilita).
Obrázek 6: Třetí demonstrační příklad po spuštění.
3. Widgety v knihovně Tkinter
Základem prakticky všech v současnosti používaných grafických uživatelských rozhraní jsou takzvané widgety, které jsou někdy poněkud nepřesně označovány také jako komponenty. Z pohledu uživatele aplikací s grafickým uživatelským rozhraním se jedná o grafické prvky zobrazené na obrazovce, které mají předem známé chování a předvídatelnou funkci. V mnoha případech je chování widgetů standardizováno či alespoň doporučováno – viz například doporučení pro (dnes již notně zastaralý) Motif, Microsoft Windows, Mac OS a v neposlední řadě také doporučení pro GNOME a KDE.
Obrázek 7: Základní sada widgetů nabízená v rámci Ttk.
Velká část widgetů se snaží svým vzhledem do jisté míry reflektovat objekty z reálného světa (tlačítka, „teploměry“, přepínače programů, objekty známé z papírových formulářů apod.). Z pohledu programátora (a zejména programátora používajícího programovací jazyk Python) je naproti tomu widget objektem, kterému lze nastavit určitý stav a který reaguje na události, které při své práci generuje uživatel (přesouvání objektů na obrazovce, stlačování obrazů tlačítek pomocí myši či stylusu, psaní textu, gesta na dotykové obrazovce atd.).
Obrázek 8: Widgety přidané v rámci projektu Pygubu (nejedná se tedy o widgety ze standardní sady).
Mnoho widgetů nalezneme přímo v knihovně Tk, další pak v její rozšířené variantě Ttk a projekt Pygubu navíc umožňuje vytvářet a přidávat další typy widgetů plně integrovatelných do vyvíjených aplikací.
4. Kontejnery
Samotné widgety nejsou na obrazovce prakticky nikdy zcela osamocené, ostatně většina knihoven pro GUI by samostatný widget ani nedokázala zobrazit. Ve skutečnosti se téměř vždy nachází v nějakém okně, dialogu či dalším nadřazeném widgetu. Programátoři grafických uživatelských rozhraní se často setkají s pojmem kontejner. Jedná se o komponentu, na kterou lze vkládat různé widgety a mnohdy i další kontejnery. Obecně tak interně vzniká stromová datová struktura jejíž kořen je představován plochou na obrazovce, na které jsou umístěna okna aplikací (dnes je ovšem i samotná plocha obrazovky součástí větší virtuální plochy zobrazované obecně na více monitorech). V těchto oknech se dále nachází kontejnery a widgety. V mnoha grafických uživatelských rozhraních přitom mohou být vybrané widgety (zdaleka ne však všechny) současně i kontejnery. Kontejnery kromě jiného řeší i rozmístění widgetů na své ploše.
Obrázek 9: Základní sada kontejnerů knihovny Tk.
Způsobů pro rozmisťování widgetů do kontejnerů existuje více. Základní dělení je na kontejnery, kde jsou widgety umisťovány absolutně (do této kategorie patří WinAPI, MFC, OWL a VCL) a naopak kontejnery, které widgety většinou umisťují podle své velikosti a vzájemných vztahů (zde se nachází javovské AWT, Swing, GTK, Qt, námi používané Tk/Tkinter a mnoho dalších). V toolkitu Tk a tím pádem i v Tkinteru se mohou widgety umisťovat několika různými způsoby (pack, place a grid).
5. Správce geometrie pack
Nejprve si ukážeme použití správce geometrie nazvaného pack, který, jak již jeho jméno naznačuje, zarovnává jednotlivé widgety vedle sebe, a to buď ve směru horizontálním, či vertikálním. V prvních verzích knihovny Tk se jednalo o jediného dostupného správce, ale od verze 4.x (a ta vyšla již v počítačovém dávnověku) se objevují i další dva typy správců. Tento manažer umožňuje vkládat komponenty do kontejneru (typicky do okna) tak, že se zadává jejich relativní umístění (horizontální či vertikální zarovnání):
button1.pack() button2.pack() button3.pack() button4.pack()
Obrázek 10: Widgety jsou umístěny pod sebou, což je implicitní chování manažeru geometrie „pack“.
Pro jednodušší dialogy může být tento správce použitelnější jednodušeji, než správce „grid“ (není nutné počítat řádky a sloupce).
Obrázek 11: Nastavení parametrů souvisejících se správcem rozvržení „pack“ v Pygubu.
6. Správce geometrie grid
Správce geometrie pack zmíněný v předchozí kapitole je sice velmi flexibilní (zejména při hierarchickém vkládání jednotlivých manažerů na sebe), v některých případech je však rozmisťování widgetů pomocí tohoto manažeru problematické nebo přinejmenším zdlouhavé. Z tohoto důvodu byl později vytvořen další správce geometrie, který je celkem trefně nazvaný grid. S využitím tohoto správce se widgety umisťují do pomyslné mřížky, přičemž rozměr mřížky se, spolu s počtem sloupců a řádků, flexibilně mění podle tvarových charakteristik vkládaných widgetů. Navíc je možné specifikovat, ke kterým okrajům jednotlivých buněk budou widgety „přilepeny“ – pokud jsou například přilepeny na západním i východním okraji, bude daný widget reagovat na změnu velikost dialogu a tím i změnu rozměrů mřížky.
Obrázek 12: Nejprimitivnější způsob použití správce rozvržení grid.
Obrázek 13: Nastavení parametrů souvisejících se správcem rozvržení „grid“ v Pygubu.
7. Základní typy widgetů v knihovně Tk/Tkinter
V průběhu mnoha let se množina widgetů používaných v různých grafických uživatelských rozhraních postupně rozšiřovala, ostatně postačí se podívat na obrázky z prvních grafických rozhraní navržených ve společnosti Xerox a porovnat je s moderním desktopem. Současně však také docházelo ke sjednocování vzhledu jednotlivých widgetů i jejich chování na různých platformách. Vzhled samozřejmě není na všech platformách přesně stejný, to však pro uživatele většinou nemusí představovat významnější praktický problém, na rozdíl od odlišného chování celého prostředí i jednotlivých widgetů.
Obrázek 14: Ukázka grafického uživatelského rozhraní počítačového systému Xerox 8010 Star Information System.
V toolkitu Tk je k dispozici poměrně velké množství widgetů, podobně jako v dalších moderních toolkitech. Navíc dnes widgety existují ve dvou podobách – starší (původní) a novější (Ttk neboli „themed Tk“), které lépe odpovídají požadavkům uživatelů současných desktopových prostředí a jejichž vzhled a chování se může od původních widgetů odlišovat. V následující tabulce je uveden seznam základních typů widgetů. Pro mnoho aplikací je níže uvedená skupina widgetů dostačující, avšak v případě, že aplikace potřebuje vytvořit nový widget, je to samozřejmě možné, protože knihovna Tk je navržena tak, že ji lze poměrně jednoduchým způsobem rozšiřovat. V následující tabulce si také můžete všimnout toho, že některé widgety jsou pojmenovány odlišným způsobem od dnes používané terminologie. Vychází to z faktu, že Tcl/Tk je mnohem starší než většina dnešních GUI toolkitů.
Jméno widgetu | Význam a funkce |
---|---|
label | widget, který zobrazuje v okně či dialogu měnitelný text |
button | graficky zobrazené tlačítko, které implicitně reaguje na levé tlačítko myši |
checkbutton | dvoustavový přepínač, který implicitně reaguje na levé tlačítko myši |
radiobutton | widget, jichž může být sdruženo větší množství, vždy pouze jeden je vybraný |
scale | dnes nazýván pojmem slider atd., jedná se o widget s posuvnou částí a přidruženým textem, kde se zobrazuje hodnota v závislosti na poloze posuvné části |
entry | widget, do kterého je možné zapisovat text, k tomu má přidruženo mnoho klávesových zkratek (jde o kombinaci staršího a novějšího standardu) |
spinbox | widget určený pro zadávání číselných hodnot kombinací klávesnice a myši (i s kontrolou mezí) |
menu | vertikální menu, které se skládá z více položek |
menubutton | používá se spolu s menu pro vytváření jednotlivých položek |
listbox | widget, jež nabízí na výběr libovolné množství řádků s textem |
scrollbar | podobné widgetu scale s tím rozdílem, že zobrazuje posuvné šipky a naopak nezobrazuje přidruženou číselnou hodnotu |
frame | jeden z několika nabízených kontejnerů; tento má tvar obdélníka (může být také neviditelný nebo může mít 3D rámeček) |
toplevel | další z kontejnerů, tento se chová jako samostatné okno či dialog |
bitmap | bitmapa, tj. rastrový obrázek |
photo/photoimage | rastrový obrázek, jež může být načten z externího souboru v mnoha různých formátech |
canvas | widget, na který lze programově vkládat další grafické komponenty (úsečky, oblouky, kružnice, polyčáry, text atd.) |
8. Vlastnosti widgetů
Ke každému widgetu je možné nastavit mnoho různých vlastností, které mění buď jeho vizuální vzhled na obrazovce počítače nebo jeho chování, tj. způsob reakce widgetu na akce uživatele. Mezi tyto akce počítáme například kliknutí tlačítkem myši, použití klávesových zkratek (hot keys), přesunutí widgetu atd. Některé vlastnosti jsou všem widgetům společné, další vlastnosti jsou však jedinečné pro jeden či pouze několik typů widgetů. Je to ostatně logické, některé widgety mají speciální chování. Vlastnosti lze nastavovat již při vytváření widgetů, na druhou stranu je také možné vlastnosti měnit až při běhu aplikace. Způsob nastavení vlastností si ukážeme na demonstračních příkladech. V následující tabulce jsou uvedeny vlastnosti, které jsou společné prakticky všem widgetům (kromě speciálních widgetů typu „položka menu“, které mají vlastnosti omezeny, stejně tak jako jejich reakce na uživatelovu činnost). Pozor! – při použití Ttk se nastavování vizuálních vlastností musí provádět přes styly, což si samozřejmě taktéž ukážeme:
Jméno vlastnosti | Popis vlastnosti |
---|---|
background | barva pozadí widgetu v případě, že widget není aktivní (vybraný) |
foreground | barva popředí widgetu (například zobrazeného textu) v případě, že widget není aktivní (vybraný) |
borderwidth | šířka okraje widgetu, která je zadaná v pixelech |
activebackground | barva pozadí widgetu v případě, že je widget vybrán (typicky kurzorem myši) |
activeforeground | barva popředí widgetu v případě, že je widget vybrán |
disabledforeground | barva popředí widgetu v případě, že je ovládání widgetu zakázáno |
relief | způsob prostorového zobrazení widgetu |
compound | způsob umístění bitmapy či obrázku na widgetu |
bitmap | bitmapa, která má být ve widgetu zobrazena |
image | obrázek, který má být ve widgetu zobrazen (více o bitmapách a obrázcích bude uvedeno v dalších dílech) |
font | jméno fontu, který je použit pro text uvnitř widgetu (font lze specifikovat platformově nezávislým způsobem) |
text | text, který má být ve widgetu (tlačítko, položka menu atd.) zobrazen |
cursor | jméno kurzoru myši, který bude použit v případě, že se kurzor nachází nad widgetem |
textvariable | jméno proměnné, která je nastavována podle uživatelových manipulací s widgetem (StringVar v Tkinteru) |
justify | zarovnání textu ve widgetu v případě, že se zobrazuje více řádků |
anchor | způsob umístění textu či obrázku ve widgetu |
Vlastnosti se nastavují dvěma způsoby – u původních widgetů přímo nastavením vlastnosti (například background=„red“) u „themed Tk“ pak změnou takzvaných stylů, což je sice nepatrně složitější, ovšem mnohem flexibilnější způsob. V Pygubu se vlastnosti nastavují v příslušném dialogu:
Obrázek 15: Nastavení stylu widgetu přímo v prostředí Pygubu designeru.
9. „Odtrhávací“ menu
Jednou ze zajímavých vlastností menu poskytovaných knihovnou Tkinter je schopnost vytvořit taková rozbalovací menu, která lze „odtrhnout“ od pruhu s menu a začít je používat jako samostatné panely. To je potenciálně velmi užitečná vlastnost, protože uživateli dovoluje, aby byly nejčastěji používané příkazy stále viditelné a přitom umístěné přesně na tom místě, kde to uživateli vyhovuje (aplikace se tedy uživateli nesnaží vnutit jedno konkrétní rozvržení ovládacích prvků – resp. ne do takové míry, jak je to běžné jinde).
Obrázek 16: „Odtržená“ menu grafického editoru mtPaint.
Vytvoření „odtrhávacích“ menu je ve skutečnosti velmi snadné, protože se jedná o vlastnost menu, kterou je možné specifikovat přímo v Pygubu:
Obrázek 17: Nastavení vlastnosti „tearoff“ je platné vždy pro celé menu.
10. Realizace aplikace s „odtrhávacím“ menu
V praxi postačuje vlastnost tearoff nastavit na pravdivostní hodnotu true (skutečně je psána s malým počátečním písmenem) pouze u jediné položky hlavního menu, protože se jedná o globálně sdílený atribut. V praxi může vygenerovaný soubor s návrhem GUI aplikace vypadat následovně:
<?xml version='1.0' encoding='utf-8'?> <interface> <object class="tk.Menu" id="MainMenu"> <child> <object class="tk.Menuitem.Submenu" id="FileMenu"> <property name="font">TkDefaultFont</property> <property name="label" translatable="yes">File</property> <property name="relief">raised</property> <property name="state">normal</property> <property name="tearoff">true</property> <property name="underline">0</property> <child> <object class="tk.Menuitem.Command" id="Command_New"> <property name="command_#e tento mana#er umo##uje vkládat komponenty do kontejneru (typicky do okna) tak, #e se zadává jejich relativní umíst#ní (horizontální #i vertikální zarovnání)id_arg">false</property> <property name="compound">left</property> <property name="image">document-new.png</property> <property name="label" translatable="yes">New</property> <property name="state">normal</property> <property name="underline">0</property> </object> </child> <child> <object class="tk.Menuitem.Command" id="Command_Open"> <property name="command_id_arg">false</property> <property name="compound">left</property> <property name="image">document-open.png</property> <property name="label" translatable="yes">Open</property> <property name="underline">0</property> </object> </child> <child> <object class="tk.Menuitem.Separator" id="Separator_1" /> </child> <child> <object class="tk.Menuitem.Command" id="Command_Quit"> <property name="command">on_command_quit_selected</property> <property name="command_id_arg">false</property> <property name="compound">left</property> <property name="image">application-exit.png</property> <property name="label" translatable="yes">Quit</property> <property name="underline">0</property> </object> </child> </object> </child> <child> <object class="tk.Menuitem.Submenu" id="EditMenu"> <property name="label" translatable="yes">Edit</property> <property name="underline">0</property> <child> <object class="tk.Menuitem.Command" id="Command_Cut"> <property name="command_id_arg">false</property> <property name="compound">left</property> <property name="image">edit-cut.png</property> <property name="label" translatable="yes">Cut</property> </object> </child> <child> <object class="tk.Menuitem.Command" id="Command_Copy"> <property name="command_id_arg">false</property> <property name="compound">left</property> <property name="image">edit-copy.png</property> <property name="label" translatable="yes">Copy</property> </object> </child> <child> <object class="tk.Menuitem.Command" id="Command_Paste"> <property name="command_id_arg">false</property> <property name="compound">left</property> <property name="image">edit-paste.png</property> <property name="label" translatable="yes">Paste</property> </object> </child> <child> <object class="tk.Menuitem.Separator" id="Separator_2" /> </child> <child> <object class="tk.Menuitem.Command" id="Command_Delete"> <property name="command_id_arg">false</property> <property name="compound">left</property> <property name="image">edit-delete.png</property> <property name="label" translatable="yes">Delete</property> <property name="state">disabled</property> </object> </child> </object> </child> </object> <object class="ttk.Frame" id="MainWindow"> <property name="height">200</property> <property name="width">200</property> <layout> <property name="column">0</property> <property name="propagate">True</property> <property name="row">0</property> </layout> <child> <object class="ttk.Button" id="Button_Hello"> <property name="command">on_button_clicked</property> <property name="compound">top</property> <property name="state">normal</property> <property name="text" translatable="yes">Quit</property> <property name="underline">0</property> <layout> <property name="column">0</property> <property name="propagate">True</property> <property name="row">0</property> </layout> </object> </child> </object> </interface>
Vlastní zdrojový kód aplikace se přitom nijak nezmění, až na přejmenování třídy představující celou aplikaci:
"""Pygubu and Tkinter: main menu in main window, callback functions, tearoff menu (working example).""" # Example4.py import tkinter as tk from tkinter import messagebox import pygubu class Example4App(pygubu.TkApplication): """Class representing a Tkinter based application.""" def _create_ui(self): """Construct and initializes all UI-related data structures.""" # step #1: Create a builder self.builder = builder = pygubu.Builder() # step #2: Load an ui file builder.add_from_file('example4.ui') # step #2B: Specify path to images and other resources builder.add_resource_path(".") # step #3: Create the mainwindow self.mainwindow = builder.get_object('MainWindow', self.master) # step #4: Set main menu self.mainmenu = menu = builder.get_object('MainMenu', self.master) self.set_menu(menu) # step $5: Configure callbacks builder.connect_callbacks(self) def on_button_clicked(self): """Define handler for Quit button.""" tk.messagebox.showinfo('Message', 'You clicked on Quit button') root.destroy() def on_command_quit_selected(self): """Define handler for Quit command.""" tk.messagebox.showinfo('Message', 'You selected Quit command') root.destroy() if __name__ == '__main__': # needed to have a menu root = tk.Tk() # run the application app = Example4App(root) app.run()
Obrázek 18: „Odtržená“ menu v testovací aplikaci.
11. Klávesové zkratky navázané na jednotlivé položky menu
Poslední vlastnost menu, kterou si dnes popíšeme, je zobrazení a navázání klávesových akcelerátorů (například Ctrl+C) k určité položce menu.
Nastavení klávesových akcelerátorů (což je další typ horkých klíčů) se musí provést ve dvou krocích. Nejprve je nutné akcelerátor specifikovat u každé položky menu s využitím volby accelerator, například:
filemenu.add_command(label="Open", underline=0, accelerator="Ctrl+O", command=lambda: print("Open"))
Knihovna Tkinter nás nijak neomezuje v tom, jaký text je u volby accelerator zapsán; samozřejmě je však vhodné, když popis souvisí se skutečně nastaveným akcelerátorem. Příklad si můžeme nepatrně upravit tak, že u každé položky (kde to má význam) uvedeme popis příslušné klávesové zkratky:
#!/usr/bin/env python import tkinter from tkinter import ttk root = tkinter.Tk() menubar = tkinter.Menu(root) filemenu = tkinter.Menu(menubar, tearoff=0) filemenu.add_command(label="Open", underline=0, accelerator="Ctrl+O", command=lambda: print("Open")) filemenu.add_command(label="Save", underline=0, accelerator="Ctrl+S", command=lambda: print("Save")) filemenu.add_separator() filemenu.add_command(label="Exit", underline=1, accelerator="Ctrl+Q", command=root.quit) menubar.add_cascade(label="File", menu=filemenu, underline=0) editmenu = tkinter.Menu(menubar, tearoff=0) editmenu.add_command(label="Undo", underline=0, accelerator="Ctrl+U", command=lambda: print("Undo")) editmenu.add_separator() editmenu.add_command(label="Cut", underline=2, accelerator="Ctrl+X", command=lambda: print("Cut")) editmenu.add_command(label="Copy", underline=0, accelerator="Ctrl+C", command=lambda: print("Copy")) editmenu.add_command(label="Paste", underline=0, accelerator="Ctrl+V", command=lambda: print("Paste")) editmenu.add_command(label="Delete", underline=2, command=lambda: print("Delete")) editmenu.add_separator() editmenu.add_command(label="Select All", underline=7, accelerator="Ctrl+A", command=lambda: print("Select All")) menubar.add_cascade(label="Edit", menu=editmenu, underline=0) helpmenu = tkinter.Menu(menubar, tearoff=0) helpmenu.add_command(label="About", underline=0, accelerator="F1", command=lambda: print("About")) menubar.add_cascade(label="Help", menu=helpmenu, underline=0) root.config(menu=menubar) root.mainloop()
Obrázek 19: Screenshot demonstračního příkladu s klávesovými akcelerátory.
12. Realizace aplikace se specifikovanými klávesovými zkratkami
Z hlediska návrhu grafického uživatelského rozhraní aplikace je nastavení klávesových zkratek jednoduché – postačuje totiž zapsat klávesovou zkratku do políčka accelerator. Tento text nebude při spuštění dále upravován, takže je možné použít například národní variantu popisu kláves Ctrl, Alt atd.:
Obrázek 20: Nastavení klávesových zkratek, které se mají zobrazit u jednotlivých položek menu.
Při exportu návrhu GUI bude ve výsledném XML souboru s koncovkou .ui doplněna vlastnost accelerator:
<?xml version='1.0' encoding='utf-8'?> <interface version="1.1"> <object class="tk.Menu" id="MainMenu"> <child> <object class="tk.Menuitem.Submenu" id="FileMenu"> <property name="font">TkDefaultFont</property> <property name="label" translatable="yes">File</property> <property name="relief">raised</property> <property name="state">normal</property> <property name="tearoff">true</property> <property name="underline">0</property> <child> <object class="tk.Menuitem.Command" id="Command_New"> <property name="accelerator">Ctrl+N</property> <property name="compound">left</property> <property name="image">document-new.png</property> <property name="label" translatable="yes">New</property> <property name="state">normal</property> <property name="underline">0</property> </object> </child> <child> <object class="tk.Menuitem.Command" id="Command_Open"> <property name="accelerator">Ctrl+O</property> <property name="compound">left</property> <property name="image">document-open.png</property> <property name="label" translatable="yes">Open</property> <property name="underline">0</property> </object> </child> <child> <object class="tk.Menuitem.Separator" id="Separator_1" /> </child> <child> <object class="tk.Menuitem.Command" id="Command_Quit"> <property name="accelerator">Ctrl+Q</property> <property args="" cbtype="simple" name="command" type="command">on_command_quit_selected</property> <property name="compound">left</property> <property name="image">application-exit.png</property> <property name="label" translatable="yes">Quit</property> <property name="underline">0</property> </object> </child> </object> </child> <child> <object class="tk.Menuitem.Submenu" id="EditMenu"> <property name="label" translatable="yes">Edit</property> <property name="underline">0</property> <child> <object class="tk.Menuitem.Command" id="Command_Cut"> <property name="accelerator">Ctrl+X</property> <property name="compound">left</property> <property name="image">edit-cut.png</property> <property name="label" translatable="yes">Cut</property> </object> </child> <child> <object class="tk.Menuitem.Command" id="Command_Copy"> <property name="accelerator">Ctrl+C</property> <property name="compound">left</property> <property name="image">edit-copy.png</property> <property name="label" translatable="yes">Copy</property> </object> </child> <child> <object class="tk.Menuitem.Command" id="Command_Paste"> <property name="accelerator">Ctrl+P</property> <property name="compound">left</property> <property name="image">edit-paste.png</property> <property name="label" translatable="yes">Paste</property> </object> </child> <child> <object class="tk.Menuitem.Separator" id="Separator_2" /> </child> <child> <object class="tk.Menuitem.Command" id="Command_Delete"> <property name="accelerator">Delete</property> <property name="compound">left</property> <property name="image">edit-delete.png</property> <property name="label" translatable="yes">Delete</property> <property name="state">disabled</property> </object> </child> </object> </child> </object> <object class="ttk.Frame" id="MainWindow"> <property name="height">200</property> <property name="width">200</property> <layout manager="grid"> <property name="column">0</property> <property name="propagate">True</property> <property name="row">0</property> </layout> <child> <object class="ttk.Button" id="Button_Hello"> <property args="" cbtype="simple" name="command" type="command">on_button_clicked</property> <property name="compound">top</property> <property name="state">normal</property> <property name="text" translatable="yes">Quit</property> <property name="underline">0</property> <layout manager="grid"> <property name="column">0</property> <property name="propagate">True</property> <property name="row">0</property> </layout> </object> </child> </object> </interface>
První varianta skriptu, který tento návrh GUI bude používat:
"""Pygubu and Tkinter: main menu in main window, callback functions, tearoff menu (working example).""" # Example5.py import tkinter as tk from tkinter import messagebox import pygubu class Example5App(pygubu.TkApplication): """Class representing a Tkinter based application.""" def _create_ui(self): """Construct and initializes all UI-related data structures.""" # step #1: Create a builder self.builder = builder = pygubu.Builder() # step #2: Load an ui file builder.add_from_file('example5.ui') # step #2B: Specify path to images and other resources builder.add_resource_path(".") # step #3: Create the mainwindow self.mainwindow = builder.get_object('MainWindow', self.master) # step #4: Set main menu self.mainmenu = menu = builder.get_object('MainMenu', self.master) self.set_menu(menu) # step $5: Configure callbacks builder.connect_callbacks(self) def on_button_clicked(self): """Define handler for Quit button.""" tk.messagebox.showinfo('Message', 'You clicked on Quit button') root.destroy() def on_command_quit_selected(self): """Define handler for Quit command.""" tk.messagebox.showinfo('Message', 'You selected Quit command') root.destroy() if __name__ == '__main__': # needed to have a menu root = tk.Tk() # run the application app = Example5App(root) app.run()
13. Registrace handleru události po stisku klávesové zkratky
Specifikace zkratky v návrhu menu však není dostačující, protože je ještě nutné akcelerátor na položku menu (resp. přesněji řečeno na nějaký příkaz) navázat. K tomu se používá nám již známý příkaz bind. Pro označení modifikátorů kláves se používají prefixy Control- a Meta-. Také si všimněte, že se rozlišují velikosti písmen stlačených kláves, takže je rozdíl mezi zápisem Control-x a Control-X (druhá možnost nemusí na některých systémech vůbec fungovat a i kdyby fungovala, není příliš často používaná):
root.bind('<Control-o>', lambda event: cmd_open()) root.bind('<Control-s>', lambda event: cmd_save()) root.bind('<Control-u>', lambda event: cmd_undo()) root.bind('<F1>', lambda event: cmd_help()) ... ... ... atd. i pro další položky menu
V našem konkrétním případě navážeme stisk klávesy Ctrl+Q s metodou on_command_quit_selected:
root.bind('<Control-q>', lambda event: self.on_command_quit_selected())
Modifikovaný zdrojový kód příkladu bude vypadat následovně:
"""Pygubu and Tkinter: main menu in main window, callback functions, tearoff menu (working example).""" # Example5.py import tkinter as tk from tkinter import messagebox import pygubu class Example5App(pygubu.TkApplication): """Class representing a Tkinter based application.""" def _create_ui(self): """Construct and initializes all UI-related data structures.""" # step #1: Create a builder self.builder = builder = pygubu.Builder() # step #2: Load an ui file builder.add_from_file('example5.ui') # step #2B: Specify path to images and other resources builder.add_resource_path(".") # step #3: Create the mainwindow self.mainwindow = builder.get_object('MainWindow', self.master) # step #4: Set main menu self.mainmenu = menu = builder.get_object('MainMenu', self.master) self.set_menu(menu) # step $5: Configure callbacks builder.connect_callbacks(self) root.bind('<Control-q>', lambda event: self.on_command_quit_selected()) def on_button_clicked(self): """Define handler for Quit button.""" tk.messagebox.showinfo('Message', 'You clicked on Quit button') root.destroy() def on_command_quit_selected(self): """Define handler for Quit command.""" tk.messagebox.showinfo('Message', 'You selected Quit command') root.destroy() if __name__ == '__main__': # needed to have a menu root = tk.Tk() # run the application app = Example5App(root) app.run()
14. Změna tématu zobrazení za běhu aplikace
Téma je možné vybrat i změnit prakticky kdykoli za běhu programu, což skutečně funguje (například ve Swingu je to problematičtější). Podívejme se však na to, co se stane, když si pomocí čtyř tlačítek necháme přepínat čtyři základní témata „clam“, „alt“, „default“ a „classic“ a současně bude nastaven styl Red.TButton. Téma přepneme jednoduše zavoláním:
style.theme_use("jméno_tématu"))
kde style je objekt získaný konstruktorem:
style = ttk.Style()
Příklad změny tématu po stisku tlačítka:
button1 = ttk.Button(root, text="clam", command=lambda: style.theme_use("clam"))
Úplný demonstrační příklad se změnou témat za běhu vypadá následovně (příklad je založen na Tkinteru):
#!/usr/bin/env python import tkinter from tkinter import ttk import sys def exit(): sys.exit(0) root = tkinter.Tk() style = ttk.Style() style.configure('Red.TButton', background='#ff8080') button1 = ttk.Button(root, text="clam", command=lambda: style.theme_use("clam")) button2 = ttk.Button(root, text="alt", command=lambda: style.theme_use("alt")) button3 = ttk.Button(root, text="default", command=lambda: style.theme_use("default")) button4 = ttk.Button(root, text="classic", command=lambda: style.theme_use("classic")) quitButton = ttk.Button(root, text="Exit", style='Red.TButton', command=exit) button1.grid(column=1, row=1, sticky="we") button2.grid(column=2, row=1, sticky="we") button3.grid(column=1, row=2, sticky="we") button4.grid(column=2, row=2, sticky="we") quitButton.grid(column=2, row=5, sticky="we") label = tkinter.Label(root, text='Hello world') entry = tkinter.Entry(root) checkbutton = tkinter.Checkbutton(text='Do you like Tkinter?') checkbutton.grid(column=1, row=3, columnspan=2, sticky="w") label.grid(column=1, row=4) entry.grid(column=2, row=4) root.mainloop()
Obrázek 21: Demonstrační příklad po výběru tématu „default“.
15. Návrh aplikace s podobným chováním v nástroji Pygubu
V nástroji Pygubu může návrh aplikace s podobným chováním, jako výše uvedená aplikace (naprogramovaná čistě v Tkinteru) vypadat následovně:
Obrázek 22: Návrh grafického uživatelského rozhraní aplikace v nástroji Pygubu.
Povšimněte si, že každému tlačítku je přiřazena nějaká metoda zavolaná ve chvíli, kdy je tlačítko vybráno uživatelem – viz zvýrazněné řádky:
<?xml version='1.0' encoding='utf-8'?> <interface version="1.1"> <object class="ttk.Frame" id="MainWindow"> <property name="height">200</property> <property name="width">200</property> <layout manager="pack"> <property name="propagate">True</property> <property name="side">top</property> </layout> <child> <object class="ttk.Button" id="clam"> <property cbtype="simple" name="command" type="command">on_button_clam_click</property> <property name="text" translatable="yes">clam</property> <layout manager="grid"> <property name="column">0</property> <property name="propagate">True</property> <property name="row">0</property> <property name="sticky">ew</property> </layout> </object> </child> <child> <object class="ttk.Button" id="alt"> <property cbtype="simple" name="command" type="command">on_button_alt_click</property> <property name="text" translatable="yes">alt</property> <layout manager="grid"> <property name="column">1</property> <property name="propagate">True</property> <property name="row">0</property> <property name="sticky">ew</property> </layout> </object> </child> <child> <object class="ttk.Button" id="default"> <property cbtype="simple" name="command" type="command">on_button_default_click</property> <property name="text" translatable="yes">default</property> <layout manager="grid"> <property name="column">0</property> <property name="propagate">True</property> <property name="row">1</property> <property name="sticky">ew</property> </layout> </object> </child> <child> <object class="ttk.Button" id="classic"> <property cbtype="simple" name="command" type="command">on_button_classic_click</property> <property name="text" translatable="yes">classic</property> <layout manager="grid"> <property name="column">1</property> <property name="propagate">True</property> <property name="row">1</property> <property name="sticky">ew</property> </layout> </object> </child> <child> <object class="ttk.Checkbutton" id="question"> <property name="text" translatable="yes">Do you like Tkinter?</property> <layout manager="grid"> <property name="column">0</property> <property name="propagate">True</property> <property name="row">2</property> </layout> </object> </child> <child> <object class="ttk.Label" id="hello_world_label"> <property name="text" translatable="yes">Hello world</property> <layout manager="grid"> <property name="column">0</property> <property name="propagate">True</property> <property name="row">3</property> </layout> </object> </child> <child> <object class="ttk.Entry" id="entry1"> <layout manager="grid"> <property name="column">1</property> <property name="propagate">True</property> <property name="row">3</property> <property name="sticky">ew</property> </layout> </object> </child> <child> <object class="ttk.Button" id="exit"> <property cbtype="simple" name="command" type="command">on_button_exit_click</property> <property name="style">Red.TButton</property> <property name="text" translatable="yes">Exit</property> <layout manager="grid"> <property name="column">1</property> <property name="propagate">True</property> <property name="row">4</property> <property name="sticky">ew</property> </layout> </object> </child> </object> </interface>
16. Implementace handlerů zavolaných po stisku tlačítek na GUI
Handlery, tedy metody třídy Example6App, musí mít shodný název se jmény uvedenými v návrhu grafického uživatelského rozhraní. Jedná se skutečně o běžné metody, které akceptují parametr self (tedy referenci na instanci třídy) a volají metody objektu style, který je globálně viditelný (je vytvořen v bloku za definicí třídy):
"""Pygubu and Tkinter: changing style.""" # Example6.py import tkinter as tk from tkinter import ttk import pygubu class Example6App(pygubu.TkApplication): """Class representing a Tkinter based application.""" def _create_ui(self): """Construct and initializes all UI-related data structures.""" # step #1: Create a builder self.builder = builder = pygubu.Builder() # step #2: Load an ui file builder.add_from_file('example6.ui') # step #2B: Specify path to images and other resources builder.add_resource_path(".") # step #3: Create the mainwindow self.mainwindow = builder.get_object('MainWindow', self.master) # step $4: Configure callbacks builder.connect_callbacks(self) root.bind('<Control-q>', lambda event: self.on_button_exit_click()) def on_button_clam_click(self): style.theme_use("clam") def on_button_alt_click(self): style.theme_use("alt") def on_button_default_click(self): style.theme_use("default") def on_button_classic_click(self): style.theme_use("classic") def on_button_exit_click(self): root.destroy() if __name__ == '__main__': # needed to have a menu root = tk.Tk() # style style = ttk.Style() # run the application app = Example6App(root) app.run()
Obrázek 23: Výsledná aplikace po svém spuštění.
17. Standardní dialogová okna s otázkou (Ok/Cancel, Yes/No, Ok/Retry)
Knihovna Tkinter obsahuje i několik standardních dialogových oken, v nichž se po uživateli vyžaduje reagovat na položenou otázku, popř. na nějaký stav aplikace (nepodařený tisk atd.) stiskem jednoho z nabízených tlačítek. Tato dialogová okna jsou opět vyvolána funkcemi, které nalezneme v modulu tkinter.messagebox:
Funkce | Ikona | Zobrazená tlačítka |
---|---|---|
askokcancel() | OK+Cancel | |
askretrycancel() | Retry+Cancel | |
askyesno() | Yes+No | |
askquestion() | ve výchozím nastavení Yes+No |
Obrázek 24: Dialog otázkou vytvořený funkcí messagebox.askokcancel().
Tato dialogová okno v nástroji Pygubu nenalezneme, protože je nutné je v aplikaci zavolat programově.
18. Příklad zobrazující všechna čtyři standardní dialogová okna
Předchozí demonstrační příklad (s výběrem stylů) můžeme velmi snadno upravit takovým způsobem, aby byla čtyři tlačítka použita pro zobrazení standardních dialogových oken. Tlačítka přejmenujeme, změníme jejich text a taktéž jména callback funkcí (resp. přesněji řečeno metod):
Obrázek 24: Návrh GUI aplikace.
Samotný skript volá přímo výše zmíněné funkce pro zobrazení standardních dialogových oken (podtržená část kódu):
"""Pygubu and Tkinter: changing style.""" # Example7.py import tkinter as tk from tkinter import ttk from tkinter import messagebox import pygubu class Example7App(pygubu.TkApplication): """Class representing a Tkinter based application.""" def _create_ui(self): """Construct and initializes all UI-related data structures.""" # step #1: Create a builder self.builder = builder = pygubu.Builder() # step #2: Load an ui file builder.add_from_file('example7.ui') # step #2B: Specify path to images and other resources builder.add_resource_path(".") # step #3: Create the mainwindow self.mainwindow = builder.get_object('MainWindow', self.master) # step $4: Configure callbacks builder.connect_callbacks(self) root.bind('<Control-q>', lambda event: self.on_button_exit_click()) def on_button_ok_cancel_click(self): messagebox.askokcancel("askokcancel()", "askokcancel()") def on_button_yes_no_click(self): messagebox.askyesno("askyesno()", "askyesno()") def on_button_retry_cancel_click(self): messagebox.askretrycancel("askretrycancel()", "askretrycancel()") def on_button_question_click(self): messagebox.askquestion("askquestion()", "askquestion()") def on_button_exit_click(self): root.destroy() if __name__ == '__main__': # needed to have a menu root = tk.Tk() # style style = ttk.Style() # run the application app = Example7App(root) app.run()
19. Repositář s demonstračními příklady
Zdrojové kódy všech minule i dnes popsaných demonstračních příkladů určených pro Python 3 a nejnovější stabilní verzi knihovny Pygubu (a pochopitelně i pro Pygubu designer) byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
# | Jméno souboru | Stručný popis souboru | Cesta |
---|---|---|---|
Ovládací prvky vložené do hlavního okna aplikace | |||
1 | example1.ui | soubor s návrhem GUI prvního demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example1.ui |
2 | example1.py | implementace prvního demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example1.py |
Menu, jehož jednotlivé položky obsahují i ikony | |||
3 | example2.ui | soubor s návrhem GUI druhého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example2.ui |
4 | example2A.py | implementace třetího demonstračního příkladu (bez menu) | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example2A.py |
5 | example2B.py | implementace třetího demonstračního příkladu (nespecifikován adresář s ikonami) | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example2B.py |
6 | example2C.py | implementace třetího demonstračního příkladu (korektní varianta) | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example2C.py |
Menu, jehož položky volají zvolené metody | |||
7 | example3.ui | soubor s návrhem GUI třetího demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example3.ui |
8 | example3.py | implementace třetího demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example3.py |
„Odtrhávací“ menu | |||
9 | example4.ui | soubor s návrhem GUI čtvrtého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example4.ui |
10 | example4.py | implementace čtvrtého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example4.py |
Klávesové zkratky přiřazené položkám menu | |||
11 | example5.ui | soubor s návrhem GUI pátého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example5.ui |
12 | example5.py | implementace pátého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example5.py |
13 | example5B.py | implementace pátého demonstračního příkladu s reakcemi na stisk kláves | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example5B.py |
Změna tématu zobrazení za běhu aplikace | |||
14 | example6.ui | soubor s návrhem GUI šestého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example6.ui |
15 | example6.py | implementace šestého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example6.py |
Standardní dialogová okna | |||
16 | example7.ui | soubor s návrhem GUI sedmého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example7.ui |
17 | example7.py | implementace sedmého demonstračního příkladu | https://github.com/tisnik/most-popular-python-libs/blob/master/pygubu/example7.py |
Ve druhém a třetím demonstračním příkladu jsou navíc použity i ikony v menu. Tyto ikony jsou uloženy samostatně ve formátu PNG a měly by být umístěny do stejného adresáře, ze kterého se spouští aplikace:
20. Odkazy na Internetu
- Seriál Grafické uživatelské rozhraní v Pythonu
https://www.root.cz/serialy/graficke-uzivatelske-rozhrani-v-pythonu/ - Pygubu na PyPi
https://pypi.org/project/pygubu/ - Repositář projektu Pygubu
https://github.com/alejandroautalan/pygubu - pygubu-designer na PyPi
https://pypi.org/project/pygubu-designer/ - Repositář projektu pygubu-designer
https://github.com/alejandroautalan/pygubu-designer - Pygubu Wiki
https://github.com/alejandroautalan/pygubu/wiki - How to install Tkinter in Python?
https://www.tutorialspoint.com/how-to-install-tkinter-in-python - Stránky projektu Glade
https://glade.gnome.org/ - Hra Breakout napísaná v Tkinteri
https://www.root.cz/clanky/hra-breakout-napisana-v-tkinteri/ - Brython aneb použití jazyka Python ve skriptech přímo v prohlížeči
https://www.root.cz/clanky/brython-aneb-pouziti-jazyka-python-ve-skriptech-primo-v-prohlizeci/ - The Hitchhiker's Guide to Pyhton: GUI Applications
http://docs.python-guide.org/en/latest/scenarios/gui/ - 7 Top Python GUI Frameworks for 2017
http://insights.dice.com/2014/11/26/5-top-python-guis-for-2015/ - GUI Programming in Python
https://wiki.python.org/moin/GuiProgramming - Cameron Laird's personal notes on Python GUIs
http://phaseit.net/claird/comp.lang.python/python_GUI.html - Python GUI development
http://pythoncentral.io/introduction-python-gui-development/ - Graphic User Interface FAQ
https://docs.python.org/2/faq/gui.html#graphic-user-interface-faq - TkInter
https://wiki.python.org/moin/TkInter - Tkinter 8.5 reference: a GUI for Python
http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/index.html - TkInter (Wikipedia)
https://en.wikipedia.org/wiki/Tkinter - Rapid application development
https://en.wikipedia.org/wiki/Rapid_application_development - Non-functional requirement
https://en.wikipedia.org/wiki/Non-functional_requirement - Graphical user interface builder
https://en.wikipedia.org/wiki/Graphical_user_interface_builder - User interface markup language
https://en.wikipedia.org/wiki/User_interface_markup_language - Top 10 programming languages that developers hate the most
https://www.techworm.net/2017/11/perl-hated-programming-language-developers-says-report.html