MaCocoa 013

Capitolo 013 - Una tabella di file

Scopo del capitolo è di selezionare un file o una directory, recuperare un po' di informazioni sul file stesso (o sulla directory) ed inserire tutto quanto all'interno di una tabella. Semplice a dirsi e, a parte qualche normale difficoltà, anche a farsi. Cominciamo.

Sorgenti: Documentazione classi

Primo inserimento: 5 Dicembre 2001

L'interfaccia

figura 01

figura 01

Velocemente costruisco una interfaccia per fare quanto detto sopra. Sarà scarna e per nulla bella, ma al momento non ci interessa.

Mi occorre un pulsante per far partire le attività, un campo dove mettere il percorso completo del file (serve solo per verifica, visto che so già come lavorare con questo tipo di elementi) ed una tabella. La tabella è un oggetto della classe NSTableView, che si può inserire nell'interfaccia trascinando uno degli elementi della palette di IB. Costruisco subito una classe che funzioni da Controllore, la chiamo FileInfoCtrl, e genero una istanza dal nome oFileInfoCtrl. Collego con una action (lsGetFile) il pulsante alla classe controllore, e collego questa al campo di testo (outlet lastFilePath). Lascio temporaneamente da parte la tabella.

Pescare il nome di un file

Per il momento, mi accontento di selezionare un file o una directory con il dialogo standard di apertura file, quello che normalmente si usa quando, dall'interno di una applicazione, si vuole aprire un file.

La classe Cocoa che mi interessa si chiama NSOpenPanel. A meno di fare cose piuttosto esotiche, la modalità standard sembra piuttosto semplice. In primo luogo, occorre recuperare un oggetto della classe NSOpenPanel. Con l'oggetto sotto mano si possono predisporre alcune caratteristiche d'uso del dialogo (quali file selezionare, quanti, nome del dialogo, cose del genere), per poi finalmente mostrare all'utente il dialogo, modale. Modale significa che l'utente non può proseguire fino a che non ha effettuato una scelta, o ha rinunciato alla scelta stessa; in altre parole, finché il dialogo è presente a video, l'utente non può interagire con le altre finestre dell'applicazione.

Dalla documentazione della classe NSOpenPanel vedo che posso modificare solo quattro proprietà: la possibilità o meno di selezionare file, di selezionare directory, di risolvere o meno gli alias e di permettere o meno la selezione multipla dei file. Non è finita qui: vado a vedere la documentazione della superclasse, che è NSSavePanel (bizzarro, ma non troppo: anche il salvataggio di un file è in effetti la selezione di un nome del file, solo che nel salvataggio posso anche scegliere il nome del file...).

Dalla documentazione di NSSavePanel trovo interessante la possibilità di modificare altre caratteristiche: il titolo della finestra, il testo del pulsante (il prompt), come trattare i file packages (tipo di elementi che ad esempio esemplificano le applicazioni), eccetera.

Potrei risalire la gerarchia delle classi, ma mi limito a capire come fare a modificare le dimensioni della finestra. All'interno di NSWindow trovo il metodo setFrame:display: che consente di impostare posizione e dimensione della finestra con un NSRect. Mi pare bizzarro che Cocoa mi lasci scegliere dove posizionare la finestra: una buona interfaccia utente posiziona la finestra di apertura file al centro della finestra da cui in qualche modo è richiesta. Infatti, a cose fatte, vedrò che non c'è verso di posizionare la finestra come mi piace, anche se per la dimensione qualcosa si può fare.

C'è un ultimo filtro da impostare, ovvero i tipi di file che si possono selezionare, scelti in base alla loro estensione. Poiché al momento non so che file pigliare, decido di poter selezionare qualsiasi file.

figura 02

figura 02

Non rimane altro che aprire la finestra di dialogo usando uno dei metodi disponibili. Al mio caso è adatto runModalForTypes:, che appunto richiede l'elenco delle estensioni dei file da filtrare: uso la parola chiave nil (nulla) che appunto imposta nessun filtro.

Il metodo restituisce un codice, che indica se l'utente ha selezionato il pulsante di scelta predefinito oppure il pulsante di cancellazione della selezione. Nel primo caso si possono recuperare il file o i file selezionati estraendoli con metodo filenames. Il tutto porta alla seguente realizzazione del metodo lsGetFile:

- (IBAction)lsGetFile:(id)sender
{
    int result;
    NSOpenPanel *oPanel = [NSOpenPanel openPanel];
    /* NSRect NSMakeRect(float x, float y, float w, float h) */
    [oPanel setFrame: NSMakeRect(0, 0, 500, 200) display: NO];
    [oPanel setTitle:@"la mia selezione"];
    [oPanel setPrompt:@"Seleziona un file qualsiasi"];
    [oPanel setCanChooseDirectories:YES];
    [oPanel setCanChooseFiles:YES];
    [oPanel setAllowsMultipleSelection:NO];
    [oPanel setResolvesAliases:NO];
    
    result = [oPanel runModalForTypes: nil];
    
    if (result == NSOKButton) {
        NSArray *filesToOpen = [oPanel filenames];
        NSString *aFile = [filesToOpen objectAtIndex:0];
        [ lastFilePath setStringValue: aFile ];
    }
}

Alcune osservazioni.

L'ultima istruzione del metodo lsGetFile si limita a inserire la stringa nel campo testo dell'interfaccia.

Informazioni del file

Ora che ho il nome completo del file, voglio recuperare un po' di informazioni sullo stesso (che so, data di modifica, tipo, cose del genere), e metterle da qualche parte.

Cerco nella documentazione, ed il primo oggetto che mi capita sottomano e che fa al mio caso si chiama NSFileManager. Proprio nella documentazione c'è un esempio calzante e che ricopio più o meno fedelmente.

Utilizzando il metodo fileAttributesAtPath:traverseLink: si possono recuperare, tre le altre, le seguenti informazioni:

Per conservare tutte queste informazioni relative al file definisco una nuova classe, adatta a contenerle. Ecco il file di intestazione LSFileInfo.h:

@interface LSFileInfo : NSObject {
    NSString     * fileFullPath ;
    NSString     * fileName ;
    NSDate        * modDate ;
    long         fileSize;
    NSString     * fgoan ;
    NSString     * foan ;
    long         filePosixPerm ;
    NSString     * fileType ;
    long         creatorCode ;
    long         typeCode ;
}

- (void) initWithPath: (NSString*) fullPath ;
- (void) dealloc;
- (NSString*)fileFullPath;
- (void)setFileFullPath:(NSString*)inFileFullPath;
    
@end

Ho aggiunto due variabili, una per contenere il path completo del file, ed una per conservare il solo nome del file. Poi ci sono tutte le variabili che contengono le informazioni sopra citate.

Ho dichiarato anche una lunga teoria di metodi. Il primo metodo serve ad inizializzare le variabili d'istanza con i valori desunti dal file puntato dal path passato come argomento. Ho dichiarato esplicitamente (e quindi lo sovrascrivo) il metodo dealloc, che non chiamerò mai esplicitamente, ma che è bene scrivere per eliminare tutti gli oggetti che dipendono da quest'oggetto (tutti gli NSString utilizzati come variabili d'istanza) quando si deve eliminare un oggetto di classe LSFileInfo.

Si trovano poi elencati a coppie tutta una serie di metodi (e qui ho solo riportato la prima coppia) chiamati accessor per accedere alle variabili d'istanza, in lettura e scrittura. Da notare la realizzazione di questi metodi quando la variabile non è un tipo semplice, ma un oggetto; ad esempio nel caso seguente di un oggetto di tipo NSDate (destinato a contenere la data di modifica del file):

- (void)setModDate:(NSDate*)newModDate
{
    if (modDate != newModDate) {
                [modDate release];
                modDate = [newModDate retain];
        }
}

Il metodo tiene conto delle avvertenze sviluppate nel capitolo precedente relativo alla questione di release, retain, eccetera.

Il metodo dealloc merita qualche commento aggiuntivo:

- (void) dealloc
{
    [ fileFullPath release ] ;
    [ fileName release ] ;
    [ modDate release ] ;
    [ fgoan release ] ;
    [ foan release ] ;
    [ fileType release ] ;
    [ super dealloc ] ;
}

Con questo metodo si rilasciano tutti gli oggetti ritenuti all'interno di initWithPath:. Non basta: al termine delle operazioni (e solo al termine!) occorre invocare anche il metodo dealloc della superclasse, utilizzando appunto super.

Finalmente arriviamo al più interessante metodo di initWithPath:, che intende rimpiazzare in tutto e per tutto il metodo standard init. Con questo intento, initWithPath: è chiamato inizializzatore designato, ed è bene chiamarlo ogni volta che si costruisce un oggetto della classe LSFileInfo. La struttura del metodo è molto semplice. La prima cosa da fare è chiamare il metodo init della superclasse (in realtà l'operazione è svolta dopo, ma tanto nessuna inizializzazione propria della classe è eseguita fino a quel momento). Poi si costruisce un oggetto della classe NSFileManager, che ci serve per l'istruzione successive, che recupera le informazioni del file indicato dal path. Ho passato NO come valore del parametro traverseLink:; questo significa che se il file puntato è in realtà un link, le informazioni recuperate sono proprio quelle del link e non quelle del file cui il link si riferisce (perché? Perché così mi andava...nessuna ragione precisa). Dopo di che, si assegnano ordinatamente i valori alle variabili d'istanza. Ci sono quelle di facile assegnamento (il path completo, ed il nome del path), quelle che si ricavano direttamente dall'elenco degli attributi del file (la data di modifica, il nome del proprietario), altri infine che bisogna elaborare prima di assegnare il valore alla variabile (tipo e creatore del file secondo la nomenclatura HFS cara al vecchio sistema operativo). Come si può notare, in ogni caso (anche quelli più banali, che meno richiedono l'accorgimento) utilizzo il metodo accessor piuttosto che assegnare direttamente il valore alla variabile d'istanza (lo avessi fatto, avrei dovuto distinguere la variabili oggetto dalle altre, dal momento che per le prime dovevo anche fare una ritenuta dell'oggetto stesso).

- (id) initWithPath: (NSString*) aFile
{
        NSFileManager *manager = [NSFileManager defaultManager];
        NSDictionary *fattrs = [manager fileAttributesAtPath: aFile traverseLink:NO];
        NSNumber *num ;

        [ super init ];
        [ self setFileFullPath: aFile ];
        [ self setFileName: [aFile lastPathComponent]];
        [ self setModDate: [fattrs fileModificationDate] ];
        [ self setFileSize: [ fattrs fileSize ] ];
        [ self setFgoan: [ fattrs fileGroupOwnerAccountName ] ];
        [ self setFoan:[ fattrs fileOwnerAccountName ] ];
        [ self setFilePosixPerm: [ fattrs filePosixPermissions ] ] ;
        [ self setFileType: [ fattrs fileType ] ] ;
        num = [ fattrs objectForKey: NSFileHFSCreatorCode ] ;
        [ self setCreatorCode: [ num longValue] ];
        num = [ fattrs objectForKey: NSFileHFSTypeCode ] ;
        [ self setTypeCode: [ num longValue] ];
        return ( self );
}

Ora che ho capito come costruire un nuovo oggetto con le informazioni relative ad un dato file, mi segno l'istruzione per fare l'intera operazione

LSFileInfo * fInfo = [[LSFileInfo alloc] initWithPath: aFile];

e mi pongo il problema di come rappresentare l'elenco dei file.

figura 03

figura 03

Ho già stabilito di utilizzare un oggetto della classe NSTableView. Dalla documentazione leggo che devo utilizzare una classe controllore ausiliaria che fornisca i dati. Torno velocemente su IB, dichiaro una nuova classe (LSDataSource), ne faccio una istanza, e la collego alla NSTableView. Meglio: collego l'oggetto NSTableView dell'interfaccia all'oggetto oDataSource (istanza di LSDataSource) utilizzando la connessione dataSource che mi si presenta nel dialogo relativo.

Perché l'oggetto controllore LSDataSource possa funzionare da sorgente di dati per NSTableView, deve realizzare due metodi. Il primo metodo deve dire semplicemente di quante righe è composto l'insieme dei dati da visualizzare. Il secondo metodo è più complesso e merita considerazioni aggiuntive.

Un oggetto NSTableView mostra una serie di dati. I dati sono rappresentati uno per riga, mentre le colonne mostrano i vari attributi di questo dato. Nel nostro caso, il dato è rappresentato dalle informazioni relative ad un file, e le varie colonne contengono i vari campi di informazione relativi.Dal punto di vista costruttivo, una NSTableView è in realtà un insieme cooperante di oggetti: c'è un oggetto header (la riga di intestazione) e ci sono tanti oggetti colonna NSTableColumn. Selezionando infatti ad una ad una le varie colonne, si possono inserire due informazioni cruciali: il nome della colonna, che sarà mostrato a video, e l'identificatore della colonna, utile per capire quale attributo del dato la colonna contiene (perché si usa un identificatore invece che un più semplice indice numerico? Ma perché le colonne possono essere spostate a piacimento dall'utente, con le solite tecniche del trascinamento... ed allora l'indice cambia, mentre la colonna mantiene il proprio identificatore; ovviamente, si possono utilizzare identificatori numerici, ma tanto sono trattati comunque come stringa...). Lascio che il contenuto della cella abbia una rappresentazione standard, e impostando sempre vari attributi tramite IB, impedisco che l'utente possa fare modifiche al contenuto della cella stessa.

A questo punto, torno ai due metodi che si diceva; il primo metodo da definire è il seguente:

- (int) numberOfRowsInTableView: (NSTableView*) tableView

che dice quante sono le righe della tabella. Il secondo metodo è più lungo da scrivere:

- (id) tableView: (NSTableView*) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (int) row

Questo metodo ha come argomenti una riga (row) ed una colonna (appunto identificata come un oggetto NSTableColumn) e deve restituire un oggetto che rappresenti il contenuto della cella così individuata.

Bene. La classe LSDataSource deve rappresentare un elenco di file; quindi, ha come variabile d'istanza un vettore (di dimensioni variabili) di oggetti. Ecco il file LSDataSource.h:

@interface LSDataSource : NSObject
{
        NSMutableArray        *listaFile ;
}
- (id) init ;

- (NSMutableArray *) listaFile ;
- (void) setListaFile: (NSMutableArray *) newListaFile ;
- (void) addFileEntry: (id) newEntry ;
- (int) numberOfRowsInTableView: (NSTableView*) tableView ;        
- (id) tableView: (NSTableView*) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (int) row ;
@end

La variabile d'istanza è il vettore di oggetti; c'è il metodo init (devo inizializzare il vettore) e dealloc (devo disfarmi del vettore allocato da init), i due metodi sopra discussi e un nuovo metodo, addFileEntry, per aggiungere un file nella base di dati. Non parlo dei metodi accessor, che sono banali.

Passiamo alla realizzazione. Il metodo init ormai è un classico:

- (id ) init
{
    self = [ super init ] ;
    listaFile = [ NSMutableArray array ] ;
    [ listaFile retain ];
    return self ;
}

Anche dealloc si fa facile:

- (void) dealloc
{
    [ listaFile release ] ;        
    [ super dealloc ] ;
}

Per aggiungere un elemento ad un vettore NSMutableArray c'è un metodo apposito, addObject, che sfrutto in addFileEntry:

- (void) addFileEntry: (id) newEntry
{
    [listaFile addObject: newEntry ];
}

Sapere quante righe ci sono è facile; basta contare quanti elementi ci sono nel vettore:

- (int) numberOfRowsInTableView: (NSTableView*) tableView
{
    return [ listaFile count ];
}

Ed adesso, la cosa più difficile: restituire il contenuto di una cella:

- (id) tableView: (NSTableView*) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (int) row
{
    NSString * colId = [ tableColumn identifier] ;
    LSFileInfo *fInfo = [ listaFile objectAtIndex: row ];
    return ( [ fInfo valueForKey: colId ] ) ;
}

Tre istruzioni: la prima recupera l'identificatore della colonna. La seconda, l'oggetto corrispondente alla riga indicata. La terza fa tutto il lavoro, ma solo perché sono stato furbo (sì, proprio: ho copiato l'idea dal libro Learning Cocoa).

Ho infatti impostato gli identificatori di ogni colonna con lo stesso nome delle variabili d'istanza corrispondenti. In realtà, posso impostare il nome che voglio, ma poi dovrei scrivere una cosa del tipo:

if ( [ colId isEqual: @"nome prima colonna")
    valore = [ fInfo <estraggo primo attributo>];
else if ( [ colId isEqual: @"nome seconda colonna")
    valore = [ fInfo <estraggo secondo attributo>];

e via così per tutte le possibili colonne. Invece, c'è un trucco. All'interno di un oggetto posso recuperare il valore di una variabile d'istanza se ne conosco il nome (non è certo il metodo più efficiente per farlo, però è mooolto comodo). Ciò si realizza con il metodo valueForKey:.

Con questo trucco, sporco e veloce, la terza istruzione completa il metodo.

C'è un ultimo passo. Mettere tutto assieme all'interno del metodo lsGetfile nell'oggetto FileInfoCtrl. Tralascio la parte di recupero del nome del file, e parto

- (IBAction)lsGetFile:(id)sender
{
    ## parte già trattata ##

    result = [oPanel runModalForTypes: nil];
        
    if (result == NSOKButton) {
        NSArray *filesToOpen = [oPanel filenames];
        NSString *aFile = [filesToOpen objectAtIndex:0];
        LSFileInfo * fInfo = [[LSFileInfo alloc] initWithPath: aFile];

        [ dataSource addFileEntry: fInfo ];
        [ lastFilePath setStringValue: aFile ];
    }
}

figura 04

figura 04

Nel caso ci sia una selezione effettiva (l'utente ha fatto clic sul pulsante di OK), recupero il nome del file, costruisco un nuovo oggetto fInfo con tutte le informazioni relative al file selezionato ed aggiungo tale oggetto alla struttura dati della classe LSDataSource invocando il metodo addFileEntry: sull'outlet dataSource che mi sono premunito, ancora in IB, di collegare.

La cosa buffa e divertente è che non funziona. O meglio, funziona, ma bisogna aiutare la finestra. Infatti, nessuno ha detto alla NSTableView che i dati sono cambiati, e quindi il contenuto della finestra non viene aggiornato. Non appena sposto o ridimensiono la finestra, ecco che compare il nuovo file aggiunto... .

Rimane quindi da aggiungere in IB un altro outlet che connetta la classe controllore con la NSTableView, ed inviare un messaggio di rinfresco dati subito dopo aver inserito un nuovo oggetto:

...
[ dataSource addFileEntry: fInfo ];
[ tableView reloadData ];
[ lastFilePath setStringValue: aFile ];

figura 05

figura 05

Adesso il tutto funziona, anche se sono pieno di dubbi sulle informazioni relative al file (ad esempio, la dimensione del file non ci somiglia per niente...).

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