MaCocoa 044

Capitolo 044 - Ridimensionamento e Selezione

In questo capitolo aggiungo la possibilità di ridimensionare gli elementi disegnati trascinando gli handle, ovvero i quadratini che individuano meglio un elemento selezionato. Già che ci sono, aggiungo la funzione di selezione di più elementi attraverso il mouse.

Faccio tutto da solo leggendo la documentazione.

Primo inserimento: 25 marzo 2004

Nuovi handle

figura 01

figura 01

Ho già parlato degli handle come di quei quadratini che contornano e individuano un oggetto selezionato. Per prima cosa, e per meglio poi rintracciare un clic all'interno di uno di questi handle, riscrivo la parte che li riguarda. Semplicemente, alla costruzione di un oggetto, costruisco anche gli handle corrispondenti, e salvo i punti individuati in un vettore; come effetto collaterale, dovrei ridisegnare la vista più velocemente, dal momento che non devono essere calcolati ogni volta.

Inserisco quindi un vettore di otto NSPoint all'interno della struttura dati di un elemento grafico, completato dal numero effettivo di handle presenti.

    int            numOfHdl ;
    NSPoint        hdlList[8] ;

Il numero massimo è otto: sono i quattro spigoli ed i quattro punto di mezzo dei lati del rettangolo che circoscrive l'elemento. Nel caso di un segmento, i punti saranno solo due (i due estremi).

La classe CCE_BasicForm prevede un metodo di default adatto alla maggior parte dei casi:

- (void) buildHdlList
{
        // gli handle sono genericamente ai quattro estremi del
        // rettangolo limite e nei quattro punti mediani
        hdlList[0] = NSMakePoint( elemLimit.origin.x, elemLimit.origin.y) ;
        hdlList[1] = NSMakePoint( elemLimit.origin.x, elemLimit.origin.y+ elemLimit.size.height) ;
        hdlList[2] = NSMakePoint( elemLimit.origin.x + elemLimit.size.width, elemLimit.origin.y+ elemLimit.size.height) ;
        hdlList[3] = NSMakePoint( elemLimit.origin.x + elemLimit.size.width, elemLimit.origin.y) ;
        hdlList[4].x = (hdlList[0].x + hdlList[1].x ) * 0.5 ;
        hdlList[4].y = (hdlList[0].y + hdlList[1].y ) * 0.5 ;
        hdlList[5].x = (hdlList[2].x + hdlList[1].x ) * 0.5 ;
        hdlList[5].y = (hdlList[2].y + hdlList[1].y ) * 0.5 ;
        hdlList[6].x = (hdlList[2].x + hdlList[3].x ) * 0.5 ;
        hdlList[6].y = (hdlList[2].y + hdlList[3].y ) * 0.5 ;
        hdlList[7].x = (hdlList[0].x + hdlList[3].x ) * 0.5 ;
        hdlList[7].y = (hdlList[0].y + hdlList[3].y ) * 0.5 ;
}

Qui si considera il rettangolo: da questo sono presi i quattro vertici ed i quattro punti di mezzo dei lati. Semplici considerazioni sulle coordinate cartesiane danno ragione delle espressioni.

Questo metodo può essere sovrascritto da ogni classe discedente per migliorare le proprie esigenze. È il caso ad esempio di CCE_Line, dove diventa:

- (void) buildHdlList
{
        // gli handle sono genericamente ai quattro estremi del
        // rettangolo limite e nei quattro punti mediani
        hdlList[0] = startPoint ;
        hdlList[1] = endPoint ;
}

Qui le operazioni sono ancora più semplici. I due handle sono gli estremi del segmento.

Ora, bisogna fare in modo che questo metodo sia chiamato ogni volta che cambiano le dimensioni di un oggetto. Non ho trovato soluzioni migliori che invocare il metodo all'interno del metodo setElemLimit, ovvero all'interno del metodo accessor che permette di impostare il rettangolo che circoscrive l'elemento.

- (void)setElemLimit:(NSRect)newElemLimit {
        elemLimit = newElemLimit;
        [ self buildHdlList ];
}

Poiché ogni metodo di inizializzazione chiama prima o poi questo accessor, ecco che automaticamente sono costruiti tutti gli handle. Purtroppo rimane fuori l'inizializzazione della variabile numOfHdl, che va fatta caso per caso. In realtà, al momento, sono solo due i casi. Il primo nel metodo init di CCE_BasicForm, che assegna il valore di Otto (perché di default tutto sta dentro ad un rettangolo). Il secondo nel metodo init di CCE_Line, il quale, dopo aver invocato il metodo init di super (e quindi di CCE_BasicForm), cambia il valore di default a due.

Per disegnare gli handle, ho poi predisposto un nuovo metodo

- (void) drawHandles
{
    if ( cceIsSelected )
    {
        int i;
        // disegno i manici per la manipolazione
        for ( i = 0 ; i < numOfHdl ; i ++ )
            ccex_drawHandleAt( hdlList[i] );
    }
}

Questo metodo disegna gli handle solo se l'elemento è considerato selezionato. In tal caso, sfrutta la buona vecchia funzione ccex_drawHandleAt per disegnare effettivamente lo handle. Questo metodo deve essere invocato all'interno dei metodi drawElement di ogni singolo elemento, sostituendo la parte corrispondente presente nella precedente realizzazione. Ad esempio, per un rettangolo il metodo è:

- (void) drawElement: (NSRect) inRect
{
    if ( cceIsStroked )
    {
        // imposto il colore del contorno
        [ cceLineColor set ];
        // imposto lo spesosre del contorno
        [ theRect setLineWidth: [ self cceLineWidth ]];
        // disegno il contorno
        [ theRect stroke ];
    }
    // se il rettangolo e' pieno
    if ( cceIsFilled )
    {
        // imposto il colore del pieno
        [ cceFillColor set ];
        // riempi tutto
        [ theRect fill ];
    }
    [ self drawHandles ] ;
}

Fino a questo punto, nulla di nuovo; ho solo riorganizzato le cose.

Selezionare un handle

Il vantaggio di tenere memorizzate le coordinate degli handle viene comodo quando si tratta di capire dove avviene un clic del mouse. Per prima cosa, costruisco un metodo della classe CCE_BasicForm in grado di capire se il clic del mouse è avvenuto sopra un handle:

- (short)
clicIntoHandle: (NSPoint) point
{
    int i;
    // disegno i manici per la manipolazione
    for ( i = 0 ; i < numOfHdl ; i ++ )
    {
        // se il punto designato e lo handle sono abbastanza vicini
        float a1 = hdlList[i].x - point.x ;
        float a2 = hdlList[i].y - point.y ;
        if ( a1 * a1 + a2 * a2 <= 4 )
            return ( i );
    }
    return ( -1 ) ;
}

Il metodo è piuttosto banalotto. Esamina ordinatamente gli handle, e verifica se i due punti (quello dello handle e quello passato come argomento, sarà il punto dove il mouse ha fatto clic) sono abbastanza vicini (per chi mastica di geometria, il metodo verifica che la distanza tra i due punti sia inferiore a due pixel). Il metodo restituisce l'indice dello handle cliccato, oppure -1 per indicare che il clic è andato altrove.

Uso questo metodo per modificare l'esame del clic del mouse:

- (CCE_BasicForm *)
getClickedElem:(NSPoint)point
{
    int                i , numElem ;
    CCE_BasicForm * elem ;
    NSMutableArray        * listaelem ;
    // esamino tutti gli elemento
    listaelem = [ theElements elemArray ];
    numElem = [ listaelem count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ listaelem objectAtIndex: i ];
        // e controllo se il click gli appartiene
        if ( [ elem checkClickOnMe: point ])
        {
            // ho finito, esco dal ciclo
            break;
        }
        // controllo un eventuale click su handle
        if ( [ elem clicIntoHandle: point] != -1 )
            break ;
    }
    // potrei non avere trovato nulla
    if ( i == numElem )
        return ( nil );
    // se arrivo qui, ho trovato un elemento
    return ( elem );
}

In pratica, considero un clic sopra un elemento quando il mouse si trova proprio sopra questo elemento, oppure se il mouse è sopra uno degli handle (che, tecnicamente, sono anche un po' al di fuori del rettangolo che contiene l'elemento).

Ancora una volta, fino a qui non ci sono grosse novità. Comincia a muoversi qualcosa all'interno del metodo che tratta il movimento degli elementi. Questa volta, se il clic è sopra uno degli handle, piuttosto che muovere un elemento, lo ridimensiono.

- (void)
handleMoveElem: (NSEvent *)theEvent
    inView:        (CoverView *)view
{
    // recupero la coordinata locale
    NSPoint point1 = [view convertPoint:[theEvent locationInWindow] fromView:nil];
    NSPoint point2 ;
    short hdlIdx = [ self clicIntoHandle: point1 ] ;
    // potrebbe aver fatto clic su uno degli hangle
    if ( hdlIdx == -1 )
    {
        // no, clic in zona generica
        // 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) ;
    }
    else
    {
        // clic su uno degli angle, ridimensiono
        [ self handleResize: theEvent inView: view movingHandle: hdlIdx ];
    }
}

Si tratta del metodo già visto in precedenza, al quale è stato aggiunto a contorno un controllo sulla locazione del clic. Se il clic è su uno degli handle, allora il controllo passa al metodo handleResize:....

E qui le cose cominciano a complicarsi.

Come sono abituato a ridimensionare un elemento (diciamo un rettangolo, per fissare le idee) attraverso gli handle? Se piglio uno degli handle sugli spigoli, voglio modificare l'intero rettangolo. Allora tengo fisso lo spigolo opposto (rispetto al centro del rettangolo) e muovo a piacimento lo spigolo sul quale ho fatto clic. Quando rilascio il mouse, ho un nuovo rettangolo, determinato dal punto fisso e dal punto di rilascio.

Se invece piglio uno degli handle in mezzo ai lati, voglio modificare solo una dimensione del rettangolo. Se seleziono il punto di mezzo di uno dei lati verticali, ridimensiono la larghezza del rettangolo, ma l'altezza del rettangolo deve rimanere fissa. Viceversa, selezionando uno degli handle nei lati orizzontali, si modifica l'altezza, ma non la larghezza.

Riassumo queste due operazioni, che sembrano piuttosto diverse tra loro, in una cornice unificante. Per ogni ridimensionamento, individuo un punto fisso, come un vertice che rimane comunque fermo nel processo di manipolazione. Individuo poi un vertice mobile, come il vertice che poi, alla fine delle operazioni, concorre col punto fermo alla determinazione del rettangolo. Le coordinate del punto mobile possono cambiare contemporaneamente per le ascisse e per le ordinate (è il primo caso, in cui lo handle in movimento è uno dei vertici), oppure solo una tra ascissa o ordinata (il secondo caso, in cui lo handle selezionato è uno dei punti di mezzo dei lati). Identifico ogni handle con l'indice che questo handle ha all'interno del vettore hdlList.

figura 02

figura 02

Bene, basandomi su quanto detto, e guardando con attenzione la figura, costruisco tre vettori; il primo indica quale handle rimane fisso nella manipolazione, il secondo quale si muove, ed il terzo vettore indica con un codice quale coordinata si muove. Supponiamo che il clic avvenga sullo handle numero 3 (il punto in alto a destra). Allora, il punto fisso è il vertice numero 1, lo handle mobile è proprio quello numero 3, e nel ridimensionamento cambiano sia l'ascissa sia l'ordinata. Questo è il motivo perchè il quarto elemento di ogni vettore (quindi, quello di indice 3) è rispettivamente 1, 3 e 3 (codice che indica ascissa e ordinata).

    static short fixedHandle[8] = { 2, 3, 0, 1, 2, 0, 0, 2 };
    static short movingHandle[8] = { 0, 1, 2, 3, 0, 2, 2, 0 };
    static short movingPoints[8] = { 3, 3, 3, 3, 2, 1, 2, 1 };

Le cose si complicano quando il clic è sopra uno degli handle di mezzo. Supponiamo clic sullo handle 7. Posso indifferentemente scegliere il vertice 1 o 2: entrambi non cambieranno coordinate. Come handle mobile, posso scegliere ancora i vertici 0 o 3; in ogni caso, entrambi variano la coordinata y (che ho indicato col codice 1). Quindi, l'ottavo elemento dei vettori (indice numero 7) sono rispettivamente 2, 0 e 1.

Capito il meccanismo, si riempiono facile le altre posizioni del vettore.

In effetti, tutto questo discorso serve proprio ad individuare tre variabili:

    NSPoint fixedPoint = hdlList[ fixedHandle[ hdlIdx ] ];
    NSPoint movingPoint = hdlList[ movingHandle[hdlIdx] ];
    short movingCoord = movingPoints[ hdlIdx ];

dove appunto conservare le coordinate del punto fisso, le coordinate del punto variabile ed il codice che dice quali coordinate devono variare. A questo punto, costruisco il solito loop che aspetta il rilascio del mouse:

    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         point = [view convertPoint:[theEvent locationInWindow] fromView:nil];         ...         // faccio qualcosa col punto         ...         // forzo un ridisegno della finestra         [ view setNeedsDisplay: YES ];     }     while ( [theEvent type] != NSLeftMouseUp) ;

Cosa fare del nuovo punto determinato dall'utente col suo girovagare? Devo costruire un nuovo rettangolo: per farlo, devo determinare il secondo punto. E qui le operazioni dipendono da quale coordinata è in movimento. Se varia la coordinata x, il nuovo punto è dato dalla coordinata x del punto nuovo, ma la coordinata y va pescata dal punto movingPoint. Se varia la coordinata y, le due coordinate si scambiano di ruolo. Infine, se entrambe le coordinate devono variare, utilizzo entrambe le nuove coordinate (ed in questo caso il punto movingPoint serve a nulla...). Ecco quindi il metodo nella sua completezza.

- (BOOL)
handleResize: (NSEvent *)theEvent
    inView:        (CoverView *)view
    movingHandle: (short) hdlIdx
{
    // indice dello handle che tengo fisso
    static short fixedHandle[8] = { 2, 3, 0, 1, 2, 0, 0, 2 };
    // indice dello handle che devo muovere (opposto)
    static short movingHandle[8] = { 2, 3, 0, 1, 0, 2, 2, 0 };
    // identificatore delle coordinate che posso muovere:
    // con 3, x e y; con 2, solo x, con 1 solo y
    static short movingPoints[8] = { 3, 3, 3, 3, 2, 1, 2, 1 };

    short movingCoord = movingPoints[ hdlIdx ];
    NSPoint fixedPoint = hdlList[ fixedHandle[ hdlIdx ] ];
    NSPoint movingPoint = hdlList[ movingHandle[hdlIdx] ];
    NSPoint point ;
    // continuo a ciclare in attesa di un mouseup
    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
        point = [view convertPoint:[theEvent locationInWindow] fromView:nil];
        // aggiusto l'aspetto dell'oggetto
        switch ( movingCoord ) {
        case 1 :
            [ self buildMeWithStartPt: fixedPoint andPt: NSMakePoint( movingPoint.x, point.y) ] ;
            break ;            
        case 2 :
            [ self buildMeWithStartPt: fixedPoint andPt: NSMakePoint( point.x, movingPoint.y) ] ;
            break ;
        case 3 :    // muovo entrambe le coordinate
            [ self buildMeWithStartPt: fixedPoint andPt: point ] ;
            break ;
        }
        // forzo un ridisegno della finestra
        [ view setNeedsDisplay: YES ];
    }
    while ( [theEvent type] != NSLeftMouseUp) ;
    // arrivato qui, ho finito il tutto
    return YES;
}

Questo metodo va bene per tutti gli elementi univocamente determinati attraverso il rettangolo che li delimita. Quindi funziona sia per CCE_Rect che per CCE_Circle (e, per il momento, anche per CCE_Text senza colpo ferire. Addirittura, stabilisco che questo metodo è la realizzazione di default; gli oggetti che si comportano in maniera diversa, devono riscriverlo.

È questo, ovviamente, il caso di un segmento. Il meccanismo è più semplice (uno dei due handle è sempre fisso, l'altro si muove in entrambe le direzioni), per cui il codice dovrebbe essere autoesplicativo.

- (BOOL)
handleResize: (NSEvent *)theEvent
    inView:        (CoverView *)view
    movingHandle: (short) hdlIdx
{    
    NSPoint fixedPoint = ( hdlIdx == 0) ? hdlList[ 1 ] : hdlList[ 0 ];
    NSPoint point ;
    // continuo a ciclare in attesa di un mouseup
    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
        point = [view convertPoint:[theEvent locationInWindow] fromView:nil];
        // aggiusto l'aspetto dell'oggetto
        [ self buildMeWithStartPt: fixedPoint andPt: point ] ;
        // forzo un ridisegno della finestra
        [ view setNeedsDisplay: YES ];
    }
    while ( [theEvent type] != NSLeftMouseUp) ;
    // arrivato qui, ho finito il tutto
    return YES;
}

A questo punto, ho messo in piedi un meccanismo per modificare le dimensioni di ogni oggetto presente all'interno di una coverView.

Selezioni multiple

Colto dalla foga, ho anche realizzato un meccanismo per effettuare selezioni multiple (non che c'entri molto col resto, ma insomma...). Ho utilizzato lo stile classico: un utente col mouse fa clic e poi, mantenendolo premuto, racchiude con un rettangolo tutti gli elementi che intende selezionare. La cosa si presenta facile, dal momento che riciclo molti concetti già utilizzati. Intanto, devo visualizzare un rettangolo effimero all'interno della coverView. Opero come col rettangolo che ho utilizzato per dare riscontro all'utente quando costruisce un elmento rettangolare: definisco una nuova variabile d'istanza della classe CoverView, normalmente nullo, ma che diventa effettivo quando è in corso l'opera di selezione. Ho chiamato la variabile

    CCE_Rect            * curSelectRect ;

ed ho definito i soliti metodi accessor. Ho poi modificato il metodo handleMouseClick perché nella parte in cui il clic è in terra di nessuno invochi invece un metodo apposta per cominciare con la selezione multipla.

- (void)
handleMouseClick: (NSEvent *)theEvent
{
    ...
    // se c'e' un elemento sotto il mouse
    if ( curElem )
    {
        ...
    }
    else
    {
        NSRect selRect ;
        // comincio la selezione multipla
        selRect = [ self handleRectSelect: theEvent ];
        [ theElements selectMeIfInRect: selRect ];
    }
    ...
}

Le due operazioni in sequenza sono: la determinazione (ed il disegno, e la gestione dell'interattività) del rettangolo di selezione, e poi la selezione di tutti gli elementi che si trovano all'interno del rettangolo individuato.

Per determinare il rettangolo, il metodo è il seguente.

- (NSRect)
handleRectSelect: (NSEvent *)theEvent
{
    NSRect        selRect = NSMakeRect(1, 1, 0, 0);
    // costruisco l'elemento grafico
    CCE_Rect * curElem = [[ [CCE_Rect alloc ] init] autorelease ] ;
    // metto a posto un di attributi base dell'elemento
    [ curElem setCceLineWidth: 0.0F ];
    [ curElem setCceLineColor: [NSColor blackColor] ];
    [ curElem setCceFillColor: [NSColor lightGrayColor] ];
    [ curElem setCceIsFilled: NO ];
    [ curElem setCceIsStroked: YES ];
    // deseleziono tutto
    [ theElements setCceIsSelected: NO ] ;
    // dico che l'elemento che sto disegnando e' selezionato
    [ curElem setCceIsSelected: NO ];
    [ self setCurSelectRect: curElem ] ;
    // gestisco la costruzione col mouse
    [curElem handleCreation: theEvent inView: self ] ;
    selRect = [ curElem elemLimit] ;
    [ self setCurSelectRect: nil ] ;
    return ( selRect );
}

Qui si procede come per la normale costruzione di un rettangolo. Si costruisce l'oggetto, se ne predispongono alcuni parametri, si assegna il rettangolo alla variabile d'istanza creata apposta, poi si gestisce il mouse con metodo handleCreation: (che è sempre quello utilizzato per gli elementi normali). Alla fine, si rimette a nil la variabile, e si restituisce il rettangolo individuato.

C'è da modificare il metodo di disegno della coverView

- (void)drawRect:(NSRect)rect
{
    // sfondo bianco
    [[ NSColor whiteColor] set ];
    NSRectFill( rect );
    // poi dico di disegnarsi a tutti gli elementi
    [ [self theElements] drawElement: rect ];
    // e poi all'elemento in corso
    if ( curCreatingElem )
    {
        [ curCreatingElem drawElement: rect ] ;
    }
    if ( curSelectRect )
    {
        NSBezierPath    * curPath = [ curSelectRect theRect ];
        float            strokePattern[2] ={ 5, 5 } ;
        [ curPath setLineDash: strokePattern count: 2 phase: 0.0];
        [ curSelectRect setTheRect: curPath ];
        [ curSelectRect drawElement: rect ] ;
    }
}

figura 03

figura 03

Ho aggiunto, oltre al normale disegno del rettangolo, una migliore caratterizzazione del rettangolo, imponendo il bordo tratteggiato. Consultando la documentazione della classe NSBezierPath ho notato appunto il metodo setLineDash:... che permette di impostare il tratteggio attraverso un vettore di numeri; questi numeri determinano la parte piena e la parte vuota del tratteggio. Nel caso, c'è un tratto pìeno di cinque pixel seguito da un uguale tratto vuoto.

Infine, non rimane che dettagliare il metodo selectMeIfInRect , invero piuttosto semplice, ma che ha bisogno di tre diverse realizzazioni, quella di default (per gli elementi rettangolari), per la classe CCE_Line e per la classe CCE_ElemGroup. Nel primo caso, si usa la banale funzione NSContainsRect per vedere se il rettangolo dell'elemento è incorporato nel rettangolo di selezione; nel secondo caso si verifica che entrambi i punti estremi risiedono nel rettangolo di selezione; nel terzo caso, infine, occorre chiedere separatamete a tutti gli elementi del gruppo se sono o meno includi all'interno del rettangolo di selezione.

- (void) selectMeIfInRect: (NSRect) selRect
{
    NSRect mioRect = [self elemLimit] ;
    if (NSContainsRect( selRect, mioRect ) )
            [ self setCceIsSelected: YES ];
    else    [ self setCceIsSelected: NO ];
}
...
- (void)
selectMeIfInRect: (NSRect) selRect
{
    if ( NSPointInRect(startPoint, selRect) && NSPointInRect(endPoint, selRect) )
            [ self setCceIsSelected: YES ];
    else    [ self setCceIsSelected: NO ];
}
...
- (void) selectMeIfInRect: (NSRect) selRect
{
    int        i , numElem ;
    CCE_BasicForm * elem ;
    // ciclo su tutti gli elementi del gruppo
    numElem = [ elemArray count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        [ elem selectMeIfInRect: selRect ] ;
    }
}

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