Hlavní navigace

Tvorba GUI v Pythonu: widgety pro zobrazení grafických informací nabízené knihovnou appJar

Pavel Tišnovský

V šestém a současně i předposledním článku o knihovně appJar určené pro tvorbu aplikací s GUI v Pythonu se budeme věnovat popisu těch ovládacích prvků, které lze použít pro zobrazení grafických informací.

Obsah

1. Ovládací prvky a další prostředky určené pro zobrazení grafických informací

2. Ovládací prvek typu Image

3. Malá odbočka – programové vytvoření procedurální textury s obrázkem

4. První demonstrační příklad – moaré jako obrázek ve stupních šedi

5. Druhý demonstrační příklad – aplikace barvové palety

6. Použití ovládacího prvku typu Image

7. Rastrový obrázek uložený přímo ve zdrojovém kódu aplikace

8. Postup při uložení obrázku do zdrojového kódu

9. Použití ovládacího prvku typu Image a obrázku ze zdrojového kódu

10. Ovládací prvek zobrazující koláčový diagram

11. Zobrazení mřížky LED (emulace počítače Micro Bit)

12. Programová změna světlosti jednotlivých LED

13. Použití prvku Canvas z Tkinteru

14. Ukázka použití Canvasu

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

16. Odkazy na Internetu

1. Ovládací prvky a další prostředky určené pro zobrazení grafických informací

Knihovna appJar vývojářům nabízí pouze velmi omezené množství ovládacích prvků, které je možné použít pro zobrazení grafických informací, ať již se to týká rastrové grafiky či grafiky vektorové. Existují vlastně pouze tři widgety, které práci s grafikou do jisté míry podporují. Prvním z těchto ovládacích prvků je widget nazvaný Image, jenž dokáže na ploše okna či dialogu zobrazit rastrový obrázek, který je načtený z externího souboru nebo ho je alternativně možné načíst z pole bajtů či znaků, které obsahují zakódovaná data obrázku. Dále tento widget dokáže reagovat na stisk tlačítka myši. Druhý widget je již specializovaný, protože slouží pro zobrazení koláčového grafu, ovšem jen s omezenými možnostmi nastavení parametrů zobrazení.

Třetí ovládací prvek, který do jisté míry podporuje zobrazení grafických informací, je widget simulující matici LED na jednodeskovém mikropočítači Micro Bit. Tato matice sice má „rozlišení“ pouze 5×5 bodů (pixelů), ovšem kupodivu právě tento widget je mezi dětmi oblíbený (což je vlastně na první pohled dost paradoxní, zvláště když vezmeme v úvahu možnosti dnešních počítačů a jejich GPU).

Pokud tyto možnosti nabízené přímo knihovnou appJar nebudou dostačující (a to pro složitější aplikace zcela jistě nebudou), je možné využít prostředky Tkinteru, tj. především prvek Canvas. I s touto možností se seznámíme, protože díky Tkinteru a Canvasu je možné skloubit hned dvě užitečné výukové pomůcky – appJar pro GUI a modul turtle pro želví grafiku.

2. Ovládací prvek typu Image

Prvním ovládacím prvkem, který si v dnešním článku popíšeme, je widget nazvaný jednoduše Image. Víme již, že tento widget dokáže na plochu okna nebo dialogu umístit rastrový obrázek, který může být načtený z externích souborů typu PPM (Portable PixelMap), GIF (Graphics Interchange Format), PNG (Portable Network Graphics) popř. JPEG.

Ovšem musíme si dát pozor na to, že pouze první dva formáty jsou přímo podporovány knihovnou TkInter, nad níž je appJar postavena. To mj. znamená, že se při načítání obrázků uložených ve formátech PNG a JPEG musí obrázek dekódovat, a to knihovnami naprogramovanými přímo v Pythonu (tyto knihovny jsou součástí instalace appJar). To s sebou přináší výhodu snadné přenositelnosti (není nutné přistupovat k nativním knihovnám, řešit nekompatibility mezi knihovnami apod.), na druhou stranu je však dekódování rastrových obrázků dosti pomalé, což se negativně projeví u rozměrnějších obrázků a/nebo u aplikací spouštěných například na jednodeskových mikropočítačích Raspberry Pi a podobně „výkonných“ strojích.

Poznámka: alternativně je možné pro převod obrázků z prakticky libovolného formátu do GIFu použít knihovny PIL neboli (Python Imaging Library) či Pillow (fork dnes již nevyvíjené knihovny PIL), to však již vyžaduje zásahy do konfigurace systému, na němž má být vyvíjená aplikace provozována.

3. Malá odbočka – programové vytvoření procedurální textury s obrázkem

Ještě předtím, než si ukážeme, jakým způsobem je možné v knihovně appJar použít rastrové obrázky, si jeden takový obrázek programově vytvoříme. Bude se jednat o procedurální texturu založenou na efektu takzvaného moaré. Tuto procedurální texturu (či možná lépe řečeno rastrový vzorek) vytvořil John Connett z Minnesotské univerzity. O tomto vzorku, který v podstatě názorně ukazuje vliv aliasu při tvorbě rastrových obrázků, později pojednal i A. K. Dewdney v časopise Scientific American. Popisovaný vzorek je generovaný velmi jednoduchým a taktéž dostatečně rychlým způsobem: každému pixelu ve vytvářeném rastrovém obrázku (bitmapě) je přiřazena dvojice souřadnic [x, y]. Tyto souřadnice obecně neodpovídají celočíselným indexům pixelu, které můžeme například označit [i, j] (záleží totiž na zvoleném faktoru zvětšení popř. zmenšení vzorku). Posléze je pro každý pixel vypočtena hodnota z na základě jednoduchého vztahu z=x2+y2.

Obrázek 1: Moaré s kružnicovým vzorkem.

A to je vlastně celý algoritmus, ke kterému ještě přidáme část, která na základě vypočtené hodnoty z vybere vhodnou barvu z barvové palety a pixel následně touto barvou vyplní. Tímto přímočarým, rychlým a současně i jednoduchým způsobem je možné vytvářet mnohdy fantastické vzorky; pouze stačí měnit barvovou paletu (ideální jsou plynulé přechody mezi barvami – gradient) a měřítko, pomocí kterého se převádí celočíselné pozice pixelů v rastru [i, j] na souřadnice [x, y].

Obrázek 2: Mez zvětšení, při kterém již kružnicový vzorek začíná mizet.

4. První demonstrační příklad – moaré jako obrázek ve stupních šedi

Způsob tvorby moaré s kružnicovým vzorkem je ukázán v dnešním prvním demonstračním příkladu, který je založen na funkci nazvané recalc_circle_pattern(). Tato funkce provádí výpočet popsaný v předchozí kapitole, na konci pak převádí vypočtenou hodnotu do celočíselného rozsahu 0..255:

# Funkce provadejici vypocet moare s kruznicovym vzorkem
def recalc_circle_pattern(image, xmin, ymin, xmax, ymax):
    width, height = image.size       # rozmery obrazku
    stepx = (xmax - xmin)/width
    stepy = (ymax - ymin)/height
    print(xmin, xmax, ymin, ymax, width, height, stepx, stepy)
 
    y1 = ymin
    for y in range(0, height):
        x1 = xmin
        for x in range(0, width):
            x1 += stepx
            x2 = x1 * x1
            y2 = y1 * y1
            i = (int)(x2 + y2) & 255
            color = (i, i, i)
            image.putpixel((x, y), color)
        y1 += stepy

Obrázek 3: Při určitém měřítku narazíme na limit, pod kterým již nevidíme další detaily (viz střed obrázku, při jehož zvětšení již další detaily nebudou patrné).

Obrázek je vytvořen knihovnou PIL, která se současně postará o jeho uložení do souboru formátu PNG:

from PIL import Image
 
mez = (2 << 5) + 50 * 2.5
image = Image.new("RGB", (IMAGE_WIDTH, IMAGE_HEIGHT))
 
recalc_circle_pattern(image, -mez, -mez, mez, mez)
image.save("bw_moare.png")

Obrázek 4: Procedurální textura vytvořená dnešním prvním demonstračním příkladem.

Následuje výpis úplného zdrojového kódu prvního demonstračního příkladu:

#!/usr/bin/env python
 
# Vytvoreni obrazku s "kruznicovym moare"
 
from PIL import Image
 
# velikost obrazku
# mocninou cisla 2
IMAGE_WIDTH = 256
IMAGE_HEIGHT = 256
 
 
# Funkce provadejici vypocet moare s kruznicovym vzorkem
def recalc_circle_pattern(image, xmin, ymin, xmax, ymax):
    width, height = image.size       # rozmery obrazku
    stepx = (xmax - xmin)/width
    stepy = (ymax - ymin)/height
 
    y1 = ymin
    for y in range(0, height):
        x1 = xmin
        for x in range(0, width):
            x1 += stepx
            x2 = x1 * x1
            y2 = y1 * y1
            i = (int)(x2 + y2) & 255
            color = (i, i, i)
            image.putpixel((x, y), color)
        y1 += stepy
 
 
mez = (2 << 5) + 50 * 2.5
image = Image.new("RGB", (IMAGE_WIDTH, IMAGE_HEIGHT))
 
recalc_circle_pattern(image, -mez, -mez, mez, mez)
image.save("bw_moare.png")

5. Druhý demonstrační příklad – aplikace barvové palety

Obrázky vykreslené ve stupních šedi sice mohou být pro některé projekty zajímavé (survival horory atd.), ovšem většinou požadujeme obrázky barevné. Ve skutečnosti je řešení jednoduché – postačuje ke každému vypočtenému indexu vybrat vhodnou barvu z barvové palety. Tradičně mají barvové palety 256 barev, ovšem samozřejmě je v případě potřeby možné vytvořit rozsáhlejší či naopak menší palety. Pro účely našeho demonstračního příkladu použijeme barvovou paletu získanou z datových souborů programu Fractint a převedenou na Pythonovskou n-tici (samozřejmě lze použít i seznam). Upravená funkce pro vytvoření textury bude vypadat následovně:

# Funkce provadejici vypocet moare s kruznicovym vzorkem
def recalc_circle_pattern(image, palette, xmin, ymin, xmax, ymax):
    width, height = image.size       # rozmery obrazku
    stepx = (xmax - xmin)/width
    stepy = (ymax - ymin)/height
 
    y1 = ymin
    for y in range(0, height):
        x1 = xmin
        for x in range(0, width):
            x1 += stepx
            x2 = x1 * x1
            y2 = y1 * y1
            i = (int)(x2 + y2) & 255
            color = (palette[i][0], palette[i][1], palette[i][2])
            image.putpixel((x, y), color)
        y1 += stepy

Obrázek 5: Procedurální textura vytvořená dnešním druhým demonstračním příkladem.

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

#!/usr/bin/env python
 
# Vytvoreni obrazku s "kruznicovym moare"
 
from PIL import Image
 
# velikost obrazku
# mocninou cisla 2
IMAGE_WIDTH = 256
IMAGE_HEIGHT = 256
 
 
# taken from Fractint
palette = (
        (000, 000,   0), (000, 000,   0), (000, 000,   4), (000, 000,  12),
        (000, 000,  16), (000, 000,  24), (000, 000,  32), (000, 000,  36),
        (000, 000,  44), (000, 000,  48), (000, 000,  56), (000, 000,  64),
        (000, 000,  68), (000, 000,  76), (000, 000,  80), (000, 000,  88),
        (000, 000,  96), (000, 000, 100), (000, 000, 108), (000, 000, 116),
        (000, 000, 120), (000, 000, 128), (000, 000, 132), (000, 000, 140),
        (000, 000, 148), (000, 000, 152), (000, 000, 160), (000, 000, 164),
        (000, 000, 172), (000, 000, 180), (000, 000, 184), (000, 000, 192),
        (000, 000, 200), (000,   4, 200), (000,  12, 200), (000,  16, 204),
        (000,  24, 204), (000,  28, 208), (000,  36, 208), (000,  40, 208),
        (000,  48, 212), (000,  56, 212), (000,  60, 216), (000,  68, 216),
        (000,  72, 216), (000,  80, 220), (000,  84, 220), (000,  92, 224),
        (000, 100, 224), (000, 104, 224), (000, 112, 228), (000, 116, 228),
        (000, 124, 232), (000, 128, 232), (000, 136, 232), (000, 140, 236),
        (000, 148, 236), (000, 156, 240), (000, 160, 240), (000, 168, 240),
        (000, 172, 244), (000, 180, 244), (000, 184, 248), (000, 192, 248),
        (000, 200, 252),   (4, 200, 252),  (12, 200, 252),  (20, 204, 252),
        (28,  204, 252),  (36, 208, 252),  (44, 208, 252),  (52, 208, 252),
        (60,  212, 252),  (68, 212, 252),  (76, 216, 252),  (84, 216, 252),
        (92,  216, 252), (100, 220, 252), (108, 220, 252), (116, 224, 252),
        (124, 224, 252), (132, 224, 252), (140, 228, 252), (148, 228, 252),
        (156, 232, 252), (164, 232, 252), (172, 232, 252), (180, 236, 252),
        (188, 236, 252), (196, 240, 252), (204, 240, 252), (212, 240, 252),
        (220, 244, 252), (228, 244, 252), (236, 248, 252), (244, 248, 252),
        (252, 252, 252), (248, 252, 252), (244, 252, 252), (240, 252, 252),
        (232, 252, 252), (228, 252, 252), (224, 252, 252), (216, 252, 252),
        (212, 252, 252), (208, 252, 252), (200, 252, 252), (196, 252, 252),
        (192, 252, 252), (184, 252, 252), (180, 252, 252), (176, 252, 252),
        (168, 252, 252), (164, 252, 252), (160, 252, 252), (156, 252, 252),
        (148, 252, 252), (144, 252, 252), (140, 252, 252), (132, 252, 252),
        (128, 252, 252), (124, 252, 252), (116, 252, 252), (112, 252, 252),
        (108, 252, 252), (100, 252, 252),  (96, 252, 252),  (92, 252, 252),
        (84,  252, 252),  (80, 252, 252),  (76, 252, 252),  (72, 252, 252),
        (64,  252, 252),  (60, 252, 252),  (56, 252, 252),  (48, 252, 252),
        (44,  252, 252),  (40, 252, 252),  (32, 252, 252),  (28, 252, 252),
        (24,  252, 252),  (16, 252, 252),  (12, 252, 252),   (8, 252, 252),
        (000, 252, 252), (000, 248, 252), (000, 244, 252), (000, 240, 252),
        (000, 232, 252), (000, 228, 252), (000, 224, 252), (000, 216, 252),
        (000, 212, 252), (000, 208, 252), (000, 200, 252), (000, 196, 252),
        (000, 192, 252), (000, 184, 252), (000, 180, 252), (000, 176, 252),
        (000, 168, 252), (000, 164, 252), (000, 160, 252), (000, 156, 252),
        (000, 148, 252), (000, 144, 252), (000, 140, 252), (000, 132, 252),
        (000, 128, 252), (000, 124, 252), (000, 116, 252), (000, 112, 252),
        (000, 108, 252), (000, 100, 252), (000,  96, 252), (000,  92, 252),
        (000,  84, 252), (000,  80, 252), (000,  76, 252), (000,  72, 252),
        (000,  64, 252), (000,  60, 252), (000,  56, 252), (000,  48, 252),
        (000,  44, 252), (000,  40, 252), (000,  32, 252), (000,  28, 252),
        (000,  24, 252), (000,  16, 252), (000,  12, 252), (000,   8, 252),
        (000, 000, 252), (000, 000, 248), (000, 000, 244), (000, 000, 240),
        (000, 000, 236), (000, 000, 232), (000, 000, 228), (000, 000, 224),
        (000, 000, 220), (000, 000, 216), (000, 000, 212), (000, 000, 208),
        (000, 000, 204), (000, 000, 200), (000, 000, 196), (000, 000, 192),
        (000, 000, 188), (000, 000, 184), (000, 000, 180), (000, 000, 176),
        (000, 000, 172), (000, 000, 168), (000, 000, 164), (000, 000, 160),
        (000, 000, 156), (000, 000, 152), (000, 000, 148), (000, 000, 144),
        (000, 000, 140), (000, 000, 136), (000, 000, 132), (000, 000, 128),
        (000, 000, 124), (000, 000, 120), (000, 000, 116), (000, 000, 112),
        (000, 000, 108), (000, 000, 104), (000, 000, 100), (000, 000,  96),
        (000, 000,  92), (000, 000,  88), (000, 000,  84), (000, 000,  80),
        (000, 000,  76), (000, 000,  72), (000, 000,  68), (000, 000,  64),
        (000, 000,  60), (000, 000,  56), (000, 000,  52), (000, 000,  48),
        (000, 000,  44), (000, 000,  40), (000, 000,  36), (000, 000,  32),
        (000, 000,  28), (000, 000,  24), (000, 000,  20), (000, 000,  16),
        (000, 000,  12), (000, 000,   8), (000, 000,   0), (000, 000,   0))
 
 
# Funkce provadejici vypocet moare s kruznicovym vzorkem
def recalc_circle_pattern(image, palette, xmin, ymin, xmax, ymax):
    width, height = image.size       # rozmery obrazku
    stepx = (xmax - xmin)/width
    stepy = (ymax - ymin)/height
 
    y1 = ymin
    for y in range(0, height):
        x1 = xmin
        for x in range(0, width):
            x1 += stepx
            x2 = x1 * x1
            y2 = y1 * y1
            i = (int)(x2 + y2) & 255
            color = (palette[i][0], palette[i][1], palette[i][2])
            image.putpixel((x, y), color)
        y1 += stepy
 
 
mez = (2 << 5) + 50 * 2.5
image = Image.new("RGB", (IMAGE_WIDTH, IMAGE_HEIGHT))
 
recalc_circle_pattern(image, palette, -mez, -mez, mez, mez)
image.save("moare.png")

6. Použití ovládacího prvku typu Image

Obrázky pro otestování již máme připraveny, takže se nyní podívejme na to, jakým způsobem je možné obrázek načíst a následně vložit na plochu hlavního okna aplikace. Ve skutečnosti je to velmi snadné, protože přímo při vytváření widgetu s obrázkem můžeme specifikovat soubor, z něhož se má obrázek načíst. Současně se z rozměrů obrázku zjistí velikost widgetu (prvním parametrem se specifikuje identifikátor widgetu):

app.addImage("image", "moare.png")

Obrázek 6: Screenshot příkladu, v němž je použit widget typu Image.

Celý příklad, který po svém spuštění zobrazí rastrový obrázek načtený z externího souboru, vypadá následovně:

#!/usr/bin/env python
 
from appJar import gui
import tkinter
 
 
app = gui()
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addImage("image", "moare.png")
 
app.go()

7. Rastrový obrázek uložený přímo ve zdrojovém kódu aplikace

V některých případech můžeme chtít, například kvůli zjednodušení instalace vyvíjené aplikace, aby byl rastrový obrázek přímo součástí zdrojových kódů. I to je možné zařídit, ovšem tato možnost se obvykle používá pouze pro obrázky s malým rozlišením, protože obrázek uložený přímo ve zdrojových kódech zabere větší prostor (o více než 33%). V dalším textu se dozvíme, proč tomu tak je. Aby bylo možné ukládat data rastrového obrázku přímo ve zdrojových kódech, je nutné je nejprve vhodným způsobem zakódovat, protože ne všechny znaky je možné ve zdrojových kódech použít (některé jsou „jen“ nečitelně či needitovatelné, další pak mají speciální význam).

Z tohoto důvodu podporuje knihovna appJar načítání obrázků zakódovaných s využitím Base64. Výsledek je umístěn do běžného řetězce reprezentovatelného v programovacím jazyku Python. Povšimněte si, že libovolná data zakódovaná do Base64 lze skutečně reprezentovat řetězcem, protože se ve výsledku nebudou nacházet žádné znaky, které by tomu bránily (v Base64 se totiž používají jen znaky a-z, A-Z, 0–9, plus, lomítko, znak = a taktéž znak pro konec řádku). Navíc mají řádky výsledného řetězce jen 64 až 76 znaků na každém řádku, což odpovídá doporučení PEP-8.

8. Postup při uložení obrázku do zdrojového kódu

Podívejme se nyní na způsob uložení dat rastrového obrázku přímo do zdrojových kódů. Zdrojem bude libovolný soubor s rastrovým obrázkem malých rozměrů. Ideální je formát GIF. Obrázek nejprve zakódujeme do Base64 a výsledek uložíme do pomocného textového souboru:

base64 obrázek.gif > obrázek.py

Obrázek 7: Ikona uložená ve formátu GIF, kterou přes utilitu base64 převedeme do plnohodnotného řetězce jazyka Python.

Pokud například budeme převádět ikonu application-exit.gif získanou z adresy (https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/appJar/icons/appli­cation-exit.gif), použijeme příkaz:

base64 application-exit.gif > application-exit.py

Zakódováním by měl vzniknout přesně tento soubor:

R0lGODlhFgAWAPZJAAAAAHgCAYcHBYgZGZUJA5cRCpsYEpkkFpkrJKQLBKgYBaQQELQCArEYGKQo
Fq4yFrUlBrAoE7g5F6g0J6Y2NqlMO6lJRrVLRLpSSLZaWbxpacg1B9U7BMwpKMw5ONQnJ9w4OMlH
FttDBtFOFudJBPNPAPdTAMFbTsZmV8J3d+FiYut9fauCebGHfqyLhK6cnLaMhL+Si7ynp8mIiMaW
j86Skt+KitGZmcSsq92trd6yrd+ysuuIh+6oqOS7reW3svKqqurDtM7OztjY2OTMyujT0/DExOjo
6Pj4+P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5
BAEAAEkALAAAAAAWABYAAAf+gEmCg4SFg0hIgz0rIB+Oj5AdjypANoI8Rjs5mzs7Pz2boZw9HoId
N6ipqquoOQ2CDDOyLy81sreztTM3C7CyOEdHMriyMsE4MzW9SQwpKURIR0gyzs7G0UQpM8sMGt5F
wUc43sbB2RozBoIJGe0aRMFFOMDmGu0pBevt+/BHQ/9DiOzTkIKAoAUUKFS4gAEDEYABUZyoMIFA
AYNJFhAQQEBBBAkuILaAoCDBxgICDm7sCIEFRCFCWJAkQMBAyiQGaCZQ0CLYEB8+YA4ZqaDmzZwd
Y4QLQoKEj39HYkBIcOAoTRpLSZQwUSLIkGA0FDgIIOgATR/RgojQypVEEGlMP8SSRXLgogIfQTZw
WNuUA4cgcQk4SInEQl0FECDoFbFWBIcNG0gqmDDXQgUHDyRICBFihOcRnDU/mICAbJIBAVKrXs16
taHXsAUFAgA7

Povšimněte si, že vytvořený soubor je větší, než originál. Je tomu tak především z toho důvodu, že se v kódování Base64 používá jen 64 vybraných znaků a nikoli všech 256 možných hodnot, tudíž se každé tři bajty (3×8=24 bitů) uloží ve čtyřech znacích (4×6=24 bitů). Navíc se do výsledného souboru ukládají znaky pro konec řádku a na samotném konci pak výplně tvořené znaky „=“.

Ve druhém kroku otevřeme vytvořený soubor „application-exit.py“ v textovém editoru a dopíšeme na první řádek deklaraci proměnné a její inicializaci řetězcem. Vzhledem k tomu, že se jedná o víceřádkový řetězec, použije se trojice uvozovek (konce řádků se v Base64 ignorují, takže vlastní data klidně mohou začít až na řádku následujícím):

image_data = """

Na posledním řádku pak pouze řetězec uzavřeme:

"""

Výsledkem by měl být následující úryvek zdrojového kódu, který je bez problémů interpretovatelný Pythonem:

image_data = """
R0lGODlhFgAWAPZJAAAAAHgCAYcHBYgZGZUJA5cRCpsYEpkkFpkrJKQLBKgYBaQQELQCArEYGKQo
Fq4yFrUlBrAoE7g5F6g0J6Y2NqlMO6lJRrVLRLpSSLZaWbxpacg1B9U7BMwpKMw5ONQnJ9w4OMlH
FttDBtFOFudJBPNPAPdTAMFbTsZmV8J3d+FiYut9fauCebGHfqyLhK6cnLaMhL+Si7ynp8mIiMaW
j86Skt+KitGZmcSsq92trd6yrd+ysuuIh+6oqOS7reW3svKqqurDtM7OztjY2OTMyujT0/DExOjo
6Pj4+P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5
BAEAAEkALAAAAAAWABYAAAf+gEmCg4SFg0hIgz0rIB+Oj5AdjypANoI8Rjs5mzs7Pz2boZw9HoId
N6ipqquoOQ2CDDOyLy81sreztTM3C7CyOEdHMriyMsE4MzW9SQwpKURIR0gyzs7G0UQpM8sMGt5F
wUc43sbB2RozBoIJGe0aRMFFOMDmGu0pBevt+/BHQ/9DiOzTkIKAoAUUKFS4gAEDEYABUZyoMIFA
AYNJFhAQQEBBBAkuILaAoCDBxgICDm7sCIEFRCFCWJAkQMBAyiQGaCZQ0CLYEB8+YA4ZqaDmzZwd
Y4QLQoKEj39HYkBIcOAoTRpLSZQwUSLIkGA0FDgIIOgATR/RgojQypVEEGlMP8SSRXLgogIfQTZw
WNuUA4cgcQk4SInEQl0FECDoFbFWBIcNG0gqmDDXQgUHDyRICBFihOcRnDU/mICAbJIBAVKrXs16
taHXsAUFAgA7
"""

9. Použití ovládacího prvku typu Image a obrázku ze zdrojového kódu

Ve chvíli, kdy již máme zakódovaná data s obrázkem uložena do řetězce, je možné tato data využít pro vytvoření widgetu s obrázkem. Pouze namísto zavolání metody addImage:

app.addImage("image", "moare.png")

použijeme metodu pojmenovanou addImageData, a to následujícím způsobem:

app.addImageData("image", image_data)

Obrázek 8: Screenshot příkladu, v němž je použit widget typu Image a obrázek uložený přímo ve zdrojovém kódu.

Celý příklad, který po svém spuštění zobrazí na ploše okna ikonu, bude vypadat následovně:

#!/usr/bin/env python
 
from appJar import gui
import tkinter
 
 
image_data = """
R0lGODlhFgAWAPZJAAAAAHgCAYcHBYgZGZUJA5cRCpsYEpkkFpkrJKQLBKgYBaQQELQCArEYGKQo
Fq4yFrUlBrAoE7g5F6g0J6Y2NqlMO6lJRrVLRLpSSLZaWbxpacg1B9U7BMwpKMw5ONQnJ9w4OMlH
FttDBtFOFudJBPNPAPdTAMFbTsZmV8J3d+FiYut9fauCebGHfqyLhK6cnLaMhL+Si7ynp8mIiMaW
j86Skt+KitGZmcSsq92trd6yrd+ysuuIh+6oqOS7reW3svKqqurDtM7OztjY2OTMyujT0/DExOjo
6Pj4+P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5
BAEAAEkALAAAAAAWABYAAAf+gEmCg4SFg0hIgz0rIB+Oj5AdjypANoI8Rjs5mzs7Pz2boZw9HoId
N6ipqquoOQ2CDDOyLy81sreztTM3C7CyOEdHMriyMsE4MzW9SQwpKURIR0gyzs7G0UQpM8sMGt5F
wUc43sbB2RozBoIJGe0aRMFFOMDmGu0pBevt+/BHQ/9DiOzTkIKAoAUUKFS4gAEDEYABUZyoMIFA
AYNJFhAQQEBBBAkuILaAoCDBxgICDm7sCIEFRCFCWJAkQMBAyiQGaCZQ0CLYEB8+YA4ZqaDmzZwd
Y4QLQoKEj39HYkBIcOAoTRpLSZQwUSLIkGA0FDgIIOgATR/RgojQypVEEGlMP8SSRXLgogIfQTZw
WNuUA4cgcQk4SInEQl0FECDoFbFWBIcNG0gqmDDXQgUHDyRICBFihOcRnDU/mICAbJIBAVKrXs16
taHXsAUFAgA7
"""
 
app = gui()
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addLabel("label", "Image loaded from data (string)")
app.addImageData("image", image_data)
 
app.go()

10. Ovládací prvek zobrazující koláčový diagram

Další ovládací prvek, který v knihovně appJar najdeme, dokáže zobrazit koláčový diagram (graf) pro zvolená data. Možnosti koláčového diagramu jsou však omezeny, například nefunguje legenda (tu lze přidat do jiného widgetu), chybí popisky výsečí, není možné jednoduše modifikovat barvy jednotlivých výsečí a jelikož se pro zobrazení používá Canvas z knihovny Tkinter, není použit antialiasing, což je jasně patrné jak na obloucích, tak i na šikmých úsečkách.

Vstupní data pro koláčový graf jsou uložena ve slovníku, kde klíče tvoří popisky výsečí a hodnoty udávají relativní velikost výseče. Pro ukázku jsem použil počet křesel obsazených stranami v posledních volbách. Data jsou seřazena podle čísel stran, nikoli podle preferencí autora :-):

data = {
    "ODS": 25,
    "CSSD": 15,
    "STAN": 6,
    "KSCM": 15,
    "Pirati": 22,
    "TOP 09": 7,
    "ANO":  78,
    "KDU-CSL": 10,
    "SPD": 22}

Koláčový graf se do hlavního okna aplikace přidá velmi jednoduše:

app.addPieChart("piechart", data)

S tímto výsledkem:

Obrázek 9: Koláčový diagram (graf) zobrazený předchozím příkazem.

Podívejme se na úplný kód příkladu:

#!/usr/bin/env python
 
from appJar import gui
import tkinter
 
 
app = gui()
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
data = {
    "ODS": 25,
    "CSSD": 15,
    "STAN": 6,
    "KSCM": 15,
    "Pirati": 22,
    "TOP 09": 7,
    "ANO":  78,
    "KDU-CSL": 10,
    "SPD": 22}
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addPieChart("piechart", data)
 
app.go()

Obrázek 10: Bublinová nápověda zobrazí data u vybrané výseče.

Poznámka: pokud vám nevyhovují barvy jednotlivých výsečí (jsou docela nudné a navzájem podobné), lze je upravit buď přímo ve zdrojovém kódu appjar.py nebo si můžete ze třídy PieChart odvodit třídu vlastní:

class PieChart(Canvas):
    # constant for available colours
    COLOURS = [
        "#023fa5",
        "#7d87b9",
        "#bec1d4",
        "#d6bcc0",
        "#bb7784",
        "#8e063b",
        "#4a6fe3",
        "#8595e1",
        "#b5bbe3",
        "#e6afb9",
        "#e07b91",
        "#d33f6a",
        "#11c638",
        "#8dd593",
        "#c6dec7",
        "#ead3c6",
        "#f0b98d",
        "#ef9708",
        "#0fcfc0",
        "#9cded6",
        "#d5eae7",
        "#f3e1eb",
        "#f6c4e1",
        "#f79cd4"]

11. Zobrazení mřížky LED (emulace počítače Micro Bit)

O tom, že je knihovna appJar určena primárně pro výuku programování, svědčí i existence posledního ovládacího prvku. Jedná se o specializovaný widget, který dokáže zobrazit mřížku 5×5 čtverečků, které svou barvou napodobují mřížku LED z populárního jednodeskového mikropočítače Micro Bit, jenž se poměrně masivně začal používat i v tuzemsku (což je ostatně jen dobře, mj. i díky rozsáhlému ekosystému, který okolo tohoto zařízení vznikl).

Mřížka s dvaceti pěti LED se do okna aplikace vkládá naprosto stejným způsobem, jako jakýkoli jiný ovládací prvek, tj. metodou addJménoWidtgetu(„identifi­kátor_widgetu“):

app.addMicroBit("microbit")

Pokud budeme chtít rozsvítit či naopak zhasnout všechny LED, je možné použít metodu setMicroBitImage. Této metodě se kromě identifikátoru widgetu předává i řetězec obsahující intenzitu světla všech LED, přičemž intenzita může být v rozsahu od nuly do devíti. LED jsou rozděleny do řádků, které jsou od sebe odděleny dvojtečkou. To znamená, že pokud budeme chtít zobrazit velké písmeno „M“, vytvoříme si nejdříve (například na papír) bitmapu s tvarem písmena:

*   *
** **
* * *
*   *
*   *

Zvolíme si intenzitu svitu diod – 0 pro zhasnuté diody, 9 pro diody rozsvícené:

90009
99099
90909
90009
90009

Převedeme intenzity na řetězec:

90009:99099:90909:90009:90009

A zavoláme výše zmíněnou metodu setMicroBitImage:

app.setMicroBitImage("microbit", "90009:99099:90909:90009:90009")

Obrázek 11: Písmeno „M“ zobrazené v matici 5×5 „LED“.

Opět si ukažme příklad, v němž se tento neobvyklý ovládací prvek použije:

#!/usr/bin/env python
 
from appJar import gui
import tkinter
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addMicroBit("microbit")
app.setMicroBitImage("microbit", "90009:99099:90909:90009:90009")
app.go()

12. Programová změna světlosti jednotlivých LED

Světlost LED v matici je možné změnit metodou nazvanou setMicroBitPixel, které se předá čtveřice parametrů – identifikátor widgetu, x-ová souřadnice LED v matici (0–4), y-ová souřadnice LED v matici (taktéž 0–4) a světlost pixelu v rozmezí 0 až 9. Všechny celočíselné hodnoty jsou po zavolání metody zkontrolovány oproti uvedeným rozsahům:

app.setMicroBitPixel("microbit", x, y, brightness)

Pokud například budeme chtít zobrazit diagonální gradientní přechod od světlosti 1 do 9, lze to provést následovně:

for y in range(0, 5):
    for x in range(0, 5):
        brightness = x + y + 1
        app.setMicroBitPixel("microbit", x, y, brightness)

Pokud budete potřebovat všechny LED vypnout, zajistí to metoda:

app.clearMicroBit("microbit")

Obrázek 12: Gradientní přechod zobrazený LED v matici 5×5.

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

#!/usr/bin/env python
 
from appJar import gui
import tkinter
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
app.addMicroBit("microbit")
 
for y in range(0, 5):
    for x in range(0, 5):
        brightness = x + y + 1
        app.setMicroBitPixel("microbit", x, y, brightness)
 
app.go()

13. Použití prvku Canvas z Tkinteru

Ve chvíli, kdy je zapotřebí zobrazit složitější grafické prvky, než jsou nemodifikovatelné rastrové obrázky, koláčové diagramy nebo matice 5×5 „LED“, je nutné obejít základní možnosti knihovny appJar a namísto toho použít nám již známé prostředky nabízené knihovnou Tkinter, které jsme si představili v trojici článků [1] [2] a [3]. Využijeme přitom toho, že referenci na objekt představující hlavní okno aplikace lze získat velmi snadno:

app = gui()
root = app.topLevel

Kreslicí plátno (canvas) poté na hlavním oknu vytvoříme přesně stejným způsobem, jaký již známe z knihovny Tkinter:

canvas = tkinter.Canvas(root, width=šířka_plátna, height=výška_plátna)
canvas.pack()

Jediný problém spočívá v tom, že se plátno sice na hlavní okno umístí, ale knihovna appJar není o této operaci korektně informována. Výsledkem je, že plátno bude vždy zobrazené ve spodní části okna, což však pro mnoho aplikací nebude velkým problémem (pokud ano, je nutné pracovat s interním objektem all.ContainerStack, jehož vlastnosti se ovšem mohou v dalších verzích knihovny appJar lišit).

14. Ukázka použití Canvasu

Podívejme se nyní na způsob použití kreslicího plátna (canvasu) v praxi. Na hlavní okno vložíme kromě kreslicího plátna i další widget, konkrétně hlavní menu:

app = gui()
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
canvas = tkinter.Canvas(app.topLevel, width=256, height=256)
canvas.pack()

Na kreslicí plátno poté vykreslíme několik objektů:

canvas.create_oval(10, 10, 100, 100, fill="red", outline="blue", width=3)
canvas.create_line(0, 0, 255, 255, width=5)
canvas.create_line(0, 255, 255, 0, dash=123)
 
canvas.create_rectangle(70, 140, 230, 180, fill="white")
canvas.create_text(150, 160, text="Hello world!", fill="brown",
                   font="Helvetica 20")

Výsledek můžeme vidět na dalším obrázku:

Obrázek 13: Kreslicí plátno vložené do okna aplikace vytvořené přes appJar.

Celý zdrojový kód příkladu s kreslicím plátnem vloženým do aplikace vytvořené přes appJar vypadá takto:

#!/usr/bin/env python
 
from appJar import gui
import tkinter
 
 
app = gui()
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
canvas = tkinter.Canvas(app.topLevel, width=256, height=256)
canvas.pack()
 
canvas.create_oval(10, 10, 100, 100, fill="red", outline="blue", width=3)
canvas.create_line(0, 0, 255, 255, width=5)
canvas.create_line(0, 255, 255, 0, dash=123)
canvas.create_rectangle(70, 140, 230, 180, fill="white")
canvas.create_text(150, 160, text="Hello world!", fill="brown",
                   font="Helvetica 20")
 
app.go()

Poznámka: knihovna appJar pro pojmenování metod používá CamelCase zatímco knihovna Tkinter u svých identifikátorů používá oddělení slov podtržítkem, takže je výsledek dosti nekonzistentní. To je jedna z nevýhod, s nimiž se setkáme při kombinaci možností obou knihoven.

Kreslicí plátno si zachovává všechny vlastnosti, které již známe z knihovny Tkinter. Můžeme tak zajistit například změnu stylu vybrané entity po najetí myši, například:

canvas.create_rectangle(230, 110, 270, 190, fill=None, activeoutline='yellow',
                        width=5)
 
canvas.create_oval(320, 220, 380, 280, fill=None, activefill='#8080ff',
                   width=5)

Obrázek 14: Kreslicí plátno s několika aktivními prvky reagujícími na najetí myši.

V dalším příkladu je na plátno vloženo patnáct prvků, které reagují na najetí myši (onMouseOver):

#!/usr/bin/env python
 
from appJar import gui
import tkinter
 
 
WIDTH = 400
HEIGHT = 400
GRID_SIZE = 100
 
 
def onMenuItemSelect(menuItem):
    if menuItem == "Quit":
        app.stop()
 
 
def basicCanvas(root, width, height, grid_size):
    canvas = tkinter.Canvas(root, width=width, height=height,
                            background='#ccffcc')
    canvas.pack()
 
    drawGrid(canvas, width, height, grid_size)
    return canvas
 
 
def drawGrid(canvas, width, height, grid_size):
    for x in range(0, width, grid_size):
        canvas.create_line(x, 0, x, height, dash=7, fill="gray")
    for y in range(0, height, grid_size):
        canvas.create_line(0, y, width, y, dash=7, fill="gray")
 
 
app = gui()
 
app.setSticky("news")
 
fileMenu = ["Quit"]
app.addMenuList("File", fileMenu, onMenuItemSelect)
 
canvas = basicCanvas(app.topLevel, WIDTH, HEIGHT, GRID_SIZE)
 
canvas.create_rectangle(10, 30, 90, 70, fill='#ff8080', width=2,
                        activefill='white')
 
canvas.create_rectangle(110, 30, 190, 70, fill='#ff8080', width=2,
                        dash=(5, 5), activedash=1)
 
canvas.create_rectangle(30, 110, 70, 190, fill='#ff8080',
                        activeoutline='yellow')
 
canvas.create_rectangle(20, 220, 80, 280, fill='#ff8080',
                        activeoutline='yellow', activewidth='5')
 
canvas.create_oval(130, 110, 170, 190, fill='#8080ff', width=2,
                   activedash=(10, 10))
 
canvas.create_oval(120, 220, 180, 280, fill=None, activefill='#8080ff')
 
canvas.create_rectangle(210, 30, 290, 70, fill=None, width=2,
                        activefill='white')
 
canvas.create_rectangle(310, 30, 390, 70, fill=None, width=2, dash=(5, 5),
                        activedash=1)
 
canvas.create_rectangle(230, 110, 270, 190, fill=None, activeoutline='yellow',
                        width=5)
 
canvas.create_rectangle(220, 220, 280, 280, fill=None, activeoutline='yellow',
                        activewidth='5')
 
canvas.create_oval(330, 110, 370, 190, fill=None, width=2, activedash=(10, 10))
 
canvas.create_oval(320, 220, 380, 280, fill=None, activefill='#8080ff',
                   width=5)
 
canvas.create_line(10, 330, 90, 370, fill='#80ff80', width=2,
                   activefill='white')
 
canvas.create_line(110, 330, 190, 370, fill='#80ff80', width=20,
                   activefill='white')
 
canvas.create_line(210, 330, 290, 370, fill='#80ff80', width=20,
                   activefill='white', dash=10)
 
 
canvas.pack()
 
app.go()

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

Zdrojové kódy všech devíti 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:

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

Obrázek 15: Příště si mj. ukážeme použití modulu turtle společně s knihovnou appJar.

16. Odkazy na Internetu

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