MaCocoa 043

Capitolo 043 - Drag Zoom Zoom

In questo capitolo aggiungo la possibilità di spostare gli elementi grafici e un meccanismo per ingrandire e rimpicciolire la coverView.

Ancora una volta, piglio esempio da Sketch.

Primo inserimento: 26 febbraio 2004

Spostare elementi

Dopo aver scritto le istruzioni per creare elementi e per selezionarli, il passo successivo nella manipolazione diretta degli elementi col mouse è certamente la possibilità di spostare gli elementi come si preferisce. Per fare questo occorre modificare un po' il metodo principale di manipolazione degli elementi all'interno della coverView. Riscrivo parzialmente il metodo handleMouseClick:, che interviene quando l'utente fa clic senza l'intenzione di creare nuovi elementi.

- (void)
handleMouseClick: (NSEvent *)theEvent
{
    NSPoint whereClick;
    CCE_BasicForm * curElem = nil;
    // vado a vedere dove e' avvenuto il click
    whereClick = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    // niente niente l'utente vuole selezionare un elemento
    curElem = [self getClickedElem:whereClick];
    // se c'e' un elemento sotto il mouse
    if ( curElem )
    {
        // abbiamo qualcosa sotto il mouse
        if ( ! [ curElem cceIsSelected ] )
        {
            // devo deselezionare tutto
            [ theElements setCceIsSelected: NO ] ;
            // e poi seleziono questo elemento
            [ curElem setCceIsSelected: YES ];
            // ridisegno tutto che la vista e' cambiata...
            [ self setNeedsDisplay: YES ];
        }
        // provo a vedere se e' il caso di muoverlo
        [ curElem handleMoveElem: theEvent inView: self];
    }
    else
    {
        // c'e' nulla sotto il mouse, deseleziono tutto
        [ theElements setCceIsSelected: NO ] ;
    }
    // ridisegno tutto, in ogni caso
    [ self setNeedsDisplay: YES ];
}

Le tre possibilità a questo punto diventano: faccio clic in zona di nessuno, ed allora deseleziono tutto; faccio clic sopra un elemento attualmente deselezionato, allora lo seleziono; se invece l'elemento è selezionato, mi appresto a muoverlo. Tuttavia, risulta naturale entrare nella gestione del movimento anche in seguito al clic su di elemento non selezionato (ovvero, il secondo caso prospettato), per permettere un movimento fluido all'utente di selezione e spostamento dell'elemento.

Ancora una volta, il meccanismo di gestione del movimento è diviso in più parti, lasciando comunque all'elemento stesso la gestione del movimento.

La prima parte del meccanismo è comune a tutti gli elementi: si tratta quindi di un metodo della classe CCE_BasicForm, che gestisce gli eventi di spostamento del mouse.

- (void)
handleMoveElem: (NSEvent *)theEvent
    inView:        (CoverView *)view
{
    // recupero la coordinata locale
    NSPoint point1 = [view convertPoint:[theEvent locationInWindow] fromView:nil];
    NSPoint point2 ;
    // ciclo piu' o meno lungo di attesa eventi
    do {
        // recupero il prossimo evento, che sia un movimento del mouse
        // oppure un mouseup (che finisce il tutto)
        theEvent = [[view window] nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
        // recupero la coordinata locale
        point2 = [view convertPoint:[theEvent locationInWindow] fromView:nil];
        // aggiusto l'aspetto dell'oggetto
        [ self shiftMeFrom: point1 to: point2 ];
        // il nuovo punto di partenza
        point1 = point2 ;
        // forzo un ridisegno della finestra
        [ view setNeedsDisplay: YES ];
    }
    while ( [theEvent type] != NSLeftMouseUp) ;
}

Si piglia e si mette da parte un primo punto, che all'inizio è quello dove l'utente ha fatto mouseDown. Poi si apre un bel ciclo in attesa del rilascio del mouse. Nel frattempo, si rintracciano tutti i movimenti (ecco il motivo della presenza della costante NSLeftMouseDraggedMask). Recupero il nuovo punto corrente, ed eseguo lo spostamento dell'elemento selezionato dal primo punto a questo secondo punto. Una volta eseguito questo spostamento, dico che il nuovo punto diventa il nuovo punto di partenza, e ricomincio il ciclo (non prima di aver forzato il ridisegno della coverView).

Ancora una volta, si sfrutta il polimorfismo degli elementi per eseguire lo spostamento in maniera propria per ciascun elemento, semplicemente realizzando in maniera opportuna il metodo

- (void) shiftMeFrom: (NSPoint) pt1 to: (NSPoint) pt2 ;

per ogni elemento da disegnare. Come al solito, mi limito a mostrare solo un paio di realizzazioni: la prima è per l'elemento linea CCE_Line:

- (void)
shiftMeFrom: (NSPoint) pt1 to: (NSPoint) pt2
{
    float deltax = pt2.x - pt1.x ;
    float deltay = pt2.y - pt1.y ;
    NSPoint p1 = NSMakePoint( startPoint.x + deltax, startPoint.y + deltay);
    NSPoint p2 = NSMakePoint( endPoint.x + deltax, endPoint.y + deltay);
    [ self buildMeWithStartPt: p1 andPt: p2 ];
}

Qui riutilizzo il comodo metodo di costruzione buildMeWithStartPt:andPt: dopo aver calcolato i due nuovi punti limite della linea.

Per il rettangolo invece il metodo più stupido che mi è venuto in mente è il seguente:

- (void)
shiftMeFrom: (NSPoint) pt1 to: (NSPoint) pt2
{
    float deltax = pt2.x - pt1.x ;
    float deltay = pt2.y - pt1.y ;
    // sposto il rettangolo di base
    [ self setElemLimit: NSOffsetRect(elemLimit, deltax, deltay)];
    [ self buildMeWithStartPt: NSMakePoint(elemLimit.origin.x, elemLimit.origin.y)
            andPt: NSMakePoint( elemLimit.origin.x + elemLimit.size.width,
                                elemLimit.origin.y + elemLimit.size.height) ];
}

dove sposto il rettangolo limite secondo quanto indicato, e poi ricostruisco il rettangolo con i nuovi punti limite.

Facili Zoom

figura 02

figura 02

Pensavo di dover penare come pochi per realizzare un meccanismo di zoom (in ingrandimento, ma anche in riduzione). In realtà è una cosa facilissima ed assai trasparente per ogni vista. In altre parole, gli oggetti NSView presentano incorporati meccanismi di zoom d'uso immediato.

Ma comincio con il preidsporre l'interfaccia utente per fare lo zoom. Mi ispiro un po' a quello che fa l'applicazione iPhoto. Predispongo cioè in basso a destra della finestra uno slider con due piccole immagini ai lati. Uso lo slider come un controllo per dare il fattore di zoom, e le immagini ai lati per fissare lo zoom massimo e lo zoom minimo.

Per fare tutto questo, ho predisposto la classe CoverWinCtl affinché presentasse un nuovo outlet e qualche metodo aggiuntivo da utilizzare per il meccanismo target/action:

@interface CoverWinCtl : NSWindowController
{
    IBOutlet CoverView * cvrView;
    IBOutlet NSTableView * volList;
    IBOutlet NSSlider * zoomSlider;
}
- (void) setMaxSlider: (id)sender ;
- (void) setMinslider: (id)sender ;
- (void) updateZoom: (id)sender ;
...
@end

L'outlet zoomSlider serve per impostare da programma il valore dello zoom, o semplicemente per leggerlo. I primi due metodi sono chiamati rispettivamente dal pulsante a destra (per fissare il massimo fattore di zoom) e a sinistra (per fissare il minimo fattore di zoom, o meglio il massimo fattore di rimpicciolimento) dello slider; il terzo metodo è quello chiamato dallo slider quando l'utente ci sta pasticciando sopra.

I primi due metodi sono di immediata realizzazione:

- (void)
setMaxSlider: (id)sender
{
    [ zoomSlider setFloatValue: [ zoomSlider maxValue]];
    [ self updateZoom: sender];
}
- (void)
setMinslider: (id)sender
{
    [ zoomSlider setFloatValue: [ zoomSlider minValue]];
    [ self updateZoom: sender];
}

Dopo aver impostato il valore corrente dello slider, chiama il terzo metodo simulando in pratica un movimento dello slider da parte dell'utente.

Il terzo metodo è stringato ma lungo da spiegare.

- (void)
updateZoom: (id)sender
{
    float        czl, rl ;
    czl = [ zoomSlider floatValue ] ;
    if ( czl >= 0 )
            rl = czl + 1;
    else    rl = 1 / (-czl+1) ;
    [cvrView setZoom: rl ];
    [ zoomValue setStringValue: displayZoomFactor(rl) ];
    [cvrView setNeedsDisplay: YES ];
}

figura 01

figura 01

Comincio da come è costruito lo slider. Questo restituisce numeri compresi tra -4 e +4, ma solo se corrispondono ad una delle 17 suddivisioni che ho impostato. In altre parole, restituisce un qualsiasi valore compreso tra -4 e +4 a passi di 0.5. Dico che valori negativi dello slider sono fattori di rimpicciolimento, mentre valori positivi sono ingrandimenti. Il valore centrale di Zero è il normale fattore di scala di uno ad uno. A questo punto dico che il valore 1 dello slider corrisponde ad un fattore di scala di 2, e così via, fino ad arrivare al valore 4 dello slider che ingrandisce di cinque volte la coverView. D'altra parte, un valore di -1 significa che la vista è ridotta alla metà, e via così fino al valore di -4 che rimpicciolisce la vista ad un quinto dell'originale. Il fattore di scale è quindi il valore dello slider più Uno se tale valore è negativo, mentre è dato dall'espressione

1 / (1 - [valore dello slider])

nel caso in cui il valore sia negativo (faccio una prova: se il valore dello slider è -2, il fattore di scala è 1/(1-(-2))=1/3 ).

Tutto questo pasticcio per impostare un fattore di zoom massimo di cinque (cinque volte più grande) e un fattore di riduzione pure pari a cinque (un quinto delle dimensioni originarie).

Tuttavia, il lavoro deve ancora essere fatto. È infatti compito del metodo setZoom:, che nella sua innocenza è di una potenza estrema.

- (void)
setZoom:(float)scaleFactor
{
    NSRect frame = [self frame];
    NSRect bounds = [self bounds];
    frame.size.width = bounds.size.width * scaleFactor;
    frame.size.height = bounds.size.height * scaleFactor;
    [self setFrameSize: frame.size];    
    [self setBoundsSize: bounds.size];
}

figura 03

figura 03

Ora, confesso che non ho proprio capito tutto per bene ciò che succede.

Provo a descriverlo, sperando nel farlo di capirci qualcosa di più. Comincio dai concetti di frame e di bounds. Ho già scritto che frame riporta il rettangolo della view nelle coordinate della sua superview. In particolare, le dimensioni orizzontali e verticali indicano quanto si sviluppa nel piano la view stessa. Il rettangolo bounds invece indica lo stesso rettangolo, ma visto all'interno delle coordinate della view. Lo guardo da un'altra parte. Il rettangolo bounds mi da le dimensioni della vista, che poi è piazzata nella superview seguendo il rettangolo frame. Ora, normalmente, senza troppe giravolte all'interno della superview, le dimensioni width e height di bounds e frame sono le stesse.

Per fare lo zoom di una view, aumento o riduco le dimensioni della view stessa. Piglio allora la dimensione orizzontale e verticale e le moltiplico per il fattore di scala a cui voglio ridimensionare il tutto (terza e quarta riga del metodo precedente). Lascio inalterata la posizione della view, visto che è ancorata in alto a sinistra (e mi va bene così). Adesso impongo il nuovo rettangolo frame con la quinta linea del metodo.

figura 04

figura 04

Questa istruzione modifica anche il rettangolo bounds (potete verificarlo con il debugger, oppure facendo qualche stampa con la comoda NSLog), che l'istruzione successiva riporta alla dimensione standard. E questo è tutto. Come mai funziona? Non ho le idee ben chiare. Vado a leggere il manuale e trovo alcune delucidazioni all'interno della documentazione relativa a setBoundsSize. Il metodo impone la dimensione del rettangolo bounds, "modificando il sistema di coordinate relativamente al rettangolo frame".

Interpreto la cosa in questo modo: il rettangolo bounds dà le dimensioni effettive della vista. Tuttavia, questo rettangolo è rappresentato sullo schermo esattamente all'interno del rettangolo frame. Se quindi il rettangolo frame è più piccolo di bounds, ho una rappresentazione su scala ridotta, se frame è più grande di bounds, ho una rappresentazione ingrandita.

Le cose sono più chiare adesso? A me, no.

Però funziona. E tanto basta.

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