MaCocoa 038

Capitolo 038 - Ho una vista

Questo capitolo, piuttosto discorsivo, introduce l'argomento viste, ovvero la classe NSView e le sue sottoclassi, punto di partenza per disegnare qualcosa sullo schermo.

Leggo la documentazione...

Primo inserimento: 8 gennaio 2004

Quattro Classi Basilari

Una applicazione è, nella sua essenza, costruita da tre classi di oggetti: NSApplication, NSWindow e NSView. Questi classi sono tutte figlie della classe NSResponder (a sua volta derivata da NSObject). Queste quattro classi sono in grado di fornire le funzioni di base necessarie al funzionamento di una applicazione, alla rappresentazione a schermo di elementi di interfaccia e per l'interazione dell'utente.

La classe NSResponder è in grado di gestire gli eventi (clic del mouse, caratteri immessi da tastiera, eccetera). Fornisce i metodi di base per la gestione degli eventi ed il meccanismo con cui gli eventi sono distribuiti ai vari oggetti costituenti l'applicazione.

Ogni applicazione possiede uno ed uno solo oggetto della classe NSApplication: è quello costruito dalla funzione NSApplicationMain chiamata all'interno del file main.m. L'oggetto supervisiona e coordina il lavoro complessivo dell'applicazione. Distribuisce ogni evento alla finestra alla quale pertiene, gestisce le finestre, i documenti, eccetera.

Le attività di una applicazione si svolgono normalmente all'interno di una finestra, oggetti di classe NSWindow. Un oggetto NSWindow gestisce una finestra sullo schermo, o meglio il contenuto ed il comportamento della finestra: discegna il contenuto e risponde alle azioni di spostamento, chiusura, ridimensionamento ed altre manipolazioni. Scopo principale di una finestra è di mostrare l'interfaccia utente (o parte di essa) al suo interno.

Arrivo finalmente agli oggetti NSView. Ogni oggetto che si può vedere all'interno di una finestra è una istanza di una sua sottoclasse (raramente un oggetto NSView è utilizzato così come si trova). Ogni NSView è rappresentato all'interno di una regione rettangolare, ed è responsabile di come sia visualizzata e di come interagisce con l'utente. Ad esempio un pulsante (NSButton) è (alla fin fine) una sottoclasse di NSView, in grado di rappresentarsi secondo l'iconografia classica di un pulsante, e di interagire con il mouse (o la tastiera) nella modalità cui siamo abituati.

Tutti gli oggetti NSView sono organizzati all'interno di una finestra secondo una gerarchia ad albero. Ogni view è dunque racchiusa all'interno di una view di livello superiore (superview) e possiede al suo interno zero, una o più view (subview). Ad esempio, nella finestra ListWinCtl la view principale (la content view della finestra) possiede come subview la barra di stato, e la NSOutlineView. A sua volta, la NSOutlineView possiede come subview le barre di scorrimento, la view d'angolo, le colonne, eccetera.

Ogni view all'interno della gerarchia è rappresentata all'interno della sua superview. Dato il rettangolo della superview, ogni sua subview può essere rappresentata solo all'interno di questo rettangolo. Associata ad ogni view ci sono due rettangoli: il primo rettangolo, frame, determina la posizione della view all'interno della sua subview; il secondo rettangolo, bounds, determina la geometria proprio della view. In altre parole, il frame è utilizzato dalla superview per riservare lo spazio alla view, mentre la view rappresenta se stessa all'interno del rettangolo bounds.

Le view rappresentano se stesse come risposta indiretta al messaggio display (o simili), generalmente gestito automaticamente dall'ambiente operativo. Il messaggio porta all'esecuzione del metodo drawRect: per la view e, in successione, per tutte le subview contenute. All'interno di questo metodo devono trovarsi tutte le istruzioni per il disegno a video del contenuto della view.

Coordinate

Per il posizionamento corretto di finestre e view, occorre un sistema di coordinate, che permette di disporre nei posti giusti i rettangoli frame. Cocoa utilizza diversi sistemi di coordinate: per lo schermo, per le finestre, per le viste. In realtà, seguono la stessa logica.

Si recupera il concetto di coordinate cartesiane. Occorre specificare una origine, i due assi cartesiani, e le unità di misura. Nel caso delle coordinate per lo schermo, l'origine è il punto in basso a sinistra dello schermo. L'asse delle ascisse (orizzontale) va verso destra, l'asse delle ordinate (verticale) va verso l'alto. L'unità di misura è il pixel. Tuttavia, essendo le coordinate espresse come numeri floating point, si possono utilizzare anche dimensioni frazionarie, lasciando al motore di visualizzazione gli arrotondamenti per la visualizzazione sullo schermo. Queste coordinate sono utilizzate per posizionare le finestre sullo schermo.

Le coordinate per le finestre sono molto simili: l'origine è il punto in basso a sinistra della finestra, gli assi cartesiani vanno verso destra e verso l'alto. Queste coordinate rimangono attaccate alla finestra, dovunque questa finestra sia spostata. In altre parole, le coordinate degli oggetti (NSView) all'interno di una finestra rimangono sempre le stesse, anche se la finestra è spostata e le posizioni assolute di questi oggetti nello schermo è cambiata.

Le coordinate per le view funzionano come per quelle delle finestre: hanno origine nel punto in basso a sinistra della view, e gli assi viaggiano normalmente verso destra e verso l'alto. Anche qui, le coordinate degli oggetti contenuti all'interno di una view rimangono fissi, anche se la view è spostata per qualsiasi motivo.

In definitiva, ogni view si porta appresso un proprio sistema di riferimento locale. Questo fatto ha una importante conseguenza sulle operazioni di disegno: le istruzioni di disegno rimangono sempre le stesse, trascurando ogni mutamento nella posizione della view o della catena di superview.

Ora che ho introdotto il concetto di coordinate, è più facile spiegare la differenza tra i rettangoli frame e bounds. Il rettangolo frame determina la posizione della view all'interno della superview. Rappresenta quindi le coordinate del rettangolo della view espresse nel sistema di riferimento della superview. Il rettangolo bounds invece rappresenta la porzione potenzialmente visibile della view espressa nelle coordinate della view stessa (potenzialmente visibile, perché lo spazio potrebbe estendersi fuori della superview o essere coperto da altre subview).

La Scroll View

Una view che è spesso utilizzata è la NSScrollView. Una scrollview permette di visualizzare view piuttosto grandi all'interno di finestre più piccole. Ogni volta che in una finestra vedete delle barre di scorrimento, abbiamo a che fare con una scrollView (beh, quasi sempre). Una scrollView è una view che gestisce una collezione di subview. Ci sono due NSScroller, due viste nate per gestire una barra di scorrimento; è possibile prevedere anche uno o due righelli NSRulerView (o sottoclassi), utili per dimensionare il contenuto della view; tipicamente c'è una NSClipView, che gestisce quale porzione del documento è visualizzata (raramente si ha a che fare con questa view direttamente); finalmente, c'è la document view, ovvero la view effettiva che contiene ciò che si intende visualizzare. L'idea di base è molto semplice: La document view funziona come se non sapesse di essere visualizzata solo in parte (o meglio, potrebbe funzionare: spesso ragioni di efficienza richiedono che la document view sia consapevole della zona rappresentata); la clipView limita al rappresentazione all'area visibile, e la scrollView gestisce le eventuali barre di scorrimento ed i righelli.

Ho chiamato la view interna genericamente 'document view' perché questa view può essere di tipo qualsiasi. Ad esempio, sto scrivendo questo testo con un semplice editor di testi. La finestra del documento è una unica NSScrollView, all'interno della quale ci sono barre di scorrimento (una sola, verticale) ed un righello (uno solo, orizzontale). Il testo vero e proprio si trova all'interno di una NSTextView. Se l'applicazione fosse invece di disegno, la finestra sarebbe ancora una scrollView, ma il disegno vero e proprio si troverebbe in una view specifica per disegnare (che non c'è, quindi, bisogna che il programmatore se la costruisca).

Disegnare in una view

La visualizzazione del contenuto di una view avviene attraverso il metodo drawRect:. All'interno di questo metodo trovano posto tutte le istruzioni per il disegno, ovvero i comandi per Quartz. Infatti, per disegnare in una finestra non si devono pilotare direttamente i pixel di uno schermo, ma si scrivono una serie di comandi di disegno, specifici di Quartz; è poi compito dell'ambiente operativo trasformare questi comandi in effettive rappresentazioni a schermo. Ma questo non è vero; siamo in un ambiente OOP, quindi, si ha a che fare con una serie di classi, di oggetti e di messaggi per il disegno.

Prima di cominciare, occorre conoscere come rappresentare i dati. Ho già parlato del sistema di coordinate cartesiane; è quindi piuttosto ovvio che un punto sia un dato di tipo NSPoint (attenzione, non è un oggetto), costituito da due coordinate

typedef struct _NSPoint {
    float    x ;
    float y ;
}    NSPoint ;

Molti costrutti si basano poi sul concetto di dimensione, il dato NSSize:

typedef struct _NSSize {
    float    width ;
    float    height ;
}    NSSize ;

Ad esempio, un rettangolo è compiutamente specificato da un punto e dalla dimensione:

typedef struct _NSRect {
    NSPoint    origin ;
    NSSize    size ;
}    NSRect ;

Esistono poi funzioni (o macro; non sono metodi) di convenienza per costruire punti, dimensioni e rettangoli: NSMakePoint, NSMakeSize, NSMakeRect.

Ci sono fondamentalmente tre tipi di elementi che possono essere disegnati: elementi grafici quali linee, rettangoli, cerchi, eccetera; caratteri di testo; immagini complesse. Ogni elemento ha le sue istruzioni caratteristiche.

Per disegnare elementi grafici quali linee, rettangoli, archi, eccetera, si ricorre all'oggetto NSBezierPath. Per ogni elemento grafico ci costruisce un oggetto di questo tipo con metodi di convenienza; ad esempio, una volta definito un quadrato di cento pixel di lato, lo si può disegnare 'pieno' a partire dal punto (10,10) con l'istruzione

[ NSBezierPath fillRect: NSMakeRect( 10, 10, 100, 100) ];

Esiste poi una lunga serie di metodi per costruire percorsi a forma di linea, di arco, per costruire percorsi complessi, eccetera, che vedrò man mano che servono.

Per disegnare testo all'interno di una view, occorre in primo luogo avere un oggetto di tipo NSString. Poi occorre specificare carattere, dimensione e tutti gli altri attributi che si desiderano, immagazzinandoli tipicamente all'interno di un oggetto NSMutableDictionary. A questo punto si aprono due strade equivalenti; la prima è di costruire un oggetto NSMutableAttributedString a partire dalla stringa e dagli attributi, ed utilizzare questo oggetto come destinatario del messaggio drawAtPoint:. Alternativamente (e più semplicemente, mi pare), si usa direttamente la stringa ed un messaggio diverso:

[ miaStringa drawAtPoint: NSMakePoint( 10,10) withAttributes: miaStringaAttributi ];

Infine, per disegnare immagini sullo schermo, si parte da un oggetto di classe NSImage. Il processo utilizzato si chiama compositing e consiste nel sovrapporre in modo determinato due immagini: quella già presente sulla view e quella nuova che si intende disegnare. La determinazione del modo avviene secondo diverse modalità (distruggendo la parte coperta, mischiando in modo opportuno, mantenendo la trasparenza, eccetera). Piuttosto che dilungarmi oltre, ritardo l'esame della cosa quando mi servirà.

Perché?

Perché ho scritto tutte queste cose? Finora, tutte le view utilizzate erano già prefabbricate: NSButton, NSOutlineView, NSTableView, eccetera, tutte queste view erano già complete e definite; si trattava solo di metterle assieme e di collegare tra loro i posti rimasti liberi. Adesso voglio avventurarmi nella costruzione di view particolari, non predefinite.

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