Standardiserat koordinatsystem

Genom att använda de transformationer som finns inbyggda i Graphics2D kan man fixa allt som har med skalning att göra en gång för alla.

Ibland behöver man använda Javas inbyggda ritfunktioner för att rita något med kod i en komponent, genom att använda de metoder som finns i Graphics-objektet som skickas in till metoden paintComponent, som finns i alla Swing-komponenter. Inte sällan vill man då att det man ritar säkert ska få plats i komponenten men samtidigt fylla ut så mycket av ytan som möjligt, alltså anpassa sig efter vilken storlek komponenten för tillfället råkar ha. Detta kan man göra genom att läsa av getWidth() och getHeight() och sedan skala alla koordinater man anger med hjälp av dessa, men det finns ett smidigare sätt där man kan göra det en gång för alla i paintComponent och sedan ange koordinater i ett fast koordinatsystem.

Det första man behöver veta är att det Graphics-objekt som skickas in till paintComponent egentligen tillhör en subklass till Graphics som heter Graphics2D. Förutom att Graphics2D innehåller fler ritfunktioner än vad som finns i den vanliga Graphics innehåller den också en massa annat matnyttigt, som t ex möjligheten att utföra koordinattransformationer, som vi ska utnyttja nu.

Låt oss skapa en klass som ärver från JPanel (det går bra med vilken komponentklass som helst, egentligen) där vi överskuggar paintComponent till att rita lite.

public class PaintPanel extends JPanel {
    
    private static final long serialVersionUID = 1L;
    
    @Override
    protected void paintComponent(Graphics g) {

Det första vi gör är att anropa metoden som vi överskuggar ifrån superklassen. Det är en bra princip att göra så om man inte har någon bra anledning att låta bli. Om inte annat ser den till att hela panelen fylls med sin bakgrundsfärg.

        super.paintComponent(g);

Nästa steg är att kasta Graphics-objektet till ett Graphics2D-objekt. Detta kan vi göra eftersom vi vet att Graphics-objektet alltid egentligen är ett Graphics2D-objekt, men vi måste skapa en ny referens för att faktiskt få utnyttja de extra metoderna. Jag brukar kalla denna referens för g2.

        Graphics2D g2 = (Graphics2D) g;

En av de saker vi kan göra är nu att slå på antialiasing, vilket ger mjukare linjer.

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

Nu kommer vi till själva transformationerna. Jag vill skala allt som ritas i panelen så att jag kan rita i ett koordinatsystem där både x- och y-koordinaterna går mellan -1 och 1 utan att behöva bry mig om komponentens faktiska bredd och höjd. Jag vill också bevara proportionerna på det jag ritar, så att t ex cirklar fortfarande är runda. Skulle det vara så att bredden och höjden inte är lika (vilket de troligen inte är i de flesta fall) vill jag fylla ut med extraplats antingen på sidorna eller uppe och nere. Det blir alltså den mindre av bredden och höjden som begränsar storleken på det jag ritar. Origo (0,0) ska hamna i mitten.

        double xScale = getWidth() / 2.0;
        double yScale = getHeight() / 2.0;
        double scale = Math.min(xScale, yScale);
        AffineTransform resetTransform = g2.getTransform();
        g2.translate(getWidth() / 2, getHeight() / 2);
        g2.scale(scale, scale);

Anropen till metoderna translate och scale säger åt Graphics2D-objektet att det ska räkna om alla koordinater som man anger innan den använder dem till att rita med. Transformationerna appliceras i omvänd ordning mot vad man anger, så här har vi angett att alla koordinater som anges först ska multipliceras med scale och sedan adderas med getWidth() / 2 respektive getHeight() / 2, för x- respektive y-koordinater.

Nu kan jag ange koordinater mellan -1 och 1 och vara säker på att det skalas på ett lämpligt sätt för att fylla ut komponenten.

Alla transformationer som vi skickar till Graphics2D-objektet lagras i något som kallas för en AffineTransform. Innan vi börjar transformera tar vi ut den aktuella transformen, så att vi kan återställa den senare, när vi har ritat klart. Eftersom samma Graphics-objekt ibland kan användas till flera komponenter (t ex för en förälder och dess barn) kan det annars bli väldigt konstiga effekter.

En sak som är lätt att glömma är att även linjetjockleken kommer att skalas. Eftersom standardvärdet för linjetjockleken är 1 innebär det att alla linjer vi drar i vårt koordinatsystem kommer att skalas om och bli breda som halva ritytan! Det är ju inte så bra, så vi sätter en linjetjocklek som passar lite bättre till vårt koordinatsystem.

        g2.setStroke(new BasicStroke(0.01f));

Låt oss nu i brist på bättre fantasi rita ett X som fyller ut hela ytan. Här lägger du naturligtvis den ritkod som du behöver till den aktuella tillämpningen.

        g2.draw(new Line2D.Double(-1, -1, 1, 1));
        g2.draw(new Line2D.Double(-1, 1, 1, -1));

Och så avslutar vi med att återställa transformationen.

        g2.setTransform(resetTransform);

Det är hela innehållet i paintComponent-metoden. Här finns hela klassen för nedladdning, inklusive en liten main-metod så att vi kan testa den.

Och här är några skärmdumpar på hur det ser ut, med olika storlekar på fönstret, och därmed på komponenten.

Naturligtvis kan man jobba i helt andra standardiserade koordinatsystem än -1 till 1. Antag till exempel att vi vill ha ett koordinatsystem där koordinaterna löpte mellan 0 och 1000 i både x- och y-led, samt att y-koordinaten går nerifrån och upp (som den vanligen gör i matematiken) istället för uppifrån och ner (som den oftast gör i datorns värld). Det kan vi göra med följande transformationer istället för de som vi använde ovan.

        double xScale = getWidth() / 1000.0;
        double yScale = getHeight() / 1000.0;
        double scale = Math.min(xScale, yScale);
        AffineTransform resetTransform = g2.getTransform();
        g2.translate(getWidth() / 2, getHeight() / 2);
        g2.scale(scale, -scale);
        g2.translate(-500, -500);

Notera minustecknet på det andra argumentet till scale. Det är det som vänder y-axeln.

Standardiserat koordinatsystem

Om bloggaren

Marcus Björkander

Marcus Björkander

Schlagernörd och småbarnspappa med tvångstankar som jobbar som utvecklare på Westbahr i Göteborg. Favoritspråket är Java, som han tidigare har undervisat i vid Chalmers i många år.

 

Nyckelord/tag moln