Paola Magillo, Univestita' di Genova, Corso di Interfacce Utente per Informatica, a.a. 2005-2006.

LABORATORIO - INTERFACCE GRAFICHE IN JAVA

Contesto generale

L'obiettivo e' disegnare in Java il diagramma di Voronoi di un insieme di punti (ved. documentazione del progetto).

In pratica dovete definire un vostro componente Java personalizzato che "si disegna" disegnando su se stesso il diagramma di Voronoi. Tale componente sara' poi inserito all'interno della gerarchia di contenimento della vostra interfaccia, e quindi visualizzato.

La vostra interfaccia, per ora, potra' essere semplicemente una finestra principale (Frame) che contiene come unico sotto-componente questo pannello.
Il diagramma di Voronoi da visualizzare potra' essere fisso e costruito da programma (come nel Main di esempio dato nel progetto).

Ci sono vari punti chiave da risolvere:

  1. come scegliere la classe di componente da cui ereditare per definire la nuova classe
  2. come accedere al diagramma di Voronoi da dentro alla classe nuova che definiamo
  3. come disegnare il diagramma di Voronoi

Non occorre consegnare nulla, questo e' solo un esercizio che vi sara' utile per il progetto finale del corso.

SUGGERIMENTO:
E' molto utile programmare tenendo aperta a fianco una finestra sul sito con il manuale on-line delle classi Java: http://java.sun.com/j2se/1.4.2/docs/api/index.html

1 - Come scegliere la classe di componente da cui ereditare

In genere non e' necessario che l'area destinata a contenere il disegno del diagramma di Voronoi abbia un comportamento particolare. Quindi possiamo definire la nostra nuova classe semplicemente come sottoclasse di Panel (in AWT) o JPanel (in Swing).

Nel seguito chiamo NOSTRA_CLASSE questa nostra sotto-classe di pannello.

Un pannello in Java non contiene nulla, fino a che non aggiungiamo componenti al suo interno. L'uso che facciamo ora del pannello e' anomalo nel senso che non lo usiamo per contenere altri componenti ma per disegnare sul suo sfondo. Quindi lo usiamo vuoto.
Siccome Java assegna dimensione preferita nulla a un pannello vuoto, e' opportuno ridefinire, nella nostra sotto-classe NOSTRA_CLASSE, la dimensione preferita.
Questa si ridefinisce in Swing chiamando la funzione setPreferredSize (ved. manuale on-line), e in AWT reimplementando la funzione getPreferredSize (ved. manuale on-line) in quanto AWT non fornisce la funzione setPreferredSize.

2 - Come vedere il diagramma di Voronoi da dentro alla classe nuova

La nostra classe NOSTRA_CLASSE deve poter accedere al diagramma di Voronoi, che sara' un oggetto di classe Voronoi.

IMPORTANTE:
Quando il nostro componente viene usato nell'interfaccia complessiva del progetto, tipicamente abbiamo gia' un oggetto diagramma-di-Voronoi, che compare come variabile in qualche altra classe della vostra interfaccia, in genere nella classe che realizza la finestra principale.
La nostra classe (come abbiamo appena detto) avra' anch'essa come variabile un oggetto di classe Voronoi.
Tale variabile dovra' contenere lo stesso oggetto contenuto nella variabile di classe Voronoi che sta all'interno della finestra principale.

Il costruttore della nostra classe NOSTRA_CLASSE prendera' in argomento un oggetto di classe Voronoi e lo assegnera' alla sua variabile interna.
Tale costruttore e' chiamato dal costruttore della classe che corrisponde alla finestra principale dell'interfaccia passando come argomento la variabile di classe Voronoi che sta all'interno della finestra principale.
In questo modo la finestra principale e il pannello condividono lo stesso diagramma di Voronoi.

In pratica, nella classe principale:

  Voronoi global_voronoi;
  NOSTRA_CLASSE voronoi_panel;
Nella classe NOSTRA_CLASSE da noi definita per disegnare il diagramma di Voronoi:
  Voronoi local_voronoi;
Il costruttore della classe NOSTRA_CLASSE:
  NOSTRA_CLASSE(Voronoi vor,...)
  {
    local_voronoi = vor;
    ...
  }
Nel costruttore dela classe principale:
  global_voronoi = new Voronoi(...);
  ...
  voronoi_panel = new NOSTRA_CLASSE(global_voronoi...);

3 - Come disegnare il diagramma di Voronoi

Sistemi di coordinate

Il nostro diagramma di Voronoi ha siti con coordinate reali comprese in un generico rettangolo di diagonale (x1,y1)-(x2,y2): questo rettangolo e' il nostro user space.
Il pannello in cui disegnamo ha coordinate intere comprese nel rettangolo (0,0)-(W,H) dove W e H sono larghezza e altezza del pannello in pixel: questo rettangolo e' il nostro device space.

Per poter disegnare, dobbiamo tradurre le coordinate da user space a device space, ovvero portare il rettangolo (x1,y1)-(x2,y2) a coincidere con il rettangolo (0,0)-(W,H) mediante operazioni di traslazione e scalatura.
Questo puo' essere fatto in due modi:

  1. Scrivere una funzione di conversione del tipo int convertX(double x, int W) che, data la x in user space e la larghezza del pannello, restituisce la x in device space; e scrivere un'analoga funzione di conversione per la y. Usare poi queste coordinate convertite per disegnare.
  2. Applicare al pannello una trasformazione di coordinate Java che esegua la traduzione. Poi, si puo' tranquillamente disegnare usando le coordinate espresse in user space.

Disegno vero e proprio

Scandire l'array dei siti del diagramma (restituito dalla funzione getPoints della classe Voronoi).

Per ogni sito farsi ritornare i vertici di contorno nella regione di Voronoi (funzione getRegion).

La regione puo' essere illimitata o comunque puo' estendersi molto al di fuori della parte di piano inquadrata nel nostro pannello di disegno. Questo puo' causare problemi a Java. Percio' e' preferibile fare il "clipping" della regione rispetto all'estensione del pannello di disegno (cioe' calcolare l'intersezione e tenere solo la parte dentro), prima di disegnare. Questo si fa con la funzione clipRegion della classe Clipping.
NOTA: il clipping avviene prima della traduzione da user a device space, quindi il rettangolo da usare per il clipping e' quello di user space.

Ora la regione puo' essere disegnata riempiendo l'interno (funzione fillPolygon della classe Graphics) o tracciando il contorno (drawPolygon).
Se si decide di fare entrambe le cose, prima riempire tutti gli interni e poi tracciare tutti i contorni (occorrono due cicli sui vertici).

In generale ricordare che le cose vengono disegnate nell'ordine in cui sono eseguiti i comandi di disegno, e quello che viene disegnato dopo copre quello che era stato disegnato prima.
Per esempio, se disegnamo anche i siti (bisogna disegnare un ellisse centrato nel punto con la funzione fillArc di Graphics), bisogna disegnarli dopo aver disegnato le regioni piene, altrimenti vengono coperti.