|
AmigaDev
ARexx
di Alfonso Ranieri
Parte decima: Una simpatica GUI
Questo mese torno, su richiesta, a parlare di GUI RxMUI. Come al
solito, piuttosto che dilungarmi in spiegazioni teoriche, preferisco
illustrare le varie tecniche trattando un esempio pratico. Lo spunto
me lo offrono i bellissimi Warp datatype, che hanno l'unico difetto
di avere un'antipatica GUI Reaction. E' proprio nel desiderio di riscrivere
la GUI di configurazione del WarpJPEG datatype che nasce questo articolo.
Entriamo subito nel vivo. Il primo passo da compiere è analizzare
il nostro obiettivo: dobbiamo creare una piccola applicazione che gestisca
la configurazione del WarpJPEG datatype. Questo salva la sua configurazione
come un file di testo in
- ENV:Datatypes/WarpJPEG:prefs
- ENVARC:Datatypes/WarpJPEG:prefs.
Il file è formato da linee del tipo
parm=value
ove parm è un parametro di configurazione e value il suo valore.
I parametri usati sono indicati nella seguente tabella:
NOME |
VALORI |
DEFAULT |
ETICHETTE |
NOTE |
DITHER_OVERRIDE |
nothing
defaults
appsc
everything |
nothing |
Nothing Defaults Only
Applications Only
Everything |
nothing disabilita
DITHER_QUALITY
DITHER_DEPTH |
DITHER_QUALITY |
poor
good
best |
Bst |
Poor (Fast)
Good (Slow)
Best (Slow) |
|
DITHER_DEPTH |
1-24 |
8 |
- |
|
PENS_OVERRIDE |
nothing
defaults
apps
everything |
nothing |
Nothing
Defaults Only
Applications Only
Everything |
nothing disablita
PENS_QUALITY |
PENS_QUALITY |
poor
good
best |
good |
Poor
Good
Best |
|
OUTPUT_MODE |
full
reduced |
full |
Full Colour (24 bit)
Reduced Colour |
full disabilita
DITHER
QUANTIZATION
NUM_COLOURS |
DCT_METHOD |
fast
slow |
slow |
Good (Fatest)
Best (Slower) |
|
FANCY_UPSAMPLING |
off
on |
off |
- |
|
DITHER |
none
orderded
fs |
fs |
None
Ordered
Floyd-Steinberg |
ordered disablita
QUANTIZATION |
QUANTIZATION |
poor
best |
best |
Poor (Fast)
Best (Slow) |
|
NUM_COLOURS |
8-256 |
256 |
- |
|
La configurazione che dobbiamo gestire è costituita quindi da un insieme
di 11 valori che conserveremo nelle variabili globali:
- global.Prefs.DITHER_OVERRIDE
- global.Prefs.DITHER_QUALITY
- global.Prefs.DITHER_DEPTH
- global.Prefs.PENS_OVERRIDE
- global.Prefs.PENS_QUALITY
- global.Prefs.OUTPUT_MODE
- global.Prefs.DCT_METHOD
- global.Prefs.FANCY_UPSAMPLING
- global.Prefs.DITHER
- global.Prefs.QUANTIZATION
- global.Prefs.NUM_COLOURS
Poiché vogliamo dare all'utente sia la possibilità di ritornare sui suoi
passi, ovvero vogliamo offrire il menù Restore, sia la possibilità di
testare al volo la configurazione attuale, ovvero vogliamo offrire il
bottone Test, ci serviremo di 2 altri insiemi:
- global.BackPrefs che conterrà l'ultima configurazione salvata (in
ENV: o ENVARC:)
- global.TestPrefs che conterrà la configurazione presente in ENV:
prima che l'utente usi la funzione di "Test".
Si copierà global.Prefs in global.BackPrefs ogniqualvolta la configurazione
sarà salvata.
Si leggerà la configurazione presente in ENV: in global.TestPrefs, un
momento prima di salvare global.Prefs in ENV: nella funzione di "Test".
A questo punto appare ovvio che avremmo bisogno di un certo numero
di funzioni che gestiscono questi insiemi:
- ConfigToGadgets()
Setta i vari oggetti della nostra GUI ai valori contenuti in global.Prefs;
- GadgetsToConfig()
Copia i valori contenuti nei vari oggetti della nostra GUI in global.Prefs;
- ConfigToDefaults()
Setta global.Prefs ai valori di default;
- ReadConfig(saved)
Se saved è l legge il file ENVARC:Datatypes/WarpJPEG.prefs altrimenti
legge ENV:Datatypes/WarpJPEG.prefs, analizza il file e setta global.Prefs;
- SaveConfig(save)
Salva la configurazione corrente in ENVARC:Datatypes/WarpJPEG.prefs,
se save è 1, in ENV:Datatypes/WarpJPEG.prefs altrimenti;
- CopyConfig(from,to)
Copia la configurazione contenuta in from su to.
Queste sono le funzioni di base su cui andremo a costruire la nostra GUI.
Come al solito, abbiamo adottato la tecnica del "divide et impera" per
individuare una insieme di operazioni "semplici" e le abbiamo implementate.
In questo modo abbiamo delle funzioni che possiamo considerare primitive
ed usando queste, scrivere il resto. Le funzioni sono abbastanza semplici
da scrivere. Meritano menzione il ciclo di ReadConfig():
do while conf~=""
parse upper var conf key "=" value "A"x conf
select
…
end
end
basato sulla lettura dell'intero file di configurazione e quindi del suo
parsing linea per linea.
E la funzione CopyConfig():
CopyConfig: procedure expose global.
parse arg from,to
interpret to".DITHER_OVERRIDE = "from".DITHER_OVERRIDE"
interpret to".DITHER_QUALITY = "from".DITHER_QUALITY"
interpret to".DITHER_DEPTH = "from".DITHER_DEPTH"
interpret to".PENS_OVERRIDE = "from".PENS_OVERRIDE"
interpret to".PENS_QUALITY = "from".PENS_QUALITY"
interpret to".OUTPUT_MODE = "from".OUTPUT_MODE"
interpret to".DCT_METHOD = "from".DCT_METHOD"
interpret to".FANCY_UPSAMPLING = "from".FANCY_UPSAMPLING"
interpret to".DITHER = "from".DITHER"
interpret to".QUANTIZATION = "from".QUANTIZATION"
interpret to".NUM_COLOURS = "from".NUM_COLOURS"
return
basata sull'uso di interpret, in un tipico trucchetto ARexx, che permette
di rendere semplice un'operazione altrimenti molto complicata.
Un'altra parte importante del programma è ovviamente la funzione che
crea la GUI stessa. Io ho scelto di dividere la GUI in:
- un gruppo verticale
- un gruppo orizzontale
- un listview che scrolla le pagine di
- un gruppo registro con le varie pagine da visualizzare
- un oggetto testo che mostra informazioni
- un gruppo orizzontale con i bottoni Save Use Test Cancel
Anche in questo caso, ricordando che il nostro scopo è creare un oggetto
Application, raffiniamo fino ad avere:
- un oggetto Application
- un MenuStrip
- una Window
- un gruppo principale verticale
- un gruppo orizzontale
- un listview
- un gruppo registro
- la pagina WarpDT
- la pagina JPEG
- un oggetto teso
- un gruppo orizzontale
Andando avanti con la raffinazione, si arriva alla descrizione in ogni
dettaglio della GUI, come riportato in Tabella 1. Vorrei fare notare che
la GUI in se stessa è molto semplice e che gran parte del tempo, nel disegnarla,
lo si perde a trovare la giusta disposizione degli oggetti. MUI rende
il procedimento di layout molto semplice, una volta fatta la mano con
le leggi che lo regolano. Ogni oggetto possiede larghezza e lunghezza
minime e massime intrinseche o specificate al momento della sua creazione:
ad esempio un oggetto String ha larghezza minima 0 e massima infinita,
altezza minima e massima fissate dal carattere in uso; un Checkmark ha
altezza e larghezza fisse e così via. La classe che regola il layout degli
oggetti è ovviamente la Group class che segue queste semplicissime regole:
- Gruppi orizzontali:
- La larghezza minima di un gruppo orizzontale è la somma delle
larghezze minime dei suoi figli;
- La larghezza massima di un gruppo orizzontale è la somma delle
larghezze massime dei suoi figli;
- L'altezza minima di un gruppo orizzontale è la più grande tra
le altezze minime dei suoi figli;
- L'altezza massima di un gruppo orizzontale è la più piccola
tra le altezze massime dei suoi figli;
- Gruppi verticali:
- L'altezza minima di un gruppo verticale è la somma delle altezze
minime dei suoi figli;
- L'altezza massima di un gruppo verticale è la somma delle altezze
massime dei suoi figli;
- La larghezza minima di un gruppo verticale è la più grande delle
larghezze minime dei suoi figli;
- La larghezza massima di un gruppo verticale è la più piccola
delle larghezze massime dei suoi figli.
Vi prego di rileggere con attenzione queste regole, perché, una volta
comprese, vi aiuteranno enormemente nel disegno di una GUI. Facciamo un
esempio, consideriamo il gruppo:
Host: |String host |
Proxy: |String proxy |
Resume: [Checkmark resume]
Definirlo è semplicissimo:
g.columns=2
g.0=Label("_Host")
g.1=String("Host","h")
g.2=Label("_Proxy")
g.3=String("Proxy","p")
g.4=Label("_Resume")
g.5=Checkmark("Resume",,"r")
Purtroppo il codice sopra non produce il risultato desiderato; infatti
per le regole che controllano le dimensioni di un gruppo, g avrà larghezza
fissa, perché la sua larghezza massima sarà "la più piccola delle larghezze
massime dei suoi figli" e quindi la larghezza del Checkmark. Per ottenere
il risultato voluto dovremo fare:
g.columns=2
g.0=Label("_Host")
g.1=String("Host","h")
g.2=Label("_Proxy")
g.3=String("Proxy","p")
g.4=Label("_Resume")
g.5="rg"
rg.class="group"
rg.horiz=1
rg.0=Checkmark("Resume",,"r")
rg.1=hspace()
Il questo caso il sesto figlio di g è il gruppo orizzontale rg che ha
come larghezza massima, la somma delle larghezze massime dei suoi figli.
Uno dei suoi due figli è uno spazio orizzontale di larghezza 0 (ovvero
con larghezza minima 0 e massima infinito) e quindi la sua larghezza massima
sarà infinita. Per riassumere: il processo di layout, cioè la disposizione
degli oggetti, è controllato dalla classe Group e può essere tenuto sotto
controllo con un attento uso degli oggetto spazio.
Definite le funzioni di base per trattare la configurazione del WarpJEPG
datatype e creata la GUI siamo ben oltre la metà dell'opera. Il codice
di gestione della GUI è il solito banale:
HandleApp: procedure expose global.
ctrl_c=2**12
do forever
call NewHandle("app","h",ctrl_c)
if and(h.signals,ctrl_c)~=0 then call SafeExit()
if h.EventFlag then
select
when h.event="QUIT" then call SafeExit()
otherwise interpret h.event
end
end
In HandleApp() controlliamo soltanto gli eventi:
- qualcuno ci ha mandato un ctrl-c, nel qual caso usciamo;
- si è ricevuto l'evento QUIT, nel qual caso usciamo;
- si è ricevuto un evento che eseguiamo come fosse una stringa ARexx.
A questo punto non rimane che stabilire le notifiche tra gli oggetti.
Diversi mi hanno scritto a proposito delle notifiche quindi faccio un
breve riassunto: una notifica è un processo automatico di applicazione
di un metodo su un oggetto target quando un attributo di un oggetto notifier
cambia valore. Bisogna istruire RxMUI a ciò attraverso la funzione Notify()
che assume la forma:
call Notify(notifier,TriggerAttr,TriggerValue,target,method,parm1,...)
e istruisce RxMUI ad invocare method su target quando l'attributo TriggerAttr
di notifier assume il valore TriggerValue.
Si noti che:
- TriggerAttr deve essere un attributo di notifier di classe N, ovvero
deve essere un attributo notificato da notifier. Ad esempio la classe
Cycle notifica l'attributo Active, ma non notifica l'attributo Entries,
quindi Active va bene come TriggerAttr, ma Entries no;
- TriggerValue deve essere un valido valore per TriggerAttr, oppure
la stringa EveryTime che significa "Ogni volta che TriggerAttr cambia
valore";
- method deve essere un metodo di target notificabile. Esistono metodi
notificabili e metodi non notificabili;
- parm1 (e gli altri parametri) devono essere validi parametri di
method o, nel caso in cui si è usato Everytime come TriggerValue,
TriggerValue che significa "il valore di TriggerAttr che ha generato
la notifica" o NotTriggerValue che significa "il complemento logico
del valore di TriggerAttr che ha generato la notifica".
Una notifica basata sulla pressione del bottone Save si definisce come:
call Notify("Save","pressed",0,...)
Una notifica basata sulla selezione del menù mabout si definisce come:
call notify("mabout","MenuTrigger","everytime",...)
Una notifica basata sulla pressione del gadget di chiusura finestra della
finestra win si definisce come:
call notify("win","CloseRequest",1,...)
Una notifica basata sul cambiamento delle voce corrente del listview hlister
si definisce come:
call notify("hlister","active","everytime",...)
Una notifica basata sul cambiamento delle voce corrente dell'oggetto Cycle
DITHER_OVERRIDE si definisce come:
call notify("DITHER_OVERRIDE","active","everytime",...)
La pressione del gadget di chiusura finestra significa "Esci", ovvero
ritorna all'oggetto Application lo speciale ID Quit:
call notify("win","CloseRequest",1,"app","ReturnID","Quit")
La selezione del menù "About..." significa "apri la finestra About", ricordando
che Application class possiede un metodo per mostrare una finestra di
About automaticamente creata, si farà semplicemente:
call notify("mabout","MenuTrigger","EveryTime","app","about","win")
Cioè: "Quando l'attributo MenuTrigger dell'oggetto mabout cambia valore
(il che vuol dire che l'utente lo ha selezionato), invoca su app il metodo
about con argomento win (ovvero centra la finestra di About sulla finestra
win).
La selezione del menù "AboutRxMUI..." significa "apri la finestra
AboutRxMUI":
call notify("maboutrxmui","MenuTrigger","EveryTime","app","AboutRxMUI","win")
La selezione del menù "AboutMUI..." significa "apri la finestra AboutMUI":
call notify("maboutmui","menutrigger","everytime","app","AboutMUI","win")
La selezione del menù "Hide" significa "iconificati", per iconificare
un'applicazione, basta settare a 1 l'attributo Iconified dell'oggetto
app, ovvero:
call notify("mhide","menutrigger","everytime","app","set","iconified",1)
La selezione del menù "Quit" significa "FINE":
call notify("mquit","menutrigger","everytime","app","ReturnID","quit")
La selezione del menù "Reset to defaults..." significa "Chiama la funzione
DefaultConfig()":
call notify("mdefault","menutrigger","everytime","app","return","call DefaultConfig()")
Scriviamo la funzione DefaultConfig(): il suo compito è quello di riportare
la configurazione attuale a quella di default, ovvero:
- settare global.Prefs alla configurazione di default
- copiare global.Prefs in global.BackPrefs
- settare tutti gli oggetti ai nuovi valori
Ciò si traduce in:
DefaultConfig: procedure expose global.
call ConfigToDefaults()
call CopyConfig("GLOBAL.PREFS","GLOBAL.BACKPREFS")
call ConfigToGadgets()
return
La selezione del menù "Last saveds..." significa "Chiama la funzione RestoreConfig(1)":
call notify("mlast","menutrigger","everytime","app","return","call RestoreConfig(1)")
Scriviamo la funzione RestoreConfig(): il suo compito è quello di riportare
la configurazione attuale a:
- l'ultima salvata se la funzione viene invocata dal menù "Last saveds"
- l'ultima usata se la funzione viene invocata dal menù "Restore"
RestoreConfig: procedure expose global.
parse arg saved
if saved then call ReadConfig(1)
else call CopyConfig("GLOBAL.BACKPREFS","GLOBAL.PREFS")
call ConfigToGadgets()
return
La selezione del menù "Restore..." significa "Chiama la funzione RestoreConfig(0)":
call notify("mrestore","menutrigger","everytime","app","return","call RestoreConfig(0)")
La selezione del menù "MUI..." significa "Apri la finestra di configurazione
di MUI":
call notify("mmui","menutrigger","everytime","app","OpenConfigWindow")
La selezione di una voce del Listview significa "spostati nella pagina
corrispondente":
call notify("hlister","active","everytime","hpager","set","activepage","triggervalue")
Se l'utente preme il bottone Save, chiama la funzione SaveConfigExit:
call notify("save","pressed",0,"app","return","call SaveConfigExit(1)")
SaveConfigExit() semplicemente:
- chiude la finestra per dare l'idea di immediatezza :-)
- legge lo stato degli Oggetti
- salva la configurazione
SaveConfigExit: procedure expose global.
parse arg save
call set("win","open",0)
call GetSaveConfig(save)
exit
Ove GetSaveConfig() è:
GetSaveConfig: procedure expose global.
parse arg save
call GadgetsToConfig()
call SaveConfig(save)
return
Se l'utente preme il bottone Use, chiama la funzione SaveConfigExit:
call notify("use","pressed",0,"app","return","call SaveConfigExit(0)")
Se l'utente preme il bottone Test, chiama la funzione TestConfig:
call notify("test","pressed",0,"app","return","call TestConfig()")
TestConfig() è più complessa: se l'utente vuole "testare" la configurazione,
dovremo copiare quella corrente in ENV: ovvero:
- salvare la configurazione in ENV: da qualche parte
- scrivere la configurazione corrente in ENV:
- settare un flag che ci indichi lo stato special di "Test", ovvero
al momento di uscire, se l'utente non ha salvato, dovremo recuperare
la configurazione
TestConfig: procedure expose global.
call CopyConfig("GLOBAL.BACKPREFS","GLOBAL.TESTPREFS")
call GetSaveConfig(0)
global.test=1
return
Il flag global.test è usato per segnalare lo speciale stato. Viene
- azzerato da SaveConfig()
- settato da TestConfig()
- controllato da SafeExit()
SafeExit: procedure expose global.
if global.test then do
call CopyConfig("GLOBAL.TESTPREFS","GLOBAL.PREFS")
call SaveConfig(0)
end
exit
Ricapitoliamo…
Ricapitoliamo. Dobbiamo gestire una configurazione contenuta in un
file di testo, fornendo all'utente la possibilità di
- salvare in ENVARC: la configurazione ("Save")
- salvare in ENV: la configurazione ("Use")
- testare la configurazione (come "Use")
- rileggere l'ultima configurazione "salvata"
- riprestinare l'ultima configurazione "usata"
Usiamo 3 array globali:
- global.Prefs contiene la configurazione attuale
- global.BackPrefs contiene l'ultima configurazione salvata in ENV:
o ENVARC:
- global.TestPrefs contiene la configurazione presente in ENV: prima
di testare
Ci serviamo di alcune funzioni primitive per la gestione della configurazione:
- ConfigToGadgets() setta i vari oggetti della nostra GUI ai volori
contenuti in global.Prefs
- GadgetsToConfig() copia i valori contenuti nei vari oggetti in global.Prefs
- ConfigToDefaults() setta global.Prefs alla configurazione di default
- ReadConfig(saved) setta global.Prefs alla configurazione contenuta
in ENV: se saved è 0, in ENVARC: altrimenti
- SaveConfig(save) salva global.Prefs in ENV: se save è 0, ENVARC:
altrimenti
- CopyConfig(from,to) copia la configurazione da from a to
Su di queste scriviamo alcune altre funzioni che completano il nostro
programma:
- GetSaveConfig(save) legge lo stato degli oggetti e salva la configurazione
- SaveConfigExit() legge lo stato degli oggetti, salva la configurzione
ed esce
- efaultConfig() resetta la global.Prefs alla configurazioen di default
e setta lo stato degli oggetti
- RestoreConfig(saved) recupera la configurazione salvata
- TestConfig() entra nello stato di Test
- SafeExit() esce controllando se si era nello stato di Test
Il programma è completo, in ogni suo particolare. Gli manca soltanto la
pic dei Warp datatype, ma quella potete aggiungerla in ogni momento concreadno
un oggetto Picture.
Il compitino a casa è: redere il tutto localizzato, leggendo le stringhe
dal catalogo WarpJPEG.catalog usando la funzione rmh.library/LocalizeStrings().
Buon divertimento. Alla prossima.
Tabella 1 - L'albero degli oggetti della GUI di WarpJPEG
Applicazione
- APP [Application]
Menu dell'applicazione
- MSTRIP [Menustrip]
- MPROJECT [Menu]
- MABOUT [Menuitem]
- MABOUTRXMUI [Menuitem]
- MABOUTMUI [Menuitem]
- bar
- MHIDE [Menuitem]
- bar
- MQUIT [Menuitem]
- MEDITOR [Menu]
- MDEFAULT [Menuitem]
- MLAST [Menuitem]
- MRESTORE [Menuitem]
- bar
- MMUI [Menuitem]
Finestra principale (ed unica)
- MWIN [Window]
Gruppo prioncipale di MWIN (il suo contenuto)
- MGROUP [Group Vert]
Gruppo orizzontale
- HG [Group Horiz]
Listview per cambiare le pagine di HPAGER
- HLISTER [Listview]
- ENTRIES [List]
Gruppo pager
- HPAGER [Group PageMode]
Pagina 1/2 di HPAGER
- WARPDT [Group Vert]
- vspace
- OS3.5DITH [Group Columns=2]
- label
- DITHER_OVERRIDE [Cycle]
- label
- DITHER_QUALITY [Cycle]
- label
- DITHER_DEPTH [Slider]
- vspace
- PENSEL [Group Columns=2]
- label
- PENS_OVERRIDE [Cycle]
- label
- PENS_QUALITY [Cycle]
- vspace
Pagina 2/2 di HPAGER
- JPEG [Group Vert]
- vspace
- DEC [Group Vert]
- DEC0 [Group Columns=2]
- label
- OUTPUT_MODE [Cycle]
- label
- DCT_METHOD [Cycle]
- DEC1 [Group Horiz]
- hspace
- FANCY_UPSAMPLING [Image]
- label
- hspace
- vspace
- COLRED [Group Vert]
- CRG [Group Columns=2]
- label
- DITHER [Cycle]
- label
- QUANTIZATION [Cycle]
- label
- NUM_COLOURS [Slider]
- vspace
Oggetto testo per mostrare informazioni
- INFO [Text]
Gruppo orizzontale con bottoni
- BG [Group Hoiriz]
- SAVE [Text Button]
- hspace
- USE [Text Button]
- hspace
- TEST [Text Button]
- hspace
- CANCEL [Text Button]
|
Torna al sommario
|