MaCocoa 047

Capitolo 047 - Grigliate

Un capitolo facile e breve che discute di come aggiungere una griglia per facilitare il posizionamento degli oggetti.

Come al solito, documentazione Apple.

Primo inserimento: 5 maggio 2004

Griglia

Ancora una volta, ricorro alla suggestione dei soliti programmi di disegno per introdurre una nuova funzione. Voglio che all'interno della coverView sia possibile posizionare gli elementi sui punti di griglia. Ovviamente l'utente deve poter decidere se utilizzare o meno la griglia, la spaziatura della griglia, il colore delle linee guida... Riassumo il tutto attraverso quattro variabili d'istanza (ed i relativi metodi accessor), che pongo all'interno della classe CoverView. In questo modo ogni coverView ha la sua griglia personalizzata.

     // parametri per la visualizzazione della griglia
    int                 gridSpacing ;
    BOOL                showGrid ;
    BOOL                snapToGrid ;
    NSColor         * gridLineColor ;

C'è un intero che rappresenta la spaziatura della griglia; è intero perché rappresenta pixel. Le due variabili booleane successive indicano rispettivamente se la griglia è da visualizzare o meno, e se il tracciamento degli elementi all'interno della coverView debba avvenire o meno solo sui punti della griglia. Oltre al tracciamento, la griglia regola anche lo spostamento ed il ridimensionamento degli elementi. L'ultima variabile tiene conto del colore con il quale è disegnata la griglia.

All'interno del metodo drawRect: della classe CoverView aggiungo poi la chiamata ad un metodo specifico per il disegno della griglia

// il disegno della vista
- (void)drawRect:(NSRect)rect
{
    // sfondo bianco
    [[ NSColor whiteColor] set ];
    NSRectFill( rect );
    // se c'e' la griglia, la disegno
    if ( showGrid )
        [ self drawGrid: rect ];
    // poi dico di disegnarsi a tutti gli elementi
    [ [self theElements] drawElement: rect ];
...

}

Il disegno della griglia è una delle prime operazioni di disegno (dopo il riempimento della finestra col colore di sfondo) proprio per evitare che il disegno della griglia si sovrapponga a quello degli elementi.

Il metodo drawGrid: è piuttosto semplice dal punto di vista concettuale, e si complica nei dettagli.

Ma prima di cominciare, due utili funzioni.

NSPoint
getNearestSnapPoint( NSPoint point, CoverView * view)
{
    BOOL    gridOn = [ view snapToGrid ];
    
    if ( gridOn == NO )
        return ( point );
    return ( getNearestGridPoint(point, view) );
}

NSPoint
getNearestGridPoint( NSPoint point, CoverView * view)
{
    int        gridSpacing = [ view gridSpacing ];
    NSPoint    retPt ;
    
    retPt.x = round(point.x / (float) gridSpacing) * gridSpacing ;
    retPt.y = round(point.y / (float) gridSpacing) * gridSpacing ;
    return ( retPt );
}

Scopo delle funzioni è di trasformare una coppia di coordinate generiche (un punto) in una coordinata sulla griglia. La prima funzione serve nei casi in cui debbo trasformare un punto per ottenere una locazione di snap, ovvero un punto dove cominciare o terminare una operazione di disegno o di spostamento. Il punto rimane immodificato se la variabile snapToGrid è NO, altrimenti si trasforma in accordo alla seconda funzione.

Qui c'è un giochino matematico per calcolare il punto sulla griglia: prima divido la coordinata per la spaziatura della griglia. Arrotondo il risultato all'intero più vicino, usando la funzione standard C round(.), poi moltiplico nuovamente per la spaziatura. In questo modo ottengo la coordinata intera, multipla della spaziatura, più vicina alla coordinata di partenza.

Disegno della griglia

Per disegnare la griglia, prevedo una variabile NSBezierPath che tenga conto delle linee veriticali ed orizzontali. Per evitare di coprire il mondo intero col disegno di una griglia, lo faccio solo all'interno del rettangolo visualizzato. Trovo quindi le due coppie di coordinate sulla griglia più vicine a due vertici opposti del rettangolo. Queste saranno le coordinate di partenza.

- (void)
drawGrid:(NSRect)rect
{
    NSPoint        topLeft, bottomRight ;
    float         kk, strokePattern[2] ={0.25*gridSpacing, 0.25*gridSpacing } ;
    NSBezierPath * curPath1 = [[[ NSBezierPath init ] alloc ] autorelease ] ;
    
    topLeft = getNearestGridPoint( rect.origin, self );
    bottomRight = NSMakePoint( rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
    bottomRight = getNearestGridPoint( bottomRight, self );
    [ gridLineColor set ];
    for ( kk = topLeft.x ; kk <= bottomRight.x; kk += gridSpacing )
    {
        [curPath1 moveToPoint: NSMakePoint( kk, topLeft.y-gridSpacing) ];
        [curPath1 lineToPoint: NSMakePoint( kk, bottomRight.y+gridSpacing) ];
    }
    for ( kk = topLeft.y ; kk <= bottomRight.y ; kk += gridSpacing )
    {
        [curPath1 moveToPoint: NSMakePoint( topLeft.x-gridSpacing, kk) ];
        [curPath1 lineToPoint: NSMakePoint( bottomRight.x+gridSpacing, kk) ];
    }
    [ curPath1 setLineWidth: 0];
    [ curPath1 setLineDash: strokePattern count: 2 phase: gridSpacing*0.125];
    [ curPath1 stroke ];
}

figura 01

figura 01

Il primo ciclo for serve a disegnare le linee verticali. Muovo la penna in testa, poi disegno una linea verticale fino in fondo. In realtà, la coordinata verticale potrebbe essere dentro o fuori al rettangolo (l'arrotondamento funziona nei due sensi). Per questo motivo, piuttosto che stare a chiedermi se la coordinata è interna o esterna, o fare altre considerazioni sull'argomento, mi limito a disegnare una linea un po' più lunga, e sicuramente fuori dal rettangolo. Ripeto lo stesso giochetto sulle linee orizzontali. Poi, finalmente, imposto i parametri della linea. Dico che lo spessore sia il minimo possibile (zero, appunto), e poi dico che la griglia è composta da linee tratteggiate. Con il metodo setLineDash:count:phase: dico appunto come è fatto il tratteggio. Ho costruito un vettore che dice che il tratteggio è formato da un quarto di griglia (0.25) pieno ed un quarto di griglia vuoto. Così facendo, ad ogni spaziatura ho due tratti pieni e due tratti vuoti. Ma nel disegno dico di spostare il tratteggio di un ottavo (0.125) di griglia. Ecco quindi che ottengo un interessante effetto: i punti della griglia sono individuati da una croce, e tra una croce e la successiva si trova un segmento perfettamente centrato.

Disegnare con la griglia

Un utente potrebbe desiderare la griglia solo per il gusto di avere delle linee di riferimento, ma il più delle volte la griglia è utilizzata per posizionare in maniera opportuna gli elementi. Ecco quindi che devo modificare tutti i metodi di disegno e di spostamento per tenere conto di queste esigenze.

La cosa è più semplice di quanto si possa pensare. In pratica, occorre modificare solo i metodi handleCreation: e handleResize: della classe CCE_BasicForm (e di tutte le sottoclassi che li sovrascrivono). In pratica, si tratta solo di modificare i metodi di CCE_BasicForm e CCE_Line. In effetti, basta eseguire la seguente funzione

     point1 = getNearestSnapPoint( point1, view);

su ogni punto che si intende piazzare. La funzione stessa si preoccupa di capire se la griglia è attiva (e quindi esegue delle modifiche sulle coordinate del punto) oppure no (e quindi lascia inalterato il punto stesso.

Poi, c'è da fare una cosa molto simile sul metodo handleMoveAll della classe CoverView, per gestire il fatto che, con la griglia attiva, il movimento può avvenire solo per coordinate multiple della spaziatura della griglia. Anche qui, la chiamata della funzione getNearestSnapPoint su ogni punto notevole del metodo è sufficiente allo scopo.

Infine, ci sono altri metodi che spostano gli elementi, e sono quelli governati dalla tastiera. Con la griglia attiva, i quattro tasti freccia non è corretto che spostino gli elementi selezionati di un solo pixel: è meglio che li spostino di una intera spaziatura di griglia. Ecco quindi che ad esempio devo modificare il metodo moveRight:

- (void)moveRight:(id)sender {
    float delta = snapToGrid ? gridSpacing : 1.0 ;
    [ theElements moveSelectedElements: NSMakePoint(delta, 0.0)];
    [ self setNeedsDisplay: YES ];
}

La quantità delta vale 1 in caso di griglia disattiva, è la spaziatura della griglia altrimenti.

Simile sorte la seguono i metodi associati agli spostamenti tramite frecce e tasto maiuscole. Non sapendo bene che quantità di spazio utilizzare nel caso di griglia attiva, ho deciso arbitrariamente per cinque.

- (void)moveRightAndModifySelection:(id)sender {
    float delta = snapToGrid ? (gridSpacing*5) : 10.0 ;
    [ theElements moveSelectedElements: NSMakePoint(delta, 0.0)];
    [ self setNeedsDisplay: YES ];
}

figura 02

figura 02

Con queste semplici correzioni, adesso anche gli spostamenti funzionano correttamente sulla griglia.

Il Dialogo della griglia

Non rimane a questo punto che permettere all'utente di determinare a proprio piacere i quattro parametri che caratterizzano la griglia: si tratta di impostare i valori delle quattro variabili d'istanza citate all'inizio del capitolo.

figura 03

figura 03

Per farlo, non c'è nulla di meglio di un dialogo chiamato da una voce di menu. Ecco quindi che modifico l'interfaccia aggiungendo la voce di menu Grid... che va ad aprire una nuova finestra GridDialog.

Nella finestra di dialogo ci sono due pulsanti di spunta per determinare se la griglia è visibile o meno, e se gli elementi vanno piazzati o meno sulla griglia. C'è poi una NSColorWell per determinare il colore con cui è disegnata la griglia, ed infine un campo testo ed uno slider per determinare la spaziatura. Lo slider ed il campo testo sono associati tra loro: l'utente può determinare la spaziatura scrivendo direttamente il numero nel capo testo, oppure manipolando lo slider.

Come ormai sono abituato a fare, aggiungo un metodo alla classe AppDelegate, che richiama l'istanza condivisa all'intera applicazione della classe controllore GridDialogWinCtl, che ho costruito associandola al file nib. La classe controllore contiene i soliti metodi

+ ( id ) sharedGridDialogWinCtl ;
- (void)mainWindowChanged:(NSNotification *)notification ;
- (void)mainWindowResigned:(NSNotification *)notification ;
- (void) setMainWindow: (NSWindow *) mainWindow ;

per tenere traccia di se e quale finestra in prima piano contiene una coverView.

Le variabili d'istanza sono gli outlet verso gli elementi del dialogo ed appunto il puntatore alla coverView.

Poiché ogni coverView ha una caratterizzazione della griglia differente dalle altre coverView, è bene sistemare gli elementi del dialogo in accordo a tali variabili. È compito del metodo setMainWindow:

- (void)
setMainWindow: (NSWindow *) mainWindow
{
    NSWindowController *controller = [mainWindow windowController];
    // se c'e' una finestra ed e' del tipo giusto
    if (controller && [controller isKindOfClass:[CoverWinCtl class]])
    {
        theDrawView = [(CoverWinCtl *)controller cvrView];
        [ gridColor setColor: [ theDrawView gridLineColor]];
        [ gridSnap setState: ([ theDrawView snapToGrid] == YES ? NSOnState : NSOffState)];
        [ sliderSpacing setIntValue: [theDrawView gridSpacing ] ];
        [ gridSpacing setIntValue: [theDrawView gridSpacing ] ];
        [ showGrid setState: ([ theDrawView showGrid] == YES ? NSOnState : NSOffState)];
    }
    else    
    {
        theDrawView = nil;
    }
}

Il metodo appunto recupera le informazioni dalla CoverView e predispone attraverso gli outlet l'aspetto dei pulsanti di spunta, dello slider, del testo e della NSColorWell.

Viceversa, ad ogni operazione su un elemento del dialogo, deve corrispondere ad una variazione dell'aspetto della griglia. I pulsanti di spunta ed il controllo NSColorWell chiamano direttamente il metodo seguente:

- (IBAction)updateGrid:(id)sender
{
    if ( theDrawView == nil )
        return ;    
    [ theDrawView setGridLineColor: [gridColor color ]];
    [ theDrawView setSnapToGrid:    ([gridSnap state] == NSOffState ? NO : YES) ] ;
    [ theDrawView setGridSpacing: [gridSpacing intValue ] ];
    [ theDrawView setShowGrid:        ([showGrid state] == NSOffState ? NO : YES) ] ;
    [ theDrawView setNeedsDisplay: YES ];
}

Senza stare a farsi troppe domande, il metodo verificare che ci sia una CoverView in primo piano, e poi modifica brutalmente tutte le variabili d'istanza relative alla griglia con i valori ottenuti dagli outlet. Poi, forza un ridisegno della vista per modificare l'aspetto della griglia in accordo coi nuovi valori.

Per lo slider ed il campo testo c'è bisogno di un passaggio in più, in quanto occorre anche aggiustare l'altro controllo. Allo slider, ad esempio, è associato il metodo:

- (IBAction)gridSpacingFromSlider:(id)sender
{
    int sliderValue = [ sender intValue ] ;
    [ gridSpacing setIntValue: sliderValue ];
    [ self updateGrid: sender ];
}

Dopo aver recuperato il valore dello slider, lo impone al campo testo; poi, ricade nel metodo updateGrid visto sopra.

Assolutamente simmetrico il metodo associato al campo testo:

- (IBAction)gridSpacingFromText:(id)sender
{
    int sliderValue = [ sender intValue ] ;
    [ sliderSpacing setIntValue: sliderValue ];
    [ self updateGrid: sender ];
}

che svolge le stesse operazioni, ma con i fattori invertiti.

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