Exempel: Punkter

Sven-Olof Nyström
OOP med Java våren -25
Informationsteknologi
Uppsala Universitet

Skansholm: Kapitel 6-8

Här tittar vi på ett litet ritprogram. Vi startar med ett mycket enkelt program som vi bygger ut efterhand.

Lite kludd: Punkter

Enklast möjliga ritprogram (program/swing-2/Punkter_0.java):

public class Punkter_0 extends JPanel implements MouseListener{

    public static void main (String[] arg) {
	JPanel s = new Punkter_0();

	JFrame f = new JFrame();
	f.add(s);
	f.setVisible(true);
	f.setBounds(100, 100, 400, 400);

	f.setVisible(true);
	f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public Punkter_0 () {
        setBackground( Color.YELLOW );
        setForeground( Color.BLACK );
        addMouseListener(this);
    }

    public void mouseClicked (MouseEvent e) {
	int x = e.getX();
	int y = e.getY();

	Graphics g = getGraphics();
	g.setColor(Color.black);
	g.fillOval(x, y, 10, 10);
    }

    public void mouseEntered (MouseEvent e) { }
    public void mouseExited (MouseEvent e) { }

    public void mousePressed (MouseEvent e) { }
    public void mouseReleased (MouseEvent e) { }
}

(Jag kommer inte att säga nåt om koden som sätter upp fönstret, väljer färger etc. Allt detta fungerar ungefär som i tidigare exempel.)

Först, notera att Punkter_0 ärver JPanel och implementerar MouseListener. Gränssnittet MouseListener deklarerar fem metoder.

I det första programmet är det bara metoden mouseClicked() som är intressant. När användaren klickar på fönstret sker följande: Metoden tar reda på var användaren klickade (x och y). Sedan plockar vi fram det grafikobjekt som är associerat med nuvarande komponent. Vi väljer svart färg, och ritar en cirkel med diameter 10 vid koordinaten x och y.

Så här kan det se ut:

En irriterande detalj är att punkten inte hamnar där musen pekar, utan lite nedanför till höger. (Varför?)

Ett annat problem är att bilden försvinner om fönstret skyms. Allt som ritas med Graphics är temporärt. För att bilden ska finnas kvar måste den lagras i någon datastruktur. (På moderna operativsystem kan bilden sparas även när den skyms. Prova i så fall att minimera fönstret och återställa det.)

Ett lite uppstädat program.

För att göra det lite lättare att arbeta med programmet stuvar jag om en smula. Jag introducerar en klass "Punkt" (mera nedan) och klassen Ritpanel som även agerar muslyssnare.

Om vi vill representera en bild med en datastruktur måste vi först bestämma hur varje enskild punkt ska representeras. Vi börjar med att definiera en klass Punkt.

class Punkt {
    private int x, y;

    public Punkt (int x0, int y0) {
	x = x0;
	y = y0;
    }

    public void draw(Graphics g) {
	g.setColor(Color.BLACK);
	g.fillOval(x, y, 10, 10);
    }
}

En punkt har två instansvariabler, x och y (dess koordinater). Den har en metod för att rita upp den.

Program program/swing-2/Punkter_1.java.

Kom ihåg punkterna

Program program/swing-2/Punkter_2.java.

I varje program där man ritar och manipulerar bilder måste man lagra informationen i bilden på nåt sätt.

Jag lägger en instansvariabel punktMängd= till klassen Ritpanel. I den här applikationen utgör ju punkterna hela bilden. Ritpanel definierar en metod addPunkt().

När användaren klickar med musen anropas metoden addPunkt(). Sen anropas repaint(). Jag valde att anropa repaint() i muslyssnaren. Det kanske vore bättre att lägga alla anrop till repaint() i klassen Ritpanel, men ett mål med exmplet var ju att visa hur man kan styra uppritning med repaint() och då vore det väl fel att gömma undan anropet.

Sen definierar vi också en metod paintComponent(). Den anropas av systemet, till exempel när fönstet varit skymt och måste ritas om.

class Ritpanel extends JPanel implements MouseListener {

    private Collection <Punkt> punktMängd =
	new ArrayList<Punkt>();

    public Ritpanel () {
        setBackground( Color.YELLOW );
        addMouseListener(this);
    }

    public void addPunkt (Punkt p) {
	punktMängd.add(p);

    }

    public void paintComponent (Graphics g) {
	super.paintComponent(g);

	for (Punkt p : punktMängd) {
	    p.draw(g);
	}
    }

    public void mouseClicked (MouseEvent e) {
	Punkt p = new Punkt(e.getX(), e.getY());
	addPunkt(p);
        repaint();
    }

    public void mouseEntered (MouseEvent e) { }
    public void mouseExited (MouseEvent e) { }

    public void mousePressed (MouseEvent e) { }
    public void mouseReleased (MouseEvent e) { }
}

Ta bort punkter

Program: program/swing-2/Punkter_3.java

Låt oss lägga till kod för att redigera bilden. Vi börjar med det allra enklaste: gör så att man kan ta bort punkter.

Här är klassen Punkt.

class Punkt {
    private int x, y;
    static final int radie = 5;

    public Punkt (int x0, int y0) {
	x = x0;
	y = y0;
    }

    public void draw(Graphics g) {
	g.setColor(Color.BLACK);
        g.fillOval(x-radie, y-radie, radie*2, radie*2);
    }

    public static int sqr(int x) {
        return x*x;
    }

    public boolean rymmer (int x1, int y1) {
        return sqr(x-x1)+sqr(y-y1) <= sqr(radie);
    }
}

Vi har lagt till en konstant med namnet radie. Tidigare var diametern inkodad i löpande programtext, men nu definieras den så:

    static final int radie = 5;
Numeriska konstanter som dyker upp utan förklaring i programkod kallas ibland magic numbers. Det brukar vara en bra idé att undvika sådana. Ge hellre konstanten ett namn, som jag gjort här.

Notera att draw() har ändrats. Den ritar numera en cirkel med centrum vid position (x,y).

Metoden rymmer() använder Pythagoras sats för att avgöra om koordinaten (x1, y1) hamnade inom punkten.

public static final int sqr(int x) {
    return x*x;
}

public boolean rymmer (int x1, int y1) {
    return sqr(x-x1)+sqr(y-y1) <= sqr(radie);
}

Vi har två nya metoder i klassen RitPanel. Först hittaPunkt.

public Punkt hittaPunkt(int x, int y) {
    for (Punkt p : punktMängd) {
        if (p.rymmer(x,y)) {
            return p;
        }
    }
    return null;
}

Metoden hittaPunkt tar en koordinat och letar efter en punkt som rymmer den koordinaten. Om en sådan hittas returneras den. I annat fall returneras null.

Tidigare var ritpanelen sin egen muslyssnare, men eftersom koden för att hantera mushändelser har blivit mer komplex flyttar jag ut den till en egen klass:

class MusLyssnare extends MouseAdapter
    implements MouseMotionListener {

    private Ritpanel ritpanel;

    private Punkt utvald;

    public MusLyssnare (Ritpanel p) {
        ritpanel = p;
    }

    public void mouseClicked (MouseEvent e) {
        int x = e.getX();
        int y = e.getY();

        Punkt p = ritpanel.hittaPunkt(x,y);

        if (p == null) {
            p = new Punkt(e.getX(), e.getY());
            ritpanel.addPunkt(p);
        }
        else {
            ritpanel.taBortPunkt(p);
        }
        ritpanel.repaint();
    }

    public void mousePressed (MouseEvent e) {
    }

    public void mouseDragged (MouseEvent e) {
    }
}

Det som är nytt är att om man klickar på en punkt tas den bort.

Om ni provkör (och har testat de föregående versionerna) kommer ni att uppskatta att punkterna numera hamnar exakt där man klickar. Prova att skapa två punkter som överlappar och se vad som händer när ni tar bort den ena.

Flytta punkter

I version 4 (program/swing-2/Punkter_4.java) har jag lagt till en metod i klassen Punkt för att flytta en punkt till en ny position:

public void flytta(int x1, int y1) {
    x = x1;
    y = y1;
}

I klassen ritpanel har vi en relaterad metod:

public void flyttaPunkt (Punkt p, int x, int y) {
    p.flytta(x, y);
}

De stora förändringarna i denna version kommer i klassen MusLyssnare. Vi introducerar en instansvariabel utvald. Om användaren håller på att flytta en punkt är utvald bunden till denna punkt.

Om användaren pressar ner musknappen över en punkt blir den utvald:

public void mousePressed (MouseEvent e) {

    int x = e.getX();
    int y = e.getY();

    utvald = ritpanel.hittaPunkt(x,y);

}

När användaren drar musen med knappen nedtryckt flyttas den utvalda punkten:

public void mouseDragged (MouseEvent e) {

    int x = e.getX();
    int y = e.getY();

    if (utvald != null) {
        ritpanel.flyttaPunkt(utvald,x,y);

    }
    ritpanel.repaint();
}

Jag hoppas att ni tar er tid att testköra de olika versionerna av det här enkla ritprogrammet. Prova också att flytta några punkter.