« Tk: applicazioni di esempio: il gioco del 15 • Appunti di Tcl/Tk • Programmazione Object-Oriented »
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.
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 15 • Appunti di Tcl/Tk • Programmazione Object-Oriented »