MaCocoa 015

Capitolo 015 - Formattatori

Un capitolo un po' discorsivo all'inizio, ma poi ci sarà un po' d'azione....

Primo inserimento: 19 Dicembre 2001

Celle e Formattatori

Molti degli elementi dell'interfaccia disponibile in IB appartengono alla famiglia dei Controlli, sottoclassi della classe NSControl. Un Controllo si chiama così perché attraverso di esso l'utente controlla l'applicazione: è proprio attraverso i Controlli che si eseguono le operazioni richieste. Un pulsante è un ovvio Controllo, ma anche un campo di testo è tutto sommato un Controllo. In particolare, anche gli elementi argomento dei due capitoli precedenti, NSTableView e NSOutlineView sono in fin dei conti un Controllo.

Una caratteristica interessante dei controlli è che sono visibili all'interno di una finestra; del resto, se non fossero visibili, non si potrebbero utilizzare per interagire con l'applicazione.

Ogni oggetto Controllo utilizza un compagno oscuro per svolgere il lavoro sporco: questo oggetto è una cella, o meglio, è un oggetto di una delle varie sottoclassi di NSCell. Sono gli oggetti di tipo NSCell i veri responsabili della visualizzazione del contenuto dei vari controlli. Abbiamo, in piccolo, l'applicazione del paradigma Model-View-Controller, dove NSControl è appunto la classe Controller, NSCell la classe View, mentre la classe Model è compito del programmatore...

Quindi, quanto si interagisce con un pulsante, in realtà le operazioni tipo target/action sono svolte dall'oggetto NSControl, mentre la visualizzazione dello stesso (compresi brillamenti ed altre facezie grafiche) è compito della NSCell.

Controlli sofisticati contengono al loro interno diverse categorie di celle, adatta a rappresentare diverse unità di informazioni; ad esempio NSTableView contiene un elemento cella per ogni colonna, ed ogni cella può essere configurata in modo differente dalle altre. In questo è possibile visualizzare all'interno della NSTableView non solo semplici stringhe, ma anche numeri, date, immagini, insomma, quello che si vuole.

Formattatori

Nella rappresentazione delle informazioni di un file presentata nelle applicazioni dei capitoli precedenti, è piuttosto brutto vedere dimensioni di file espressi in numeri un po' bizzarri, date espresse con formati strani, eccetera. In altre applicazioni potrebbe ad esempio essere necessario visualizzare numeri di telefono, ma non esiste alcuna cella in grado di visualizzare in bella forma questa informazione.

La soluzione a questo problema è la classe NSFormatter, una classe astratta che intende raccogliere dei meccanismi di presentazione delle informazioni in bella copia. I formattatori sono oggetti che traducono il valore di altri oggetti in una rappresentazione più adatta alla visualizzazione; e viceversa, devono cioè essere in grado di passare dalla forma visualizzata in bella copia nella rappresentazione interna.

Esistono una coppia di classi formattatrici direttamente accessibili da IB. Il primo formattatore è adatto alla conversione di numeri, in vari formati; è così possibile rappresentare numeri come dollari, eventualmente in rosso se fossero negativi, trasformare numeri automaticamente in percentuali, cose del genere.

Il secondo formattatore invece consente la rappresentazione di date secondo diverse modalità, scrivendo il giorno della settimana, oppure no, eccetera. In entrambi i casi, esiste anche la possibilità di inventarsi il formato specificandolo attraverso un formato di esempio.

figura 01

figura 01

Per utilizzare queste classi formattatici standard, c'è un metodo pratico, direttamente da IB. Si tratta di eseguire l'operazione di drag and drop del formattatore prescelto dalla palette di IB dove sono presenti tutti gli elementi dell'interfaccia, sopra la cella cui il formattatore si applica. Pigliando ad esempio l'applicazione del capitolo precedente, ho utilizzato il formattatore di data per meglio rappresentare la data di modifica del file, trascinando appunto il simbolo della classe NSDateFormatter sopra la corretta colonna della NSOutlineView.

Farsi un formattatore

Ma c'è di più, ovvero è possibile realizzare formattatori a piacimento, semplicemente facendo una sottoclasse della classe madre di tutti i formattatori, ovvero NSFormatter. È quello che ho pensato di fare per meglio visualizzare una serie di informazioni riguardanti il file.

In particolare mi interessa rappresentare in maniera più intelligibile la dimensione del file e le due stringhe di Tipo e Creatore tipiche di ogni file dei sistemi Os9 e precedenti.

Per quanto riguarda la dimensione del file, si tratta semplicemente di trasformare il numero crudo di byte in una stringa più simpatica a vedersi, in cui siano rappresentati K o anche Mega, per maggior leggibilità. Per il tipo ed il creatore, faccio finta che non ci siano funzioni di Carbon/Cocoa che eseguano automaticamente la traduzione (in realtà, lo ignoro, ma secondo me da qualche parte ci sono), ma mi ricordo che un tipo o creatore è una stringa di quattro caratteri di otto bit, considerata come fosse un intero a 32 bit (sono un po' oscuro, ma forse l'esempio successivo chiarisce tutto).

Ora, per farsi in casa un formattatore, bisogna definire tre metodi: uno per convertire dalla rappresentazione interna alla stringa che sarà visualizzata, uno per ritornare indietro, ed un terzo, il cui uso mi è poco chiaro, ma che dovrebbe servire a capire se sono applicate al testo strane formattazioni (del tipo: il numero è rosso se negativo).

In realtà, poiché intendo solo visualizzare i dati in bella copia, il secondo ed il terzo metodo li realizzo solo per accontentare Cocoa, ma li lascerò bellamente vuoti.

Parto quindi col formattatore per la dimensione del file:

@interface FileSizeForm : NSFormatter {

}
- (NSString *)stringForObjectValue:(id)anObject ;
- (BOOL)getObjectValue:(id *)anObject forString:(NSString *)string errorDescription:(NSString **)error ;
- (NSAttributedString *)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attributes ;

@end

Ho costruito un sottoclasse di NSFormatter, non ci sono variabili d'istanza, ma solo i tre metodi citati sopra. Passo alla realizzazione.

@implementation FileSizeForm

- (NSString *)stringForObjectValue:(id)anObject
{
    long    fSize, ff ;
    float     fff ;
    
    if (![anObject isKindOfClass:[NSNumber class]]) {
        return nil;
    }
    fSize = [ anObject longValue ] ;
    // se il file e' piccolo, mostro byte
    if ( fSize < 1024 )
        return ( [ NSString stringWithFormat: @" %8d b", fSize] );
    // la dimensione e' in byte, divido per 1024 ed arrotondo, ottengo K
    ff = (long) (( fSize / 1024.0 ) + 0.5 );
    // se il file e' medio, mostro K
    if ( ff < 1024 )
        return ( [ NSString stringWithFormat: @" %8d K", ff] );
    //in tutti gli altri casi, ritorno Mega, con due cifre decimali
    fff = ( ff / 1024.0 ) ;
    return ( [ NSString stringWithFormat: @" %6.2f M", fff] );
}

- (BOOL)getObjectValue:(id *)anObject forString:(NSString *)string errorDescription:(NSString **)error
{
    return ( YES );
}

- (NSAttributedString *)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attributes
{
    return ( nil);
}
@end

Il secondo ed il terzo metodo, come si può vedere, fanno proprio nulla (si limitano a restituire qualcosa di sensato). Il primo metodo è quello interessante. Da notare subito la prima istruzione.

if (![anObject isKindOfClass:[NSNumber class]]) {

Mi chiedo se l'oggetto che mi è stato chiesto di convertire è un numero. Del resto, la dimensione del file (andate a vedere la classe LSFileInfo) è dichiarata essere un long. Mi aspetto quindi che l'oggetto da convertire sia un numero. In ObjectiveC, quando si parla di numeri puri, sono rappresentati come oggetti della classe NSNumber. Il numero ricordo che salta fuori dal metodo outlineView:objectValueForTableColumn:byItem:, ed in particolare è il risultato dell'istruzione

[ item valueForKey: colId ]

È ben vero che stiamo parlando di un numero intero, ma questo, in un ambiente OOP, è comunque incapsulato all'interno di un oggetto.

Noto per inciso che l'istruzione if mi ha fatto penare non poco. Infatti, senza di essa, l'applicazione muore in maniera spettacolare. Questo è ciò che mi viene risposto:

2001-12-17 23:05:59.777 myGetFile[966] *** -[NSCFString longValue]: selector not recognized

Questa criptica frase afferma che nell'esecuzione dell'istruzione

fSize = [ anObject longValue ] ;

è capitato che anObject fosse un oggetto della classe NSCFString (credo una stringa). Ignoro chi o cosa sia, e soprattutto da dove salta fuori. Una indagine più approfondita mi ha fatto scoprire che in effetti il formattatore viene invocato una prima volta passando una NSString come argomento anObject, il cui valore è la misteriosa stringa Field. Ancora una volta, ignoro da dove salti fuori questa stringa.

Torniamo a bomba, cioè al metodo convertitore. Una volta deciso che l'oggetto passato parametro è effettivamente un numero, ne ricavo il valore con il metodo longValue; a questo punto, è solo C. I commenti incorporati nel codice dovrebbero essere sufficienti a spiegare il procedimento. L'unica cosa da notare è la costruzione al volo di una NSString utilizzando il metodo stringWithFormat:, cosa che permette l'uso, per me molto familiare, della sintassi della funzione printf del C standard.

Il passo successivo è il formattatore per il tipo/creatore. Riporto solo il metodo più interessante, per il resto è tutto simile a quanto visto sopra.

- (NSString *)stringForObjectValue:(id)anObject
{
    char    ss[20] ;
    union {
        long cc ;
        char ccc[4];
    }    ccu ;
    
    if (![anObject isKindOfClass:[NSNumber class]]) {
        return nil;
    }
    
    ccu.cc = [ anObject longValue ] ;
    sprintf( ss, "%c%c%c%c", ccu.ccc[0], ccu.ccc[1], ccu.ccc[2], ccu.ccc[3]);
    return ( [ NSString stringWithCString: ss ] );
}

Quando dicevo che il tipo ed il creatore del file sono quattro caratteri rappresentati tramite un intero, ho semplicemente spiegato malamente la union che compare all'inizio. Dopo di ché, è tutto semplice e lineare.

L'ultimo compito da eseguire è di appiccicare il formattatore alla cella che ci interessa.

Ho vagabondato a lungo attraverso la documentazione della varie classi, fino a scoprire quanto ho detto all'inizio. Queste semplici affermazioni si tramutano nelle istruzioni seguenti:

Queste semplici affermazioni si tramutano nelle istruzioni seguenti:

FileSizeForm     *myDF2 = [[ [ FileSizeForm alloc] init ] autorelease ];
[ [[ outlineView tableColumnWithIdentifier: @"fileSize"] dataCell] setFormatter: myDF2 ];

Nella prima istruzione, alloco ed inizializzo un formattatore e lo chiamo myDF2. C'è da spiegare il metodo autorelease. Con questa istruzione dico che, per quanto riguarda la funzione in cui si trovano queste istruzioni, l'oggetto myDF2 può essere buttato via quando è finito il loop degli eventi. Infatti, il metodo setFormatter esegue un retain dell'oggetto formattatore. Quando verrà distrutta (se mai lo sarà) la cella, al formattatore sarà inviato il messaggio release. La famigerata variabile contavita torna a zero, l'oggetto formattatore è distrutto. Se non avessi messo autorelease, contavita sarebbe stata già 1 dopo la prima istruzione, diventa 2 per l'incremento causato da setFormatter:, ritorna 1 alla distruzione della cella. L'oggetto rimane in memoria, ma nessuno lo sta usando; dovrei esplicitamente distruggerlo contestualmente alla distruzione della cella.

Passiamo alla seconda istruzione, che come al solito è piuttosto pasticciata e la divido in vari pezzi. Per prima cosa, devo recuperare la NSTableColumn che mi interessa:

[ outlineView tableColumnWithIdentifier: @"fileSize"]

Della NSTableColumn in realtà mi interessa la cella che lo rappresenta:

[ <colonna> dataCell]

è proprio a questa cella che appiccico il formattatore:

[ <cella> setFormatter: myDF2 ];

Sembra complicato, ma poi guardando bene non lo è.

Dove inserire queste istruzioni? Il momento adatto è dopo il caricamento dell'interfaccia utente (o meglio, del file nib che contiene tutti gli elementi dell'interfaccia), ma prima della sua visualizzazione. Sto ovviamente parlando del metodo awakeFromNib della classe che controlla l'intera applicazione (FileInfoCtrl), che è qui presentato completo dell'installazione di un altro formattatore, condiviso da due colonne (e qui è veramente essenziale autorelease...):

- (void) awakeFromNib
{
    TOS9TCForm     *myDF1 = [[ [ TOS9TCForm alloc] init ] autorelease ];
    FileSizeForm     *myDF2 = [[ [ FileSizeForm alloc] init ] autorelease ];
    [ [[ outlineView tableColumnWithIdentifier: @"typeCode"] dataCell] setFormatter: myDF1 ];
    [ [[ outlineView tableColumnWithIdentifier: @"creatorCode"] dataCell] setFormatter: myDF1 ];
    [ [[ outlineView tableColumnWithIdentifier: @"fileSize"] dataCell] setFormatter: myDF2 ];
}

Filtro sui file

Per concludere l'intero progetto, ho inserito all'interno della classe FileStruct due funzioni aggiuntive scritte in C (funzioni, non metodi! E sì, si possono usare le classiche funzioni in C all'interno di una classe) per evitare di esplorare directory e file non interessanti.

Ho definito due funzioni che rispondono Vero o Falso quando le si sottopone il nome di un file.

Se il file ha un nome che comincia per punto, allora è un file che non ci interessa. I file punto in Unix sono normalmente tenuti nascosti e servono per immagazzinare dati anche essenziali ma che non è bello mostrare ad un utente. Per rendervi conto della cosa, aprire un Terminale e digitate ls -a o ls -la al posto del classico ls. Compare in genere qualche file in più il cui nome comincia appunto con un punto.

Inoltre, se il file finisce con .app, allora è una applicazione, e non si deve esplorare il suo contenuto. Come dovrebbe essere noto, una applicazione in Mac Os X è in realtà una cartella, la cui struttura è ben definita. All'interno della cartella-applicazione sono contenute tutte le risorse necessarie al funzionamento dell'applicazione, help e readme compresi. Dico che nel leggere una directory, se questa è una applicazione, lascio perdere.

Le due funzioni descritte si realizzano facilmente utilizzando due metodi della classe NSString:

BOOL myFileFilterInsert( NSString * fileName )
{
    if ( [ fileName hasPrefix: @"."] )
        return ( NO );
    return (YES );
}

BOOL myFileFilterExpand( NSString * fileName )
{
    if ( [ fileName hasSuffix: @".app"] )
        return ( NO );
    return (YES );
}

per cui il metodo initTreeFromPath diventa qualcosa del tipo:

- (id) initTreeFromPath: (NSString*) fullPath parent: (FileStruct*) myParentDir
{
    // mi metto via un file manager
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL    isAdir, fileOK, expandOK ;
    int    i ;

    // per prima cosa, inizializzo super
    [ super initWithPath: fullPath ] ;
    // assegno la directory geniotrice
    [ self setParentDir: myParentDir ] ;
    // e adesso, se il file puntato e' una directory, espando
    fileOK = [fileManager fileExistsAtPath:fullPath isDirectory:&isAdir];
    // ma solo se lo voglio veramente
    expandOK = myFileFilterExpand( [ fullPath lastPathComponent] );
    if (expandOK && fileOK && isAdir)
    {
        // ok, e' una directory, recupero l'elenco dei file
        NSArray *dirContent = [fileManager directoryContentsAtPath:fullPath];
        // vedo quanti file ci sono all'interno
        int numFile = [dirContent count];
        // costruisco un vettore destinato a contenere i file e lo assegno
        fileList = [[NSMutableArray alloc] initWithCapacity:numFile];
        // adesso, per ogni elemento, lo aggiungo e costruisco l'albero
        for ( i = 0; i < numFile; i++)
        {
            NSString *myfile = [dirContent objectAtIndex:i] ;
            // ma solo se lo voglio veramente
            if ( myFileFilterInsert( myfile) )
                [fileList addObject:[
                    [FileStruct alloc]
                        initTreeFromPath:[ fullPath stringByAppendingPathComponent:
                            myfile] parent:self
                    ]
                ];
        }
    }
    else
    {
        // va beh, e' un file normale
        fileList = (id) -1 ;
    }
    return ( self );
}

il metodo contiene ovviamente un errore (logico, e che non inficia il funzionamento dell'applicazione), che al momento non ho idea di come risolvere. Bisognerebbe leggere la documentazione...

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