Hlavní navigace

Tvorba GUI v Pythonu s PySide: signály a sloty, správci rozložení komponent

Pavel Tišnovský

V dnešní části seriálu o tvorbě aplikací s grafickým uživatelským rozhraním v Pythonu budeme pokračovat v popisu knihovny PySide. Nejdříve si ukážeme práci se signály a sloty a posléze si popíšeme tři základní správce rozložení komponent.

Obsah

1. Tvorba GUI v Pythonu s využitím frameworku PySide: signály a sloty, správci rozložení komponent

2. Navázání signálu s využitím funkce QtCore.QObject.connect

3. Navázání signálu s využitím metody QtGui.QAbstractButton.clicked

4. Použití dekorátoru u slotu, zavolání metody namísto funkce při příjmu signálu

5. Umístění většího množství tlačítek do okna

6. Absolutní pozicování ovládacích prvků

7. Správci rozložení ovládacích prvků v kontejneru

8. Správce rozložení QHBoxLayout

9. Ukázka použití správce rozložení QHBoxLayout

10. Vložení pružných mezer mezi ovládací prvky

11. Správce rozložení QVBoxLayout

12. Ukázka použití správce rozložení QVBoxLayout

13. Správce rozložení QGridLayout

14. Ukázka použití správce rozložení QGridLayout

15. Komponenty umístěné do mřížky přes větší množství řádků či sloupců

16. Horizontální a vertikální zarovnání komponent v mřížce

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

18. Odkazy na Internetu

1. Tvorba GUI v Pythonu s využitím frameworku PySide: signály a sloty, správci rozložení komponent

V první části dnešního článku se budeme zabývat způsobem práce s takzvanými signály a sloty. V knihovně PySide může každý ovládací prvek (widget) generovat signály, a to konkrétně v případě vzniku nějaké události (tou může být například stisk tlačítka, změna pozice posuvníku, změna velikosti okna atd.) nebo změny stavu widgetu. Signály mohou být napojené na takzvané sloty, což je pojmenování pro funkce, které mohou reagovat na příchozí signál. V knihovně Qt, nad níž je PySide postaven, jsou signály zpracovávány nativními funkcemi, PySide ovšem celý koncept signálů a slotů dokáže „obalit“ takovým způsobem, že sloty jsou běžnými pythonovskými funkcemi či metodami. Musíme však explicitně specifikovat propojení mezi signálem (resp. jeho typem) a slotem. To lze zajistit dvěma způsoby popsanými v navazujících kapitolách.

Poznámka: ve skutečnosti se signály a sloty nepoužívají pouze pro práci s GUI, ale jedná se o univerzálnější koncept, který je v PySide používaný i pro další činnosti.

2. Navázání signálu s využitím funkce QtCore.QObject.connect

První způsob navázání signálu na slot používá funkci QtCore.QObject.connect. Této funkci se předávají tři parametry, které přesně specifikují propojení mezi signálem vyslaným určitým widgetem se slotem:

Parametr Význam parametru
sender widget, který bude signál vysílat (například tlačítko)
signal přesná specifikace typu signálu
method metoda nebo funkce, která bude zavolána při přijetí signálu

Význam prvního i třetího parametru je zřejmý, ještě si však musíme vysvětlit, jakým způsobem se získá typ signálu. Pro tento účel se používá funkce QtCore.SIGNAL, která dokáže převést název signálu reprezentovaný textovým řetězcem do formátu akceptovatelného funkcí QtCore.QObject.connect. Pokud tedy budeme chtít, aby se po stisku tlačítka reprezentovatelného objektem button zavolala metoda closeApplication, provedeme to následovně:

QtCore.QObject.connect(button, QtCore.SIGNAL('clicked()'), closeApplication)

Nám již známý příklad z předchozího článku, v němž je vytvořeno okno s jediným tlačítkem, nyní upravíme tak, aby stisk tlačítka aplikaci ukončil. Nejdříve vytvoříme funkci, která bude reagovat na signál:

def closeApplication():
    print("Closing...")
    sys.exit(0)

Dále vytvoříme tlačítko, nastavíme jeho vlastnosti a nakonfigurujeme příjemce signálu vyslaného po stisku tlačítka:

# tlačítko
button = QtGui.QPushButton("Quit", self)
button.resize(button.sizeHint())
button.setToolTip("Immediately quit this application")
 
# starý způsob navázání signálu, který není příliš Python-friendly
QtCore.QObject.connect(button, QtCore.SIGNAL ('clicked()'), closeApplication)

Obrázek 1: Tlačítko umístěné v oknu dnešního prvního demonstračního příkladu.

Úplný zdrojový kód příkladu bude vypadat následovně:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# callback funkce
def closeApplication():
    print("Closing...")
    sys.exit(0)
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítko
        button = QtGui.QPushButton("Quit", self)
        button.resize(button.sizeHint())
        button.setToolTip("Immediately quit this application")
 
        # starý způsob navázání signálu, který není příliš Python-friendly
        QtCore.QObject.connect(button, QtCore.SIGNAL ('clicked()'), closeApplication)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

3. Navázání signálu s využitím metody QtGui.QAbstractButton.clicked

Volání funkce QtCore.QObject.connect sice velmi přesně reflektuje způsob, jakým se signály a sloty konfigurují v knihovně Qt a v jazyku C++, ovšem v PySide můžeme použít i přímočařejší a z hlediska zápisu kratší způsob. Konkrétně u tlačítek, tedy u widgetů typu QPushButton, můžeme použít trojici metod pro registraci příjemce příslušného signálu:

Metoda Stručný popis
QPushButton.clicked() kliknutí na tlačítko či stisk mezerníku, pokud má tlačítko fokus
QPushButton.pressed() stisk tlačítka (používáno méně často)
QPushButton.released() puštění tlačítka (opět používáno méně často)

Pokud konfigurujeme signál vyslaný po stisku widgetu představovaného objektem button, můžeme namísto volání:

QtCore.QObject.connect(button, QtCore.SIGNAL('clicked()'), closeApplication)

použít kratší a přehlednější zápis:

button.clicked.connect(closeApplication)

Opět se podívejme, jak bude příklad upraven. Jedná se vlastně o změnu jediného řádku:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# callback funkce
def closeApplication():
    print("Closing...")
    sys.exit(0)
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítko
        button = QtGui.QPushButton("Quit", self)
        button.resize(button.sizeHint())
        button.setToolTip("Immediately quit this application")
 
        # navázání akce na signál
        button.clicked.connect(closeApplication)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

4. Použití dekorátoru u slotu, zavolání metody namísto funkce při příjmu signálu

Často se také setkáme s tím, že příjemce signálu, tj. funkce vystupující v roli slotu, je označen dekorátorem. Může to vypadat takto:

# callback funkce
@QtCore.Slot()
def closeApplication():
    print("Closing...")
    sys.exit(0)

Zde sice není použití dekorátoru nutné, ovšem obecně jejich využití vede k menší spotřebě operační paměti a k nepatrně rychlejšímu běhu aplikace. V dalších příkladech prozatím tento dekorátor používat nebudeme, ale ještě se k jeho funkci vrátíme ve chvíli, kdy si budeme ukazovat způsob vytvoření vlastního nového typu signálu.

Dále si ukažme, že příjemcem signálu nemusí být jen funkce, ale i metoda, což je ostatně užitečnější. Namísto registrace funkce:

button.clicked.connect(closeApplication)

můžeme zaregistrovat metodu:

button.clicked.connect(self.quit)

Tato metoda musí mít hlavičku:

def quit(self):

Samozřejmě i zde lze přidat dekorátor:

@QtCore.Slot()
def quit(self):

Pokud metoda nepřistupuje k atributům objektu, může být statická (nepředává se jí self):

button.clicked.connect(MainWindow.quit)
 
    @QtCore.Slot()
    @staticmethod
    def quit():
        print("Closing...")

V dalším příkladu již používáme korektní způsob ukončení aplikace v metodě MainWindow.quit:

def quit(self):
    print("Closing...")
    self.close()

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

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítko
        button = QtGui.QPushButton("Quit", self)
        button.resize(button.sizeHint())
        button.setToolTip("Immediately quit this application")
 
        # navázání akce na signál
        button.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    @QtCore.Slot()
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

5. Umístění většího množství tlačítek do okna

Ve druhé části dnešního článku se budeme zabývat tím, jak se vlastně jednotlivé ovládací prvky (widgety) umisťují do okna či do dalšího kontejneru. Tento problém je nutné vyřešit u všech aplikací, v nichž se nachází okna a dialogy s více než jedním widgetem. Abychom si ukázali, že se jedná o důležité téma, nebudeme se v dalším příkladu o umístění komponent do hlavního okna nijak starat a budeme jen pozorovat, jaké je implicitní chování. Vytvoříme pět tlačítek a umístíme je do hlavního okna:

# tlačítka
button1 = QtGui.QPushButton("One", self)
button2 = QtGui.QPushButton("Two", self)
button3 = QtGui.QPushButton("Three", self)
button4 = QtGui.QPushButton("Four", self)
button5 = QtGui.QPushButton("Five", self)

Výsledek po spuštění bude vypadat následovně – všechna tlačítka jsou zobrazena na stejném místě, konkrétně v levém horním rohu hlavního okna:

Obrázek 2: Všechna tlačítka se vměstnala na stejné místo v hlavním oknu, konkrétně do levého horního rohu.

Zdrojový kód příkladu, v němž se do hlavního okna vykreslí pět tlačítek (na stejném místě) vypadá takto:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One", self)
        button2 = QtGui.QPushButton("Two", self)
        button3 = QtGui.QPushButton("Three", self)
        button4 = QtGui.QPushButton("Four", self)
        button5 = QtGui.QPushButton("Five", self)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

Mimochodem: povšimněte si, že všechna tlačítka mají signály navázané na shodnou callback funkci:

# navázání akce na signál
button1.clicked.connect(self.quit)
button2.clicked.connect(self.quit)
button3.clicked.connect(self.quit)
button4.clicked.connect(self.quit)
button5.clicked.connect(self.quit)

6. Absolutní pozicování ovládacích prvků

Jednou z možností rozmístění ovládacích prvků (widgetů) na ploše okna je použití tzv. absolutního pozicování. To jistě znají pamětníci RAD nástrojů typu Delphi či Visual Basic. Při použití absolutního pozicování se explicitně specifikují pozice a rozměry každého widgetu, nezávisle na widgetech ostatních a taktéž nezávisle na rozlišení, globálního nastavení velikosti fontů, rozměrů okna či dialogu atd. Tento způsob rozmístění widgetů může být dosti problematický zejména ve chvíli, kdy je aplikace provozována na počítači s jinak nastaveným prostředím (stačí se jen přepnout do režimu pro slabozraké atd.). Ovšem pro úplnost si ukažme, jak je možné absolutního pozicování docílit. Není to nic složitého, protože každý widget podporuje metodu move, které se předává dvojice souřadnic [x,y] platných v rámci lokálního souřadného systému okna.

Ukažme si nyní, jak se přesunou čtyři tlačítka z naší pětice tak, aby si navzájem nepřekážela. Je to snadné:

# přesun tlačítek na absolutní pozice
button2.move(30, 30)
button3.move(60, 60)
button4.move(90, 90)
button5.move(120, 120)

Výsledek naší snahy je vidět na dalším screenshotu:

Obrázek 3: Čtyři tlačítka (z pěti) byla přemístěna na absolutní souřadnice.

Opět si ukažme úplný zdrojový kód takto upraveného demonstračního příkladu s vyznačením provedených změn:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One", self)
        button2 = QtGui.QPushButton("Two", self)
        button3 = QtGui.QPushButton("Three", self)
        button4 = QtGui.QPushButton("Four", self)
        button5 = QtGui.QPushButton("Five", self)
 
        # přesun tlačítek na absolutní pozice
        button2.move(30, 30)
        button3.move(60, 60)
        button4.move(90, 90)
        button5.move(120, 120)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

7. Správci rozložení ovládacích prvků v kontejneru

U většiny aplikací je vhodnější namísto absolutního pozicování jednotlivých ovládacích prvků použít již připravené správce rozložení (layout manager), kteří se sami starají o umístění komponent na základě jejich vzájemných vztahů. Při použití knihovny PySide si můžeme vybrat hned z několika typů správců, popř. lze správce kombinovat, a to díky tomu, že do okna je možné vložit další komponentu ve funkci kontejneru pro další komponenty. Seznam správců rozložení podporovaných knihovnou PySide naleznete v další tabulce:

Správce rozložení
QHBoxLayout
QVBoxLayout
QGridLayout
QStackedLayout
QFormLayout

V dalších kapitolách si popíšeme první tři správce, tj. QHBoxLayout, QVBoxLayout a QGridLayout.

8. Správce rozložení QHBoxLayout

Velmi jednoduchým správcem rozložení komponent v okně/dialogu je QHBoxLayout, který zajišťuje, že se jednotlivé ovládací prvky uspořádají horizontálně za sebe do jedné řady (v našich podmínkách zleva doprava). Komponenty se při použití tohoto správce přidávají metodou addWidget, které se v nejjednodušším případě pouze předá reference na přidávaný widget. Podívejme se nyní na způsob použití tohoto správce.

Nejprve vytvoříme novou instanci správce QHBoxLayout:

layout = QtGui.QHBoxLayout()

Následně již můžeme začít přidávat jednotlivé widgety, přičemž první widget bude umístěn zcela vlevo, další napravo od něj atd. atd.:

layout.addWidget(button1)
layout.addWidget(button2)
layout.addWidget(button3)
layout.addWidget(button4)
layout.addWidget(button5)

Následně nesmíme zapomenout oznámit hlavnímu oknu, že má použít právě vytvořený a nakonfigurovaný správce rozložení. Pokud na tento krok zapomeneme, komponenty se nevykreslí!

self.setLayout(layout)

Použití je tedy velmi podobné správci geometrie pack z knihovny Tkinter.

Obrázek 4: Pětice tlačítek umístěných do okna s využitím QHBoxLayout.

Do metody QHBoxLayout.addWidget lze předat i další dva nepovinné parametry nazvané stretch a alignment. Parametr stretch je celočíselný a slouží k řízení míry rozpínání widgetů v okně či dialogu, pokud se mění jeho velikost. Implicitní hodnotou je nula. Parametr alignment je možné využít ke specifikaci zarovnání ovládacích prvků (v rámci jednoho řádku):

Hodnota alignment Význam
QtCore.Qt.AlignTop zarovnání widgetu nahoru
QtCore.Qt.AlignBottom zarovnání widgetu dolů
QtCore.Qt.AlignVCenter vertikální vycentrování widgetu
QtCore.Qt.AlignCenter vycentrování widgetu ve vertikálním i horizontálním směru

9. Ukázka použití správce rozložení QHBoxLayout

Podívejme se nyní, jak je možné správce rozložení QHBoxLayout použít v praktickém příkladu:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One")
        button2 = QtGui.QPushButton("Two")
        button3 = QtGui.QPushButton("Three")
        button4 = QtGui.QPushButton("Four")
        button5 = QtGui.QPushButton("Five")
 
        # vytvoření správce geometrie a umístění widgetů
        layout = QtGui.QHBoxLayout()
        layout.addWidget(button1)
        layout.addWidget(button2)
        layout.addWidget(button3)
        layout.addWidget(button4)
        layout.addWidget(button5)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

10. Vložení pružných mezer mezi ovládací prvky

Mezi jednotlivé viditelné widgety je možné vkládat pružné mezery s využitím metody addStretch. Vliv těchto mezer se samozřejmě projeví ve chvíli, kdy se mění velikost okna nebo dialogu. Zkusme si nejdříve vložit pružnou mezeru před první tlačítko, tj. mezera bude vložena doleva. U malého okna bude mít mezera nulovou šířku, která se ale bude se zvětšováním postupně měnit:

# vytvoření správce geometrie a umístění widgetů
layout = QtGui.QHBoxLayout()
 
# pružná mezera
layout.addStretch(1)
layout.addWidget(button1)
layout.addWidget(button2)
layout.addWidget(button3)
layout.addWidget(button4)
layout.addWidget(button5)

Obrázek 5: Pružná mezera vložená před první tlačítko (okno v původní velikosti).

Obrázek 6: Pružná mezera vložená před první tlačítko (okno po manuálním zvětšení).

Pro úplnost – zdrojový text příkladu nyní vypadá takto:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One")
        button2 = QtGui.QPushButton("Two")
        button3 = QtGui.QPushButton("Three")
        button4 = QtGui.QPushButton("Four")
        button5 = QtGui.QPushButton("Five")
 
        # vytvoření správce geometrie a umístění widgetů
        layout = QtGui.QHBoxLayout()
 
        # pružná mezera
        layout.addStretch(1)
        layout.addWidget(button1)
        layout.addWidget(button2)
        layout.addWidget(button3)
        layout.addWidget(button4)
        layout.addWidget(button5)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

Nyní se podívejme, co se stane při vložení pružné mezery až za druhé tlačítko:

# vytvoření správce geometrie a umístění widgetů
layout = QtGui.QHBoxLayout()
 
layout.addWidget(button1)
layout.addWidget(button2)
 
# pružná mezera
layout.addStretch(1)
 
layout.addWidget(button3)
layout.addWidget(button4)
layout.addWidget(button5)

Obrázek 7: Pružná mezera vložená až za druhé tlačítko (okno v původní velikosti).

Obrázek 8: Pružná mezera vložená až za druhé tlačítko (okno po manuálním zvětšení).

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

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One")
        button2 = QtGui.QPushButton("Two")
        button3 = QtGui.QPushButton("Three")
        button4 = QtGui.QPushButton("Four")
        button5 = QtGui.QPushButton("Five")
 
        # vytvoření správce geometrie a umístění widgetů
        layout = QtGui.QHBoxLayout()
 
        layout.addWidget(button1)
        layout.addWidget(button2)
 
        # pružná mezera
        layout.addStretch(1)
        layout.addWidget(button3)
        layout.addWidget(button4)
        layout.addWidget(button5)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

11. Správce rozložení QVBoxLayout

Správce rozložení nazvaný QVBoxLayout umisťuje jednotlivé ovládací prvky (komponenty) pod sebe, tedy vertikálně. Jeho použití je prakticky shodné s výše popsaným správcem QHBoxLayout, takže si na tomto místě pouze uveďme základní příklad použití:

# vytvoření správce geometrie a umístění widgetů
layout = QtGui.QVBoxLayout()
 
layout.addWidget(button1)
layout.addWidget(button2)
layout.addWidget(button3)
layout.addWidget(button4)
layout.addWidget(button5)
 
# nastavení správce geometrie a vložení všech komponent do okna
self.setLayout(layout)

Obrázek 9: Pětice tlačítek rozmístěných do okna pomocí QVBoxLayout.

Ve skutečnosti je do metody QVBoxLayout.addWidget opět možné předat i další dva nepovinné parametry nazvané stretch a alignment. Parametr stretch je celočíselný a slouží k řízení míry rozpínání widgetů v okně či dialogu, pokud se mění jeho velikost. Implicitní hodnotou je nula. Parametr alignment je možné využít ke specifikaci zarovnání ovládacích prvků (pod sebe):

Hodnota alignment Význam
QtCore.Qt.AlignLeft zarovnání widgetu doleva
QtCore.Qt.AlignRight zarovnání widgetu doprava
QtCore.Qt.AlignHCenter horizontální vycentrování widgetu
QtCore.Qt.AlignCenter vycentrování widgetu ve vertikálním i horizontálním směru

Poznámka: kromě QtCore.Qt.AlignCenter se hodnoty zarovnání odlišují od správce QHBoxLayout.

12. Ukázka použití správce rozložení QVBoxLayout

Z následujícího zdrojového kódu je patrné, že se správce rozložení QVBoxLayout používá naprosto stejným způsobem, jako již popsaný správce rozložení QHBoxLayout, i když výsledná podoba hlavního okna je samozřejmě odlišná:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # velikost není potřeba specifikovat
        # self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One")
        button2 = QtGui.QPushButton("Two")
        button3 = QtGui.QPushButton("Three")
        button4 = QtGui.QPushButton("Four")
        button5 = QtGui.QPushButton("Five")
 
        # vytvoření správce geometrie a umístění widgetů
        layout = QtGui.QVBoxLayout()
        layout.addWidget(button1)
        layout.addWidget(button2)
        layout.addWidget(button3)
        layout.addWidget(button4)
        layout.addWidget(button5)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

13. Správce rozložení QGridLayout

Zatímco správci rozložení QHBoxLayout a QVBoxLayout mohou posloužit pro tvorbu spíše jednodušších dialogů popř. pro hierarchickou tvorbu GUI s využitím kontejnerů umístěných v jiných kontejnerech, je správce rozvržení nazvaný QGridLayout mnohem univerzálnější. Podobně jako správce Grid z knihovny Tkinter umožňuje QGridLayout umístit komponenty do pomyslné mřížky, jejíž velikost (počet sloupců a řádků) i rozměry buněk závisí na komponentách, které se do mřížky vloží. V tom nejjednodušším případě je pro každou komponentu vyhrazena jedna buňka mřížky, jejíž koordináty se specifikují při vkládání komponenty do GUI. První číslo odpovídá řádku, druhé sloupci:

# vytvoření správce geometrie a umístění widgetů
layout = QtGui.QGridLayout()
 
#                komponenta  řádek  sloupec
layout.addWidget(button1,    1,     1)
layout.addWidget(button2,    2,     2)
layout.addWidget(button3,    2,     3)
layout.addWidget(button4,    3,     2)
layout.addWidget(button5,    4,     1)
layout.addWidget(button6,    4,     2)
 
# nastavení správce geometrie a vložení všech komponent do okna
self.setLayout(layout)

Obrázek 10: Šest tlačítek umístěných do pomyslné mřížky.

14. Ukázka použití správce rozložení QGridLayout

Upravme si nyní náš demonstrační příklad takovým způsobem, aby se v něm namísto správce QHBoxLayout či QVBoxLayout využil právě správce QGridLayout. Úprava je velmi snadná:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # velikost není potřeba specifikovat
        # self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One")
        button2 = QtGui.QPushButton("Two")
        button3 = QtGui.QPushButton("Three")
        button4 = QtGui.QPushButton("Four")
        button5 = QtGui.QPushButton("Five")
        button6 = QtGui.QPushButton("Six")
 
        # vytvoření správce geometrie a umístění widgetů
        layout = QtGui.QGridLayout()
        layout.addWidget(button1, 1, 1)
        layout.addWidget(button2, 2, 2)
        layout.addWidget(button3, 2, 3)
        layout.addWidget(button4, 3, 2)
        layout.addWidget(button5, 4, 1)
        layout.addWidget(button6, 4, 2)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
        button6.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

15. Komponenty umístěné do mřížky přes větší množství řádků či sloupců

Pro složitější okna a dialogy si nevystačíme s pouhým umístěním komponent do jednotlivých buněk tabulky, ale budeme muset některé komponenty umístit přes větší množství buněk. I to je samozřejmě možné, protože při vkládání komponenty do mřížky je možné použít další dva nepovinné parametry, jimiž se zadává počet sloučených řádků a počet sloučených buněk. Pokud nejsou tyto parametry explicitně zapsány, předpokládá se, že jsou rovny jedné:

# vytvoření správce geometrie a umístění widgetů
layout = QtGui.QGridLayout()
 
#                komponenta  řádek  sloupec   slouč. řádků  slouč. sloupců
layout.addWidget(button1,    1,     1,        1,            2)
layout.addWidget(button2,    2,     2)
layout.addWidget(button3,    2,     3)
layout.addWidget(button4,    3,     2,        1,            2)
layout.addWidget(button5,    3,     1,        2,            1)
layout.addWidget(button6,    4,     2)
 
# nastavení správce geometrie a vložení všech komponent do okna
self.setLayout(layout)

Obrázek 11: Šest tlačítek, z nichž některá jsou umístěná do několika sloučených buněk.

Opět se podívejme na výpis úplného zdrojového kódu demonstračního příkladu se zvýrazněním provedených změn:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # velikost není potřeba specifikovat
        # self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One")
        button2 = QtGui.QPushButton("Two")
        button3 = QtGui.QPushButton("Three")
        button4 = QtGui.QPushButton("Four")
        button5 = QtGui.QPushButton("Five")
        button6 = QtGui.QPushButton("Six")
 
        # vytvoření správce geometrie a umístění widgetů
        layout = QtGui.QGridLayout()
        layout.addWidget(button1, 1, 1, 1, 2)
        layout.addWidget(button2, 2, 2)
        layout.addWidget(button3, 2, 3)
        layout.addWidget(button4, 3, 2, 1, 2)
        layout.addWidget(button5, 3, 1, 2, 1)
        layout.addWidget(button6, 4, 2)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
        button6.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

16. Horizontální a vertikální zarovnání komponent v mřížce

S problematikou zarovnání komponent jsme se již setkali, takže si jen doplňme, že i při použití správce rozvržení QGridLayout je možné specifikovat, jak se jednotlivé komponenty zarovnají:

# vytvoření správce geometrie a umístění widgetů
layout = QtGui.QGridLayout()
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(20)
 
#                komponenta  řádek  sloupec   slouč. řádků  slouč. sloupců  zarovnání
layout.addWidget(button1,    1,     1)
layout.addWidget(button2,    2,     1)
layout.addWidget(button3,    3,     1)
layout.addWidget(button4,    4,     1)
layout.addWidget(button5,    1,     2,        2,            1,              QtCore.Qt.AlignTop)
layout.addWidget(button6,    3,     2,        2,            1,              QtCore.Qt.AlignBottom)

Obrázek 12: Šest tlačítek, z nichž některá jsou umístěna do několika sloučených buněk. Dvě tlačítka ve druhém sloupci jsou explicitně zarovnána na horní resp. dolní okraj sloučených buněk.

Nyní však máme k dispozici ucelený repertoár zarovnání – jak horizontálního, tak i vertikálního:

Hodnota alignment Význam
QtCore.Qt.AlignLeft zarovnání widgetu doleva
QtCore.Qt.AlignRight zarovnání widgetu doprava
QtCore.Qt.AlignHCenter horizontální vycentrování widgetu
QtCore.Qt.AlignTop zarovnání widgetu nahoru
QtCore.Qt.AlignBottom zarovnání widgetu dolů
QtCore.Qt.AlignVCenter vertikální vycentrování widgetu
QtCore.Qt.AlignCenter vycentrování widgetu ve vertikálním i horizontálním směru

Opět se podívejme na příklad, dnes již poslední. V příkladu navíc explicitně nastavujeme mezery mezi komponentami:

layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(20)

Zdrojový kód:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindow(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # velikost není potřeba specifikovat
        # self.resize(320, 240)
        self.setWindowTitle("Quit Button")
 
        # tlačítka
        button1 = QtGui.QPushButton("One")
        button2 = QtGui.QPushButton("Two")
        button3 = QtGui.QPushButton("Three")
        button4 = QtGui.QPushButton("Four")
        button5 = QtGui.QPushButton("Five")
        button6 = QtGui.QPushButton("Six")
 
        # vytvoření správce geometrie a umístění widgetů
        layout = QtGui.QGridLayout()
        layout.setHorizontalSpacing(0)
        layout.setVerticalSpacing(20)
 
        layout.addWidget(button1, 1, 1)
        layout.addWidget(button2, 2, 1)
        layout.addWidget(button3, 3, 1)
        layout.addWidget(button4, 4, 1)
        layout.addWidget(button5, 1, 2, 2, 1, QtCore.Qt.AlignTop)
        layout.addWidget(button6, 3, 2, 2, 1, QtCore.Qt.AlignBottom)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
        # navázání akce na signál
        button1.clicked.connect(self.quit)
        button2.clicked.connect(self.quit)
        button3.clicked.connect(self.quit)
        button4.clicked.connect(self.quit)
        button5.clicked.connect(self.quit)
        button6.clicked.connect(self.quit)
 
    def run(self, app):
        # zobrazení okna na obrazovce
        self.show()
        # vstup do smyčky událostí (event loop)
        app.exec_()
 
    def quit(self):
        print("Closing...")
        self.close()
 
 
def main():
    app = QtGui.QApplication(sys.argv)
    MainWindow().run(app)
 
 
if __name__ == '__main__':
    main()

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

Zdrojové kódy všech dvanácti dnes popsaných demonstračních příkladů byly opět 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:

18. Odkazy na Internetu

  1. PySide documentation
    https://srinikom.github.io/pyside-docs/index.html
  2. Differences Between PySide and PyQt
    https://wiki.qt.io/Differen­ces_Between_PySide_and_PyQt
  3. PySide tutorials
    https://srinikom.github.io/pyside-docs/tutorials/index.html
  4. PySide tutorial
    http://zetcode.com/gui/py­sidetutorial/
  5. QLayout
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QLayout.html
  6. QStackedLayout
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QStackedLayout.html
  7. QFormLayout
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QFormLayout.html
  8. QBoxLayout
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QBoxLayout.html
  9. QHBoxLayout
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QHBoxLayout.html
  10. QVBoxLayout
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QVBoxLayout.html
  11. QGridLayout
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QGridLayout.html
  12. Signals & Slots
    http://doc.qt.io/qt-4.8/signalsandslots.html
  13. Signals and Slots in PySide
    http://wiki.qt.io/Signals_an­d_Slots_in_PySide
  14. Intro to PySide/PyQt: Basic Widgets and Hello, World!
    http://www.pythoncentral.io/intro-to-pysidepyqt-basic-widgets-and-hello-world/
  15. QWidget
    https://srinikom.github.io/pyside-docs/PySide/QtGui/QWidget.html
  16. Leo editor
    http://leoeditor.com/
  17. IPython Qt Console aneb vylepšený pseudoterminál
    https://mojefedora.cz/integrovana-vyvojova-prostredi-ve-fedore-ipython-a-ipython-notebook/#k06
  18. Vývojová prostředí ve Fedoře (4. díl)
    https://mojefedora.cz/vyvojova-prostredi-ve-fedore-4-dil/
  19. Seriál Letní škola programovacího jazyka Logo
    http://www.root.cz/serialy/letni-skola-programovaciho-jazyka-logo/
  20. Educational programming language
    http://en.wikipedia.org/wi­ki/Educational_programmin­g_language
  21. Logo Tree Project:
    http://www.elica.net/downlo­ad/papers/LogoTreeProject­.pdf
  22. Hra Breakout napísaná v Tkinteri
    https://www.root.cz/clanky/hra-breakout-napisana-v-tkinteri/
  23. Hra Snake naprogramovaná v Pythone s pomocou Tkinter
    https://www.root.cz/clanky/hra-snake-naprogramovana-v-pythone-s-pomocou-tkinter/
  24. 24.1. turtle — Turtle graphics
    https://docs.python.org/3­.5/library/turtle.html#mo­dule-turtle
  25. TkDND
    http://freecode.com/projects/tkdnd
  26. Python Tkinter Fonts
    https://www.tutorialspoin­t.com/python/tk_fonts.htm
  27. The Tkinter Canvas Widget
    http://effbot.org/tkinter­book/canvas.htm
  28. Ovládací prvek (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Ovl%C3%A1dac%C3%AD_prvek_­%28po%C4%8D%C3%ADta%C4%8D%29
  29. Rezervovaná klíčová slova v Pythonu
    https://docs.python.org/3/re­ference/lexical_analysis.html#ke­ywords
  30. TkDocs: Styles and Themes
    http://www.tkdocs.com/tuto­rial/styles.html
  31. Drawing in Tkinter
    http://zetcode.com/gui/tkin­ter/drawing/
  32. Changing ttk widget text color (StackOverflow)
    https://stackoverflow.com/qu­estions/16240477/changing-ttk-widget-text-color
  33. The Hitchhiker's Guide to Pyhton: GUI Applications
    http://docs.python-guide.org/en/latest/scenarios/gui/
  34. 7 Top Python GUI Frameworks for 2017
    http://insights.dice.com/2014/11/26/5-top-python-guis-for-2015/
  35. GUI Programming in Python
    https://wiki.python.org/mo­in/GuiProgramming
  36. Cameron Laird's personal notes on Python GUIs
    http://phaseit.net/claird/com­p.lang.python/python_GUI.html
  37. Python GUI development
    http://pythoncentral.io/introduction-python-gui-development/
  38. Graphic User Interface FAQ
    https://docs.python.org/2/faq/gu­i.html#graphic-user-interface-faq
  39. TkInter
    https://wiki.python.org/moin/TkInter
  40. Tkinter 8.5 reference: a GUI for Python
    http://infohost.nmt.edu/tcc/hel­p/pubs/tkinter/web/index.html
  41. TkInter (Wikipedia)
    https://en.wikipedia.org/wiki/Tkinter
  42. appJar
    http://appjar.info/
  43. appJar (Wikipedia)
    https://en.wikipedia.org/wiki/AppJar
  44. appJar na Pythonhosted
    http://pythonhosted.org/appJar/
  45. appJar widgets
    http://appjar.info/pythonWidgets/
  46. Stránky projektu PyGTK
    http://www.pygtk.org/
  47. PyGTK (Wikipedia)
    https://cs.wikipedia.org/wiki/PyGTK
  48. Stránky projektu PyGObject
    https://wiki.gnome.org/Pro­jects/PyGObject
  49. Stránky projektu Kivy
    https://kivy.org/#home
  50. Stránky projektu PyQt
    https://riverbankcomputin­g.com/software/pyqt/intro
  51. PyQt (Wikipedia)
    https://cs.wikipedia.org/wiki/PyGTK
  52. Stránky projektu PySide
    https://wiki.qt.io/PySide
  53. PySide (Wikipedia)
    https://en.wikipedia.org/wiki/PySide
  54. Stránky projektu Kivy
    https://kivy.org/#home
  55. Kivy (framework, Wikipedia)
    https://en.wikipedia.org/wi­ki/Kivy_(framework)
  56. QML Applications
    http://doc.qt.io/qt-5/qmlapplications.html
  57. KDE
    https://www.kde.org/
  58. Qt
    https://www.qt.io/
  59. GNOME
    https://en.wikipedia.org/wiki/GNOME
  60. Category:Software that uses PyGTK
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_PyGTK
  61. Category:Software that uses PyGObject
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_PyGObject
  62. Category:Software that uses wxWidgets
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_wxWidgets
  63. GIO
    https://developer.gnome.or­g/gio/stable/
  64. GStreamer
    https://gstreamer.freedesktop.org/
  65. GStreamer (Wikipedia)
    https://en.wikipedia.org/wi­ki/GStreamer
  66. Wax Gui Toolkit
    https://wiki.python.org/moin/Wax
  67. Python Imaging Library (PIL)
    http://infohost.nmt.edu/tcc/hel­p/pubs/pil/
  68. 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?