MaCocoa 024

Capitolo 024 - A Proposito

Questo capitolo introduce molti nuovi concetti, forse frettolosamente, ma con lo scopo di arrivare ad una finestra About personalizzata ed animata.

Sorgenti: Faccio mio un tutorial su CocoaDevCentral.

Primo inserimento: 2 settembre 2002

A proposito

figura 01

figura 01

Quando si utilizza Cocoa, XCode e IB per la costruzione di una applicazione, si ottengono molte caratteristiche gratuite (copia, incolla, menu, eccetera). Tra le altre, si ottiene gratis anche la finestra About, in cui sono presentate le informazioni relative al programma. In maniera automatica Cocoa fornisce la finestra, che viene riempita dall'icona del programma (a proposito, ho cambiato l'icona del programma con quella porcheria che potete vedere in figura... se qualche anima pia mi fornisce gratuitamente una icona esteticamente più pregevole, è benvenuto) e dalle informazioni che sono recuperate dal file Info.plist. Inoltre, se è presente un file dal nome Credits.rtf, il suo contenuto è mostrato, completo di stili, all'interno di un campo di testo. Non sempre tale finestra è sufficiente a placare l'esibizionismo del programmatore. In questo capitolo, seguendo un tutorial che ho trovato sul sito Cocoa Dev Central, costruisco una finestra About personalizzata, in cui il testo del file Credits.rtf è mostrato scorrevole, a mo' di titoli di coda di un film. Per raggiungere questo scopo, saranno necessari molti nuovi concetti, che sfiorerò solamente per arrivare in tempi brevi all'obiettivo.

La finestra

figura 02

figura 02

Vado in IB e costruisco la nuova finestra di About. Genero un nuovo file nib, che nomino fantasiosamente AboutBox.nib. Oltre a riportare l'icona e qualche scritta di benvenuto, introduco un campo di testo. Si tratta di un oggetto della classe NSTextView.

figura 03

figura 03

figura 04

figura 04

Non si tratta del solito campo di testo del quale ho approfittato ampiamente finora, ma di un genere tutto diverso di bestia. È il cavallo di battaglia di ogni applicazione avente a che fare con la manipolazione di testo; per capirci, si può costruire un'applicazione simile a TextEdit con una finestra all'interno della quale c'è un solo oggetto NSTextView. Con questo oggetto si possono visualizzare testi in varie forme, fogge e dimensioni, immagini, sono attivi drag'n'ndrop, equivalenti di tastiera per la manipolazione, copia/incolla, controllo ortografico, eccetera. Tuttavia, tutto questo a me servirà a nulla. Tutto ciò di cui ho bisogno è il fatto che NSTextView è una sottoclasse di NSView e che posso, tramite i metodi ereditati, controllare da programma l'ammontare dello scrolling del testo. L'idea infatti è di infilare il file Credits.rtf all'interno del campo di testo scrollabile, nascondere la barra di scorrimento e controllare da programma lo scrolling del testo. Quest'ultima affermazione giustifica gli attributi del campo che ho impostato: accetto font multipli, ma non mostro la barra di scorrimento. Costruisco poi una sottoclasse di NSWindowController, che chiamo AboutWinCtrl; definisco un unico outlet, il campo di testo; costringo File's Owner ad essere una istanza di questa classe, collego l'outlet al campo.

Uovo di Pasqua

Già che sono in IB, aggiungo una action a MainMenu.nib, in totale similitudine con quanto fatto per le finestre di Info, la Palette dei Comandi e la finestra delle Preferenze. Quindi, al posto del collegamento standard della voce About del menu dell'applicazione, ci sostituisco questa azione. Tengo presente però il metodo invocato, perché mi serve per la realizzazione del metodo seguente:

- (IBAction)showAboutBox:(id)sender
{
    // recupero l'evento corrente, controllo lo stato della tastiera
    // e verifico se il tasto Alt è premuto
    if ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)
    {
        // la risposta e' si', procedo alla visualizzazioen della nuova finestra
        // uso la solita tecnica
        [ [AboutWinCtrl sharedAboutWinCtrl] showWindow: sender] ;    
    }
    else
    {
        // nessun tasto premuto, faccio vedere le solite cose
        [ NSApp    orderFrontStandardAboutPanel: self ];
    }
}

Questa volta, invece di eseguire le solite operazioni, realizzo un Easter Egg, letteralmente 'uovo di pasqua', ma sarebbe meglio parlare della sorpresa di questo uovo. Molto spesso i programmatori aggiungono caratteristiche nascoste ai loro programmi; spesso accade che la finestra di About, in particolari condizioni, sia diversa dal solito (famosa è rimasta la bandiera con l'iguana in Mac OS 7). Qui faccio lo stesso: se l'utente seleziona la voce del menu in condizioni normali, faccio vedere la finestra normale prodotta da Cocoa. Se invece tiene premuto il tasto Alt (Option per qualcuno), esibisco la nuova finestra.

Mostrare la finestra

A parte i soliti metodi di init e sharedAboutWinCtrl, le cose diventano interessanti con il metodo windowDidLoad, in cui si devono cominciare a predisporre un po' di cose:

- (void)
windowDidLoad
{
    NSString *creditsPath;
    NSAttributedString *creditsString;

    [ super windowDidLoad ] ;

Fino a qui non c'è nulla di particolare; lasciamo per il momento perdere le tre istruzioni seguenti:

    // inizio la posizione corrente verticale
    currentPosition = 0;
    restartAtTop = NO;
    // imposto l'istante di partenza, qualche secondo dopo l'inizio
    startTime = [NSDate timeIntervalSinceReferenceDate] + ABOUTWIN_STARTSCROLLDELAY ;

Qui di seguito carico in memoria il contenuto del file Credits.rtf; prima costruisco il path completo tenendo conto delle possibili localizzazioni (e quindi ricorro ai bundle), poi metto il contenuto del file all'interno di una NSAttributedString, piuttosto che in una normale NSString. Infatti una NSAttributedString è una stringa che possiede degli attributi, dove per attributo si intende un insieme di caratteristiche (font, dimensione, kerning, eccetera) che si applicano ad uno o più caratteri. In pratica, è una stringa di caratteri che mantiene la piena definizione degli stili presenti. Con l'oggetto NSAttributedString ci riempio poi il campo di testo, mantenendo la formattazione RTF.

    // costuisco il path al file Credits.rtf localizzato
    creditsPath = [[NSBundle mainBundle] pathForResource:@"Credits" ofType:@"rtf"];
    // carico il contenuto del file
    creditsString = [[NSAttributedString alloc] initWithPath:creditsPath documentAttributes:nil];
    // riempio il campo di testo con il contenuto del file
    [scrollText replaceCharactersInRange:NSMakeRange( 0, 0 ) withRTF:
        [creditsString    RTFFromRange: NSMakeRange( 0, [creditsString length] )
                documentAttributes:nil] ];

Dimentichiamo ancora una volta le due istruzioni seguenti...

    // calcolo l'altezza del capo di testo
    maxScrollHeight = [ creditsString size ].height ;
    // si comincia con l'altezza a Zero
    [scrollText scrollPoint:NSMakePoint( 0, 0 )];

figura 05

figura 05

Qui c'è un bel tocco di classe (che infame gioco di parole): normalmente (il comportamento di default previsto da Cocoa) un campo di testo si porta dietro un menu contestuale che permette di eseguire le operazioni di copia/incolla, di controllo ortografico, eccetera. Poiché questo non è il comportamento voluto in questo momento, dico di non associare menu alla vista. Poi il metodo chiude con le solite istruzioni relative alla finestra nel suo complesso.

    // evito di associare menu al campo (font, spelling, ecc)
    [scrollText setMenu:nil ];

    // non voglio che compaia nella lista delle finestre
    [[self window] setExcludedFromWindowsMenu:YES];
    // evito di associare menu alla finestra
    [[self window] setMenu:nil];
    // porto la finestra davanti a tutti
    [[self window] makeKeyAndOrderFront:nil];
}

È il momento adesso di parlare dello scrolling del campo e di come questo sarà eseguito. Dicevo che l'idea consiste nel mettere il testo all'interno di un campo scorrevole, nascondere la barra di scorrimento laterale e controllare da programma lo scorrimento del testo.

Per fare questo ci occorrono un po' di variabili: la posizione corrente di scrolling (currentPosition) e la massima posizione raggiungibile (maxScrollHeight); ci serve poi un flag per sapere se si deve ricominciare dall'inizio (restartAtTop), ed una indicazione di tempo per tenere fermo, all'inizio, lo scrolling, di modo che l'utente possa leggere con calma la parte iniziale del testo (che altrimenti scompare non appena inizia lo scrolling). Il metodo appena visto predispone dei valori adatti alle operazioni previste: si comincia dall'inizio, quindi currentPosition è nullo e restartAtTop è Vero, ed il tempo quando iniziare lo scrolling è quello attuale (al caricamento della finestra) più tre secondi (tre è appunto il valore di ABOUTWIN_STARTSCROLLDELAY). Per quanto riguarda l'altezza massima, c'è un metodo chiamato size, che restituisce le dimensioni del rettangolo di spazio occupato dalla visualizzazione della NSAttributedString; come questo metodo funzioni, lo ignoro (restituisce il rettangolo occupato all'interno della finestra, ma nessuno ha informato la NSAttributedString di questo fatto...). Tuttavia, pragmaticamente, accetto il risultato e lo uso, limitatamente alla parte di altezza, visto che la larghezza è quella della NSTextView.

E infatti: size restituisce il rettangolo di spazio occupato 'naturalmente' dal testo, a prescindere dalla sua visualizzazione; qui, per pura fortuna, la dimensione orizzontale del testo è inferiore allo spazio reso disponibile dal campo di testo, per cui la dimensione verticale del testo concide con lo spazio occupato dal testo stesso una volta visualizzato; al momento non ho idea di come stabilire la reale dimensione verticale del testo una volta visualizzato.

Batti il tuo tempo

Adesso viene la parte interessante. Per eseguire lo scrolling da programma, devo avere la possibilità di eseguire l'aggiornamento della posizione di scrolling ad intervalli prefissati di tempo. In generale, qualsiasi animazione richiede l'esecuzione di un qualche ridisegno ad intervalli determinati di tempo. Esiste in Cocoa una classe pensata proprio allo scopo, ovvero NSTimer.

Sostanzialmente, un oggetto NSTimer attende un certo tempo, poi fa qualcosa. Tipicamente, invia un messaggio a qualcuno. Se istruito correttamente, continua a mandare il messaggio ad intervalli regolari. È proprio quello che mi serve. Da qualche parte all'interno della classe metterò una istruzione del tipo:

scrollTimer = [NSTimer scheduledTimerWithTimeInterval: ABOUTWIN_TIMERINTERVAL
    target:self
    selector: @selector(scrollCredits:)
    userInfo: nil
    repeats: YES];

in cui costruisco un oggetto NSTimer con le caratteristiche di inviare il messaggio scrollCredits: (argomento di selector) a se stesso (argomento di target) e di ripetere (argomento di repeats) l'operazione ogni ABOUTWIN_TIMERINTERVAL secondi (argomento di scheduledTimerWithTimeInterval). Quando devo fermare l'animazione, invio un messaggio del tipo:

[ scrollTimer invalidate];

che ferma le operazioni del timer.

Il posizionamento di queste due istruzioni introduce un nuovo concetto ancora, o meglio, una nuova notifica. Tra le notifiche inviate automaticamente da una finestra, c'è NSWindowDidBecomeKeyNotification, inviata quando la finestra si mette davanti a tutti (ed in particolare è prima destinataria di ogni attività sulla tastiera: questo spiega la parola 'key'). Ugualmente, quando perde questa caratteristica, c'è la notifica NSWindowDidResignKeyNotification. Utilizzando una classe delegata della finestra, per rispondere a queste due notifiche occorre scrivere i due metodi windowDidBecomeKey: e windowDidResignKey:. All'interno di questi due metodi inserisco le due istruzioni, la prima per innescare il timer e l'attività di animazione, la seconda per fermare il timer e l'animazione stessa.

Perché il meccanismo funzioni, bisogna ricordarsi di assegnare File's Owner come delegato della finestra di About all'interno del file AboutBox.nib (detto così è ovvio, ma provate voi a capire perché le cose non funzionano... ci ho messo un bel po'...).

Animazione

Eccomi finalmente al cuore di tutta la faccenda, ovvero al metodo scrollCredits:, periodicamente invocato dal meccanismo di NSTimer.

- (void)
scrollCredits: (NSTimer *)timer
{
    // vediamo se e' passato il tempo di attesa iniziale
    if ([NSDate timeIntervalSinceReferenceDate] < startTime)
        return ;

Qui controllo se è passato sufficiente tempo dall'apertura della finestra, per tenere ferma l'animazione per un po' di tempo in modo che si possano leggere le prime righe.

    // se arrivo qui, bisogna cominciare a scrollare
    // vedo se devo ripartire dall'inizio
    if (restartAtTop)
    {
        // e' meglio aspettare un po'
        startTime = [NSDate timeIntervalSinceReferenceDate] + ABOUTWIN_STARTSCROLLDELAY;
        // non devo piu' ricominciare dall'inizio
        restartAtTop = NO;
        // imposto la posizione dall'inizio
        [scrollText scrollPoint:NSMakePoint( 0, 0 )];            
        return;
    }

Questo è il pezzo di codice in cui si entra quando si deve cominciare a scrollare la finestra (perché è appena scaduto il tempo di attesa, oppure perché si è arrivati alla fine del giro precedente). Imposto un paio di variabili, e poi dico che la posizione di scroll è Zero. Questa posizione va data come una coordinata orizzontale ed una coordinata verticale, per poter gestire scrolling nelle due direzioni. In questo esempio ci interessa la sola coordinata verticale (la seconda), che imposto a zero. La funzione (è una funzione, non un metodo) NSMakePoint costruisce un punto, che è appunto costituito da una coordinata orizzontale ed una verticale.

    // se arrivo qui, proseguo lo scrolling
    // vedo se per caso sono arrivato alal fine
    if (currentPosition >= maxScrollHeight)
    {
        // sono arrivato alal fine, reimposto il timer
        startTime = [NSDate timeIntervalSinceReferenceDate] + ABOUTWIN_INTERSCROLLDELAY;
        // e riparto dall'inizio
        currentPosition = 0;
        restartAtTop = YES;
        return ;
    }

Qui si controlla se lo scrolling è terminato, verificando se la posizione corrente ha superato (o eguaglia) la massima. Le istruzioni elencate impostano una nuova attesa (per permettere la lettura delle ultime righe appena comparse) e le variabili per poter ricominciare dall'inizio.

    // se sono arrivato qui, non e' successo nulla di speciale
    // imposto allo scroll la posizione corrente
    [scrollText scrollPoint:NSMakePoint( 0, currentPosition )];
    // che poi incremento per la volta successiva
    currentPosition += ABOUTWIN_SCROLLINCREMENT;
}

Infine, questa è la parte che esegue lo scrolling vero e proprio: semplicemente, imposta il valore di scrolling corrente, e pi incrementare di un po' la posizione corrente. Variando il valore di ABOUTWIN_SCROLLINCREMENT (ed eventualmente dell'intervallo di attivazione del timer) si possono ottenere diverse velocità di scorrimento del testo.

figura 06

figura 06

Per vedere qualcosa di sensato, ho dovuto aggiungere un po' di testo inutile in coda al file Credits.rft, altrimenti il testo completo del file stava all'interno della NSTextView e non si aveva alcun effetto di scrolling.

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