MaCocoa 009

Capitolo 009 - Finalmente Cocoa

Dove il romanzo giustifica il suo titolo (epigrafe del Capitan Fracassa).

Sorgenti: C'era un tutorial dal nome Currency Converter Tutorial all'interno della documentazione dei Developer's Tools; adesso si trova tutto all'interno di Reference Library / Cocoa Fundamentals / Objective-C, Developing Cocoa Objective-C Applications: A Tutorial

Primo inserimento: 12 Novembre 2001

Ritocchi per aggiornamento XCode 2.1: 8 Agosto 2005

Finalmente Cocoa

È tempo di scrivere la prima applicazione Cocoa. Riprendo la classe Contatore: voglio metterci una bella (!) interfaccia grafica. Ho quindi una finestra all'interno della quale visualizzo il valore corrente del contatore, più due pulsanti, uno per incrementare il contatore, e l'altro per decrementarlo. Fine. Prima, però, bisogna parlare di Interface Builder.

Interface Builder

Interface Builder, per gli amici IB, è una applicazione per costruire l'interfaccia di applicazioni. È una sorta di ambiente visuale per la costruzione di applicazioni, che si integra facilmente con XCode.

Con IB si costruisce l'interfaccia pigliando gli elementi dell'interfaccia, pulsanti, campi di testo, caselle, eccetera, da una serie di palette, e trascinandoli sulla finestra principale della costruenda applicazione. C'è la possibilità di definire nuove finestre, menù e tutto quanto concorre alla rappresentazione grafica di una applicazione. Di più, si possono inserire alcuni legami tra i vari elementi dell'interfaccia, in modo che operando su un elemento accada qualcosa sugli elementi collegati. In altre parole, c'è la possibilità in IB di mandare messaggi tra i vari oggetti che costruiscono l'interfaccia.

File NIB

Abbiamo alcuni concetti da esplorare prima di avventurarci in pratica. Il primo concetto è il file NIB (che dovrebbe stare per Nextstep Interface Builder). Un file nib è associato ad una finestra, o meglio, è un archivio di oggetti generati attraverso IB. Ovvero, IB salva tutto ciò che è in grado di manipolare all'interno di un file nib. Un file nib in genere raggruppa una serie di elementi (oggetti) dell'interfaccia che fanno logicamente parte della stessa parte di programma: mi figuro che una finestra, con tutti i controlli relativi, sia contenuta all'interno di un file nib. Esiste un file nib principale, aperto al lancio dell'applicazione, che contiene il menu dell'applicazione e, verosimilmente, la finestra principale. Pare che convenga dividere le varie finestre di una applicazione in diversi file nib, in modo che siano caricati solo all'occorrenza e non sempre.

All'interno di un file nib possono essere conservati anche gli outlet (ne abbiamo già parlato), ovvero delle variabili che conservano un riferimento ad un altro oggetto dell'interfaccia.

Target/Action

Il secondo concetto che ci occorre è quello di Target/Action. Tipicamente, quando ho un controllo all'interno di una interfaccia (diciamo un pulsante), l'attivazione di questo controllo richiede che sia eseguita una qualche operazione. All'interno del paradigma Object Oriented, stiamo parlando di un oggetto (il pulsante), che, in risposta ad una azione dell'utente, invia un messaggio opportuno a qualche altro oggetto, che si preoccuperà di portare avanti le operazioni. L'oggetto destinatario è il target dell'azione, e l'azione è in pratica il messaggio comunicato. IB mette a disposizione un metodo grafico per realizzare questi collegamenti.

Una Applicazione Cocoa

figura 01

figura 01

A questo punto, parto con la nostra applicazione. Apro Xcode e specifico un nuovo progetto, questa volta una Cocoa Application. Xcode, di suo, ci mette un po' di cartelle e qualche file predefinito. Ho una cartella Classes dove sono (saranno) inserite le Classi da me definite e che costituiscono l'applicazione. Nella cartella Other Sources c'è il file main.c. A vederlo, non è che ci sia molto da modificare, per il momento (è una riga sola):

#import <Cocoa/Cocoa.h>

int main(int argc, const char *argv[])
{
    return NSApplicationMain(argc, argv);
}

La cartella Framework contiene appunto i framework Foundation, Application e Cocoa: possiamo intenderli come librerie necessarie alla realizzazione di una applicazione Cocoa. Sono lì contenuti tutti gli oggetti che a vario titolo partecipano ad una applicazione sviluppata in Cocoa. La cartella Products contiene nulla al momento (il file indicato in rosso in realtà non esiste ancora), ma alla fine conterrà il risultato del lavoro (l'applicazione vera e propria, insomma).

Ho lasciato per ultima la cartella Resources sulla quale appunto l'attenzione. Lascio per il momento da parte il file InfoPlist.string, e noto il file MainMenu.nib. Facendo un doppio clic su di esso, si apre Interface Builder.

figura 02

figura 02

Si apre un'insieme di finestre. Ce ne sono due per la costruzione dell'interfaccia, e due di ausilio per tale operazione.

figura 03

figura 03

La prima finestra, larga e stretta, denominata MainMenu.nib - MainMenu, è un po' speciale perché contiene i menu che fanno parte dell'applicazione. Con un po' di pazienza e un uso dosato dei clic del mouse, è possibile costruire il menu, voce per voce, completo di scorciatoie da tastiera. Allo scopo, è molto utile aprire una ulteriore finestra, dal menu Tools, la voce Show Inspector. In questa finestra, dal contenuto variabile in dipendenza dall'elemento selezionato, si trovano, in diversi pannelli, le opzioni di configurazione dell'elemento stesso (nel caso, la voce del menu).

La seconda finestra è proprio una finestra, ovvero il punto di partenza di costruzione dell'applicazione. Questa finestra è ancora vuota, ma si può utilizzare la palette delle Cocoa-view per aggiungere elementi dell'interfaccia.

Questa palette, divisa in diversi pannelli, contiene infatti, raggruppati per tipo, i vari elementi che possono entrare a fare parte dell'interfaccia di una finestra: bottoni, campi testo, icone, eccetera. Non mancano elementi quali finestre ed interi menu da aggiungere ai menu standard. Con la semplice tecnica del drag and drop si possono trasferire questi elementi dalla palette alla finestra (o alla diverse finestre) dell'applicazione.

L'interfaccia di contatore

La mia prima applicazione è piuttosto semplice, anche per l'interfaccia. Si diceva che si voleva un campo testo dove rappresentare il valore corrente del contatore e due pulsanti, uno per incrementare il valore e l'altro per decrementarlo. Una scritta aggiuntiva e decorativa completa l'interfaccia.

figura 04

figura 04

La finestra da me disegnata è piuttosto orribile a vedersi, e non dovrebbe essere difficile fare qualcosa di più coreografico e piacevole alla vista. Ci sono alcune cose da notare:

La Classe controllante

Ad un certo punto della storia avevo introdotto il modello MVC (Model-View-Controller) come un utile paradigma per la realizzazione di applicazioni object oriented, particolarmente utile nei casi in cui si ha a che fare con una interfaccia grafica. È giunto il momento di applicare questo paradigma. Avendo costruito con IB l'interfaccia, ho già definito tutte le classi di tipo View interessate all'applicazione: sono i pulsanti, la finestra, eccetera.

Ho già oltretutto le classi della parte Model: in realtà è un'unica sola, è proprio la classe Contatore già realizzata (e che adesso riuso tale e quale!!!). Mi mancano le classi di tipo controllore che servono a legare il tutto. Alcune classi controllore non si vedono neppure: sono quelle che si occupano per noi delle funzioni nascoste dell'interfaccia (aprire i menu, passare le richieste alle finestre, eccetera). Mi interessa in realtà una unica classe controllore per mettere in comunicazione le viste (l'interfaccia) con il modello (il contatore). Devo cioè definire una classe che si occupi di mediare le richieste dell'utente (clic sul pulsante Incrementa o sul pulsante Diminuisci) e le funzionalità svolte da Contatore. Posso fare questa cosa direttamente all'interno di IB.

Seleziono il pannello Classes dalla finestra MainMenu.nib. Compare un elenco di tutte le classi disponibili (rappresentate in forma gerarchica, in una lista normale o nelle colonne). Mi posiziono su di una queste classi, che sarà la superclasse della nostra nuova classe. Poi, con menu contestuale o dal menu Classes, dico di volere una sottoclasse. Ecco che si costruisce una nuova classe nell'elenco, alla quale posso dare il nome che voglio.

Ho costruito una classe dal nome CountCtrl figlia diretta di NSObject. L'idea della classe controllore CountCtrl è quella di fungere da centro di smistamento. Gli elementi dell'interfaccia grafica mandano i loro messaggi a CountCtrl. Questa classe interagisce con Contatore per effettuare le operazioni richieste; il risultato (il valore del contatore) è restituito ancora una volta a CountCtrl che lo trasferisce nuovamente all'interfaccia.

Perché Contatore non è collegato direttamente all'interfaccia (come programmaticamente farebbe ogni programmatore in programmazione procedurale)? Perché seguendo la filosofia della OOP, Contatore non deve sapere, non è proprio interessato, anzi, è meglio che non sappia, cosa viene fatto del suo valore. È bene che questo valore sia inviato alla classe controllore, che si occuperà di tutta la sporca faccenda. In questo modo la classe Contatore potrà essere utilizzata con successo molte altre volte, non essendo legata al particolare uso che ne viene fatto. Invece, la classe controllore è molto legata all'applicazione all'interno della quale è nascosta, ed è quindi di difficile (per non dire impossibile) riusabilità. Ma questo è poco importante, visto che la classe controllore si occupa in genere semplicemente di dirigere il traffico, di mettere in bella copia ed evitare pasticci. Cose in genere semplici che si fan presto a fare. In genere, sono le classi modello che contengono tutta la complessità della faccenda.

Detto questo, e creata la classe CountCtrl, vediamo di farle fare qualcosa. In primo luogo, faccio in modo che i pulsanti interagiscano con il controllore. Ci aiuta il paradigma target/action sopra discusso. Il pulsante Incrementa dovrà in qualche modo individuare la classe CountCtrl come target di una azione di incremento del contatore, ovvero, bisogna fare in modo che, una volta cliccato, il pulsante invia un messaggio 'incrementa' all'oggetto CountCtrl.

Un momento, non ce l'ho un oggetto destinatario (ho solo dichiarato la classe!). Bene, vado nella vista classi, seleziono la classe CountCtrl, e attraverso menu contestuale o voce relativa nel menu Classes dico Instanziate....

figura 05

figura 05

La finestra passa subito alla vista Instances dove mostra l'icona di un cubetto dal nome CountCtrl. Questa è l'istanza della classe (classe ed oggetto hanno lo stesso nome... non è una bella cosa, potrebbe ingenerare confusione, ma poiché c'è una sola istanza della classe, lo sopporto). Ma io torno indietro alla vista per Classi, perché devo ancora perfezionare la dichiarazione della classe CountCtrl.

In primo luogo, questa classe deve essere punto di riferimento (target) di due azioni, una per incrementare il contatore countUp e una per decrementarlo countDown. In secondo luogo, deve stabilire un legame (cntDisp) con l'oggetto dell'interfaccia in grado di visualizzare il valore del contatore, cioè il campo di testo.

Detta in altre parole: occorre dichiarare due metodi countUp: e countDown: ed una variabile d'istanza cntDisp. IB chiama queste due cose rispettivamente una Action e un Outlet.

Parentesi terminologica. Non è che Action sia un altro nome per Metodo e Outlet un altro nome per variabile d'istanza. Una Action è un metodo utilizzato per comunicazioni tra oggetti facenti parte dell'interfaccia, perché tipicamente richiesta dall'utente tramite una azione volontaria (un clic). Un Outlet è una variabile d'istanza destinata a contenere un (puntatore ad un) oggetto, utilizzato ancora una volta per la realizzazione di una interfaccia. Fine parentesi.

figura 06

figura 06

Esistono diversi metodi per definire Action e Outlet, però poi tutti portano allo stesso posto, nella palette dello Inspector, sul pannello Attributes. Con i pulsanti appositi, aggiungo le due action e il singolo outlet.

figura 07

figura 07

Infine, faccio i collegamenti tra le classi View e la classe Controller. Attenzione al meccanismo: seleziono il pulsante Incrementa. Tengo premuto il tasto 'control'. Trascino il mouse sull'istanza CountCtrl (dentro la finestra principale). IB disegna una linea grigia che collega i due oggetti, e contestualmente la finestra Inspector diventa quella delle 'Connections' dell'oggetto pulsante. La voce Targetè evidenziata, e sono disponibili le due scelte, countUp e countDown, che avevo immesso in precedenza per l'oggetto CountCtrl. Seleziono countUp e faccio clic sul pulsante Connect.

Tutto questo lavorio (che ci si mette diecimila volte più tempo a scrivere che a fare) è semplicemente un metodo grafico per dire che l'oggetto pulsante Incrementa, quando cliccato, manda un messaggio countUp: all'oggetto CountCtrl.

Faccio la stessa cosa (ma con l'action countDown) per il pulsante Decrementa.

figura 08

figura 08

Poi passo all'outlet, ovvero al collegamento tra il campo testo dove si trova il contatore e la classe controllore. Adesso faccio prima clic sull'istanza di CountCtrl e poi, tenendo premuto il tasto 'control', mi sposto sul campo testo. Questa volta la finestra Inspector si apre sulle Connections di CountCtrl e mostra disponibile la sola voce outlet cntDisp. Faccio clic sul pulsante connect ed ecco che ho assegnato un valore alla variabile d'istanza. Questo valore è proprio l'oggetto campo testo che mi interessa manipolare.

A questo punto l'interfaccia della nostra applicazione è finita. Bisogna tornare in Xcode per fare i collegamenti tra la classe controllore e la classe modello.

Ma prima, il tocco finale di magia di IB. Seleziono dalla vista per classi della finestra MainMenu.nib la classe CountCtrl. Con menu contestuale o equivalente, scelgo Create files for.... IB costruisce automaticamente due file, CountCtrl.h e CountCtrl.m, in cui scrivere i dettagli finali della classe, già completi delle informazioni già note (i due metodi countUp: e countDown: e la variabile outlet).

Adesso si tratta di riempire gli spazi vuoti.

In primo luogo, aggiungo i file della classe Contatore al nostro progetto. Uso il menu Project e la voce Add To Project.... È bene avere selezionato la cartella Sources in modo che vengano piazzati nel posto più logico. C'è comunque la possibilità di spostarli in un momento successivo. I due file della classe CountCtrl dovrebbero essere già al posto giusto. Ma adesso abbiamo due problemi.

Il fabbricante di oggetti

Il primo problema è posto dalla domanda: chi costruisce gli oggetti? Per quanto riguarda tutti gli oggetti presenti nel file nib, cioè gli oggetti dell'interfaccia, chi costruisce gli oggetti dovrebbe essere l'applicazione stessa. Mi aspetto che la struttura di programmazione alla base di Cocoa si occupi di costruire i vari oggetti (pulsanti, campi di testo, eccetera) che ho preventivamente definito all'interno di IB. Poiché CountCtrl è una classe definita all'interno di IB, l'oggetto di tale classe presente nel file nib è costruito al lancio dell'applicazione. Brilla per la sua assenza l'oggetto Contatore che ci occorre per fare i calcoli.

Non ho al momento idea di quale sia il metodo migliore o quello consigliato da Cocoa per costruire l'oggetto Contatore. Ciò che mi è venuto in mente non mi piace particolarmente, ed è suscettibile di revisioni. Tuttavia, il metodo che ora illustro ha il pregio di introdurre un nuovo ed importante concetto di Objective C.

Uno dei metodi di NSObject, ovviamente ereditato da tutte le classi, è il metodo init. Con questo metodo si realizzano tutte le inizializzazioni interne dell'oggetto; ad esempio, è il posto migliore dove mettere i valori di default delle variabili d'istanza. Ricordo anche che, per costruire un oggetto, prima bisogna inviare un messaggio alloc alla classe (che predispone lo spazio in memoria per conservare l'oggetto), e poi appunto il messaggio init (che riempie lo spazio precedentemente predisposto con valori sensati). E fin qui, nulla di speciale. Adesso però consideriamo la definizione di una nuova classe. Questa differisce dalla sua superclasse per qualche particolarità, tipicamente un metodo o una nuova variabile d'istanza. Se c'è una nuova variabile d'istanza, è bene inizializzare questa variabile. Non si può usare il metodo init ereditato, perché non contiene ovviamente le nuove operazioni. Non ha senso scrivere un nuovo metodo init totalmente nuovo (fare cioè l'overloading del metodo init); in certi casi non è neppure possibile (ad esempio, non conosciamo la struttura interna dell'oggetto NSObject, e quindi qualsiasi init da me definito sarà sbagliato...). Esiste però la possibilità di fare l'overload del metodo init senza cancellare il vecchio init, o meglio, riutilizzare il vecchio init per le cose standard. Il trucco è molto semplice: scrivo il mio metodo di init. Però, la prima cosa che faccio all'interno di questo metodo è di eseguire l'init della superclasse. In altre parole, il metodo init della classe figlia manda il messaggio init alla superclasse. Per fare questo, si utilizza una variabile d'istanza presente in ogni oggetto, chiamata super. Super, in ogni oggetto è un puntatore alla superclasse, in modo da potersi riferire ad essa quando necessario. C'è un problema. Il metodo init è così dichiarato:

- (id)    init

Il metodo non richiede alcun parametro di messaggio, ma restituisce un oggetto (un puntatore a...). Che oggetto? Ma quello che la classe ha appena inizializzato, proprio l'oggetto che sto costruendo. In effetti, ricordo la classica definizione di una istanza d'oggetto (mi cito):

Contatore    * miocont ;
miocont = [[Contatore alloc] init ];

Definito il puntatore con la prima istruzione, a questo puntatore è assegnato il valore di ritorno del messaggio init. Tutto questo lungo discorso per giustificare la seguente struttura del codice del metodo init:

- (id)    init
{
    self = [super init] ;
    // le nuove inizializzazioni
    return ( self );
}

Compare una nuova variabile, self, anch'essa predefinita d'istanza. Rappresenta l'oggetto corrente. Per capire come funzionano le cose, si deve partire dal fatto che qualcuno ha già invocato il metodo alloc sulla classe. Questo metodo ritorna un puntatore ad oggetto, cioè, un puntatore ad una zona di memoria ancora vuota destinata a contenere l'oggetto stesso. All'oggetto adesso è inviato il messaggio di init. La prima istruzione invia un messaggio init alla superclasse. Così facendo, sono eseguite le operazioni di inizializzazione, relativamente alla parte spettante alla superclasse, della zona di memoria dell'oggetto. Il metodo restituisce ancora il puntatore all'oggetto, che non dovrebbe essere cambiato. Questo è il puntatore all'oggetto, che quindi assegno a self. Dopo, non devo far altro che inizializzare gli elementi nuovi di pertinenza del nuovo oggetto, e restituire come risultato il puntatore all'oggetto stesso (come appunto aveva fatto il metodo init della superclasse).

Questa tecnica, di invocare il metodo della superclasse utilizzando la variabile super, non è limitata al solo metodo init (anche se qui è usata molto spesso), ma è estesa a tutti i metodi in cui è possibile riutilizzare in gran parte il lavoro già svolto. Addirittura, se la nuova classe si differenzia dalla superclasse solo per i valori iniziali delle variabili d'istanza, è certamente più chiaro e pulito chiamare init della superclasse per eseguire le inizializzazioni di default, e poi riscrivere le sole nuove inizializzazioni, anche se in qualche caso ciò significa fare le cose due volte (buttando via quelle fatte la prima volta).

Il creatore del contatore

Sono a questo punto in grado di dire chi costruisce l'oggetto Contatore.

All'interno della classe CountCtrl aggiungo una variabile d'istanza, che è proprio l'oggetto Contatore. Dentro il metodo init di CountCtrl faccio le inizializzazioni standard, e poi passo a costruire ed inizializzare proprio l'oggetto contatore. Ci metto di più a spiegarlo che a farlo: Il file CountCtrl.h è allora il seguente:

#import <Cocoa/Cocoa.h>
#import "Contatore.h"

@interface CountCtrl : NSObject
{
    IBOutlet id cntDisp;
    Contatore    * cnt ;
}
- (id)         init ;
- (IBAction)    countDown: (id)sender ;
- (IBAction)    countUp: (id)sender ;
@end

Poi, all'interno di CountCtrl.m la definizione del metodo init è questa:

- (id) init
{
    self = [super init] ;
    cnt = [ [Contatore alloc] init ];
    [ cntDisp setIntValue: 0];
    return ( self );
}

Ne approfitto anche per dare un valore iniziale a Contatore, nullo. A dire la verità, avrei potuto farne a meno, ora che ho imparato il meccanismo di init. Infatti, posso aggiungere alla classe Contatore il metodo init, in modo che, alla creazione, il contatore sia inizializzato a zero:

@implementation Contatore
- (id) init
{
    self = [super init] ;
    counter = 0 ;
    return ( self );
}
...

La scrittura del valore

Diventa questo punto molto semplice per CountCtrl passare la palla all'oggetto Contatore quando riceve un messaggio da uno dei due pulsanti dell'interfaccia. Pigliamo il metodo per incrementare il valore del contatore:

- (IBAction)countUp:(id)sender
{
    short nuovo Val ;
    [ cnt countUp];
    nuovoVal = [ cnt getValue ] ;
    // ed adesso?
}

Mi sono imbattuto nel secondo problema: come fare a dire al campo di tipo testo (che è puntato dall'outlet cntDisp) di mostrare il nuovo valore del contatore. Mi dico: basta mandargli un messaggio apposito, passandogli come argomento appunto cosa si vuole mostrare nel campo testo. Quindi, vado a vedere cosa dice la documentazione. Il campo testo è un oggetto della classe NSTextField, vado e trovo metodi per: controllare l'editabilità e la selezione del testo; per il colore; l'aspetto; il background; il bordo del campo; per agganciare tra loro campi; per selezionare il testo; per impostare il delegato (che diavolo è?)... niente per leggere o scrivere il contenuto.

Ci ho messo circa un paio d'ore di navigazione in mezzo all'aiuto prima di ottenere la risposta più ovvia e razionale. Perbacco, ma NSTextField è una sottoclasse di NSControl. Se non ci sono metodi propri ed esclusivi di NSTextField, è perché sono già stati definiti dalla sua superclasse! (ovvio, vero? dopo tutto quello che ho detto sugli oggetti...).

Infatti, tra le altre miriadi di metodi di NSControl ce ne è un gruppo assolutamente fantastico per i nostri scopi, che permette di impostare i valori del campo senza bisogno di formattare in precedenza il valore. Copio brutalmente dalla documentazione:

setIntValue:
- (void)setIntValue:(int)anInt
Sets the value of the receiver's cell (or selected cell) to the integer anInt.

Assolutamente fantastico. Questo mi permette di scrivere il metodo in due sole righe:

- (IBAction)countUp:(id)sender
{
    [ cnt countUp];
    [ cntDisp setIntValue: [ cnt getValue ] ];
}

Con la prima riga, predispongo il contatore; con la seconda, prima ricavo il valore, che uso come parametro del messaggio setIntvalue: inviato al campo di testo.

Ovviamente, il metodo gemello è presto scritto:

- (IBAction)countDown:(id)sender
{
    [ cnt countDown];
    [ cntDisp setIntValue: [ cnt getValue ] ];
}

Volendo, avrei potuto fare tutto in una sola riga. Basta riscrivere il metodo countUp: (e countDown:) del contatore perché restituisca il nuovo valore del contatore. Non sarebbe quindi più necessario invocare il metodo getValue: sul contatore...

Anzi, mi pare una talmente buona idea che lo faccio subito.

Riporto tutti e quattro i file interessati.

Cominciamo con Contatore.h

#import <Foundation/Foundation.h>

@interface Contatore : NSObject {
    short    counter ;
}
- (id)        init ;
- (short)    setValue: (short) startVal ;
- (short)    getValue ;
- (short)    countUp ;
- (short)    countDown ;
@end

Poi Contatore.m

#import "Contatore.h"

@implementation Contatore
- (id)    init
{
    self = [super init] ;
    counter = 0 ;
    return ( self );
}
-(short) setValue: (short) valore
{
    counter = valore ;
    return ( counter );
}

-(short) countUp
{
    return(++counter) ;
}

-(short) countDown ;
{
    return(--counter) ;
}

- (short) getValue
{
    return ( counter );
}
@end

Di seguito, CountCtrl.h:

@interface CountCtrl : NSObject
{
    IBOutlet id cntDisp;
    Contatore    * cnt ;
}
- (id)        init ;
- (IBAction)    countDown: (id)sender;
- (IBAction)    countUp: (id)sender;
@end

e per finire CountCtrl.m:

#import "CountCtrl.h"

@implementation CountCtrl
- (id)    init
{
    self = [super init] ;
    cnt = [ [Contatore alloc] init ];
    return ( self );
}

- (IBAction)countDown:(id)sender
{
    [ cntDisp setIntValue: [ cnt countDown] ];
}

- (IBAction)countUp:(id)sender
{
    [ cntDisp setIntValue: [ cnt countUp] ];
}
@end

Ora si compila l'applicazione, la si esegue, e si è contenti perché funziona. Ci sono ancora migliaia di particolari da mettere a punto (il testo si può editare? non dovrebbe... Che succede se ridimensiono la finestra? Oh che disastro... cose del genere), altre cose già disponibili gratuitamente (provate a stampare, a fare About... ), ma il grosso pare fatto...

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