Indhold:
Oprettelse og brug af grafiske vinduer
At tegne grafik med Graphics-objektet
Større opgave: Matador-spillet som et grafisk program
Kapitlet forudsættes i kapitel 10, Appletter, kapitel 11, Grafiske komponenter og kapitel 12, Interfaces.
Forudsætter kapitel 3, Objekter (4, Definition af klasser og 5, Nedarvning er en fordel). Den større opgave forudsætter kapitel 5, Nedarvning.
Vi kan tegne grafik på skærmen ved at skrive en klasse, der arver fra klassen JPanel og definere metoden paintComponent(). Dette metode vil systemet kalde for at få grafikken tegnet på skærmen. Systemet overfører et Graphics-objekt (beskrevet i afsnit 9.1), som vi kan bruge til at tegne med.
I eksemplet nedenfor tegner vi en linje, en fyldt oval og noget tekst med grøn skrift.
import java.awt.*;
import javax.swing.*;
public class Grafikpanel extends JPanel
{
public void paintComponent(Graphics g)
{
// Herunder referer g til et Graphics-objekt man kan tegne med
super.paintComponent(g); // tegn først baggrunden på panelet
g.drawLine(0,0,50,50);
g.fillOval(5,10,300,30);
g.setColor(Color.GREEN);
g.drawString("Hej grafiske verden!",100,30);
System.out.println("Der blev tegnet!!");
}
}
For at følge med i hvornår der bliver tegnet skriver vi Der blev tegnet!! ud når metoden bliver kaldt. For at se grafikken skal vi definere en main()-metode, der opretter et Grafikpanel-objekt og et vindue (JFrame), som viser panelet på skærmen:
import javax.swing.JFrame;
public class BenytGrafikpanel
{
public static void main(String[] arg)
{
Grafikpanel panel = new Grafikpanel(); // opret panelet
JFrame vindue = new JFrame("Grafikpanel"); // opret et vindue på skærmen
vindue.add( panel ); // vis panelet i vinduet
vindue.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // reagér på luk
vindue.setSize(350,70); // sæt vinduets størrelse
vindue.setVisible(true); // åbn vinduet
}
}
Der blev tegnet!!
Her ses, hvordan vinduet ser ud på skærmen (under Linux):
Vinduets øverste venstre hjørne er i (0,0) og koordinaterne regnes mod højre og nedad.
Graphics er beregnet til at tegne grafik (på skærm eller printer). Man skal ikke selv oprette Graphics-objekter med new, i stedet får man givet et "i hånden" af styresystemet. Herunder gengives kun nogle af metoderne – se Javadokumentationen for en komplet liste.
java.awt.Graphics – til tegning af grafik
Metoder
void
drawString(String
tekst, int x, int y)
tegner en tekst med bundlinjen startende i
(x,y).
void
drawImage(Image
billede, int x, int y, ImageObserver observatør)
tegner et
billede med øverste venstre hjørne i (x,y); observatør
bør være objektet selv (this).
void
drawLine(int x1, int
y1, int x2, int y2)
tegner en linje mellem punkterne (x1, y1) og
(x2, y2).
void
drawRect(int x, int y, int bredde, int højde)
tegner
omridset af et rektangel.
void
drawRoundRect(int x,
int y, int bredde, int højde, int buebredde, int
buehøjde)
tegner omridset af et rektangel, der er afrundet
i hjørnerne.
void
drawOval(int x, int
y, int bredde, int højde)
tegner en oval med øverste
venstre hjørne i (x,y). Er bredde==højde, tegnes en
cirkel.
void
drawArc(int x, int y,
int bredde, int højde, int startvinkel, int vinkel)
tegner
en del af en oval, men kun buen fra startvinkel
og vinkel
grader rundt (mellem 0 og 360).
void
drawPolygon(Polygon
p)
tegner en polygon (mangekant) ud fra et Polygon-objekt.
Tilsvarende findes fillRect, fillRoundRect, fillOval, fillArc og fillPolygon.
void
clearRect(int
x, int y, int bredde, int højde)
udfylder et rektangel med
baggrundsfarven.
Rectangle
getClipBounds()
giver
klipnings-omridset. Kun punkter inden for dette omrids bliver faktisk
tegnet, ting uden for omridset bliver beskåret til den del, der
er inden for omridset.
void
translate(int
x, int y)
forskyder koordinatsystemet, sådan at (x,y) bliver
(0,0)
void
setColor(Color
nyFarve)
sætter tegningsfarven til nyFarve. Alt bliver
herefter tegnet med denne farve.
Color getColor()
aflæser tegningsfarven.
void
setFont(Font
nySkrifttype)
sætter skrifttypen til nySkrifttype. Dette
påvirker tekster skrevet med drawString() herefter.
Font
getFont()
aflæser
skrifttypen.
Har man brug for flere faciliteter til tegning af grafik end ovenstående giver, kan man gå over til at bruge Java2D (se avanceret-afsnittet i slutningen af kapitlet).
Her følger et eksempel, der viser mange af mulighederne, der er med Graphics-objektet.
import java.awt.*;
import javax.swing.*;
public class Grafikdemo extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g); // tegn først baggrunden på panelet
g.drawRoundRect(10,10,80,80,25,25); // tegn rektangel med runde hjørner
g.drawArc(110,10,80,80,20,320); // tegn buestykke
g.fillArc(210,10,80,80,20,320); // tegn lagkagestykke (udfyldt)
Polygon p = new Polygon(); // lav polygon, der viser en pil:
p.addPoint(0,13); p.addPoint(45,13); // frem
p.addPoint(45,0); p.addPoint(60,15); p.addPoint(45,30); // spidsen
p.addPoint(45,17); p.addPoint(0,17); // tilbage
p.translate(300,10); // flyt polygonen
g.drawPolygon(p); // tegn polygonen
p.translate(0,50); // flyt polygonen mere
g.fillPolygon(p); // tegn polygonen udfyldt
for (int i=0; i<4; i++) // tegn forskellige skriftstørrelser
{
int størrelse = 10+i*4;
Font skrifttype = new Font("Serif", Font.ITALIC, størrelse);
g.setFont(skrifttype);
g.drawString("Skrift "+størrelse, 400, 15+25*i);
}
// Indlæs billede. Forudsætter at "bog.gif" er der, hvor programmet køres.
// Bemærk: I en applet, skriv i stedet getImage(getCodeBase(), "bog.gif")
// Bemærk: Billedformatet skal være platformsneutralt, f.eks GIF, JPG, PNG.
Image billede = Toolkit.getDefaultToolkit().getImage("bog.gif");
g.drawImage(billede, 110, 100, this); // tegn billedet
g.drawImage(billede, 0, 100, 100, 160, this); // tegn billedet skaleret
}
}
Og main()-metode:
import javax.swing.*;
public class BenytGrafikdemo
{
public static void main(String[] arg)
{
JFrame vindue = new JFrame("Grafikdemo"); // opret et vindue på skærmen
vindue.add( new Grafikdemo() ); // vis panelet i vinduet
vindue.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // reagér på luk
vindue.setSize(500,200); // sæt vinduets størrelse
vindue.setVisible(true); // åbn vinduet
}
}
Her er en klasse, der tegner en farvet kurve over sinus-funktionen.
I konstruktøren bestemmer vi farverne (opretter Color-objekter) for punkterne, der skal tegnes (vi bruger heltalsdivision med %, se afsnit 2.11.4, for at få nogle gode farveværdier).
Her opretter vi også et vindue og viser panelet på skærmen (bemærk, at fordi vi gør det inden i panel-objektet, skal vi skrive vindue.add(this) i stedet for vindue.add(panel)).
Vi tegner punkterne i paintComponent(), der er gjort så lille og hurtig som muligt (bl.a. ved ikke at oprette objekter i denne metode) – den kaldes jo hver gang skærmen skal opdateres.
Farverne huskes i listen farver, der er defineret som et felt (sådan at den er kendt, så længe Kurvetegning-objektet findes). På den måde får vi data overført fra konstruktør til paintComponent().
import java.awt.*;
import javax.swing.*;
import java.util.ArrayList;
public class Kurvetegning extends JPanel
{
ArrayList<Color> farver;//felt kendt både i konstruktør og paintComponent
int forskydning = 50; // en forskydning i farvevalget (bruges i afsnit 9.4.1)
public Kurvetegning() // forbered punkterne i konstruktøren
{
farver = new ArrayList<Color>();
for (int i=0; i<400; i++)
{
Color farve = new Color(i%256, (i*2)%256, (i*4)%256);
farver.add(farve);
}
JFrame vindue = new JFrame("Kurvetegning"); // opret et vindue på skærmen
vindue.add( this ); // vis dette panel i vinduet
vindue.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // reagér på luk
vindue.setSize(400,300); // sæt vinduets størrelse
vindue.setVisible(true); // åbn vinduet
}
public void paintComponent(Graphics g) // tegn punkterne
{
super.paintComponent(g); // tegn først baggrunden på panelet
g.drawString("Kurvetegning", forskydning%400, forskydning%300);
for (int x=0; x<farver.size(); x++)
{
int y = 140 - (int) (130*Math.sin(0.05*x));
int i = (x+forskydning)%400;
Color farve = farver.get(i);
g.setColor(farve);
g.fillRect(x, y, 5, 5);
}
}
}
Her er klassen, der opretter Kurvetegning-objektet:
public class BenytKurvetegning
{
public static void main(String[] arg)
{
Kurvetegning kt = new Kurvetegning();
}
}
Ud over at tegne grafik har man også ofte brug for at påvirke selve vinduet eller panelet, f.eks sætte vinduets størrelse eller titel eller bede systemet om at gentegne skærmen.
JFrame-klassens metoder – (generel) betyder, at metoden også findes i andre grafiske klasser.
repaint() (generel)
forårsager,
at systemet kalder paintComponent() lidt senere, hvorved
vinduet/panelet bliver gentegnet.
void setSize(int bredde, int
højde) (generel)
sætter bredden og højden.
void setLocation(int x, int
y) (kun JFrame)
sætter vinduets position på skærmen.
void setTitle(String
titel) (kun JFrame)
sætter vinduets titel.
void setVisible(boolean
synlig) (generel)
bestemmer, om vinduet/panelet/komponenten er
synlig.
Kald setVisible(true) for at åbne et vindue og
setVisible(false) for at lukke det.
void setCursor(Cursor
museudseende) (generel)
bestemmer musens udseende (muligheder er
bl.a.: Cursor.DEFAULT_CURSOR,
Cursor.HAND_CURSOR,
Cursor.CROSSHAIR_CURSOR, Cursor.MOVE_CURSOR)
void setForeground(Color
forgrundsfarve) (generel)
sætter forgrundfarven, som er den
farve, Graphics-objektet normalt tegner med.
void setBackground(Color
baggrundsfarve) (generel)
sætter baggrundfarven, som er den
farve, baggrunden bliver udfyldt med.
void
setFont(Font
nySkrifttype) (generel)
sætter skrifttypen (som er den
skrifttype, Graphics-objektet normalt tegner med).
Dimension
getSize() (generel)
returnerer
vinduets/panelets størrelse som et Dimension-objekt med bredde
og højde.
Tilsvarende findes getLocation, getTitle, getCursor, getForeground, getBackground og getFont.
Metoder, der er markeret med (generel), findes i alle grafiske objekter, dvs. også JPanel, JButton,..
Bemærk at det er systemet og ikke dig, der kalder paintComponent().
Metoden paintComponent() kaldes af systemet når vinduet skal gentegnes
Det er næsten altid en fejl at kalde paintComponent() fra sin egen kode
Ønsker man at vinduet skal gentegnes, kalder man repaint()
I afsnit 9.4.4, Passiv versus aktiv visning, vil dette blive uddybet.
Bemærk også, at der er forskel på om en metode på et objekt kaldes inde fra objektet, eller udefra. Udefra kalder man metoden med en variabel, der peger på objektet, det gør man ikke indefra (man kan dog bruge this). Dette er forklaret i kapitel 4, Definition af klasser.
Sådan kaldes en metode inde fra panelet:
public class Grafikpanel extends JPanel
{
public void paintComponent(Graphics g)
{
Dimension d = this.getSize(); // eller bare: getSize()
System.out.println("Jeg har størrelsen: "+d);
Sådan kaldes en metode udefra:
public class BenytGrafikpanel
{
public static void main(String[] arg)
{
Grafikpanel panel = new Grafikpanel();
...
Dimension d = panel.getSize();
System.out.println("Panelet har størrelsen: "+d);
}
}
Ændr Grafikpanel til at tegne nogle andre figurer.
Skriv noget ud når paintComponent() bliver kaldt (med System.out.println()) og se hvornår paintComponent() bliver kaldt (prøv f.eks. at minimere og gendanne vinduet eller dække det halvt over).
Lav et program, der viser et digitalur som
tekst (vink: Brug et Date-objekt).
Sørg for at uret
opdateres hvert sekund (vink: se 9.4.1, Simple animationer)).
Lav et program, der viser et analogt
ur.
Vink: Du kan benytte følgende formler til at beregne
viserens længde i de to retninger:
x
= r*Math.sin(2*Math.PI*s/60); y = r*Math.cos(2*Math.PI*s/60)
Lad
urets størrelse afhænge af panelets størrelse
(vink: Brug getSize()).
Ændr Grafikpanel, så den tegner
et skakbræt med sorte og hvide felter.
Et tårn og en
bonde tegnes derefter på brættet.
Ændr programmet, sådan at det er
nemt at ændre brikkernes koordinater.
Koordinaterne gemmes
i to Point-objekter:
Point
tårn = new Point(100,200);
Point bonde = new Point(200,200);
Udvid matadorspillet fra kapitel 5, Nedarvning, til at kunne vises grafisk i et vindue.
Når du skal programmere, så vær systematisk og del opgaven op i små delopgaver. Løs en delopgave ad gangen og afprøv om det fungerer, før du går videre.
Hent kildeteksten til matadorspillet (version 2: Felt.java, Gade2.java, Grund2.java, Helle.java, Rederi2.java, Start.java, Spiller.java, BenytMatadorspil.java og Matadorspil.java ændret til at bruge Gade2 og Rederi2) og prøv det.
Genbrug Grafikpanel ovenfor. Lad initialisering ske i konstruktøren. Variabler, der skal deles mellem flere metoder, skal være felter (lokale variabler eksisterer jo kun i den metode, de er defineret i). Tjek om spillet stadig kan køre.
Udbyg derefter Grafikpanel til at tegne felternes navne og evt også spillerne og bilerne.
Felterne bør tegnes specielt afhængigt af deres type, sådan at f.eks. en gade også kan vise hvor mange huse, der er på den. Definér tegn(Graphics g, int x, int y) på Felt.
Det er bedst, at du bruger hovedet og kun ser på vinkene, hvis du er gået i stå.
Prøve programmet.
Har du ikke
allerede prøvet matadorspillet, så prøv at køre
programmet og forstå hvordan det virker. Herefter er det
naturligvis meget lettere, at lave en grafisk udgave! Brug trinvis
gennemgang (trace into/step over), indtil du føler, du har
forstået programkoden. Først da er du klar til at prøve
grafisk.
Struktur i et grafisk program.
Lav en
reference i Grafikpanel til Matadorspil (evt. overført i
konstruktøren):
import java.awt.*;
import javax.swing.*;
public class MatadorGrafikpanel extends JPanel
{
Matadorspil spil;
Husk, at vinduet først tegnes, når initialiseringen er færdig, så hvis du f.eks. kører 20 runder i konstruktøren, tager det lang tid, før systemet kalder paintComponent()!
Definér panelets paintComponent()-metode til at tegne felternes navne:
public void paintComponent(Graphics g)
{
super.paintComponent(g); // tegn først baggrunden på panelet
for (int i = 0; i < felter.size(); i++) {
Felt felt = felter.get(i);
double v = Math.PI * 2 * i / felter.size(); // vinkel i radianer
int x = 100 + (int) (100 * Math.cos(v));
int y = 110 + (int) (100 * Math.sin(v));
g.setColor(Color.BLACK);
g.drawString(felt.navn, x, y); // tegn feltets navn
Nu ændrer du sådan, at felterne har deres egen tegnemetode (der tegner navnet):
public class Felt
{
String navn;
public void tegn(Graphics g, int x, int y)
{
g.drawString(f.navn, x, y);
}
... og i Grafikpanel definerer lader vi paintComponent()-metoden kalde tegn():
g.setColor(Color.BLACK);
felt.tegn(g, x, y);
Grund2 skal også tegne en ejer, så den får sin egen tegn()-metode
public void tegn(Graphics g, int x, int y)
{
super.tegn(g, x, y); // kald Felts tegn() for at tegne navnet
if (ejer != null) g.drawString(ejer.navn, x, y+15);
}
Ligeledes med Gade2, der også skal vise et antal huse:
public void tegn(Graphics g, int x, int y)
{
super.tegn(g, x, y); // kald Grund2s tegn() for at tegne navnet og ejeren
if (antalHuse > 0) g.drawString(antalHuse + " huse", x, y+30);
}
1Det er dog muligt at lave en blanding af aktiv og passiv visning. Havde tegn() kun optegnet billedet, dvs ikke også ventet og opdateret koordinater, kunne vi kalde tegn() fra paintComponent().
2Bliver grafikken vist i et vindue vil det billede blive kopieret hen til det rigtige sted i hukommelsen. I fuldskærmstilstand vil grafikkortet i stedet blive dirigeret til at vise det nye hukommelsesområde (hvis grafikkortet og styresystemet understøtter det), hvilket er meget hurtigere,