MaCocoa 055

Capitolo 055 - Immagina...

Aggiungo (o meglio, ritorno ad aggiungere) un elemento in grado di rappresentare una immagine (jpeg, gif, pict, eccetera) all'interno di una copertina.

Sorgente: la solita documentazione

Prima stesura: 29 agosto 2004

Elementi Immagine

All'inizio della storia della costruzione della finestra con la copertina avevo già introdotto l'elemento CCE_Image, che poi avevo barbaramente ucciso il capitolo successivo. Adesso riprendo in mano la questione, perfezionando il tutto.

figura 01

figura 01

Per prima cosa, apro in Interface Builder la palette degli strumenti della finestra e attivo l'ultimo strumento rimasto, cambiando leggermente l'icona in modo che rappresenti una immagine (data la mia classica incapacità di disegno, copio brutalmente l'icona dell'applicazione Anteprima e la ridimensiono come più mi piace). Modifico anche leggermente il metodo

- (IBAction)
handleToolSel:(id)sender
{
    switch ( [ [toolMatrix selectedCell] tag ] ) {
    case 0 : currCCE = nil ; break ;
    case 1 : currCCE = [ CCE_Image class ] ; break ;
    case 2 : currCCE = [ CCE_Line class ] ; break ;
    case 3 : currCCE = [ CCE_Rect class ] ; break ;
    case 4 : currCCE = [ CCE_Circle class ]; break ;
    case 5 : currCCE = [ CCE_Text class ] ; break ;
    }
}

in modo che restituisca la classe CCE_Image nel posto corretto.

L'idea per l'inserimento di un elemento di tipo immagine è che l'utente tracci un bel rettangolo dove prevede di inserire una immagine; immediatamente dopo, gli viene proposto un dialogo per la scelta di un file, che individua appunto l'immagine da inserire.

Però, per iniziare, occorre costruire la classe CCE_Image e scrivere un po' di metodi. Comincio con le variabili d'istanza:

@interface     CCE_Image : CCE_Rect
{
    NSImage        * image ;
    NSSize        imgRepDim ;
    float        compFraction ;
}

Le variabili sono tre; con la prima variabile conservo un oggetto della classe NSImage. Questa classe è piuttosto ricca, e permette di conservare più o meno ogni tipo di immagine leggibile con QuickTime e con l'applicazione Anteprima: quindi JPEG, GIF, PICT, EPS, TIFF, eccetera. Per i miei scopi è più che sufficiente. Conservo poi esplicitamente in una variabile di tipo NSSize le dimensioni originali dell'immagine; mi servono per dare una buona rappresentazione dell'immagine stessa quando si tratta di disegnarla all'interno della CoverView. La terza variabile rappresenta il canale alfa, o il fattore di trasparenza dell'immagine. Un valore di 1 per la variabile significa che l'immagine è del tutto opaca, e nasconde ogni pixel dietro di essa. Un valore di zero dice che l'immagine è completamente trasparente, mentre un valore intermedio compone l'immagine con lo sfondo combinando i pixel. L'effetto risultate è un'immagine più o meno trasparente, che lascia intravedere lo sfondo.

figura 02

figura 02

I metodi di inizializzazione (uno ricco ed uno più povero) utilizzano una immagine di default per la visualizzazione; scelgo l'icona dell'applicazione utilizzando uno degli identificatori predefiniti del metodo imageNamed:.

- (id)
initWithId: (int) ident inView: (CoverView*) cv
    withRect: (NSRect) aRect andAngle: (float) angle
{
    self = [ super    initWithId: ident inView: cv
                    withRect: aRect andAngle: angle ] ;
    if ( self )
    {
        // imposto una immagine di default
        [ self setImage: [ NSImage imageNamed: @"NSApplicationIcon" ] ];
        // metto a posto la dimensione originale ed la trasparenza
        imgRepDim = [ image size ];
        compFraction = 1.0F ;
        // l'immagine ha le coordinate rovesciate come la view
        [ image setFlipped: YES ];
    }
    return self ;
}

- (id)
init
{
    return( [ self    initWithId: 0 inView: nil
                    withRect: NSMakeRect(100,100, 50,50)
                    andAngle: 0] );
}

Il metodo di inizializzazione è necessario in pratica per predisporre le variabili d'istanza proprie della classe.

Visto che si costruisce un oggetto NSImage all'interno dei metodi di inizializzazione, occorre scrivere anche un metodo dealloc:

- (void)
dealloc
{
    [ image dealloc ];
    [ super dealloc ];
}

Impostazione e disegno

Ora, torniamo alla procedura di inserimento di una immagine. Non ci sono metodi da modificare, basta utilizzare il meccanismo di costruzione presente in CoverView (all'interno del metodo mouseDown:) e sfruttare l'invocazione del metodo startEditing per completare le operazioni:

- (void)
startEditing
{
    // tutti i tipi di immagine leggibili
    NSArray        * fileTypes = [NSImage imageFileTypes ];
    // il pannello di selezione file
    NSOpenPanel * oPanel = [NSOpenPanel openPanel];
    NSImage        * curImg ;    // l'immagine prescelta
    NSString    * aFile ;    // il path dell'immagine
    // dico che si puo' selezionare un solo file alla volta
    [oPanel setAllowsMultipleSelection:NO];
    // apro il pannello e consento la selezione
    if ( [oPanel runModalForTypes:fileTypes] != NSOKButton)
        return ;
    // se arrivo qui, e' stato selezionato un file
    // recupero il path del file
    aFile = [ [oPanel filenames] objectAtIndex:0];
    // costruisco l'immagine a partire dal file
    curImg = [[ NSImage alloc] initWithContentsOfFile: aFile ];
    // rovescio le coordinate, visto che anche la CoverView lo fa
    [ curImg setFlipped: YES ];
    // imposto l'elemento immagine con le nuova immagine
    [ self setImage: curImg ];
    // le nuove dimensioni
    [ self setImgRepDim: [ curImg size]];
    // forzo un ridisegno della vista
    [ ownerView setNeedsDisplay: YES ];
}

All'interno di questo metodo apro un panello per la selezione di un file di tipo grafico. Mi faccio direttamente specificare dalla classe NSImage quali siano i tipi di file in grado di essere gestiti. Il vettore contiene (nel mio caso) ottanta diversi elementi; in realtà i tipi differenti di file sono meno. Sono comunque compresi: PDF, PICT, EPS, PS, icone, JPEG, TGA, TARGA, RGB, SGI, MacPaint, PNG, GIF, BMP e TIFF (e questi sono solo i formati che conosco...). A partire dal file selezionato, costruisco un oggetto NSImage, che poi attribuisco all'elemento CCE_Image. Metto a posto le dimensioni e forzo il ridisegno.

Non occorre un metodo endEditing, che non avrebbe nulla da fare.

Rimane il metodo per il disegno vero e proprio.

- (void)
specificDrawing: (NSRect) inRect
{
    // aggiusto le dimensioni per non ingenerare confusione
    NSRect locrect = arrangeRect( localSize );
    // disegno il rettangolo e lo sfondo
    [ super specificDrawing: inRect ] ;
    // disegno l'immagine scalandola opportunamente
    [ image drawInRect: locrect
            fromRect: NSMakeRect(0,0, imgRepDim.width, imgRepDim.height)
            operation: NSCompositeSourceOver fraction: compFraction ];
}

Come prima operazione c'è sempre bisogno di aggiustare il rettangolo in cui disegnare l'immagine, esattamente nello stesso modo in cui ho operato nell'elemento CCE_Text. Quindi, sposto la funzione arrangeRect(.) dal file CCE_Text.m e la inserisco nel file djZeroUtils.m, dove vanno a finire tutti quei pezzi di codice che non sono specifici di una classe.

figura 03

figura 03

Con l'invocazione del metodo di disegno della superclasse eseguo il disegno del percorso che inscrive l'immagine ed eventualmente il riempimento con il colore di sfondo. Poi eseguo il disegno vero e proprio dell'immagine; qui la classe NSImage mette a disposizione diversi metodi, con alcune differenze significative. Tra le varie possibilità, ho scelto il metodo drawInRect:fromRect:operation:fraction:; il metodo utilizza due rettangoli. Il primo rettangolo indica il rettangolo destinazione, ovvero il rettangolo in cui deve essere disegnata l'immagine. L'immagine però è disegnata limitatamente a quanto specificato dal secondo rettangolo. Nel mio caso, per il primo rettangolo ho indicato l'effettivo rettangolo che racchiude l'immagine all'interno della CoverView (così come aggiustato dalla funzione arrangeRect(.), mentre come secondo rettangolo ho specificato quello che specifica l'intera immagine. In questo modo si ottiene un ridimensionamento dell'intera immagine alle dimensioni specificate dal rettangolo indicato dall'utente.

figura 04

figura 04

Gli altri due parametri del metodo indicano come il disegno dell'immagine interagisce col resto del disegno. Il parametro indicato da operation specifica come l'immagine sorgente (cioé quella su cui opera il metodo) si combina con l'immagine destinazione (nel caso, il contenuto della CoverView). Piuttosto che dilungarmi, vi rimando all'esempio CompositeLab presente nei file installati con XCode (in /Developer/Examples/AppKit), che mostra tutte le varie possibilità combinatorie (ahimé senza considerare la trasparenza).

Infine il quarto argomento specifica il valore di trasparenza dell'immagine sorgente. Per come utilizzo il metodo, ci sono tre livelli di disegno: la CoverView, lo sfondo dell'elemento CCE_Image, l'immagine stessa, della classe NSImage. La trasparenza permette di intervenire sull'opacità della NSImage rispetto allo sfondo, che è sempre disegnato opaco (se però lo sfondo non c'è, allora la trasparenza avviene rispetto alla CoverView).

Vorrei che apprezzaste il fatto che l'elemento CCE_Image può essere spostato, ruotato e ridimensionato senza dover mettere mano a metodi specifici, ma utilizzando sempre e solo i metodi standard di CCE_BasicForm.

Ancora sui menu contestuali

Prima di continuare con i metodi propri della classe CCE_Image, devo inserire un intervallo e riparlare dei menu contestuali. La versione precedente non è particolarmente soddisfacente; in primo luogo perché c'è qualche voce errata, ed in secondo luogo perché devo complicare il metodo che sceglie il menu ogni volta che un elemento ha bisogno di un menu specifico. Meglio lasciar fare le cose all'elemento selezionato stesso.

- (NSMenu *)
menuForEvent: (NSEvent *)theEvent
{
    NSPoint            whereClick;
    CCE_BasicForm * curElem ;
    whereClick = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    curElem = [self getClickedElem:whereClick] ;
    if ( curElem == nil || ( ! [ curElem cceIsSelected]) )
        return ( nil ) ;
    curElem = [ AppDelegate getTheSelectedElem];
    if ( curElem == nil )
        return ( nil ) ;
    else if ( curElem == (CCE_BasicForm*)(-1) )
        return ( mltElemMenu );
    return ( [ curElem contextMenu ]);
}

Risolti dunque i casi semplici (nessun elemento selezionato, troppi elementi selezionati), ho scritto un metodo proprio della classe CCE_BasicForm che producesse il menu più adatto alla situazione. La versione standard del metodo produce il menu standard:

- (NSMenu*)
contextMenu
{
    return ( [ ownerView stdElemMenu ] );
}

figura 05

figura 05

Per i casi specifici, vale a dire CCE_Text, CCE_ElemGroup ed il nuovo CCE_Image, c'è una versione particolare, in cui il menu restituito è rispettivamente txtElemMenu, grpElemMenu e imgElemMenu. Va da sé che ho dovuto aggiungere i menu nel file CoversWin.nib, le variabili d'istanza e i relativi metodi accessor nella classe CoverView. Adesso sono presenti due nuovi menu: il primo menu è attivo quando l'unico elemento selezionato è un gruppo (ed ha quindi in particolare la voce di Ungroup, tolta dal menu che compare quando ci sono più elementi selezionati); il secondo menu è quello relativo ad un elemento CCE_Image. Qui sono presenti due nuovi voci, legate alla gestione delle dimensioni dell'immagine ad all'impostazione del valore di trasparenza.

Dimensioni originali

Per come ho costruito il metodo di disegno, la manipolazione tramite handle di un elemento immagine ridimensiona l'immagine, cambiandone larghezza ed altezza. È facile arrivare in tal modo a rappresentazioni estreme; conviene permettere all'utente il ridimensionamento dell'immagine alle dimensioni naturali dell'immagine stessa, così come desunte dal file che la contiene. Allo scopo ho approntato la voce di menu nel menu contestuale, che chiama un metodo della CoverView:

- (IBAction)
setOriginalSize: (id)sender
{
    CCE_Image    * myImg = (CCE_Image*) [ AppDelegate getTheSelectedElem] ;
    [ myImg setOriginalSize ] ;    
    [ self setNeedsDisplay: YES ];
}

Questo metodo, a sua volta, chiama un metodo proprio dell'elemento, che è normalmente nullo (ancora una volta, per il momento). Nel caso di un elemento CCE_Image, il metodo ha invece alcune istruzioni significative:

- (void)
setOriginalSize
{
    NSRect        aRect ;
    // reimposto le dimensioni dell'immagine a quelle originali
    [ self setLocalSize: imgRepDim ];
    // ricostruisco il percorso
    aRect = NSMakeRect( 0, 0, localSize.width, localSize.height );
    [ self setTheDrawPath: [NSBezierPath bezierPathWithRect: aRect ] ];
    // e ricalcolo le trasformazioni e gli handle, che son cambiati
    [ self calcLocalTranform ];
    [ self buildHdlList ];
}

In pratica, recupero le dimensioni originali, che conservo nella variabile imgRepDim, e le utilizzo per modificare la dimensione corrente dell'elemento. Tutto ciò è facile e veloce, anche se ha qualche controindicazione (visiva, non funzionale) quando l'elemento è stato ridimensionato con insistenza.

Trasparenza

La seconda voce del menu contestuale ad un elemento CCE_Image serve per gestire il valore della trasparenza. Questo è un numero compreso tra Zero (elemento completamente trasparente, quindi invisibile) e Uno (elemento opaco, nessuna trasparenza).

figura 06

figura 06

Utilizzo la pratica consolidata: apro una finestra (che ho costruito tramite Interface Builder nel file TranspWin.nib), dove c'è un campo testo ed uno slider per impostare il valore. La classe controllore associata è TranspWinCtl, che è costruita sulla falsariga delle altre finestre accessorie (come ad esempio RotatewinCtl). Ho aggiunto la finestra TranspWin nella gestione unificata degli aggiornamenti all'interno della classe AppDelegate (metodo updateCoverViewInfo), ed in generale ho ripetuto le solite operazione spesso descritte.

Riporto quindi solo un metodo di questa classe, quello che imposta il nuovo valore di trasparenza:

- (IBAction)
setTransparency:(id)sender
{
    float        newComp = [ sender floatValue ];
    CoverView * cv = [AppDelegate getTheDrawView ] ;
    CCE_BasicForm    * selImg = [AppDelegate getTheSelectedElem ] ;

    [ transpSlider setFloatValue: newComp ] ;
    [ transpText setFloatValue: newComp ] ;
    // non dovrebbe mai succedere...
    if ( cv == nil ) return ;
    if ( selImg == nil || ( selImg == (CCE_BasicForm*)(-1) ) ) return ;
    if ( [selImg isKindOfClass:[CCE_Image class]] )
    {
        CCE_Image    * selez = (CCE_Image*)selImg ;
        [ selez setCompFraction: newComp ] ;
        [ [ selez ownerView] setNeedsDisplay: YES ];
    }
}

L'operazione è svolta quando l'unico elemento selezionato è proprio della classe CCE_Image, e si limita ad impostare la variabile d'istanza e a forzare un rinfresco della finestra.

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).