GRAFICA BI-DIMENSIONALE IN JAVA

Disegnare in un componente java

Ogni componente Java possiede il metodo: Per disegnare l'interfaccia, il sistema chiama paint su tutti i suoi componenti a partire dalla radice della gerarchia di contenimento.

IN AWT, il metodo paint e' "monolitico".

In Swing, il metodo paint e' implementato invocando nell'ordine i seguenti tre metodi:

L'ultimo dei tre metodi e' reso necessario dal fatto che componenti Swing sono lightweight.

Personalizzare il disegno

Per personalizzare la grafica di un componente, occorre definire una sottoclasse con una nuova implementazione del metodo paint.

Scegliere la classe di componente adatta (es Button), in modo da ereditare tutti i comportamenti previsti da tale classe (es. per bottone la capacita' di catturare ActionEvent).
Se non sono necessari comportamenti particolari si puo' usare un Panel.

Ridefinire il metodo paint inserendovi il codice per compiere il disegnamento voluto. Chiamare sempre super.paint all'inizio in modo da effettuare anche tutto il redisegnanento previsto di default (es. pulitura dello sfondo).

In Swing il metodo che bisogna reimplementare non e' paint ma e' paintComponent.

Classi per disegnare

Metodo paint ha come argomento un oggetto di classe Graphics che viene passato automaticamente dal sistema.

void paint (Graphics g)
{
  ....
}

Tutto il disegno si fa chiamando metodi della classe Graphics sull'oggetto g.

La classe Graphics contiene:

In realta' l'oggetto che viene passato dal sistema a paint non e' semplicemente di classe Graphics, ma in particolare e' della sottoclasse Graphics2D.

Essendo di classe Graphics2D (sottoclasse), g e' anche di classe Graphics (superclasse), e normalmente si usa come se fosse di classe Graphics.
Se voglio vederlo come oggetto di classe Graphics2D (per poter vedere le funzionalita' avanzate), devo fare una conversione esplicita (cast):

Graphics2D g2 = (Graphics2D)g;
dopo di che in g2 ho sempre g ma visto come oggetto di classe Graphics2D

Noi vedremo le funzionalita' di Graphics2D.

Ingredienti per disegnare

Quello che si disegna dipende da:

Sistemi di coordinate e trasformazioni

Device space

Ogni componente ha un sistema di coordinate intere (in pixel) con origine (0,0) in alto a sinistra e punto in basso a destra di coordinate (d.width-1,d.height-1) dove Dimension d = getSize() sono le dimensioni in pixel del componente. Le coordinate x crescono da sinistra verso destra, le coordinate y crescono dall'alto verso il basso.

In Swing devo tenere conto che parte del rettangolo della componente puo' essere occupata dal bordo. Insets i = getInsets() ritorna informazioni sullo spessore del bordo.
La parte di rettangolo libera dal bordo ha angolo in alto a sinistra (i.left,i.top) e in basso a destra (d.width-i.right-1,d.height-i.bottom-1).

Le coordinate sono infinitamente sottili e collocate tra un pixel e l'altro. La punta scrivente traccia la linea sul pixel immediatamente a destra e in basso.

Cio' significa che, se traccio un rettangolo pieno e il suo contorno vuoto dando le stesse coordinate, il contorno si estende una riga di pixel in piu' in basso e una colonna di pixel in piu' a destra rispetto al rettangolo pieno.

User space

Sistema di coordinate logico in cui il programmatore esprime le primitive da disegnare.

Una trasformazione affine determina come portare le primitive da User space a Device space.

Di default la trasformazione e' l'identita' (nessuna trasformazione, i due spazi di coordinate coincidono). Posso impostare traslazioni, rotazioni, scalature e shear.

Nota: Graphics2D ha trasformazioni e quindi distinzione tra device space e user space. Invece Graphics non ha trasformazioni e devo esprimere le primitive direttamente in device space.

Primitive e attributi

Primitive (che cosa posso disegnare): Vedremo dopo in dettaglio le primitive.

Attributi pittorici (come posso disegnare):

Modo di composizione del colore

Quando due primitive vanno a sovrapporsi sulla stessa area della finestra, quale delle due "vince"? Lo stabilisce il modo di composizione del colore:

Clipping

Clip = tagliare via (qui: dal disegno). Non disegnare quello che cade fuori da una certa area specificata da un perimetro di clipping (clipping path).

Di norma il perimetro di clipping e' la finestra: cio' che ha coordinate che eccedono i limiti delle coordinate e' tagliato via e non si vede. Tutto quello che cade dentro la finestra si vede.

Posso specificare perimetri di clipping (clipping path) aggiuntivi. Allora anche cio' che cade dentro la finestra ma fuori dal perimetro di clipping sara' tagliato.

Primitive disponibili

Metodi per disegnare direttamente una figura, i parametri sono interi esprimenti numero di pixel: La classe Shape la vedremo dopo.

Attributi disponibili

Colore

Il colore con cui disegno si legge con Color getColor() e si imposta con setColor(Color c).

Un colore si crea specificando le componenti RGB con new Color(r,g,b) dove r,g,b sono interi tra 0 e 255 oppure float tra 0.0 e 1.0. La classe Color ha anche costanti per i colori piu' comuni: Color.black, Color.white, Color.red, Color.green...

Tratto (stroke)

Il tratto e' il modo di tracciamento delle linee. Si legge con Stroke getStroke e si imposta con setStroke(Stroke s).

Il tratto e' implementato dalla classe BasicStroke, e puo' essere:

Posso specificare anche il modo di gestire gli angoli (appuntiti o arrotondati) ed altri dettagli.

Trama (paint)

La trama e' il modo di riempimento delle aree. Si legge con Paint getPaint() e si imposta con setPaint(Paint p).

La trama puo' essere:

Font

La font e' il tipo di carattere usato per tracciare le stringhe. Si legge con Font getFont() e si imposta con setFont(Font f).

Una font si crea con: new Font (nome, stile, grandezza) dove

Il default nella mia versione e' "Dialog", PLAIN, 12 pt. Per avere tutte le font disponibili sulla macchina:
    GraphicsEnvironment genv =
            GraphicsEnvironment.getLocalGraphicsEnvironment();
    Font[] f = genv.getAllFonts();

Esempio

Pannello con grafica personalizzata: PaintedPanel.java.
La funzione paint riempe un rettangolo sfumato al centro del pannello, traccia con tratto spesso le due diagonali del pannello, disegna una "a" in grande al centro.

Classi per figure geometriche (Shape)

La classe Shape fornisce nelle sue sottoclassi vari tipi di forme geometriche che posso disegnare usando i metodi della classe Graphics2D: Sottoclassi di shape: Le shape sono contenute nel package java.awt.geom.

Alcune delle figure di cui sopra corrispondono a funzioni draw/fill gia' viste (es: draw di un Rectangle2D equivale a drawRect).
Altre sono nuove. In piu' e' possibile esprimere le coordinate con float e double.

La possibilita' di disegnare le shape esiste solo in Graphics2D, non in Graphics. Le coordinate possono essere numeri reali anziche' interi perche' Graphics2D ammette coordinate logiche diverse da quelle fisiche (cioe' dai pixel).

La classe Area prevede le operazioni add, subtract, intersect, exclusiveOr con argomento un'altra area. Ogni shape puo' essere trasformata in un'area. Posso costruire una forma complessa aggiungendo, sottraendo, intersecando e facendo l'OR esclusivo di forme semplici.

Esempio

Mela con morso ottenuta unendo due ellissi e sottraendone un altro: MelaMorsa.java
  Ellipse2D.Double sinistra, destra, morso;
  Area a;
  sinistra = new Ellipse2D.Double();
  sinistra.setFrame(20, 20, 60, 80);
  destra = new Ellipse2D.Double();  
  destra.setFrame(40, 20, 60, 80);  
  morso = new Ellipse2D.Double();   
  morso.setFrame(80, 70, 60, 60);   
  a = new Area();
  a.add( new Area(sinistra) );
  a.add( new Area(destra) );  
  a.subtract( new Area(morso) );

Trasformazioni

Graphics2D contiene una trasformazione affine che e' usata per traslare, ruotare, scalare e deformare le primitive durante la loro traduzione da user space a device space.

La trasformazione e' un oggetto di classe AffineTransform. Internamente e' implementata come una matrice 3x3.

Posso creare un una nuova trasformazione con:

e poi impostarla con il metodo di Graphics2D setTransform.

Oppure modificare la trasformazione corrente concatenandone un'altra con i metodi di Graphics2D:

La rotazione e' attorno all'origine, la scalatura tiene come punto fermo l'origine.

Posso ottenere qualsiasi trasformazione componendo queste.

Importante: la composizione delle matrici avviene moltiplicando la nuova matrice a destra di quella corrente, quindi PRIMA viene eseguita la trasformazione corrispondente alla NUOVA matrice e DOPO la trasformazione corrispondente alla VECCHIA matrice.

In pratica le trasformazioni sono eseguite IN ORDINE INVERSO a come le specifico nel codice.

Esempio:
Parto dalla matrice identita' I.
g2.translate(...); matrice = I T = T dove T = matrice di traslazione
g2.rotate(...); matrice = T R dove R = matrice di rotazione
Risultato: nell'ordine prima ruoto poi traslo i punti

Esempio di trasfomazione composta

Rotazione attorno a un punto C = (xC,yC) diverso da origine:
  1. traslo di (-xC,-yC) per portare C nell'origine del nuovo riferimento
  2. ruoto dell'angolo alpha voluto attorno a origine
  3. traslo di (xC,yC) per portare C alla posizione originale
Nel codice:

Accortezza

La trasformazione va cambiata solo momentaneamente durante l'esecuzione di paint, alla fine del codice di paint bisogna ripristinare la trasformazione che c'era prima. Altrimenti si potrebbero avere effetti inaspettati. Per fare cio':

Esempio

Disegna 8 triangoli disposti a stella in uno spazio logico con coordinate x ed y tra -100 e 100: Stella.java.
Per trasformare lo spazio logico in quello fisico prima lo scala per far coincidere l'ampiezza di 200 unita' con l'ampiezza (in x ed y) della finestra, poi lo trasla per portare il punto (0,0) al centro della finestra.
Si vede che redimensionando la finestra lo spazio logico la occupa comunque tutta (deformando il disegno).
Sono disegnate 8 copie dello stesso triangolo ogni volta concatenando una rotazione di 1/8 di giro, in modo da formare la stella.

Animazione

Animazione = cambiamento dinamico della grafica mostrata su un componente.

Ad intervalli regolari (gestiti con un timer) oppure in seguito ad azioni dell'utente si ridisegna il componente cambiandone la grafica.

Forzare il redisegnamento

Di norma il metodo paint e' chiamato dal sistema, attraverso canali suoi, quando il sistema ritiene che il componente vada ridisegnato (es. quando la finestra che lo contiene e' mappata o torna visibile dopo essere stata oscurata da altre, quando viene ridimensionata...).

L'animazione rende necessario ridisegnare anche in casi diversi da quelli previsti dal sistema.

Il programma NON DEVE chiamare direttamente paint! Per provocare da programma il ridisegnamento del componente bisogna usare il metodo:

Siccome la gestione della coda e' asincrona, puo' capitare che piu' chiamate a repaint vengano collassate in una sola chiamata a paint.

L'animazione si ottiene cambiando alcuni parametri usati dentro la funzione paint e poi invocando repaint.

Esempio

Pannello con cerchio che scorre avanti e indietro per un pannello: MuoviCerchio.java
Una variabile contiene l'ascissa corrente ed e' usata da paint per disegnare il cerchio.
C'e' un timer che ogni tanti millisecondi cambia il valore della variabile e invoca repaint.

Interazione con l'utente

Vi sono due tipi di azioni interessanti che l'utente puo' compiere sull'area grafica:

Azioni sullo spazio

Esempio:
Cliccare in un punto, il programma poi compiera' qualche operazione con le coordinate di quel punto (es: disegnare qualcosa in quel punto)

Si catturano associando al pannello gli event listener opportuni. Nell'esempio associo un MouseListener che nel suo metodo mouseClicked compia l'operazione corrispondente. Le coordinate del punto cliccato si ottengono chiamando getX() e getY() sull'evento MouseEvent.

Azioni sulle primitive

Esempio:
Cliccare sopra una primitiva per selezionarla, il programma poi compiera' qualche operazione sulla primitiva selezionata (es: cancellarla)

Anche queste si catturano con event listener, ma in piu' richiedono un modo per conoscere che primitiva e' disegnata nel punto dove e' avvenuto il click.

E' necessario aver disegnato le figure che vogliamo rendere sensibili al click usando oggetti di classe Shape ed il metodo fill(Shape s) della classe Graphics2D.

Si scorre la lista delle shape che sono state disegnate (e' necessario averle memorizzate) e si chiede a ognuna se e' stata interessata dal click, usando i metodi della classe Shape:

Esempio

Programma che permette di disegnare cerchi e di cancellarli: ClickCircle.java