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 standardkomponenter 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 få et vindue op på skærmen ved at skrive en klasse, der arver fra klassen Frame og definere metoden paint(), som systemet vil kalde når vinduet skal tegnes op på skærmen. paint() får et Graphics-objekt (beskrevet i afsnit 9.1) overført, som vi kan tegne med.
I eksemplet nedenfor tegner vi en linje, en fyldt oval og noget tekst med grøn skrift.
import java.awt.*; public class GrafiskVindue extends Frame { public void paint(Graphics g) { // Herunder referer g til et Graphics-objekt man kan tegne med g.drawLine(0,0,50,50); g.fillOval(5,20,300,30); g.setColor(Color.GREEN); g.drawString("Hej grafiske verden!",100,40); } }
For at vise det grafiske vindue på skærmen skal vi definere en main()-metode, der opretter et GrafiskVindue-objekt, sætter vinduets størrelse og gør det synligt på skærmen:
public class VisGrafiskVindue { public static void main(String[] arg) { GrafiskVindue vindue = new GrafiskVindue(); // opret vinduet vindue.setSize(350,60); // sæt vinduets størrelse vindue.setTitle("GrafiskVindue"); // sæt vinduets titel vindue.setVisible(true); // åbn vinduet } }
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.
Netop i en Frame regnes koordinaterne inklusiv vinduets dekoration, så (0,0) er dækket af titellinjen på vinduet. Ønskes det, kan nulpunktet forskydes i paint() med kommandoerne
Insets in = getInsets(); // kun Frame: forskyd nulpunktet g.translate(in.left,in.top); // (0,0) til under titellinjen
Hvis man kører programmet, opdager man, at vinduet ikke reagerer, når man forsøger at lukke det. Man kan i stedet stoppe programmet (f.eks. vælge 'Stop' i udviklingsværktøjet eller trykke Ctrl-C fra kommandolinjen). Man kan også bruge klassen LukProgram, defineret i afsnit 13.4.1, og tilføje følgende linje i main()-metoden:
vindue.addWindowListener( new LukProgram() );
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 øverste venstre
hjørne 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 vinduet (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 2D-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.*; public class Grafikdemo extends Frame { public void paint(Graphics g) { Insets in = getInsets(); // kun Frame: forskyd nulpunktet g.translate(in.left,in.top); // (0,0) til under titellinjen g.drawRoundRect(10,10,80,80,25,25); // tegn rektangel med runde hjører 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 } }
Herunder er en klasse med en main()-metode, der åbner vinduet.
public class VisGrafikdemo { public static void main(String[] arg) { Grafikdemo vindue = new Grafikdemo(); vindue.setSize(500,200); vindue.setTitle("Grafikdemo"); // vindue.addWindowListener( new LukProgram() ); // defineret i kapitel 13 vindue.setVisible(true); } }
Ud over at tegne grafik har man også ofte brug for at påvirke selve vinduet, f.eks sætte vinduets størrelse eller titel eller bede systemet om at gentegne vinduet.
Frame-klassens metoder - (generel) betyder, at metoden også findes i andre grafiske klasser.
repaint() (generel)
forårsager,
at systemet kalder paint() lidt senere, hvorved skærmen bliver
gentegnet.
void setSize(int bredde, int
højde)
sætter vinduets bredde og højde.
void setLocation(int x, int
y)
sætter vinduets position på skærmen.
void setVisible(boolean
synlig)
bestemmer, om vinduet er synligt.
Kald
setVisible(true) for at åbne vinduet og setVisible(false) for
at lukke det.
void setTitle(String
titel)
sætter vinduets titel.
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 vinduets forgrundfarve, som
er den farve, Graphics-objektet normalt tegner med.
void setBackground(Color
baggrundsfarve) (generel)
sætter vinduets baggrundfarve, som
er den farve, baggrunden bliver udfyldt med.
void
setFont(Font
nySkrifttype) (generel)
sætter vinduets skrifttype (som er
den skrifttype, Graphics-objektet normalt tegner med).
Dimension
getSize() (generel)
returnerer
vinduets størrelse som et Dimension-objekt med bredde og
højde.
Tilsvarende findes getLocation, getTitle, getCursor, getForeground, getBackground og getFont.
Insets getInsets()
for
netop en Frame regnes koordinaterne inklusiv vinduets dekoration, så
(0,0) er normalt dækket af titellinjen på vinduet. Denne
metode giver startkoordinaterne for indholdet af vinduet.
De af ovenstående metoder, der også findes i andre grafiske objekter, er markeret med (generel)
Bemærk, 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 vinduet:
public class Grafikdemo extends Frame { public void paint(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 VisGrafikdemo { public static void main(String[] arg) { Grafikdemo vindue = new Grafikdemo(); Dimension d = vindue.getSize(); System.out.println("Vinduet har størrelsen: "+d);
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. Her kalder vi også setSize() og setVisible(true) for at åbne vinduet.
Vi tegner punkterne i paint(), der er gjort så lille og hurtig som muligt (bl.a. ved ikke at oprette objekter i dene metode) - den kaldes jo hver gang vinduet bliver gentegnet.
Farverne huskes i en liste, der er defineret som objektvariabel (sådan at den er kendt, så længe Kurvetegning-objektet findes). På den måde får vi data fra konstruktør til paint().
import java.util.*; import java.awt.*; public class Kurvetegning extends Frame { ArrayList<Color> farver;//objektvariabel kendt i både konstruktøren og paint() 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); } this.setSize(400,300); // eller fra main(): vindue.setSize(400,300) this.setVisible(true); // eller fra main(): vindue.setVisible(true) } public void paint(Graphics g) // tegn punkterne { g.drawString("Kurvetegning", forskydning%400, forskydning%300); for (int x=0; x<farver.size(); x++) { int y = 150 - (int) (120*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 VisKurvetegning { public static void main(String[] arg) { Kurvetegning vindue = new Kurvetegning(); } }
Ændr GrafiskVindue til at tegne nogle andre figurer.
Skiv noget ud når paint() bliver kaldt (med System.out.println()) og se hvornår paint() 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 vinduets størrelse
(vink: Brug getSize()).
Ændr GrafiskVindue, så den tegner
et skakbræt med sorte og hvide felter.
En springer 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.
Ændr SpilMatador sådan, at den også
opretter et grafisk vindue, der viser brættet.
Lad
programmet spille en tur hvert sekund, eller lad brugeren styre (se
f.eks afsnit 2.12.3).
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 og SpilMatador.java ændret til at bruge Gade2 og Rederi2) og prøv det.
Genbrug GrafiskVindue ovenfor. Lad initialisering ske i konstruktøren. Variabler, der skal deles mellem flere metoder, skal være objektvariabler (de lokale eksisterer jo kun i den metode, de er defineret i).
Føj en metode til Felt, der tegner feltet. Hvert felt skal også have en position (den er en del af initialiseringen, så sæt den fra konstruktøren).
Løb igennem alle felter og tegn dem i paint().
Udbyg derefter spillet efter egen smag. Tegn f.eks. også spillerne og deres biler.
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 vindue.
Flyt
initialiseringen fra SpilMatador.java til konstruktøren af
GrafiskVindue. Husk at importere java.util.* øverst for at få
adgang til ArrayList-klassen. Variablerne felter, sp1, sp2 skal nu
være objektvariabler (før var de lokale variabler), for
at de kan ses i resten af objektet:
import java.awt.*; import java.util.*; public class GrafiskVindue extends Frame { // objektvariabler: Spiller sp1=new Spiller("Søren",50000,Color.GREEN); // opret spiller 1 Spiller sp2=new Spiller("Gitte",50000,Color.YELLOW); // opret spiller 2 ArrayList felter=new ArrayList(); public GrafiskVindue() { felter.add(new Start(5000)); felter.add(new Gade2("Gade 1",10000, 400,1000)); felter.add(new Gade2("Gade 2",10000, 400,1000)); //... osv.
Husk, at vinduet først tegnes, når initialiseringen er færdig, så hvis du f.eks. kører 20 runder i initialiseringen, tager det lang tid, førend systemet når til at kalde paint()!
Hvert felt skal have en position. Føj en position (af typen Point) til Felt-klassen:
import java.awt.*; public class Felt { String navn; Point position = new Point();
og definér metoden tegn(Graphics g) på Felt, der tegner feltets navn på positionen:
public void tegn(Graphics g) { g.setColor(Color.BLACK); g.drawString(navn,position.x,position.y); }
Husk at importere java.awt.* øverst for at få adgang til Point- og Graphics-klassen.
Løb alle felterne igennem i konstruktøren og sæt koordinaterne på felterne:
felter.add(new Gade2("Gade 8",20000,1100,2000)); felter.add(new Gade2("Gade 9",30000,1500,2200)); for (int i=0; i<felter.size(); i++) { double v = Math.PI*2*i/felter.size(); // vinkel i radianer Felt f = felter.get(i); f.position = new Point( 100 + (int) (100*Math.cos(v)), 110 + (int) (100*Math.sin(v)) ); }
Definér vinduets paint()-metode til at kalde felternes tegn() for at tegne brættet:
public void paint(Graphics g) { for (int i=0; i<felter.size(); i++) { Felt f = felter.get(i); f.tegn(g); }
En grund skal også have tegnet ejeren nedenunder, så den skal have en anderledes tegn(). Definér tegn() i Grund2. En gade skal også vise antallet af huse. Definér også tegn() i Gade2.