Cocoa: 1000 finestre sul mondo

Stimolato da una domanda rivoltami da un amico, che mi chiedeva consiglio sul da farsi, ho riflettuto un po' sul problema di creare un'applicazione Cocoa (di quelle a documento singolo) che possa mostrare un numero arbitrario di finestre, anche di tipologia diversa, a seconda delle esigenze dell'utente. Ne è uscito un piccolo progetto dimostratore, MultiWindowsDemo, dal quale ho pensato di ricavare un breve tutorial che vi sottopongo, sperando che vi possa interessare. Per poterlo seguire, è necessario che già abbiate appreso i fondamenti di programmazione Cocoa/Objective-C, che abbiate XCode installato e funzionante e che abbiate dimestichezza col suo funzionamento.

L'idea

Le cosiddette "Cocoa applications" sono applicazioni che non prevedono una struttura a documenti: ciò che propongono è contenuto in una o più finestre, e non c'è necessità di doverne creare di nuove. Esempi tipici di applicazioni che non creano documenti sono iTunes, le Preferenze di Sistema, o la Rubrica Indirizzi. Di contro, esempi di applicazioni che creano documenti multipli sono Safari, Word, o il Terminale. Tuttavia, talvolta capita che anche in un'applicazione che non ha bisogno di gestire documenti sorga la necessità di visualizzare informazioni e dati in modi diversi, che richiedono finestre diverse; addirittura, le modalità di presentazione di informazioni e dati possono essere virtualmente infinite (è l'utente che le decide), e pertanto è necessario che l'applicazione sia in grado di creare e gestire un numero arbitrario di finestre, anche di tipologie diverse, e dal contenuto variabile. Ecco, è di questo che si occuopa MultiWindowsDemo.

L'applicazione

L'interfaccia del nostro programma dimostrativo è ridotta all'essenziale:



Un menu denominato Action permette all'utente di creare, in numero arbitrario, due diverse tipologie di finestre, "rosse" e "verdi". Una finestra denominata Status riporta quante finestre per tipo siano aperte in ogni istante, mentre le due diverse tipologie di finestre si distinguono per i differenti contenuti (e per il colore, bianco, oppure rosso o verde, con cui si possono colorare). Potete creare nuove finestre, e chiuderne di preesistenti, e il conteggio presentato nella finestra Status seguirà fedelmente l'evolversi della situazione. Come potete sperimentare voi stessi (ma dovete prima compilarvi il codice sorgente del progetto, perché l'eseguibile non è incluso nel download), ogni finestra è indipendente da tutte le altre, e potete colorarla o riempirla di bianco a vostro piacimento. Ovviamente, trattandosi di un programma dimostrativo, non fa nulla di particolarmente utile, ma starà a voi scegliere quante tipologie di finestre avere, e che cosa esse debbano contenere.

Il progetto: la classe di controllo

Con XCode, create un nuovo progetto di tipo "Cocoa application" e chiamatelo come volete, ad esempio MultiWindowsDemo. Editate quindi con InterfaceBuilder il suo file di risorse MainMenu.nib (naturalmente, se scaricate il file compresso a cui punta il link all'inizio di questo articolo trovate già tutto fatto; anzi, siete invitati a scaricare il progetto e seguire queste istruzioni sui file già pronti in esso contenuti), in modo da creare il menu Action e la finestra Status: quest'ultima conterrà 4 NSTextField, due con le scritte, e due con i numeri (inizialmente pari a zero). Create una classe di controllo sottoclasse di NSObject (io l'ho chiamata Controller), e dotatela di 2 outlet (uno per ognuno dei due NSTextField che contengono numeri nella finestra Status), e due azioni, che risponderanno alle due voci presenti nel menu Action. Dopo aver creato i file, istanziato la classe di controllo e fatto i collegamenti, salvate le modifiche e tornate ad XCode.
L'interfaccia della classe Controller è molto semplice:

/* Controller */

#import 
#import "RedWindowController.h"
#import "GreenWindowController.h"

@interface Controller : NSObject
{
	NSMutableArray		*redWindows,*greenWindows;
	IBOutlet NSTextField	*numRedWindows,*numGreenWindows;
}

- (IBAction)newGreenWindow:(id)sender;
- (IBAction)newRedWindow:(id)sender;
- (void)closingWindow:(NSNotification *)not;
- (void)updateWindowCount;
@end


Importiamo due file di header che ancora non abbiamo creato, e che sono i file di interfaccia delle due classi che si occuperanno di gestire le due diverse tipologie di finestre ("rosse" e "verdi"), creiamo i due outlet e le due azioni, e aggiungiamo due NSMutableArray che conterranno la lista delle finestre di ogni tipo, un metodo che risponderà ad una notifica ogni volta che una finestra viene chiusa, e un altro metodo che aggiornerà quando necessario i due NSTextField che tengono il conto delle finestre aperte.
L'implementazione della classe Controller non è molto più complicata.

#import "Controller.h"

@implementation Controller

- (id)init
{
	[super init];
	redWindows=[[NSMutableArray alloc] initWithCapacity:1];
	greenWindows=[[NSMutableArray alloc] initWithCapacity:1];
	return self;
}

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


Si parte con del codice assolutamente standard, per l'inizializzazione della classe e delle due NSMutableArray, e relativo codice per la loro distruzione.

- (void)awakeFromNib
{
	[[NSNotificationCenter defaultCenter] addObserver:self
                    selector:@selector(closingWindow:)
                    name:@"closing red window"
                    object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self
                    selector:@selector(closingWindow:)
                    name:@"closing green window"
                    object:nil];
}


Nel metodo -awakeFromNib registriamo la nostra classe di controllo per ricevere due diverse tipologie di notifiche, che genereremo rispettivamente quando l'utente chiuderà finestre "rosse" e "verdi"; il metodo che risponde a queste notifiche è però lo stesso, -closingWindow:. Avremmo naturalmente potuto rispondere ad una sola tipologia di notifica, ed implementare in altro modo (ad esempio nel dizionario userInfo della notifica stessa) l'informazione se ad essere chiusa fosse una finestra "rossa" o "verde", forse sarebbe stato persino più elegante (lasciamo questa miglioria come compito a casa per il lettore).

- (IBAction)newGreenWindow:(id)sender
{
	GreenWindowController		*c;
	
	c=[[GreenWindowController alloc] init:self];
	[c showWindow];
	[greenWindows addObject:c];
	[c release];
	[self updateWindowCount];
}

- (IBAction)newRedWindow:(id)sender
{
	RedWindowController		*c;
	
	c=[[RedWindowController alloc] init:self];
	[c showWindow];
	[redWindows addObject:c];
	[c release];
	[self updateWindowCount];
}


Il codice per la creazione delle finestre, e che risponde all'apposito comando del menu Action, è ovviamente uguale, salvo minime variazioni, per le due tipologie di finestre. Un nuovo oggetto, sottoclasse di NSWindowController, viene creato e inizializzato, ed aggiunto all'array opportuna (redWindows oppure greenWindows), così che la classe Controller possa mantenere efficacemente traccia di tutte le finestre, di ogni tipologia, che sono state create. Una chiamata a -updateWindowCount permette di aggiornare il contenuto della finestra Status.

- (void)closingWindow:(NSNotification *)not
{
	if([[not name] caseInsensitiveCompare:@"closing red window"]==NSOrderedSame)
		[redWindows removeObject:[not object]];
	else if([[not name] caseInsensitiveCompare:@"closing green window"]==NSOrderedSame)
		[greenWindows removeObject:[not object]];
	[self updateWindowCount];
}


Il metodo che risponde alle notifiche non fa altro che verificare di quale notifica si tratti (se di quella generata dalla chiusura di una finestra "rossa" o di quella generata dalla chiusura di una finestra "verde"), e non fa altro che rimuovere l'oggetto che ha generato la notifica (la sottoclasse di NSWindowController che abbiamo generato in precedenza) dall'apposita array. Anche in questo caso, una chiamata a updateWindowCount aggiorna il contenuto della finestra Status.

- (void)updateWindowCount
{
	[numRedWindows setIntValue:[redWindows count]];
	[numGreenWindows setIntValue:[greenWindows count]];
}

@end


-updateWindowCount non fa altro che contare gli elementi contenuti nelle due array redWindows e greenWindows, e riportare tale numero nei due NSTextField della finestra Status.

Il progetto: le finestre "rosse"

Creiamo ora tutto ciò che riguarda le finestre "rosse", sia le risorse che il codice. Tornate a InterfaceBuilder, e create un nuovo file nib di tipo "Cocoa Window", che aggiungerete al progetto MultiWindowsDemo. Chiamate RedWindow.nib tale file di risorse. Come prima cosa, sottoclassate NSView e date alla nuova classe il nome RedView. Create i file per RedView, e istanziatela. Quindi sottoclassate NSWindowController e date alla nuova classe il nome RedWindowController. Essa avrà due outlet, uno di classe NSWindow che punterà alla finestra, e l'altro di classe RedView, che punterà ad una custom view che aggiungerete alla finestra (potete consultare il mio tutorial Visto che vista? per maggiori informazioni su come si creano e gestiscono le custom view). RedWindowController avrà anche due azioni, che risponderanno ai due pulsanti che aggiungerete alla finestra stessa. Accertatevi di assegnare alla custom view la classe RedView, e di assegnare al File's Owner di RedWindow.nib la classe RedWindowController. Fate tutti i collegamenti (anche di outlet e azioni del File's Owner!), create tutti i file che ancora non avete creato, e accertatevi di impostare RedWindowController come delegato della finestra. Per concludere salvate le modifiche.
Nuovamente in XCode, procedete ad editare il file di interfaccia della classe RedWindowController:

/* RedWindowController */

#import 
#import "RedView.h"

@interface RedWindowController : NSWindowController
{
	IBOutlet NSWindow		*window;
	IBOutlet RedView		*drawingRegion;
}

- (id)init:(id)sender;
- (void)showWindow;
- (IBAction)disegna:(id)sender;
- (IBAction)cancella:(id)sender;

// NSWindow delegate methods
- (void)windowWillClose:(NSNotification *)not;

@end


Qui importiamo il file di interfaccia della custom view, definiamo i due outlet e le due azioni, e naturalmente il metodo di inizializzazione. Un metodo ulteriore (-showWindow) è quello che viene chiamato da -newGreenWindow: e -newRedWindow: di Controller.m per mostrare a schermo la finestra. Infine, il metodo -windowWillClose: verrà qui implementato, dal momento che in InterfaceBuilder abbiamo chiesto che RedWindowController fosse delegato di NSWindow.
L'implementazione è nuovamente molto semplice:

#import "RedWindowController.h"
#import "Controller.h"

@implementation RedWindowController

- (id)init:(id)sender
{	
	[super initWithWindowNibName:@"RedWindow"];
	return self;
}


L'inizializzazione si fa col metodo -initWithWindowNibName: di NSWindowController, che ci permette di caricare da un apposito file di risorse una finestra (quante volte vogliamo) con tutti i suoi annessi e connessi (custom view, classi di controllo, ecc.).

- (void)showWindow
{
	[window setWindowController:self];
	[self showWindow:self];
	[window makeKeyAndOrderFront:self];
}


Quando Controller.m crea il controllore della finestra, subito dopo la mostra a schermo; è in -showWindow che associamo alla finestra il controllore (mediante -setWindowController:), e poi naturalmente provvediamo a mostrare la finestra a schermo.

- (void)windowWillClose:(NSNotification *)not
{
	[[NSNotificationCenter defaultCenter] postNotificationName:@"closing red window"
                        object:[[not object] windowController]
                        userInfo:nil];
}


Quando l'utente clicca sul pulsante di chiusura della finestra, il metodo del delegato windowWillClose: viene invocato; qui solleviamo la notifica che Controller.m intercetterà, mettendo come oggetto proprio il window controller ([[not object] windowController]), così che sia esattamente lo stesso oggetto contenuto nell'array redWindows della classe Controller e possa quindi essere facilmente identificato e rimosso.

- (IBAction)disegna:(id)sender
{
	[drawingRegion draw];
}

- (IBAction)cancella:(id)sender
{
	[drawingRegion clear];
}

@end


Le operazioni di disegno della custom view sono delegate alla classe RedView, che qui ci limitiamo a chiamare con due metodi di comodità che rispondono rispettivamente al pulsanti Disegna e Cancella della finestra.
L'interfaccia della classe RedView è ancora più semplice:

/* RedView */

#import 

@interface RedView : NSView
{
	BOOL		draw;
}

- (void)draw;
- (void)clear;

@end


I due metodi -draw e -clear sono qui definiti, e una variabile di tipo BOOL viene definita per sapere se bisognerà colorare la custom view di rosso o di bianco. Naturalmente, se prevedete di disegnare forme più complesse nella vostra custom view probabilmente dovrete utilizzare una NSMutableArray che contenga dei NSBezierPath, ma tutte queste cose oltrepassano lo scopo di questo tutorial, e le potete trovare ad esempio nel già citato tutorial intitolato "Visto che vista?". L'implementazione di RedView non riserva certo sorprese:

#import "RedView.h"

@implementation RedView

- (id)initWithFrame:(NSRect)frameRect
{
	if ((self = [super initWithFrame:frameRect]) != nil)
	{
		draw=NO;
	}
	return self;
}

- (void)drawRect:(NSRect)rect
{
	NSBezierPath		*bp;
	NSColor				*c;
	
	if(draw)
		c=[NSColor redColor];
	else
		c=[NSColor whiteColor];
	[c set];
	[c setFill];
	bp=[NSBezierPath bezierPathWithRect:[self bounds]];
	[bp stroke];
	[bp fill];
}

- (void)draw
{
	draw=YES;
	[self setNeedsDisplay:YES];
}

- (void)clear
{
	draw=NO;
	[self setNeedsDisplay:YES];
}

@end


L'inizializzazione è assolutamente standard, viene solo impostata a NO la variabile draw, che poi i metodi -draw e -clear modificheranno in maniera opportuna. Il metodo -drawRect: altro non farà che riempire di un colore uniforme (bianco o rosso) l'intera custom view.

Il progetto: le finestre "verdi"

La creazione del file di risorse e dei file di codice per le finestre "verdi" segue passi assolutamente identici a quelli che abbiamo percorso per le finestre "rosse", salvo le evidenti modifiche di aspetto della finestra, di nomi di metodi e variabili, e del colore di riempimento. Naturalmente, il vostro progetto potrebbe prevedere che finestre di tipologie diverse contengano dati o informazioni molto diversi tra di loro; in questo caso, comunque, non c'è di che preoccuparsi: per ogni tipologia di finestra avete, nel rispettivo file nib, anche l'apposita sottoclasse di NSWindowController, che potrete utilizzare per rispondere a tutti gli eventi riguardanti la finestra; il fatto che ci sia una custom view è naturalmente un fatto assolutamente accidentale, nel vostro caso potreste avere tabelle, web view, filmati, testo o chissà che altro.

Conclusioni

Seppur molto scarno, MultiWindowsDemo mostra come sia possibile gestire un numero arbitrario di finestre, anche di tipologie diverse, in un'applicazione che non supporta documenti multipli. Ma in realtà, ciò che abbiamo fatto qui può essere replicato anche per applicazioni che supportano documenti multipli, là dove ogni documento si possa articolare in una moltitudine di finestre diverse. La generalità dell'approccio che abbiamo utilizzato consente di non avere vincoli particolari quando si desidera adattare questo progetto alle proprie esigenze. La classe principale di controllo mantiene rigorosamente traccia di tutte le finestre che vengono create e distrutte, e può comunicare coi rispettivi controllori, i quali però sono poi interamente responsabili del comportamento della loro finestra. Tutto ciò che è di competenza di una tipologia di finestre, tranne naturalmente i dati che questa dovrà mostrare (che nel paradigma MVC provengono come sapete dal model) è contenuto in un unico file di risorse (nib), che potrà poi naturalmente essere localizzato.

Buona programmazione!