Pagina principale faq Amiga Life a Pianeta Amiga La redazione
Galleria Indice generale
Enigma Amiga Life

AmigaLife 122 ?

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
          • una lista di voci
        • 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

Copyright (C) 1999-2002, la redazione di AmigaLife.
Il logo e le copertine della rivista sono tratti dal sito Pluricom e sono Copyright (C) 1992-2001 Pluricom S.r.l.