Tk

« Tk: applicazioni di esempio: il gioco del 15Appunti di Tcl/TkProgrammazione Object-Oriented »

Descrizione di un demo Tk: ixset

Il programma ixset fa parte dei demo distribuiti con Tk. Il suo scopo è di fornire una interfaccia grafica semplice da usare per il programma xset(1) che server per impostare varie opzioni di un display X.

ixset
Fig. 1. Una schermata di ixset.

Analizziamo riga per riga questo programma a scopo didattico. Cominciamo dalle righe iniziali:

#!/bin/sh
# the next line restarts using wish \
exec wish8.4 "$0" ${1+"$@"}

Il parametro speciale $0 viene espanso col nome e percorso dello script. In caso ci fossero degli spazi nel percorso la quotatura assicura che questo sia un parametro unico (il primo) per wish8.4. $@ viene sostituito con tutti i parametri posizionali a partire dal primo. Quando l'espansione di $@ avviene all'interno degli apici doppi, la shell sh(1) fa si che ogni parametro posizionale venga espanso come un argomento separato. Il ${1+"$@"} se $1 non è settato oppure è null, viene sostituito con niente, altrimenti gli viene sostituito il risultato di "$@". La forma, limitatamente a questo caso particolare in cui compare dopo il + un "$@", è pleonastica, ovvero sarebbe bastato scrivere "$@" invece di ${1+"$@"}. Infatti quando non ci sono parametri posizionali oltre $0, la shell fa si che "$@" non generi alcun argomento, nemmeno quello vuoto, nonostante la presenza degli apici doppi.

Il programma inizialmente chiama la procedura readsettings. Questa crea ed inizializza delle variabili globali che rappresentano i valori di default stabiliti da ixset per tutte le impostazioni che esso permette di fare, come evidente dalla Figura 1:


    global kbdrep ;     set kbdrep      "on"
    global kbdcli ;     set kbdcli      0
    global bellvol ;    set bellvol     100
    global bellpit ;    set bellpit     440
    global belldur ;    set belldur     100
    global mouseacc ;   set mouseacc    "3/1"
    global mousethr ;   set mousethr    4
    global screenbla ;  set screenbla   "blank"
    global screentim ;  set screentim   600
    global screencyc ;  set screencyc   600

Sempre all'interno di readsettings viene eseguito il comando xset q che riporta le informazioni di stato in un formato simile a questo:

$ xset q
Keyboard Control:
  auto repeat:  on    key click percent:  0    LED mask:  00000000
  auto repeat delay:  660    repeat rate:  25
  auto repeating keys:  00ffffffdffffbbf
                        fadfffffffdfe5ff
                        ffffffffffffffff
                        ffffffffffffffff
  bell percent:  50    bell pitch:  400    bell duration:  100
Pointer Control:
  acceleration:  2/1    threshold:  4
Screen Saver:
  prefer blanking:  yes    allow exposures:  yes
  timeout:  600    cycle:  600
Colors:
  default colormap:  0x20    BlackPixel:  0    WhitePixel:  16777215
Font Path:
  /usr/X11R6/lib/X11/fonts/misc/,/usr/X11R6/lib/X11/fonts/Speedo/,
/usr/X11R6/lib/X11/fonts/Type1/,/usr/X11R6/lib/X11/fonts/75dpi/,
/usr/X11R6/lib/X11/fonts/100dpi/,/usr/X11R6/lib/X11/fonts/TTF/
Bug Mode: compatibility mode is disabled
DPMS (Energy Star):
  Standby: 1200    Suspend: 1800    Off: 2400
  DPMS is Enabled
  Monitor is On
Font cache:
  hi-mark (KB): 5120  low-mark (KB): 3840  balance (%): 70
File paths:
  Config file:  /etc/X11/XF86Config
  Modules path: /usr/X11R6/lib/modules
  Log file:     /var/log/XFree86.0.log
 

Il codice seguente legge una linea per volta tramite gets(n) dall'output di xset q finché l'output non termina (gets ritorna il numero di caratteri letti o -1 quando incontra la fine del file); viene poi estratta la prima parola di ciascuna riga per capire che tipo di settings riguarda quella linea; le opportune parole che indicano i valori dei settings sulla stessa linea vengono così estratte.


    set xfd [open "|xset q" r]
    while {[gets $xfd line] > -1} {
        set kw [lindex $line 0]

        case $kw in {
            {auto}
                {
                    set rpt [lindex $line 1]
                    if {[expr "{$rpt} == {repeat:}"]} then {
                        set kbdrep [lindex $line 2]
                        set kbdcli [lindex $line 6]
                    }
                }
            {bell}
                {
                    set bellvol [lindex $line 2]
                    set bellpit [lindex $line 5]
                    set belldur [lindex $line 8]
                }
            {acceleration:}
                {
                    set mouseacc [lindex $line 1]
                    set mousethr [lindex $line 3]
                }
            {prefer}
                {
                    set bla [lindex $line 2]
                    set screenbla [expr "{$bla} == {yes} ? {blank} : {noblank}"]
                }
            {timeout:}
                {
                    set screentim [lindex $line 1]
                    set screencyc [lindex $line 3]
                }
        }
    }
    close $xfd

Alla fine tutte le variabili globali vengono aggiornate con i valori correnti delle impostazioni. Questa fase è delicata e dipende da come xset q organizza il suo output; se il formato dovesse variare, ixset non opererebbe più correttamente; troviamo anche del codice di debugging che è stato commentato:


    # puts stdout [format "Key REPEAT = %s\n" $kbdrep]
    # puts stdout [format "Key CLICK  = %s\n" $kbdcli]
    # puts stdout [format "Bell VOLUME = %s\n" $bellvol]
    # puts stdout [format "Bell PITCH = %s\n" $bellpit]
    # puts stdout [format "Bell DURATION = %s\n" $belldur]
    # puts stdout [format "Mouse ACCELERATION = %s\n" $mouseacc]
    # puts stdout [format "Mouse THRESHOLD = %s\n" $mousethr]
    # puts stdout [format "Screen BLANCK = %s\n" $screenbla]
    # puts stdout [format "Screen TIMEOUT = %s\n" $screentim]
    # puts stdout [format "Screen CYCLE = %s\n" $screencyc]

Viene poi chiamata la proc createwindows. Questo primo blocco di istruzioni crea la fila di tasti superiore:


    frame .buttons
    button .buttons.ok     -default active -command ok     -text "Ok"    
    button .buttons.apply  -default normal -command apply  -text "Apply" \
            -state disabled
    button .buttons.cancel -default normal -command cancel -text "Cancel" \
            -state disabled
    button .buttons.quit   -default normal -command quit   -text "Quit"  

    pack .buttons.ok .buttons.apply .buttons.cancel .buttons.quit \
            -side left -expand yes -pady 5

Un tasto (button) può essere in tre stati, come stabilito inizialmente dall'opzione -state: normal, active e disabled. Nello stato normale, che è quello di default, il tasto utilizza i colori stabiliti dalle opzioni -foreground e -background. Nello stato attivo, che tipicamente viene assunto dal tasto quando il puntatore del mouse vi si trova sopra, i colori sono stabiliti invece dalle opzioni -activeforeground e -activebackground. Nello stato disabilitato il tasto invece non può essere utilizzato e i binding di default ad esso associati non verranno attivati (i click del mouse sul tasto verranno ignorati) e i colori che verranno utilizzati per disegnarlo sono stabiliti dalle opzioni -disabledforeground e -background.

L'opzione -default pure può assumere tre valori, normal, active e disabled, ma indica invece quale tipo di contorno utilizzare. L'effetto è dipendende dalla piattaforma utilizzata. In generale si può dire che active produce un tasto con l'aspetto di tasto "di default" (come il tasto Ok nella Figura 1). Il valore normal produce invece un tasto che appare come tasto di non-default (ma viene comunque lasciato spazio sufficiente per aggiungere il contorno che è caratteristico di un tasto di default, in modo che i tasti normali e attivi abbiano la stessa dimensione a parità di contenuto). Il valore disabilitato indica invece che si vuole un tasto che appaia sempre come un tasto di non-default, ma senza lasciare lo spazio che serve per disegnare il contorno di un tasto di default. Uno stesso bottone nello stile disabilitato potrebbe quindi avere una dimensione inferiore rispetto allo stile attivo.

Il valore di default dell'opzione -default - il gioco di parole è inevitabile qui - è disabled. Per ottenere un buon effetto di allineamento quindi si è utilizzato lo stile normal per tutti e tre i tasti di tipo non-default.

Per le opzioni di packing, notate come l'opzione -expand yes abbia in pratica l'effetto di distribuire orizzontalmente lo spazio tra tutti i tasti, qualsiasi sia la dimensione della finestra, mentre -pady 5 richiede un po' di spazio verticale in più da entrambi i lati per evitare che la fila di tasti sembri troppo attaccata alla barra del titolo della finestra oppure al labelframe che segue sotto.

Il codice seguente definisce i binding per questa fila di tasti:


    bind . <Return> {.buttons.ok   flash; .buttons.ok   invoke}
    bind . <Escape> {.buttons.quit flash; .buttons.quit invoke}
    bind . <1> {
        if {![string match .buttons* %W]} {
            .buttons.apply  configure -state normal
            .buttons.cancel configure -state normal
        }
    }
    bind . <Key> {
        if {![string match .buttons* %W]} {
            switch -glob %K {
                Return - Escape - Tab - *Shift* {}
                default {
                    .buttons.apply  configure -state normal
                    .buttons.cancel configure -state normal
                }
            }
        }
    }

Se si preme il tasto Return, il tasto Ok verrà evidenziato tramite un rapido flashing e quindi verrà invocato il comando Tcl specificato per il tasto tramite l'opzione -command come se si fosse fatto click sul tasto stesso. Questo li conferisce il comportamento di tasto di default, mentre l'aspetto di tasto di default glielo conferiva l'opzione -default active (notate come spesso la definizione di aspetti e comportamenti va fatta separatamente in Tk e questo consente la massima flessibilità al programmatore).

Il secondo binding stabilisce che premere Esc nella finestra dell'applicazione equivale, a parte l'effetto flash, a fare click sul tasto Quit.

Notate che inizialmente i tasti Apply e Cancel sono disabilitati. Occorrerà abilitarli allorquando qualche controllo viene cambiato. Una soluzione più semplice ma non del tutto precisa, che poi è quella che è stata adottata, consiste nell'abilitarli nel momento si cui si fa click tramite il mouse oppure la tastiera su qualcuno dei controlli della finestra di dialogo.

Occorre però gestire delle eccezioni: se l'evento di click del mouse o della tastiera avviene sul frame .button o in qualsiasi widget figlio di questo, deve essere ignorato. Anche se viene premuto un tasto come Return, Esc, Tab o qualche tasto che nel suo nome ha la parola Shift, l'evento deve essere ignorato.

Segue poi la sezione che crea i controlli relativi al campanello del terminale grafico:


    #
    # Bell settings
    #

    labelframe .bell -text "Bell Settings" -padx 1.5m -pady 1.5m
    scale .bell.vol \
            -from 0 -to 100 -length 200 -tickinterval 20 \
            -label "Volume (%)" -orient horizontal

    frame .bell.val
    labelentry .bell.val.pit "Pitch (Hz)"    6 {25 20000}
    labelentry .bell.val.dur "Duration (ms)" 6 {1 10000}
    pack .bell.val.pit -side left -padx 5
    pack .bell.val.dur -side right -padx 5
    pack .bell.vol .bell.val -side top -expand yes

Un labelframe(n) è come un frame(n), ma in più permette di visualizzare una etichetta (o anche qualche altro tipo di widget come un radiobutton o un checkbutton) lungo il bordo, che si imposta con l'opzione -text. Inoltre mentre per default un frame ha -relief flat e -borderwidth 0, un labelframe ha -relief groove e -borderwidth 2.

Tramite le opzioni -padx e -pady, si aggiunge un millimetro e mezzo di spazio all'interno del labelframe. All'interno del labelframe .bell vengono pacchettati verticalmente (uno sopra l'altro) il controllo di scorrimento .bell.vol e un frame invisibile .bell.val (tenete presente la Figura 1).

La procedura labelentry, quando chiamata con 3 parametri, crea un nuovo frame di nome path, contenenti appaiati una etichetta con testo text e una casella di input di dimensione iniziale adatta a contenere length caratteri; qualora venga aggiunto un ulteriore parametro range, invece di una entry viene creata una spinbox e il parametro range deve essere una lista di due elementi che indicano l'intervallo di valori del controllo con scorrimento:


proc labelentry {path text length {range {}}} {
    frame $path
    label $path.label -text $text
    if {[llength $range]} {
        spinbox $path.entry -width $length -relief sunken \
                -from [lindex $range 0] -to [lindex $range 1]
    } else {
        entry $path.entry -width $length -relief sunken
    }
    pack $path.label -side left
    pack $path.entry -side right -expand y -fill x
}

Questa procedura è stata introdotta per semplificare il codice che crea e pacchetta i widget e infatti verrà riutilizzata parecchie volte anche in seguito dal codice che definisce i tre frame successivi, sempre per creare caselle di testo semplici o con scorrimento, precedute da una etichetta. Questo è un uso molto comune delle procedure, che consente di scrivere programmi più brevi e mantenibili: allorquando un programma presenta delle sezioni di codice molto simili e ripetute, conviene incapsularle in una procedura con opportuni parametri. In questo modo si semplifica anche la manutenzione, in quanto eventuali modifiche al codice che implementa le funzionalità offerte dalla procedura vanno fatte solo al corpo della procedura stessa, anziché in modo sparso in tutto il programma.


    #
    # Keyboard settings
    #

    labelframe .kbd -text "Keyboard Repeat Settings" -padx 1.5m -pady 1.5m

    frame .kbd.val
    checkbutton .kbd.val.onoff \
            -text "On" \
            -onvalue "on" -offvalue "off" -variable kbdrep \
            -relief flat
    scale .kbd.val.cli \
            -from 0 -to 100 -length 200 -tickinterval 20 \
            -label "Click Volume (%)" -orient horizontal
    pack .kbd.val.onoff -side left -fill x -expand yes -padx {0 1m}
    pack .kbd.val.cli -side left -expand yes -fill x -padx {1m 0}

    pack .kbd.val -side top -expand yes -pady 2 -fill x

    #
    # Mouse settings
    #

    labelframe .mouse -text "Mouse Settings" -padx 1.5m -pady 1.5m

    frame .mouse.hor
    labelentry .mouse.hor.acc "Acceleration" 5
    labelentry .mouse.hor.thr "Threshold (pixels)" 3 {1 2000}

    pack .mouse.hor.acc -side left -padx {0 1m}
    pack .mouse.hor.thr -side right -padx {1m 0}

    pack .mouse.hor -side top -expand yes

    #
    # Screen Saver settings
    #

    labelframe .screen -text "Screen-saver Settings" -padx 1.5m -pady 1.5m

    radiobutton .screen.blank \
            -text "Blank" -relief flat \
            -value "blank" -variable screenbla -anchor w
    radiobutton .screen.pat \
            -text "Pattern" -relief flat \
            -value "noblank" -variable screenbla -anchor w
    labelentry .screen.tim "Timeout (s)" 5 {1 100000}
    labelentry .screen.cyc "Cycle (s)" 5 {1 100000}

    grid .screen.blank .screen.tim -sticky e
    grid .screen.pat   .screen.cyc -sticky e
    grid configure .screen.blank .screen.pat -sticky ew

Alcune osservazioni su questo codice: da notare che a ciascun radiobutton o checkbutton è associata una variabile globale, il cui nome è stabilito tramite l'opzione -variable; in mancanza di tale opzione il nome per default vale selectedButton per un radiobutton, oppure coincide con l'ultimo componenente del path name del widget nel caso di un checkbutton.

A questa variabile, per un radiobutton, viene assegnato il valore stabilito tramite l'opzione -value (per default la stringa vuota) ogni volta che il tasto è selezionato. Viceversa, assegnando il valore stabilito da -value alla variabile oppure un valore diverso si produce un cambiamento di stato del tasto collegato (rispettivamente da deselezionato a selezionato o al contrario).

Per creare quindi dei gruppi di radiobutton che permettano di selezionare tra una serie di opzioni una sola voce, occorre che questi siano associati alla stessa variabile globale (stesso valore dell'opzione -variable), ma impostino differenti valori quando selezionati (diversi valori dell'opzioni -value). Il valore della variabile indicherà infatti il radiobutton che è stato selezionato.

Come esempio, confrontate questi due spezzoni di codice HTML e Tcl/Tk che entrambi permettono di selezionare tra due opzioni Blank e Pattern:

<form>
<input type="radio" name="screenbla" value="blank" checked> Blank
<input type="radio" name="screenbla" value="noblank"> Pattern
</form>
set screenbla blank
radiobutton .blank -text Blank -variable screenbla -value blank
radiobutton .pat -text Pattern -variable screenbla -value noblank
pack .blank .pat -side left

Nel caso invece di un checkbutton, i valori che il tasto assegna alla variabile collegata sono due: uno viene assegnato quando il tasto è selezionato, l'altro quando è deselezionato e si stabiliscono rispettivamente tramite le opzioni -onvalue e -offvalue (i valori di default di queste opzioni sono rispettivamente 1 e 0). Un checkbutton monìtora la variabile associata e si seleziona o deseleziona automaticamente allorquando tale variabile assume il valore stabilito da -onvalue o un valore diverso (rispettivamente). Tipicamente gruppi di checkbutton hanno gli stessi valori per le opzioni -onvalue e -offvalue e questi sono valori booleani oppure le stringhe "on" o "off" (ma la scelta è libera) e differenti valori per l'opzione -variable. Questo consente di selezionare un sottoinsieme dell'insieme di opzioni disponibili, come illustra il seguente esempio, relativo ad alcune opzioni tipiche di una finestra di ricerca interattiva di un editor di testo, presentato sia in HTML che in Tcl/Tk:

<form>
<input type="checkbox" name="case" value="1"> Case sensitive<br>
<input type="checkbox" name="wrap" value="1"> Wrap around<br>
<input type="checkbox" name="back" value="1"> Search backwards
</form>
checkbutton .case -text {Case sensitive} -variable case 
checkbutton .wrap -text {Wrap around} -variable wrap
checkbutton .back -text {Search backwards} -variable back
grid .case -sticky w
grid .wrap -sticky w
grid .back -sticky w

Tornando al codice che definisce l'interfaccia, notare inoltre che alcune opzioni -padx del comando pack(n) hanno come argomento una lista di due valori. Questo permette di specificare valori diversi dello spazio orizzontale extra (il padding) sulla sinistra e sulla destra del widget (in questo ordine). Nel nostro caso uno dei due valori è 0, in quanto in una direzione esiste già dello spazio di riempimento richiesto dal labelframe contenente.

La proc createwindows termina col codice che mette insieme i frame per costruire la finestra completa dell'applicazione, che non presenta particolari difficoltà:


    #
    # Main window
    #

    pack .buttons -side top -fill both
    pack .bell .kbd .mouse .screen -side top -fill both -expand yes \
            -padx 1m -pady 1m

    #
    # Let the user resize our window
    #
    wm minsize . 10 10

Adesso, occorre inizializzare i valori di tutti i controlli GUI con quelli delle variabili globali lette dalla proc readsettings, in quanto dove non obbligatorio si è preferito non connettere una variabile tramite l'opzione -variable, per evitare di mettere sotto traccia troppe variabili (che ha un costo). Ad esempio non si è usata l'opzione -variable per le entry e le scale. Comunque per completezza la proc dispsettings agirà anche su checkbutton e radiobutton, dove è pleonastico, poiché abbiamo usato l'opzione -variable.


    #
    # Sends all settings to the window
    #

    proc dispsettings {} {
        global kbdrep kbdcli  bellvol bellpit belldur
        global mouseacc mousethr  screenbla screentim screencyc

        .bell.vol set $bellvol
        .bell.val.pit.entry delete 0 end
        .bell.val.pit.entry insert 0 $bellpit
        .bell.val.dur.entry delete 0 end
        .bell.val.dur.entry insert 0 $belldur

        .kbd.val.onoff [expr "{$kbdrep} == {on} ? {select} : {deselect}"]
        .kbd.val.cli set $kbdcli

        .mouse.hor.acc.entry delete 0 end
        .mouse.hor.acc.entry insert 0 $mouseacc
        .mouse.hor.thr.entry delete 0 end
        .mouse.hor.thr.entry insert 0 $mousethr

        .screen.blank [expr "{$screenbla}=={blank} ? {select} : {deselect}"]
        .screen.pat   [expr "{$screenbla}!={blank} ? {select} : {deselect}"]
        .screen.tim.entry delete 0 end
        .screen.tim.entry insert 0 $screentim
        .screen.cyc.entry delete 0 end
        .screen.cyc.entry insert 0 $screencyc
    }

A questo punto non resta che attendere gli eventi e rispondere opportunamente alle azioni dei tasti: quando si preme sul tasto Quit basta distruggere la finestra principale dell'applicazione in modo da uscire. Al comando cancel si risponde rileggendo le impostazione correnti del server X nelle variabili globali preposte (richiamando readsettings) e poi riaggiustando i controlli chiamando dispsetting. Occorre poi ricordarsi, di disabilitare i pulsanti cancell e apply.


    #
    # Button actions
    #

    proc quit {} {
        destroy .
    }

    proc ok {} {
        writesettings
        quit
    }

    proc cancel {} {
        readsettings
        dispsettings
        .buttons.apply configure -state disabled
        .buttons.cancel configure -state disabled
    }

    proc apply {} {
        writesettings
        .buttons.apply configure -state disabled
        .buttons.cancel configure -state disabled
    }

I tasti Apply e Ok invece hanno bisogno di un'azione nuova che consiste nel salvare le impostazioni correnti dei controlli nel server X, costruendo un comando xset con la corretta sintassi dei parametri ed eseguendolo come sottoprocesso. La procedura writesettings si occupa di questo e in più, visto che la cosa non comporta alcuna complicazione, salva anche le nuove impostazioni nelle variabili globali preposte allo scopo anziché usare variabili locali. In questo modo non sarà necessario richiamare readsettings dopo una writesettings in apply:

    #
    # Write settings into the X server
    #

    proc writesettings {} {
        global kbdrep kbdcli  bellvol bellpit belldur
        global mouseacc mousethr  screenbla screentim screencyc

        set bellvol         [.bell.vol get]
        set bellpit         [.bell.val.pit.entry get]
        set belldur         [.bell.val.dur.entry get]

        if {[expr "{$kbdrep} == {on}"]} then {
            set kbdcli      [.kbd.val.cli get]
        } else {
            set kbdcli      "off"
        }

        set mouseacc        [.mouse.hor.acc.entry get]
        set mousethr        [.mouse.hor.thr.entry get]

        set screentim       [.screen.tim.entry get]
        set screencyc       [.screen.cyc.entry get]

        exec xset \
            b $bellvol $bellpit $belldur \
            c $kbdcli \
            r $kbdrep \
            m $mouseacc $mousethr \
            s $screentim $screencyc \
            s $screenbla
    }

Come proposta di esercizi, potreste provare a scrivere dei front-end grafici per altre utility a linea di comando di X Window, quali per esempio xsetroot(1) o xhost(1).

« Tk: applicazioni di esempio: il gioco del 15Appunti di Tcl/TkProgrammazione Object-Oriented »