Programovací jazyk TCL (10)

20. 9. 2005
Doba čtení: 12 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
V dnešní části seriálu si ukážeme, jakým způsobem je možné vytvořit jednoduchý vektorově orientovaný editor založený na widgetu canvas. Také si popíšeme práci s widgetem text, jež slouží pro vytváření a zobrazení víceřádkového textu s formátováním. Pomocí tohoto widgetu je také možné poměrně jednoduše vytvořit třeba WWW prohlížeč.

Obsah

1. Jednoduchý grafický editor založený na widgetu canvas
2. První verze grafického editoru
3. Další funkce grafického editoru
4. Optimalizace programu
5. Výsledná verze grafického editoru
6. Widget text
7. Základy práce s widgetem text
8. Obsah dalšího pokračování tohoto seriálu

1. Jednoduchý grafický editor založený na widgetu canvas

předchozím pokračování tohoto seriálu jsme si ukázali způsob práce s widgetem canvas. Dnes si řekneme, jakým způsobem je možné tento widget použít při programování velmi jednoduchého grafického editoru, který umožňuje práci s vektorově orientovanými obrázky, tj. s obrázky, které se skládají z objektů popsaných pomocí jednoduchých rovnic (bude se pro jednoduchost jednat pouze o úsečky, kružnice, elipsy, čtverce a obdélníky). Nejprve si řekněme, jakým způsobem by měl editor pracovat. Budeme přitom vycházet z již připravených schopností widgetu canvas (dále jen plátna) uchovávat vlastnosti objektů, které jsou na něj vkládány. To znamená, že se jednotlivé objekty budou vkládat příkazem jméno_plátna create typ_objektu. O případné zrušení objektů se postará příkaz jméno_plátna delete, kterému se předá název objektu (ten lze, jak uvidíme dále, získat ze souřadnic kurzoru myši). Při vkládání objektů na plátno bude nutné specifikovat jejich souřadnice, proto také musíme umět pracovat s kurzorem myši.

2. První verze grafického editoru

První verze grafického editoru je velmi jednoduchá. Nejprve je vytvořeno plátno, které je vloženo do hlavního (a současně i jediného) okna aplikace. Posléze jsou vytvořeny dvě procedury, které jsou navázány na pravé a levé tlačítko myši. Procedura nazvaná start je zavolána ve chvíli, kdy uživatel stiskne nad plátnem levé tlačítko myši, procedura stop je naopak zavolána při puštění tohoto tlačítka. Při stisknutí tlačítka se pouze zapamatují souřadnice kurzoru do globálních proměnných x1 a y1, při puštění se potom objekt vykreslí (během stisknutého tlačítka myši není vykreslovaný objekt vidět, i když i tuto funkcionalitu je možné zařídit). V první verzi grafického editoru budeme pracovat pouze s úsečkami, proto je celý program velmi jednoduchý, jak je to ostatně patrné z následujícího výpisu:

#!/usr/bin/wish

# Jednoduchý grafický editor založený na widgetu canvas
# První verze

# globální proměnné
set x1 0
set y1 0

# kontejner pro vkládání dalších widgetů
frame .editor

# plátno, na které se bude kreslit
canvas .editor.platno -width 512 -height 384

# vložení všech widgetů do okna aplikace
pack .editor
pack .editor.platno

# navázání událostí od myši
bind .editor.platno <ButtonPress-1>   { start %x %y }
bind .editor.platno <ButtonRelease-1> { stop %x %y }

# procedura, která je zavolána v případě,
# že uživatel stiskne levé tlačítko myši
proc start { x y } {
    # zapamatování polohy kurzoru myši
    global x1
    global y1
    set x1 $x
    set y1 $y
}

# procedura, která je zavolána v případě,
# že uživatel pustí levé tlačítko myši
proc stop { x y } {
    global x1
    global y1

    .editor.platno create line $x1 $y1 $x $y
} 
TCL 10 - 1

Obrázek 1: screenshot první verze grafického editoru

3. Další funkce grafického editoru

Výše uvedený program je samozřejmě možné rozšiřovat o další funkce. My si ukážeme dvě rozšíření. První rozšíření spočívá v tom, že bude možné vybrat několik grafických objektů (entit), které bude možné vykreslit. Druhé rozšíření zajistí vymazání libovolné entity – implementace vymazání je provedena tak, že se vymaže entita, která se nachází nejblíže ke kurzoru myši. Vzhledem k tomu, že je zapotřebí vybírat jednotlivé operace, je na hlavní okno programu přidán (kromě plátna) ještě kontejner frame a na tento kontejner jednotlivá tlačítka. Kromě toho jsou rozšířeny i procedury start a stop tak, aby bylo možné vykreslit všechny podporované entity (vybraný režim činnosti je uschován v globální proměnné rezim). Druhá, prozatím neoptimalizovaná verze grafického editoru vypadá následovně:

#!/usr/bin/wish

# Jednoduchý grafický editor založený na widgetu canvas
# Druhá verze

# globální proměnné
set rezim usecka
set x1 0
set y1 0

# kontejner pro vkládání dalších widgetů
frame .editor
frame .editor.tlacitka

# plátno, na které se bude kreslit
canvas .editor.platno -width 512 -height 384

# vytvoření tlačítek, kterými se mění režim editoru
button .editor.tlacitka.usecka   -text Úsečka   -command { nastavRezim usecka }
button .editor.tlacitka.kruznice -text Kružnice -command { nastavRezim kruznice }
button .editor.tlacitka.elipsa   -text Elipsa   -command { nastavRezim elipsa }
button .editor.tlacitka.ctverec  -text Čtverec  -command { nastavRezim ctverec }
button .editor.tlacitka.obdelnik -text Obdélník -command { nastavRezim obdelnik }
button .editor.tlacitka.vymazat  -text "Vymazat entitu"  -command { nastavRezim mazani }

# vložení všech widgetů do okna aplikace
pack .editor
pack .editor.platno
pack .editor.tlacitka -side top -fill both -expand true

# vložení všech tlačítek na kontejner
pack .editor.tlacitka.usecka   -side left -fill both -expand true
pack .editor.tlacitka.kruznice -side left -fill both -expand true
pack .editor.tlacitka.elipsa   -side left -fill both -expand true
pack .editor.tlacitka.ctverec  -side left -fill both -expand true
pack .editor.tlacitka.obdelnik -side left -fill both -expand true
pack .editor.tlacitka.vymazat  -side left -fill both -expand true

# navázání událostí od myši
bind .editor.platno <ButtonPress-1>   { start %x %y }
bind .editor.platno <ButtonRelease-1> { stop %x %y }

# procedura pro nastavení režimu editoru
proc nastavRezim {rez} {
    global rezim
    set rezim $rez
}

# procedura, která je zavolána v případě,
# že uživatel stiskne levé tlačítko myši
proc start { x y } {
    global rezim
    # zapamatování polohy kurzoru myši
    global x1
    global y1
    set x1 $x
    set y1 $y
    # v režimu mazání se vymaže nejbližší entita
    # ke kurzoru myši
    if { [string compare $rezim mazani] == 0 } {
        .editor.platno delete [.editor.platno find closest $x $y]
    }
}

# procedura, která je zavolána v případě,
# že uživatel pustí levé tlačítko myši
proc stop { x y } {
    global rezim
    global x1
    global y1

    # rozdíly mezi starou a novou polohou
    # kurzoru myši
    set dx [expr abs($x1-$x)]
    set dy [expr abs($y1-$y)]

    # vykreslení jednotlivých entit
    if { [string compare $rezim usecka] == 0 } {
        .editor.platno create line $x1 $y1 $x $y
    }
    if { [string compare $rezim kruznice] == 0 } {
        if { $dx>$dy} {
            .editor.platno create oval $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
        } else {
            .editor.platno create oval $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
        }
    }
    if { [string compare $rezim elipsa] == 0 } {
        .editor.platno create oval $x1 $y1 $x $y
    }
    if { [string compare $rezim ctverec] == 0 } {
        if { $dx>$dy} {
            .editor.platno create rect $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
        } else {
            .editor.platno create rect $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
        }
    }
    if { [string compare $rezim obdelnik] == 0 } {
        .editor.platno create rect $x1 $y1 $x $y
    }
} 
TCL 10 - 2

Obrázek 2: screenshot druhé verze grafického editoru

4. Optimalizace programu

Výše uvedený příklad je sice napsán korektně, ale zcela jistě není optimální. Prvním krokem k optimalizaci je použití příkazu switch namísto několika složitě konstruovaných příkazů if v proceduře stop. Původní procedura vypadala následovně:

# procedura, která je zavolána v případě,
# že uživatel pustí levé tlačítko myši
proc stop { x y } {
    global rezim
    global x1
    global y1

    # rozdíly mezi starou a novou polohou
    # kurzoru myši
    set dx [expr abs($x1-$x)]
    set dy [expr abs($y1-$y)]

    # vykreslení jednotlivých entit
    if { [string compare $rezim usecka] == 0 } {
        .editor.platno create line $x1 $y1 $x $y
    }
    if { [string compare $rezim kruznice] == 0 } {
        if { $dx>$dy} {
            .editor.platno create oval $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
        } else {
            .editor.platno create oval $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
        }
    }
    if { [string compare $rezim elipsa] == 0 } {
        .editor.platno create oval $x1 $y1 $x $y
    }
    if { [string compare $rezim ctverec] == 0 } {
        if { $dx>$dy} {
            .editor.platno create rect $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
        } else {
            .editor.platno create rect $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
        }
    }
    if { [string compare $rezim obdelnik] == 0 } {
        .editor.platno create rect $x1 $y1 $x $y
    }
} 

Nová procedura pro porovnání řetězců využívá příkaz switch, který je v tomto případě mnohem jednodušší:

# procedura, která je zavolána v případě,
# že uživatel pustí levé tlačítko myši
proc stop { x y } {
    global rezim
    global x1
    global y1

    # rozdíly mezi starou a novou polohou
    # kurzoru myši
    set dx [expr abs($x1-$x)]
    set dy [expr abs($y1-$y)]

    # vykreslení jednotlivých entit
    switch $rezim {
       usecka   {.editor.platno create line $x1 $y1 $x $y}
       kruznice {
            if { $dx>$dy} {
                .editor.platno create oval $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
            } else {
                .editor.platno create oval $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
            }
       }
       elipsa   {.editor.platno create oval $x1 $y1 $x $y}
       ctverec  {
            if { $dx>$dy} {
                .editor.platno create rect $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
            } else {
                .editor.platno create rect $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
            }
       }
       obdelnik {.editor.platno create rect $x1 $y1 $x $y}
       default  {}
    }
} 

Dalším, a pro pochopení možná obtížnějším krokem je náhrada otrocky zadávaných příkazů pro vytváření a umístění tlačítek iterační smyčkou. Zde můžeme s výhodou použít smyčku foreach, která nám umožňuje procházet jedním nebo více seznamy. V každé iteraci se do zadaných proměnných vloží jeden prvek ze seznamu. Původní část programu, která vytvářela tlačítka a umisťovala je na kontejner frame, vypadala následovně:

# vytvoření tlačítek, kterými se mění režim editoru
button .editor.tlacitka.usecka   -text Úsečka   -command { nastavRezim usecka }
button .editor.tlacitka.kruznice -text Kružnice -command { nastavRezim kruznice }
button .editor.tlacitka.elipsa   -text Elipsa   -command { nastavRezim elipsa }
button .editor.tlacitka.ctverec  -text Čtverec  -command { nastavRezim ctverec }
button .editor.tlacitka.obdelnik -text Obdélník -command { nastavRezim obdelnik }
button .editor.tlacitka.vymazat  -text "Vymazat entitu" -command { nastavRezim mazani }

# vložení všech tlačítek na kontejner
pack .editor.tlacitka.usecka   -side left -fill both -expand true
pack .editor.tlacitka.kruznice -side left -fill both -expand true
pack .editor.tlacitka.elipsa   -side left -fill both -expand true
pack .editor.tlacitka.ctverec  -side left -fill both -expand true
pack .editor.tlacitka.obdelnik -side left -fill both -expand true
pack .editor.tlacitka.vymazat  -side left -fill both -expand true 

Výše zmíněný postup je nejenom zdlouhavý, ale i nevýhodný v tom, že pro každé nově vložené tlačítko je nutné změnit kód na více místech. S využitím smyček foreach se programové názvy tlačítek i jejich popisky uloží do dvou seznamů, a program tak lze podstatně zkrátit a zestručnit. Na dále uvedeném fragmentu kódu si všimněte způsobu explicitního spojování řetězců, například ve výrazu .editor.tlacit­ka.$entita:

# seznam tlačítek, kterými se editor bude ovládat
set tlacitka {usecka kruznice elipsa ctverec obdelnik vymazat konec}
set nazvy {Úsečka Kružnice Elipsa Čtverec Obdélník "Vymazat entitu" Konec}

# vytvoření tlačítek, kterými se mění režim editoru
foreach entita $tlacitka nazev $nazvy {
    button .editor.tlacitka.$entita -text $nazev -command " nastavRezim $entita "
}

# vložení všech tlačítek na kontejner
foreach entita $tlacitka {
    pack .editor.tlacitka.$entita -side left -fill both -expand true
} 

5. Výsledná verze grafického editoru

#!/usr/bin/wish

# Jednoduchý grafický editor založený na widgetu canvas
# výsledná verze editoru

# globální proměnné
set rezim usecka
set x1 0
set y1 0

# seznam tlačítek, kterými se editor bude ovládat
set tlacitka {usecka kruznice elipsa ctverec obdelnik vymazat konec}
set nazvy {Úsečka Kružnice Elipsa Čtverec Obdélník "Vymazat entitu" Konec}

# kontejner pro vkládání dalších widgetů
frame .editor
frame .editor.tlacitka

# plátno, na které se bude kreslit
canvas .editor.platno -width 512 -height 384

# vytvoření tlačítek, kterými se mění režim editoru
foreach entita $tlacitka nazev $nazvy {
    button .editor.tlacitka.$entita -text $nazev -command " nastavRezim $entita "
}

# vložení všech widgetů do okna aplikace
pack .editor
pack .editor.platno
pack .editor.tlacitka -side top -fill both -expand true

# vložení všech tlačítek na kontejner
foreach entita $tlacitka {
    pack .editor.tlacitka.$entita -side left -fill both -expand true
}

# navázání událostí od myši
bind .editor.platno <ButtonPress-1>   { start %x %y }
bind .editor.platno <ButtonRelease-1> { stop %x %y }

# procedura pro nastavení režimu editoru
proc nastavRezim {rez} {
    global rezim
    set rezim $rez
    if { [string compare $rezim konec] == 0 } {
        exit
    }
}

# procedura, která je zavolána v případě,
# že uživatel stiskne levé tlačítko myši
proc start { x y } {
    global rezim
    # zapamatování polohy kurzoru myši
    global x1
    global y1
    set x1 $x
    set y1 $y
    # v režimu mazání se vymaže nejbližší entita
    # ke kurzoru myši
    if { [string compare $rezim vymazat] == 0 } {
        .editor.platno delete [.editor.platno find closest $x $y]
    }
}

# procedura, která je zavolána v případě,
# že uživatel pustí levé tlačítko myši
proc stop { x y } {
    global rezim
    global x1
    global y1

    # rozdíly mezi starou a novou polohou
    # kurzoru myši
    set dx [expr abs($x1-$x)]
    set dy [expr abs($y1-$y)]

    # vykreslení jednotlivých entit
    switch $rezim {
       usecka   {.editor.platno create line $x1 $y1 $x $y}
       kruznice {
            if { $dx>$dy} {
                .editor.platno create oval $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
            } else {
                .editor.platno create oval $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
            }
       }
       elipsa   {.editor.platno create oval $x1 $y1 $x $y}
       ctverec  {
            if { $dx>$dy} {
                .editor.platno create rect $x1 $y1 [expr $x1+$dx] [expr $y1+$dx]
            } else {
                .editor.platno create rect $x1 $y1 [expr $x1+$dy] [expr $y1+$dy]
            }
       }
       obdelnik {.editor.platno create rect $x1 $y1 $x $y}
       default  {}
    }
} 
TCL 10 - 3

Obrázek 3: screenshot výsledné verze grafického editoru

6. Widget text

Widget text patří mezi nejsložitější a současně funkčně nejmocnější widgety, které jsou toolkitem Tk programátorům nabízeny. Pokud použijeme nejjednodušší podobu tohoto widgetu, je s ním možné zobrazovat „pouze“ víceřádkový text – v tomto případě se jedná o přímočaré rozšíření již dříve popsaného widgetu sloužícího pro vstup jednoho řádku textu. Možnosti widgetu text jsou však mnohem větší – vložený text je možné sdružovat do bloků a každému bloku samostatně nastavit atributy, zejména font (písmo), barvu textu, velikost textu, styl apod. Kromě textu lze do tohoto widgetu vkládat i další objekty včetně obrázků (tj. bitmap i pixmap). Jak text, tak i obrázky mohou být hypertextovými odkazy, proto tvoří tento widget srdce mnoha hypertextových prohlížečů postavených na Tcl/Tk. Podobnou funkcionalitu (i když zdaleka ne tak pružnou) lze najít ve WinAPI u objektu určeného pro zpracování RTF – Rich Text Format. Příkladem aplikace, která tento objekt „obaluje“, je známý WordPad.

7. Základy práce s widgetem text

Widget text se v nejjednodušší podobě dá vytvořit příkazem:

text jméno_widgetu_s_cestou 

Do widgetu lze vkládat text pomocí příkazu:

jméno_widgetu_s_cestou insert index text 

Kde se za index dosadí místo, do kterého se text bude vkládat, a za text se dosadí vlastní text. Místo číselného indexu je možné použít i slovo end, které značí vkládání na konec již vloženého textu. Příklad použití těchto příkazů je velmi jednoduchý (všimněte si, že se znak pro konec řádku musí vkládat manuálně, jinak by byly jednotlivé texty spojeny):

#!/usr/bin/wish

text .text
pack .text
.text insert end "Hello\n"
.text insert end "world!\n" 
TCL 10 - 4

Obrázek 4: screenshot příkladu používajícího widget text

Podrobněji se budeme prací s tímto widgetem zabývat až v další části seriálu.

linux_sprava_tip

8. Obsah dalšího pokračování tohoto seriálu

V další části tohoto seriálu dokončíme popis widgetu text. Ukážeme si způsob formátování textu, vkládání obrázku do textu a navázání událostí, které mohou při práci s textem vzniknout. Dále si ukážeme další velmi důležitou součást velkého množství aplikací – systému menu.

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.