MaCocoa 040

Capitolo 040 - Polimorfismo classico

Comincio ad organizzare la modalità di disegno all'interno delle view.

C'è un esempio di Apple nei Dev Tools, si chiama Sketch, da cui trarre consigli.

Primo inserimento: 15 gennaio 2004

Un classico modello OOP

Uno degli esempi più abusati di quando si intende spiegare la programmazione object oriented è di rappresentare un elemento grafico come un oggetto. Poiché ogni oggetto è autocontenuto e possiede nel suo interno tutte le informazioni e le capacità necessarie, per disegnare una collezione di oggetti potrei mandare a tutti lo stesso messaggio.

Bene, è proprio questo il meccanismo che mi accingo a realizzare per strutturare meglio il disegno di elementi grafici all'interno della coverView. Finora infatti il disegno avveniva brutalmente all'interno del metodo drawRect: con la pedissequa sequenza delle operazioni.

In realtà, l'idea della view è che l'utente disegni la copertina come meglio gli aggrada, magari partendo da uno schema predefinito (le linee guida di taglio, cose del genere), aggiungendo o togliendo elementi dalla finestra. Per fare questo, bisogna che il contenuto della view sia disegnato non staticamente, ma in base ad una collezione di elementi grafici che sono stati definiti dall'utente (e magari in parte definiti per la natura della copertina). Vedo che sto cercando di spiegarmi con un mare agitato di parole; vi invito allora a pensare ad una applicazione di disegno vettoriale (Adobe Illustrator, ad esempio, ma anche l'esempio citato Sketch va benissimo). Qui l'utente disegna sulla finestra linee, rettangoli, inserisce testi, eccetera. Alla fine la mia finestra coverView vorrà, nel suo piccolo, fornire le stesse funzioni (ovviamente, in forma semplificata). In più dovrà fornire una serie di template, ovvero schemi precostituiti come il contorno da ritagliare del retro della copertina (sul quale finora mi sono esercitato) come guida per la composizione.

Come esercizio per questo capitolo voglio ripetere il disegno del capitolo precedente; questa volta, tuttavia, le linee, i rettangoli, i testi e le immagini non sono disegnate brutalmente dall'interno del metodo drawRect:, ma derivano da una operazione propriamente OOP su una struttura dati, dove è descritto, in maniera astratta, il contenuto della view.

Un modello per la grafica

Il primo passo è costruire una classe modello per un elemento grafico, una sorta di classe astratta dalla quale far discendere poi tutti gli elementi grafici.

Pigliando ispirazione da Sketch (ma rifuggo dall'ipotesi di plagio: ho già sfruttato tale costruzione molti anni fa in altri ambienti, e poi si tratta comunque di una tecnica oserei dire scolastica), dichiaro una classe siffatta:

@interface CCE_BasicForm : NSObject {
    // rettangolo che contiene l'oggetto
    NSRect        elemLimit ;
    // spessore della linea di contorno
    float        cceLineWidth ;
    // colore dell'eventuale interno e del contorno
    NSColor        * cceFillColor ;
    NSColor        * cceLineColor ;
    // se devo colorare l'interno o meno
    BOOL        cceIsFilled ;
    // se l'elemento e' selezionato o meno
    BOOL        cceIsSelected ;
}
// metodi che ogni sottoclasse deve realizzare
// inizializzazione, in qualche forma
- (id) init ;
// deallocazione
- (void) dealloc ;
// disegno dell'elemento
- (void) drawElement: (NSRect) inRect ;
...
@end

In questa classe raccolgo un po' di variabili d'istanza che serviranno più o meno sempre in tutti gli elementi grafici da disegnare; per tutti aggiungo i metodi accessor, così mi tolgo il pensiero e li rendo disponibili a tutte le sottoclassi.

La parte importante della classe sono però i tre metodi lì dichiarati, che ogni sottoclasse deve in qualche forma realizzare. In particolare, il primo non dovrà essere sempre così semplice (sarò chiaro più tardi), il secondo viene da sé, mentre il terzo è il cuore del meccanismo. Se infatti faccio in modo che tutti gli elementi che a qualche titolo si possono disegnare all'interno della coverView rispondono al messaggio drawElement, ecco che per ri-disegnare il contenuto della view, è sufficiente inviare questo messaggio a tutti gli elementi.

Dal punto di vista concettuale, ho già finito. Adesso aggiungo alla coverView una variabile d'istanza della classe NSMutableArray (un vettore di dimensione variabile); ogni volta che voglio rappresentare un elemento grafico nella coverView costruisco un oggetto delle classe CCE_BasicElem che risponda al messaggio drawElement, e poi aggiungo quest'elemento al vettore.

Il metodo drawRect: della coverView diventa di una semplicità disarmante (elemArray è il vettore NSMutableArray).

- (void) drawRect: (NSRect) aRect
{
    int        i , numElem ;
    CCE_BasicForm * elem ;
    // conto quanti elementi sono presenti
    numElem = [ elemArray count ];
    // spazzolo tutit gli elementi del vettore
    for ( i = 0 ; i < numElem ; i ++ )
    {
        // recupero l'elemento i-esimo
        elem = [ elemArray objectAtIndex: i ];
        // gli dico di disegnarsi
        [ elem drawElement: aRect ];
    }
}

Come si può vedere, tutto molto bello ed elegante.

Detto questo, tutto il percorso è adesso in discesa.

Pigliamo ad esempio come fare a disegnare una linea. Non si può fare a meno di richiedere due punti, il punto di partenza e quello di arrivo; il metodo di inizializzazione presenta quindi due argomenti:

- (id) initStartPt: (NSPoint) stPt toPt: (NSPoint) endPt ;

Poi, per evitare ogni volta di costruire un oggetto NSBezierPath (come fatto nel capitolo precedente, nel quale per ogni segmento si costruiva un oggetto del genere, per poi buttarlo via subito), mi conservo il segmento come variabile d'istanza. In conclusione, la dichiarazione della classe è la seguente:

@interface CCE_Line : CCE_BasicForm {
    // punto di inizio
    NSPoint            startPoint ;
    // punto di terminazione
    NSPoint            endPoint ;
    // tengo il percorso per meglio disegnare
    NSBezierPath    * theLine ;
}
// i soliti metodi comuni
- (id) initStartPt: (NSPoint) stPt toPt: (NSPoint) endPt ;
- (void) dealloc ;
- (void) drawElement: (NSRect) inRect ;
...
@end

La realizzazione è molto semplice:

-(id) initStartPt: (NSPoint) stPt toPt: (NSPoint) endPt
{
    self = [ super init ];
    if ( self )
    {
        // per mia praticita', costruisco un percorso
        NSBezierPath *path = [NSBezierPath bezierPath];
        // assegno i punti di partenza ed arrivo
        startPoint = stPt ;
        endPoint = endPt ;
        // costruisco il percorso
        [path moveToPoint: startPoint ];
        [path lineToPoint: endPoint ];
        // lo assegno
        [ self setTheLine: path ];
        // metto a posto il rettangolo che lo contiene
        [self setElemLimit: [theLine bounds] ];
    }
    return ( self );
}

Mi tengo da parte i due punti, poi costruisco ed assegno il segmento. Per il momento, mi tengo anche i punti estremi ed il rettangolo che racchiude il segmento (anche se in effetti non servono) in quanto ho il sospetto mi potranno servire poi (ad esempio, per capire se un clic all'interno della coverView avviene proprio sopra in segmento; una informazione del genere è fondamentale per poter selezionare e manipolare il segmento...).

Perfino banale il metodo che realizza il disegno vero e proprio:

- (void) drawElement: (NSRect) inRect
{
    // imposto il colore
    [ cceLineColor set ];
    // imposto lo spessore della linea
    [ theLine setLineWidth: [ self cceLineWidth ]];
    // disengo la linea
    [ theLine stroke ];
}

Si tratta di impostare colore e spessore, e di procedere al disegno.

Arrivato a questo punto, non voglio tediarvi con la spiegazione degli altri elementi grafici che ho definito, ovvero le classi CCE_Rect, CCE_Text e CCE_Image, le quali, nell'ordine, servono a rappresentare un rettangolo, un testo variamente orientato, ed una immagine. In pratica ho estratto il codice del vecchio metodo drawRect: e l'ho spezzato, elemento per elemento per elemento, inglobando ogni pezzo all'interno dell'oggetto di appartenenza.

Vettori, gruppi e template

C'è però un particolare elemento grafico che mi preme descrivere, in quanto non è un elemento grafico, ma è particolarmente utile ad altri scopi.

Accade spesso che un certo insieme di elementi grafici semplici concorrano a realizzare un elemento più complicato. Oppure questi elementi fanno tutti parte di una entità logica comune. Se avete usato Illustrator o un CAD qualsiasi, sto parlando del concetto di Gruppo. Un gruppo è un insieme di oggetti che dovrebbe essere manipolato assieme in quanto è un tutt'uno logico. Concetti parenti del Gruppo sono quello di layer (suddivisione degli elementi secondo un piano di appartenenza) e quello di template (o blocco moduli, o file modello, chiamatelo come preferite:insieme di elementi predefiniti, generalmente non modificabile, a partire dal quale costruire rappresentazioni più complesse). In altre parole, spesso conviene organizzare tutti gli elementi grafici non tanto sotto forma di vettore (in cui le uniche relazioni tra gli elementi sono chi viene prima e chi viene dopo) ma sotto una forma gerarchica più complicata (un albero, tipicamente).

Per tenere conto in qualche modo di queste esigenze, ho definito una sottoclasse CCE_ElemGroup; questa classe in effetti è un vettore di elementi grafici. Tuttavia, come sua caratteristica peculiare, è in grado di rispondere ai (per ora) tre caratteristici metodi di un elemento grafico: init, dealloc e drawElement. Ma per rendersi conto della semplicità e della potenza di questo oggetto, occorre guardare la sua dichiarazione e la sua realizzazione.

@interface CCE_ElemGroup : CCE_BasicForm {
    // il vettore degli elementi raggruppati
    NSMutableArray        * elemArray ;
}
// i soliti metodi comuni
- (id) init ;
- (void) dealloc ;
- (void) drawElement: (NSRect) inRect ;

// per aggiungere un elemento al gruppo
- (void) addElem: (CCE_BasicForm *) elem ;
...
@end

Fin qui, nulla di speciale; l'unica variabile d'istanza è un NSMutableArray; è quindi giocoforza aggiungere anche metodi per manipolare tale vettore. Al momento c'è solo il metodo per aggiungere un elemento, ma mi figuro che poi ce ne saranno altri (per togliere, ordinare, eccetera).

Il metodo di inizializzazione non è particolarmente brillante:

-(id) init
{
    self = [ super init ] ;
    if ( self )
        [ self setElemArray: [ NSMutableArray array ]];    
    return self ;
}

Ma passiamo al metodo di disegno:

- (void) drawElement: (NSRect) aRect
{
    int        i , numElem ;
    CCE_BasicForm * elem ;
    
    numElem = [ elemArray count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        [ elem drawElement: aRect ];
    }
}

ricorda qualcosa, vero? è esattamente lo stesso codice del metodo drawRect: della coverView.

Bene, seguite questa visione: tutti gli elementi all'interno della coverView sono raccolti dentro vettori, o meglio, ancora, dentro CCE_ElemGroup. L'insieme di tutti gli elementi è un CCE_ElemGroup; il quale vettore a sua volta è composto da elementi grafici ed altri CCE_ElemGroup; e questi CCE_ElemGroup, a cascata, da altri elementi ed altri CCE_ElemGroup.

Quando, fra qualche (chissà quanti) capitoli, sarà possibile selezionare un po' di elementi e poi, da comando di menu, farne un gruppo, si pigliano questi elementi e li si riuniscono all'interno di un CCE_ElemGroup, che li sostituisce all'interno del CCE_ElemGroup che li elencava in precedenza. E poiché il gruppo è un elemento grafico, sarà in grado di rispondere a tutti i messaggi comuni ed eseguire le funzionalità comuni a tutti gli elementi. Ad esempio, un clic dell'utente su un elemento del gruppo sarà in prima battuta intercettato dall'elemento CCE_ElemGroup che definisce il gruppo: l'utente quindi seleziona il gruppo (comportamento corretto) e non l'elemento (che sarebbe scorretto). Non sono sicuro di aver reso appieno la potenza e l'eleganza di questo costrutto informatico. Tutto quello che posso fare è completare questo capitolo con le modifiche della coverView.

Disegnare per elementi

La classe coverView elimina tutte le precedenti variabili d'istanza (che servivano come appoggio per disegni vari, ed utilizza (a parte gli outlet, che ovviamente rimangono) una singola variabile d'istanza, che guarda un po', è la seguente:

    CCE_ElemGroup        * theElements ;

Questa variabile contiene tutti gli elementi da rappresentare all'interno della coverView. Il metodo drawRect: è quindi diventato molto ma molto semplice:

- (void)drawRect:(NSRect)rect
{
    // sfondo bianco
    [[ NSColor whiteColor] set ];
    NSRectFill( rect );
    // poi dico di disegnarsi a tutti gli elementi
    [ [self theElements] drawElement: rect ];
}

Per il momento, per disegnare qualcosa all'interno, aggiungo elementi alla variabile theElements all'interno del metodo initWithFrame della coverView. In realtà, chi aggiungerà elementi alla variabile sarà l'utente, disegnandoli a schemi come in ogni programma di disegno vettoriale che si rispetti. Anche questa volta, accenno solamente a parti del codice del metodo, lungo e noioso:

- (id)initWithFrame:(NSRect)frameRect
{
    float x1, x2, y1, y2 ;

    if ((self = [super initWithFrame:frameRect]) != nil)
    {
        // costruisco il vettore degli elementi
        [ self setTheElements: [[CCE_ElemGroup alloc] init ] ];
        // aggiungo un po' di roba
        if ( 1 )
        { // qui disegno tutto lo sfondo della copertina
            CCE_Line        * tmpLine ;
            CCE_Rect        * tmpRect ;
            CCE_ElemGroup * backCover ;
            // raccolto tutti gli elementi in un gruppo
            backCover = [ [ [ CCE_ElemGroup alloc ] init ] autorelease ];
            // due linee di taglio inferiori verticali
            x1 = STARTPOINT_X + CUTSIGN_DIM + CUTSIGN_DIST ;
            y1 = STARTPOINT_Y ;
            x2 = STARTPOINT_X + CUTSIGN_DIM + CUTSIGN_DIST ;
            y2 = STARTPOINT_Y + CUTSIGN_DIM ;    
            tmpLine = [ [ CCE_Line alloc] initStartPt: NSMakePoint( CM2PX(x1), CM2PX(y1) )
                                        toPt: NSMakePoint( CM2PX(x2), CM2PX(y2) ) ] ;
            [ tmpLine setCceLineColor: [ NSColor redColor] ];
            [ tmpLine setCceLineWidth: 0 ];
            [ backCover addElem: tmpLine ];

Il metodo inizia inizializzando l'elemento theElements, vuoto.

Con la prima sezione di istruzioni intendo definire un gruppo; questo gruppo è costruito da tutti gli elementi che formato la rappresentazione di base dello schema della copertina (le linee di taglio e il rettangolo che la contiene, completa delle due bande laterali col titolo del CD).

Le istruzioni, di volta in volta, predispongono i punti di partenza ed arrivo dei segmenti, il colore della linea, il suo spessore, eccetera; aggiungo poi l'elemento grafico così costruito non alla lista degli elementi theElements, ma all'elemento backCover che li raggruppa. Solo alla fine, col codice seguente, completo l'operazione.

            x1 = STARTPOINT_X + CUTSIGN_DIM + CUTSIGN_DIST + BACK_OUTER_HOR_DIM - BACK_LABEL_HOR_DIM ;
            y1 = STARTPOINT_Y + CUTSIGN_DIM + CUTSIGN_DIST ;
            x2 = STARTPOINT_X + CUTSIGN_DIM + CUTSIGN_DIST + BACK_OUTER_HOR_DIM - BACK_LABEL_HOR_DIM ;
            y2 = STARTPOINT_Y + CUTSIGN_DIM + CUTSIGN_DIST + BACK_OUTER_VER_DIM ;    
            tmpLine = [ [ CCE_Line alloc] initStartPt: NSMakePoint( CM2PX(x1), CM2PX(y1) )
                                        toPt: NSMakePoint( CM2PX(x2), CM2PX(y2) ) ] ;
            [ tmpLine setCceLineColor: [ NSColor blackColor] ];
            [ tmpLine setCceLineWidth: 0 ];
            [ backCover addElem: tmpLine ];
            // aggiungo l'intero gruppo...
            [ [self theElements] addElem: backCover ];
        }

Qui il gruppo è completato con l'ultima linea; poi il gruppo completo è aggiunto, finalmente, all'elenco degli elementi grafici.

La storia prosegue con le stringhe di testo (una verticale ed una orizzontale) e si conclude con la solita immagine.

        if ( 1 )
        { // aggiungo il disegno dell'immagine
            NSImage        * locImg ;
            CCE_Image * tmpImg ;
            // recupero l'immagine
            locImg = [NSImage imageNamed: @"Add"] ;
            // calcolo il punto
            x1 = STARTPOINT_X + CUTSIGN_DIM + CUTSIGN_DIST + BACK_OUTER_HOR_DIM / 2 ;
            y1 = STARTPOINT_Y + CUTSIGN_DIST+ CUTSIGN_DIST + BACK_OUTER_VER_DIM / 2 ;
            tmpImg = [[ CCE_Image alloc] initWithImage: locImg
                        atPoint: NSMakePoint( CM2PX(x1), CM2PX(y1) ) ];
            // aggiungo l'elemento al vettore
            [ [self theElements] addElem: tmpImg ];
        }

Non succede nulla di stano; si costruisce l'immagine e la si aggiunge al vettore degli elementi. In definitiva, gli elementi di cui il disegno è composto sono quattro: due stringhe di testo, una immagine e un gruppo. Il gruppo a sua volta è costruito da una diecina di elementi grafici (vari segmenti ed un rettangolo). Se non vi ho ancora convinto della bontà di questo costrutto, vi lascio immaginare lo scenario seguente. Un oggetto CCE_ElemGroup è un oggetto come tanti altri; diciamo che si tratta del gruppo visto sopra che traccia lo schema della copertina. Potrei salvarlo su file coi meccanismi classici (encodeWithCoder:...). Come si trova su file, posso recuperalo così com'è, e ridisegnarlo tale e quale su altre coverView.

Ho appena descritto un meccanismo per costruire, importare ed esportare template.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).