Mòdul 6

Pràctica 6: dibuix i pintura
Tornar presentació tema
Pràctica 2 Pràctica 2 Pràctica 2 Pràctica 2 Pràctica 2 Pràctica 6 Pràctica 6  
     
     
  Dibuixar i pintar...  
     
  Sovint convé presentar a l'usuari d'una certa aplicació Java alguna mena de diagrama, de dibuix, de representació. Això implica la necessitat de disposar d'algun conjunt de classes i mètodes que ens permetin elaborar aquests diagrames, dibuixos i representacions.  
     
  Grosso modo, a Java, tots els objectes de les classes derivades de la classe java.awt.Component tenen un context gràfic que permet pintar-hi a sobre tota mena de formes. El control de l'acte de pintar el té un objecte de la classe java.awt.Graphics o de la classe java.awt.Graphics2D que representa aquest context gràfic. Les esmentades classes, java.awt.Graphics i java.awt.Graphics2D contenen els mètodes necessaris per dibuixar el que calgui.  
     
  Precisem-ho una mica més (no pas gaire, encara!): Tota classe derivada de la classe java.awt.Component conté el mètode  
     
 
public void paint (Graphics g)
 
     
  el qual és cridat cada vegada que el component es dibuixa o redibuixa a la pantalla. El qua cal fer, aleshores, és sobreescriure (override) aquest métode per tal que el component es dibuixi segons allò que el programador desitja.  
     
  La classe Graphics  
     
  La classe java.awt.Graphics representa el context gràfic d'un component i conté un seguit de mètodes per fer dibuixos diversos:  
     
 
  • public void setColor(Color c). Selecciona l'objecte java.awt.Color, (veure més avall) el qual determina el color amb què es pintaran tots els dibuixis següents, fins una nova crida a setColor().

  • public void drawLine(int x1, int y1, int x2, int y2). Dibuixa una línia entre els punts (x1,y1) i (x2,y2).

  • public void drawOval(int x, int y, int width, int height). Dibuixa el contorn de l'el·lipse inscrita en un rectangle de dimensions int width i int height, amb el vèrtex superior de l'esquerra a la posició (x,y).

  • public void fillOval(int x, int y, int width, int height). Dibuixa l'interior de l'el·lipse inscrita en un rectangle de dimensions int width i int height, amb el vèrtex superior de l'esquerra a la posició (x,y).

  • public void drawRect(int x, int y, int width, int height). Dibuixa el contorn del rectangle de dimensions int width i int height, amb el vèrtex superior de l'esquerra a la posició (x,y).

  • public void fillRect(int x, int y, int width, int height). Dibuixa l'interior del rectangle de dimensions int width i int height, amb el vèrtex superior de l'esquerra a la posició (x,y).

  • public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight). Dibuixa el contorn del rectangle de vèrtexs arrodonits de dimensions int width i int height, amb el vèrtex superior de l'esquerra a la posició (x,y). Els paràmetres int arcWidth i int arcHeight determinen les dimensions de l'arc d'arrodoniment dels vèrtexs.

  • public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight). Dibuixa l'interior del rectangle de vèrtexs arrodonits de dimensions int width i int height, amb el vèrtex superior de l'esquerra a la posició (x,y). Els paràmetres int arcWidth i int arcHeight determinen les dimensions de l'arc d'arrodoniment dels vèrtexs.

  • public void draw3DRect(int x, int y, int width, int height, boolean raised). El mateix que drawRect(), però ara la línia té relleu positiu si raised és true i regatiu si raised és false.

  • public void fill3DRect(int x, int y, int width, int height, boolean raised). El mateix que fillRect(), però ara l'interior del rectangle té relleu positiu si raised és true i regatiu si raised és false.

  • public void drawString(String str, int x, int y). Pinta la cadena de text representada per l'String str, a la posició (vèrtex superior esquerre del rectangle que la conté) (x,y). Cal remarcar que el resultat és text pintat, no editable ni seleccionable!

  • public void setFont(Font font). Selecciona el font amb el qual es pintaran les cadenes de text.
 
     
  Les coordenades, en aquestos contextos gràfics, són nombres enters (int) que compten píxels. El punt o píxel (0,0) és a dalt a l'esquerra, la primera coordenada (x, width o similar) creix d'esquerra a dreta i la segona (y, height, etc.) de dalt a baix:  
     
 
 
     
  La classe java.awt.Graphics conté encara molts d'altres mètodes, que trobareu perfectament descrits a la documentació corresponent.  
     
  La classe java.awt.Color  
     
   A Java tot són classes i no podia ésser d'altra manera amb el color! Examina ara la documentació de la classe java.awt.Color. Si vols obtenir un determinat color pots acudir directament als camps static d'aquesta classe:  
     
  Color Color.BLACK
Color Color.BLUE
Color Color.CYAN
Color Color.DARK_GRAY
Color Color.GRAY
Color Color.GREEN
Color Color.LIGHT_GRAY
Color Color.MAGENTA
Color Color.ORANGE
Color Color.PINK
Color Color.RED
Color Color.WHITE
Color Color.YELLOW

 
     
  o bé, construir el color RGB amb algun dels mètodes constructors de java.awt.Color. Per exemple, això
 
     
 
setColor(Color.BLUE);
 
     
  és completament equivalent a això altre  
     
 
setColor(new Color(0,0,255));
 
     
  El mètode constructor fet servir aquí és aquest:  
     
 
public Color(int vermell,int verd,int blau);
 
     
  i els paràmetres int vermell, int verd i int blau són nombres enters entre 0 i 255.  
     
  DIbuixar i pintar sobre un JPanel:    
     
Ara pots comprovar com funciona tot això: a un objecte JFrame, canvia-li l'objecte contentPane que porta per defecte per un objecte JPanel, que és l'adequat per pintar-hi a sobre (JPanel és una classe filla de Component):  
     
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Dimension;

/**
* Escriviu aquí una descripcìó de la classe ProvaDePintura
*
* @author (el vostre nom)
* @version (un número de versió o la data)
*/

public class ProvaDePintura extends JFrame {

    /**
     * Mètode constructor per a objectes de la classe ProvaDePintura
     */
    public ProvaDePintura() {
        setTitle("Proves de pintura");

        // Un JPanel com a nou contentPane del JFrame
        JPanel cnt=new JPanel();
        // Cal donar-li dimensions, perquè no contindrà cap
        // component, només s'hi dibuixarà a sobre
        cnt.setPreferredSize(new Dimension(300,200));
        // Ara se substitueix el contentPane per defecte pel
        // JPanel creat
        setContentPane(cnt);

        pack();
        show();
    }

}
 
     
  Ara ja tens preparada la superfície per pintar-hi:  
     
 
     
  Només cal que sobreescriguis (override) el mètode paint() del JPanel. Com que només faràs això, no cal crear una classe nova sinó que amb intercalar el nou mètode paint() en la construcció del JPanel n'hi ha prou:  
     
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Font;

/**
* Escriviu aquí una descripcìó de la classe ProvaDePintura
*
* @author (el vostre nom)
* @version (un número de versió o la data)
*/
public class ProvaDePintura extends JFrame {

    /**
     * Mètode constructor per a objectes de la classe ProvaDePintura
     */
    public ProvaDePintura() {
        setTitle("Proves de pintura");

        // Un JPanel com a nou contentPane del JFrame. Se subs-
        // titueix (override) el mètode paint() original per un
        // de nou
        JPanel cnt=new JPanel() {
                       public void paint (Graphics g) {
                           // Un rectangle "enfonsat" blanc
                           g.setColor(Color.WHITE);
                           // El "marc"
                           g.draw3DRect(4,4,292,192,false);
                           // El "fons"
                           g.fillRect(5,5,290,190);
                           // Un rectangle arrodonit vermell amb
                           // contorn negre
                           g.setColor(Color.RED);
                           g.fillRoundRect(50,30,200,150,25,20);
                           g.setColor(Color.BLACK);
                           g.drawRoundRect(50,30,200,150,25,20);
                           // Un text de color blau
                           g.setColor(Color.BLUE);
                           g.setFont(new Font("Serif",
                                              Font.PLAIN,30));
                           g.drawString("Això és un text",10,70);
                           // Una el·lipse marró sense contorn
                           g.setColor(new Color(180,100,0));
                           g.fillOval(190,100,100,50);
                       }
                   };

        // Cal donar-li dimensions, perquè no contindrà cap
        // component, només s'hi dibuixarà a sobre
        cnt.setPreferredSize(new Dimension(300,200));
        // Ara se substitueix el contentPane per defecte pel
        // JPanel creat
        setContentPane(cnt);

        pack();
        show();
    }

}
 
     
  Observa com ha estat escrit aquest codi: Abans de tancar la línia  
     
 
        JPanel cnt=new JPanel()
 
     
  amb el corresponent punt i coma (;) s'obre un espai entre una parella de claus ({ ... }) a l'interior del qual hi ha escrit el nou codi del mètode paint(). Aquesta manera de procedir és útil si no cal modificar massa la classe JPanel (o la que sigui del cas), pero si no, més val crear una classe filla de JPanel a part, i codificar-ne els mètodes amb més comoditat:  
     
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Font;

/**
* Escriviu aquí una descripcìó de la classe ProvaDePintura
*
* @author (el vostre nom)
* @version (un número de versió o la data)
*/
public class ProvaDePintura extends JFrame {

    /**
     * Mètode constructor per a objectes de la classe ProvaDePintura
     */
    public ProvaDePintura() {
        setTitle("Proves de pintura");

        // Un ElMeuPanel com a nou contentPane del JFrame.
        ElMeuPanel cnt=new ElMeuPanel();
        // Com abans, cal donar-li dimensions, perquè no
        // contindrà cap component, només s'hi dibuixarà
        // a sobre

        cnt.setPreferredSize(new Dimension(300,200));
        // Ara se substitueix el contentPane per defecte pel
        // JPanel creat
        setContentPane(cnt);

        pack();
        show();
    }

}

/**
 * La classe ElMeuPanel és un JPanel amb una implementació
 * particular del mètode paint():
 */

class ElMeuPanel extends JPanel {

    /**
     * Mètode paint per a objectes de la classe ElMeuPanel.
     * @override paint de la classe JPanel
     */

    public void paint (Graphics g) {
        // Un rectangle "enfonsat" blanc
        g.setColor(Color.WHITE);
        // El "marc"
        g.draw3DRect(4,4,292,192,false);
        // El "fons"
        g.fillRect(5,5,290,190);
        // Un rectangle arrodonit vermell amb
        // contorn negre
        g.setColor(Color.RED);
        g.fillRoundRect(50,30,200,150,25,20);
        g.setColor(Color.BLACK);
        g.drawRoundRect(50,30,200,150,25,20);
        // Un text de color blau
        g.setColor(Color.BLUE);
        g.setFont(new Font("Serif",
                           Font.PLAIN,30));
        g.drawString("Això és un text",10,70);
        // Una el·lipse marró sense contorn
        g.setColor(new Color(180,100,0));
        g.fillOval(190,100,100,50);
    }

}
 
     
  En ambdós casos, després de construir un objecte ProvaDePintura, el resultat és aquest:  
     
 
     
  La classe Graphics2D  
     
 

A partit de la primera edició del SDK que es va dir Java 2 (SDK versions 1.2 i següents) la capacitat gràfica de Java es va posar d'acord amb els progressos i les noves possibilitats de les targetes gràfiques dels ordenadors. Es posà a la disposició dels programadors la nova classe java.awt.Graphics2D, filla de la vella classe java.awt.Graphics, amb moltíssimes millores incorporades. Des d'aleshores, els contextos gràfics són, en realitat, de la classe Graphics2D i no de la classe Graphics, però, per compatibilitat amb versions anteriors del SDK, calia conservar objectes Graphics com a paràmetres dels mètodes paint().

 
     
  En conseqüència, per tal que l'objecte controlador del context gràfic actui com a objecte de la classe Graphics2D cal fer el càsting corresponent (no t'oblidessis pas d'importar la classe java.awt.Graphics2D!):  
     
<codi anterior>

    /**
     * Mètode paint per a objectes de la classe ElMeuPanel.
     * @override paint de la classe JPanel
     */
    public void paint (Graphics g) {

        // Objecte Graphics g convertit a objecte Graphics2D
        Graphics2D g2D=(Graphics2D)g;
        // A partir d'ara, qui diguixa és l'objecte g2D:
        // Un rectangle "enfonsat" blanc
        g2D.setColor(Color.WHITE);
        // El "marc"
        g2D.draw3DRect(4,4,292,192,false);
        // El "fons"
        g2D.fillRect(5,5,290,190);
        // Un rectangle arrodonit vermell amb
        // contorn negre
        g2D.setColor(Color.RED);
        g2D.fillRoundRect(50,30,200,150,25,20);
        g2D.setColor(Color.BLACK);
        g2D.drawRoundRect(50,30,200,150,25,20);
        // Un text de color blau
        g2D.setColor(Color.BLUE);
        g2D.setFont(new Font("Serif",
                           Font.PLAIN,30));
        g2D.drawString("Això és un text",10,70);
        // Una el·lipse marró sense contorn
        g2D.setColor(new Color(180,100,0));
        g2D.fillOval(190,100,100,50);
    }

}
 
     
  RenderingHints    
     
 

Es poden aconseguir ara millores en el rendering gràfic. Els objectes de la classe Graphics2D poden suggerir a la targeta gràfica de la màquina algunes maneres de presentar els gràfics i, si la targeta ho pot fer, ho fa. Aquests suggeriments són objectes de la classe java.awt.RenderingHints. El mètode corresponent és de la classe Graphics2D i és:

 
     
 
public void setRenderingHint(RenderingHints.Key hintKey,
                             Object hintValue)
 
     
  Per exemple, si volem antialiasing, aleshores cal fer servir els valors RenderingHints.KEY_ANTIALIASING i RenderingHints.VALUE_ANTIALIAS_ON:  
     
<codi anterior>

    /**
     * Mètode paint per a objectes de la classe ElMeuPanel.
     * @override paint de la classe JPanel
     */
    public void paint (Graphics g) {
        // Objecte Graphics g convertit a objecte Graphics2D
        Graphics2D g2D=(Graphics2D)g;
        // A partir d'ara, qui diguixa és l'objecte g2D:

        // El RenderingHint per activar antialiasing

        g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);

        // Un rectangle "enfonsat" blanc
        g2D.setColor(Color.WHITE);
        // El "marc"
        g2D.draw3DRect(4,4,292,192,false);

<codi següent>
 
     
  Mira què passa:  
     
 
     
  Veus la millora? Mira que, ara, els contorns queden ben perfilats i no esglaonats (pixelats, se'n diu d'això!) com abans. Si amplies la lletra "A" d'"Això", ho veuràs més bé:  
     
 
 
  Sense antialiasing   Amb antialiasing  
     
  La resta de renderingHints, com aquest, té a veure amb les diverses maneres de renderitzar gràfics i és, més aviat, objecte d'un curs de gràfics que d'un curs de Java com aquest.  
     
  Shapes (formes)    
     
  Una segona millora de la classe Graphics2D respecte de la classe Graphics és la capacitat de dibuixar el contorn (draw) o l'interior (fill) dels objectes que implementen la interfície (interface) java.awt.Shape (forma). Els mètodes són:  
     
 
public void draw(Shape forma)
 
     
  i  
     
 
public void fill(Shape forma)
 
     
  Pots fer proves amb un objecte java.awt.Polygon, que inclou l'interfície Shape. Aquesta classe representa un polígon i el constructor és  
     
 
public Polygon(int[] xPunts, int[] yPunts, int numPunts)
 
     
  Les matrius de nombres enters xPunts i yPunts contenen, respectivament, les coordenades x i y dels vèrtexs del polígon i numPunts és el nombre de vèrtexs (que, com és lògic, no ha de ser més gran que les longituds (xPunts.length i yPunts.length) de les matrius de les coordenades. Ara, el mètode paint() de la classe ProvaDePintura quedaria així:  
     
    /**
     * Mètode paint per a objectes de la classe ElMeuPanel.
     * @override paint de la classe JPanel
     */
    public void paint (Graphics g) {
        // Objecte Graphics g convertit a objecte Graphics2D
        Graphics2D g2D=(Graphics2D)g;
        // A partir d'ara, qui diguixa és l'objecte g2D:
        // El RenderingHint per activar antialiasing
        g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);
        // Un rectangle "enfonsat" blanc
        g2D.setColor(Color.WHITE);
        // El "marc"
        g2D.draw3DRect(4,4,292,192,false);
        // El "fons"
        g2D.fillRect(5,5,290,190);

        // Les coordenades "x" de set vèrtexs d'un polígon
        int[] lesX={50,120,190,250,200,150,80};
        // Les coordenades "y" de set vèrtexs d'un polígon
        int[] lesY={110,20,90,10,190,140,190};
        // Construcció del polígon:
        Polygon poligon=new Polygon(lesX,lesY,7);
        // L'interior del polígon de color blau:
        g2D.setColor(Color.BLUE);
        g2D.fill(poligon);
        // El contorn del polígon de color negre:
        g2D.setColor(Color.BLACK);
        g2D.draw(poligon);
    }
 
     
  No oblidis importar la classe java.awt.Polygon!  
     
  Obtindràs això:  
     
 
     
  Transformacions afins    
     
  Finalment, la classe Graphics2D és capaç de modificar les coordenades de les formes que pinta segons transformacions afins. Parlant molt en general, una transformació afí és una transformació del pla que conserva paral·lelismes i proporcions: translacions, rotacions, simetries i homotècies són transformacions afins, però n'hi ha moltes altres.  
     
  Ara cal una mica de Matemàtiques: una transformació afí vé determinada per una matriu de tres files i tres columnes  
     
 
 
     
  i, per trobar el transformat del punt (x, y), se'l considera com a un vector amb la tercera component igual a 1 i s'opera segons la pràctica habitual "fila per columna":  
     
 
 
     
  Els termes mij determinen la mena de transformació i els termes tij són les components de la translació subseqüent a la transformació. Cal que tinguis en compte que, si els termes tij són zero, aleshores no hi ha translació i el punt (0,0) queda fix!  
     
  Algunes transformacions habituals són:  
     
 
Translació de p unitats cap a la dreta i q unitats cap a baix. Si cal anar cap a l'esquerra o cap a dalt, els corresponents valors han de ser negatius.  
     
 
Ampliació p vegades en direcció horitzontal i q vegades en direcció vertical. Si cal reduir, els valors han de ser més petits que 1.  
     
 
Rotació d'angle A a l'entorn del punt (0,0).  
     
 
Simetria respecte d'una recta que passa pel punt (0,0) i inclinada un angle A.  
     
  La classe java.awt.geom.AffineTransform representa aquestes transformacions. El mètode constructor és  
     
 
public AffineTransform(double m00, double m10,
                       double m01, double m11,
                       double t02, double t12)
 
     
  Per exemple, per girar 30º i, després traslladar 150 a la dreta i 100 cap a baix, cal fer  
     
 
AffineTransform afTr=new AffineTransform(0.866,0.5,
                                         -0.5,0.866,
                                         150,100);
 
     
Pots provar ara com funcionen les transformacions afins. Comença per traslladar el polígon de l'exemple d'abans per tal que inclogui el punt (0,0). És qüestió de restar 150 a les coordenades x i 100 a les coordenades y:  
     
    /**
     * Mètode paint per a objectes de la classe ElMeuPanel.
     * @override paint de la classe JPanel
     */
    public void paint (Graphics g) {
        // Objecte Graphics g convertit a objecte Graphics2D
        Graphics2D g2D=(Graphics2D)g;
        // A partir d'ara, qui diguixa és l'objecte g2D:
        // El RenderingHint per activar antialiasing
        g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);
        // Un rectangle "enfonsat" blanc
        g2D.setColor(Color.WHITE);
        // El "marc"
        g2D.draw3DRect(4,4,292,192,false);
        // El "fons"
        g2D.fillRect(5,5,290,190);
        // Les coordenades "x" de set vèrtexs d'un polígon
        int[] lesX={-100,-30,40,100,50,0,-70};
        // Les coordenades "y" de set vèrtexs d'un polígon
        int[] lesY={10,-80,-10,-90,90,40,90};
        // Construcció del polígon:
        Polygon poligon=new Polygon(lesX,lesY,7);
        // L'interior del polígon de color blau:
        g2D.setColor(Color.BLUE);
        g2D.fill(poligon);
        // El contorn del polígon de color negre:
        g2D.setColor(Color.BLACK);
        g2D.draw(poligon);
    }
 
     
  A veure si ho has fet bé?  
     
 
     
  Sí, sí, ja està bé. Ara cal introduir la transformació. El mètode és de la classe Graphics2D i és  
     
 
public void setTransform(AffineTransform afTr)
 
     
  Som-hi!  
     
    /**
     * Mètode paint per a objectes de la classe ElMeuPanel.
     * @override paint de la classe JPanel
     */
    public void paint (Graphics g) {
        // Objecte Graphics g convertit a objecte Graphics2D
        Graphics2D g2D=(Graphics2D)g;
        // A partir d'ara, qui diguixa és l'objecte g2D:
        // El RenderingHint per activar antialiasing
        g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);
        // La transformació:
        AffineTransform afTr=new AffineTransform(0.866,0.5,
                                                 -0.5,0.866,
                                                 150,100);
        // Un rectangle "enfonsat" blanc
        g2D.setColor(Color.WHITE);
        // El "marc"
        g2D.draw3DRect(4,4,292,192,false);
        // El "fons"
        g2D.fillRect(5,5,290,190);
        // Les coordenades "x" de set vèrtexs d'un polígon
        int[] lesX={-100,-30,40,100,50,0,-70};
        // Les coordenades "y" de set vèrtexs d'un polígon
        int[] lesY={10,-80,-10,-90,90,40,90};
        // Construcció del polígon:
        Polygon poligon=new Polygon(lesX,lesY,7);
        // Activació de la transformació
        g2D.setTransform(afTr);
        // L'interior del polígon de color blau:
        g2D.setColor(Color.BLUE);
        g2D.fill(poligon);
        // El contorn del polígon de color negre:
        g2D.setColor(Color.BLACK);
        g2D.draw(poligon);
    }
 
     
  (Ep! ja havies importat la classe java.awt.geom.AffineTransform?)  
     
  Ara, efectivament, es veu el polígon girat 30º i traslladat:  
     
 
     
  Un exercici:  
     
Es tracta que aconsegueixis una finestra pintada com aquesta:  
     
 
 
     
  Els textos hauran de ser pintats sense canviar el font, que sempre serà el de defecte. Les transformacions que sofriran han de ser fetes només a partir d'aplicar successivament el mètode  
     
 
public void setTransform(AffineTransform afTr)
 
     
  amb objectes adequats de la classe java.awt.geom.AffineTransform.  
     
  Complicat, eh? Una solució és aquí, però espera i no la miris!)  
     
    Tornar al principi