Grafikk

Relevant litteratur i offisiell tutorial: Getting started with graphics


I INF101 skal vi bruke Swing som rammeverk for å lage grafiske brukergrensesnitt. I forhold til alternative rammeverk man kunne valgt er Swing lett å komme i gang med, og har god støtte for ulike plattformer uten å måtte gjøre spesielle tilpasninger.

Rammeverket

For å bruke Swing som rammeverk for å lage grafikk, trenger vi to elementer: en klasse som utvider JPanel og en main-metode som starter programmet. Det ville virket fint å ha main -metoden i samme klasse som utvidet JPanel, men det er bedre stil å ha dem adskilt.

import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;

public class MyFirstCanvas extends JPanel {

  public MyFirstCanvas() {
    this.setPreferredSize(new Dimension(220, 100));
  }

  @Override
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;

    // Tegner først et fyllt rosa kvadrat
    Rectangle2D shape1 = new Rectangle2D.Double(100, 20, 50, 50);
    g2.setColor(Color.PINK);
    g2.fill(shape1);

    // Tegner deretter omrisset av et svart rektangel.
    // Ny tegning kommer «oppå» det som er tegnet fra før.
    Rectangle2D shape2 = new Rectangle2D.Double(110, 30, 50, 30);
    g2.setColor(Color.BLACK);
    g2.draw(shape2);
  }
}
import javax.swing.JFrame;

public class Main {
  public static void main(String[] args) {
    MyFirstCanvas canvas = new MyFirstCanvas();
    JFrame frame = new JFrame();
    frame.setTitle("INF101");
    frame.setContentPane(canvas);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }
}
Illustrasjon av koden over

  • MyFirstCanvas canvas = new MyFirstCanvas();
    Her oppretter vi et objekt av typen MyFirstCanvas. Dette objektet representerer lerretet som tegnes inne i vinduet vårt.
  • JFrame frame = new JFrame();
    Vi oppretter et objekt i klassen JFrame. Dette objektet representerer «rammen» rundt vinduet, som inneholder tittelen og knappene for å lukke vinduet.
  • frame.setTitle("INF101");
    Vi setter tittelen som vises på rammen.
  • frame.setContentPane(canvas);
    Vi setter lerretet vårt inn i rammen, slik at det fra nå av er en del av rammen.
  • frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    Vi spesifiserer at når noen lukker vinduet, skal programmet avsluttes. Hvis vi ikke hadde gjort dette, ville programmet fortsatt kjøre i bakgrunnen, selv om vinduet var lukket.
  • frame.pack();
    Her sier vi at rammen skal justere størrelsen sin slik at den passer til lerretet sin foretrukne størrelse.
  • frame.setVisible(true);
    Vi gjør rammen synlig på skjermen («henger rammet opp på veggen»).

  • MyFirstCanvas extends JPanel
    Her integrerer vi klassen vår inn i Swing-rammeverket. JPanel er en klasse som er definert i Swing, og som representerer et lerret som kan tegnes på. Når vi utvider JPanel, arver vi alle metodene som er definert der, slik som setPreferredSize(Dimension) og paintComponent(Graphics).
  • this.setPreferredSize(new Dimension(220, 100));
    Vi spesifiserer at lerretet «foretrekker» å ha en størrelse på 300 piksler i bredden og 150 piksler i høyden.

  • @Override public void paintComponent(Graphics g)
    Vi overskriver metoden paintComponent(Graphics) som er definert i klassen JPanel. Denne metoden blir kalt når lerretet skal tegnes, og vi overskriver den slik at vi kan tegne vår egen tegning, og ikke bare et blankt vindu.
  • super.paintComponent(g);
    Vi tegner bakgrunnen til lerretet. Teknisk sett kaller vi her på metoden paintComponent(Graphics) slik den er definert i klassen JPanel, som er vår super-klasse (siden vår klasse utvider JPanel). Når man overskriver en metode som allerede er implementert i klassen vi arver fra, slik vi gjør her, er det vanlig at man ønsker å legge til funksjonalitet i stedet for å endre den fullstendig. Dette gjøres her.
  • Graphics2D g2 = (Graphics2D) g;
    Objektet g vi kommer til få som argument i denne metoden er egentlig av typen Graphics2D, som er en undertype av Graphics. På denne linjen caster vi g til typen Graphics2D, noe som er mulig fordi objektet g allerede har denne typen. Hensikten med å gjøre dette er at vi får tilgang til metoder som er definert i klassen Graphics2D, som gjør oss i stand til å tegne på flere måter, blant annet fill og draw -metodene vi bruker under.

  • Rectangle2D shape1 = new Rectangle2D.Double(100, 20, 50, 50);
    Vi oppretter et rektangel-objekt med hjørne øverst til venstre i punktet (100, 20), og med bredde 50 og høyde 50. Merk at koordinatsystemet er «opp ned», slik at øverst til venstre på lerretet er punktet (0, 0) og punktet nederst til høyre er (220, 100).
  • g2.setColor(Color.RED);
    Vi velger fargen vi skal bruke til å tegne med.
  • g2.fill(shape1);
    Vi tegner figuren med en fylt farge.

  • Rectangle2D shape2 = new Rectangle2D.Double(110, 30, 50, 30);
    Vi oppretter et rektangel-objekt med hjørne øverst til venstre i punktet (110, 30), og med bredde 50 og høyde 30. Merk at koordinatsystemet er «opp ned», slik at dette hjørnet til er til høyre og ned i forhold til forrige rektangel.
  • g2.setColor(Color.BLACK);
    Vi velger fargen vi skal bruke til å tegne med.
  • g2.fill(shape2);
    Vi tegner omrisset av figuren.

Når vi beskriver eksempler for å tegne figurer videre i disse kursnotatene, er viser vi kun frem koden i paintComponent som kommer etter at vi caster til Graphics2D med mindre noe annet er spesifisert.

Koordinatesystemet

Ulikt det vi er vant til fra matematikken på skolen, vokser y-aksen nedover istedet for oppover. Dermed er \((0, 0)\) punktet til venstre øverst på lerretet, mens punktet \((\text{width}, \text{height})\) er punktet til høyre nederst. For et lerret med bredde 220 og høyde på 100, får hjørnene koordinatene under:

Koordinater for hjørnene til et lerret på 220x100

Grunnleggende figurer

I tabellen under bruker vi varianter av denne koden for å tegne figurer:

Shape shape = new /* lim inn kodelinje fra tabellen under her */;
g2.setColor(Color.PINK);
g2.fill(shape);

g2.setColor(Color.BLACK);
g2.draw(shape);

Vi tegner altså den samme figuren to ganger, først med fyllfargen rosa, deretter med omriss i svart.

De fleste klassene i dette avsnittet må importeres fra java.awt.geom, mens Shape må importeres fra java.awt.

Kode Figur
Line2D.Double(10, 20, 180, 60)
Parametre:
  • x1
  • y1
  • x2
  • y2
PS: fill tegner ingenting, må tegnes med draw.
Illustrasjon av linje
Rectangle2D.Double(10, 20, 180, 60)
Parametre:
  • x1 (venstre side)
  • y1 (øverst)
  • bredde
  • høyde
Illustrasjon av Rectangle2D
Ellipse2D.Double(10, 20, 180, 60)
Parametre:
  • x1 (venstre side)
  • y1 (øverst)
  • bredde
  • høyde
Illustrasjon av Ellipse2D
Arc2D.Double(
new Rectangle2D.Double(10, 30, 180, 60),
15, 270, Arc2D.PIE
)
Parametre:
  • Et rektangel -objekt som omslutter figuren
  • startvinkel
  • buevinkel
  • buetype (PIE, CHORD eller OPEN)
Illustrasjon av Arc2D
RoundRectangle2D.Double(10, 30, 180, 60, 30, 20)
Parametre:
  • x1 (venstre side)
  • y1 (øverst)
  • bredde
  • høyde
  • buebredde
  • buehøyde
Illustrasjon av RoundRectangle2D
Path2D shape = new Path2D.Double();
shape.moveTo(10, 30);
shape.lineTo(180, 60);
shape.lineTo(60, 70);
shape.closePath();
Merk:
  • shape må ha typen Path2D slik at vi kan legge til punkter.
  • Første punkt angis med moveTo, påfølgende med lineTo.
Illustrasjon av Path2D

Hjelpemetoder

Generelt i programmering er det alltid lurt å bruke hjelpemetoder. En hjelpemetode er en metode man kaller på et annet sted for å gjøre en avgrenset del av oppgaven til koden som kaller på den.

Når vi jobber med grafikk er hjelpemetoder like viktige or nyttige som i annen programmering. Noen eksempler:

@Override
public void paintComponent(Graphics g) {
  super.paintComponent(g);
  Graphics2D g2 = (Graphics2D) g;

  // Fire sirkler inni hverandre
  fillCenteredCircle(g2, 50, 50, 30, Color.DARK_GRAY);
  fillCenteredCircle(g2, 50, 50, 20, Color.GRAY);
  fillCenteredCircle(g2, 50, 50, 10, Color.LIGHT_GRAY);
  fillCenteredCircle(g2, 50, 50, 2, Color.WHITE);

  // To strekmenn
  drawMan(g2, new Rectangle2D.Double(120, 10, 30, 80));
  drawMan(g2, new Rectangle2D.Double(170, 20, 30, 70));
}

/** Fill circle with radius r centered around (cx, cy) with given color */
private static void fillCenteredCircle(Graphics2D g2, double cx, double cy,
                                        double r, Color color) {
  double xLeft = cx - r;
  double yTop = cy - r;
  double diameter = 2*r;
  Ellipse2D circle = new Ellipse2D.Double(xLeft, yTop, diameter, diameter);
  g2.setColor(color);
  g2.fill(circle);
}

/** Draw a stick man within the bounds of the given box */
private static void drawMan(Graphics2D g2, Rectangle2D box) {
  // Selvdokumenterende kode har beskrivende variabelnavn 
  // i stedet for mystiske utregninger
  double xLeft = box.getMinX();
  double xMid = box.getCenterX();
  double xRight = box.getMaxX();
  double yTop = box.getMinY();
  double yNeck = yTop + box.getHeight() * 0.33;
  double yHands = yTop + box.getHeight() * 0.5;
  double yWaist = yTop + box.getHeight() * 0.66;
  double yFeet = yTop + box.getHeight();
  double headHeight = yNeck - yTop;
  double headWidth = box.getWidth();

  // Hodet
  g2.setColor(Color.YELLOW);
  g2.fill(new Ellipse2D.Double(xMid - headWidth/2, yTop, headWidth, headHeight));
  g2.setColor(Color.BLACK);
  g2.draw(new Ellipse2D.Double(xMid - headWidth/2, yTop, headWidth, headHeight));
  // Kroppen og armene
  g2.draw(new Line2D.Double(xMid, yNeck, xMid, yWaist));
  g2.draw(new Line2D.Double(xMid, yNeck, xLeft, yHands));
  g2.draw(new Line2D.Double(xMid, yNeck, xRight, yHands));
  // Beina
  g2.draw(new Line2D.Double(xMid, yWaist, xLeft, yFeet));
  g2.draw(new Line2D.Double(xMid, yWaist, xRight, yFeet));
}
Illustrasjon av hjelpemetoder

Farger

Vi bestemmer fargen vi skal bruke for å tegne figurer ved å kalle setColor-metoden på Graphics2D-objektet. Vi kan bruke fargenavn eller RGB-verdier for å lage fargeobjekter. Noen eksempler:

Shape rect1 = new Rectangle2D.Double(10, 10, 60, 35);
g2.setColor(Color.BLUE); // Noen få farger er innebygd med navn
g2.fill(rect1);

Shape rect2 = new Rectangle2D.Double(80, 10, 60, 35);
g2.setColor(Color.BLUE.darker()); // .darker() og .brighter() kan endre fargen
g2.fill(rect2);

Shape rect3 = new Rectangle2D.Double(150, 10, 60, 35);
g2.setColor(new Color(0x8800ff)); // RGB som hexadesimaltall
g2.fill(rect3);

Shape rect4 = new Rectangle2D.Double(150, 60, 60, 35);
g2.setColor(Color.decode("#8800ff")); // RGB som streng med hexadesimaltall
g2.fill(rect4);

Shape ball1 = new Ellipse2D.Double(10, 50, 40, 40);
g2.setColor(new Color(0, 180, 255)); // RGB (0-255 for hver av rød, grønn og blå)
g2.fill(ball1);

// Gjennomsiktig ball
Shape ball2 = new Ellipse2D.Double(30, 20, 100, 100);
g2.setColor(new Color(0, 128, 0, 80)); // RGBA (0-255) (alpha = gjennomsiktighet)
g2.fill(ball2);
Illustrasjon av farger

Piksel

I en LED-skjerm (som er en vanlig dataskjerm) tegnes bildet på skjermen ved at hver enkelt piksel (liten prikk på skjermen) får en bestemt farge. Inne i selve skjermen sitter det tre lamper inne i hver piksel: en rød lampe, en grønn lampe og en blå lampe. Når alle tre lampene lyser med maksimal intensitet, ser vi hvitt lys komme ut av pikselen. Dersom ingen av lampene lyser, er pikselen svart. Alle fargene skjermen kan produsere, blir laget av en kombinasjon av lysintensiteter i de tre pikslene.

Hvis man zoomer inn svært tett på dataskjermen, kan man skimte at en hvit piksel ikke faktisk er helt hvit, men består egentlig av en rød, grønn og blå lampe ved siden av hverandre som lyser. Her er et bilde jeg har tatt av musenpekeren min på skjermen:

Bilde av musepekeren

Om vi zoomer litt inn på bildet, kan vi skimte at hver piksel består av tre lamper: en rød, en grønn og en blå.

Bilde av musepekeren

På grunn av regler beskrevet i fysikken, vil en blanding av røde, grønne og blå lyssignaler se ut som det er hvitt.

Når man kjøper en LED-skjerm på butikken, finnes det ulike fargedypder eller man får oppgitt antall farger skjermen kan vise. Denne spesifikasjonen bestemmes av i hvor mange “trinn” man kan justere intensiteten til hver av de fargede lampene i en piksel. Det har lenge vært vanlig at man bruker 256 slike trinn. En farge i dette systemet kan derfor sees på som tre tall (r, g, b), der hver av r, g og b er et tall mellom 0 og 255.

Selv om nyere og dyre skjermer teknisk sett kan ha flere trinn, bruker som regel software som ikke er rettet spesielt mot high-end bildebehandling fremdeles dette systemet som standard.

Alle farger har en RGB-verdi. I tabellen ser vi at hver farge har en gitt styrke av rød (R), grønn (G) og blå (B), som er et tall mellom 0 og 255. Denne RGB-verdien kan også skrives i heksadesimalt format (se kolonnen Hex), hvor de to første tegnene etter 0x representerer styrken på rød, de to neste representerer grønn, og de to siste representerer blå sin styrke.

Farge R G B Hex Kallenavn
 
0 0 0 0x000000 BLACK
 
255 0 0 0xff0000 RED
 
0 255 0 0x00ff00 GREEN
 
0 0 255 0x0000ff BLUE
 
255 255 0 0xffff00 YELLOW
 
0 255 255 0x00ffff CYAN
 
255 0 255 0xff00ff MAGENTA
 
255 255 255 0xffffff WHITE
 
128 128 128 0x808080 GRAY
 
255 171 173 0xffabad PINK
 
255 197 0 0xffc500 ORANGE
 
224 227 206 0xe0e3ce -
 
249 248 245 0xf9f8f5 -

For flere farger, se for eksempel listen over farger (A-F) på Wikipedia, eller prøv RGB-kalkulatoren til w3schools.com.

Strenger

Vi kan skrive tekst i et vindu ved å bruke drawString-metoden i Graphics2D-objektet. Denne metoden tar inn en streng og to tall, som er koordinatene til venstre side av «stamlinjen» man kan forestille seg går under teksten.

g2.setColor(Color.DARK_GRAY);
g2.setFont(new Font("Arial", Font.BOLD, 20));
g2.drawString("Her kan jeg skrive", 10, 30);

// Tegn prikk samme sted som koordinatet gitt til drawString
Shape marker = new Ellipse2D.Double(8, 28, 5, 5);
g2.setColor(Color.RED);
g2.fill(marker);
Illustrasjon av strenger

Fonter. Merk at hvilke fonter som er tilgjengelig kommer an på hvilket operativsystem du er på og fontene som er installert der. Fontene Monospaced, Serif, SansSerif, Dialog og DialogInput vil derimot alltid være tilgjengelige, selv om de kan se litt forskjellig ut på ulike operativsystemer. Se også offisiell dokumentasjon for java.awt.Font og offisiell tutorial om fonter.

g2.setColor(Color.DARK_GRAY);
g2.setFont(new Font("Monospaced", Font.PLAIN, 12));
g2.drawString("Hurra", 50, 20);

g2.setColor(Color.RED);
g2.setFont(new Font("Serif", Font.BOLD, 24));
g2.drawString("Hurra", 50, 40);

g2.setColor(Color.ORANGE.darker());
g2.setFont(new Font("SansSerif", Font.BOLD|Font.ITALIC, 48));
g2.drawString("Hurra", 50, 80);
Forskjellige fonter

Å sentrere tekst. Vi har laget en hjelpemetode i Inf101Graphics som gjør det enkelt å sentrere tekst. Parametrene til drawCenteredString -metoden er

Merk at metoden ikke vil respektere størrelsen på rektangelet når teksten skrives; vi bare oppgir posisjonen på denne måten for enkelhets skyld, siden et vanlig use-case er å sentrere teksten på et rektangel (se eksempel under). Det er fullt mulig å la bredde og høyde være 0, da sentreres teksten rundt posisjonen (x, y).

For enkelhets skyld inneholder Inf101Graphics også en variant av metoden som tar et Rectangle2D -objekt som input i stedet for de fire double-verdiene, samt en variant som har to double-variabler x og y som parametre uten å ta med bredde og høyde.

g2.setColor(Color.RED);
g2.setFont(new Font("Arial", Font.BOLD, 20));
g2.drawRect(10, 30, 180, 60);
Inf101Graphics.drawCenteredString(g2, "Midtstilt tekst", 10, 30, 180, 60);
Midtstilt tekst

Bilder

Vi har laget et par hjelpemetoder i Inf101Graphics som gjør det enkelt å vise bilder. Vi anbefaler at bildene ligger i mappen src/main/resources i maven-prosjektet, og at du bruker loadImageFromResources-metoden for å laste inn bildet først. Deretter kan vi bruke drawImage-metoden for å tegne bildet på skjermen. Parametrene til drawImage og drawCenteredImage -metodene er

BufferedImage må importeres fra java.awt.image

// Begynn med "/" før filnavnet, og la bildet ligge i "resources" -mappen
BufferedImage image = Inf101Graphics.loadImageFromResources("/inf101.png");
g2.setColor(Color.RED); // Farge til prikkene som viser (x, y) -posisjon

double x = 10;
double y = 30;
Inf101Graphics.drawImage(g2, image, x, y, 0.15);
g2.fill(new Ellipse2D.Double(x - 3, y - 3, 6, 6));

x = 90;
Inf101Graphics.drawImage(g2, image, x, y, 0.15, Math.PI/16);
g2.fill(new Ellipse2D.Double(x - 3, y - 3, 6, 6));

x = 170;
Inf101Graphics.drawCenteredImage(g2, image, x, y, 0.15, Math.PI/6);
g2.fill(new Ellipse2D.Double(x - 3, y - 3, 6, 6));
Bilder

Tegnestil

På samme måte som vi kan endre hvilken farge det blir tegnet med, er det også mulig å endre på «stilen» til streker og figurer.

Linjetykkelse

// Vi tegner for eksempel en sti, men det kunne også vært noe annet
// vi skal tegne med 'draw', f. eks en linje, en oval, rektangel etc.
Path2D path = new Path2D.Double();
path.moveTo(20, 30);
path.lineTo(30, 70);
path.lineTo(180, 80);

// Linjetykkelse 20
g2.setColor(Color.GREEN.darker().darker());
g2.setStroke(new BasicStroke(20));
g2.draw(path);

// Til sammenligning: et fylt rektangel med høyde på 20 piksler
g2.fill(new Rectangle2D.Double(130, 10, 50, 20));

// Linjektykkelse 10
g2.setColor(Color.decode("#FFFF88"));
g2.setStroke(new BasicStroke(10));
g2.draw(path);

// Linjetykkelse 1
g2.setColor(Color.BLACK);
g2.setStroke(new BasicStroke(1));
g2.draw(path);
Linjetykkelse

Legg merke til at tykke linjer begynner «før» punktet vi oppgir som startpunkt, og at de slutter «etter» punktet vi oppgir som sluttpunkt. Dette kan imidlertid konfigureres. Les mer om dette og mer (f. eks. hvordan ha stiplede linjer) i dokumentasjonene til BasicStroke og i offisiell tutorial om Stroking and Filling Graphics Primitives.

Gradiente farger

// I stedet for å bruke setColor, kan vi bruke setPaint
// for å sette fargen til en GradientPaint
Color color1 = Color.MAGENTA.darker();
Color color2 = Color.ORANGE;
g2.setPaint(new GradientPaint(60, 40, color1, 140, 80, color2));

// Tegner linjen hvor gradienten skjer (argumentene til GradientPaint)
g2.setColor(Color.WHITE);
g2.draw(new Line2D.Double(60, 40, 140, 80));
Gradiente farger

Les mer om dette i dokumentasjonen til GradientPaint og i offisiell tutorial om Stroking and Filling Graphics Primitives.

Se også:

Adaptiv tegning

Selv om et lerretet har en foretrukket størrelse (se rammeverket), er det ikke alltid at lerretet faktisk vil ha denne størrelsen. Det kan være for eksempel dersom brukeren manuelt endrer størrelse på vinduet, eller dersom lerretet inngår i en større helhet hvor andre deler av programmet griper inn og endrer lerretets størrelse. Fordi slike tilfeller er svært vanlige, anbefaler vi alltid å tegne på lerretet med tanke på at det kan ha en størrelse som er forskjellig fra foretrukket størrelse.

For å gjøre tegningen adaptiv, bruker vi metodene this.getWidth() og this.getHeight() som gir oss henholdsvis bredde og høyde av lerretet. Så regner vi ut størrelsen og plasseringen til ting vi vil tegne basert på disse verdiene. Noen eksempler:

Plassering relativt til lerretets sentrum

// En sirkel med radius 25px som alltid er i midten av lerretet
double r = 25;
double cx = ((double) this.getWidth()) / 2;
double cy = ((double) this.getHeight()) / 2;
g2.setColor(Color.RED);
g2.fill(new Ellipse2D.Double(cx - r, cy - r, 2 * r, 2 * r));

// En strek som alltid er 10px til høyre for sirkelen
double x = cx + r + 10;
g2.setColor(Color.BLACK);
g2.draw(new Line2D.Double(x, cy - r, x, cy + r));
Plassering relativ til lerretets sentrum

Størrelse relativ til lerretets størrelse

// Et sentrert rektangel med 80% av lerretets høyde og 80% av lerretets bredde
double height = this.getHeight() * 0.8;
double width = this.getWidth() * 0.8;
double x = this.getWidth() * 0.1;
double y = this.getHeight() * 0.1;
g2.setColor(Color.LIGHT_GRAY);
g2.fill(new Rectangle2D.Double(x, y, width, height));
Størrelse relativ til lerretets størrelse

Fast avstand til kantene på lerretet

// Et rektangel som alltid har 15px avstand til kantene på lerretet
double margin = 15;
double x = margin;
double y = margin;
double width = this.getWidth() - 2 * margin;
double height = this.getHeight() - 2 * margin;
g2.setColor(Color.GREEN.darker().darker());
g2.fill(new Rectangle2D.Double(x, y, width, height));
Fast avstand til kantene på lerretet

Rute med størrelse avhengig av tekst

Dette eksempelet benytter en hjelpemetode for å tegne midtstilte strenger hentet fra Inf101Graphics.

// En rute nede til høyre med størrelse avhengig av teksten inne i ruten
String text = "Min tekst";

// Vi lar fontstørrelsen variere med høyden på lerretet
// (ikke særlig vanlig i praksis, men for å illustrere at det er mulig)
int fontSize = this.getHeight() / 6; // 6 er bare et tilfeldig tall
g2.setFont(new Font("SansSerif", Font.PLAIN, fontSize));

// Hente informasjon om størrelsen på teksten
int textWidth = g2.getFontMetrics().stringWidth(text);  // Bredde av teksten
int textHeight = g2.getFontMetrics().getHeight();       // Høyde av teksten

// Regne ut størrelsen på boksen
double textMargin = 10; // Avstand mellom teksten og ruten
double boxWidth = textWidth + 2 * textMargin;
double boxHeight = textHeight + 2 * textMargin;

// Regne ut posisjonen til boksen
double boxMargin = 20; // Avstand mellom ruten og kantene på lerretet
double boxX = this.getWidth() - boxWidth - boxMargin;
double boxY = this.getHeight() - boxHeight - boxMargin;
Rectangle2D box = new Rectangle2D.Double(boxX, boxY, boxWidth, boxHeight);

// Tegne
g2.setColor(Color.BLACK);
g2.draw(box);
Inf101Graphics.drawCenteredString(g2, text, box);
Rute med størrelse avhengig av tekst